diff --git a/libtaskmanager/CMakeLists.txt b/libtaskmanager/CMakeLists.txt --- a/libtaskmanager/CMakeLists.txt +++ b/libtaskmanager/CMakeLists.txt @@ -5,6 +5,7 @@ abstracttasksmodel.cpp activityinfo.cpp concatenatetasksproxymodel.cpp + flattentaskgroupsproxymodel.cpp launchertasksmodel.cpp startuptasksmodel.cpp taskfilterproxymodel.cpp @@ -34,10 +35,10 @@ Qt5::Core Qt5::Gui Qt5::Quick + KF5::ItemModels KF5::WaylandClient PRIVATE KF5::Activities - KF5::ItemModels KF5::ConfigCore KF5::KIOCore KF5::KIOWidgets @@ -64,6 +65,7 @@ abstracttasksmodeliface.h activityinfo.h concatenatetasksproxymodel.h + flattentaskgroupsproxymodel.h launchertasksmodel.h startuptasksmodel.h taskfilterproxymodel.h diff --git a/libtaskmanager/flattentaskgroupsproxymodel.h b/libtaskmanager/flattentaskgroupsproxymodel.h new file mode 100644 --- /dev/null +++ b/libtaskmanager/flattentaskgroupsproxymodel.h @@ -0,0 +1,191 @@ +/******************************************************************** +Copyright 2016 Eike Hein + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) version 3, or any +later version accepted by the membership of KDE e.V. (or its +successor approved by the membership of KDE e.V.), which shall +act as a proxy defined in Section 6 of version 3 of the license. + +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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ + +#ifndef FLATTENTASKGROUPSPROXYMODEL_H +#define FLATTENTASKGROUPSPROXYMODEL_H + +#include "abstracttasksmodeliface.h" + +#include + +#include "taskmanager_export.h" + +namespace TaskManager +{ + +/** + * @short A proxy tasks model for flattening a tree-structured tasks model + * into a list-structured tasks model. + * + * This proxy model is a subclass of KDescendantsProxyModel implementing + * AbstractTasksModelIface. + * + * @author Eike Hein + **/ + +class TASKMANAGER_EXPORT FlattenTaskGroupsProxyModel : public KDescendantsProxyModel, + public AbstractTasksModelIface +{ + Q_OBJECT + +public: + explicit FlattenTaskGroupsProxyModel(QObject *parent = 0); + virtual ~FlattenTaskGroupsProxyModel(); + + void setSourceModel(QAbstractItemModel *sourceModel) override; + + /** + * Request activation of the task at the given index. Derived classes are + * free to interpret the meaning of "activate" themselves depending on + * the nature and state of the task, e.g. launch or raise a window task. + * + * @param index An index in this tasks model. + **/ + void requestActivate(const QModelIndex &index); + + /** + * Request an additional instance of the application backing the task + * at the given index. + * + * @param index An index in this tasks model. + **/ + void requestNewInstance(const QModelIndex &index); + + /** + * Request the task at the given index be closed. + * + * @param index An index in this tasks model. + **/ + void requestClose(const QModelIndex &index); + + /** + * Request starting an interactive move for the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + void requestMove(const QModelIndex &index); + + /** + * Request starting an interactive resize for the task at the given index. + * + * This is meant for tasks that have an associated window, and may be a + * no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + void requestResize(const QModelIndex &index); + + /** + * Request toggling the minimized state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + void requestToggleMinimized(const QModelIndex &index); + + /** + * Request toggling the maximized state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + void requestToggleMaximized(const QModelIndex &index); + + /** + * Request toggling the keep-above state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + void requestToggleKeepAbove(const QModelIndex &index); + + /** + * Request toggling the keep-below state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + void requestToggleKeepBelow(const QModelIndex &index); + + /** + * Request toggling the fullscreen state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + void requestToggleFullScreen(const QModelIndex &index); + + /** + * Request toggling the shaded state of the task at the given index. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + **/ + void requestToggleShaded(const QModelIndex &index); + + /** + * Request moving the task at the given index to the specified virtual + * desktop. + * + * This is meant for tasks that have an associated window, and may be + * a no-op when there is no window. + * + * @param index An index in this tasks model. + * @param desktop A virtual desktop number. + **/ + void requestVirtualDesktop(const QModelIndex &index, qint32 desktop); + + /** + * Request informing the window manager of new geometry for a visual + * delegate for the task at the given index. The geometry should be in + * screen coordinates. + * + * @param index An index in this tasks model. + * @param geometry Visual delegate geometry in screen coordinates. + * @param delegate The delegate. Implementations are on their own with + * regard to extracting information from this, and should take care to + * reject invalid objects. + **/ + void requestPublishDelegateGeometry(const QModelIndex &index, const QRect &geometry, + QObject *delegate = nullptr); + +private: + class Private; + QScopedPointer d; +}; + +} + +#endif diff --git a/libtaskmanager/flattentaskgroupsproxymodel.cpp b/libtaskmanager/flattentaskgroupsproxymodel.cpp new file mode 100644 --- /dev/null +++ b/libtaskmanager/flattentaskgroupsproxymodel.cpp @@ -0,0 +1,150 @@ +/******************************************************************** +Copyright 2016 Eike Hein + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) version 3, or any +later version accepted by the membership of KDE e.V. (or its +successor approved by the membership of KDE e.V.), which shall +act as a proxy defined in Section 6 of version 3 of the license. + +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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ + +#include "flattentaskgroupsproxymodel.h" + +namespace TaskManager +{ + +class FlattenTaskGroupsProxyModel::Private +{ +public: + Private(FlattenTaskGroupsProxyModel *q); + + AbstractTasksModelIface *sourceTasksModel = nullptr; + +private: + FlattenTaskGroupsProxyModel *q; +}; + +FlattenTaskGroupsProxyModel::Private::Private(FlattenTaskGroupsProxyModel *q) + : q(q) +{ +} + +FlattenTaskGroupsProxyModel::FlattenTaskGroupsProxyModel(QObject *parent) + : KDescendantsProxyModel(parent) + , d(new Private(this)) +{ +} + +FlattenTaskGroupsProxyModel::~FlattenTaskGroupsProxyModel() +{ +} + +void FlattenTaskGroupsProxyModel::setSourceModel(QAbstractItemModel *sourceModel) +{ + d->sourceTasksModel = dynamic_cast(sourceModel); + + KDescendantsProxyModel::setSourceModel(sourceModel); +} + +void FlattenTaskGroupsProxyModel::requestActivate(const QModelIndex &index) +{ + if (d->sourceTasksModel && index.isValid() && index.model() == this) { + d->sourceTasksModel->requestActivate(mapToSource(index)); + } +} + +void FlattenTaskGroupsProxyModel::requestNewInstance(const QModelIndex &index) +{ + if (d->sourceTasksModel && index.isValid() && index.model() == this) { + d->sourceTasksModel->requestNewInstance(mapToSource(index)); + } +} + +void FlattenTaskGroupsProxyModel::requestClose(const QModelIndex &index) +{ + if (d->sourceTasksModel && index.isValid() && index.model() == this) { + d->sourceTasksModel->requestClose(mapToSource(index)); + } +} + +void FlattenTaskGroupsProxyModel::requestMove(const QModelIndex &index) +{ + if (d->sourceTasksModel && index.isValid() && index.model() == this) { + d->sourceTasksModel->requestMove(mapToSource(index)); + } +} + +void FlattenTaskGroupsProxyModel::requestResize(const QModelIndex &index) +{ + if (d->sourceTasksModel && index.isValid() && index.model() == this) { + d->sourceTasksModel->requestResize(mapToSource(index)); + } +} + +void FlattenTaskGroupsProxyModel::requestToggleMinimized(const QModelIndex &index) +{ + if (d->sourceTasksModel && index.isValid() && index.model() == this) { + d->sourceTasksModel->requestToggleMinimized(mapToSource(index)); + } +} + +void FlattenTaskGroupsProxyModel::requestToggleMaximized(const QModelIndex &index) +{ + if (d->sourceTasksModel && index.isValid() && index.model() == this) { + d->sourceTasksModel->requestToggleMaximized(mapToSource(index)); + } +} + +void FlattenTaskGroupsProxyModel::requestToggleKeepAbove(const QModelIndex &index) +{ + if (d->sourceTasksModel && index.isValid() && index.model() == this) { + d->sourceTasksModel->requestToggleKeepAbove(mapToSource(index)); + } +} + +void FlattenTaskGroupsProxyModel::requestToggleKeepBelow(const QModelIndex &index) +{ + if (d->sourceTasksModel && index.isValid() && index.model() == this) { + d->sourceTasksModel->requestToggleKeepBelow(mapToSource(index)); + } +} + +void FlattenTaskGroupsProxyModel::requestToggleFullScreen(const QModelIndex &index) +{ + if (d->sourceTasksModel && index.isValid() && index.model() == this) { + d->sourceTasksModel->requestToggleFullScreen(mapToSource(index)); + } +} + +void FlattenTaskGroupsProxyModel::requestToggleShaded(const QModelIndex &index) +{ + if (d->sourceTasksModel && index.isValid() && index.model() == this) { + d->sourceTasksModel->requestToggleShaded(mapToSource(index)); + } +} + +void FlattenTaskGroupsProxyModel::requestVirtualDesktop(const QModelIndex &index, qint32 desktop) +{ + if (d->sourceTasksModel && index.isValid() && index.model() == this) { + d->sourceTasksModel->requestVirtualDesktop(mapToSource(index), desktop); + } +} + +void FlattenTaskGroupsProxyModel::requestPublishDelegateGeometry(const QModelIndex &index, const QRect &geometry, QObject *delegate) +{ + if (index.isValid() && index.model() == this) { + d->sourceTasksModel->requestPublishDelegateGeometry(mapToSource(index), geometry, delegate); + } +} + +} diff --git a/libtaskmanager/launchertasksmodel.cpp b/libtaskmanager/launchertasksmodel.cpp --- a/libtaskmanager/launchertasksmodel.cpp +++ b/libtaskmanager/launchertasksmodel.cpp @@ -175,14 +175,27 @@ } if (d->launchers != urls) { - beginResetModel(); + // Common case optimization: If the list changed but its size + // did not (e.g. due to reordering by a user of this model), + // just clear the caches and announce new data instead of + // resetting. + if (d->launchers.count() == urls.count()) { + d->launchers.clear(); + d->appDataCache.clear(); - d->launchers.clear(); - d->appDataCache.clear(); + d->launchers = urls; - d->launchers = urls; + emit dataChanged(index(0, 0), index(d->launchers.count() - 1, 0)); + } else { + beginResetModel(); - endResetModel(); + d->launchers.clear(); + d->appDataCache.clear(); + + d->launchers = urls; + + endResetModel(); + } emit launcherListChanged(); } diff --git a/libtaskmanager/tasksmodel.h b/libtaskmanager/tasksmodel.h --- a/libtaskmanager/tasksmodel.h +++ b/libtaskmanager/tasksmodel.h @@ -81,6 +81,7 @@ Q_PROPERTY(bool launchInPlace READ launchInPlace WRITE setLaunchInPlace NOTIFY launchInPlaceChanged) Q_PROPERTY(GroupMode groupMode READ groupMode WRITE setGroupMode NOTIFY groupModeChanged) + Q_PROPERTY(bool groupInline READ groupInline WRITE setGroupInline NOTIFY groupInlineChanged) Q_PROPERTY(int groupingWindowTasksThreshold READ groupingWindowTasksThreshold WRITE setGroupingWindowTasksThreshold NOTIFY groupingWindowTasksThresholdChanged) Q_PROPERTY(QStringList groupingAppIdBlacklist READ groupingAppIdBlacklist @@ -187,18 +188,14 @@ * The id of the activity used in filtering by activity. Usually * set to the id of the current activity. Defaults to an empty id. * - * FIXME: Implement. - * * @see setActivity * @returns the id of the activity used in filtering. **/ QString activity() const; /** * Set the id of the activity to use in filtering by activity. * - * FIXME: Implement. - * * @see activity * @param activity An activity id. **/ @@ -361,14 +358,47 @@ void setGroupMode(TasksModel::GroupMode mode); /** + * Returns whether grouping is done "inline" or not, i.e. whether groups + * are maintained inside the flat, top-level list, or by forming a tree. + * In inline grouping mode, move() on a group member will move all siblings + * as well, and sorting is first done among groups, then group members. + * + * Further, in inline grouping mode, the groupingWindowTasksThreshold + * setting is ignored: Grouping is always done. + * + * @see setGroupInline + * @see move + * @see groupingWindowTasksThreshold + * @returns whether grouping is done inline or not. + **/ + bool groupInline() const; + + /** + * Sets whether grouping is done "inline" or not, i.e. whether groups + * are maintained inside the flat, top-level list, or by forming a tree. + * In inline grouping mode, move() on a group member will move all siblings + * as well, and sorting is first done among groups, then group members. + * + * @see groupInline + * @see move + * @see groupingWindowTasksThreshold + * @param inline Whether to do grouping inline or not. + **/ + void setGroupInline(bool groupInline); + + /** * As window tasks (AbstractTasksModel::IsWindow) come and go, groups will * be formed when this threshold value is exceeded, and broken apart when * it matches or falls below. * * Defaults to @c -1, which means grouping is done regardless of the number * of window tasks. * + * When the groupInline property is set to @c true, the threshold is ignored: + * Grouping is always done. + * * @see setGroupingWindowTasksThreshold + * @see groupInline * @return the threshold number of window tasks used in grouping decisions. **/ int groupingWindowTasksThreshold() const; @@ -380,7 +410,11 @@ * If set to -1, grouping will be done regardless of the number of window tasks * in the source model. * + * When the groupInline property is set to @c true, the threshold is ignored: + * Grouping is always done. + * * @see groupingWindowTasksThreshold + * @see groupInline * @param threshold A threshold number of window tasks used in grouping * decisions. **/ @@ -625,11 +659,16 @@ /** * Moves a (top-level) task to a new position in the list. The insert * position is bounded to the list start and end. + * * syncLaunchers() should be called after a set of move operations to - * update the launcher list to reflect the new order. + * update the launcherList property to reflect the new order. + * + * When the groupInline property is set to @c true, a move request + * for a group member will bring all siblings along. * * @see syncLaunchers * @see launcherList + * @see setGroupInline * @param index An index in this tasks model. * @param newPos The new list position to move the task to. */ @@ -682,6 +721,7 @@ void separateLaunchersChanged() const; void launchInPlaceChanged() const; void groupModeChanged() const; + void groupInlineChanged() const; void groupingWindowTasksThresholdChanged() const; void groupingAppIdBlacklistChanged() const; void groupingLauncherUrlBlacklistChanged() const; diff --git a/libtaskmanager/tasksmodel.cpp b/libtaskmanager/tasksmodel.cpp --- a/libtaskmanager/tasksmodel.cpp +++ b/libtaskmanager/tasksmodel.cpp @@ -21,6 +21,7 @@ #include "tasksmodel.h" #include "activityinfo.h" #include "concatenatetasksproxymodel.h" +#include "flattentaskgroupsproxymodel.h" #include "taskfilterproxymodel.h" #include "taskgroupingproxymodel.h" #include "tasktools.h" @@ -63,6 +64,8 @@ ConcatenateTasksProxyModel* concatProxyModel = nullptr; TaskFilterProxyModel* filterProxyModel = nullptr; TaskGroupingProxyModel* groupingProxyModel = nullptr; + FlattenTaskGroupsProxyModel* flattenGroupsProxyModel = nullptr; + AbstractTasksModelIface *abstractTasksSourceModel = nullptr; bool anyTaskDemandsAttention = false; @@ -81,10 +84,14 @@ static ActivityInfo* activityInfo; static int activityInfoUsers; + bool groupInline = false; + int groupingWindowTasksThreshold = -1; + void initModels(); void updateAnyTaskDemandsAttention(); void updateManualSortMap(); - void syncManualSortMapForGroup(const QModelIndex &parent); + void consolidateManualSortMapForGroup(const QModelIndex &groupingProxyIndex); + void updateGroupInline(); QModelIndex preFilterIndex(const QModelIndex &sourceIndex) const; void updateActivityTaskCounts(); void forceResort(); @@ -148,10 +155,11 @@ { // NOTE: Overview over the entire model chain assembled here: // {X11,Wayland}WindowTasksModel, StartupTasksModel, LauncherTasksModel - // -> ConcatenateTasksProxyModel concatenates them into a single list. - // -> TaskFilterProxyModel filters by state (e.g. virtual desktop). - // -> TaskGroupingProxyModel groups by application (we go from flat list to tree). - // -> TasksModel collapses top-level items into task lifecycle abstraction, sorts. + // -> concatProxyModel concatenates them into a single list. + // -> filterProxyModel filters by state (e.g. virtual desktop). + // -> groupingProxyModel groups by application (we go from flat list to tree). + // -> flattenGroupsProxyModel (optionally, if groupInline == true) flattens groups out. + // -> TasksModel collapses (top-level) items into task lifecycle abstraction; sorts. if (!windowTasksModel && QGuiApplication::platformName().startsWith(QLatin1String("wayland"))) { windowTasksModel = new WaylandTasksModel(); @@ -302,17 +310,17 @@ groupingProxyModel->setSourceModel(filterProxyModel); QObject::connect(groupingProxyModel, &TaskGroupingProxyModel::groupModeChanged, q, &TasksModel::groupModeChanged); - QObject::connect(groupingProxyModel, &TaskGroupingProxyModel::windowTasksThresholdChanged, - q, &TasksModel::groupingWindowTasksThresholdChanged); QObject::connect(groupingProxyModel, &TaskGroupingProxyModel::blacklistedAppIdsChanged, q, &TasksModel::groupingAppIdBlacklistChanged); QObject::connect(groupingProxyModel, &TaskGroupingProxyModel::blacklistedLauncherUrlsChanged, q, &TasksModel::groupingLauncherUrlBlacklistChanged); QObject::connect(groupingProxyModel, &QAbstractItemModel::rowsInserted, q, [this](const QModelIndex &parent, int first, int last) { - // We can ignore group members. if (parent.isValid()) { + consolidateManualSortMapForGroup(parent); + + // Existence of a group means everything below this has already been done. return; } @@ -400,7 +408,7 @@ Q_UNUSED(bottomRight) // We can ignore group members. - if (topLeft.isValid()) { + if (topLeft.parent().isValid()) { return; } @@ -415,27 +423,10 @@ [this]() { updateAnyTaskDemandsAttention(); } ); + abstractTasksSourceModel = groupingProxyModel; q->setSourceModel(groupingProxyModel); - QObject::connect(q, &QAbstractItemModel::rowsInserted, q, - [this](const QModelIndex &parent, int first, int last) { - Q_UNUSED(first) - Q_UNUSED(last) - - q->countChanged(); - - // If we're in manual sort mode, we need to consolidate new children - // of a group in the manual sort map to prepare for when a group - // gets dissolved. - // This is done after we've already had a chance to sort the new child - // in alphabetically in this proxy. - if (sortMode == SortManual && parent.isValid()) { - syncManualSortMapForGroup(parent); - } - } - ); - - + QObject::connect(q, &QAbstractItemModel::rowsInserted, q, &TasksModel::countChanged); QObject::connect(q, &QAbstractItemModel::rowsRemoved, q, &TasksModel::countChanged); QObject::connect(q, &QAbstractItemModel::modelReset, q, &TasksModel::countChanged); } @@ -471,6 +462,17 @@ TasksModelLessThan lt(concatProxyModel, q, false); std::stable_sort(sortedPreFilterRows.begin(), sortedPreFilterRows.end(), lt); + // Consolidate sort map entries for groups. + if (q->groupMode() != GroupDisabled) { + for (int i = 0; i < groupingProxyModel->rowCount(); ++i) { + const QModelIndex &groupingIndex = groupingProxyModel->index(i, 0); + + if (groupingIndex.data(AbstractTasksModel::IsGroupParent).toBool()) { + consolidateManualSortMapForGroup(groupingIndex); + } + } + } + return; } @@ -519,27 +521,84 @@ } } -void TasksModel::Private::syncManualSortMapForGroup(const QModelIndex &parent) +void TasksModel::Private::consolidateManualSortMapForGroup(const QModelIndex &groupingProxyIndex) +{ + // Consolidates sort map entries for a group's items to be contiguous + // after the group's first item and the same order as in groupingProxyModel. + + const int childCount = groupingProxyModel->rowCount(groupingProxyIndex); + + if (!childCount) { + return; + } + + const QModelIndex &leader = groupingProxyIndex.child(0, 0); + const QModelIndex &preFilterLeader = filterProxyModel->mapToSource(groupingProxyModel->mapToSource(leader)); + + // We're moving the trailing children to the sort map position of + // the first child, so we're skipping the first child. + for (int i = 1; i < childCount; ++i) { + const QModelIndex &child = groupingProxyIndex.child(i, 0); + const QModelIndex &preFilterChild = filterProxyModel->mapToSource(groupingProxyModel->mapToSource(child)); + const int leaderPos = sortedPreFilterRows.indexOf(preFilterLeader.row()); + const int childPos = sortedPreFilterRows.indexOf(preFilterChild.row()); + const int insertPos = (leaderPos + i) + ((leaderPos + i) > childPos ? -1 : 0); + sortedPreFilterRows.move(childPos, insertPos); + } +} + +void TasksModel::Private::updateGroupInline() { - const int childCount = q->rowCount(parent); + if (q->groupMode() != GroupDisabled && groupInline) { + if (flattenGroupsProxyModel) { + return; + } - if (childCount != -1) { - const QModelIndex &preFilterParent = preFilterIndex(q->mapToSource(parent)); + // Exempting tasks which demand attention from grouping is not + // necessary when all group children are shown inline anyway + // and would interfere with our sort-tasks-together goals. + groupingProxyModel->setGroupDemandingAttention(true); - // We're moving the trailing children to the sort map position of - // the first child, so we're skipping the first child. - for (int i = 1; i < childCount; ++i) { - const QModelIndex &preFilterChildIndex = preFilterIndex(q->mapToSource(parent.child(i, 0))); - const int childSortIndex = sortedPreFilterRows.indexOf(preFilterChildIndex.row()); - const int parentSortIndex = sortedPreFilterRows.indexOf(preFilterParent.row()); - const int insertPos = (parentSortIndex + i) + ((parentSortIndex + i) > childSortIndex ? -1 : 0); - sortedPreFilterRows.move(childSortIndex, insertPos); + // Likewise, ignore the window tasks threshold when making + // grouping decisions. + groupingProxyModel->setWindowTasksThreshold(-1); + + flattenGroupsProxyModel = new FlattenTaskGroupsProxyModel(q); + flattenGroupsProxyModel->setSourceModel(groupingProxyModel); + + abstractTasksSourceModel = flattenGroupsProxyModel; + q->setSourceModel(flattenGroupsProxyModel); + + if (sortMode == SortManual) { + forceResort(); + } + } else { + if (!flattenGroupsProxyModel) { + return; + } + + groupingProxyModel->setGroupDemandingAttention(false); + groupingProxyModel->setWindowTasksThreshold(groupingWindowTasksThreshold); + + abstractTasksSourceModel = groupingProxyModel; + q->setSourceModel(groupingProxyModel); + + delete flattenGroupsProxyModel; + flattenGroupsProxyModel = nullptr; + + if (sortMode == SortManual) { + forceResort(); } } } QModelIndex TasksModel::Private::preFilterIndex(const QModelIndex &sourceIndex) const { - return filterProxyModel->mapToSource(groupingProxyModel->mapToSource(sourceIndex)); + // Only in inline grouping mode, we have an additional proxy layer. + if (flattenGroupsProxyModel) { + return filterProxyModel->mapToSource(groupingProxyModel->mapToSource(flattenGroupsProxyModel->mapToSource(sourceIndex))); + } else { + return filterProxyModel->mapToSource(groupingProxyModel->mapToSource(sourceIndex)); + } } void TasksModel::Private::updateActivityTaskCounts() @@ -737,11 +796,12 @@ { QList launchers = QUrl::fromStringList(d->launcherTasksModel->launcherList()); - for(int i = 0; i < d->filterProxyModel->rowCount(); ++i) { + for (int i = 0; i < d->filterProxyModel->rowCount(); ++i) { const QModelIndex &filterIndex = d->filterProxyModel->index(i, 0); if (!filterIndex.data(AbstractTasksModel::IsLauncher).toBool()) { - // TODO: It would be much faster if we didn't ask for a URL with serialized PNG data in it, just to discard it a few lines below + // TODO: It would be much faster if we didn't ask for a URL with serialized PNG + // data in it, just to discard it a few lines below. const QUrl &launcherUrl = filterIndex.data(AbstractTasksModel::LauncherUrl).toUrl(); QMutableListIterator it(launchers); @@ -897,6 +957,7 @@ if (d->separateLaunchers != separate) { d->separateLaunchers = separate; + d->updateManualSortMap(); d->forceResort(); emit separateLaunchersChanged(); @@ -931,23 +992,46 @@ void TasksModel::setGroupMode(GroupMode mode) { if (d->groupingProxyModel) { + if (mode == GroupDisabled && d->flattenGroupsProxyModel) { + d->flattenGroupsProxyModel->setSourceModel(nullptr); + } + d->groupingProxyModel->setGroupMode(mode); + d->updateGroupInline(); } } -int TasksModel::groupingWindowTasksThreshold() const +bool TasksModel::groupInline() const { - if (!d->groupingProxyModel) { - return -1; + return d->groupInline; +} + +void TasksModel::setGroupInline(bool groupInline) +{ + if (d->groupInline != groupInline) { + d->groupInline = groupInline; + + d->updateGroupInline(); + + emit groupInlineChanged(); } +} - return d->groupingProxyModel->windowTasksThreshold(); +int TasksModel::groupingWindowTasksThreshold() const +{ + return d->groupingWindowTasksThreshold; } void TasksModel::setGroupingWindowTasksThreshold(int threshold) { - if (d->groupingProxyModel) { - d->groupingProxyModel->setWindowTasksThreshold(threshold); + if (d->groupingWindowTasksThreshold != threshold) { + d->groupingWindowTasksThreshold = threshold; + + if (!d->groupInline && d->groupingProxyModel) { + d->groupingProxyModel->setWindowTasksThreshold(threshold); + } + + emit groupingWindowTasksThresholdChanged(); } } @@ -1039,98 +1123,100 @@ void TasksModel::requestActivate(const QModelIndex &index) { if (index.isValid() && index.model() == this) { - d->groupingProxyModel->requestActivate(mapToSource(index)); + d->abstractTasksSourceModel->requestActivate(mapToSource(index)); } } void TasksModel::requestNewInstance(const QModelIndex &index) { if (index.isValid() && index.model() == this) { - d->groupingProxyModel->requestNewInstance(mapToSource(index)); + d->abstractTasksSourceModel->requestNewInstance(mapToSource(index)); } } void TasksModel::requestClose(const QModelIndex &index) { if (index.isValid() && index.model() == this) { - d->groupingProxyModel->requestClose(mapToSource(index)); + d->abstractTasksSourceModel->requestClose(mapToSource(index)); } } void TasksModel::requestMove(const QModelIndex &index) { if (index.isValid() && index.model() == this) { - d->groupingProxyModel->requestMove(mapToSource(index)); + d->abstractTasksSourceModel->requestMove(mapToSource(index)); } } void TasksModel::requestResize(const QModelIndex &index) { if (index.isValid() && index.model() == this) { - d->groupingProxyModel->requestResize(mapToSource(index)); + d->abstractTasksSourceModel->requestResize(mapToSource(index)); } } void TasksModel::requestToggleMinimized(const QModelIndex &index) { if (index.isValid() && index.model() == this) { - d->groupingProxyModel->requestToggleMinimized(mapToSource(index)); + d->abstractTasksSourceModel->requestToggleMinimized(mapToSource(index)); } } void TasksModel::requestToggleMaximized(const QModelIndex &index) { if (index.isValid() && index.model() == this) { - d->groupingProxyModel->requestToggleMaximized(mapToSource(index)); + d->abstractTasksSourceModel->requestToggleMaximized(mapToSource(index)); } } void TasksModel::requestToggleKeepAbove(const QModelIndex &index) { if (index.isValid() && index.model() == this) { - d->groupingProxyModel->requestToggleKeepAbove(mapToSource(index)); + d->abstractTasksSourceModel->requestToggleKeepAbove(mapToSource(index)); } } void TasksModel::requestToggleKeepBelow(const QModelIndex &index) { if (index.isValid() && index.model() == this) { - d->groupingProxyModel->requestToggleKeepBelow(mapToSource(index)); + d->abstractTasksSourceModel->requestToggleKeepBelow(mapToSource(index)); } } void TasksModel::requestToggleFullScreen(const QModelIndex &index) { if (index.isValid() && index.model() == this) { - d->groupingProxyModel->requestToggleFullScreen(mapToSource(index)); + d->abstractTasksSourceModel->requestToggleFullScreen(mapToSource(index)); } } void TasksModel::requestToggleShaded(const QModelIndex &index) { if (index.isValid() && index.model() == this) { - d->groupingProxyModel->requestToggleShaded(mapToSource(index)); + d->abstractTasksSourceModel->requestToggleShaded(mapToSource(index)); } } void TasksModel::requestVirtualDesktop(const QModelIndex &index, qint32 desktop) { if (index.isValid() && index.model() == this) { - d->groupingProxyModel->requestVirtualDesktop(mapToSource(index), desktop); + d->abstractTasksSourceModel->requestVirtualDesktop(mapToSource(index), desktop); } } void TasksModel::requestPublishDelegateGeometry(const QModelIndex &index, const QRect &geometry, QObject *delegate) { if (index.isValid() && index.model() == this) { - d->groupingProxyModel->requestPublishDelegateGeometry(mapToSource(index), geometry, delegate); + d->abstractTasksSourceModel->requestPublishDelegateGeometry(mapToSource(index), geometry, delegate); } } void TasksModel::requestToggleGrouping(const QModelIndex &index) { if (index.isValid() && index.model() == this) { - d->groupingProxyModel->requestToggleGrouping(mapToSource(index)); + const QModelIndex &target = (d->flattenGroupsProxyModel + ? d->flattenGroupsProxyModel->mapToSource(mapToSource(index)) : mapToSource(index)); + d->groupingProxyModel->requestToggleGrouping(target); } } @@ -1175,24 +1261,69 @@ } } - beginMoveRows(QModelIndex(), row, row, QModelIndex(), (newPos >row) ? newPos + 1 : newPos); + // Treat flattened-out groups as single items. + if (d->flattenGroupsProxyModel) { + QModelIndex groupingRowIndex = d->flattenGroupsProxyModel->mapToSource(mapToSource(index(row, 0))); + const QModelIndex &groupingRowIndexParent = groupingRowIndex.parent(); + QModelIndex groupingNewPosIndex = d->flattenGroupsProxyModel->mapToSource(mapToSource(index(newPos, 0))); + const QModelIndex &groupingNewPosIndexParent = groupingNewPosIndex.parent(); + + // Disallow moves within a flattened-out group (TODO: for now, anyway). + if (groupingRowIndexParent.isValid() + && (groupingRowIndexParent == groupingNewPosIndex + || groupingRowIndexParent == groupingNewPosIndexParent)) { + return false; + } + + int offset = 0; + int extraChildCount = 0; + + if (groupingRowIndexParent.isValid()) { + offset = groupingRowIndex.row(); + extraChildCount = d->groupingProxyModel->rowCount(groupingRowIndexParent) - 1; + groupingRowIndex = groupingRowIndexParent; + } + + if (groupingNewPosIndexParent.isValid()) { + groupingNewPosIndex = groupingNewPosIndexParent; + } + + beginMoveRows(QModelIndex(), (row - offset), (row - offset) + extraChildCount, + QModelIndex(), (newPos > row) ? newPos + 1 : newPos); + + row = d->sortedPreFilterRows.indexOf(d->filterProxyModel->mapToSource(d->groupingProxyModel->mapToSource(groupingRowIndex)).row()); + newPos = d->sortedPreFilterRows.indexOf(d->filterProxyModel->mapToSource(d->groupingProxyModel->mapToSource(groupingNewPosIndex)).row()); + + // Update sort mappings. + d->sortedPreFilterRows.move(row, newPos); - // Translate to sort map indices. - const QModelIndex &rowIndex = index(row, 0); - const QModelIndex &preFilterRowIndex = d->preFilterIndex(mapToSource(rowIndex)); - row = d->sortedPreFilterRows.indexOf(preFilterRowIndex.row()); - newPos = d->sortedPreFilterRows.indexOf(d->preFilterIndex(mapToSource(index(newPos, 0))).row()); + if (groupingRowIndexParent.isValid()) { + d->consolidateManualSortMapForGroup(groupingRowIndexParent); + } + + if (groupingNewPosIndexParent.isValid()) { + d->consolidateManualSortMapForGroup(groupingNewPosIndexParent); + } + + endMoveRows(); + } else { + beginMoveRows(QModelIndex(), row, row, QModelIndex(), (newPos >row) ? newPos + 1 : newPos); - // Update sort mapping. - d->sortedPreFilterRows.move(row, newPos); + // Translate to sort map indices. + const QModelIndex &groupingRowIndex = mapToSource(index(row, 0)); + const QModelIndex &preFilterRowIndex = d->preFilterIndex(groupingRowIndex); + row = d->sortedPreFilterRows.indexOf(preFilterRowIndex.row()); + newPos = d->sortedPreFilterRows.indexOf(d->preFilterIndex(mapToSource(index(newPos, 0))).row()); - endMoveRows(); + // Update sort mapping. + d->sortedPreFilterRows.move(row, newPos); - // Move children along with the group. - // This can be safely done after the row move transaction as the sort - // map isn't consulted for rows below the top level. - if (groupMode() != GroupDisabled && rowCount(rowIndex)) { - d->syncManualSortMapForGroup(rowIndex); + // If we moved a group parent, consolidate sort map for children. + if (groupMode() != GroupDisabled && d->groupingProxyModel->rowCount(groupingRowIndex)) { + d->consolidateManualSortMapForGroup(groupingRowIndex); + } + + endMoveRows(); } // Resort. @@ -1234,6 +1365,9 @@ setLauncherList(QUrl::toStringList(sortedLaunchers.values())); d->launcherSortingDirty = false; + + d->updateManualSortMap(); + d->forceResort(); } QModelIndex TasksModel::activeTask() const @@ -1282,12 +1416,19 @@ bool TasksModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { - // All our filtering occurs at the top-level; group children always go through. + // All our filtering occurs at the top-level; anything below always + // goes through. if (sourceParent.isValid()) { return true; } const QModelIndex &sourceIndex = sourceModel()->index(sourceRow, 0); + + // In inline grouping mode, filter out group parents. + if (d->flattenGroupsProxyModel && sourceIndex.data(AbstractTasksModel::IsGroupParent).toBool()) { + return false; + } + const QString &appId = sourceIndex.data(AbstractTasksModel::AppId).toString(); const QString &appName = sourceIndex.data(AbstractTasksModel::AppName).toString(); @@ -1321,9 +1462,8 @@ if ((!appId.isEmpty() && appId == filteredAppId) || (launcherUrl.isValid() && launcherUrlsMatch(launcherUrl, filteredIndex.data(AbstractTasksModel::LauncherUrl).toUrl(), IgnoreQueryItems))) { - // TODO: Do this outside of filterAcceptsRow, based on notification that something changed + // TODO: Do this outside of filterAcceptsRow, based on notification that something changed. QMetaObject::invokeMethod(const_cast(this), "updateLauncherCount", Qt::QueuedConnection); - return false; } } @@ -1334,10 +1474,8 @@ bool TasksModel::lessThan(const QModelIndex &left, const QModelIndex &right) const { - // In manual sort mode we sort top-level items by referring to a map we keep. - // Insertions into the map are placed using a combination of Private::lessThan - // and simple append behavior. Child items are sorted alphabetically. - if (d->sortMode == SortManual && !left.parent().isValid() && !right.parent().isValid()) { + // In manual sort mode, sort by map. + if (d->sortMode == SortManual) { return (d->sortedPreFilterRows.indexOf(d->preFilterIndex(left).row()) < d->sortedPreFilterRows.indexOf(d->preFilterIndex(right).row())); }