From: Peter Penz Date: Fri, 21 Oct 2011 23:31:02 +0000 (+0200) Subject: Implement grouping for names X-Git-Url: https://cloud.milkyroute.net/gitweb/dolphin.git/commitdiff_plain/eac436d0374d3be323c6ba36799d3cddc98301ea Implement grouping for names - Use a custom header for KFileItems - Cache the groups - Allow enabling/disabling grouping dynamically Currently there is a random crash in combination with the groupheader-recycler, this will be fixed during the next days. --- diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 93225c5c8..b5c773ae6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -18,6 +18,7 @@ add_subdirectory(tests) ########### next target ############### set(dolphinprivate_LIB_SRCS + kitemviews/kfileitemlistgroupheader.cpp kitemviews/kfileitemlistview.cpp kitemviews/kfileitemlistwidget.cpp kitemviews/kfileitemmodel.cpp diff --git a/src/kitemviews/kfileitemlistgroupheader.cpp b/src/kitemviews/kfileitemlistgroupheader.cpp new file mode 100644 index 000000000..969bd925b --- /dev/null +++ b/src/kitemviews/kfileitemlistgroupheader.cpp @@ -0,0 +1,43 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz * + * * + * Based on the Itemviews NG project from Trolltech Labs: * + * http://qt.gitorious.org/qt-labs/itemviews-ng * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * + ***************************************************************************/ + +#include "kfileitemlistgroupheader.h" + +#include + +KFileItemListGroupHeader::KFileItemListGroupHeader(QGraphicsWidget* parent) : + KItemListGroupHeader(parent) +{ +} + +KFileItemListGroupHeader::~KFileItemListGroupHeader() +{ +} + +void KFileItemListGroupHeader::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) +{ + KItemListGroupHeader::paint(painter, option, widget); + // TODO: Use dataChanged() hook to prepare a cached property + painter->drawText(QRectF(0, 0, size().width(), size().height()), data().toString()); +} + +#include "kfileitemlistgroupheader.moc" diff --git a/src/kitemviews/kfileitemlistgroupheader.h b/src/kitemviews/kfileitemlistgroupheader.h new file mode 100644 index 000000000..3c23f13d4 --- /dev/null +++ b/src/kitemviews/kfileitemlistgroupheader.h @@ -0,0 +1,40 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * + ***************************************************************************/ + +#ifndef KFILEITEMLISTGROUPHEADER_H +#define KFILEITEMLISTGROUPHEADER_H + +#include + +#include + +class LIBDOLPHINPRIVATE_EXPORT KFileItemListGroupHeader : public KItemListGroupHeader +{ + Q_OBJECT + +public: + KFileItemListGroupHeader(QGraphicsWidget* parent = 0); + virtual ~KFileItemListGroupHeader(); + + /** @reimp */ + virtual void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget = 0); +}; +#endif + + diff --git a/src/kitemviews/kfileitemlistview.cpp b/src/kitemviews/kfileitemlistview.cpp index a3cf88290..783af5a26 100644 --- a/src/kitemviews/kfileitemlistview.cpp +++ b/src/kitemviews/kfileitemlistview.cpp @@ -19,7 +19,7 @@ #include "kfileitemlistview.h" -#include "kitemlistgroupheader.h" +#include "kfileitemlistgroupheader.h" #include "kfileitemmodelrolesupdater.h" #include "kfileitemlistwidget.h" #include "kfileitemmodel.h" @@ -51,7 +51,7 @@ KFileItemListView::KFileItemListView(QGraphicsWidget* parent) : setScrollOrientation(Qt::Vertical); setWidgetCreator(new KItemListWidgetCreator()); - setGroupHeaderCreator(new KItemListGroupHeaderCreator()); + setGroupHeaderCreator(new KItemListGroupHeaderCreator()); m_updateVisibleIndexRangeTimer = new QTimer(this); m_updateVisibleIndexRangeTimer->setSingleShot(true); diff --git a/src/kitemviews/kfileitemmodel.cpp b/src/kitemviews/kfileitemmodel.cpp index 46de7c5aa..da2126aa3 100644 --- a/src/kitemviews/kfileitemmodel.cpp +++ b/src/kitemviews/kfileitemmodel.cpp @@ -45,6 +45,7 @@ KFileItemModel::KFileItemModel(KDirLister* dirLister, QObject* parent) : m_maximumUpdateIntervalTimer(0), m_pendingItemsToInsert(), m_pendingEmitLoadingCompleted(false), + m_groups(), m_rootExpansionLevel(-1), m_expandedUrls(), m_restoredExpandedUrls() @@ -225,20 +226,34 @@ QString KFileItemModel::roleDescription(const QByteArray& role) const QList > KFileItemModel::groups() const { - // TODO: dirty hack for initial testing of grouping functionality - QPair group1(0, "Group 1"); - QPair group2(2, "Group 2"); - QPair group3(10, "Group 3"); - QPair group4(11, "Group 4"); - QPair group5(40, "Group 5"); + if (!m_data.isEmpty() && m_groups.isEmpty()) { +#ifdef KFILEITEMMODEL_DEBUG + QElapsedTimer timer; + timer.start(); +#endif + switch (roleIndex(sortRole())) { + case NameRole: m_groups = nameRoleGroups(); break; + case SizeRole: m_groups = sizeRoleGroups(); break; + case DateRole: m_groups = dateRoleGroups(); break; + case PermissionsRole: m_groups = permissionRoleGroups(); break; + case OwnerRole: m_groups = ownerRoleGroups(); break; + case GroupRole: m_groups = groupRoleGroups(); break; + case TypeRole: m_groups = typeRoleGroups(); break; + case DestinationRole: m_groups = destinationRoleGroups(); break; + case PathRole: m_groups = pathRoleGroups(); break; + case NoRole: break; + case IsDirRole: break; + case IsExpandedRole: break; + case ExpansionLevelRole: break; + default: Q_ASSERT(false); break; + } - QList > groups; - groups.append(group1); - groups.append(group2); - groups.append(group3); - groups.append(group4); - groups.append(group5); - return groups; +#ifdef KFILEITEMMODEL_DEBUG + kDebug() << "[TIME] Calculating groups for" << count() << "items:" << timer.elapsed(); +#endif + } + + return m_groups; } KFileItem KFileItemModel::fileItem(int index) const @@ -413,6 +428,7 @@ void KFileItemModel::restoreExpandedUrls(const QSet& urls) void KFileItemModel::onGroupedSortingChanged(bool current) { Q_UNUSED(current); + m_groups.clear(); } void KFileItemModel::onSortRoleChanged(const QByteArray& current, const QByteArray& previous) @@ -500,6 +516,8 @@ void KFileItemModel::slotRefreshItems(const QList >& kDebug() << "Refreshing" << items.count() << "items"; #endif + m_groups.clear(); + // Get the indexes of all items that have been refreshed QList indexes; indexes.reserve(items.count()); @@ -554,6 +572,8 @@ void KFileItemModel::slotClear() kDebug() << "Clearing all items"; #endif + m_groups.clear(); + m_minimumUpdateIntervalTimer->stop(); m_maximumUpdateIntervalTimer->stop(); m_pendingItemsToInsert.clear(); @@ -601,6 +621,8 @@ void KFileItemModel::insertItems(const KFileItemList& items) kDebug() << "Inserting" << items.count() << "items"; #endif + m_groups.clear(); + KFileItemList sortedItems = items; sort(sortedItems.begin(), sortedItems.end()); @@ -671,6 +693,8 @@ void KFileItemModel::removeItems(const KFileItemList& items) kDebug() << "Removing " << items.count() << "items"; #endif + m_groups.clear(); + KFileItemList sortedItems = items; sort(sortedItems.begin(), sortedItems.end()); @@ -738,6 +762,8 @@ void KFileItemModel::resortAllItems() return; } + m_groups.clear(); + const KFileItemList oldSortedItems = m_sortedItems; const QHash oldItems = m_items; const QList > oldData = m_data; @@ -1131,4 +1157,110 @@ bool KFileItemModel::useMaximumUpdateInterval() const return dirLister && !dirLister->url().isLocalFile(); } +QList > KFileItemModel::nameRoleGroups() const +{ + Q_ASSERT(!m_data.isEmpty()); + + const int maxIndex = count() - 1; + QList > groups; + + QString groupValue; + QChar firstChar; + bool isLetter = false; + for (int i = 0; i <= maxIndex; ++i) { + const QString name = m_data.at(i).value("name").toString(); + + // Use the first character of the name as group indication + QChar newFirstChar = name.at(0).toUpper(); + if (newFirstChar == QLatin1Char('~') && name.length() > 1) { + newFirstChar = name.at(1); + } + + if (firstChar != newFirstChar) { + QString newGroupValue; + if (newFirstChar >= QLatin1Char('A') && newFirstChar <= QLatin1Char('Z')) { + // Apply group 'A' - 'Z' + newGroupValue = newFirstChar; + isLetter = true; + } else if (newFirstChar >= QLatin1Char('0') && newFirstChar <= QLatin1Char('9')) { + // Apply group 'Numerics' for any name that starts with a digit + newGroupValue = i18nc("@title:group", "Numerics"); + isLetter = false; + } else { + if (isLetter) { + // If the current group is 'A' - 'Z' check whether a locale character + // fits into the existing group. + const QChar prevChar(firstChar.unicode() - ushort(1)); + const QChar nextChar(firstChar.unicode() + ushort(1)); + const QString currChar(newFirstChar); + const bool partOfCurrentGroup = currChar.localeAwareCompare(prevChar) > 0 && + currChar.localeAwareCompare(nextChar) < 0; + if (partOfCurrentGroup) { + continue; + } + } + newGroupValue = i18nc("@title:group", "Others"); + isLetter = false; + } + + if (newGroupValue != groupValue) { + groupValue = newGroupValue; + groups.append(QPair(i, newGroupValue)); + } + + firstChar = newFirstChar; + } + } + return groups; +} + +QList > KFileItemModel::sizeRoleGroups() const +{ + Q_ASSERT(!m_data.isEmpty()); + + return QList >(); +} + +QList > KFileItemModel::dateRoleGroups() const +{ + Q_ASSERT(!m_data.isEmpty()); + return QList >(); +} + +QList > KFileItemModel::permissionRoleGroups() const +{ + Q_ASSERT(!m_data.isEmpty()); + return QList >(); +} + +QList > KFileItemModel::ownerRoleGroups() const +{ + Q_ASSERT(!m_data.isEmpty()); + return QList >(); +} + +QList > KFileItemModel::groupRoleGroups() const +{ + Q_ASSERT(!m_data.isEmpty()); + return QList >(); +} + +QList > KFileItemModel::typeRoleGroups() const +{ + Q_ASSERT(!m_data.isEmpty()); + return QList >(); +} + +QList > KFileItemModel::destinationRoleGroups() const +{ + Q_ASSERT(!m_data.isEmpty()); + return QList >(); +} + +QList > KFileItemModel::pathRoleGroups() const +{ + Q_ASSERT(!m_data.isEmpty()); + return QList >(); +} + #include "kfileitemmodel.moc" diff --git a/src/kitemviews/kfileitemmodel.h b/src/kitemviews/kfileitemmodel.h index 7d10aad8e..8ccff95e8 100644 --- a/src/kitemviews/kfileitemmodel.h +++ b/src/kitemviews/kfileitemmodel.h @@ -194,6 +194,16 @@ private: bool useMaximumUpdateInterval() const; + QList > nameRoleGroups() const; + QList > sizeRoleGroups() const; + QList > dateRoleGroups() const; + QList > permissionRoleGroups() const; + QList > ownerRoleGroups() const; + QList > groupRoleGroups() const; + QList > typeRoleGroups() const; + QList > destinationRoleGroups() const; + QList > pathRoleGroups() const; + private: QWeakPointer m_dirLister; @@ -214,6 +224,9 @@ private: KFileItemList m_pendingItemsToInsert; bool m_pendingEmitLoadingCompleted; + // Cache for KFileItemModel::groups() + mutable QList > m_groups; + // Stores the smallest expansion level of the root-URL. Is required to calculate // the "expansionLevel" role in an efficient way. A value < 0 indicates that // it has not been initialized yet. diff --git a/src/kitemviews/kitemlistgroupheader.cpp b/src/kitemviews/kitemlistgroupheader.cpp index 4db253293..5aac02c82 100644 --- a/src/kitemviews/kitemlistgroupheader.cpp +++ b/src/kitemviews/kitemlistgroupheader.cpp @@ -29,7 +29,9 @@ #include KItemListGroupHeader::KItemListGroupHeader(QGraphicsWidget* parent) : - QGraphicsWidget(parent, 0) + QGraphicsWidget(parent, 0), + m_role(), + m_data() { } @@ -37,6 +39,36 @@ KItemListGroupHeader::~KItemListGroupHeader() { } +void KItemListGroupHeader::setRole(const QByteArray& role) +{ + if (m_role != role) { + const QByteArray previous = m_role; + m_role = role; + update(); + roleChanged(role, previous); + } +} + +QByteArray KItemListGroupHeader::role() const +{ + return m_role; +} + +void KItemListGroupHeader::setData(const QVariant& data) +{ + if (m_data != data) { + const QVariant previous = m_data; + m_data = data; + update(); + dataChanged(data, previous); + } +} + +QVariant KItemListGroupHeader::data() const +{ + return m_data; +} + QSizeF KItemListGroupHeader::sizeHint(Qt::SizeHint which, const QSizeF& constraint) const { Q_UNUSED(which); @@ -55,4 +87,16 @@ void KItemListGroupHeader::paint(QPainter* painter, const QStyleOptionGraphicsIt //painter->drawText(rect(), QString::number(m_index)); } +void KItemListGroupHeader::roleChanged(const QByteArray& current, const QByteArray& previous) +{ + Q_UNUSED(current); + Q_UNUSED(previous); +} + +void KItemListGroupHeader::dataChanged(const QVariant& current, const QVariant& previous) +{ + Q_UNUSED(current); + Q_UNUSED(previous); +} + #include "kitemlistgroupheader.moc" diff --git a/src/kitemviews/kitemlistgroupheader.h b/src/kitemviews/kitemlistgroupheader.h index 135fd5e5f..081607eef 100644 --- a/src/kitemviews/kitemlistgroupheader.h +++ b/src/kitemviews/kitemlistgroupheader.h @@ -22,7 +22,9 @@ #include +#include #include +#include class KItemListView; @@ -34,11 +36,23 @@ public: KItemListGroupHeader(QGraphicsWidget* parent = 0); virtual ~KItemListGroupHeader(); - void setIndex(int index); - int index() const; + void setRole(const QByteArray& role); + QByteArray role() const; + + void setData(const QVariant& data); + QVariant data() const; virtual QSizeF sizeHint(Qt::SizeHint which = Qt::PreferredSize, const QSizeF& constraint = QSizeF()) const; virtual void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget = 0); + +protected: + virtual void roleChanged(const QByteArray& current, const QByteArray& previous); + virtual void dataChanged(const QVariant& current, const QVariant& previous); + +private: + QByteArray m_role; + QVariant m_data; + }; #endif diff --git a/src/kitemviews/kitemlistview.cpp b/src/kitemviews/kitemlistview.cpp index eb464a9e7..a1012bfb2 100644 --- a/src/kitemviews/kitemlistview.cpp +++ b/src/kitemviews/kitemlistview.cpp @@ -847,6 +847,27 @@ void KItemListView::slotItemsChanged(const KItemRangeList& itemRanges, void KItemListView::slotGroupedSortingChanged(bool current) { m_grouped = current; + if (m_grouped) { + // Assure that headers from already visible items get created + QHashIterator it(m_visibleItems); + while (it.hasNext()) { + it.next(); + KItemListWidget* widget = it.value(); + updateGroupHeaderForWidget(widget); + } + } else { + // Clear all visible headers + QHashIterator it (m_visibleGroups); + while (it.hasNext()) { + it.next(); + KItemListGroupHeader* header = it.value(); + m_groupHeaderCreator->recycle(header); + } + m_visibleGroups.clear(); + } + + m_layouter->markAsDirty(); + updateLayout(); } void KItemListView::slotCurrentChanged(int current, int previous) @@ -1151,8 +1172,6 @@ void KItemListView::doLayout(LayoutAnimationHint hint, int changedIndex, int cha return; } - //markVisibleRolesSizesAsDirty(); - const int firstVisibleIndex = m_layouter->firstVisibleIndex(); const int lastVisibleIndex = m_layouter->lastVisibleIndex(); if (firstVisibleIndex < 0) { @@ -1315,14 +1334,7 @@ KItemListWidget* KItemListView::createWidget(int index) m_visibleItems.insert(index, widget); if (m_grouped) { - if (m_layouter->isFirstGroupItem(index)) { - KItemListGroupHeader* header = m_groupHeaderCreator->create(widget); - header->show(); - // TODO: - header->setPos(0, -50); - header->resize(50, 50); - m_visibleGroups.insert(widget, header); - } + updateGroupHeaderForWidget(widget); } initializeItemListWidget(widget); @@ -1345,33 +1357,15 @@ void KItemListView::recycleWidget(KItemListWidget* widget) void KItemListView::setWidgetIndex(KItemListWidget* widget, int index) { - if (m_grouped) { - bool createHeader = m_layouter->isFirstGroupItem(index); - KItemListGroupHeader* header = m_visibleGroups.value(widget); - if (header) { - if (createHeader) { - createHeader = false; - } else { - m_groupHeaderCreator->recycle(header); - m_visibleGroups.remove(widget); - } - } - - if (createHeader) { - KItemListGroupHeader* header = m_groupHeaderCreator->create(widget); - header->show(); - // TODO: - header->setPos(0, -50); - header->resize(50, 50); - m_visibleGroups.insert(widget, header); - } - } - const int oldIndex = widget->index(); m_visibleItems.remove(oldIndex); updateWidgetProperties(widget, index); m_visibleItems.insert(index, widget); + if (m_grouped) { + updateGroupHeaderForWidget(widget); + } + initializeItemListWidget(widget); } @@ -1444,6 +1438,48 @@ void KItemListView::updateWidgetProperties(KItemListWidget* widget, int index) widget->setData(m_model->data(index)); } +void KItemListView::updateGroupHeaderForWidget(KItemListWidget* widget) +{ + const int index = widget->index(); + KItemListGroupHeader* header = m_visibleGroups.value(widget); + if (!m_layouter->isFirstGroupItem(index)) { + // The widget does not represent the first item of a group + // and hence requires no header + if (header) { + m_groupHeaderCreator->recycle(header); + m_visibleGroups.remove(widget); + } + return; + } + + if (!header) { + header = m_groupHeaderCreator->create(widget); + m_visibleGroups.insert(widget, header); + } + + // TODO: + header->show(); + header->setPos(0, -50); + header->resize(200, 50); + header->setRole(model()->sortRole()); + + // Determine the shown data for the header by doing a binary + // search in the groups-list + const QList > groups = model()->groups(); + int min = 0; + int max = groups.count() - 1; + int mid = 0; + do { + mid = (min + max) / 2; + if (index > groups.at(mid).first) { + min = mid + 1; + } else { + max = mid - 1; + } + } while (groups.at(mid).first != index && min <= max); + header->setData(groups.at(mid).second); +} + QHash KItemListView::headerRolesWidths() const { QHash rolesWidths; diff --git a/src/kitemviews/kitemlistview.h b/src/kitemviews/kitemlistview.h index 8a26a1535..ee84abe1c 100644 --- a/src/kitemviews/kitemlistview.h +++ b/src/kitemviews/kitemlistview.h @@ -329,6 +329,12 @@ private: */ void updateWidgetProperties(KItemListWidget* widget, int index); + /** + * Helper method for createWidget() and setWidgetIndex() to create or update + * the itemlist groupheader. + */ + void updateGroupHeaderForWidget(KItemListWidget* widget); + /** * @return The widths of each visible role that is shown in the KItemListHeader. */