diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a3ab884d3..a8073fa48 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -272,6 +272,9 @@ set(SOURCES widgets/fileviewlist.cpp widgets/freespacebar.cpp widgets/fullscreenhypnotoad.cpp + widgets/kcategorizedsortfilterproxymodel.cpp + widgets/kcategorizedview.cpp + widgets/kcategorydrawer.cpp widgets/lineedit.cpp widgets/linetextedit.cpp widgets/multiloadingindicator.cpp @@ -475,6 +478,8 @@ set(HEADERS widgets/fileview.h widgets/fileviewlist.h widgets/freespacebar.h + widgets/kcategorizedview.h + widgets/kcategorydrawer.h widgets/lineedit.h widgets/linetextedit.h widgets/multiloadingindicator.h diff --git a/src/covers/albumcoverfetcher.h b/src/covers/albumcoverfetcher.h index 0c39ec8ce..620965325 100644 --- a/src/covers/albumcoverfetcher.h +++ b/src/covers/albumcoverfetcher.h @@ -49,8 +49,13 @@ struct CoverSearchRequest { // It contains an URL that leads to a found cover plus it's description (usually // the "artist - album" string). struct CoverSearchResult { + // used for grouping in the user interface. defaults to the name of the + // provider that this result came from. + QString category; + // description of this result (we suggest using the "artist - album" format) QString description; + // an URL of a cover image described by this CoverSearchResult QString image_url; }; diff --git a/src/covers/albumcoverfetchersearch.cpp b/src/covers/albumcoverfetchersearch.cpp index 113ec594b..704f62785 100644 --- a/src/covers/albumcoverfetchersearch.cpp +++ b/src/covers/albumcoverfetchersearch.cpp @@ -83,6 +83,14 @@ void AlbumCoverFetcherSearch::ProviderSearchFinished() { if(reply->error() == QNetworkReply::NoError) { CoverSearchResults partial_results = provider->ParseReply(reply); + + // Add categories to the results if the provider didn't specify them + for (int i=0 ; iname(); + } + } + // add results from the current provider to our pool results_.append(partial_results); } else { diff --git a/src/covers/albumcoverfetchersearch.h b/src/covers/albumcoverfetchersearch.h index f3c165b86..937dfe885 100644 --- a/src/covers/albumcoverfetchersearch.h +++ b/src/covers/albumcoverfetchersearch.h @@ -48,7 +48,7 @@ class AlbumCoverFetcherSearch : public QObject { signals: // It's the end of search (when there was no fetch-me-a-cover request). - void SearchFinished(quint64, CoverSearchResults results); + void SearchFinished(quint64, const CoverSearchResults& results); // It's the end of search and we've fetched a cover. void AlbumCoverFetched(quint64, const QImage& cover); diff --git a/src/generated_cpp/clementine/clementine0.h b/src/generated_cpp/clementine/clementine0.h index b65d9981d..e332cd7ab 100644 --- a/src/generated_cpp/clementine/clementine0.h +++ b/src/generated_cpp/clementine/clementine0.h @@ -172,6 +172,8 @@ PythonQtShell_CoverSearchResult* a = new PythonQtShell_CoverSearchResult(); *((CoverSearchResult*)a) = other; return a; } void delete_CoverSearchResult(CoverSearchResult* obj) { delete obj; } +void py_set_category(CoverSearchResult* theWrappedObject, QString category){ theWrappedObject->category = category; } +QString py_get_category(CoverSearchResult* theWrappedObject){ return theWrappedObject->category; } void py_set_description(CoverSearchResult* theWrappedObject, QString description){ theWrappedObject->description = description; } QString py_get_description(CoverSearchResult* theWrappedObject){ return theWrappedObject->description; } void py_set_image_url(CoverSearchResult* theWrappedObject, QString image_url){ theWrappedObject->image_url = image_url; } diff --git a/src/ui/albumcoversearcher.cpp b/src/ui/albumcoversearcher.cpp index 8b07f2e35..4b7c80469 100644 --- a/src/ui/albumcoversearcher.cpp +++ b/src/ui/albumcoversearcher.cpp @@ -17,15 +17,69 @@ #include "albumcoversearcher.h" #include "ui_albumcoversearcher.h" +#include "core/logging.h" #include "covers/albumcoverfetcher.h" #include "covers/albumcoverloader.h" +#include "widgets/kcategorizedsortfilterproxymodel.h" #include #include +#include +#include + + +const int AlbumCoverCategoryDrawer::kBarThickness = 2; +const int AlbumCoverCategoryDrawer::kBarMarginTop = 3; +const int AlbumCoverCategoryDrawer::kBarMarginBottom = 10; + + +AlbumCoverCategoryDrawer::AlbumCoverCategoryDrawer(KCategorizedView* view) + : KCategoryDrawerV3(view), + total_height_(view->fontMetrics().height() + + kBarMarginTop + kBarThickness + kBarMarginBottom) { + setLeftMargin(kBarMarginBottom); +} + +int AlbumCoverCategoryDrawer::categoryHeight(const QModelIndex&, + const QStyleOption&) const { + return total_height_; +} + +void AlbumCoverCategoryDrawer::drawCategory(const QModelIndex& index, int, + const QStyleOption& option, + QPainter* painter) const { + painter->save(); + + // Bold font + QFont font(view()->font()); + font.setBold(true); + QFontMetrics metrics(font); + + // Draw text + const QString category = tr("Covers from %1").arg( + index.data(KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString()); + painter->setFont(font); + painter->drawText(option.rect, category); + + // Draw a line underneath + const QPoint start(option.rect.left(), + option.rect.top() + metrics.height() + kBarMarginTop); + const QPoint end(option.rect.right(), start.y()); + + painter->setRenderHint(QPainter::Antialiasing, true); + painter->setPen(QPen(option.palette.color(QPalette::Disabled, QPalette::Text), + kBarThickness, Qt::SolidLine, Qt::RoundCap)); + painter->setOpacity(0.5); + painter->drawLine(start, end); + + painter->restore(); +} + AlbumCoverSearcher::AlbumCoverSearcher(const QIcon& no_cover_icon, QWidget* parent) : QDialog(parent), ui_(new Ui_AlbumCoverSearcher), + model_(new QStandardItemModel(this)), no_cover_icon_(no_cover_icon), loader_(new BackgroundThreadImplementation(this)), fetcher_(NULL), @@ -34,6 +88,12 @@ AlbumCoverSearcher::AlbumCoverSearcher(const QIcon& no_cover_icon, QWidget* pare ui_->setupUi(this); ui_->busy->hide(); + KCategorizedSortFilterProxyModel* proxy = new KCategorizedSortFilterProxyModel(this); + proxy->setCategorizedModel(true); + proxy->setSourceModel(model_); + ui_->covers->setModel(proxy); + ui_->covers->setCategoryDrawer(new AlbumCoverCategoryDrawer(ui_->covers)); + loader_->Start(true); loader_->Worker()->SetDefaultOutputImage(QImage(":nocover.png")); loader_->Worker()->SetScaleOutputImage(false); @@ -65,10 +125,12 @@ QImage AlbumCoverSearcher::Exec(const QString &query) { if (exec() == QDialog::Rejected) return QImage(); - if (!ui_->covers->currentItem()) + + QModelIndex selected = ui_->covers->currentIndex(); + if (!selected.isValid() || !selected.data(Role_ImageFetchFinished).toBool()) return QImage(); - QIcon icon = ui_->covers->currentItem()->icon(); + QIcon icon = selected.data(Qt::DecorationRole).value(); if (icon.cacheKey() == no_cover_icon_.cacheKey()) return QImage(); @@ -81,7 +143,7 @@ void AlbumCoverSearcher::Search() { ui_->query->setEnabled(false); ui_->covers->setEnabled(false); - ui_->covers->clear(); + model_->clear(); cover_loading_tasks_.clear(); id_ = fetcher_->SearchForCovers(ui_->query->text()); @@ -102,12 +164,17 @@ void AlbumCoverSearcher::SearchFinished(quint64 id, const CoverSearchResults& re quint64 id = loader_->Worker()->LoadImageAsync(result.image_url, QString()); - QListWidgetItem* item = new QListWidgetItem(ui_->covers); + QStandardItem* item = new QStandardItem; item->setIcon(no_cover_icon_); item->setText(result.description); - item->setData(Role_ImageURL, result.image_url); - item->setData(Role_ImageRequestId, id); - item->setData(Qt::TextAlignmentRole, QVariant(Qt::AlignTop | Qt::AlignHCenter)); + item->setData(result.image_url, Role_ImageURL); + item->setData(id, Role_ImageRequestId); + item->setData(false, Role_ImageFetchFinished); + item->setData(QVariant(Qt::AlignTop | Qt::AlignHCenter), Qt::TextAlignmentRole); + item->setData(result.category, KCategorizedSortFilterProxyModel::CategoryDisplayRole); + item->setData(result.category, KCategorizedSortFilterProxyModel::CategorySortRole); + + model_->appendRow(item); cover_loading_tasks_[id] = item; } @@ -126,7 +193,8 @@ void AlbumCoverSearcher::ImageLoaded(quint64 id, const QImage& image) { icon.addPixmap(QPixmap::fromImage(image.scaled(ui_->covers->iconSize(), Qt::KeepAspectRatio, Qt::SmoothTransformation))); - QListWidgetItem* item = cover_loading_tasks_.take(id); + QStandardItem* item = cover_loading_tasks_.take(id); + item->setData(true, Role_ImageFetchFinished); item->setIcon(icon); item->setToolTip(item->text() + " (" + QString::number(image.width()) + "x" + QString::number(image.height()) + ")"); diff --git a/src/ui/albumcoversearcher.h b/src/ui/albumcoversearcher.h index 0116cdf74..e7a88605c 100644 --- a/src/ui/albumcoversearcher.h +++ b/src/ui/albumcoversearcher.h @@ -20,6 +20,7 @@ #include "core/backgroundthread.h" #include "covers/albumcoverfetcher.h" +#include "widgets/kcategorydrawer.h" #include #include @@ -29,8 +30,26 @@ class AlbumCoverLoader; class Ui_AlbumCoverSearcher; -class QListWidgetItem; class QModelIndex; +class QStandardItem; +class QStandardItemModel; + +class AlbumCoverCategoryDrawer : public KCategoryDrawerV3 { +public: + AlbumCoverCategoryDrawer(KCategorizedView* view); + + int categoryHeight(const QModelIndex& index, const QStyleOption& option) const; + void drawCategory(const QModelIndex& index, int sortRole, + const QStyleOption& option, QPainter* painter) const; + +private: + static const int kBarThickness; + static const int kBarMarginTop; + static const int kBarMarginBottom; + + int total_height_; +}; + // This is a dialog that lets the user search for album covers class AlbumCoverSearcher : public QDialog { @@ -43,6 +62,7 @@ public: enum Role { Role_ImageURL = Qt::UserRole + 1, Role_ImageRequestId, + Role_ImageFetchFinished, }; void Init(AlbumCoverFetcher* fetcher); @@ -62,12 +82,14 @@ private slots: private: Ui_AlbumCoverSearcher* ui_; + QStandardItemModel* model_; + QIcon no_cover_icon_; BackgroundThread* loader_; AlbumCoverFetcher* fetcher_; quint64 id_; - QMap cover_loading_tasks_; + QMap cover_loading_tasks_; }; #endif // ALBUMCOVERSEARCHER_H diff --git a/src/ui/albumcoversearcher.ui b/src/ui/albumcoversearcher.ui index 5d8b7e62c..1d1b0ad88 100644 --- a/src/ui/albumcoversearcher.ui +++ b/src/ui/albumcoversearcher.ui @@ -6,8 +6,8 @@ 0 0 - 556 - 354 + 829 + 518 @@ -36,7 +36,7 @@ - + Qt::ScrollBarAlwaysOff @@ -92,6 +92,11 @@ QLabel
widgets/busyindicator.h
+ + KCategorizedView + QListView +
widgets/kcategorizedview.h
+
query diff --git a/src/widgets/kcategorizedsortfilterproxymodel.cpp b/src/widgets/kcategorizedsortfilterproxymodel.cpp new file mode 100644 index 000000000..0cb93acd3 --- /dev/null +++ b/src/widgets/kcategorizedsortfilterproxymodel.cpp @@ -0,0 +1,167 @@ +/** + * This file is part of the KDE project + * Copyright (C) 2007 Rafael Fernández López + * Copyright (C) 2007 John Tapsell + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "kcategorizedsortfilterproxymodel.h" +#include "kcategorizedsortfilterproxymodel_p.h" + +#include + +#include +#include +#include + +KCategorizedSortFilterProxyModel::KCategorizedSortFilterProxyModel(QObject *parent) + : QSortFilterProxyModel(parent) + , d(new Private()) + +{ +} + +KCategorizedSortFilterProxyModel::~KCategorizedSortFilterProxyModel() +{ + delete d; +} + +void KCategorizedSortFilterProxyModel::sort(int column, Qt::SortOrder order) +{ + d->sortColumn = column; + d->sortOrder = order; + + QSortFilterProxyModel::sort(column, order); +} + +bool KCategorizedSortFilterProxyModel::isCategorizedModel() const +{ + return d->categorizedModel; +} + +void KCategorizedSortFilterProxyModel::setCategorizedModel(bool categorizedModel) +{ + if (categorizedModel == d->categorizedModel) + { + return; + } + + d->categorizedModel = categorizedModel; + + invalidate(); +} + +int KCategorizedSortFilterProxyModel::sortColumn() const +{ + return d->sortColumn; +} + +Qt::SortOrder KCategorizedSortFilterProxyModel::sortOrder() const +{ + return d->sortOrder; +} + +void KCategorizedSortFilterProxyModel::setSortCategoriesByNaturalComparison(bool sortCategoriesByNaturalComparison) +{ + if (sortCategoriesByNaturalComparison == d->sortCategoriesByNaturalComparison) + { + return; + } + + d->sortCategoriesByNaturalComparison = sortCategoriesByNaturalComparison; + + invalidate(); +} + +bool KCategorizedSortFilterProxyModel::sortCategoriesByNaturalComparison() const +{ + return d->sortCategoriesByNaturalComparison; +} + +bool KCategorizedSortFilterProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + if (d->categorizedModel) + { + int compare = compareCategories(left, right); + + if (compare > 0) // left is greater than right + { + return false; + } + else if (compare < 0) // left is less than right + { + return true; + } + } + + return subSortLessThan(left, right); +} + +bool KCategorizedSortFilterProxyModel::subSortLessThan(const QModelIndex &left, const QModelIndex &right) const +{ + return QSortFilterProxyModel::lessThan(left, right); +} + +int KCategorizedSortFilterProxyModel::compareCategories(const QModelIndex &left, const QModelIndex &right) const +{ + QVariant l = (left.model() ? left.model()->data(left, CategorySortRole) : QVariant()); + QVariant r = (right.model() ? right.model()->data(right, CategorySortRole) : QVariant()); + + Q_ASSERT(l.isValid()); + Q_ASSERT(r.isValid()); + Q_ASSERT(l.type() == r.type()); + + if (l.type() == QVariant::String) + { + QString lstr = l.toString(); + QString rstr = r.toString(); + + if (d->sortCategoriesByNaturalComparison) + { + return QString::localeAwareCompare(lstr, rstr); + } + else + { + if (lstr < rstr) + { + return -1; + } + + if (lstr > rstr) + { + return 1; + } + + return 0; + } + } + + qlonglong lint = l.toLongLong(); + qlonglong rint = r.toLongLong(); + + if (lint < rint) + { + return -1; + } + + if (lint > rint) + { + return 1; + } + + return 0; +} diff --git a/src/widgets/kcategorizedsortfilterproxymodel.h b/src/widgets/kcategorizedsortfilterproxymodel.h new file mode 100644 index 000000000..de356c3f4 --- /dev/null +++ b/src/widgets/kcategorizedsortfilterproxymodel.h @@ -0,0 +1,172 @@ +/* + * This file is part of the KDE project + * Copyright (C) 2007 Rafael Fernández López + * Copyright (C) 2007 John Tapsell + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef KCATEGORIZEDSORTFILTERPROXYMODEL_H +#define KCATEGORIZEDSORTFILTERPROXYMODEL_H + +#include + +class QItemSelection; + + +/** + * This class lets you categorize a view. It is meant to be used along with + * KCategorizedView class. + * + * In general terms all you need to do is to reimplement subSortLessThan() and + * compareCategories() methods. In order to make categorization work, you need + * to also call setCategorizedModel() class to enable it, since the categorization + * is disabled by default. + * + * @see KCategorizedView + * + * @author Rafael Fernández López + */ +class KCategorizedSortFilterProxyModel + : public QSortFilterProxyModel +{ +public: + enum AdditionalRoles { + // Note: use printf "0x%08X\n" $(($RANDOM*$RANDOM)) + // to define additional roles. + CategoryDisplayRole = 0x17CE990A, ///< This role is used for asking the category to a given index + + CategorySortRole = 0x27857E60 ///< This role is used for sorting categories. You can return a + ///< string or a long long value. Strings will be sorted alphabetically + ///< while long long will be sorted by their value. Please note that this + ///< value won't be shown on the view, is only for sorting purposes. What will + ///< be shown as "Category" on the view will be asked with the role + ///< CategoryDisplayRole. + }; + + KCategorizedSortFilterProxyModel(QObject *parent = 0); + virtual ~KCategorizedSortFilterProxyModel(); + + /** + * Overridden from QSortFilterProxyModel. Sorts the source model using + * @p column for the given @p order. + */ + virtual void sort(int column, Qt::SortOrder order = Qt::AscendingOrder); + + /** + * @return whether the model is categorized or not. Disabled by default. + */ + bool isCategorizedModel() const; + + /** + * Enables or disables the categorization feature. + * + * @param categorizedModel whether to enable or disable the categorization feature. + */ + void setCategorizedModel(bool categorizedModel); + + /** + * @return the column being used for sorting. + */ + int sortColumn() const; + + /** + * @return the sort order being used for sorting. + */ + Qt::SortOrder sortOrder() const; + + /** + * Set if the sorting using CategorySortRole will use a natural comparison + * in the case that strings were returned. If enabled, QString::localeAwareCompare + * will be used for sorting. + * + * @param sortCategoriesByNaturalComparison whether to sort using a natural comparison or not. + */ + void setSortCategoriesByNaturalComparison(bool sortCategoriesByNaturalComparison); + + /** + * @return whether it is being used a natural comparison for sorting. Enabled by default. + */ + bool sortCategoriesByNaturalComparison() const; + +protected: + /** + * Overridden from QSortFilterProxyModel. If you are subclassing + * KCategorizedSortFilterProxyModel, you will probably not need to reimplement this + * method. + * + * It calls compareCategories() to sort by category. If the both items are in the + * same category (i.e. compareCategories returns 0), then subSortLessThan is called. + * + * @return Returns true if the item @p left is less than the item @p right when sorting. + * + * @warning You usually won't need to reimplement this method when subclassing + * from KCategorizedSortFilterProxyModel. + */ + virtual bool lessThan(const QModelIndex &left, const QModelIndex &right) const; + + /** + * This method has a similar purpose as lessThan() has on QSortFilterProxyModel. + * It is used for sorting items that are in the same category. + * + * @return Returns true if the item @p left is less than the item @p right when sorting. + */ + virtual bool subSortLessThan(const QModelIndex &left, const QModelIndex &right) const; + + /** + * This method compares the category of the @p left index with the category + * of the @p right index. + * + * Internally and if not reimplemented, this method will ask for @p left and + * @p right models for role CategorySortRole. In order to correctly sort + * categories, the data() metod of the model should return a qlonglong (or numeric) value, or + * a QString object. QString objects will be sorted with QString::localeAwareCompare if + * sortCategoriesByNaturalComparison() is true. + * + * @note Please have present that: + * QString(QChar(QChar::ObjectReplacementCharacter)) > + * QString(QChar(QChar::ReplacementCharacter)) > + * [ all possible strings ] > + * QString(); + * + * This means that QString() will be sorted the first one, while + * QString(QChar(QChar::ObjectReplacementCharacter)) and + * QString(QChar(QChar::ReplacementCharacter)) will be sorted in last + * position. + * + * @warning Please note that data() method of the model should return always + * information of the same type. If you return a QString for an index, + * you should return always QStrings for all indexes for role CategorySortRole + * in order to correctly sort categories. You can't mix by returning + * a QString for one index, and a qlonglong for other. + * + * @note If you need a more complex layout, you will have to reimplement this + * method. + * + * @return A negative value if the category of @p left should be placed before the + * category of @p right. 0 if @p left and @p right are on the same category, and + * a positive value if the category of @p left should be placed after the + * category of @p right. + */ + virtual int compareCategories(const QModelIndex &left, const QModelIndex &right) const; + +private: + class Private; + Private *const d; +}; + + +#endif // KCATEGORIZEDSORTFILTERPROXYMODEL_H diff --git a/src/widgets/kcategorizedsortfilterproxymodel_p.h b/src/widgets/kcategorizedsortfilterproxymodel_p.h new file mode 100644 index 000000000..eb1a67b33 --- /dev/null +++ b/src/widgets/kcategorizedsortfilterproxymodel_p.h @@ -0,0 +1,48 @@ +/** + * This file is part of the KDE project + * Copyright (C) 2007 Rafael Fernández López + * Copyright (C) 2007 John Tapsell + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef KCATEGORIZEDSORTFILTERPROXYMODEL_P_H +#define KCATEGORIZEDSORTFILTERPROXYMODEL_P_H + +class KCategorizedSortFilterProxyModel; + +class KCategorizedSortFilterProxyModel::Private +{ +public: + Private() + : sortColumn(0) + , sortOrder(Qt::AscendingOrder) + , categorizedModel(false) + , sortCategoriesByNaturalComparison(true) + { + } + + ~Private() + { + } + + int sortColumn; + Qt::SortOrder sortOrder; + bool categorizedModel; + bool sortCategoriesByNaturalComparison; +}; + +#endif diff --git a/src/widgets/kcategorizedview-without-kde.patch b/src/widgets/kcategorizedview-without-kde.patch new file mode 100644 index 000000000..f8722ef46 --- /dev/null +++ b/src/widgets/kcategorizedview-without-kde.patch @@ -0,0 +1,263 @@ +diff --git a/src/widgets/kcategorizedsortfilterproxymodel.cpp b/src/widgets/kcategorizedsortfilterproxymodel.cpp +index c433c3d..0cb93ac 100644 +--- a/src/widgets/kcategorizedsortfilterproxymodel.cpp ++++ b/src/widgets/kcategorizedsortfilterproxymodel.cpp +@@ -28,8 +28,6 @@ + #include + #include + +-#include +- + KCategorizedSortFilterProxyModel::KCategorizedSortFilterProxyModel(QObject *parent) + : QSortFilterProxyModel(parent) + , d(new Private()) +@@ -94,14 +92,6 @@ bool KCategorizedSortFilterProxyModel::sortCategoriesByNaturalComparison() const + return d->sortCategoriesByNaturalComparison; + } + +-#ifndef KDE_NO_DEPRECATED +-int KCategorizedSortFilterProxyModel::naturalCompare(const QString &a, +- const QString &b) +-{ +- return KStringHandler::naturalCompare(a, b); +-} +-#endif +- + bool KCategorizedSortFilterProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const + { + if (d->categorizedModel) +@@ -142,7 +132,7 @@ int KCategorizedSortFilterProxyModel::compareCategories(const QModelIndex &left, + + if (d->sortCategoriesByNaturalComparison) + { +- return KStringHandler::naturalCompare(lstr, rstr); ++ return QString::localeAwareCompare(lstr, rstr); + } + else + { +diff --git a/src/widgets/kcategorizedsortfilterproxymodel.h b/src/widgets/kcategorizedsortfilterproxymodel.h +index a3146dc..de356c3 100644 +--- a/src/widgets/kcategorizedsortfilterproxymodel.h ++++ b/src/widgets/kcategorizedsortfilterproxymodel.h +@@ -24,8 +24,6 @@ + + #include + +-#include +- + class QItemSelection; + + +@@ -42,7 +40,7 @@ class QItemSelection; + * + * @author Rafael Fernández López + */ +-class KDEUI_EXPORT KCategorizedSortFilterProxyModel ++class KCategorizedSortFilterProxyModel + : public QSortFilterProxyModel + { + public: +@@ -104,17 +102,6 @@ public: + */ + bool sortCategoriesByNaturalComparison() const; + +- /** +- * Does a natural comparing of the strings. A negative value is returned if \a a +- * is smaller than \a b. A positive value is returned if \a a is greater than \a b. 0 +- * is returned if both values are equal. +- * @deprecated +- * Use KStringHandler::naturalCompare() instead. +- */ +-#ifndef KDE_NO_DEPRECATED +- KDE_DEPRECATED static int naturalCompare(const QString &a, const QString &b); +-#endif +- + protected: + /** + * Overridden from QSortFilterProxyModel. If you are subclassing +diff --git a/src/widgets/kcategorizedview.cpp b/src/widgets/kcategorizedview.cpp +index 46a1cde..7e99468 100644 +--- a/src/widgets/kcategorizedview.cpp ++++ b/src/widgets/kcategorizedview.cpp +@@ -33,7 +33,6 @@ + #include "kcategorizedview_p.h" + + #include // trunc on C99 compliant systems +-#include // trunc for not C99 compliant systems + + #include + #include +@@ -1455,28 +1454,6 @@ void KCategorizedView::rowsInserted(const QModelIndex &parent, + d->rowsInserted(parent, start, end); + } + +-#ifndef KDE_NO_DEPRECATED +-void KCategorizedView::rowsInsertedArtifficial(const QModelIndex &parent, +- int start, +- int end) +-{ +- Q_UNUSED(parent); +- Q_UNUSED(start); +- Q_UNUSED(end); +-} +-#endif +- +-#ifndef KDE_NO_DEPRECATED +-void KCategorizedView::rowsRemoved(const QModelIndex &parent, +- int start, +- int end) +-{ +- Q_UNUSED(parent); +- Q_UNUSED(start); +- Q_UNUSED(end); +-} +-#endif +- + void KCategorizedView::slotLayoutChanged() + { + if (!d->isCategorized()) { +@@ -1492,5 +1469,3 @@ void KCategorizedView::slotLayoutChanged() + } + + //END: Public part +- +-#include "kcategorizedview.moc" +diff --git a/src/widgets/kcategorizedview.h b/src/widgets/kcategorizedview.h +index b03744e..8f5ad9a 100644 +--- a/src/widgets/kcategorizedview.h ++++ b/src/widgets/kcategorizedview.h +@@ -23,8 +23,6 @@ + + #include + +-#include +- + class KCategoryDrawer; + class KCategoryDrawerV2; + +@@ -76,7 +74,7 @@ class KCategoryDrawerV2; + * + * @author Rafael Fernández López + */ +-class KDEUI_EXPORT KCategorizedView ++class KCategorizedView + : public QListView + { + Q_OBJECT +@@ -310,26 +308,6 @@ protected: + protected Q_SLOTS: + /** + * @internal +- * @warning Deprecated since 4.4. +- */ +-#ifndef KDE_NO_DEPRECATED +- virtual KDE_DEPRECATED void rowsInsertedArtifficial(const QModelIndex &parent, +- int start, +- int end); +-#endif +- +- /** +- * @internal +- * @warning Deprecated since 4.4. +- */ +-#ifndef KDE_NO_DEPRECATED +- virtual KDE_DEPRECATED void rowsRemoved(const QModelIndex &parent, +- int start, +- int end); +-#endif +- +- /** +- * @internal + * Reposition items as needed. + */ + virtual void slotLayoutChanged(); +@@ -341,4 +319,6 @@ private: + Q_PRIVATE_SLOT(d, void _k_slotCollapseOrExpandClicked(QModelIndex)) + }; + ++#include "kcategorizedview_p.h" ++ + #endif // KCATEGORIZEDVIEW_H +diff --git a/src/widgets/kcategorydrawer.cpp b/src/widgets/kcategorydrawer.cpp +index 2747e5b..5f5ce78 100644 +--- a/src/widgets/kcategorydrawer.cpp ++++ b/src/widgets/kcategorydrawer.cpp +@@ -24,7 +24,6 @@ + #include + #include + +-#include + #include + #include + +@@ -280,5 +279,3 @@ void KCategoryDrawerV3::mouseButtonDoubleClicked(const QModelIndex&, const QRect + void KCategoryDrawerV3::mouseLeft(const QModelIndex&, const QRect&) + { + } +- +-#include "kcategorydrawer.moc" +diff --git a/src/widgets/kcategorydrawer.h b/src/widgets/kcategorydrawer.h +index c24a0b8..d69b1b9 100644 +--- a/src/widgets/kcategorydrawer.h ++++ b/src/widgets/kcategorydrawer.h +@@ -21,8 +21,6 @@ + #ifndef KCATEGORYDRAWER_H + #define KCATEGORYDRAWER_H + +-#include +- + #include + #include + +@@ -40,10 +38,10 @@ class KCategorizedView; + * + * @warning Please use KCategoryDrawerV3 instead + */ +-class KDEUI_EXPORT KCategoryDrawer ++class KCategoryDrawer + { + public: +- KDE_DEPRECATED KCategoryDrawer(); ++ KCategoryDrawer(); + + virtual ~KCategoryDrawer(); + +@@ -110,23 +108,23 @@ private: + * + * @warning Please use KCategoryDrawerV3 instead + */ +-class KDEUI_EXPORT KCategoryDrawerV2 ++class KCategoryDrawerV2 + : public QObject + , public KCategoryDrawer + { + Q_OBJECT + + public: +- KDE_DEPRECATED KCategoryDrawerV2(QObject *parent = 0); ++ KCategoryDrawerV2(QObject *parent = 0); + virtual ~KCategoryDrawerV2(); + +- KDE_DEPRECATED virtual void mouseButtonPressed(const QModelIndex &index, QMouseEvent *event); ++ virtual void mouseButtonPressed(const QModelIndex &index, QMouseEvent *event); + +- KDE_DEPRECATED virtual void mouseButtonReleased(const QModelIndex &index, QMouseEvent *event); ++ virtual void mouseButtonReleased(const QModelIndex &index, QMouseEvent *event); + +- KDE_DEPRECATED virtual void mouseButtonMoved(const QModelIndex &index, QMouseEvent *event); ++ virtual void mouseButtonMoved(const QModelIndex &index, QMouseEvent *event); + +- KDE_DEPRECATED virtual void mouseButtonDoubleClicked(const QModelIndex &index, QMouseEvent *event); ++ virtual void mouseButtonDoubleClicked(const QModelIndex &index, QMouseEvent *event); + + Q_SIGNALS: + /** +@@ -147,7 +145,7 @@ Q_SIGNALS: + /** + * @since 4.5 + */ +-class KDEUI_EXPORT KCategoryDrawerV3 ++class KCategoryDrawerV3 + : public KCategoryDrawerV2 + { + friend class KCategorizedView; diff --git a/src/widgets/kcategorizedview.cpp b/src/widgets/kcategorizedview.cpp new file mode 100644 index 000000000..7e99468fd --- /dev/null +++ b/src/widgets/kcategorizedview.cpp @@ -0,0 +1,1471 @@ +/** + * This file is part of the KDE project + * Copyright (C) 2007, 2009 Rafael Fernández López + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +/** + * IMPLEMENTATION NOTES: + * + * QListView::setRowHidden() and QListView::isRowHidden() are not taken into + * account. This methods should actually not exist. This effect should be handled + * by an hypothetical QSortFilterProxyModel which filters out the desired rows. + * + * In case this needs to be implemented, contact me, but I consider this a faulty + * design. + */ + +#include "kcategorizedview.h" +#include "kcategorizedview_p.h" + +#include // trunc on C99 compliant systems + +#include +#include +#include + +#include "kcategorydrawer.h" +#include "kcategorizedsortfilterproxymodel.h" + +//BEGIN: Private part + +struct KCategorizedView::Private::Item +{ + Item() + : topLeft(QPoint()) + , size(QSize()) + { + } + + QPoint topLeft; + QSize size; +}; + +struct KCategorizedView::Private::Block +{ + Block() + : topLeft(QPoint()) + , height(-1) + , firstIndex(QModelIndex()) + , quarantineStart(QModelIndex()) + , items(QList()) + , outOfQuarantine(false) + , alternate(false) + , collapsed(false) + { + } + + bool operator!=(const Block &rhs) const + { + return firstIndex != rhs.firstIndex; + } + + static bool lessThan(const Block &left, const Block &right) + { + Q_ASSERT(left.firstIndex.isValid()); + Q_ASSERT(right.firstIndex.isValid()); + return left.firstIndex.row() < right.firstIndex.row(); + } + + QPoint topLeft; + int height; + QPersistentModelIndex firstIndex; + // if we have n elements on this block, and we inserted an element at position i. The quarantine + // will start at index (i, column, parent). This means that for all elements j where i <= j <= n, the + // visual rect position of item j will have to be recomputed (cannot use the cached point). The quarantine + // will only affect the current block, since the rest of blocks can be affected only in the way + // that the whole block will have different offset, but items will keep the same relative position + // in terms of their parent blocks. + QPersistentModelIndex quarantineStart; + QList items; + + // this affects the whole block, not items separately. items contain the topLeft point relative + // to the block. Because of insertions or removals a whole block can be moved, so the whole block + // will enter in quarantine, what is faster than moving all items in absolute terms. + bool outOfQuarantine; + + // should we alternate its color ? is just a hint, could not be used + bool alternate; + bool collapsed; +}; + +KCategorizedView::Private::Private(KCategorizedView *q) + : q(q) + , proxyModel(0) + , categoryDrawer(0) + , categoryDrawerV2(0) + , categoryDrawerV3(0) + , categorySpacing(5) + , alternatingBlockColors(false) + , collapsibleBlocks(false) + , hoveredBlock(new Block()) + , hoveredIndex(QModelIndex()) + , pressedPosition(QPoint()) + , rubberBandRect(QRect()) +{ +} + +KCategorizedView::Private::~Private() +{ + delete hoveredBlock; +} + +bool KCategorizedView::Private::isCategorized() const +{ + return proxyModel && categoryDrawer && proxyModel->isCategorizedModel(); +} + +QStyleOptionViewItemV4 KCategorizedView::Private::blockRect(const QModelIndex &representative) +{ + QStyleOptionViewItemV4 option(q->viewOptions()); + const int height = categoryDrawer->categoryHeight(representative, option); + const QString categoryDisplay = representative.data(KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString(); + QPoint pos = blockPosition(categoryDisplay); + pos.ry() -= height; + option.rect.setTopLeft(pos); + option.rect.setWidth(viewportWidth() + categoryDrawer->leftMargin() + categoryDrawer->rightMargin()); + option.rect.setHeight(height + blockHeight(categoryDisplay)); + option.rect = mapToViewport(option.rect); + + return option; +} + +QPair KCategorizedView::Private::intersectingIndexesWithRect(const QRect &_rect) const +{ + const int rowCount = proxyModel->rowCount(); + + const QRect rect = _rect.normalized(); + + // binary search to find out the top border + int bottom = 0; + int top = rowCount - 1; + while (bottom <= top) { + const int middle = (bottom + top) / 2; + const QModelIndex index = proxyModel->index(middle, q->modelColumn(), q->rootIndex()); + QRect itemRect = q->visualRect(index); + const int verticalOff = q->verticalOffset(); + const int horizontalOff = q->horizontalOffset(); + itemRect.topLeft().ry() += verticalOff; + itemRect.topLeft().rx() += horizontalOff; + itemRect.bottomRight().ry() += verticalOff; + itemRect.bottomRight().rx() += horizontalOff; + if (itemRect.bottomRight().y() <= rect.topLeft().y()) { + bottom = middle + 1; + } else { + top = middle - 1; + } + } + + const QModelIndex bottomIndex = proxyModel->index(bottom, q->modelColumn(), q->rootIndex()); + + // binary search to find out the bottom border + bottom = 0; + top = rowCount - 1; + while (bottom <= top) { + const int middle = (bottom + top) / 2; + const QModelIndex index = proxyModel->index(middle, q->modelColumn(), q->rootIndex()); + QRect itemRect = q->visualRect(index); + const int verticalOff = q->verticalOffset(); + const int horizontalOff = q->horizontalOffset(); + itemRect.topLeft().ry() += verticalOff; + itemRect.topLeft().rx() += horizontalOff; + itemRect.bottomRight().ry() += verticalOff; + itemRect.bottomRight().rx() += horizontalOff; + if (itemRect.topLeft().y() <= rect.bottomRight().y()) { + bottom = middle + 1; + } else { + top = middle - 1; + } + } + + const QModelIndex topIndex = proxyModel->index(top, q->modelColumn(), q->rootIndex()); + + return qMakePair(bottomIndex, topIndex); +} + +QPoint KCategorizedView::Private::blockPosition(const QString &category) +{ + Block &block = blocks[category]; + + if (block.outOfQuarantine && !block.topLeft.isNull()) { + return block.topLeft; + } + + QPoint res(categorySpacing, 0); + + const QModelIndex index = block.firstIndex; + + for (QHash::Iterator it = blocks.begin(); it != blocks.end(); ++it) { + Block &block = *it; + const QModelIndex categoryIndex = block.firstIndex; + if (index.row() < categoryIndex.row()) { + continue; + } + res.ry() += categoryDrawer->categoryHeight(categoryIndex, q->viewOptions()) + categorySpacing; + if (index.row() == categoryIndex.row()) { + continue; + } + res.ry() += blockHeight(it.key()); + } + + block.outOfQuarantine = true; + block.topLeft = res; + + return res; +} + +int KCategorizedView::Private::blockHeight(const QString &category) +{ + Block &block = blocks[category]; + + if (block.collapsed) { + return 0; + } + + if (block.height > -1) { + return block.height; + } + + const QModelIndex firstIndex = block.firstIndex; + const QModelIndex lastIndex = proxyModel->index(firstIndex.row() + block.items.count() - 1, q->modelColumn(), q->rootIndex()); + const QRect topLeft = q->visualRect(firstIndex); + QRect bottomRight = q->visualRect(lastIndex); + + if (hasGrid()) { + bottomRight.setHeight(qMax(bottomRight.height(), q->gridSize().height())); + } else { + if (!q->uniformItemSizes()) { + bottomRight.setHeight(highestElementInLastRow(block) + q->spacing() * 2); + } + } + + const int height = bottomRight.bottomRight().y() - topLeft.topLeft().y() + 1; + block.height = height; + + return height; +} + +int KCategorizedView::Private::viewportWidth() const +{ + return q->viewport()->width() - categorySpacing * 2 - categoryDrawer->leftMargin() - categoryDrawer->rightMargin(); +} + +void KCategorizedView::Private::regenerateAllElements() +{ + for (QHash::Iterator it = blocks.begin(); it != blocks.end(); ++it) { + Block &block = *it; + block.outOfQuarantine = false; + block.quarantineStart = block.firstIndex; + block.height = -1; + } +} + +void KCategorizedView::Private::rowsInserted(const QModelIndex &parent, int start, int end) +{ + if (!isCategorized()) { + return; + } + + for (int i = start; i <= end; ++i) { + const QModelIndex index = proxyModel->index(i, q->modelColumn(), parent); + + Q_ASSERT(index.isValid()); + + const QString category = categoryForIndex(index); + + Block &block = blocks[category]; + + //BEGIN: update firstIndex + // save as firstIndex in block if + // - it forced the category creation (first element on this category) + // - it is before the first row on that category + const QModelIndex firstIndex = block.firstIndex; + if (!firstIndex.isValid() || index.row() < firstIndex.row()) { + block.firstIndex = index; + } + //END: update firstIndex + + Q_ASSERT(block.firstIndex.isValid()); + + const int firstIndexRow = block.firstIndex.row(); + + block.items.insert(index.row() - firstIndexRow, Private::Item()); + block.height = -1; + + q->visualRect(index); + q->viewport()->update(); + } + + //BEGIN: update the items that are in quarantine in affected categories + { + const QModelIndex lastIndex = proxyModel->index(end, q->modelColumn(), parent); + const QString category = categoryForIndex(lastIndex); + Private::Block &block = blocks[category]; + block.quarantineStart = block.firstIndex; + } + //END: update the items that are in quarantine in affected categories + + //BEGIN: mark as in quarantine those categories that are under the affected ones + { + const QModelIndex firstIndex = proxyModel->index(start, q->modelColumn(), parent); + const QString category = categoryForIndex(firstIndex); + const QModelIndex firstAffectedCategory = blocks[category].firstIndex; + //BEGIN: order for marking as alternate those blocks that are alternate + QList blockList = blocks.values(); + qSort(blockList.begin(), blockList.end(), Block::lessThan); + QList firstIndexesRows; + foreach (const Block &block, blockList) { + firstIndexesRows << block.firstIndex.row(); + } + //END: order for marking as alternate those blocks that are alternate + for (QHash::Iterator it = blocks.begin(); it != blocks.end(); ++it) { + Private::Block &block = *it; + if (block.firstIndex.row() > firstAffectedCategory.row()) { + block.outOfQuarantine = false; + block.alternate = firstIndexesRows.indexOf(block.firstIndex.row()) % 2; + } else if (block.firstIndex.row() == firstAffectedCategory.row()) { + block.alternate = firstIndexesRows.indexOf(block.firstIndex.row()) % 2; + } + } + } + //END: mark as in quarantine those categories that are under the affected ones +} + +QRect KCategorizedView::Private::mapToViewport(const QRect &rect) const +{ + const int dx = -q->horizontalOffset(); + const int dy = -q->verticalOffset(); + return rect.adjusted(dx, dy, dx, dy); +} + +QRect KCategorizedView::Private::mapFromViewport(const QRect &rect) const +{ + const int dx = q->horizontalOffset(); + const int dy = q->verticalOffset(); + return rect.adjusted(dx, dy, dx, dy); +} + +int KCategorizedView::Private::highestElementInLastRow(const Block &block) const +{ + //Find the highest element in the last row + const QModelIndex lastIndex = proxyModel->index(block.firstIndex.row() + block.items.count() - 1, q->modelColumn(), q->rootIndex()); + const QRect prevRect = q->visualRect(lastIndex); + int res = prevRect.height(); + QModelIndex prevIndex = proxyModel->index(lastIndex.row() - 1, q->modelColumn(), q->rootIndex()); + if (!prevIndex.isValid()) { + return res; + } + Q_FOREVER { + const QRect tempRect = q->visualRect(prevIndex); + if (tempRect.topLeft().y() < prevRect.topLeft().y()) { + break; + } + res = qMax(res, tempRect.height()); + if (prevIndex == block.firstIndex) { + break; + } + prevIndex = proxyModel->index(prevIndex.row() - 1, q->modelColumn(), q->rootIndex()); + } + + return res; +} + +bool KCategorizedView::Private::hasGrid() const +{ + const QSize gridSize = q->gridSize(); + return gridSize.isValid() && !gridSize.isNull(); +} + +QString KCategorizedView::Private::categoryForIndex(const QModelIndex &index) const +{ + const QModelIndex categoryIndex = index.model()->index(index.row(), proxyModel->sortColumn(), index.parent()); + return categoryIndex.data(KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString(); +} + +void KCategorizedView::Private::leftToRightVisualRect(const QModelIndex &index, Item &item, + const Block &block, const QPoint &blockPos) const +{ + const int firstIndexRow = block.firstIndex.row(); + + if (hasGrid()) { + const int relativeRow = index.row() - firstIndexRow; + const int maxItemsPerRow = qMax(viewportWidth() / q->gridSize().width(), 1); + if (q->layoutDirection() == Qt::LeftToRight) { + item.topLeft.rx() = (relativeRow % maxItemsPerRow) * q->gridSize().width() + blockPos.x() + categoryDrawer->leftMargin(); + } else { + item.topLeft.rx() = viewportWidth() - ((relativeRow % maxItemsPerRow) + 1) * q->gridSize().width() + categoryDrawer->leftMargin() + categorySpacing; + } + item.topLeft.ry() = (relativeRow / maxItemsPerRow) * q->gridSize().height(); + } else { + if (q->uniformItemSizes()) { + const int relativeRow = index.row() - firstIndexRow; + const QSize itemSize = q->sizeHintForIndex(index); + const int maxItemsPerRow = qMax((viewportWidth() - q->spacing()) / (itemSize.width() + q->spacing()), 1); + if (q->layoutDirection() == Qt::LeftToRight) { + item.topLeft.rx() = (relativeRow % maxItemsPerRow) * itemSize.width() + blockPos.x() + categoryDrawer->leftMargin(); + } else { + item.topLeft.rx() = viewportWidth() - (relativeRow % maxItemsPerRow) * itemSize.width() + categoryDrawer->leftMargin() + categorySpacing; + } + item.topLeft.ry() = (relativeRow / maxItemsPerRow) * itemSize.height(); + } else { + const QSize currSize = q->sizeHintForIndex(index); + if (index != block.firstIndex) { + const int viewportW = viewportWidth() - q->spacing(); + QModelIndex prevIndex = proxyModel->index(index.row() - 1, q->modelColumn(), q->rootIndex()); + QRect prevRect = q->visualRect(prevIndex); + prevRect = mapFromViewport(prevRect); + if ((prevRect.bottomRight().x() + 1) + currSize.width() - blockPos.x() + q->spacing() > viewportW) { + // we have to check the whole previous row, and see which one was the + // highest. + Q_FOREVER { + prevIndex = proxyModel->index(prevIndex.row() - 1, q->modelColumn(), q->rootIndex()); + const QRect tempRect = q->visualRect(prevIndex); + if (tempRect.topLeft().y() < prevRect.topLeft().y()) { + break; + } + if (tempRect.bottomRight().y() > prevRect.bottomRight().y()) { + prevRect = tempRect; + } + if (prevIndex == block.firstIndex) { + break; + } + } + if (q->layoutDirection() == Qt::LeftToRight) { + item.topLeft.rx() = categoryDrawer->leftMargin() + blockPos.x() + q->spacing(); + } else { + item.topLeft.rx() = viewportWidth() - currSize.width() + categoryDrawer->leftMargin() + categorySpacing; + } + item.topLeft.ry() = (prevRect.bottomRight().y() + 1) + q->spacing() - blockPos.y(); + } else { + if (q->layoutDirection() == Qt::LeftToRight) { + item.topLeft.rx() = (prevRect.bottomRight().x() + 1) + q->spacing(); + } else { + item.topLeft.rx() = (prevRect.bottomLeft().x() - 1) - q->spacing() - item.size.width() + categoryDrawer->leftMargin() + categorySpacing; + } + item.topLeft.ry() = prevRect.topLeft().y() - blockPos.y(); + } + } else { + if (q->layoutDirection() == Qt::LeftToRight) { + item.topLeft.rx() = blockPos.x() + categoryDrawer->leftMargin() + q->spacing(); + } else { + item.topLeft.rx() = viewportWidth() - currSize.width() + categoryDrawer->leftMargin() + categorySpacing; + } + item.topLeft.ry() = q->spacing(); + } + } + } + item.size = q->sizeHintForIndex(index); +} + +void KCategorizedView::Private::topToBottomVisualRect(const QModelIndex &index, Item &item, + const Block &block, const QPoint &blockPos) const +{ + const int firstIndexRow = block.firstIndex.row(); + + if (hasGrid()) { + const int relativeRow = index.row() - firstIndexRow; + item.topLeft.rx() = blockPos.x() + categoryDrawer->leftMargin(); + item.topLeft.ry() = relativeRow * q->gridSize().height(); + } else { + if (q->uniformItemSizes()) { + const int relativeRow = index.row() - firstIndexRow; + const QSize itemSize = q->sizeHintForIndex(index); + item.topLeft.rx() = blockPos.x() + categoryDrawer->leftMargin(); + item.topLeft.ry() = relativeRow * itemSize.height(); + } else { + if (index != block.firstIndex) { + QModelIndex prevIndex = proxyModel->index(index.row() - 1, q->modelColumn(), q->rootIndex()); + QRect prevRect = q->visualRect(prevIndex); + prevRect = mapFromViewport(prevRect); + const QSize currSize = q->sizeHintForIndex(index); + item.topLeft.rx() = blockPos.x() + categoryDrawer->leftMargin() + q->spacing(); + item.topLeft.ry() = (prevRect.bottomRight().y() + 1) + q->spacing() - blockPos.y(); + } else { + item.topLeft.rx() = blockPos.x() + categoryDrawer->leftMargin() + q->spacing(); + item.topLeft.ry() = q->spacing(); + } + } + } + item.size = q->sizeHintForIndex(index); + item.size.setWidth(viewportWidth()); +} + +void KCategorizedView::Private::_k_slotCollapseOrExpandClicked(QModelIndex) +{ +} + +//END: Private part + +//BEGIN: Public part + +KCategorizedView::KCategorizedView(QWidget *parent) + : QListView(parent) + , d(new Private(this)) +{ +} + +KCategorizedView::~KCategorizedView() +{ + delete d; +} + +void KCategorizedView::setModel(QAbstractItemModel *model) +{ + if (d->proxyModel == model) { + return; + } + + d->blocks.clear(); + + if (d->proxyModel) { + disconnect(d->proxyModel, SIGNAL(layoutChanged()), this, SLOT(slotLayoutChanged())); + } + + d->proxyModel = dynamic_cast(model); + + if (d->proxyModel) { + connect(d->proxyModel, SIGNAL(layoutChanged()), this, SLOT(slotLayoutChanged())); + } + + QListView::setModel(model); + + // if the model already had information inserted, update our data structures to it + if (model->rowCount()) { + slotLayoutChanged(); + } +} + +void KCategorizedView::setGridSize(const QSize &size) +{ + setGridSizeOwn(size); +} + +void KCategorizedView::setGridSizeOwn(const QSize &size) +{ + d->regenerateAllElements(); + QListView::setGridSize(size); +} + +QRect KCategorizedView::visualRect(const QModelIndex &index) const +{ + if (!d->isCategorized()) { + return QListView::visualRect(index); + } + + if (!index.isValid()) { + return QRect(); + } + + const QString category = d->categoryForIndex(index); + + if (!d->blocks.contains(category)) { + return QRect(); + } + + Private::Block &block = d->blocks[category]; + const int firstIndexRow = block.firstIndex.row(); + + Q_ASSERT(block.firstIndex.isValid()); + + if (index.row() - firstIndexRow < 0 || index.row() - firstIndexRow >= block.items.count()) { + return QRect(); + } + + const QPoint blockPos = d->blockPosition(category); + + Private::Item &ritem = block.items[index.row() - firstIndexRow]; + + if (ritem.topLeft.isNull() || (block.quarantineStart.isValid() && + index.row() >= block.quarantineStart.row())) { + if (flow() == LeftToRight) { + d->leftToRightVisualRect(index, ritem, block, blockPos); + } else { + d->topToBottomVisualRect(index, ritem, block, blockPos); + } + + //BEGIN: update the quarantine start + const bool wasLastIndex = (index.row() == (block.firstIndex.row() + block.items.count() - 1)); + if (index.row() == block.quarantineStart.row()) { + if (wasLastIndex) { + block.quarantineStart = QModelIndex(); + } else { + const QModelIndex nextIndex = d->proxyModel->index(index.row() + 1, modelColumn(), rootIndex()); + block.quarantineStart = nextIndex; + } + } + //END: update the quarantine start + } + + // we get now the absolute position through the relative position of the parent block. do not + // save this on ritem, since this would override the item relative position in block terms. + Private::Item item(ritem); + item.topLeft.ry() += blockPos.y(); + + const QSize sizeHint = item.size; + + if (d->hasGrid()) { + const QSize sizeGrid = gridSize(); + const QSize resultingSize = sizeHint.boundedTo(sizeGrid); + QRect res(item.topLeft.x() + ((sizeGrid.width() - resultingSize.width()) / 2), + item.topLeft.y(), resultingSize.width(), resultingSize.height()); + if (block.collapsed) { + // we can still do binary search, while we "hide" items. We move those items in collapsed + // blocks to the left and set a 0 height. + res.setLeft(-resultingSize.width()); + res.setHeight(0); + } + return d->mapToViewport(res); + } + + QRect res(item.topLeft.x(), item.topLeft.y(), sizeHint.width(), sizeHint.height()); + if (block.collapsed) { + // we can still do binary search, while we "hide" items. We move those items in collapsed + // blocks to the left and set a 0 height. + res.setLeft(-sizeHint.width()); + res.setHeight(0); + } + return d->mapToViewport(res); +} + +KCategoryDrawer *KCategorizedView::categoryDrawer() const +{ + return d->categoryDrawer; +} + +void KCategorizedView::setCategoryDrawer(KCategoryDrawer *categoryDrawer) +{ + if (d->categoryDrawerV2) { + disconnect(d->categoryDrawerV2, SIGNAL(collapseOrExpandClicked(QModelIndex)), + this, SLOT(_k_slotCollapseOrExpandClicked(QModelIndex))); + } + + d->categoryDrawer = categoryDrawer; + d->categoryDrawerV2 = dynamic_cast(categoryDrawer); + d->categoryDrawerV3 = dynamic_cast(categoryDrawer); + + if (d->categoryDrawerV2) { + connect(d->categoryDrawerV2, SIGNAL(collapseOrExpandClicked(QModelIndex)), + this, SLOT(_k_slotCollapseOrExpandClicked(QModelIndex))); + } +} + +int KCategorizedView::categorySpacing() const +{ + return d->categorySpacing; +} + +void KCategorizedView::setCategorySpacing(int categorySpacing) +{ + if (d->categorySpacing == categorySpacing) { + return; + } + + d->categorySpacing = categorySpacing; + + for (QHash::Iterator it = d->blocks.begin(); it != d->blocks.end(); ++it) { + Private::Block &block = *it; + block.outOfQuarantine = false; + } +} + +bool KCategorizedView::alternatingBlockColors() const +{ + return d->alternatingBlockColors; +} + +void KCategorizedView::setAlternatingBlockColors(bool enable) +{ + d->alternatingBlockColors = enable; +} + +bool KCategorizedView::collapsibleBlocks() const +{ + return d->collapsibleBlocks; +} + +void KCategorizedView::setCollapsibleBlocks(bool enable) +{ + d->collapsibleBlocks = enable; +} + +QModelIndexList KCategorizedView::block(const QString &category) +{ + QModelIndexList res; + const Private::Block &block = d->blocks[category]; + if (block.height == -1) { + return res; + } + QModelIndex current = block.firstIndex; + const int first = current.row(); + for (int i = 1; i <= block.items.count(); ++i) { + if (current.isValid()) { + res << current; + } + current = d->proxyModel->index(first + i, modelColumn(), rootIndex()); + } + return res; +} + +QModelIndexList KCategorizedView::block(const QModelIndex &representative) +{ + return block(representative.data(KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString()); +} + +QModelIndex KCategorizedView::indexAt(const QPoint &point) const +{ + if (!d->isCategorized()) { + return QListView::indexAt(point); + } + + const int rowCount = d->proxyModel->rowCount(); + if (!rowCount) { + return QModelIndex(); + } + + // Binary search that will try to spot if there is an index under point + int bottom = 0; + int top = rowCount - 1; + while (bottom <= top) { + const int middle = (bottom + top) / 2; + const QModelIndex index = d->proxyModel->index(middle, modelColumn(), rootIndex()); + QRect rect = visualRect(index); + const int verticalOff = verticalOffset(); + int horizontalOff = horizontalOffset(); + if (layoutDirection() == Qt::RightToLeft) { + horizontalOff *= -1; + } + rect.topLeft().ry() += verticalOff; + rect.topLeft().rx() += horizontalOff; + rect.bottomRight().ry() += verticalOff; + rect.bottomRight().rx() += horizontalOff; + if (rect.contains(point)) { + if (index.model()->flags(index) & Qt::ItemIsEnabled) { + return index; + } + return QModelIndex(); + } + bool directionCondition; + if (layoutDirection() == Qt::LeftToRight) { + directionCondition = point.x() > rect.bottomRight().x(); + } else { + directionCondition = point.x() < rect.bottomLeft().x(); + } + if (point.y() > rect.bottomRight().y() || + (point.y() > rect.topLeft().y() && point.y() < rect.bottomRight().y() && directionCondition)) { + bottom = middle + 1; + } else { + top = middle - 1; + } + } + return QModelIndex(); +} + +void KCategorizedView::reset() +{ + d->blocks.clear(); + QListView::reset(); +} + +void KCategorizedView::paintEvent(QPaintEvent *event) +{ + if (!d->isCategorized()) { + QListView::paintEvent(event); + return; + } + + const QPair intersecting = d->intersectingIndexesWithRect(viewport()->rect().intersected(event->rect())); + + QPainter p(viewport()); + p.save(); + + Q_ASSERT(selectionModel()->model() == d->proxyModel); + + //BEGIN: draw categories + QHash::ConstIterator it(d->blocks.constBegin()); + while (it != d->blocks.constEnd()) { + const Private::Block &block = *it; + const QModelIndex categoryIndex = d->proxyModel->index(block.firstIndex.row(), d->proxyModel->sortColumn(), rootIndex()); + QStyleOptionViewItemV4 option(viewOptions()); + option.features |= d->alternatingBlockColors && block.alternate ? QStyleOptionViewItemV4::Alternate + : QStyleOptionViewItemV4::None; + option.state |= !d->collapsibleBlocks || !block.collapsed ? QStyle::State_Open + : QStyle::State_None; + const int height = d->categoryDrawer->categoryHeight(categoryIndex, option); + QPoint pos = d->blockPosition(it.key()); + pos.ry() -= height; + option.rect.setTopLeft(pos); + option.rect.setWidth(d->viewportWidth() + d->categoryDrawer->leftMargin() + d->categoryDrawer->rightMargin()); + option.rect.setHeight(height + d->blockHeight(it.key())); + option.rect = d->mapToViewport(option.rect); + if (!option.rect.intersects(viewport()->rect())) { + ++it; + continue; + } + d->categoryDrawer->drawCategory(categoryIndex, d->proxyModel->sortRole(), option, &p); + ++it; + } + //END: draw categories + + if (intersecting.first.isValid() && intersecting.second.isValid()) { + //BEGIN: draw items + int i = intersecting.first.row(); + int indexToCheckIfBlockCollapsed = i; + QModelIndex categoryIndex; + QString category; + Private::Block *block = 0; + while (i <= intersecting.second.row()) { + //BEGIN: first check if the block is collapsed. if so, we have to skip the item painting + if (i == indexToCheckIfBlockCollapsed) { + categoryIndex = d->proxyModel->index(i, d->proxyModel->sortColumn(), rootIndex()); + category = categoryIndex.data(KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString(); + block = &d->blocks[category]; + indexToCheckIfBlockCollapsed = block->firstIndex.row() + block->items.count(); + if (block->collapsed) { + i = indexToCheckIfBlockCollapsed; + continue; + } + } + //END: first check if the block is collapsed. if so, we have to skip the item painting + + Q_ASSERT(block); + + const bool alternateItem = (i - block->firstIndex.row()) % 2; + + const QModelIndex index = d->proxyModel->index(i, modelColumn(), rootIndex()); + const Qt::ItemFlags flags = d->proxyModel->flags(index); + QStyleOptionViewItemV4 option(viewOptions()); + option.rect = visualRect(index); + option.widget = this; + option.features |= wordWrap() ? QStyleOptionViewItemV2::WrapText + : QStyleOptionViewItemV2::None; + option.features |= alternatingRowColors() && alternateItem ? QStyleOptionViewItemV4::Alternate + : QStyleOptionViewItemV4::None; + if (flags & Qt::ItemIsSelectable) { + option.state |= selectionModel()->isSelected(index) ? QStyle::State_Selected + : QStyle::State_None; + } else { + option.state &= ~QStyle::State_Selected; + } + option.state |= (index == currentIndex()) ? QStyle::State_HasFocus + : QStyle::State_None; + if (!(flags & Qt::ItemIsEnabled)) { + option.state &= ~QStyle::State_Enabled; + } else { + option.state |= (index == d->hoveredIndex) ? QStyle::State_MouseOver + : QStyle::State_None; + } + + itemDelegate(index)->paint(&p, option, index); + ++i; + } + //END: draw items + } + + //BEGIN: draw selection rect + if (isSelectionRectVisible() && d->rubberBandRect.isValid()) { + QStyleOptionRubberBand opt; + opt.initFrom(this); + opt.shape = QRubberBand::Rectangle; + opt.opaque = false; + opt.rect = d->mapToViewport(d->rubberBandRect).intersected(viewport()->rect().adjusted(-16, -16, 16, 16)); + p.save(); + style()->drawControl(QStyle::CE_RubberBand, &opt, &p); + p.restore(); + } + //END: draw selection rect + + p.restore(); +} + +void KCategorizedView::resizeEvent(QResizeEvent *event) +{ + d->regenerateAllElements(); + QListView::resizeEvent(event); +} + +void KCategorizedView::setSelection(const QRect &rect, + QItemSelectionModel::SelectionFlags flags) +{ + if (!d->isCategorized()) { + QListView::setSelection(rect, flags); + return; + } + + if (rect.topLeft() == rect.bottomRight()) { + const QModelIndex index = indexAt(rect.topLeft()); + selectionModel()->select(index, flags); + return; + } + + const QPair intersecting = d->intersectingIndexesWithRect(rect); + + QItemSelection selection; + + //TODO: think of a faster implementation + QModelIndex firstIndex; + QModelIndex lastIndex; + for (int i = intersecting.first.row(); i <= intersecting.second.row(); ++i) { + const QModelIndex index = d->proxyModel->index(i, modelColumn(), rootIndex()); + const bool visualRectIntersects = visualRect(index).intersects(rect); + if (firstIndex.isValid()) { + if (visualRectIntersects) { + lastIndex = index; + } else { + selection << QItemSelectionRange(firstIndex, lastIndex); + firstIndex = QModelIndex(); + } + } else if (visualRectIntersects) { + firstIndex = index; + lastIndex = index; + } + } + + if (firstIndex.isValid()) { + selection << QItemSelectionRange(firstIndex, lastIndex); + } + + selectionModel()->select(selection, flags); +} + +void KCategorizedView::mouseMoveEvent(QMouseEvent *event) +{ + QListView::mouseMoveEvent(event); + d->hoveredIndex = indexAt(event->pos()); + const SelectionMode itemViewSelectionMode = selectionMode(); + if (state() == DragSelectingState && isSelectionRectVisible() && itemViewSelectionMode != SingleSelection + && itemViewSelectionMode != NoSelection) { + QRect rect(d->pressedPosition, event->pos() + QPoint(horizontalOffset(), verticalOffset())); + rect = rect.normalized(); + update(rect.united(d->rubberBandRect)); + d->rubberBandRect = rect; + } + if (!d->categoryDrawerV2) { + return; + } + QHash::ConstIterator it(d->blocks.constBegin()); + while (it != d->blocks.constEnd()) { + const Private::Block &block = *it; + const QModelIndex categoryIndex = d->proxyModel->index(block.firstIndex.row(), d->proxyModel->sortColumn(), rootIndex()); + QStyleOptionViewItemV4 option(viewOptions()); + const int height = d->categoryDrawer->categoryHeight(categoryIndex, option); + QPoint pos = d->blockPosition(it.key()); + pos.ry() -= height; + option.rect.setTopLeft(pos); + option.rect.setWidth(d->viewportWidth() + d->categoryDrawer->leftMargin() + d->categoryDrawer->rightMargin()); + option.rect.setHeight(height + d->blockHeight(it.key())); + option.rect = d->mapToViewport(option.rect); + const QPoint mousePos = viewport()->mapFromGlobal(QCursor::pos()); + if (option.rect.contains(mousePos)) { + if (d->categoryDrawerV3 && d->hoveredBlock->height != -1 && *d->hoveredBlock != block) { + const QModelIndex categoryIndex = d->proxyModel->index(d->hoveredBlock->firstIndex.row(), d->proxyModel->sortColumn(), rootIndex()); + const QStyleOptionViewItemV4 option = d->blockRect(categoryIndex); + d->categoryDrawerV3->mouseLeft(categoryIndex, option.rect); + *d->hoveredBlock = block; + d->hoveredCategory = it.key(); + viewport()->update(option.rect); + } else if (d->hoveredBlock->height == -1) { + *d->hoveredBlock = block; + d->hoveredCategory = it.key(); + } else if (d->categoryDrawerV3) { + d->categoryDrawerV3->mouseMoved(categoryIndex, option.rect, event); + } else { + d->categoryDrawerV2->mouseButtonMoved(categoryIndex, event); + } + viewport()->update(option.rect); + return; + } + ++it; + } + if (d->categoryDrawerV3 && d->hoveredBlock->height != -1) { + const QModelIndex categoryIndex = d->proxyModel->index(d->hoveredBlock->firstIndex.row(), d->proxyModel->sortColumn(), rootIndex()); + const QStyleOptionViewItemV4 option = d->blockRect(categoryIndex); + d->categoryDrawerV3->mouseLeft(categoryIndex, option.rect); + *d->hoveredBlock = Private::Block(); + d->hoveredCategory = QString(); + viewport()->update(option.rect); + } +} + +void KCategorizedView::mousePressEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) { + d->pressedPosition = event->pos(); + d->pressedPosition.rx() += horizontalOffset(); + d->pressedPosition.ry() += verticalOffset(); + } + if (!d->categoryDrawerV2) { + QListView::mousePressEvent(event); + return; + } + QHash::ConstIterator it(d->blocks.constBegin()); + while (it != d->blocks.constEnd()) { + const Private::Block &block = *it; + const QModelIndex categoryIndex = d->proxyModel->index(block.firstIndex.row(), d->proxyModel->sortColumn(), rootIndex()); + const QStyleOptionViewItemV4 option = d->blockRect(categoryIndex); + const QPoint mousePos = viewport()->mapFromGlobal(QCursor::pos()); + if (option.rect.contains(mousePos)) { + if (d->categoryDrawerV3) { + d->categoryDrawerV3->mouseButtonPressed(categoryIndex, option.rect, event); + } else { + d->categoryDrawerV2->mouseButtonPressed(categoryIndex, event); + } + viewport()->update(option.rect); + if (!event->isAccepted()) { + QListView::mousePressEvent(event); + } + return; + } + ++it; + } + QListView::mousePressEvent(event); +} + +void KCategorizedView::mouseReleaseEvent(QMouseEvent *event) +{ + d->pressedPosition = QPoint(); + d->rubberBandRect = QRect(); + if (!d->categoryDrawerV2) { + QListView::mouseReleaseEvent(event); + return; + } + QHash::ConstIterator it(d->blocks.constBegin()); + while (it != d->blocks.constEnd()) { + const Private::Block &block = *it; + const QModelIndex categoryIndex = d->proxyModel->index(block.firstIndex.row(), d->proxyModel->sortColumn(), rootIndex()); + const QStyleOptionViewItemV4 option = d->blockRect(categoryIndex); + const QPoint mousePos = viewport()->mapFromGlobal(QCursor::pos()); + if (option.rect.contains(mousePos)) { + if (d->categoryDrawerV3) { + d->categoryDrawerV3->mouseButtonReleased(categoryIndex, option.rect, event); + } else { + d->categoryDrawerV2->mouseButtonReleased(categoryIndex, event); + } + viewport()->update(option.rect); + if (!event->isAccepted()) { + QListView::mouseReleaseEvent(event); + } + return; + } + ++it; + } + QListView::mouseReleaseEvent(event); +} + +void KCategorizedView::leaveEvent(QEvent *event) +{ + QListView::leaveEvent(event); + if (d->hoveredIndex.isValid()) { + viewport()->update(visualRect(d->hoveredIndex)); + d->hoveredIndex = QModelIndex(); + } + if (d->categoryDrawerV3 && d->hoveredBlock->height != -1) { + const QModelIndex categoryIndex = d->proxyModel->index(d->hoveredBlock->firstIndex.row(), d->proxyModel->sortColumn(), rootIndex()); + const QStyleOptionViewItemV4 option = d->blockRect(categoryIndex); + d->categoryDrawerV3->mouseLeft(categoryIndex, option.rect); + *d->hoveredBlock = Private::Block(); + d->hoveredCategory = QString(); + viewport()->update(option.rect); + } +} + +void KCategorizedView::startDrag(Qt::DropActions supportedActions) +{ + QListView::startDrag(supportedActions); +} + +void KCategorizedView::dragMoveEvent(QDragMoveEvent *event) +{ + QListView::dragMoveEvent(event); + d->hoveredIndex = indexAt(event->pos()); +} + +void KCategorizedView::dragEnterEvent(QDragEnterEvent *event) +{ + QListView::dragEnterEvent(event); +} + +void KCategorizedView::dragLeaveEvent(QDragLeaveEvent *event) +{ + QListView::dragLeaveEvent(event); +} + +void KCategorizedView::dropEvent(QDropEvent *event) +{ + QListView::dropEvent(event); +} + +//TODO: improve se we take into account collapsed blocks +//TODO: take into account when there is no grid and no uniformItemSizes +QModelIndex KCategorizedView::moveCursor(CursorAction cursorAction, + Qt::KeyboardModifiers modifiers) +{ + if (!d->isCategorized()) { + return QListView::moveCursor(cursorAction, modifiers); + } + + const QModelIndex current = currentIndex(); + const QRect currentRect = visualRect(current); + if (!current.isValid()) { + const int rowCount = d->proxyModel->rowCount(rootIndex()); + if (!rowCount) { + return QModelIndex(); + } + return d->proxyModel->index(0, modelColumn(), rootIndex()); + } + + switch (cursorAction) { + case MoveLeft: { + if (!current.row()) { + return QModelIndex(); + } + const QModelIndex previous = d->proxyModel->index(current.row() - 1, modelColumn(), rootIndex()); + const QRect previousRect = visualRect(previous); + if (previousRect.top() == currentRect.top()) { + return previous; + } + + return QModelIndex(); + } + case MoveRight: { + if (current.row() == d->proxyModel->rowCount() - 1) { + return QModelIndex(); + } + const QModelIndex next = d->proxyModel->index(current.row() + 1, modelColumn(), rootIndex()); + const QRect nextRect = visualRect(next); + if (nextRect.top() == currentRect.top()) { + return next; + } + + return QModelIndex(); + } + case MoveDown: { + if (d->hasGrid() || uniformItemSizes()) { + const QModelIndex current = currentIndex(); + const QSize itemSize = d->hasGrid() ? gridSize() + : sizeHintForIndex(current); + const Private::Block &block = d->blocks[d->categoryForIndex(current)]; + const int maxItemsPerRow = qMax(d->viewportWidth() / itemSize.width(), 1); + const bool canMove = current.row() + maxItemsPerRow < block.firstIndex.row() + + block.items.count(); + + if (canMove) { + return d->proxyModel->index(current.row() + maxItemsPerRow, modelColumn(), rootIndex()); + } + + const int currentRelativePos = (current.row() - block.firstIndex.row()) % maxItemsPerRow; + const QModelIndex nextIndex = d->proxyModel->index(block.firstIndex.row() + block.items.count(), modelColumn(), rootIndex()); + + if (!nextIndex.isValid()) { + return QModelIndex(); + } + + const Private::Block &nextBlock = d->blocks[d->categoryForIndex(nextIndex)]; + + if (nextBlock.items.count() <= currentRelativePos) { + return QModelIndex(); + } + + if (currentRelativePos < (block.items.count() % maxItemsPerRow)) { + return d->proxyModel->index(nextBlock.firstIndex.row() + currentRelativePos, modelColumn(), rootIndex()); + } + + return QModelIndex(); + } + } + case MoveUp: { + if (d->hasGrid() || uniformItemSizes()) { + const QModelIndex current = currentIndex(); + const QSize itemSize = d->hasGrid() ? gridSize() + : sizeHintForIndex(current); + const Private::Block &block = d->blocks[d->categoryForIndex(current)]; + const int maxItemsPerRow = qMax(d->viewportWidth() / itemSize.width(), 1); + const bool canMove = current.row() - maxItemsPerRow >= block.firstIndex.row(); + + if (canMove) { + return d->proxyModel->index(current.row() - maxItemsPerRow, modelColumn(), rootIndex()); + } + + const int currentRelativePos = (current.row() - block.firstIndex.row()) % maxItemsPerRow; + const QModelIndex prevIndex = d->proxyModel->index(block.firstIndex.row() - 1, modelColumn(), rootIndex()); + + if (!prevIndex.isValid()) { + return QModelIndex(); + } + + const Private::Block &prevBlock = d->blocks[d->categoryForIndex(prevIndex)]; + + if (prevBlock.items.count() <= currentRelativePos) { + return QModelIndex(); + } + + const int remainder = prevBlock.items.count() % maxItemsPerRow; + if (currentRelativePos < remainder) { + return d->proxyModel->index(prevBlock.firstIndex.row() + prevBlock.items.count() - remainder + currentRelativePos, modelColumn(), rootIndex()); + } + + return QModelIndex(); + } + } + default: + break; + } + + return QModelIndex(); +} + +void KCategorizedView::rowsAboutToBeRemoved(const QModelIndex &parent, + int start, + int end) +{ + if (!d->isCategorized()) { + QListView::rowsAboutToBeRemoved(parent, start, end); + return; + } + + *d->hoveredBlock = Private::Block(); + d->hoveredCategory = QString(); + + if (end - start + 1 == d->proxyModel->rowCount()) { + d->blocks.clear(); + QListView::rowsAboutToBeRemoved(parent, start, end); + return; + } + + // Removal feels a bit more complicated than insertion. Basically we can consider there are + // 3 different cases when going to remove items. (*) represents an item, Items between ([) and + // (]) are the ones which are marked for removal. + // + // - 1st case: + // ... * * * * * * [ * * * ... + // + // The items marked for removal are the last part of this category. No need to mark any item + // of this category as in quarantine, because no special offset will be pushed to items at + // the right because of any changes (since the removed items are those on the right most part + // of the category). + // + // - 2nd case: + // ... * * * * * * ] * * * ... + // + // The items marked for removal are the first part of this category. We have to mark as in + // quarantine all items in this category. Absolutely all. All items will have to be moved to + // the left (or moving up, because rows got a different offset). + // + // - 3rd case: + // ... * * [ * * * * ] * * ... + // + // The items marked for removal are in between of this category. We have to mark as in + // quarantine only those items that are at the right of the end of the removal interval, + // (starting on "]"). + // + // It hasn't been explicitly said, but when we remove, we have to mark all blocks that are + // located under the top most affected category as in quarantine (the block itself, as a whole), + // because such a change can force it to have a different offset (note that items themselves + // contain relative positions to the block, so marking the block as in quarantine is enough). + // + // Also note that removal implicitly means that we have to update correctly firstIndex of each + // block, and in general keep updated the internal information of elements. + + QStringList listOfCategoriesMarkedForRemoval; + + QString lastCategory; + int alreadyRemoved = 0; + for (int i = start; i <= end; ++i) { + const QModelIndex index = d->proxyModel->index(i, modelColumn(), parent); + + Q_ASSERT(index.isValid()); + + const QString category = d->categoryForIndex(index); + + if (lastCategory != category) { + lastCategory = category; + alreadyRemoved = 0; + } + + Private::Block &block = d->blocks[category]; + block.items.removeAt(i - block.firstIndex.row() - alreadyRemoved); + ++alreadyRemoved; + + if (!block.items.count()) { + listOfCategoriesMarkedForRemoval << category; + } + + block.height = -1; + + viewport()->update(); + } + + //BEGIN: update the items that are in quarantine in affected categories + { + const QModelIndex lastIndex = d->proxyModel->index(end, modelColumn(), parent); + const QString category = d->categoryForIndex(lastIndex); + Private::Block &block = d->blocks[category]; + if (block.items.count() && start <= block.firstIndex.row() && end >= block.firstIndex.row()) { + block.firstIndex = d->proxyModel->index(end + 1, modelColumn(), parent); + } + block.quarantineStart = block.firstIndex; + } + //END: update the items that are in quarantine in affected categories + + Q_FOREACH (const QString &category, listOfCategoriesMarkedForRemoval) { + d->blocks.remove(category); + } + + //BEGIN: mark as in quarantine those categories that are under the affected ones + { + //BEGIN: order for marking as alternate those blocks that are alternate + QList blockList = d->blocks.values(); + qSort(blockList.begin(), blockList.end(), Private::Block::lessThan); + QList firstIndexesRows; + foreach (const Private::Block &block, blockList) { + firstIndexesRows << block.firstIndex.row(); + } + //END: order for marking as alternate those blocks that are alternate + for (QHash::Iterator it = d->blocks.begin(); it != d->blocks.end(); ++it) { + Private::Block &block = *it; + if (block.firstIndex.row() > start) { + block.outOfQuarantine = false; + block.alternate = firstIndexesRows.indexOf(block.firstIndex.row()) % 2; + } else if (block.firstIndex.row() == start) { + block.alternate = firstIndexesRows.indexOf(block.firstIndex.row()) % 2; + } + } + } + //END: mark as in quarantine those categories that are under the affected ones + + QListView::rowsAboutToBeRemoved(parent, start, end); +} + +void KCategorizedView::updateGeometries() +{ + const int oldVerticalOffset = verticalOffset(); + + QListView::updateGeometries(); + + if (!d->isCategorized()) { + return; + } + + const int rowCount = d->proxyModel->rowCount(); + if (!rowCount) { + verticalScrollBar()->setRange(0, 0); + return; + } + + const QModelIndex lastIndex = d->proxyModel->index(rowCount - 1, modelColumn(), rootIndex()); + Q_ASSERT(lastIndex.isValid()); + QRect lastItemRect = visualRect(lastIndex); + + if (d->hasGrid()) { + lastItemRect.setSize(lastItemRect.size().expandedTo(gridSize())); + } else { + if (uniformItemSizes()) { + QSize itemSize = sizeHintForIndex(lastIndex); + itemSize.setHeight(itemSize.height() + spacing()); + lastItemRect.setSize(itemSize); + } else { + QSize itemSize = sizeHintForIndex(lastIndex); + const QString category = d->categoryForIndex(lastIndex); + itemSize.setHeight(d->highestElementInLastRow(d->blocks[category]) + spacing()); + lastItemRect.setSize(itemSize); + } + } + + const int bottomRange = lastItemRect.bottomRight().y() + verticalOffset() - viewport()->height(); + + if (verticalScrollMode() == ScrollPerItem) { + verticalScrollBar()->setSingleStep(lastItemRect.height()); + const int rowsPerPage = qMax(viewport()->height() / lastItemRect.height(), 1); + verticalScrollBar()->setPageStep(rowsPerPage * lastItemRect.height()); + } + + verticalScrollBar()->setRange(0, bottomRange); + verticalScrollBar()->setValue(oldVerticalOffset); + + //TODO: also consider working with the horizontal scroll bar. since at this level I am not still + // supporting "top to bottom" flow, there is no real problem. If I support that someday + // (think how to draw categories), we would have to take care of the horizontal scroll bar too. + // In theory, as KCategorizedView has been designed, there is no need of horizontal scroll bar. + horizontalScrollBar()->setRange(0, 0); +} + +void KCategorizedView::currentChanged(const QModelIndex ¤t, + const QModelIndex &previous) +{ + QListView::currentChanged(current, previous); +} + +void KCategorizedView::dataChanged(const QModelIndex &topLeft, + const QModelIndex &bottomRight) +{ + QListView::dataChanged(topLeft, bottomRight); + if (!d->isCategorized()) { + return; + } + + *d->hoveredBlock = Private::Block(); + d->hoveredCategory = QString(); + + //BEGIN: since the model changed data, we need to reconsider item sizes + int i = topLeft.row(); + int indexToCheck = i; + QModelIndex categoryIndex; + QString category; + Private::Block *block; + while (i <= bottomRight.row()) { + const QModelIndex currIndex = d->proxyModel->index(i, modelColumn(), rootIndex()); + if (i == indexToCheck) { + categoryIndex = d->proxyModel->index(i, d->proxyModel->sortColumn(), rootIndex()); + category = categoryIndex.data(KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString(); + block = &d->blocks[category]; + block->quarantineStart = currIndex; + indexToCheck = block->firstIndex.row() + block->items.count(); + } + visualRect(currIndex); + ++i; + } + //END: since the model changed data, we need to reconsider item sizes +} + +void KCategorizedView::rowsInserted(const QModelIndex &parent, + int start, + int end) +{ + QListView::rowsInserted(parent, start, end); + if (!d->isCategorized()) { + return; + } + + *d->hoveredBlock = Private::Block(); + d->hoveredCategory = QString(); + d->rowsInserted(parent, start, end); +} + +void KCategorizedView::slotLayoutChanged() +{ + if (!d->isCategorized()) { + return; + } + + d->blocks.clear(); + *d->hoveredBlock = Private::Block(); + d->hoveredCategory = QString(); + if (d->proxyModel->rowCount()) { + d->rowsInserted(rootIndex(), 0, d->proxyModel->rowCount() - 1); + } +} + +//END: Public part diff --git a/src/widgets/kcategorizedview.h b/src/widgets/kcategorizedview.h new file mode 100644 index 000000000..8f5ad9a57 --- /dev/null +++ b/src/widgets/kcategorizedview.h @@ -0,0 +1,324 @@ +/** + * This file is part of the KDE project + * Copyright (C) 2007, 2009 Rafael Fernández López + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef KCATEGORIZEDVIEW_H +#define KCATEGORIZEDVIEW_H + +#include + +class KCategoryDrawer; +class KCategoryDrawerV2; + +/** + * @short Item view for listing items in a categorized fashion optionally + * + * KCategorizedView basically has the same functionality as QListView, only that it also lets you + * layout items in a way that they are categorized visually. + * + * For it to work you will need to set a KCategorizedSortFilterProxyModel and a KCategoryDrawer + * with methods setModel() and setCategoryDrawer() respectively. Also, the model will need to be + * flagged as categorized with KCategorizedSortFilterProxyModel::setCategorizedModel(true). + * + * The way it works (if categorization enabled): + * + * - When sorting, it does more things than QListView does. It will ask the model for the + * special role CategorySortRole (@see KCategorizedSortFilterProxyModel). This can return + * a QString or an int in order to tell the view the order of categories. In this sense, for + * instance, if we are sorting by name ascending, "A" would be before than "B". If we are + * sorting by size ascending, 512 bytes would be before 1024 bytes. This way categories are + * also sorted. + * + * - When the view has to paint, it will ask the model with the role CategoryDisplayRole + * (@see KCategorizedSortFilterProxyModel). It will for instance return "F" for "foo.pdf" if + * we are sorting by name ascending, or "Small" if a certain item has 100 bytes, for example. + * + * For drawing categories, KCategoryDrawer will be used. You can inherit this class to do your own + * drawing. + * + * @note All examples cited before talk about filesystems and such, but have present that this + * is a completely generic class, and it can be used for whatever your purpose is. For + * instance when talking about animals, you can separate them by "Mammal" and "Oviparous". In + * this very case, for example, the CategorySortRole and the CategoryDisplayRole could be the + * same ("Mammal" and "Oviparous"). + * + * @note There is a really performance boost if CategorySortRole returns an int instead of a QString. + * Have present that this role is asked (n * log n) times when sorting and compared. Comparing + * ints is always faster than comparing strings, whithout mattering how fast the string + * comparison is. Consider thinking of a way of returning ints instead of QStrings if your + * model can contain a high number of items. + * + * @warning Note that for really drawing items in blocks you will need some things to be done: + * - The model set to this view has to be (or inherit if you want to do special stuff + * in it) KCategorizedSortFilterProxyModel. + * - This model needs to be set setCategorizedModel to true. + * - Set a category drawer by calling setCategoryDrawer. + * + * @see KCategorizedSortFilterProxyModel, KCategoryDrawer + * + * @author Rafael Fernández López + */ +class KCategorizedView + : public QListView +{ + Q_OBJECT + Q_PROPERTY(int categorySpacing READ categorySpacing WRITE setCategorySpacing) + Q_PROPERTY(bool alternatingBlockColors READ alternatingBlockColors WRITE setAlternatingBlockColors) + Q_PROPERTY(bool collapsibleBlocks READ collapsibleBlocks WRITE setCollapsibleBlocks) + +public: + KCategorizedView(QWidget *parent = 0); + + ~KCategorizedView(); + + /** + * Reimplemented from QAbstractItemView. + */ + virtual void setModel(QAbstractItemModel *model); + + /** + * Calls to setGridSizeOwn(). + */ + void setGridSize(const QSize &size); + + /** + * @warning note that setGridSize is not virtual in the base class (QListView), so if you are + * calling to this method, make sure you have a KCategorizedView pointer around. This + * means that something like: + * @code + * QListView *lv = new KCategorizedView(); + * lv->setGridSize(mySize); + * @endcode + * + * will not call to the expected setGridSize method. Instead do something like this: + * + * @code + * QListView *lv; + * ... + * KCategorizedView *cv = qobject_cast(lv); + * if (cv) { + * cv->setGridSizeOwn(mySize); + * } else { + * lv->setGridSize(mySize); + * } + * @endcode + * + * @note this method will call to QListView::setGridSize among other operations. + * + * @since 4.4 + */ + void setGridSizeOwn(const QSize &size); + + /** + * Reimplemented from QAbstractItemView. + */ + virtual QRect visualRect(const QModelIndex &index) const; + + /** + * Returns the current category drawer. + */ + KCategoryDrawer *categoryDrawer() const; + + /** + * The category drawer that will be used for drawing categories. + */ + void setCategoryDrawer(KCategoryDrawer *categoryDrawer); + + /** + * @return Category spacing. The spacing between categories. + * + * @since 4.4 + */ + int categorySpacing() const; + + /** + * Stablishes the category spacing. This is the spacing between categories. + * + * @since 4.4 + */ + void setCategorySpacing(int categorySpacing); + + /** + * @return Whether blocks should be drawn with alternating colors. + * + * @since 4.4 + */ + bool alternatingBlockColors() const; + + /** + * Sets whether blocks should be drawn with alternating colors. + * + * @since 4.4 + */ + void setAlternatingBlockColors(bool enable); + + /** + * @return Whether blocks can be collapsed or not. + * + * @since 4.4 + */ + bool collapsibleBlocks() const; + + /** + * Sets whether blocks can be collapsed or not. + * + * @since 4.4 + */ + void setCollapsibleBlocks(bool enable); + + /** + * @return Block of indexes that are into @p category. + * + * @since 4.5 + */ + QModelIndexList block(const QString &category); + + /** + * @return Block of indexes that are represented by @p representative. + * + * @since 4.5 + */ + QModelIndexList block(const QModelIndex &representative); + + /** + * Reimplemented from QAbstractItemView. + */ + virtual QModelIndex indexAt(const QPoint &point) const; + + /** + * Reimplemented from QAbstractItemView. + */ + virtual void reset(); + +protected: + /** + * Reimplemented from QWidget. + */ + virtual void paintEvent(QPaintEvent *event); + + /** + * Reimplemented from QWidget. + */ + virtual void resizeEvent(QResizeEvent *event); + + /** + * Reimplemented from QAbstractItemView. + */ + virtual void setSelection(const QRect &rect, + QItemSelectionModel::SelectionFlags flags); + + /** + * Reimplemented from QWidget. + */ + virtual void mouseMoveEvent(QMouseEvent *event); + + /** + * Reimplemented from QWidget. + */ + virtual void mousePressEvent(QMouseEvent *event); + + /** + * Reimplemented from QWidget. + */ + virtual void mouseReleaseEvent(QMouseEvent *event); + + /** + * Reimplemented from QWidget. + */ + virtual void leaveEvent(QEvent *event); + + /** + * Reimplemented from QAbstractItemView. + */ + virtual void startDrag(Qt::DropActions supportedActions); + + /** + * Reimplemented from QAbstractItemView. + */ + virtual void dragMoveEvent(QDragMoveEvent *event); + + /** + * Reimplemented from QAbstractItemView. + */ + virtual void dragEnterEvent(QDragEnterEvent *event); + + /** + * Reimplemented from QAbstractItemView. + */ + virtual void dragLeaveEvent(QDragLeaveEvent *event); + + /** + * Reimplemented from QAbstractItemView. + */ + virtual void dropEvent(QDropEvent *event); + + /** + * Reimplemented from QAbstractItemView. + */ + virtual QModelIndex moveCursor(CursorAction cursorAction, + Qt::KeyboardModifiers modifiers); + + /** + * Reimplemented from QAbstractItemView. + */ + virtual void rowsAboutToBeRemoved(const QModelIndex &parent, + int start, + int end); + + /** + * Reimplemented from QAbstractItemView. + */ + virtual void updateGeometries(); + + /** + * Reimplemented from QAbstractItemView. + */ + virtual void currentChanged(const QModelIndex ¤t, + const QModelIndex &previous); + + /** + * Reimplemented from QAbstractItemView. + */ + virtual void dataChanged(const QModelIndex &topLeft, + const QModelIndex &bottomRight); + + /** + * Reimplemented from QAbstractItemView. + */ + virtual void rowsInserted(const QModelIndex &parent, + int start, + int end); + +protected Q_SLOTS: + /** + * @internal + * Reposition items as needed. + */ + virtual void slotLayoutChanged(); + +private: + class Private; + Private *const d; + + Q_PRIVATE_SLOT(d, void _k_slotCollapseOrExpandClicked(QModelIndex)) +}; + +#include "kcategorizedview_p.h" + +#endif // KCATEGORIZEDVIEW_H diff --git a/src/widgets/kcategorizedview_p.h b/src/widgets/kcategorizedview_p.h new file mode 100644 index 000000000..4c175fbb3 --- /dev/null +++ b/src/widgets/kcategorizedview_p.h @@ -0,0 +1,159 @@ +/** + * This file is part of the KDE project + * Copyright (C) 2007, 2009 Rafael Fernández López + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef KCATEGORIZEDVIEW_P_H +#define KCATEGORIZEDVIEW_P_H + +class KCategorizedSortFilterProxyModel; +class KCategoryDrawer; +class KCategoryDrawerV2; +class KCategoryDrawerV3; + +/** + * @internal + */ +class KCategorizedView::Private +{ +public: + struct Block; + struct Item; + + Private(KCategorizedView *q); + ~Private(); + + /** + * @return whether this view has all required elements to be categorized. + */ + bool isCategorized() const; + + /** + * @return the block rect for the representative @p representative. + */ + QStyleOptionViewItemV4 blockRect(const QModelIndex &representative); + + /** + * Returns the first and last element that intersects with rect. + * + * @note see that here we cannot take out items between first and last (as we could + * do with the rubberband). + * + * Complexity: O(log(n)) where n is model()->rowCount(). + */ + QPair intersectingIndexesWithRect(const QRect &rect) const; + + /** + * Returns the position of the block of @p category. + * + * Complexity: O(n) where n is the number of different categories when the block has been + * marked as in quarantine. O(1) the rest of the times (the vast majority). + */ + QPoint blockPosition(const QString &category); + + /** + * Returns the height of the block determined by @p category. + */ + int blockHeight(const QString &category); + + /** + * Returns the actual viewport width. + */ + int viewportWidth() const; + + /** + * Marks all elements as in quarantine. + * + * Complexity: O(n) where n is model()->rowCount(). + * + * @warning this is an expensive operation + */ + void regenerateAllElements(); + + /** + * Update internal information, and keep sync with the real information that the model contains. + */ + void rowsInserted(const QModelIndex &parent, int start, int end); + + /** + * Returns @p rect in viewport terms, taking in count horizontal and vertical offsets. + */ + QRect mapToViewport(const QRect &rect) const; + + /** + * Returns @p rect in absolute terms, converted from viewport position. + */ + QRect mapFromViewport(const QRect &rect) const; + + /** + * Returns the height of the highest element in last row. This is only applicable if there is + * no grid set and uniformItemSizes is false. + * + * @param block in which block are we searching. Necessary to stop the search if we hit the + * first item in this block. + */ + int highestElementInLastRow(const Block &block) const; + + /** + * Returns whether the view has a valid grid size. + */ + bool hasGrid() const; + + /** + * Returns the category for the given index. + */ + QString categoryForIndex(const QModelIndex &index) const; + + /** + * Updates the visual rect for item when flow is LeftToRight. + */ + void leftToRightVisualRect(const QModelIndex &index, Item &item, + const Block &block, const QPoint &blockPos) const; + + /** + * Updates the visual rect for item when flow is TopToBottom. + * @note we only support viewMode == ListMode in this case. + */ + void topToBottomVisualRect(const QModelIndex &index, Item &item, + const Block &block, const QPoint &blockPos) const; + + /** + * Called when expand or collapse has been clicked on the category drawer. + */ + void _k_slotCollapseOrExpandClicked(QModelIndex); + + KCategorizedView *q; + KCategorizedSortFilterProxyModel *proxyModel; + KCategoryDrawer *categoryDrawer; + KCategoryDrawerV2 *categoryDrawerV2; + KCategoryDrawerV3 *categoryDrawerV3; + int categorySpacing; + bool alternatingBlockColors; + bool collapsibleBlocks; + + Block *hoveredBlock; + QString hoveredCategory; + QModelIndex hoveredIndex; + + QPoint pressedPosition; + QRect rubberBandRect; + + QHash blocks; +}; + +#endif // KCATEGORIZEDVIEW_P_H diff --git a/src/widgets/kcategorydrawer.cpp b/src/widgets/kcategorydrawer.cpp new file mode 100644 index 000000000..5f5ce7822 --- /dev/null +++ b/src/widgets/kcategorydrawer.cpp @@ -0,0 +1,281 @@ +/** + * This file is part of the KDE project + * Copyright (C) 2007, 2009 Rafael Fernández López + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "kcategorydrawer.h" + +#include +#include +#include + +#include +#include + +#define HORIZONTAL_HINT 3 + +class KCategoryDrawer::Private +{ +public: + Private() + : leftMargin(0) + , rightMargin(0) + { + } + + ~Private() + { + } + + int leftMargin; + int rightMargin; +}; + +KCategoryDrawer::KCategoryDrawer() + : d(new Private) +{ + setLeftMargin(2); + setRightMargin(2); +} + +KCategoryDrawer::~KCategoryDrawer() +{ + delete d; +} + +void KCategoryDrawer::drawCategory(const QModelIndex &index, + int /*sortRole*/, + const QStyleOption &option, + QPainter *painter) const +{ + painter->setRenderHint(QPainter::Antialiasing); + + const QString category = index.model()->data(index, KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString(); + const QRect optRect = option.rect; + QFont font(QApplication::font()); + font.setBold(true); + const QFontMetrics fontMetrics = QFontMetrics(font); + + QColor outlineColor = option.palette.text().color(); + outlineColor.setAlphaF(0.35); + + //BEGIN: top left corner + { + painter->save(); + painter->setPen(outlineColor); + const QPointF topLeft(optRect.topLeft()); + QRectF arc(topLeft, QSizeF(4, 4)); + arc.translate(0.5, 0.5); + painter->drawArc(arc, 1440, 1440); + painter->restore(); + } + //END: top left corner + + //BEGIN: left vertical line + { + QPoint start(optRect.topLeft()); + start.ry() += 3; + QPoint verticalGradBottom(optRect.topLeft()); + verticalGradBottom.ry() += fontMetrics.height() + 5; + QLinearGradient gradient(start, verticalGradBottom); + gradient.setColorAt(0, outlineColor); + gradient.setColorAt(1, Qt::transparent); + painter->fillRect(QRect(start, QSize(1, fontMetrics.height() + 5)), gradient); + } + //END: left vertical line + + //BEGIN: horizontal line + { + QPoint start(optRect.topLeft()); + start.rx() += 3; + QPoint horizontalGradTop(optRect.topLeft()); + horizontalGradTop.rx() += optRect.width() - 6; + painter->fillRect(QRect(start, QSize(optRect.width() - 6, 1)), outlineColor); + } + //END: horizontal line + + //BEGIN: top right corner + { + painter->save(); + painter->setPen(outlineColor); + QPointF topRight(optRect.topRight()); + topRight.rx() -= 4; + QRectF arc(topRight, QSizeF(4, 4)); + arc.translate(0.5, 0.5); + painter->drawArc(arc, 0, 1440); + painter->restore(); + } + //END: top right corner + + //BEGIN: right vertical line + { + QPoint start(optRect.topRight()); + start.ry() += 3; + QPoint verticalGradBottom(optRect.topRight()); + verticalGradBottom.ry() += fontMetrics.height() + 5; + QLinearGradient gradient(start, verticalGradBottom); + gradient.setColorAt(0, outlineColor); + gradient.setColorAt(1, Qt::transparent); + painter->fillRect(QRect(start, QSize(1, fontMetrics.height() + 5)), gradient); + } + //END: right vertical line + + //BEGIN: text + { + QRect textRect(option.rect); + textRect.setTop(textRect.top() + 7); + textRect.setLeft(textRect.left() + 7); + textRect.setHeight(fontMetrics.height()); + textRect.setRight(textRect.right() - 7); + + painter->save(); + painter->setFont(font); + QColor penColor(option.palette.text().color()); + penColor.setAlphaF(0.6); + painter->setPen(penColor); + painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, category); + painter->restore(); + } + //END: text +} + +int KCategoryDrawer::categoryHeight(const QModelIndex &index, const QStyleOption &option) const +{ + Q_UNUSED(index); + Q_UNUSED(option) + + QFont font(QApplication::font()); + font.setBold(true); + QFontMetrics fontMetrics(font); + + const int height = fontMetrics.height() + 1 /* 1 pixel-width gradient */ + + 11 /* top and bottom separation */; + return height; +} + +int KCategoryDrawer::leftMargin() const +{ + return d->leftMargin; +} + +void KCategoryDrawer::setLeftMargin(int leftMargin) +{ + d->leftMargin = leftMargin; +} + +int KCategoryDrawer::rightMargin() const +{ + return d->rightMargin; +} + +void KCategoryDrawer::setRightMargin(int rightMargin) +{ + d->rightMargin = rightMargin; +} + +KCategoryDrawer &KCategoryDrawer::operator=(const KCategoryDrawer &cd) +{ + d->leftMargin = cd.d->leftMargin; + d->rightMargin = cd.d->rightMargin; + return *this; +} + +KCategoryDrawerV2::KCategoryDrawerV2(QObject *parent) + : QObject(parent) + , KCategoryDrawer() +{ +} + +KCategoryDrawerV2::~KCategoryDrawerV2() +{ +} + +void KCategoryDrawerV2::mouseButtonPressed(const QModelIndex&, QMouseEvent *event) +{ + event->ignore(); +} + +void KCategoryDrawerV2::mouseButtonReleased(const QModelIndex&, QMouseEvent *event) +{ + event->ignore(); +} + +void KCategoryDrawerV2::mouseButtonMoved(const QModelIndex&, QMouseEvent *event) +{ + event->ignore(); +} + +void KCategoryDrawerV2::mouseButtonDoubleClicked(const QModelIndex&, QMouseEvent *event) +{ + event->ignore(); +} + +class KCategoryDrawerV3::Private +{ +public: + Private(KCategorizedView *view) + : view(view) + { + } + + ~Private() + { + } + + KCategorizedView *view; +}; + +KCategoryDrawerV3::KCategoryDrawerV3(KCategorizedView *view) + : KCategoryDrawerV2(view) + , d(new Private(view)) +{ +} + +KCategoryDrawerV3::~KCategoryDrawerV3() +{ + delete d; +} + +KCategorizedView *KCategoryDrawerV3::view() const +{ + return d->view; +} + +void KCategoryDrawerV3::mouseButtonPressed(const QModelIndex&, const QRect&, QMouseEvent *event) +{ + event->ignore(); +} + +void KCategoryDrawerV3::mouseButtonReleased(const QModelIndex&, const QRect&, QMouseEvent *event) +{ + event->ignore(); +} + +void KCategoryDrawerV3::mouseMoved(const QModelIndex&, const QRect&, QMouseEvent *event) +{ + event->ignore(); +} + +void KCategoryDrawerV3::mouseButtonDoubleClicked(const QModelIndex&, const QRect&, QMouseEvent *event) +{ + event->ignore(); +} + +void KCategoryDrawerV3::mouseLeft(const QModelIndex&, const QRect&) +{ +} diff --git a/src/widgets/kcategorydrawer.h b/src/widgets/kcategorydrawer.h new file mode 100644 index 000000000..d69b1b93f --- /dev/null +++ b/src/widgets/kcategorydrawer.h @@ -0,0 +1,228 @@ +/** + * This file is part of the KDE project + * Copyright (C) 2007, 2009 Rafael Fernández López + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef KCATEGORYDRAWER_H +#define KCATEGORYDRAWER_H + +#include +#include + +class QPainter; +class QModelIndex; +class QStyleOption; + +class KCategorizedView; + +/** + * @deprecated + * + * The category drawing is performed by this class. It also gives information about the category + * height and margins. + * + * @warning Please use KCategoryDrawerV3 instead + */ +class KCategoryDrawer +{ +public: + KCategoryDrawer(); + + virtual ~KCategoryDrawer(); + + /** + * This method purpose is to draw a category represented by the given + * @param index with the given @param sortRole sorting role + * + * @note This method will be called one time per category, always with the + * first element in that category + */ + virtual void drawCategory(const QModelIndex &index, + int sortRole, + const QStyleOption &option, + QPainter *painter) const; + + /** + * @return The category height for the category representated by index @p index with + * style options @p option. + */ + virtual int categoryHeight(const QModelIndex &index, const QStyleOption &option) const; + + //TODO KDE5: make virtual as leftMargin + /** + * @note 0 by default + * + * @since 4.4 + */ + int leftMargin() const; + + /** + * @note call to this method on the KCategoryDrawer constructor to set the left margin + * + * @since 4.4 + */ + void setLeftMargin(int leftMargin); + + //TODO KDE5: make virtual as rightMargin + /** + * @note 0 by default + * + * @since 4.4 + */ + int rightMargin() const; + + /** + * @note call to this method on the KCategoryDrawer constructor to set the right margin + * + * @since 4.4 + */ + void setRightMargin(int rightMargin); + + KCategoryDrawer &operator=(const KCategoryDrawer &cd); + +private: + class Private; + Private *const d; +}; + + +/** + * @deprecated + * + * @since 4.4 + * + * @warning Please use KCategoryDrawerV3 instead + */ +class KCategoryDrawerV2 + : public QObject + , public KCategoryDrawer +{ + Q_OBJECT + +public: + KCategoryDrawerV2(QObject *parent = 0); + virtual ~KCategoryDrawerV2(); + + virtual void mouseButtonPressed(const QModelIndex &index, QMouseEvent *event); + + virtual void mouseButtonReleased(const QModelIndex &index, QMouseEvent *event); + + virtual void mouseButtonMoved(const QModelIndex &index, QMouseEvent *event); + + virtual void mouseButtonDoubleClicked(const QModelIndex &index, QMouseEvent *event); + +Q_SIGNALS: + /** + * This signal becomes emitted when collapse or expand has been clicked. + */ + void collapseOrExpandClicked(const QModelIndex &index); + + /** + * Emit this signal on your subclass implementation to notify that something happened. Usually + * this will be triggered when you have received an event, and its position matched some "hot spot". + * + * You give this action the integer you want, and having connected this signal to your code, + * the connected slot can perform the needed changes (view, model, selection model, delegate...) + */ + void actionRequested(int action, const QModelIndex &index); +}; + +/** + * @since 4.5 + */ +class KCategoryDrawerV3 + : public KCategoryDrawerV2 +{ + friend class KCategorizedView; + +public: + KCategoryDrawerV3(KCategorizedView *view); + virtual ~KCategoryDrawerV3(); + + /** + * @return The view this category drawer is associated with. + */ + KCategorizedView *view() const; + + using KCategoryDrawerV2::mouseButtonPressed; + using KCategoryDrawerV2::mouseButtonReleased; + using KCategoryDrawerV2::mouseButtonDoubleClicked; + +protected: + /** + * Method called when the mouse button has been pressed. + * + * @param index The representative index of the block of items. + * @param blockRect The rect occupied by the block of items. + * @param event The mouse event. + * + * @warning You explicitly have to determine whether the event has been accepted or not. You + * have to call event->accept() or event->ignore() at all possible case branches in + * your code. + */ + virtual void mouseButtonPressed(const QModelIndex &index, const QRect &blockRect, QMouseEvent *event); + + /** + * Method called when the mouse button has been released. + * + * @param index The representative index of the block of items. + * @param blockRect The rect occupied by the block of items. + * @param event The mouse event. + * + * @warning You explicitly have to determine whether the event has been accepted or not. You + * have to call event->accept() or event->ignore() at all possible case branches in + * your code. + */ + virtual void mouseButtonReleased(const QModelIndex &index, const QRect &blockRect, QMouseEvent *event); + + /** + * Method called when the mouse has been moved. + * + * @param index The representative index of the block of items. + * @param blockRect The rect occupied by the block of items. + * @param event The mouse event. + */ + virtual void mouseMoved(const QModelIndex &index, const QRect &blockRect, QMouseEvent *event); + + /** + * Method called when the mouse button has been double clicked. + * + * @param index The representative index of the block of items. + * @param blockRect The rect occupied by the block of items. + * @param event The mouse event. + * + * @warning You explicitly have to determine whether the event has been accepted or not. You + * have to call event->accept() or event->ignore() at all possible case branches in + * your code. + */ + virtual void mouseButtonDoubleClicked(const QModelIndex &index, const QRect &blockRect, QMouseEvent *event); + + /** + * Method called when the mouse button has left this block. + * + * @param index The representative index of the block of items. + * @param blockRect The rect occupied by the block of items. + */ + virtual void mouseLeft(const QModelIndex &index, const QRect &blockRect); + +private: + class Private; + Private *const d; +}; + +#endif // KCATEGORYDRAWER_H