1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2006, 2007 Andreas Hartmetz <ahartmetz@gmail.com>
4 SPDX-FileCopyrightText: 2008 Urs Wolfer <uwolfer@kde.org>
5
6 SPDX-License-Identifier: LGPL-2.0-or-later
7*/
8
9#include "kextendableitemdelegate.h"
10
11#include <QApplication>
12#include <QModelIndex>
13#include <QPainter>
14#include <QScrollBar>
15#include <QTreeView>
16
17class KExtendableItemDelegatePrivate
18{
19public:
20 KExtendableItemDelegatePrivate(KExtendableItemDelegate *parent)
21 : q(parent)
22 , stateTick(0)
23 , cachedStateTick(-1)
24 , cachedRow(-20)
25 , // Qt uses -1 for invalid indices
26 extenderHeight(0)
27
28 {
29 }
30
31 void _k_extenderDestructionHandler(QObject *destroyed);
32 void _k_verticalScroll();
33
34 QSize maybeExtendedSize(const QStyleOptionViewItem &option, const QModelIndex &index) const;
35 QModelIndex indexOfExtendedColumnInSameRow(const QModelIndex &index) const;
36 void scheduleUpdateViewLayout();
37
38 KExtendableItemDelegate *const q;
39
40 /**
41 * Delete all active extenders
42 */
43 void deleteExtenders();
44
45 // this will trigger a lot of auto-casting QModelIndex <-> QPersistentModelIndex
46 QHash<QPersistentModelIndex, QWidget *> extenders;
47 QHash<QWidget *, QPersistentModelIndex> extenderIndices;
48 QMultiHash<QWidget *, QPersistentModelIndex> deletionQueue;
49 QPixmap extendPixmap;
50 QPixmap contractPixmap;
51 int stateTick;
52 int cachedStateTick;
53 int cachedRow;
54 QModelIndex cachedParentIndex;
55 QWidget *extender = nullptr;
56 int extenderHeight;
57};
58
59KExtendableItemDelegate::KExtendableItemDelegate(QAbstractItemView *parent)
60 : QStyledItemDelegate(parent)
61 , d(new KExtendableItemDelegatePrivate(this))
62{
63 connect(sender: parent->verticalScrollBar(), SIGNAL(valueChanged(int)), receiver: this, SLOT(_k_verticalScroll()));
64}
65
66KExtendableItemDelegate::~KExtendableItemDelegate() = default;
67
68void KExtendableItemDelegate::extendItem(QWidget *ext, const QModelIndex &index)
69{
70 // qDebug() << "Creating extender at " << ext << " for item " << index.model()->data(index,Qt::DisplayRole).toString();
71
72 if (!ext || !index.isValid()) {
73 return;
74 }
75 // maintain the invariant "zero or one extender per row"
76 d->stateTick++;
77 contractItem(index: d->indexOfExtendedColumnInSameRow(index));
78 d->stateTick++;
79 // reparent, as promised in the docs
80 QAbstractItemView *aiv = qobject_cast<QAbstractItemView *>(object: parent());
81 if (!aiv) {
82 return;
83 }
84 ext->setParent(aiv->viewport());
85 d->extenders.insert(key: index, value: ext);
86 d->extenderIndices.insert(key: ext, value: index);
87 connect(sender: ext, SIGNAL(destroyed(QObject *)), receiver: this, SLOT(_k_extenderDestructionHandler(QObject *)));
88 Q_EMIT extenderCreated(extender: ext, index);
89 d->scheduleUpdateViewLayout();
90}
91
92void KExtendableItemDelegate::contractItem(const QModelIndex &index)
93{
94 QWidget *extender = d->extenders.value(key: index);
95 if (!extender) {
96 return;
97 }
98 // qDebug() << "Collapse extender at " << extender << " for item " << index.model()->data(index,Qt::DisplayRole).toString();
99 extender->hide();
100 extender->deleteLater();
101
102 QPersistentModelIndex persistentIndex = d->extenderIndices.take(key: extender);
103 d->extenders.remove(key: persistentIndex);
104
105 d->deletionQueue.insert(key: extender, value: persistentIndex);
106
107 d->scheduleUpdateViewLayout();
108}
109
110void KExtendableItemDelegate::contractAll()
111{
112 d->deleteExtenders();
113}
114
115// slot
116void KExtendableItemDelegatePrivate::_k_extenderDestructionHandler(QObject *destroyed)
117{
118 // qDebug() << "Removing extender at " << destroyed;
119
120 QWidget *extender = static_cast<QWidget *>(destroyed);
121 stateTick++;
122
123 QPersistentModelIndex persistentIndex = deletionQueue.take(key: extender);
124 if (persistentIndex.isValid() && q->receivers(SIGNAL(extenderDestroyed(QWidget *, QModelIndex))) != 0) {
125 QModelIndex index = persistentIndex;
126 Q_EMIT q->extenderDestroyed(extender, index);
127 }
128
129 scheduleUpdateViewLayout();
130}
131
132// slot
133void KExtendableItemDelegatePrivate::_k_verticalScroll()
134{
135 for (QWidget *extender : std::as_const(t&: extenders)) {
136 // Fast scrolling can lead to artifacts where extenders stay in the viewport
137 // of the parent's scroll area even though their items are scrolled out.
138 // Therefore we hide all extenders when scrolling.
139 // In paintEvent() show() will be called on actually visible extenders and
140 // Qt's double buffering takes care of eliminating flicker.
141 // ### This scales badly to many extenders. There are probably better ways to
142 // avoid the artifacts.
143 extender->hide();
144 }
145}
146
147bool KExtendableItemDelegate::isExtended(const QModelIndex &index) const
148{
149 return d->extenders.value(key: index);
150}
151
152QSize KExtendableItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
153{
154 QSize ret;
155
156 if (!d->extenders.isEmpty()) {
157 ret = d->maybeExtendedSize(option, index);
158 } else {
159 ret = QStyledItemDelegate::sizeHint(option, index);
160 }
161
162 bool showExtensionIndicator = index.model() ? index.model()->data(index, role: ShowExtensionIndicatorRole).toBool() : false;
163 if (showExtensionIndicator) {
164 ret.rwidth() += d->extendPixmap.width() / d->extendPixmap.devicePixelRatio();
165 }
166
167 return ret;
168}
169
170void KExtendableItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
171{
172 int indicatorX = 0;
173 int indicatorY = 0;
174
175 QStyleOptionViewItem indicatorOption(option);
176 initStyleOption(option: &indicatorOption, index);
177 if (index.column() == 0) {
178 indicatorOption.viewItemPosition = QStyleOptionViewItem::Beginning;
179 } else if (index.column() == index.model()->columnCount() - 1) {
180 indicatorOption.viewItemPosition = QStyleOptionViewItem::End;
181 } else {
182 indicatorOption.viewItemPosition = QStyleOptionViewItem::Middle;
183 }
184
185 QStyleOptionViewItem itemOption(option);
186 initStyleOption(option: &itemOption, index);
187 if (index.column() == 0) {
188 itemOption.viewItemPosition = QStyleOptionViewItem::Beginning;
189 } else if (index.column() == index.model()->columnCount() - 1) {
190 itemOption.viewItemPosition = QStyleOptionViewItem::End;
191 } else {
192 itemOption.viewItemPosition = QStyleOptionViewItem::Middle;
193 }
194
195 const bool showExtensionIndicator = index.model()->data(index, role: ShowExtensionIndicatorRole).toBool();
196
197 if (showExtensionIndicator) {
198 const QSize extendPixmapSize = d->extendPixmap.size() / d->extendPixmap.devicePixelRatio();
199 if (QApplication::isRightToLeft()) {
200 indicatorX = option.rect.right() - extendPixmapSize.width();
201 itemOption.rect.setRight(indicatorX);
202 indicatorOption.rect.setLeft(indicatorX);
203 } else {
204 indicatorX = option.rect.left();
205 indicatorOption.rect.setRight(indicatorX + extendPixmapSize.width());
206 itemOption.rect.setLeft(indicatorX + extendPixmapSize.width());
207 }
208 indicatorY = option.rect.top() + ((option.rect.height() - extendPixmapSize.height()) >> 1);
209 }
210
211 // fast path
212 if (d->extenders.isEmpty()) {
213 QStyledItemDelegate::paint(painter, option: itemOption, index);
214 if (showExtensionIndicator) {
215 painter->save();
216 QApplication::style()->drawPrimitive(pe: QStyle::PE_PanelItemViewItem, opt: &indicatorOption, p: painter);
217 painter->restore();
218 painter->drawPixmap(x: indicatorX, y: indicatorY, pm: d->extendPixmap);
219 }
220 return;
221 }
222
223 int row = index.row();
224 QModelIndex parentIndex = index.parent();
225
226 // indexOfExtendedColumnInSameRow() is very expensive, try to avoid calling it.
227 if (row != d->cachedRow //
228 || d->cachedStateTick != d->stateTick //
229 || d->cachedParentIndex != parentIndex) {
230 d->extender = d->extenders.value(key: d->indexOfExtendedColumnInSameRow(index));
231 d->cachedStateTick = d->stateTick;
232 d->cachedRow = row;
233 d->cachedParentIndex = parentIndex;
234 if (d->extender) {
235 d->extenderHeight = d->extender->sizeHint().height();
236 }
237 }
238
239 if (!d->extender) {
240 QStyledItemDelegate::paint(painter, option: itemOption, index);
241 if (showExtensionIndicator) {
242 painter->save();
243 QApplication::style()->drawPrimitive(pe: QStyle::PE_PanelItemViewItem, opt: &indicatorOption, p: painter);
244 painter->restore();
245 painter->drawPixmap(x: indicatorX, y: indicatorY, pm: d->extendPixmap);
246 }
247 return;
248 }
249
250 // an extender is present - make two rectangles: one to paint the original item, one for the extender
251 if (isExtended(index)) {
252 QStyleOptionViewItem extOption(option);
253 initStyleOption(option: &extOption, index);
254 extOption.rect = extenderRect(extender: d->extender, option, index);
255 updateExtenderGeometry(extender: d->extender, option: extOption, index);
256 // if we show it before, it will briefly flash in the wrong location.
257 // the downside is, of course, that an api user effectively can't hide it.
258 d->extender->show();
259 }
260
261 indicatorOption.rect.setHeight(option.rect.height() - d->extenderHeight);
262 itemOption.rect.setHeight(option.rect.height() - d->extenderHeight);
263 // tricky:make sure that the modified options' rect really has the
264 // same height as the unchanged option.rect if no extender is present
265 //(seems to work OK)
266 QStyledItemDelegate::paint(painter, option: itemOption, index);
267
268 if (showExtensionIndicator) {
269 const int extendPixmapHeight = d->extendPixmap.height() / d->extendPixmap.devicePixelRatio();
270 // indicatorOption's height changed, change this too
271 indicatorY = indicatorOption.rect.top() + ((indicatorOption.rect.height() - extendPixmapHeight) >> 1);
272 painter->save();
273 QApplication::style()->drawPrimitive(pe: QStyle::PE_PanelItemViewItem, opt: &indicatorOption, p: painter);
274 painter->restore();
275
276 if (d->extenders.contains(key: index)) {
277 painter->drawPixmap(x: indicatorX, y: indicatorY, pm: d->contractPixmap);
278 } else {
279 painter->drawPixmap(x: indicatorX, y: indicatorY, pm: d->extendPixmap);
280 }
281 }
282}
283
284QRect KExtendableItemDelegate::extenderRect(QWidget *extender, const QStyleOptionViewItem &option, const QModelIndex &index) const
285{
286 Q_ASSERT(extender);
287 QRect rect(option.rect);
288 rect.setTop(rect.bottom() + 1 - extender->sizeHint().height());
289
290 int indentation = 0;
291 if (QTreeView *tv = qobject_cast<QTreeView *>(object: parent())) {
292 int indentSteps = 0;
293 for (QModelIndex idx(index.parent()); idx.isValid(); idx = idx.parent()) {
294 indentSteps++;
295 }
296 if (tv->rootIsDecorated()) {
297 indentSteps++;
298 }
299 indentation = indentSteps * tv->indentation();
300 }
301
302 QAbstractScrollArea *container = qobject_cast<QAbstractScrollArea *>(object: parent());
303 Q_ASSERT(container);
304 if (qApp->isLeftToRight()) {
305 rect.setLeft(indentation);
306 rect.setRight(container->viewport()->width() - 1);
307 } else {
308 rect.setRight(container->viewport()->width() - 1 - indentation);
309 rect.setLeft(0);
310 }
311 return rect;
312}
313
314QSize KExtendableItemDelegatePrivate::maybeExtendedSize(const QStyleOptionViewItem &option, const QModelIndex &index) const
315{
316 QWidget *extender = extenders.value(key: index);
317 QSize size(q->QStyledItemDelegate::sizeHint(option, index));
318 if (!extender) {
319 return size;
320 }
321 // add extender height to maximum height of any column in our row
322 int itemHeight = size.height();
323
324 int row = index.row();
325 int thisColumn = index.column();
326
327 // this is quite slow, but Qt is smart about when to call sizeHint().
328 for (int column = 0; index.model()->columnCount() < column; column++) {
329 if (column == thisColumn) {
330 continue;
331 }
332 QModelIndex neighborIndex(index.sibling(arow: row, acolumn: column));
333 if (!neighborIndex.isValid()) {
334 break;
335 }
336 itemHeight = qMax(a: itemHeight, b: q->QStyledItemDelegate::sizeHint(option, index: neighborIndex).height());
337 }
338
339 // we only want to reserve vertical space, the horizontal extender layout is our private business.
340 size.rheight() = itemHeight + extender->sizeHint().height();
341 return size;
342}
343
344QModelIndex KExtendableItemDelegatePrivate::indexOfExtendedColumnInSameRow(const QModelIndex &index) const
345{
346 const QAbstractItemModel *const model = index.model();
347 const QModelIndex parentIndex(index.parent());
348 const int row = index.row();
349 const int columnCount = model->columnCount();
350
351 // slow, slow, slow
352 for (int column = 0; column < columnCount; column++) {
353 QModelIndex indexOfExt(model->index(row, column, parent: parentIndex));
354 if (extenders.value(key: indexOfExt)) {
355 return indexOfExt;
356 }
357 }
358
359 return QModelIndex();
360}
361
362void KExtendableItemDelegate::updateExtenderGeometry(QWidget *extender, const QStyleOptionViewItem &option, const QModelIndex &index) const
363{
364 Q_UNUSED(index);
365 extender->setGeometry(option.rect);
366}
367
368void KExtendableItemDelegatePrivate::deleteExtenders()
369{
370 for (QWidget *ext : std::as_const(t&: extenders)) {
371 ext->hide();
372 ext->deleteLater();
373 }
374 deletionQueue.unite(other: extenderIndices);
375 extenders.clear();
376 extenderIndices.clear();
377}
378
379// make the view re-ask for sizeHint() and redisplay items with their new size
380//### starting from Qt 4.4 we could emit sizeHintChanged() instead
381void KExtendableItemDelegatePrivate::scheduleUpdateViewLayout()
382{
383 QAbstractItemView *aiv = qobject_cast<QAbstractItemView *>(object: q->parent());
384 // prevent crashes during destruction of the view
385 if (aiv) {
386 // dirty hack to call aiv's protected scheduleDelayedItemsLayout()
387 aiv->setRootIndex(aiv->rootIndex());
388 }
389}
390
391void KExtendableItemDelegate::setExtendPixmap(const QPixmap &pixmap)
392{
393 d->extendPixmap = pixmap;
394}
395
396void KExtendableItemDelegate::setContractPixmap(const QPixmap &pixmap)
397{
398 d->contractPixmap = pixmap;
399}
400
401QPixmap KExtendableItemDelegate::extendPixmap()
402{
403 return d->extendPixmap;
404}
405
406QPixmap KExtendableItemDelegate::contractPixmap()
407{
408 return d->contractPixmap;
409}
410
411#include "moc_kextendableitemdelegate.cpp"
412

source code of kitemviews/src/kextendableitemdelegate.cpp