Akonadi

collectionstatisticsdelegate.cpp
1/*
2 SPDX-FileCopyrightText: 2008 Thomas McGuire <thomas.mcguire@gmx.net>
3 SPDX-FileCopyrightText: 2012-2025 Laurent Montel <montel@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "collectionstatisticsdelegate.h"
9
10#include "akonadiwidgets_debug.h"
11#include <KColorScheme>
12#include <KFormat>
13
14#include <QAbstractItemView>
15#include <QPainter>
16#include <QStyle>
17#include <QStyleOption>
18#include <QStyleOptionViewItem>
19#include <QTreeView>
20
21#include "collection.h"
22#include "collectionstatistics.h"
23#include "entitytreemodel.h"
24#include "progressspinnerdelegate_p.h"
25
26using namespace Akonadi;
27
28namespace Akonadi
29{
30enum CountType {
31 UnreadCount,
32 TotalCount,
33};
34
35class CollectionStatisticsDelegatePrivate
36{
37public:
38 QAbstractItemView *const parent;
39 bool drawUnreadAfterFolder = false;
40 DelegateAnimator *animator = nullptr;
41 std::array<QColor, 3> mSelectedUnreadColor;
42 std::array<QColor, 3> mDeselectedUnreadColor;
43
44 explicit CollectionStatisticsDelegatePrivate(QAbstractItemView *treeView)
45 : parent(treeView)
46 {
47 updateColor();
48 }
49
50 void getCountRecursive(const QModelIndex &index, qint64 &totalCount, qint64 &unreadCount, qint64 &totalSize) const
51 {
52 auto collection = qvariant_cast<Collection>(index.data(EntityTreeModel::CollectionRole));
53 // Do not assert on invalid collections, since a collection may be deleted
54 // in the meantime and deleted collections are invalid.
55 if (collection.isValid()) {
56 CollectionStatistics statistics = collection.statistics();
57 totalCount += qMax(0LL, statistics.count());
58 unreadCount += qMax(0LL, statistics.unreadCount());
59 totalSize += qMax(0LL, statistics.size());
60 if (index.model()->hasChildren(index)) {
61 const int rowCount = index.model()->rowCount(index);
62 for (int row = 0; row < rowCount; row++) {
63 static const int column = 0;
64 getCountRecursive(index.model()->index(row, column, index), totalCount, unreadCount, totalSize);
65 }
66 }
67 }
68 }
69
70 void updateColor()
71 {
72 static constexpr std::array states = {QPalette::Active, QPalette::Disabled, QPalette::Inactive};
73 for (const auto state : states) {
74 mSelectedUnreadColor[state] = KColorScheme(state, KColorScheme::Selection).foreground(KColorScheme::ActiveText).color();
75 mDeselectedUnreadColor[state] = KColorScheme(state, KColorScheme::View).foreground(KColorScheme::ActiveText).color();
76 }
77 }
78};
79
80} // namespace Akonadi
81
87
90 , d_ptr(new CollectionStatisticsDelegatePrivate(parent))
91{
92}
93
95
97{
99 d->drawUnreadAfterFolder = enable;
100}
101
103{
105 return d->drawUnreadAfterFolder;
106}
107
109{
111 if (enable == (d->animator != nullptr)) {
112 return;
113 }
114 if (enable) {
115 Q_ASSERT(!d->animator);
116 auto animator = new Akonadi::DelegateAnimator(d->parent);
117 d->animator = animator;
118 } else {
119 delete d->animator;
120 d->animator = nullptr;
121 }
122}
123
124bool CollectionStatisticsDelegate::progressAnimationEnabled() const
125{
127 return (d->animator != nullptr);
128}
129
131{
133
134 auto noTextOption = qstyleoption_cast<QStyleOptionViewItem *>(option);
135 QStyledItemDelegate::initStyleOption(noTextOption, index);
136 if (option->decorationPosition != QStyleOptionViewItem::Top) {
137 if (noTextOption) {
138 noTextOption->text.clear();
139 }
140 }
141
142 if (d->animator) {
144 if (!fetchState.isValid() || fetchState.toInt() != Akonadi::EntityTreeModel::FetchingState) {
145 d->animator->pop(index);
146 return;
147 }
148
149 d->animator->push(index);
150
151 if (auto v4 = qstyleoption_cast<QStyleOptionViewItem *>(option)) {
152 v4->icon = d->animator->sequenceFrame(index);
153 }
154 }
155}
156
157class PainterStateSaver
158{
159public:
160 explicit PainterStateSaver(QPainter *painter)
161 {
162 mPainter = painter;
163 mPainter->save();
164 }
165
166 ~PainterStateSaver()
167 {
168 mPainter->restore();
169 }
170
171private:
172 Q_DISABLE_COPY(PainterStateSaver)
173 QPainter *mPainter = nullptr;
174};
175
176void CollectionStatisticsDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
177{
179 PainterStateSaver stateSaver(painter);
180
181 const auto textColor = index.data(Qt::ForegroundRole).value<QColor>();
182 // First, paint the basic, but without the text. We remove the text
183 // in initStyleOption(), which gets called by QStyledItemDelegate::paint().
184 QStyledItemDelegate::paint(painter, option, index);
185
186 // Now, we retrieve the correct style option by calling initStyleOption from
187 // the superclass.
188 QStyleOptionViewItem option4 = option;
190 QString text = option4.text;
191
192 // Now calculate the rectangle for the text
193 QStyle *s = d->parent->style();
194 const QWidget *widget = option4.widget;
195 const QRect textRect = s->subElementRect(QStyle::SE_ItemViewItemText, &option4, widget);
196
197 // When checking if the item is expanded, we need to check that for the first
198 // column, as Qt only recognizes the index as expanded for the first column
199 const QModelIndex firstColumn = index.sibling(index.row(), 0);
200 auto treeView = qobject_cast<QTreeView *>(d->parent);
201 bool expanded = treeView && treeView->isExpanded(firstColumn);
202
204 painter->setPen(option.palette.color(QPalette::Disabled, QPalette::Text));
205 } else if (option.state & QStyle::State_Selected) {
206 painter->setPen(textColor.isValid() ? textColor : option.palette.highlightedText().color());
207 } else {
208 painter->setPen(textColor.isValid() ? textColor : option.palette.text().color());
209 }
210
211 auto collection = firstColumn.data(EntityTreeModel::CollectionRole).value<Collection>();
212
213 if (!collection.isValid()) {
214 qCCritical(AKONADIWIDGETS_LOG) << "Invalid collection at index" << firstColumn << firstColumn.data().toString() << "sibling of" << index
215 << "rowCount=" << index.model()->rowCount(index.parent()) << "parent=" << index.parent().data().toString();
216 return;
217 }
218
219 CollectionStatistics statistics = collection.statistics();
220
221 qint64 unreadCount = qMax(0LL, statistics.unreadCount());
222 qint64 totalRecursiveCount = 0;
223 qint64 unreadRecursiveCount = 0;
224 qint64 totalSize = 0;
225 bool needRecursiveCounts = false;
226 bool needTotalSize = false;
227 if ((d->drawUnreadAfterFolder && index.column() == 0) || (index.column() == 1 || index.column() == 2)) {
228 needRecursiveCounts = true;
229 } else if (index.column() == 3 && !expanded) {
230 needTotalSize = true;
231 }
232
233 if (needRecursiveCounts || needTotalSize) {
234 d->getCountRecursive(firstColumn, totalRecursiveCount, unreadRecursiveCount, totalSize);
235 }
236
237 // Draw the unread count after the folder name (in parenthesis)
238 if (d->drawUnreadAfterFolder && index.column() == 0) {
239 // Construct the string which will appear after the foldername (with the
240 // unread count)
241 QString unread;
242 // qCDebug(AKONADIWIDGETS_LOG) << expanded << unreadCount << unreadRecursiveCount;
243 if (expanded && unreadCount > 0) {
244 unread = QStringLiteral(" (%1)").arg(unreadCount);
245 } else if (!expanded) {
246 if (unreadCount != unreadRecursiveCount) {
247 unread = QStringLiteral(" (%1 + %2)").arg(unreadCount).arg(unreadRecursiveCount - unreadCount);
248 } else if (unreadCount > 0) {
249 unread = QStringLiteral(" (%1)").arg(unreadCount);
250 }
251 }
252
253 PainterStateSaver stateSaver(painter);
254
255 if (!unread.isEmpty()) {
256 QFont font = painter->font();
257 font.setBold(true);
258 painter->setFont(font);
259 }
260
261 const QPalette::ColorGroup group =
263 const QColor unreadColor = (option.state & QStyle::State_Selected) ? d->mSelectedUnreadColor[group] : d->mDeselectedUnreadColor[group];
264 const QRect iconRect = s->subElementRect(QStyle::SE_ItemViewItemDecoration, &option4, widget);
265
266 if (option.decorationPosition == QStyleOptionViewItem::Left || option.decorationPosition == QStyleOptionViewItem::Right) {
267 // Squeeze the folder text if it is to big and calculate the rectangles
268 // where the folder text and the unread count will be drawn to
269 QString folderName = text;
270 QFontMetrics fm(painter->fontMetrics());
271 const int unreadWidth = fm.horizontalAdvance(unread);
272 int folderWidth(fm.horizontalAdvance(folderName));
273 const bool enoughPlaceForText = (option.rect.width() > (folderWidth + unreadWidth + iconRect.width()));
274
275 if (!enoughPlaceForText && (folderWidth + unreadWidth > textRect.width())) {
276 folderName = fm.elidedText(folderName, Qt::ElideRight, option.rect.width() - unreadWidth - iconRect.width());
277 folderWidth = fm.horizontalAdvance(folderName);
278 }
279 QRect folderRect = textRect;
280 QRect unreadRect = textRect;
281 folderRect.setRight(textRect.left() + folderWidth);
282 unreadRect = QRect(folderRect.right(), folderRect.top(), unreadWidth, unreadRect.height());
283
284 // Draw folder name and unread count
285 painter->drawText(folderRect, Qt::AlignLeft | Qt::AlignVCenter, folderName);
286 painter->setPen(unreadColor);
287 painter->drawText(unreadRect, Qt::AlignLeft | Qt::AlignVCenter, unread);
288 } else if (option.decorationPosition == QStyleOptionViewItem::Top) {
289 if (unreadCount > 0) {
290 // draw over the icon
291 // the iconRect is enlarged to the whole width of the item, in case the text is wider than the underlying icon
292 painter->setPen(unreadColor);
293 painter->drawText(QRect(option.rect.x(), iconRect.y(), option.rect.width(), iconRect.height()), Qt::AlignCenter, QString::number(unreadCount));
294 }
295 }
296 return;
297 }
298
299 // For the unread/total column, paint the summed up count if the item
300 // is collapsed
301 if ((index.column() == 1 || index.column() == 2)) {
302 QFont savedFont = painter->font();
303 QString sumText;
304 if (index.column() == 1 && ((!expanded && unreadRecursiveCount > 0) || (expanded && unreadCount > 0))) {
305 QFont font = painter->font();
306 font.setBold(true);
307 painter->setFont(font);
308 sumText = QString::number(expanded ? unreadCount : unreadRecursiveCount);
309 } else {
310 qint64 totalCount = statistics.count();
311 if (index.column() == 2 && ((!expanded && totalRecursiveCount > 0) || (expanded && totalCount > 0))) {
312 sumText = QString::number(expanded ? totalCount : totalRecursiveCount);
313 }
314 }
315
316 painter->drawText(textRect, Qt::AlignRight | Qt::AlignVCenter, sumText);
317 painter->setFont(savedFont);
318 return;
319 }
320
321 // total size
322 if (index.column() == 3 && !expanded) {
323 KFormat format;
324 painter->drawText(textRect, option4.displayAlignment | Qt::AlignVCenter, format.formatByteSize(totalSize));
325 return;
326 }
327
328 painter->drawText(textRect, option4.displayAlignment | Qt::AlignVCenter, text);
329}
330
336
337#include "moc_collectionstatisticsdelegate.cpp"
A delegate that draws unread and total count for StatisticsProxyModel.
void initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const override
bool unreadCountShown() const
Returns whether the unread count is drawn next to the folder name.
void setUnreadCountShown(bool enable)
Sets whether the unread count is drawn next to the folder name.
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
CollectionStatisticsDelegate(QAbstractItemView *parent)
Creates a new collection statistics delegate.
~CollectionStatisticsDelegate() override
Destroys the collection statistics delegate.
Provides statistics information of a Collection.
Represents a collection of PIM items.
Definition collection.h:62
@ FetchingState
There is a fetch of items in this collection in progress.
@ CollectionRole
The collection.
@ PendingCutRole
Used to indicate items which are to be cut.
@ FetchStateRole
Returns the FetchState of a particular item.
QString formatByteSize(double size, int precision=1, KFormat::BinaryUnitDialect dialect=KFormat::DefaultBinaryDialect, KFormat::BinarySizeUnits units=KFormat::DefaultBinaryUnits) const
Helper integration between Akonadi and Qt.
QAction * statistics(const QObject *recvr, const char *slot, QObject *parent)
virtual bool hasChildren(const QModelIndex &parent) const const
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const=0
virtual int rowCount(const QModelIndex &parent) const const=0
void setBold(bool enable)
QString elidedText(const QString &text, Qt::TextElideMode mode, int width, int flags) const const
int horizontalAdvance(QChar ch) const const
int column() const const
QVariant data(int role) const const
const QAbstractItemModel * model() const const
QModelIndex parent() const const
int row() const const
QModelIndex sibling(int row, int column) const const
QObject * parent() const const
T qobject_cast(QObject *object)
void drawText(const QPoint &position, const QString &text)
const QFont & font() const const
QFontMetrics fontMetrics() const const
void save()
void setFont(const QFont &font)
void setPen(Qt::PenStyle style)
int height() const const
int left() const const
int right() const const
void setRight(int x)
int top() const const
int width() const const
int y() const const
QString arg(Args &&... args) const const
bool isEmpty() const const
QString number(double n, char format, int precision)
SE_ItemViewItemText
virtual QRect subElementRect(SubElement element, const QStyleOption *option, const QWidget *widget) const const=0
QStyledItemDelegate(QObject *parent)
virtual void initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const const
virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const const override
AlignLeft
ForegroundRole
ElideRight
bool isValid() const const
bool toBool() const const
int toInt(bool *ok) const const
QString toString() const const
T value() const const
Q_D(Todo)
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Feb 21 2025 11:50:41 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.