Plasma-workspace

notificationgroupingproxymodel.cpp
1/*
2 SPDX-FileCopyrightText: 2016 Eike Hein <hein@kde.org>
3 SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik <kde@privat.broulik.de>
4
5 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
6*/
7
8#include "notificationgroupingproxymodel_p.h"
9
10#include <QDateTime>
11
12#include "notifications.h"
13
14using namespace NotificationManager;
15
16NotificationGroupingProxyModel::NotificationGroupingProxyModel(QObject *parent)
17 : QAbstractProxyModel(parent)
18{
19}
20
21NotificationGroupingProxyModel::~NotificationGroupingProxyModel() = default;
22
23bool NotificationGroupingProxyModel::appsMatch(const QModelIndex &a, const QModelIndex &b) const
24{
27
28 const QString aDesktopEntry = a.data(Notifications::DesktopEntryRole).toString();
29 const QString bDesktopEntry = b.data(Notifications::DesktopEntryRole).toString();
30
31 const QString aOriginName = a.data(Notifications::OriginNameRole).toString();
32 const QString bOriginName = b.data(Notifications::OriginNameRole).toString();
33
34 return !aName.isEmpty() && aName == bName && aDesktopEntry == bDesktopEntry && aOriginName == bOriginName;
35}
36
37bool NotificationGroupingProxyModel::isGroup(int row) const
38{
39 if (row < 0 || row >= rowMap.count()) {
40 return false;
41 }
42
43 return (rowMap.at(row)->count() > 1);
44}
45
46bool NotificationGroupingProxyModel::tryToGroup(const QModelIndex &sourceIndex, bool silent)
47{
48 // Meat of the matter: Try to add this source row to a sub-list with source rows
49 // associated with the same application.
50 for (int i = 0; i < rowMap.count(); ++i) {
51 const QModelIndex &groupRep = sourceModel()->index(rowMap.at(i)->constFirst(), 0);
52
53 // Don't match a row with itself.
54 if (sourceIndex == groupRep) {
55 continue;
56 }
57
58 if (appsMatch(sourceIndex, groupRep)) {
59 const QModelIndex parent = index(i, 0);
60
61 if (!silent) {
62 const int newIndex = rowMap.at(i)->count();
63
64 if (newIndex == 1) {
65 beginInsertRows(parent, 0, 1);
66 } else {
67 beginInsertRows(parent, newIndex, newIndex);
68 }
69 }
70
71 rowMap[i]->append(sourceIndex.row());
72
73 if (!silent) {
74 endInsertRows();
75
76 Q_EMIT dataChanged(parent, parent);
77 }
78
79 return true;
80 }
81 }
82
83 return false;
84}
85
86void NotificationGroupingProxyModel::adjustMap(int anchor, int delta)
87{
88 for (int i = 0; i < rowMap.count(); ++i) {
89 QList<int> *sourceRows = rowMap.at(i);
90 for (auto it = sourceRows->begin(); it != sourceRows->end(); ++it) {
91 if ((*it) >= anchor) {
92 *it += delta;
93 }
94 }
95 }
96}
97
98void NotificationGroupingProxyModel::rebuildMap()
99{
100 qDeleteAll(rowMap);
101 rowMap.clear();
102
103 const int rows = sourceModel()->rowCount();
104
105 rowMap.reserve(rows);
106
107 for (int i = 0; i < rows; ++i) {
108 rowMap.append(new QList<int>{i});
109 }
110
111 checkGrouping(true /* silent */);
112}
113
114void NotificationGroupingProxyModel::checkGrouping(bool silent)
115{
116 for (int i = (rowMap.count()) - 1; i >= 0; --i) {
117 if (isGroup(i)) {
118 continue;
119 }
120
121 // FIXME support skip grouping hint, maybe?
122 // The new grouping keeps every notification separate, still, so perhaps we don't need to
123
124 if (tryToGroup(sourceModel()->index(rowMap.at(i)->constFirst(), 0), silent)) {
125 beginRemoveRows(QModelIndex(), i, i);
126 delete rowMap.takeAt(i); // Safe since we're iterating backwards.
127 endRemoveRows();
128 }
129 }
130}
131
132void NotificationGroupingProxyModel::formGroupFor(const QModelIndex &index)
133{
134 // Already in group or a group.
135 if (index.parent().isValid() || isGroup(index.row())) {
136 return;
137 }
138
139 // We need to grab a source index as we may invalidate the index passed
140 // in through grouping.
141 const QModelIndex &sourceTarget = mapToSource(index);
142
143 for (int i = (rowMap.count() - 1); i >= 0; --i) {
144 const QModelIndex &sourceIndex = sourceModel()->index(rowMap.at(i)->constFirst(), 0);
145
146 if (!appsMatch(sourceTarget, sourceIndex)) {
147 continue;
148 }
149
150 if (tryToGroup(sourceIndex)) {
151 beginRemoveRows(QModelIndex(), i, i);
152 delete rowMap.takeAt(i); // Safe since we're iterating backwards.
153 endRemoveRows();
154 }
155 }
156}
157
158void NotificationGroupingProxyModel::setSourceModel(QAbstractItemModel *sourceModel)
159{
160 if (sourceModel == QAbstractProxyModel::sourceModel()) {
161 return;
162 }
163
164 beginResetModel();
165
167 QAbstractProxyModel::sourceModel()->disconnect(this);
168 }
169
171
172 if (sourceModel) {
173 rebuildMap();
174
175 // FIXME move this stuff into separate slot methods
176
177 connect(sourceModel, &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex &parent, int start, int end) {
178 if (parent.isValid()) {
179 return;
180 }
181
182 adjustMap(start, (end - start) + 1);
183
184 for (int i = start; i <= end; ++i) {
185 if (!tryToGroup(this->sourceModel()->index(i, 0))) {
186 beginInsertRows(QModelIndex(), rowMap.count(), rowMap.count());
187 rowMap.append(new QList<int>{i});
188 endInsertRows();
189 }
190 }
191
192 checkGrouping();
193 });
194
195 connect(sourceModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, [this](const QModelIndex &parent, int first, int last) {
196 if (parent.isValid()) {
197 return;
198 }
199
200 for (int i = first; i <= last; ++i) {
201 for (int j = 0; j < rowMap.count(); ++j) {
202 const QList<int> *sourceRows = rowMap.at(j);
203 const int mapIndex = sourceRows->indexOf(i);
204
205 if (mapIndex != -1) {
206 // Remove top-level item.
207 if (sourceRows->count() == 1) {
208 beginRemoveRows(QModelIndex(), j, j);
209 delete rowMap.takeAt(j);
210 endRemoveRows();
211 // Dissolve group.
212 } else if (sourceRows->count() == 2) {
213 const QModelIndex parent = index(j, 0);
214 beginRemoveRows(parent, 0, 1);
215 rowMap[j]->remove(mapIndex);
216 endRemoveRows();
217
218 // We're no longer a group parent.
219 Q_EMIT dataChanged(parent, parent);
220 // Remove group member.
221 } else {
222 const QModelIndex parent = index(j, 0);
223 beginRemoveRows(parent, mapIndex, mapIndex);
224 rowMap[j]->remove(mapIndex);
225 endRemoveRows();
226
227 // Various roles of the parent evaluate child data, and the
228 // child list has changed.
229 Q_EMIT dataChanged(parent, parent);
230
231 // Signal children count change for all other items in the group.
232 Q_EMIT dataChanged(index(0, 0, parent), index(rowMap.count() - 1, 0, parent), {Notifications::GroupChildrenCountRole});
233 }
234
235 break;
236 }
237 }
238 }
239 });
240
241 connect(sourceModel, &QAbstractItemModel::rowsRemoved, this, [this](const QModelIndex &parent, int start, int end) {
242 if (parent.isValid()) {
243 return;
244 }
245
246 adjustMap(start + 1, -((end - start) + 1));
247
248 checkGrouping();
249 });
250
251 connect(sourceModel, &QAbstractItemModel::modelAboutToBeReset, this, &NotificationGroupingProxyModel::beginResetModel);
252 connect(sourceModel, &QAbstractItemModel::modelReset, this, [this] {
253 rebuildMap();
254 endResetModel();
255 });
256
257 connect(sourceModel,
259 this,
260 [this](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles) {
261 for (int i = topLeft.row(); i <= bottomRight.row(); ++i) {
262 const QModelIndex &sourceIndex = this->sourceModel()->index(i, 0);
263 QModelIndex proxyIndex = mapFromSource(sourceIndex);
264
265 if (!proxyIndex.isValid()) {
266 return;
267 }
268
269 const QModelIndex parent = proxyIndex.parent();
270
271 // If a child item changes, its parent may need an update as well as many of
272 // the data roles evaluate child data. See data().
273 // TODO: Some roles do not need to bubble up as they fall through to the first
274 // child in data(); it _might_ be worth adding constraints here later.
275 if (parent.isValid()) {
276 Q_EMIT dataChanged(parent, parent, roles);
277 }
278
279 Q_EMIT dataChanged(proxyIndex, proxyIndex, roles);
280 }
281 });
282 }
283
284 endResetModel();
285}
286
287QModelIndex NotificationGroupingProxyModel::index(int row, int column, const QModelIndex &parent) const
288{
289 if (row < 0 || column != 0) {
290 return QModelIndex();
291 }
292
293 if (parent.isValid() && row < rowMap.at(parent.row())->count()) {
294 return createIndex(row, column, rowMap.at(parent.row()));
295 }
296
297 if (row < rowMap.count()) {
298 return createIndex(row, column, nullptr);
299 }
300
301 return QModelIndex();
302}
303
304QModelIndex NotificationGroupingProxyModel::parent(const QModelIndex &child) const
305{
306 if (child.internalPointer() == nullptr) {
307 return QModelIndex();
308 } else {
309 const int parentRow = rowMap.indexOf(static_cast<QList<int> *>(child.internalPointer()));
310
311 if (parentRow != -1) {
312 return index(parentRow, 0);
313 }
314
315 // If we were asked to find the parent for an internalPointer we can't
316 // locate, we have corrupted data: This should not happen.
317 Q_ASSERT(parentRow != -1);
318 }
319
320 return QModelIndex();
321}
322
323QModelIndex NotificationGroupingProxyModel::mapFromSource(const QModelIndex &sourceIndex) const
324{
325 if (!sourceIndex.isValid() || sourceIndex.model() != sourceModel()) {
326 return QModelIndex();
327 }
328
329 for (int i = 0; i < rowMap.count(); ++i) {
330 const QList<int> *sourceRows = rowMap.at(i);
331 const int childIndex = sourceRows->indexOf(sourceIndex.row());
332 const QModelIndex parent = index(i, 0);
333
334 if (childIndex == 0) {
335 // If the sub-list we found the source row in is larger than 1 (i.e. part
336 // of a group, map to the logical child item instead of the parent item
337 // the source row also stands in for. The parent is therefore unreachable
338 // from mapToSource().
339 if (isGroup(i)) {
340 return index(0, 0, parent);
341 // Otherwise map to the top-level item.
342 } else {
343 return parent;
344 }
345 } else if (childIndex != -1) {
346 return index(childIndex, 0, parent);
347 }
348 }
349
350 return QModelIndex();
351}
352
353QModelIndex NotificationGroupingProxyModel::mapToSource(const QModelIndex &proxyIndex) const
354{
355 if (!proxyIndex.isValid() || proxyIndex.model() != this || !sourceModel()) {
356 return QModelIndex();
357 }
358
359 const QModelIndex &parent = proxyIndex.parent();
360
361 if (parent.isValid()) {
362 if (parent.row() < 0 || parent.row() >= rowMap.count()) {
363 return QModelIndex();
364 }
365
366 return sourceModel()->index(rowMap.at(parent.row())->at(proxyIndex.row()), 0);
367 } else {
368 // Group parents items therefore equate to the first child item; the source
369 // row logically appears twice in the proxy.
370 // mapFromSource() is not required to handle this well (consider proxies can
371 // filter out rows, too) and opts to map to the child item, as the group parent
372 // has its Qt::DisplayRole mangled by data(), and it's more useful for trans-
373 // lating dataChanged() from the source model.
374 // NOTE we changed that to be last
375 if (rowMap.isEmpty()) { // FIXME
376 // How can this happen? (happens when closing a group)
377 return QModelIndex();
378 }
379 return sourceModel()->index(rowMap.at(proxyIndex.row())->constLast(), 0);
380 }
381
382 return QModelIndex();
383}
384
385int NotificationGroupingProxyModel::rowCount(const QModelIndex &parent) const
386{
387 if (!sourceModel()) {
388 return 0;
389 }
390
391 if (parent.isValid() && parent.model() == this) {
392 // Don't return row count for top-level item at child row: Group members
393 // never have further children of their own.
394 if (parent.parent().isValid()) {
395 return 0;
396 }
397
398 if (parent.row() < 0 || parent.row() >= rowMap.count()) {
399 return 0;
400 }
401
402 const int rowCount = rowMap.at(parent.row())->count();
403
404 // If this sub-list in the map only has one entry, it's a plain item, not
405 // parent to a group.
406 if (rowCount == 1) {
407 return 0;
408 } else {
409 return rowCount;
410 }
411 }
412
413 return rowMap.count();
414}
415
416bool NotificationGroupingProxyModel::hasChildren(const QModelIndex &parent) const
417{
418 if ((parent.model() && parent.model() != this) || !sourceModel()) {
419 return false;
420 }
421
422 return rowCount(parent);
423}
424
425int NotificationGroupingProxyModel::columnCount(const QModelIndex &parent) const
426{
427 Q_UNUSED(parent)
428
429 return 1;
430}
431
432QVariant NotificationGroupingProxyModel::data(const QModelIndex &proxyIndex, int role) const
433{
434 if (!proxyIndex.isValid() || proxyIndex.model() != this || !sourceModel()) {
435 return QVariant();
436 }
437
438 const QModelIndex &parent = proxyIndex.parent();
439 const bool isGroup = (!parent.isValid() && this->isGroup(proxyIndex.row()));
440
441 // For group parent items, this will map to the last child task.
442 const QModelIndex &sourceIndex = mapToSource(proxyIndex);
443
444 if (!sourceIndex.isValid()) {
445 return QVariant();
446 }
447
448 if (isGroup) {
449 switch (role) {
451 return true;
453 return rowCount(proxyIndex);
455 return false;
456
457 // Combine all notifications into one for some basic grouping
460 QString body;
461 for (int i = 0; i < rowCount(proxyIndex); ++i) {
462 const QString stringData = index(i, 0, proxyIndex).data(role).toString();
463 if (!stringData.isEmpty()) {
464 if (!body.isEmpty()) {
465 body.append(QLatin1String("<br>"));
466 }
467 body.append(stringData);
468 }
469 }
470 return body;
471 }
472
476 for (int i = 0; i < rowCount(proxyIndex); ++i) {
477 const QString stringData = index(i, 0, proxyIndex).data(role).toString();
478 if (!stringData.isEmpty()) {
479 return stringData;
480 }
481 }
482 return QString();
483
484 case Notifications::ConfigurableRole: // if there is any configurable child item
485 for (int i = 0; i < rowCount(proxyIndex); ++i) {
486 if (index(i, 0, proxyIndex).data(Notifications::ConfigurableRole).toBool()) {
487 return true;
488 }
489 }
490 return false;
491
492 case Notifications::ClosableRole: // if there is any closable child item
493 for (int i = 0; i < rowCount(proxyIndex); ++i) {
494 if (index(i, 0, proxyIndex).data(Notifications::ClosableRole).toBool()) {
495 return true;
496 }
497 }
498 return false;
499 }
500 } else {
501 switch (role) {
503 return false;
504 // So a notification knows with how many other items it is in a group
506 if (proxyIndex.parent().isValid()) {
507 return rowCount(proxyIndex.parent());
508 }
509 break;
511 return parent.isValid();
512 }
513 }
514
515 return sourceIndex.data(role);
516}
@ ApplicationNameRole
The user-visible name of the application (e.g. Spectacle)
@ ConfigurableRole
Whether the notification can be configured because a desktopEntry or notifyRcName is known,...
@ NotifyRcNameRole
The notifyrc name (e.g. spectaclerc) of the application that sent the notification.
@ BodyRole
The notification body text.
@ OriginNameRole
The name of the device or account the notification originally came from, e.g.
@ IsInGroupRole
Whether the notification is currently inside a group.
@ DesktopEntryRole
The desktop entry (without .desktop suffix, e.g. org.kde.spectacle) of the application that sent the ...
@ IsGroupRole
Whether the item is a group.
@ ClosableRole
Whether the item can be closed. Notifications are always closable, jobs are only when in JobStateStop...
@ GroupChildrenCountRole
The number of children in a group.
Q_SCRIPTABLE Q_NOREPLY void start()
const QList< QKeySequence > & end()
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList< int > &roles)
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const=0
void modelAboutToBeReset()
void rowsAboutToBeRemoved(const QModelIndex &parent, int first, int last)
void rowsInserted(const QModelIndex &parent, int first, int last)
void rowsRemoved(const QModelIndex &parent, int first, int last)
virtual void setSourceModel(QAbstractItemModel *sourceModel)
const_reference at(qsizetype i) const const
iterator begin()
qsizetype count() const const
iterator end()
qsizetype indexOf(const AT &value, qsizetype from) const const
QVariant data(int role) const const
void * internalPointer() const const
bool isValid() const const
const QAbstractItemModel * model() const const
QModelIndex parent() const const
int row() const const
QString & append(QChar ch)
bool isEmpty() const const
AccessibleDescriptionRole
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
QString toString() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 18 2024 12:14:59 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.