1/*
2 Copyright (c) 2008 Thomas McGuire <thomas.mcguire@gmx.net>
3 Copyright (c) 2012 Laurent Montel <montel@kde.org>
4
5 This library is free software; you can redistribute it and/or modify it
6 under the terms of the GNU Library General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or (at your
8 option) any later version.
9
10 This library is distributed in the hope that it will be useful, but WITHOUT
11 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
13 License for more details.
14
15 You should have received a copy of the GNU Library General Public License
16 along with this library; see the file COPYING.LIB. If not, write to the
17 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 02110-1301, USA.
19*/
20
21#include "collectionstatisticsdelegate.h"
22#include "collectionstatisticsmodel.h"
23
24#include <kcolorscheme.h>
25#include <kdebug.h>
26#include <kio/global.h>
27
28#include <QPainter>
29#include <QStyle>
30#include <QStyleOption>
31#include <QStyleOptionViewItemV4>
32#include <QAbstractItemView>
33#include <QTreeView>
34
35#include "entitytreemodel.h"
36#include "collectionstatistics.h"
37#include "collection.h"
38#include "progressspinnerdelegate_p.h"
39
40using namespace Akonadi;
41
42namespace Akonadi {
43
44enum CountType {
45 UnreadCount,
46 TotalCount
47};
48
49class CollectionStatisticsDelegatePrivate
50{
51public:
52 QAbstractItemView *parent;
53 bool drawUnreadAfterFolder;
54 DelegateAnimator *animator;
55 QColor mSelectedUnreadColor;
56 QColor mDeselectedUnreadColor;
57
58 CollectionStatisticsDelegatePrivate(QAbstractItemView *treeView)
59 : parent(treeView)
60 , drawUnreadAfterFolder(false)
61 , animator(0)
62 {
63 updateColor();
64 }
65
66 void getCountRecursive(const QModelIndex &index, qint64 &totalCount, qint64 &unreadCount, qint64 &totalSize) const
67 {
68 Collection collection = qvariant_cast<Collection>(index.data(EntityTreeModel::CollectionRole));
69 // Do not assert on invalid collections, since a collection may be deleted
70 // in the meantime and deleted collections are invalid.
71 if (collection.isValid()) {
72 CollectionStatistics statistics = collection.statistics();
73 totalCount += qMax(0LL, statistics.count());
74 unreadCount += qMax(0LL, statistics.unreadCount());
75 totalSize += qMax(0LL, statistics.size());
76 if (index.model()->hasChildren(index)) {
77 const int rowCount = index.model()->rowCount(index);
78 for (int row = 0; row < rowCount; row++) {
79 static const int column = 0;
80 getCountRecursive(index.model()->index(row, column, index), totalCount, unreadCount, totalSize);
81 }
82 }
83 }
84 }
85
86 void updateColor()
87 {
88 mSelectedUnreadColor = KColorScheme(QPalette::Active, KColorScheme::Selection)
89 .foreground(KColorScheme::LinkText).color();
90 mDeselectedUnreadColor = KColorScheme(QPalette::Active, KColorScheme::View)
91 .foreground(KColorScheme::LinkText).color();
92 }
93};
94
95}
96
97CollectionStatisticsDelegate::CollectionStatisticsDelegate(QAbstractItemView *parent)
98 : QStyledItemDelegate(parent)
99 , d_ptr(new CollectionStatisticsDelegatePrivate(parent))
100{
101
102}
103
104CollectionStatisticsDelegate::CollectionStatisticsDelegate(QTreeView *parent)
105 : QStyledItemDelegate(parent)
106 , d_ptr(new CollectionStatisticsDelegatePrivate(parent))
107{
108
109}
110
111CollectionStatisticsDelegate::~CollectionStatisticsDelegate()
112{
113 delete d_ptr;
114}
115
116void CollectionStatisticsDelegate::setUnreadCountShown(bool enable)
117{
118 Q_D(CollectionStatisticsDelegate);
119 d->drawUnreadAfterFolder = enable;
120}
121
122bool CollectionStatisticsDelegate::unreadCountShown() const
123{
124 Q_D(const CollectionStatisticsDelegate);
125 return d->drawUnreadAfterFolder;
126}
127
128void CollectionStatisticsDelegate::setProgressAnimationEnabled(bool enable)
129{
130 Q_D(CollectionStatisticsDelegate);
131 if (enable == (d->animator != 0)) {
132 return;
133 }
134 if (enable) {
135 Q_ASSERT(!d->animator);
136 Akonadi::DelegateAnimator *animator = new Akonadi::DelegateAnimator(d->parent);
137 d->animator = animator;
138 } else {
139 delete d->animator;
140 d->animator = 0;
141 }
142}
143
144bool CollectionStatisticsDelegate::progressAnimationEnabled() const
145{
146 Q_D(const CollectionStatisticsDelegate);
147 return d->animator != 0;
148}
149
150void CollectionStatisticsDelegate::initStyleOption(QStyleOptionViewItem *option,
151 const QModelIndex &index) const
152{
153 Q_D(const CollectionStatisticsDelegate);
154
155 QStyleOptionViewItemV4 *noTextOption =
156 qstyleoption_cast<QStyleOptionViewItemV4 *>(option);
157 QStyledItemDelegate::initStyleOption(noTextOption, index);
158 if (option->decorationPosition != QStyleOptionViewItem::Top) {
159 noTextOption->text.clear();
160 }
161
162 if (d->animator) {
163
164 const QVariant fetchState = index.data(Akonadi::EntityTreeModel::FetchStateRole);
165 if (!fetchState.isValid() || fetchState.toInt() != Akonadi::EntityTreeModel::FetchingState) {
166 d->animator->pop(index);
167 return;
168 }
169
170 d->animator->push(index);
171
172 if (QStyleOptionViewItemV4 *v4 = qstyleoption_cast<QStyleOptionViewItemV4 *>(option)) {
173 v4->icon = d->animator->sequenceFrame(index);
174 }
175 }
176}
177
178class PainterStateSaver
179{
180public:
181 PainterStateSaver(QPainter *painter)
182 {
183 mPainter = painter;
184 mPainter->save();
185 }
186
187 ~PainterStateSaver()
188 {
189 mPainter->restore();
190 }
191
192private:
193 QPainter *mPainter;
194};
195
196void CollectionStatisticsDelegate::paint(QPainter *painter,
197 const QStyleOptionViewItem &option,
198 const QModelIndex &index) const
199{
200 Q_D(const CollectionStatisticsDelegate);
201 PainterStateSaver stateSaver(painter);
202
203 const QColor textColor = index.data(Qt::ForegroundRole).value<QColor>();
204 // First, paint the basic, but without the text. We remove the text
205 // in initStyleOption(), which gets called by QStyledItemDelegate::paint().
206 QStyledItemDelegate::paint(painter, option, index);
207
208 // No, we retrieve the correct style option by calling intiStyleOption from
209 // the superclass.
210 QStyleOptionViewItemV4 option4 = option;
211 QStyledItemDelegate::initStyleOption(&option4, index);
212 QString text = option4.text;
213
214 // Now calculate the rectangle for the text
215 QStyle *s = d->parent->style();
216 const QWidget *widget = option4.widget;
217 const QRect textRect = s->subElementRect(QStyle::SE_ItemViewItemText, &option4, widget);
218
219 // When checking if the item is expanded, we need to check that for the first
220 // column, as Qt only recognises the index as expanded for the first column
221 const QModelIndex firstColumn = index.sibling(index.row(), 0);
222 QTreeView *treeView = qobject_cast<QTreeView *>(d->parent);
223 bool expanded = treeView && treeView->isExpanded(firstColumn);
224
225 if (option.state & QStyle::State_Selected) {
226 painter->setPen(textColor.isValid() ? textColor : option.palette.highlightedText().color());
227 }
228
229 Collection collection = firstColumn.data(EntityTreeModel::CollectionRole).value<Collection>();
230
231 if (!collection.isValid()) {
232 kError() << "Invalid collection: " << collection;
233 }
234
235 Q_ASSERT(collection.isValid()); // TODO: I seem to hit this when removing a duplicated "Personal Contacts" or "Personal Calendar"
236
237 CollectionStatistics statistics = collection.statistics();
238
239 qint64 unreadCount = qMax(0LL, statistics.unreadCount());
240 qint64 totalRecursiveCount = 0;
241 qint64 unreadRecursiveCount = 0;
242 qint64 totalSize = 0;
243 bool needRecursiveCounts = false;
244 bool needTotalSize = false;
245 if (d->drawUnreadAfterFolder && index.column() == 0) {
246 needRecursiveCounts = true;
247 } else if ((index.column() == 1 || index.column() == 2)) {
248 needRecursiveCounts = true;
249 } else if (index.column() == 3 && !expanded) {
250 needTotalSize = true;
251 }
252
253 if (needRecursiveCounts || needTotalSize) {
254 d->getCountRecursive(firstColumn, totalRecursiveCount, unreadRecursiveCount, totalSize);
255 }
256
257 // Draw the unread count after the folder name (in parenthesis)
258 if (d->drawUnreadAfterFolder && index.column() == 0) {
259 // Construct the string which will appear after the foldername (with the
260 // unread count)
261 QString unread;
262// qDebug() << expanded << unreadCount << unreadRecursiveCount;
263 if (expanded && unreadCount > 0) {
264 unread = QString::fromLatin1(" (%1)").arg(unreadCount);
265 } else if (!expanded) {
266 if (unreadCount != unreadRecursiveCount) {
267 unread = QString::fromLatin1(" (%1 + %2)").arg(unreadCount).arg(unreadRecursiveCount - unreadCount);
268 } else if (unreadCount > 0) {
269 unread = QString::fromLatin1(" (%1)").arg(unreadCount);
270 }
271 }
272
273 PainterStateSaver stateSaver(painter);
274
275 if (!unread.isEmpty()) {
276 QFont font = painter->font();
277 font.setBold(true);
278 painter->setFont(font);
279 }
280
281 const QColor unreadColor = (option.state & QStyle::State_Selected) ? d->mSelectedUnreadColor : d->mDeselectedUnreadColor;
282 const QRect iconRect = s->subElementRect(QStyle::SE_ItemViewItemDecoration, &option4, widget);
283
284 if (option.decorationPosition == QStyleOptionViewItem::Left ||
285 option.decorationPosition == QStyleOptionViewItem::Right) {
286 // Squeeze the folder text if it is to big and calculate the rectangles
287 // where the folder text and the unread count will be drawn to
288 QString folderName = text;
289 QFontMetrics fm(painter->fontMetrics());
290 const int unreadWidth = fm.width(unread);
291 int folderWidth(fm.width(folderName));
292 const bool enoughPlaceForText = (option.rect.width() > (folderWidth + unreadWidth + iconRect.width()));
293
294 if (!enoughPlaceForText && (folderWidth + unreadWidth > textRect.width())) {
295 folderName = fm.elidedText(folderName, Qt::ElideRight,
296 option.rect.width() - unreadWidth - iconRect.width());
297 folderWidth = fm.width(folderName);
298 }
299 QRect folderRect = textRect;
300 QRect unreadRect = textRect;
301 folderRect.setRight(textRect.left() + folderWidth);
302 unreadRect = QRect(folderRect.right(), folderRect.top(), unreadRect.width(), unreadRect.height());
303 if (textColor.isValid()) {
304 painter->setPen(textColor);
305 }
306
307 // Draw folder name and unread count
308 painter->drawText(folderRect, Qt::AlignLeft | Qt::AlignVCenter, folderName);
309 painter->setPen(unreadColor);
310 painter->drawText(unreadRect, Qt::AlignLeft | Qt::AlignVCenter, unread);
311 } else if (option.decorationPosition == QStyleOptionViewItem::Top) {
312 if (unreadCount > 0) {
313 // draw over the icon
314 painter->setPen(unreadColor);
315 painter->drawText(iconRect, Qt::AlignCenter, QString::number(unreadCount));
316 }
317 }
318 return;
319 }
320
321 // For the unread/total column, paint the summed up count if the item
322 // is collapsed
323 if ((index.column() == 1 || index.column() == 2)) {
324
325 QFont savedFont = painter->font();
326 QString sumText;
327 if (index.column() == 1 && ((!expanded && unreadRecursiveCount > 0) || (expanded && unreadCount > 0))) {
328 QFont font = painter->font();
329 font.setBold(true);
330 painter->setFont(font);
331 sumText = QString::number(expanded ? unreadCount : unreadRecursiveCount);
332 } else {
333
334 qint64 totalCount = statistics.count();
335 if (index.column() == 2 && ((!expanded && totalRecursiveCount > 0) || (expanded && totalCount > 0))) {
336 sumText = QString::number(expanded ? totalCount : totalRecursiveCount);
337 }
338 }
339
340 painter->drawText(textRect, Qt::AlignRight | Qt::AlignVCenter, sumText);
341 painter->setFont(savedFont);
342 return;
343 }
344
345 //total size
346 if (index.column() == 3 && !expanded) {
347 if (textColor.isValid()) {
348 painter->setPen(textColor);
349 }
350 painter->drawText(textRect, option4.displayAlignment | Qt::AlignVCenter, KIO::convertSize((KIO::filesize_t)totalSize));
351 return;
352 }
353
354 if (textColor.isValid()) {
355 painter->setPen(textColor);
356 }
357 painter->drawText(textRect, option4.displayAlignment | Qt::AlignVCenter, text);
358}
359
360void CollectionStatisticsDelegate::updatePalette()
361{
362 Q_D(CollectionStatisticsDelegate);
363 d->updateColor();
364}
365