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>
37using namespace Akonadi;
38
39/**
40 * @internal
41 */
42class StatisticsProxyModel::Private
43{
44public:
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
198void 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 extraTopLeft = mParent->index(topLeft.row(), parentColumnCount - 1 - 3 , parent);
206 QModelIndex extraBottomRight = 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
225void 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
240void 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
258void 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
271void 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
284StatisticsProxyModel::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
292StatisticsProxyModel::~StatisticsProxyModel()
293{
294 delete d;
295}
296
297void StatisticsProxyModel::setToolTipEnabled(bool enable)
298{
299 d->mToolTipEnabled = enable;
300}
301
302bool StatisticsProxyModel::isToolTipEnabled() const
303{
304 return d->mToolTipEnabled;
305}
306
307void StatisticsProxyModel::setExtraColumnsEnabled(bool enable)
308{
309 d->mExtraColumnsEnabled = enable;
310}
311
312bool StatisticsProxyModel::isExtraColumnsEnabled() const
313{
314 return d->mExtraColumnsEnabled;
315}
316
317QModelIndex 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
333QVariant 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
384QVariant StatisticsProxyModel::headerData(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
399Qt::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
410int 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
420QModelIndexList 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