KItemViews

kextendableitemdelegate.cpp
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
39
40 /**
41 * Delete all active extenders
42 */
43 void deleteExtenders();
44
45 // this will trigger a lot of auto-casting QModelIndex <-> QPersistentModelIndex
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
60 : QStyledItemDelegate(parent)
61 , d(new KExtendableItemDelegatePrivate(this))
62{
63 connect(parent->verticalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(_k_verticalScroll()));
64}
65
66KExtendableItemDelegate::~KExtendableItemDelegate() = default;
67
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(d->indexOfExtendedColumnInSameRow(index));
78 d->stateTick++;
79 // reparent, as promised in the docs
80 QAbstractItemView *aiv = qobject_cast<QAbstractItemView *>(parent());
81 if (!aiv) {
82 return;
83 }
84 ext->setParent(aiv->viewport());
85 d->extenders.insert(index, ext);
86 d->extenderIndices.insert(ext, index);
87 connect(ext, SIGNAL(destroyed(QObject *)), this, SLOT(_k_extenderDestructionHandler(QObject *)));
88 Q_EMIT extenderCreated(ext, index);
89 d->scheduleUpdateViewLayout();
90}
91
93{
94 QWidget *extender = d->extenders.value(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(extender);
103 d->extenders.remove(persistentIndex);
104
105 d->deletionQueue.insert(extender, persistentIndex);
106
107 d->scheduleUpdateViewLayout();
108}
109
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(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(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
148{
149 return d->extenders.value(index);
150}
151
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, 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(&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(&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, ShowExtensionIndicatorRole).toBool();
196
197 if (showExtensionIndicator) {
198 const QSize extendPixmapSize = d->extendPixmap.size() / d->extendPixmap.devicePixelRatio();
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, itemOption, index);
214 if (showExtensionIndicator) {
215 painter->save();
217 painter->restore();
218 painter->drawPixmap(indicatorX, indicatorY, 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(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, itemOption, index);
241 if (showExtensionIndicator) {
242 painter->save();
244 painter->restore();
245 painter->drawPixmap(indicatorX, indicatorY, 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(&extOption, index);
254 extOption.rect = extenderRect(d->extender, option, index);
255 updateExtenderGeometry(d->extender, 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, 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();
274 painter->restore();
275
276 if (d->extenders.contains(index)) {
277 painter->drawPixmap(indicatorX, indicatorY, d->contractPixmap);
278 } else {
279 painter->drawPixmap(indicatorX, indicatorY, d->extendPixmap);
280 }
281 }
282}
283
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 *>(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 *>(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(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(row, column));
333 if (!neighborIndex.isValid()) {
334 break;
335 }
336 itemHeight = qMax(itemHeight, q->QStyledItemDelegate::sizeHint(option, 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, parentIndex));
354 if (extenders.value(indexOfExt)) {
355 return indexOfExt;
356 }
357 }
358
359 return QModelIndex();
360}
361
363{
364 Q_UNUSED(index);
365 extender->setGeometry(option.rect);
366}
367
368void KExtendableItemDelegatePrivate::deleteExtenders()
369{
370 for (QWidget *ext : std::as_const(extenders)) {
371 ext->hide();
372 ext->deleteLater();
373 }
374 deletionQueue.unite(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 *>(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
392{
393 d->extendPixmap = pixmap;
394}
395
397{
398 d->contractPixmap = pixmap;
399}
400
402{
403 return d->extendPixmap;
404}
405
407{
408 return d->contractPixmap;
409}
410
411#include "moc_kextendableitemdelegate.cpp"
This delegate makes it possible to display an arbitrary QWidget ("extender") that spans all columns b...
QPixmap contractPixmap()
Return the pixmap that is displayed to contract an item.
void contractItem(const QModelIndex &index)
Remove the extender of item at index from the view.
void contractAll()
Close all extenders and delete all extender widgets.
QPixmap extendPixmap()
Return the pixmap that is displayed to extend an item.
void setExtendPixmap(const QPixmap &pixmap)
The pixmap that is displayed to extend an item.
void setContractPixmap(const QPixmap &pixmap)
The pixmap that is displayed to contract an item.
QRect extenderRect(QWidget *extender, const QStyleOptionViewItem &option, const QModelIndex &index) const
Reimplement this function to fine-tune the position of the extender.
bool isExtended(const QModelIndex &index) const
Return whether there is an extender that belongs to index.
KExtendableItemDelegate(QAbstractItemView *parent)
Create a new KExtendableItemDelegate that belongs to parent.
virtual void updateExtenderGeometry(QWidget *extender, const QStyleOptionViewItem &option, const QModelIndex &index) const
Reimplement this function to adjust the internal geometry of the extender.
void extendItem(QWidget *extender, const QModelIndex &index)
Insert the extender for item at index into the view.
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
Re-implemented for internal reasons.
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override
Re-implemented for internal reasons.
void extenderDestroyed(QWidget *extender, const QModelIndex &index)
This signal indicates that the extender belonging to index has emitted the destroyed() signal.
void extenderCreated(QWidget *extender, const QModelIndex &index)
This signal indicates that the item at index was extended with extender.
virtual int columnCount(const QModelIndex &parent) const const=0
virtual QVariant data(const QModelIndex &index, int role) const const=0
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const=0
QModelIndex rootIndex() const const
virtual void setRootIndex(const QModelIndex &index)
QWidget * viewport() const const
QStyle * style()
bool isRightToLeft()
void clear()
T value(const Key &key) const const
int column() const const
bool isValid() const const
const QAbstractItemModel * model() const const
QModelIndex parent() const const
int row() const const
QModelIndex sibling(int row, int column) const const
T take(const Key &key)
QMultiHash< Key, T > & unite(const QHash< Key, T > &other)
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void deleteLater()
void destroyed(QObject *obj)
QObject * parent() const const
int receivers(const char *signal) const const
void drawPixmap(const QPoint &point, const QPixmap &pixmap)
void restore()
void save()
bool isValid() const const
int bottom() const const
void setLeft(int x)
void setRight(int x)
void setTop(int y)
int height() const const
int & rwidth()
int width() const const
PE_PanelItemViewItem
virtual void drawPrimitive(PrimitiveElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget) const const=0
virtual void initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const const
virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const const override
virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const const override
bool toBool() const const
void setGeometry(const QRect &)
void hide()
void setParent(QWidget *parent)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:17:32 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.