diff --git a/applets/notifications/package/contents/ui/FullRepresentation.qml b/applets/notifications/package/contents/ui/FullRepresentation.qml index a05076590..e8bf4280f 100644 --- a/applets/notifications/package/contents/ui/FullRepresentation.qml +++ b/applets/notifications/package/contents/ui/FullRepresentation.qml @@ -1,561 +1,562 @@ /* * Copyright 2018-2019 Kai Uwe Broulik * * 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) 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 14 of version 3 of the license. * * 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, see */ import QtQuick 2.8 import QtQuick.Layouts 1.1 import org.kde.plasma.plasmoid 2.0 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.plasma.components 3.0 as PlasmaComponents3 import org.kde.plasma.extras 2.0 as PlasmaExtras import org.kde.kcoreaddons 1.0 as KCoreAddons import org.kde.notificationmanager 1.0 as NotificationManager import "global" ColumnLayout{ // FIXME fix popup size when resizing panel smaller (so it collapses) //Layout.preferredWidth: units.gridUnit * 18 //Layout.preferredHeight: units.gridUnit * 24 //Layout.minimumWidth: units.gridUnit * 10 //Layout.minimumHeight: units.gridUnit * 15 Layout.fillHeight: plasmoid.formFactor === PlasmaCore.Types.Vertical spacing: units.smallSpacing // TODO these should be configurable in the future readonly property int dndMorningHour: 6 readonly property int dndEveningHour: 20 // HACK forward focus to the list onActiveFocusChanged: { if (activeFocus) { list.forceActiveFocus(); } } Connections { target: plasmoid onExpandedChanged: { if (plasmoid.expanded) { list.positionViewAtBeginning(); list.currentIndex = -1; } } } // header ColumnLayout { Layout.fillWidth: true spacing: 0 RowLayout { Layout.fillWidth: true spacing: 0 RowLayout { id: dndRow spacing: units.smallSpacing PlasmaComponents3.CheckBox { id: dndCheck text: i18n("Do not disturb") spacing: units.smallSpacing checkable: true checked: Globals.inhibited // Let the menu open on press onPressed: { if (!Globals.inhibited) { dndMenu.date = new Date(); // shows ontop of CheckBox to hide the fact that it's unchecked // until you actually select something :) dndMenu.open(0, 0); } } // but disable only on click onClicked: { if (Globals.inhibited) { notificationSettings.notificationsInhibitedUntil = undefined; notificationSettings.revokeApplicationInhibitions(); notificationSettings.save(); } } contentItem: RowLayout { spacing: dndCheck.spacing PlasmaCore.IconItem { Layout.leftMargin: dndCheck.mirrored ? 0 : dndCheck.indicator.width + dndCheck.spacing Layout.rightMargin: dndCheck.mirrored ? dndCheck.indicator.width + dndCheck.spacing : 0 source: "notifications-disabled" Layout.preferredWidth: units.iconSizes.smallMedium Layout.preferredHeight: units.iconSizes.smallMedium } PlasmaComponents.Label { text: i18n("Do not disturb") } } PlasmaComponents.ModelContextMenu { id: dndMenu property date date visualParent: dndCheck onClicked: { notificationSettings.notificationsInhibitedUntil = model.date; notificationSettings.save(); } model: { var model = []; // For 1 hour var d = dndMenu.date; d.setHours(d.getHours() + 1); d.setSeconds(0); model.push({date: d, text: i18n("For 1 hour")}); d = dndMenu.date; d.setHours(d.getHours() + 4); d.setSeconds(0); model.push({date: d, text: i18n("For 4 hours")}); // Until this evening if (dndMenu.date.getHours() < dndEveningHour) { d = dndMenu.date; // TODO make the user's preferred time schedule configurable d.setHours(dndEveningHour); d.setMinutes(0); d.setSeconds(0); model.push({date: d, text: i18n("Until this evening")}); } // Until next morning if (dndMenu.date.getHours() > dndMorningHour) { d = dndMenu.date; d.setDate(d.getDate() + 1); d.setHours(dndMorningHour); d.setMinutes(0); d.setSeconds(0); model.push({date: d, text: i18n("Until tomorrow morning")}); } // Until Monday // show Friday and Saturday, Sunday is "0" but for that you can use "until tomorrow morning" if (dndMenu.date.getDay() >= 5) { d = dndMenu.date; d.setHours(dndMorningHour); // wraps around if neccessary d.setDate(d.getDate() + (7 - d.getDay() + 1)); d.setMinutes(0); d.setSeconds(0); model.push({date: d, text: i18n("Until Monday")}); } // Until "turned off" d = dndMenu.date; // Just set it to one year in the future so we don't need yet another "do not disturb enabled" property d.setFullYear(d.getFullYear() + 1); model.push({date: d, text: i18n("Until turned off")}); return model; } } } } Item { Layout.fillWidth: true } PlasmaComponents.ToolButton { iconName: "configure" // remove mnemonics tooltip: plasmoid.action("openKcm").text.replace(/([^&]*)&(.)([^&]*)/g, function (match, p1, p2, p3) { return p1.concat(p2, p3); }); visible: plasmoid.action("openKcm").enabled onClicked: plasmoid.action("openKcm").trigger() } PlasmaComponents.ToolButton { iconName: "edit-clear-history" tooltip: i18n("Clear History") enabled: plasmoid.action("clearHistory").visible onClicked: action_clearHistory() } } PlasmaExtras.DescriptiveLabel { Layout.leftMargin: dndCheck.mirrored ? 0 : dndCheck.indicator.width + 2 * dndCheck.spacing + units.iconSizes.smallMedium Layout.rightMargin: dndCheck.mirrored ? dndCheck.indicator.width + 2 * dndCheck.spacing + units.iconSizes.smallMedium : 0 Layout.fillWidth: true wrapMode: Text.WordWrap textFormat: Text.PlainText text: { if (!Globals.inhibited) { return ""; } var inhibitedUntil = notificationSettings.notificationsInhibitedUntil; var inhibitedByApp = notificationSettings.notificationsInhibitedByApplication; var sections = []; // Show until time if valid but not if too far int he future if (!isNaN(inhibitedUntil.getTime()) && inhibitedUntil.getTime() - new Date().getTime() < 365 * 24 * 60 * 60 * 1000 /* 1 year*/) { sections.push(i18nc("Do not disturb until date", "Until %1", KCoreAddons.Format.formatRelativeDateTime(inhibitedUntil, Locale.ShortFormat))); } if (inhibitedByApp) { var inhibitionAppNames = notificationSettings.notificationInhibitionApplications; var inhibitionAppReasons = notificationSettings.notificationInhibitionReasons; for (var i = 0, length = inhibitionAppNames.length; i < length; ++i) { var name = inhibitionAppNames[i]; var reason = inhibitionAppReasons[i]; if (reason) { sections.push(i18nc("Do not disturb until app has finished (reason)", "While %1 is active (%2)", name, reason)); } else { sections.push(i18nc("Do not disturb until app has finished", "While %1 is active", name)); } } } return sections.join(" ยท "); } visible: text !== "" } } PlasmaCore.SvgItem { elementId: "horizontal-line" Layout.fillWidth: true // why is this needed here but not in the delegate? Layout.preferredHeight: naturalSize.height svg: PlasmaCore.Svg { id: lineSvg imagePath: "widgets/line" } } // actual notifications PlasmaExtras.ScrollArea { Layout.fillWidth: true Layout.fillHeight: true Layout.preferredWidth: units.gridUnit * 18 Layout.preferredHeight: units.gridUnit * 24 ListView { id: list model: historyModel currentIndex: -1 Keys.onDeletePressed: { var idx = historyModel.index(currentIndex, 0); if (historyModel.data(idx, NotificationManager.Notifications.ClosableRole)) { historyModel.close(idx); // TODO would be nice to stay inside the current group when deleting an item } } Keys.onEnterPressed: Keys.onReturnPressed(event) Keys.onReturnPressed: { // Trigger default action, if any var idx = historyModel.index(currentIndex, 0); if (historyModel.data(idx, NotificationManager.Notifications.HasDefaultActionRole)) { historyModel.invokeDefaultAction(idx); return; } // Trigger thumbnail URL if there's one var urls = historyModel.data(idx, NotificationManager.Notifications.UrlsRole); if (urls && urls.length === 1) { Qt.openUrlExternally(urls[0]); historyModel.expire(idx); return; } // TODO for finished jobs trigger "Open" or "Open Containing Folder" action } Keys.onLeftPressed: setGroupExpanded(currentIndex, LayoutMirroring.enabled) Keys.onRightPressed: setGroupExpanded(currentIndex, !LayoutMirroring.enabled) Keys.onPressed: { switch (event.key) { case Qt.Key_Home: currentIndex = 0; break; case Qt.Key_End: currentIndex = count - 1; break; } } function isRowExpanded(row) { var idx = historyModel.index(row, 0); return historyModel.data(idx, NotificationManager.Notifications.IsGroupExpandedRole); } function setGroupExpanded(row, expanded) { var rowIdx = historyModel.index(row, 0); var persistentRowIdx = historyModel.makePersistentModelIndex(rowIdx); var persistentGroupIdx = historyModel.makePersistentModelIndex(historyModel.groupIndex(rowIdx)); historyModel.setData(rowIdx, expanded, NotificationManager.Notifications.IsGroupExpandedRole); // If the current item went away when the group collapsed, scroll to the group heading if (!persistentRowIdx || !persistentRowIdx.valid) { if (persistentGroupIdx && persistentGroupIdx.valid) { list.positionViewAtIndex(persistentGroupIdx.row, ListView.Contain); // When closed via keyboard, also set a sane current index if (list.currentIndex > -1) { list.currentIndex = persistentGroupIdx.row; } } } } highlightMoveDuration: 0 highlightResizeDuration: 0 // Not using PlasmaComponents.Highlight as this is only for indicating keyboard focus highlight: PlasmaCore.FrameSvgItem { imagePath: "widgets/listitem" prefix: "pressed" } add: Transition { SequentialAnimation { PropertyAction { property: "opacity"; value: 0 } PauseAnimation { duration: units.longDuration } ParallelAnimation { NumberAnimation { property: "opacity"; from: 0; to: 1; duration: units.longDuration } NumberAnimation { property: "height"; from: 0; duration: units.longDuration } } } } addDisplaced: Transition { NumberAnimation { properties: "y"; duration: units.longDuration } } remove: Transition { ParallelAnimation { NumberAnimation { property: "opacity"; to: 0; duration: units.longDuration } NumberAnimation { property: "x"; to: list.width; duration: units.longDuration } } } removeDisplaced: Transition { SequentialAnimation { PauseAnimation { duration: units.longDuration } NumberAnimation { properties: "y"; duration: units.longDuration } } } // This is so the delegates can detect the change in "isInGroup" and show a separator section { property: "isInGroup" criteria: ViewSection.FullString } delegate: Loader { id: delegateLoader width: list.width sourceComponent: model.isGroup ? groupDelegate : notificationDelegate Component { id: groupDelegate NotificationHeader { applicationName: model.applicationName applicationIconSource: model.applicationIconName + originName: model.originName || "" // don't show timestamp for group configurable: model.configurable closable: model.closable closeButtonTooltip: i18n("Close Group") onCloseClicked: { historyModel.close(historyModel.index(index, 0)) if (list.count === 0) { plasmoid.expanded = false; } } onConfigureClicked: historyModel.configure(historyModel.index(index, 0)) } } Component { id: notificationDelegate ColumnLayout { spacing: units.smallSpacing RowLayout { Item { id: groupLineContainer Layout.fillHeight: true Layout.topMargin: units.smallSpacing width: units.iconSizes.small visible: model.isInGroup PlasmaCore.SvgItem { elementId: "vertical-line" svg: lineSvg anchors.horizontalCenter: parent.horizontalCenter width: units.iconSizes.small height: parent.height } } NotificationItem { Layout.fillWidth: true notificationType: model.type inGroup: model.isInGroup applicationName: model.applicationName applicationIconSource: model.applicationIconName originName: model.originName || "" time: model.updated || model.created // configure button on every single notifications is bit overwhelming configurable: !inGroup && model.configurable dismissable: model.type === NotificationManager.Notifications.JobType && model.jobState !== NotificationManager.Notifications.JobStateStopped && model.dismissed // TODO would be nice to be able to undismiss jobs even when they autohide && notificationSettings.permanentJobPopups dismissed: model.dismissed || false closable: model.closable summary: model.summary body: model.body || "" icon: model.image || model.iconName urls: model.urls || [] jobState: model.jobState || 0 percentage: model.percentage || 0 jobError: model.jobError || 0 suspendable: !!model.suspendable killable: !!model.killable jobDetails: model.jobDetails || null configureActionLabel: model.configureActionLabel || "" // In the popup the default action is triggered by clicking on the popup // however in the list this is undesirable, so instead show a clickable button // in case you have a non-expired notification in history (do not disturb mode) // unless it has the same label as an action readonly property bool addDefaultAction: (model.hasDefaultAction && model.defaultActionLabel && (model.actionLabels || []).indexOf(model.defaultActionLabel) === -1) ? true : false actionNames: { var actions = (model.actionNames || []); if (addDefaultAction) { actions.unshift("default"); // prepend } return actions; } actionLabels: { var labels = (model.actionLabels || []); if (addDefaultAction) { labels.unshift(model.defaultActionLabel); } return labels; } onCloseClicked: { historyModel.close(historyModel.index(index, 0)); if (list.count === 0) { plasmoid.expanded = false; } } onDismissClicked: { model.dismissed = false; plasmoid.expanded = false; } onConfigureClicked: historyModel.configure(historyModel.index(index, 0)) onActionInvoked: { if (actionName === "default") { historyModel.invokeDefaultAction(historyModel.index(index, 0)); } else { historyModel.invokeAction(historyModel.index(index, 0), actionName); } // Keep it in the history historyModel.expire(historyModel.index(index, 0)); } onOpenUrl: { Qt.openUrlExternally(url); historyModel.expire(historyModel.index(index, 0)); } onFileActionInvoked: historyModel.expire(historyModel.index(index, 0)) onSuspendJobClicked: historyModel.suspendJob(historyModel.index(index, 0)) onResumeJobClicked: historyModel.resumeJob(historyModel.index(index, 0)) onKillJobClicked: historyModel.killJob(historyModel.index(index, 0)) } } PlasmaComponents.ToolButton { Layout.preferredWidth: minimumWidth iconName: model.isGroupExpanded ? "arrow-up" : "arrow-down" text: model.isGroupExpanded ? i18n("Show Fewer") : i18nc("Expand to show n more notifications", "Show %1 More", (model.groupChildrenCount - model.expandedGroupChildrenCount)) visible: (model.groupChildrenCount > model.expandedGroupChildrenCount || model.isGroupExpanded) && delegateLoader.ListView.nextSection !== delegateLoader.ListView.section onClicked: list.setGroupExpanded(model.index, !model.isGroupExpanded) } PlasmaCore.SvgItem { Layout.fillWidth: true Layout.bottomMargin: units.smallSpacing elementId: "horizontal-line" svg: lineSvg // property is only atached to the delegate itself (the Loader in our case) visible: (!model.isInGroup || delegateLoader.ListView.nextSection !== delegateLoader.ListView.section) && delegateLoader.ListView.nextSection !== "" // don't show after last item } } } } PlasmaExtras.Heading { width: list.width level: 3 opacity: 0.6 text: i18n("No unread notifications.") visible: list.count === 0 } } } } diff --git a/libnotificationmanager/notificationgroupingproxymodel.cpp b/libnotificationmanager/notificationgroupingproxymodel.cpp index 7960e18f2..79e5e1ad2 100644 --- a/libnotificationmanager/notificationgroupingproxymodel.cpp +++ b/libnotificationmanager/notificationgroupingproxymodel.cpp @@ -1,524 +1,521 @@ /* * Copyright 2016 Eike Hein * Copyright 2018-2019 Kai Uwe Broulik * * 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 "notificationgroupingproxymodel_p.h" #include #include "notifications.h" using namespace NotificationManager; NotificationGroupingProxyModel::NotificationGroupingProxyModel(QObject *parent) : QAbstractProxyModel(parent) { } NotificationGroupingProxyModel::~NotificationGroupingProxyModel() = default; bool NotificationGroupingProxyModel::appsMatch(const QModelIndex &a, const QModelIndex &b) const { const QString aName = a.data(Notifications::ApplicationNameRole).toString(); const QString bName = b.data(Notifications::ApplicationNameRole).toString(); const QString aDesktopEntry = a.data(Notifications::DesktopEntryRole).toString(); const QString bDesktopEntry = b.data(Notifications::DesktopEntryRole).toString(); + const QString aOriginName = a.data(Notifications::OriginNameRole).toString(); + const QString bOriginName = a.data(Notifications::OriginNameRole).toString(); + return !aName.isEmpty() && aName == bName - && aDesktopEntry == bDesktopEntry; + && aDesktopEntry == bDesktopEntry + && aOriginName == bOriginName; } bool NotificationGroupingProxyModel::isGroup(int row) const { if (row < 0 || row >= rowMap.count()) { return false; } return (rowMap.at(row)->count() > 1); } bool NotificationGroupingProxyModel::tryToGroup(const QModelIndex &sourceIndex, bool silent) { // Meat of the matter: Try to add this source row to a sub-list with source rows // associated with the same application. for (int i = 0; i < rowMap.count(); ++i) { const QModelIndex &groupRep = sourceModel()->index(rowMap.at(i)->constFirst(), 0); // Don't match a row with itself. if (sourceIndex == groupRep) { continue; } if (appsMatch(sourceIndex, groupRep)) { const QModelIndex parent = index(i, 0); if (!silent) { const int newIndex = rowMap.at(i)->count(); if (newIndex == 1) { beginInsertRows(parent, 0, 1); } else { beginInsertRows(parent, newIndex, newIndex); } } rowMap[i]->append(sourceIndex.row()); if (!silent) { endInsertRows(); dataChanged(parent, parent); } return true; } } return false; } void NotificationGroupingProxyModel::adjustMap(int anchor, int delta) { for (int i = 0; i < rowMap.count(); ++i) { QVector *sourceRows = rowMap.at(i); QMutableVectorIterator it(*sourceRows); while (it.hasNext()) { it.next(); if (it.value() >= anchor) { it.setValue(it.value() + delta); } } } } void NotificationGroupingProxyModel::rebuildMap() { qDeleteAll(rowMap); rowMap.clear(); const int rows = sourceModel()->rowCount(); rowMap.reserve(rows); for (int i = 0; i < rows; ++i) { rowMap.append(new QVector{i}); } checkGrouping(true /* silent */); } void NotificationGroupingProxyModel::checkGrouping(bool silent) { for (int i = (rowMap.count()) - 1; i >= 0; --i) { if (isGroup(i)) { continue; } // FIXME support skip grouping hint, maybe? // The new grouping keeps every notification separate, still, so perhaps we don't need to if (tryToGroup(sourceModel()->index(rowMap.at(i)->constFirst(), 0), silent)) { beginRemoveRows(QModelIndex(), i, i); delete rowMap.takeAt(i); // Safe since we're iterating backwards. endRemoveRows(); } } } void NotificationGroupingProxyModel::formGroupFor(const QModelIndex &index) { // Already in group or a group. if (index.parent().isValid() || isGroup(index.row())) { return; } // We need to grab a source index as we may invalidate the index passed // in through grouping. const QModelIndex &sourceTarget = mapToSource(index); for (int i = (rowMap.count() - 1); i >= 0; --i) { const QModelIndex &sourceIndex = sourceModel()->index(rowMap.at(i)->constFirst(), 0); if (!appsMatch(sourceTarget, sourceIndex)) { continue; } if (tryToGroup(sourceIndex)) { beginRemoveRows(QModelIndex(), i, i); delete rowMap.takeAt(i); // Safe since we're iterating backwards. endRemoveRows(); } } } void NotificationGroupingProxyModel::setSourceModel(QAbstractItemModel *sourceModel) { if (sourceModel == QAbstractProxyModel::sourceModel()) { return; } beginResetModel(); if (QAbstractProxyModel::sourceModel()) { QAbstractProxyModel::sourceModel()->disconnect(this); } QAbstractProxyModel::setSourceModel(sourceModel); if (sourceModel) { rebuildMap(); // FIXME move this stuff into separate slot methods connect(sourceModel, &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex &parent, int start, int end) { if (parent.isValid()) { return; } adjustMap(start, (end - start) + 1); for (int i = start; i <= end; ++i) { if (!tryToGroup(this->sourceModel()->index(i, 0))) { beginInsertRows(QModelIndex(), rowMap.count(), rowMap.count()); rowMap.append(new QVector{i}); endInsertRows(); } } checkGrouping(); }); connect(sourceModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, [this](const QModelIndex &parent, int first, int last) { if (parent.isValid()) { return; } for (int i = first; i <= last; ++i) { for (int j = 0; j < rowMap.count(); ++j) { const QVector *sourceRows = rowMap.at(j); const int mapIndex = sourceRows->indexOf(i); if (mapIndex != -1) { // Remove top-level item. if (sourceRows->count() == 1) { beginRemoveRows(QModelIndex(), j, j); delete rowMap.takeAt(j); endRemoveRows(); // Dissolve group. } else if (sourceRows->count() == 2) { const QModelIndex parent = index(j, 0); beginRemoveRows(parent, 0, 1); rowMap[j]->remove(mapIndex); endRemoveRows(); // We're no longer a group parent. dataChanged(parent, parent); // Remove group member. } else { const QModelIndex parent = index(j, 0); beginRemoveRows(parent, mapIndex, mapIndex); rowMap[j]->remove(mapIndex); endRemoveRows(); // Various roles of the parent evaluate child data, and the // child list has changed. dataChanged(parent, parent); // Signal children count change for all other items in the group. emit dataChanged(index(0, 0, parent), index(rowMap.count() - 1, 0, parent), {Notifications::GroupChildrenCountRole}); } break; } } } }); connect(sourceModel, &QAbstractItemModel::rowsRemoved, this, [this](const QModelIndex &parent, int start, int end) { if (parent.isValid()) { return; } adjustMap(start + 1, -((end - start) + 1)); checkGrouping(); }); connect(sourceModel, &QAbstractItemModel::modelAboutToBeReset, this, &NotificationGroupingProxyModel::beginResetModel); connect(sourceModel, &QAbstractItemModel::modelReset, this, [this] { rebuildMap(); endResetModel(); }); connect(sourceModel, &QAbstractItemModel::dataChanged, this, [this](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { for (int i = topLeft.row(); i <= bottomRight.row(); ++i) { const QModelIndex &sourceIndex = this->sourceModel()->index(i, 0); QModelIndex proxyIndex = mapFromSource(sourceIndex); if (!proxyIndex.isValid()) { return; } const QModelIndex parent = proxyIndex.parent(); // If a child item changes, its parent may need an update as well as many of // the data roles evaluate child data. See data(). // TODO: Some roles do not need to bubble up as they fall through to the first // child in data(); it _might_ be worth adding constraints here later. if (parent.isValid()) { dataChanged(parent, parent, roles); } dataChanged(proxyIndex, proxyIndex, roles); } }); } endResetModel(); } QModelIndex NotificationGroupingProxyModel::index(int row, int column, const QModelIndex &parent) const { if (row < 0 || column != 0) { return QModelIndex(); } if (parent.isValid() && row < rowMap.at(parent.row())->count()) { return createIndex(row, column, rowMap.at(parent.row())); } if (row < rowMap.count()) { return createIndex(row, column, nullptr); } return QModelIndex(); } QModelIndex NotificationGroupingProxyModel::parent(const QModelIndex &child) const { if (child.internalPointer() == nullptr) { return QModelIndex(); } else { const int parentRow = rowMap.indexOf(static_cast *>(child.internalPointer())); if (parentRow != -1) { return index(parentRow, 0); } // If we were asked to find the parent for an internalPointer we can't // locate, we have corrupted data: This should not happen. Q_ASSERT(parentRow != -1); } return QModelIndex(); } QModelIndex NotificationGroupingProxyModel::mapFromSource(const QModelIndex &sourceIndex) const { if (!sourceIndex.isValid() || sourceIndex.model() != sourceModel()) { return QModelIndex(); } for (int i = 0; i < rowMap.count(); ++i) { const QVector *sourceRows = rowMap.at(i); const int childIndex = sourceRows->indexOf(sourceIndex.row()); const QModelIndex parent = index(i, 0); if (childIndex == 0) { // If the sub-list we found the source row in is larger than 1 (i.e. part // of a group, map to the logical child item instead of the parent item // the source row also stands in for. The parent is therefore unreachable // from mapToSource(). if (isGroup(i)) { return index(0, 0, parent); // Otherwise map to the top-level item. } else { return parent; } } else if (childIndex != -1) { return index(childIndex, 0, parent); } } return QModelIndex(); } QModelIndex NotificationGroupingProxyModel::mapToSource(const QModelIndex &proxyIndex) const { if (!proxyIndex.isValid() || proxyIndex.model() != this || !sourceModel()) { return QModelIndex(); } const QModelIndex &parent = proxyIndex.parent(); if (parent.isValid()) { if (parent.row() < 0 || parent.row() >= rowMap.count()) { return QModelIndex(); } return sourceModel()->index(rowMap.at(parent.row())->at(proxyIndex.row()), 0); } else { // Group parents items therefore equate to the first child item; the source // row logically appears twice in the proxy. // mapFromSource() is not required to handle this well (consider proxies can // filter out rows, too) and opts to map to the child item, as the group parent // has its Qt::DisplayRole mangled by data(), and it's more useful for trans- // lating dataChanged() from the source model. // NOTE we changed that to be last if (rowMap.isEmpty()) { // FIXME // How can this happen? (happens when closing a group) return QModelIndex(); } return sourceModel()->index(rowMap.at(proxyIndex.row())->constLast(), 0); } return QModelIndex(); } int NotificationGroupingProxyModel::rowCount(const QModelIndex &parent) const { if (!sourceModel()) { return 0; } if (parent.isValid() && parent.model() == this) { // Don't return row count for top-level item at child row: Group members // never have further children of their own. if (parent.parent().isValid()) { return 0; } if (parent.row() < 0 || parent.row() >= rowMap.count()) { return 0; } const int rowCount = rowMap.at(parent.row())->count(); // If this sub-list in the map only has one entry, it's a plain item, not // parent to a group. if (rowCount == 1) { return 0; } else { return rowCount; } } return rowMap.count(); } bool NotificationGroupingProxyModel::hasChildren(const QModelIndex &parent) const { if ((parent.model() && parent.model() != this) || !sourceModel()) { return false; } return rowCount(parent); } int NotificationGroupingProxyModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent) return 1; } QVariant NotificationGroupingProxyModel::data(const QModelIndex &proxyIndex, int role) const { if (!proxyIndex.isValid() || proxyIndex.model() != this || !sourceModel()) { return QVariant(); } const QModelIndex &parent = proxyIndex.parent(); const bool isGroup = (!parent.isValid() && this->isGroup(proxyIndex.row())); // For group parent items, this will map to the last child task. const QModelIndex &sourceIndex = mapToSource(proxyIndex); if (!sourceIndex.isValid()) { return QVariant(); } if (isGroup) { // For group parent items, DisplayRole is mapped to AppName of the first child. switch (role) { case Notifications::IsGroupRole: return true; case Notifications::GroupChildrenCountRole: return rowCount(proxyIndex); case Notifications::IsInGroupRole: return false; case Notifications::DesktopEntryRole: - for (int i = 0; i < rowCount(proxyIndex); ++i) { - const QString desktopEntry = index(i, 0, proxyIndex).data(Notifications::DesktopEntryRole).toString(); - if (!desktopEntry.isEmpty()) { - return desktopEntry; - } - } - return QString(); case Notifications::NotifyRcNameRole: + case Notifications::OriginNameRole: for (int i = 0; i < rowCount(proxyIndex); ++i) { - const QString notifyRcName = index(i, 0, proxyIndex).data(Notifications::NotifyRcNameRole).toString(); - if (!notifyRcName.isEmpty()) { - return notifyRcName; + const QString stringData = index(i, 0, proxyIndex).data(role).toString(); + if (!stringData.isEmpty()) { + return stringData; } } return QString(); - case Notifications::ConfigurableRole: // if there is any configurable child item for (int i = 0; i < rowCount(proxyIndex); ++i) { if (index(i, 0, proxyIndex).data(Notifications::ConfigurableRole).toBool()) { return true; } } return false; case Notifications::ClosableRole: // if there is any closable child item for (int i = 0; i < rowCount(proxyIndex); ++i) { if (index(i, 0, proxyIndex).data(Notifications::ClosableRole).toBool()) { return true; } } return false; } } else { switch (role) { case Notifications::IsGroupRole: return false; // So a notification knows with how many other items it is in a group case Notifications::GroupChildrenCountRole: if (proxyIndex.parent().isValid()) { return rowCount(proxyIndex.parent()); } break; case Notifications::IsInGroupRole: return parent.isValid(); } } return sourceIndex.data(role); }