1 | /* |
2 | Copyright (c) 2009 Kevin Ottens <ervin@kde.org> |
3 | |
4 | This library is free software; you can redistribute it and/or modify it |
5 | under the terms of the GNU Library General Public License as published by |
6 | the Free Software Foundation; either version 2 of the License, or (at your |
7 | option) any later version. |
8 | |
9 | This library is distributed in the hope that it will be useful, but WITHOUT |
10 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
11 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public |
12 | License for more details. |
13 | |
14 | You should have received a copy of the GNU Library General Public License |
15 | along with this library; see the file COPYING.LIB. If not, write to the |
16 | Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
17 | 02110-1301, USA. |
18 | */ |
19 | |
20 | #include "statisticsproxymodel.h" |
21 | |
22 | #include "entitytreemodel.h" |
23 | #include "collectionutils_p.h" |
24 | |
25 | #include <akonadi/collectionquotaattribute.h> |
26 | #include <akonadi/collectionstatistics.h> |
27 | #include <akonadi/entitydisplayattribute.h> |
28 | |
29 | #include <kdebug.h> |
30 | #include <kiconloader.h> |
31 | #include <klocalizedstring.h> |
32 | #include <kio/global.h> |
33 | |
34 | #include <QApplication> |
35 | #include <QPalette> |
36 | #include <KIcon> |
37 | using namespace Akonadi; |
38 | |
39 | /** |
40 | * @internal |
41 | */ |
42 | class StatisticsProxyModel::Private |
43 | { |
44 | public: |
45 | Private(StatisticsProxyModel *parent) |
46 | : mParent(parent) |
47 | , mToolTipEnabled(false) |
48 | , mExtraColumnsEnabled(true) |
49 | { |
50 | } |
51 | |
52 | int sourceColumnCount(const QModelIndex &parent) |
53 | { |
54 | return mParent->sourceModel()->columnCount(mParent->mapToSource(parent)); |
55 | } |
56 | |
57 | void getCountRecursive(const QModelIndex &index, qint64 &totalSize) const |
58 | { |
59 | Collection collection = qvariant_cast<Collection>(index.data(EntityTreeModel::CollectionRole)); |
60 | // Do not assert on invalid collections, since a collection may be deleted |
61 | // in the meantime and deleted collections are invalid. |
62 | if (collection.isValid()) { |
63 | CollectionStatistics statistics = collection.statistics(); |
64 | totalSize += qMax(0LL, statistics.size()); |
65 | if (index.model()->hasChildren(index)) { |
66 | const int rowCount = index.model()->rowCount(index); |
67 | for (int row = 0; row < rowCount; row++) { |
68 | static const int column = 0; |
69 | getCountRecursive(index.model()->index(row, column, index), totalSize); |
70 | } |
71 | } |
72 | } |
73 | } |
74 | |
75 | QString toolTipForCollection(const QModelIndex &index, const Collection &collection) |
76 | { |
77 | QString bckColor = QApplication::palette().color(QPalette::ToolTipBase).name(); |
78 | QString txtColor = QApplication::palette().color(QPalette::ToolTipText).name(); |
79 | |
80 | QString tip = QString::fromLatin1( |
81 | "<table width=\"100%\" border=\"0\" cellpadding=\"2\" cellspacing=\"0\">\n" |
82 | ); |
83 | const QString textDirection = (QApplication::layoutDirection() == Qt::LeftToRight) ? QLatin1String("left" ) : QLatin1String("right" ); |
84 | tip += QString::fromLatin1( |
85 | " <tr>\n" |
86 | " <td bgcolor=\"%1\" colspan=\"2\" align=\"%4\" valign=\"middle\">\n" |
87 | " <div style=\"color: %2; font-weight: bold;\">\n" |
88 | " %3\n" |
89 | " </div>\n" |
90 | " </td>\n" |
91 | " </tr>\n" |
92 | ).arg(txtColor).arg(bckColor).arg(index.data(Qt::DisplayRole).toString()).arg(textDirection); |
93 | |
94 | tip += QString::fromLatin1( |
95 | " <tr>\n" |
96 | " <td align=\"%1\" valign=\"top\">\n" |
97 | ).arg(textDirection); |
98 | |
99 | QString tipInfo; |
100 | tipInfo += QString::fromLatin1( |
101 | " <strong>%1</strong>: %2<br>\n" |
102 | " <strong>%3</strong>: %4<br><br>\n" |
103 | ).arg(i18n("Total Messages" )).arg(collection.statistics().count()) |
104 | .arg(i18n("Unread Messages" )).arg(collection.statistics().unreadCount()); |
105 | |
106 | if (collection.hasAttribute<CollectionQuotaAttribute>()) { |
107 | CollectionQuotaAttribute *quota = collection.attribute<CollectionQuotaAttribute>(); |
108 | if (quota->currentValue() > -1 && quota->maximumValue() > 0) { |
109 | qreal percentage = (100.0 * quota->currentValue()) / quota->maximumValue(); |
110 | |
111 | if (qAbs(percentage) >= 0.01) { |
112 | QString percentStr = QString::number(percentage, 'f', 2); |
113 | tipInfo += QString::fromLatin1( |
114 | " <strong>%1</strong>: %2%<br>\n" |
115 | ).arg(i18n("Quota" )).arg(percentStr); |
116 | } |
117 | } |
118 | } |
119 | |
120 | qint64 currentFolderSize(collection.statistics().size()); |
121 | tipInfo += QString::fromLatin1( |
122 | " <strong>%1</strong>: %2<br>\n" |
123 | ).arg(i18n("Storage Size" )).arg(KIO::convertSize((KIO::filesize_t)(currentFolderSize))); |
124 | |
125 | qint64 totalSize = 0; |
126 | getCountRecursive(index, totalSize); |
127 | totalSize -= currentFolderSize; |
128 | if (totalSize > 0) { |
129 | tipInfo += QString::fromLatin1( |
130 | "<strong>%1</strong>: %2<br>" |
131 | ).arg(i18n("Subfolder Storage Size" )).arg(KIO::convertSize((KIO::filesize_t)(totalSize))); |
132 | } |
133 | |
134 | QString iconName = CollectionUtils::defaultIconName(collection); |
135 | if (collection.hasAttribute<EntityDisplayAttribute>() && |
136 | !collection.attribute<EntityDisplayAttribute>()->iconName().isEmpty()) { |
137 | if (!collection.attribute<EntityDisplayAttribute>()->activeIconName().isEmpty() && collection.statistics().unreadCount() > 0) { |
138 | iconName = collection.attribute<EntityDisplayAttribute>()->activeIconName(); |
139 | } else { |
140 | iconName = collection.attribute<EntityDisplayAttribute>()->iconName(); |
141 | } |
142 | } |
143 | |
144 | int iconSizes[] = { 32, 22 }; |
145 | int icon_size_found = 32; |
146 | |
147 | QString iconPath; |
148 | |
149 | for (int i = 0; i < 2; i++) { |
150 | iconPath = KIconLoader::global()->iconPath(iconName, -iconSizes[i], true); |
151 | if (!iconPath.isEmpty()) { |
152 | icon_size_found = iconSizes[i]; |
153 | break; |
154 | } |
155 | } |
156 | |
157 | if (iconPath.isEmpty()) { |
158 | iconPath = KIconLoader::global()->iconPath(QLatin1String("folder" ), -32, false); |
159 | } |
160 | |
161 | QString tipIcon = QString::fromLatin1( |
162 | " <table border=\"0\"><tr><td width=\"32\" height=\"32\" align=\"center\" valign=\"middle\">\n" |
163 | " <img src=\"%1\" width=\"%2\" height=\"32\">\n" |
164 | " </td></tr></table>\n" |
165 | " </td>\n" |
166 | ).arg(iconPath).arg(icon_size_found) ; |
167 | |
168 | if (QApplication::layoutDirection() == Qt::LeftToRight) { |
169 | tip += tipInfo + QString::fromLatin1("</td><td align=\"%3\" valign=\"top\">" ).arg(textDirection) + tipIcon; |
170 | } else { |
171 | tip += tipIcon + QString::fromLatin1("</td><td align=\"%3\" valign=\"top\">" ).arg(textDirection) + tipInfo; |
172 | } |
173 | |
174 | tip += QString::fromLatin1( |
175 | " </tr>" \ |
176 | "</table>" |
177 | ); |
178 | |
179 | return tip; |
180 | } |
181 | |
182 | void proxyDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); |
183 | |
184 | void sourceLayoutAboutToBeChanged(); |
185 | void sourceLayoutChanged(); |
186 | |
187 | QVector<QModelIndex> m_nonPersistent; |
188 | QVector<QModelIndex> m_nonPersistentFirstColumn; |
189 | QVector<QPersistentModelIndex> m_persistent; |
190 | QVector<QPersistentModelIndex> m_persistentFirstColumn; |
191 | |
192 | StatisticsProxyModel *mParent; |
193 | |
194 | bool mToolTipEnabled; |
195 | bool mExtraColumnsEnabled; |
196 | }; |
197 | |
198 | void StatisticsProxyModel::Private::proxyDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) |
199 | { |
200 | if (mExtraColumnsEnabled) { |
201 | // Ugly hack. |
202 | // The proper solution is a KExtraColumnsProxyModel, but this will do for now. |
203 | QModelIndex parent = topLeft.parent(); |
204 | int parentColumnCount = mParent->columnCount(parent); |
205 | QModelIndex = mParent->index(topLeft.row(), parentColumnCount - 1 - 3 , parent); |
206 | QModelIndex = mParent->index(bottomRight.row(), parentColumnCount - 1, parent); |
207 | mParent->disconnect(mParent, SIGNAL(dataChanged(QModelIndex,QModelIndex)), |
208 | mParent, SLOT(proxyDataChanged(QModelIndex,QModelIndex))); |
209 | emit mParent->dataChanged(extraTopLeft, extraBottomRight); |
210 | |
211 | // We get this signal when the statistics of a row changes. |
212 | // However, we need to emit data changed for the statistics of all ancestor rows too |
213 | // so that recursive totals can be updated. |
214 | while (parent.isValid()) { |
215 | emit mParent->dataChanged(parent.sibling(parent.row(), parentColumnCount - 1 - 3), |
216 | parent.sibling(parent.row(), parentColumnCount - 1)); |
217 | parent = parent.parent(); |
218 | parentColumnCount = mParent->columnCount(parent); |
219 | } |
220 | mParent->connect(mParent, SIGNAL(dataChanged(QModelIndex,QModelIndex)), |
221 | SLOT(proxyDataChanged(QModelIndex,QModelIndex))); |
222 | } |
223 | } |
224 | |
225 | void StatisticsProxyModel::Private::sourceLayoutAboutToBeChanged() |
226 | { |
227 | QModelIndexList persistent = mParent->persistentIndexList(); |
228 | const int columnCount = mParent->sourceModel()->columnCount(); |
229 | foreach (const QModelIndex &idx, persistent) { |
230 | if (idx.column() >= columnCount) { |
231 | m_nonPersistent.push_back(idx); |
232 | m_persistent.push_back(idx); |
233 | const QModelIndex firstColumn = idx.sibling(0, idx.column()); |
234 | m_nonPersistentFirstColumn.push_back(firstColumn); |
235 | m_persistentFirstColumn.push_back(firstColumn); |
236 | } |
237 | } |
238 | } |
239 | |
240 | void StatisticsProxyModel::Private::sourceLayoutChanged() |
241 | { |
242 | QModelIndexList oldList; |
243 | QModelIndexList newList; |
244 | |
245 | const int columnCount = mParent->sourceModel()->columnCount(); |
246 | |
247 | for (int i = 0; i < m_persistent.size(); ++i) { |
248 | const QModelIndex persistentIdx = m_persistent.at(i); |
249 | const QModelIndex nonPersistentIdx = m_nonPersistent.at(i); |
250 | if (m_persistentFirstColumn.at(i) != m_nonPersistentFirstColumn.at(i) && persistentIdx.column() >= columnCount) { |
251 | oldList.append(nonPersistentIdx); |
252 | newList.append(persistentIdx); |
253 | } |
254 | } |
255 | mParent->changePersistentIndexList(oldList, newList); |
256 | } |
257 | |
258 | void StatisticsProxyModel::setSourceModel(QAbstractItemModel *sourceModel) |
259 | { |
260 | // Order is important here. sourceLayoutChanged must be called *before* any downstreams react |
261 | // to the layoutChanged so that it can have the QPersistentModelIndexes uptodate in time. |
262 | disconnect(this, SIGNAL(layoutChanged()), this, SLOT(sourceLayoutChanged())); |
263 | connect(this, SIGNAL(layoutChanged()), SLOT(sourceLayoutChanged())); |
264 | QSortFilterProxyModel::setSourceModel(sourceModel); |
265 | // This one should come *after* any downstream handlers of layoutAboutToBeChanged. |
266 | // The connectNotify stuff below ensures that it remains the last one. |
267 | disconnect(this, SIGNAL(layoutAboutToBeChanged()), this, SLOT(sourceLayoutAboutToBeChanged())); |
268 | connect(this, SIGNAL(layoutAboutToBeChanged()), SLOT(sourceLayoutAboutToBeChanged())); |
269 | } |
270 | |
271 | void StatisticsProxyModel::connectNotify(const char *signal) |
272 | { |
273 | static bool ignore = false; |
274 | if (ignore || QLatin1String(signal) == SIGNAL(layoutAboutToBeChanged())) { |
275 | return QSortFilterProxyModel::connectNotify(signal); |
276 | } |
277 | ignore = true; |
278 | disconnect(this, SIGNAL(layoutAboutToBeChanged()), this, SLOT(sourceLayoutAboutToBeChanged())); |
279 | connect(this, SIGNAL(layoutAboutToBeChanged()), SLOT(sourceLayoutAboutToBeChanged())); |
280 | ignore = false; |
281 | QSortFilterProxyModel::connectNotify(signal); |
282 | } |
283 | |
284 | StatisticsProxyModel::StatisticsProxyModel(QObject *parent) |
285 | : QSortFilterProxyModel(parent) |
286 | , d(new Private(this)) |
287 | { |
288 | connect(this, SIGNAL(dataChanged(QModelIndex,QModelIndex)), |
289 | SLOT(proxyDataChanged(QModelIndex,QModelIndex))); |
290 | } |
291 | |
292 | StatisticsProxyModel::~StatisticsProxyModel() |
293 | { |
294 | delete d; |
295 | } |
296 | |
297 | void StatisticsProxyModel::setToolTipEnabled(bool enable) |
298 | { |
299 | d->mToolTipEnabled = enable; |
300 | } |
301 | |
302 | bool StatisticsProxyModel::isToolTipEnabled() const |
303 | { |
304 | return d->mToolTipEnabled; |
305 | } |
306 | |
307 | void StatisticsProxyModel::setExtraColumnsEnabled(bool enable) |
308 | { |
309 | d->mExtraColumnsEnabled = enable; |
310 | } |
311 | |
312 | bool StatisticsProxyModel::isExtraColumnsEnabled() const |
313 | { |
314 | return d->mExtraColumnsEnabled; |
315 | } |
316 | |
317 | QModelIndex Akonadi::StatisticsProxyModel::index(int row, int column, const QModelIndex &parent) const |
318 | { |
319 | if (!hasIndex(row, column, parent)) { |
320 | return QModelIndex(); |
321 | } |
322 | |
323 | int sourceColumn = column; |
324 | |
325 | if (column >= d->sourceColumnCount(parent)) { |
326 | sourceColumn = 0; |
327 | } |
328 | |
329 | QModelIndex i = QSortFilterProxyModel::index(row, sourceColumn, parent); |
330 | return createIndex(i.row(), column, i.internalPointer()); |
331 | } |
332 | |
333 | QVariant StatisticsProxyModel::data(const QModelIndex &index, int role) const |
334 | { |
335 | if (!sourceModel()) { |
336 | return QVariant(); |
337 | } |
338 | if (role == Qt::DisplayRole && index.column() >= d->sourceColumnCount(index.parent())) { |
339 | const QModelIndex sourceIndex = mapToSource(index.sibling(index.row(), 0)); |
340 | Collection collection = sourceModel()->data(sourceIndex, EntityTreeModel::CollectionRole).value<Collection>(); |
341 | |
342 | if (collection.isValid() && collection.statistics().count() >= 0) { |
343 | if (index.column() == d->sourceColumnCount(QModelIndex()) + 2) { |
344 | return KIO::convertSize((KIO::filesize_t)(collection.statistics().size())); |
345 | } else if (index.column() == d->sourceColumnCount(QModelIndex()) + 1) { |
346 | return collection.statistics().count(); |
347 | } else if (index.column() == d->sourceColumnCount(QModelIndex())) { |
348 | if (collection.statistics().unreadCount() > 0) { |
349 | return collection.statistics().unreadCount(); |
350 | } else { |
351 | return QString(); |
352 | } |
353 | } else { |
354 | kWarning() << "We shouldn't get there for a column which is not total, unread or size." ; |
355 | return QVariant(); |
356 | } |
357 | } |
358 | |
359 | } else if (role == Qt::TextAlignmentRole && index.column() >= d->sourceColumnCount(index.parent())) { |
360 | return Qt::AlignRight; |
361 | |
362 | } else if (role == Qt::ToolTipRole && d->mToolTipEnabled) { |
363 | const QModelIndex sourceIndex = mapToSource(index.sibling(index.row(), 0)); |
364 | Collection collection = sourceModel()->data(sourceIndex, EntityTreeModel::CollectionRole).value<Collection>(); |
365 | |
366 | if (collection.isValid() && collection.statistics().count() > 0) { |
367 | return d->toolTipForCollection(index, collection); |
368 | } |
369 | |
370 | } else if (role == Qt::DecorationRole && index.column() == 0) { |
371 | const QModelIndex sourceIndex = mapToSource(index.sibling(index.row(), 0)); |
372 | Collection collection = sourceModel()->data(sourceIndex, EntityTreeModel::CollectionRole).value<Collection>(); |
373 | |
374 | if (collection.isValid()) { |
375 | return KIcon(CollectionUtils::displayIconName(collection)); |
376 | } else { |
377 | return QVariant(); |
378 | } |
379 | } |
380 | |
381 | return QAbstractProxyModel::data(index, role); |
382 | } |
383 | |
384 | QVariant StatisticsProxyModel::(int section, Qt::Orientation orientation, int role) const |
385 | { |
386 | if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { |
387 | if (section == d->sourceColumnCount(QModelIndex()) + 2) { |
388 | return i18nc("collection size" , "Size" ); |
389 | } else if (section == d->sourceColumnCount(QModelIndex()) + 1) { |
390 | return i18nc("number of entities in the collection" , "Total" ); |
391 | } else if (section == d->sourceColumnCount(QModelIndex())) { |
392 | return i18nc("number of unread entities in the collection" , "Unread" ); |
393 | } |
394 | } |
395 | |
396 | return QSortFilterProxyModel::headerData(section, orientation, role); |
397 | } |
398 | |
399 | Qt::ItemFlags StatisticsProxyModel::flags(const QModelIndex &index) const |
400 | { |
401 | if (index.column() >= d->sourceColumnCount(index.parent())) { |
402 | return QSortFilterProxyModel::flags(index.sibling(index.row(), 0)) |
403 | & (Qt::ItemIsSelectable | Qt::ItemIsDragEnabled // Allowed flags |
404 | | Qt::ItemIsDropEnabled | Qt::ItemIsEnabled); |
405 | } |
406 | |
407 | return QSortFilterProxyModel::flags(index); |
408 | } |
409 | |
410 | int StatisticsProxyModel::columnCount(const QModelIndex &parent) const |
411 | { |
412 | if (sourceModel() == 0) { |
413 | return 0; |
414 | } else { |
415 | return d->sourceColumnCount(parent) |
416 | + (d->mExtraColumnsEnabled ? 3 : 0); |
417 | } |
418 | } |
419 | |
420 | QModelIndexList StatisticsProxyModel::match(const QModelIndex &start, int role, const QVariant &value, |
421 | int hits, Qt::MatchFlags flags) const |
422 | { |
423 | if (role < Qt::UserRole) { |
424 | return QSortFilterProxyModel::match(start, role, value, hits, flags); |
425 | } |
426 | |
427 | QModelIndexList list; |
428 | QModelIndex proxyIndex; |
429 | foreach (const QModelIndex &idx, sourceModel()->match(mapToSource(start), role, value, hits, flags)) { |
430 | proxyIndex = mapFromSource(idx); |
431 | if (proxyIndex.isValid()) { |
432 | list << proxyIndex; |
433 | } |
434 | } |
435 | |
436 | return list; |
437 | } |
438 | |
439 | #include "moc_statisticsproxymodel.cpp" |
440 | |