Akonadi

collectionstatisticsdelegate.cpp
1 /*
2  SPDX-FileCopyrightText: 2008 Thomas McGuire <[email protected]>
3  SPDX-FileCopyrightText: 2012-2023 Laurent Montel <[email protected]>
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 <KIO/Global>
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 
26 using namespace Akonadi;
27 
28 namespace Akonadi
29 {
30 enum CountType {
31  UnreadCount,
32  TotalCount,
33 };
34 
35 class CollectionStatisticsDelegatePrivate
36 {
37 public:
38  QAbstractItemView *const parent;
39  bool drawUnreadAfterFolder = false;
40  DelegateAnimator *animator = nullptr;
41  QColor mSelectedUnreadColor;
42  QColor 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  {
74  }
75 };
76 
77 } // namespace Akonadi
78 
80  : QStyledItemDelegate(parent)
81  , d_ptr(new CollectionStatisticsDelegatePrivate(parent))
82 {
83 }
84 
86  : QStyledItemDelegate(parent)
87  , d_ptr(new CollectionStatisticsDelegatePrivate(parent))
88 {
89 }
90 
92 
94 {
96  d->drawUnreadAfterFolder = enable;
97 }
98 
100 {
102  return d->drawUnreadAfterFolder;
103 }
104 
106 {
108  if (enable == (d->animator != nullptr)) {
109  return;
110  }
111  if (enable) {
112  Q_ASSERT(!d->animator);
113  auto animator = new Akonadi::DelegateAnimator(d->parent);
114  d->animator = animator;
115  } else {
116  delete d->animator;
117  d->animator = nullptr;
118  }
119 }
120 
121 bool CollectionStatisticsDelegate::progressAnimationEnabled() const
122 {
124  return (d->animator != nullptr);
125 }
126 
128 {
130 
131  auto noTextOption = qstyleoption_cast<QStyleOptionViewItem *>(option);
132  QStyledItemDelegate::initStyleOption(noTextOption, index);
133  if (option->decorationPosition != QStyleOptionViewItem::Top) {
134  if (noTextOption) {
135  noTextOption->text.clear();
136  }
137  }
138 
139  if (d->animator) {
140  const QVariant fetchState = index.data(Akonadi::EntityTreeModel::FetchStateRole);
141  if (!fetchState.isValid() || fetchState.toInt() != Akonadi::EntityTreeModel::FetchingState) {
142  d->animator->pop(index);
143  return;
144  }
145 
146  d->animator->push(index);
147 
148  if (auto v4 = qstyleoption_cast<QStyleOptionViewItem *>(option)) {
149  v4->icon = d->animator->sequenceFrame(index);
150  }
151  }
152 }
153 
154 class PainterStateSaver
155 {
156 public:
157  explicit PainterStateSaver(QPainter *painter)
158  {
159  mPainter = painter;
160  mPainter->save();
161  }
162 
163  ~PainterStateSaver()
164  {
165  mPainter->restore();
166  }
167 
168 private:
169  Q_DISABLE_COPY(PainterStateSaver)
170  QPainter *mPainter = nullptr;
171 };
172 
173 void CollectionStatisticsDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
174 {
176  PainterStateSaver stateSaver(painter);
177 
178  const auto textColor = index.data(Qt::ForegroundRole).value<QColor>();
179  // First, paint the basic, but without the text. We remove the text
180  // in initStyleOption(), which gets called by QStyledItemDelegate::paint().
181  QStyledItemDelegate::paint(painter, option, index);
182 
183  // Now, we retrieve the correct style option by calling initStyleOption from
184  // the superclass.
185  QStyleOptionViewItem option4 = option;
186  QStyledItemDelegate::initStyleOption(&option4, index);
187  QString text = option4.text;
188 
189  // Now calculate the rectangle for the text
190  QStyle *s = d->parent->style();
191  const QWidget *widget = option4.widget;
192  const QRect textRect = s->subElementRect(QStyle::SE_ItemViewItemText, &option4, widget);
193 
194  // When checking if the item is expanded, we need to check that for the first
195  // column, as Qt only recognizes the index as expanded for the first column
196  const QModelIndex firstColumn = index.sibling(index.row(), 0);
197  auto treeView = qobject_cast<QTreeView *>(d->parent);
198  bool expanded = treeView && treeView->isExpanded(firstColumn);
199 
201  painter->setPen(option.palette.color(QPalette::Disabled, QPalette::Text));
202  } else if (option.state & QStyle::State_Selected) {
203  painter->setPen(textColor.isValid() ? textColor : option.palette.highlightedText().color());
204  } else {
205  painter->setPen(textColor.isValid() ? textColor : option.palette.text().color());
206  }
207 
208  auto collection = firstColumn.data(EntityTreeModel::CollectionRole).value<Collection>();
209 
210  if (!collection.isValid()) {
211  qCCritical(AKONADIWIDGETS_LOG) << "Invalid collection at index" << firstColumn << firstColumn.data().toString() << "sibling of" << index
212  << "rowCount=" << index.model()->rowCount(index.parent()) << "parent=" << index.parent().data().toString();
213  return;
214  }
215 
216  CollectionStatistics statistics = collection.statistics();
217 
218  qint64 unreadCount = qMax(0LL, statistics.unreadCount());
219  qint64 totalRecursiveCount = 0;
220  qint64 unreadRecursiveCount = 0;
221  qint64 totalSize = 0;
222  bool needRecursiveCounts = false;
223  bool needTotalSize = false;
224  if ((d->drawUnreadAfterFolder && index.column() == 0) || (index.column() == 1 || index.column() == 2)) {
225  needRecursiveCounts = true;
226  } else if (index.column() == 3 && !expanded) {
227  needTotalSize = true;
228  }
229 
230  if (needRecursiveCounts || needTotalSize) {
231  d->getCountRecursive(firstColumn, totalRecursiveCount, unreadRecursiveCount, totalSize);
232  }
233 
234  // Draw the unread count after the folder name (in parenthesis)
235  if (d->drawUnreadAfterFolder && index.column() == 0) {
236  // Construct the string which will appear after the foldername (with the
237  // unread count)
238  QString unread;
239  // qCDebug(AKONADIWIDGETS_LOG) << expanded << unreadCount << unreadRecursiveCount;
240  if (expanded && unreadCount > 0) {
241  unread = QStringLiteral(" (%1)").arg(unreadCount);
242  } else if (!expanded) {
243  if (unreadCount != unreadRecursiveCount) {
244  unread = QStringLiteral(" (%1 + %2)").arg(unreadCount).arg(unreadRecursiveCount - unreadCount);
245  } else if (unreadCount > 0) {
246  unread = QStringLiteral(" (%1)").arg(unreadCount);
247  }
248  }
249 
250  PainterStateSaver stateSaver(painter);
251 
252  if (!unread.isEmpty()) {
253  QFont font = painter->font();
254  font.setBold(true);
255  painter->setFont(font);
256  }
257 
258  const QColor unreadColor = (option.state & QStyle::State_Selected) ? d->mSelectedUnreadColor : d->mDeselectedUnreadColor;
259  const QRect iconRect = s->subElementRect(QStyle::SE_ItemViewItemDecoration, &option4, widget);
260 
261  if (option.decorationPosition == QStyleOptionViewItem::Left || option.decorationPosition == QStyleOptionViewItem::Right) {
262  // Squeeze the folder text if it is to big and calculate the rectangles
263  // where the folder text and the unread count will be drawn to
264  QString folderName = text;
265  QFontMetrics fm(painter->fontMetrics());
266  const int unreadWidth = fm.horizontalAdvance(unread);
267  int folderWidth(fm.horizontalAdvance(folderName));
268  const bool enoughPlaceForText = (option.rect.width() > (folderWidth + unreadWidth + iconRect.width()));
269 
270  if (!enoughPlaceForText && (folderWidth + unreadWidth > textRect.width())) {
271  folderName = fm.elidedText(folderName, Qt::ElideRight, option.rect.width() - unreadWidth - iconRect.width());
272  folderWidth = fm.horizontalAdvance(folderName);
273  }
274  QRect folderRect = textRect;
275  QRect unreadRect = textRect;
276  folderRect.setRight(textRect.left() + folderWidth);
277  unreadRect = QRect(folderRect.right(), folderRect.top(), unreadWidth, unreadRect.height());
278 
279  // Draw folder name and unread count
280  painter->drawText(folderRect, Qt::AlignLeft | Qt::AlignVCenter, folderName);
281  painter->setPen(unreadColor);
282  painter->drawText(unreadRect, Qt::AlignLeft | Qt::AlignVCenter, unread);
283  } else if (option.decorationPosition == QStyleOptionViewItem::Top) {
284  if (unreadCount > 0) {
285  // draw over the icon
286  // the iconRect is enlarged to the whole width of the item, in case the text is wider than the underlying icon
287  painter->setPen(unreadColor);
288  painter->drawText(QRect(option.rect.x(), iconRect.y(), option.rect.width(), iconRect.height()), Qt::AlignCenter, QString::number(unreadCount));
289  }
290  }
291  return;
292  }
293 
294  // For the unread/total column, paint the summed up count if the item
295  // is collapsed
296  if ((index.column() == 1 || index.column() == 2)) {
297  QFont savedFont = painter->font();
298  QString sumText;
299  if (index.column() == 1 && ((!expanded && unreadRecursiveCount > 0) || (expanded && unreadCount > 0))) {
300  QFont font = painter->font();
301  font.setBold(true);
302  painter->setFont(font);
303  sumText = QString::number(expanded ? unreadCount : unreadRecursiveCount);
304  } else {
305  qint64 totalCount = statistics.count();
306  if (index.column() == 2 && ((!expanded && totalRecursiveCount > 0) || (expanded && totalCount > 0))) {
307  sumText = QString::number(expanded ? totalCount : totalRecursiveCount);
308  }
309  }
310 
311  painter->drawText(textRect, Qt::AlignRight | Qt::AlignVCenter, sumText);
312  painter->setFont(savedFont);
313  return;
314  }
315 
316  // total size
317  if (index.column() == 3 && !expanded) {
318  painter->drawText(textRect, option4.displayAlignment | Qt::AlignVCenter, KIO::convertSize(totalSize));
319  return;
320  }
321 
322  painter->drawText(textRect, option4.displayAlignment | Qt::AlignVCenter, text);
323 }
324 
326 {
328  d->updateColor();
329 }
330 
331 #include "moc_collectionstatisticsdelegate.cpp"
const QColor & color() const const
virtual bool hasChildren(const QModelIndex &parent) const const
Provides statistics information of a Collection.
AlignLeft
void setPen(const QColor &color)
bool isValid() const const
ForegroundRole
SE_ItemViewItemText
KIOCORE_EXPORT QString convertSize(KIO::filesize_t size)
QString number(int n, int base)
~CollectionStatisticsDelegate() override
Destroys the collection statistics delegate.
virtual int rowCount(const QModelIndex &parent) const const=0
QModelIndex sibling(int row, int column) const const
int right() const const
int column() const const
T value() const const
int horizontalAdvance(const QString &text, int len) const const
bool unreadCountShown() const
Returns whether the unread count is drawn next to the folder name.
Represents a collection of PIM items.
Definition: collection.h:61
int width() const const
void drawText(const QPointF &position, const QString &text)
int left() const const
void setRight(int x)
QVariant data(int role) const const
int top() const const
virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const const override
@ CollectionRole
The collection.
void initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const override
bool isEmpty() const const
QFontMetrics fontMetrics() const const
int toInt(bool *ok) const const
QAction * statistics(const QObject *recvr, const char *slot, QObject *parent)
virtual void initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const const
QBrush foreground(ForegroundRole=NormalText) const
bool toBool() const const
int row() const const
@ PendingCutRole
Used to indicate items which are to be cut.
@ FetchingState
There is a fetch of items in this collection in progress.
ElideRight
CollectionStatisticsDelegate(QAbstractItemView *parent)
Creates a new collection statistics delegate.
int height() const const
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
virtual QRect subElementRect(QStyle::SubElement element, const QStyleOption *option, const QWidget *widget) const const=0
const QFont & font() const const
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const=0
void setUnreadCountShown(bool enable)
Sets whether the unread count is drawn next to the folder name.
QModelIndex parent() const const
void save()
@ FetchStateRole
Returns the FetchState of a particular item.
void setBold(bool enable)
void setFont(const QFont &font)
QString elidedText(const QString &text, Qt::TextElideMode mode, int width, int flags) const const
A delegate that draws unread and total count for StatisticsProxyModel.
QObject * parent() const const
const QAbstractItemModel * model() const const
Q_D(Todo)
QString toString() const const
Helper integration between Akonadi and Qt.
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Tue Dec 5 2023 03:52:44 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.