1 /*
2     SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@privat.broulik.de>
3 
4     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
5 */
6 
7 #include "notificationgroupcollapsingproxymodel_p.h"
8 
9 #include "notifications.h"
10 
11 #include "debug.h"
12 
13 using namespace NotificationManager;
14 
NotificationGroupCollapsingProxyModel(QObject * parent)15 NotificationGroupCollapsingProxyModel::NotificationGroupCollapsingProxyModel(QObject *parent)
16     : QSortFilterProxyModel(parent)
17 {
18 }
19 
20 NotificationGroupCollapsingProxyModel::~NotificationGroupCollapsingProxyModel() = default;
21 
setSourceModel(QAbstractItemModel * source)22 void NotificationGroupCollapsingProxyModel::setSourceModel(QAbstractItemModel *source)
23 {
24     if (source == QAbstractProxyModel::sourceModel()) {
25         return;
26     }
27 
28     if (QAbstractProxyModel::sourceModel()) {
29         disconnect(QAbstractProxyModel::sourceModel(), nullptr, this, nullptr);
30     }
31 
32     QSortFilterProxyModel::setSourceModel(source);
33 
34     if (source) {
35         connect(source, &QAbstractItemModel::rowsInserted, this, &NotificationGroupCollapsingProxyModel::invalidateFilter);
36         connect(source, &QAbstractItemModel::rowsRemoved, this, &NotificationGroupCollapsingProxyModel::invalidateFilter);
37 
38         // When a group is removed, there is no item that's being removed, instead the item morphs back into a single notification
39         connect(source,
40                 &QAbstractItemModel::dataChanged,
41                 this,
42                 [this, source](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles) {
43                     if (roles.isEmpty() || roles.contains(Notifications::IsGroupRole)) {
44                         for (int i = topLeft.row(); i <= bottomRight.row(); ++i) {
45                             const QModelIndex sourceIdx = source->index(i, 0);
46 
47                             if (!sourceIdx.data(Notifications::IsGroupRole).toBool()) {
48                                 if (m_expandedGroups.contains(sourceIdx)) {
49                                     setGroupExpanded(topLeft, false);
50                                 }
51                             }
52                         }
53                     }
54                 });
55     }
56 }
57 
data(const QModelIndex & index,int role) const58 QVariant NotificationGroupCollapsingProxyModel::data(const QModelIndex &index, int role) const
59 {
60     switch (role) {
61     case Notifications::IsGroupExpandedRole: {
62         if (m_limit > 0) {
63             // so each item in a group knows whether the group is expanded
64             const QModelIndex sourceIdx = mapToSource(index);
65             return m_expandedGroups.contains(sourceIdx.parent().isValid() ? sourceIdx.parent() : sourceIdx);
66         }
67         return true;
68     }
69     case Notifications::ExpandedGroupChildrenCountRole:
70         return rowCount(index.parent().isValid() ? index.parent() : index);
71     }
72 
73     return QSortFilterProxyModel::data(index, role);
74 }
75 
setData(const QModelIndex & index,const QVariant & value,int role)76 bool NotificationGroupCollapsingProxyModel::setData(const QModelIndex &index, const QVariant &value, int role)
77 {
78     if (role == Notifications::IsGroupExpandedRole && m_limit > 0) {
79         QModelIndex groupIdx = index;
80         // so an item inside a group can expand/collapse the group
81         if (groupIdx.parent().isValid()) {
82             groupIdx = groupIdx.parent();
83         }
84 
85         const bool expanded = value.toBool();
86         if (!groupIdx.data(Notifications::IsGroupRole).toBool()) {
87             qCWarning(NOTIFICATIONMANAGER) << "Cannot" << (expanded ? "expand" : "collapse") << "an item isn't a group or inside of one";
88             return false;
89         }
90 
91         return setGroupExpanded(groupIdx, expanded);
92     }
93 
94     return QSortFilterProxyModel::setData(index, value, role);
95 }
96 
limit() const97 int NotificationGroupCollapsingProxyModel::limit() const
98 {
99     return m_limit;
100 }
101 
setLimit(int limit)102 void NotificationGroupCollapsingProxyModel::setLimit(int limit)
103 {
104     if (m_limit != limit) {
105         m_limit = limit;
106         invalidateFilter();
107         invalidateGroupRoles();
108         emit limitChanged();
109     }
110 }
111 
lastRead() const112 QDateTime NotificationGroupCollapsingProxyModel::lastRead() const
113 {
114     return m_lastRead;
115 }
116 
setLastRead(const QDateTime & lastRead)117 void NotificationGroupCollapsingProxyModel::setLastRead(const QDateTime &lastRead)
118 {
119     if (m_lastRead != lastRead) {
120         m_lastRead = lastRead;
121         invalidateFilter();
122         invalidateGroupRoles();
123         emit lastReadChanged();
124     }
125 }
126 
expandUnread() const127 bool NotificationGroupCollapsingProxyModel::expandUnread() const
128 {
129     return m_expandUnread;
130 }
131 
setExpandUnread(bool expand)132 void NotificationGroupCollapsingProxyModel::setExpandUnread(bool expand)
133 {
134     if (m_expandUnread != expand) {
135         m_expandUnread = expand;
136         invalidateFilter();
137         invalidateGroupRoles();
138         emit expandUnreadChanged();
139     }
140 }
141 
collapseAll()142 void NotificationGroupCollapsingProxyModel::collapseAll()
143 {
144     m_expandedGroups.clear();
145 
146     invalidateFilter();
147     invalidateGroupRoles();
148 }
149 
setGroupExpanded(const QModelIndex & idx,bool expanded)150 bool NotificationGroupCollapsingProxyModel::setGroupExpanded(const QModelIndex &idx, bool expanded)
151 {
152     if (idx.data(Notifications::IsGroupExpandedRole).toBool() == expanded) {
153         return false;
154     }
155 
156     QPersistentModelIndex persistentIdx(mapToSource(idx));
157     if (expanded) {
158         m_expandedGroups.append(persistentIdx);
159     } else {
160         m_expandedGroups.removeOne(persistentIdx);
161     }
162 
163     invalidateFilter();
164 
165     const QVector<int> dirtyRoles = {Notifications::ExpandedGroupChildrenCountRole, Notifications::IsGroupExpandedRole};
166 
167     emit dataChanged(idx, idx, dirtyRoles);
168     emit dataChanged(index(0, 0, idx), index(rowCount(idx) - 1, 0, idx), dirtyRoles);
169 
170     return true;
171 }
172 
invalidateGroupRoles()173 void NotificationGroupCollapsingProxyModel::invalidateGroupRoles()
174 {
175     const QVector<int> dirtyRoles = {Notifications::ExpandedGroupChildrenCountRole, Notifications::IsGroupExpandedRole};
176 
177     emit dataChanged(index(0, 0), index(rowCount() - 1, 0), dirtyRoles);
178 
179     for (int row = 0; row < rowCount(); ++row) {
180         const QModelIndex groupIdx = index(row, 0);
181         emit dataChanged(index(0, 0, groupIdx), index(rowCount(groupIdx) - 1, 0, groupIdx), dirtyRoles);
182     }
183 }
184 
filterAcceptsRow(int source_row,const QModelIndex & source_parent) const185 bool NotificationGroupCollapsingProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
186 {
187     if (m_limit > 0 && source_parent.isValid()) {
188         if (!m_expandedGroups.isEmpty() && m_expandedGroups.contains(source_parent)) {
189             return true;
190         }
191 
192         if (m_expandUnread && m_lastRead.isValid()) {
193             const QModelIndex sourceIdx = sourceModel()->index(source_row, 0, source_parent);
194 
195             if (!sourceIdx.data(Notifications::ReadRole).toBool()) {
196                 QDateTime time = sourceIdx.data(Notifications::UpdatedRole).toDateTime();
197                 if (!time.isValid()) {
198                     time = sourceIdx.data(Notifications::CreatedRole).toDateTime();
199                 }
200 
201                 if (time.isValid() && m_lastRead < time) {
202                     return true;
203                 }
204             }
205         }
206 
207         // should we raise the limit when there's just one group?
208 
209         // FIXME why is this reversed?
210         // grouping proxy model seems to reverse the order?
211         return source_row >= sourceModel()->rowCount(source_parent) - m_limit;
212     }
213 
214     return true;
215 }
216