File indexing completed on 2024-09-29 12:08:57

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2006, 2007 Andreas Hartmetz <ahartmetz@gmail.com>
0004     SPDX-FileCopyrightText: 2008 Urs Wolfer <uwolfer@kde.org>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 #include "kextendableitemdelegate.h"
0010 
0011 #include <QApplication>
0012 #include <QModelIndex>
0013 #include <QPainter>
0014 #include <QScrollBar>
0015 #include <QTreeView>
0016 
0017 class KExtendableItemDelegatePrivate
0018 {
0019 public:
0020     KExtendableItemDelegatePrivate(KExtendableItemDelegate *parent)
0021         : q(parent)
0022         , stateTick(0)
0023         , cachedStateTick(-1)
0024         , cachedRow(-20)
0025         , // Qt uses -1 for invalid indices
0026         extenderHeight(0)
0027 
0028     {
0029     }
0030 
0031     void _k_extenderDestructionHandler(QObject *destroyed);
0032     void _k_verticalScroll();
0033 
0034     QSize maybeExtendedSize(const QStyleOptionViewItem &option, const QModelIndex &index) const;
0035     QModelIndex indexOfExtendedColumnInSameRow(const QModelIndex &index) const;
0036     void scheduleUpdateViewLayout();
0037 
0038     KExtendableItemDelegate *const q;
0039 
0040     /**
0041      * Delete all active extenders
0042      */
0043     void deleteExtenders();
0044 
0045     // this will trigger a lot of auto-casting QModelIndex <-> QPersistentModelIndex
0046     QHash<QPersistentModelIndex, QWidget *> extenders;
0047     QHash<QWidget *, QPersistentModelIndex> extenderIndices;
0048     QMultiHash<QWidget *, QPersistentModelIndex> deletionQueue;
0049     QPixmap extendPixmap;
0050     QPixmap contractPixmap;
0051     int stateTick;
0052     int cachedStateTick;
0053     int cachedRow;
0054     QModelIndex cachedParentIndex;
0055     QWidget *extender = nullptr;
0056     int extenderHeight;
0057 };
0058 
0059 KExtendableItemDelegate::KExtendableItemDelegate(QAbstractItemView *parent)
0060     : QStyledItemDelegate(parent)
0061     , d(new KExtendableItemDelegatePrivate(this))
0062 {
0063     connect(parent->verticalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(_k_verticalScroll()));
0064 }
0065 
0066 KExtendableItemDelegate::~KExtendableItemDelegate() = default;
0067 
0068 void KExtendableItemDelegate::extendItem(QWidget *ext, const QModelIndex &index)
0069 {
0070     // qDebug() << "Creating extender at " << ext << " for item " << index.model()->data(index,Qt::DisplayRole).toString();
0071 
0072     if (!ext || !index.isValid()) {
0073         return;
0074     }
0075     // maintain the invariant "zero or one extender per row"
0076     d->stateTick++;
0077     contractItem(d->indexOfExtendedColumnInSameRow(index));
0078     d->stateTick++;
0079     // reparent, as promised in the docs
0080     QAbstractItemView *aiv = qobject_cast<QAbstractItemView *>(parent());
0081     if (!aiv) {
0082         return;
0083     }
0084     ext->setParent(aiv->viewport());
0085     d->extenders.insert(index, ext);
0086     d->extenderIndices.insert(ext, index);
0087     connect(ext, SIGNAL(destroyed(QObject *)), this, SLOT(_k_extenderDestructionHandler(QObject *)));
0088     Q_EMIT extenderCreated(ext, index);
0089     d->scheduleUpdateViewLayout();
0090 }
0091 
0092 void KExtendableItemDelegate::contractItem(const QModelIndex &index)
0093 {
0094     QWidget *extender = d->extenders.value(index);
0095     if (!extender) {
0096         return;
0097     }
0098     // qDebug() << "Collapse extender at " << extender << " for item " << index.model()->data(index,Qt::DisplayRole).toString();
0099     extender->hide();
0100     extender->deleteLater();
0101 
0102     QPersistentModelIndex persistentIndex = d->extenderIndices.take(extender);
0103     d->extenders.remove(persistentIndex);
0104 
0105     d->deletionQueue.insert(extender, persistentIndex);
0106 
0107     d->scheduleUpdateViewLayout();
0108 }
0109 
0110 void KExtendableItemDelegate::contractAll()
0111 {
0112     d->deleteExtenders();
0113 }
0114 
0115 // slot
0116 void KExtendableItemDelegatePrivate::_k_extenderDestructionHandler(QObject *destroyed)
0117 {
0118     // qDebug() << "Removing extender at " << destroyed;
0119 
0120     QWidget *extender = static_cast<QWidget *>(destroyed);
0121     stateTick++;
0122 
0123     QPersistentModelIndex persistentIndex = deletionQueue.take(extender);
0124     if (persistentIndex.isValid() && q->receivers(SIGNAL(extenderDestroyed(QWidget *, QModelIndex))) != 0) {
0125         QModelIndex index = persistentIndex;
0126         Q_EMIT q->extenderDestroyed(extender, index);
0127     }
0128 
0129     scheduleUpdateViewLayout();
0130 }
0131 
0132 // slot
0133 void KExtendableItemDelegatePrivate::_k_verticalScroll()
0134 {
0135     for (QWidget *extender : std::as_const(extenders)) {
0136         // Fast scrolling can lead to artifacts where extenders stay in the viewport
0137         // of the parent's scroll area even though their items are scrolled out.
0138         // Therefore we hide all extenders when scrolling.
0139         // In paintEvent() show() will be called on actually visible extenders and
0140         // Qt's double buffering takes care of eliminating flicker.
0141         // ### This scales badly to many extenders. There are probably better ways to
0142         //     avoid the artifacts.
0143         extender->hide();
0144     }
0145 }
0146 
0147 bool KExtendableItemDelegate::isExtended(const QModelIndex &index) const
0148 {
0149     return d->extenders.value(index);
0150 }
0151 
0152 QSize KExtendableItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
0153 {
0154     QSize ret;
0155 
0156     if (!d->extenders.isEmpty()) {
0157         ret = d->maybeExtendedSize(option, index);
0158     } else {
0159         ret = QStyledItemDelegate::sizeHint(option, index);
0160     }
0161 
0162     bool showExtensionIndicator = index.model() ? index.model()->data(index, ShowExtensionIndicatorRole).toBool() : false;
0163     if (showExtensionIndicator) {
0164         ret.rwidth() += d->extendPixmap.width() / d->extendPixmap.devicePixelRatio();
0165     }
0166 
0167     return ret;
0168 }
0169 
0170 void KExtendableItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
0171 {
0172     int indicatorX = 0;
0173     int indicatorY = 0;
0174 
0175     QStyleOptionViewItem indicatorOption(option);
0176     initStyleOption(&indicatorOption, index);
0177     if (index.column() == 0) {
0178         indicatorOption.viewItemPosition = QStyleOptionViewItem::Beginning;
0179     } else if (index.column() == index.model()->columnCount() - 1) {
0180         indicatorOption.viewItemPosition = QStyleOptionViewItem::End;
0181     } else {
0182         indicatorOption.viewItemPosition = QStyleOptionViewItem::Middle;
0183     }
0184 
0185     QStyleOptionViewItem itemOption(option);
0186     initStyleOption(&itemOption, index);
0187     if (index.column() == 0) {
0188         itemOption.viewItemPosition = QStyleOptionViewItem::Beginning;
0189     } else if (index.column() == index.model()->columnCount() - 1) {
0190         itemOption.viewItemPosition = QStyleOptionViewItem::End;
0191     } else {
0192         itemOption.viewItemPosition = QStyleOptionViewItem::Middle;
0193     }
0194 
0195     const bool showExtensionIndicator = index.model()->data(index, ShowExtensionIndicatorRole).toBool();
0196 
0197     if (showExtensionIndicator) {
0198         const QSize extendPixmapSize = d->extendPixmap.size() / d->extendPixmap.devicePixelRatio();
0199         if (QApplication::isRightToLeft()) {
0200             indicatorX = option.rect.right() - extendPixmapSize.width();
0201             itemOption.rect.setRight(indicatorX);
0202             indicatorOption.rect.setLeft(indicatorX);
0203         } else {
0204             indicatorX = option.rect.left();
0205             indicatorOption.rect.setRight(indicatorX + extendPixmapSize.width());
0206             itemOption.rect.setLeft(indicatorX + extendPixmapSize.width());
0207         }
0208         indicatorY = option.rect.top() + ((option.rect.height() - extendPixmapSize.height()) >> 1);
0209     }
0210 
0211     // fast path
0212     if (d->extenders.isEmpty()) {
0213         QStyledItemDelegate::paint(painter, itemOption, index);
0214         if (showExtensionIndicator) {
0215             painter->save();
0216             QApplication::style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &indicatorOption, painter);
0217             painter->restore();
0218             painter->drawPixmap(indicatorX, indicatorY, d->extendPixmap);
0219         }
0220         return;
0221     }
0222 
0223     int row = index.row();
0224     QModelIndex parentIndex = index.parent();
0225 
0226     // indexOfExtendedColumnInSameRow() is very expensive, try to avoid calling it.
0227     if (row != d->cachedRow //
0228         || d->cachedStateTick != d->stateTick //
0229         || d->cachedParentIndex != parentIndex) {
0230         d->extender = d->extenders.value(d->indexOfExtendedColumnInSameRow(index));
0231         d->cachedStateTick = d->stateTick;
0232         d->cachedRow = row;
0233         d->cachedParentIndex = parentIndex;
0234         if (d->extender) {
0235             d->extenderHeight = d->extender->sizeHint().height();
0236         }
0237     }
0238 
0239     if (!d->extender) {
0240         QStyledItemDelegate::paint(painter, itemOption, index);
0241         if (showExtensionIndicator) {
0242             painter->save();
0243             QApplication::style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &indicatorOption, painter);
0244             painter->restore();
0245             painter->drawPixmap(indicatorX, indicatorY, d->extendPixmap);
0246         }
0247         return;
0248     }
0249 
0250     // an extender is present - make two rectangles: one to paint the original item, one for the extender
0251     if (isExtended(index)) {
0252         QStyleOptionViewItem extOption(option);
0253         initStyleOption(&extOption, index);
0254         extOption.rect = extenderRect(d->extender, option, index);
0255         updateExtenderGeometry(d->extender, extOption, index);
0256         // if we show it before, it will briefly flash in the wrong location.
0257         // the downside is, of course, that an api user effectively can't hide it.
0258         d->extender->show();
0259     }
0260 
0261     indicatorOption.rect.setHeight(option.rect.height() - d->extenderHeight);
0262     itemOption.rect.setHeight(option.rect.height() - d->extenderHeight);
0263     // tricky:make sure that the modified options' rect really has the
0264     // same height as the unchanged option.rect if no extender is present
0265     //(seems to work OK)
0266     QStyledItemDelegate::paint(painter, itemOption, index);
0267 
0268     if (showExtensionIndicator) {
0269         const int extendPixmapHeight = d->extendPixmap.height() / d->extendPixmap.devicePixelRatio();
0270         // indicatorOption's height changed, change this too
0271         indicatorY = indicatorOption.rect.top() + ((indicatorOption.rect.height() - extendPixmapHeight) >> 1);
0272         painter->save();
0273         QApplication::style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &indicatorOption, painter);
0274         painter->restore();
0275 
0276         if (d->extenders.contains(index)) {
0277             painter->drawPixmap(indicatorX, indicatorY, d->contractPixmap);
0278         } else {
0279             painter->drawPixmap(indicatorX, indicatorY, d->extendPixmap);
0280         }
0281     }
0282 }
0283 
0284 QRect KExtendableItemDelegate::extenderRect(QWidget *extender, const QStyleOptionViewItem &option, const QModelIndex &index) const
0285 {
0286     Q_ASSERT(extender);
0287     QRect rect(option.rect);
0288     rect.setTop(rect.bottom() + 1 - extender->sizeHint().height());
0289 
0290     int indentation = 0;
0291     if (QTreeView *tv = qobject_cast<QTreeView *>(parent())) {
0292         int indentSteps = 0;
0293         for (QModelIndex idx(index.parent()); idx.isValid(); idx = idx.parent()) {
0294             indentSteps++;
0295         }
0296         if (tv->rootIsDecorated()) {
0297             indentSteps++;
0298         }
0299         indentation = indentSteps * tv->indentation();
0300     }
0301 
0302     QAbstractScrollArea *container = qobject_cast<QAbstractScrollArea *>(parent());
0303     Q_ASSERT(container);
0304     if (qApp->isLeftToRight()) {
0305         rect.setLeft(indentation);
0306         rect.setRight(container->viewport()->width() - 1);
0307     } else {
0308         rect.setRight(container->viewport()->width() - 1 - indentation);
0309         rect.setLeft(0);
0310     }
0311     return rect;
0312 }
0313 
0314 QSize KExtendableItemDelegatePrivate::maybeExtendedSize(const QStyleOptionViewItem &option, const QModelIndex &index) const
0315 {
0316     QWidget *extender = extenders.value(index);
0317     QSize size(q->QStyledItemDelegate::sizeHint(option, index));
0318     if (!extender) {
0319         return size;
0320     }
0321     // add extender height to maximum height of any column in our row
0322     int itemHeight = size.height();
0323 
0324     int row = index.row();
0325     int thisColumn = index.column();
0326 
0327     // this is quite slow, but Qt is smart about when to call sizeHint().
0328     for (int column = 0; index.model()->columnCount() < column; column++) {
0329         if (column == thisColumn) {
0330             continue;
0331         }
0332         QModelIndex neighborIndex(index.sibling(row, column));
0333         if (!neighborIndex.isValid()) {
0334             break;
0335         }
0336         itemHeight = qMax(itemHeight, q->QStyledItemDelegate::sizeHint(option, neighborIndex).height());
0337     }
0338 
0339     // we only want to reserve vertical space, the horizontal extender layout is our private business.
0340     size.rheight() = itemHeight + extender->sizeHint().height();
0341     return size;
0342 }
0343 
0344 QModelIndex KExtendableItemDelegatePrivate::indexOfExtendedColumnInSameRow(const QModelIndex &index) const
0345 {
0346     const QAbstractItemModel *const model = index.model();
0347     const QModelIndex parentIndex(index.parent());
0348     const int row = index.row();
0349     const int columnCount = model->columnCount();
0350 
0351     // slow, slow, slow
0352     for (int column = 0; column < columnCount; column++) {
0353         QModelIndex indexOfExt(model->index(row, column, parentIndex));
0354         if (extenders.value(indexOfExt)) {
0355             return indexOfExt;
0356         }
0357     }
0358 
0359     return QModelIndex();
0360 }
0361 
0362 void KExtendableItemDelegate::updateExtenderGeometry(QWidget *extender, const QStyleOptionViewItem &option, const QModelIndex &index) const
0363 {
0364     Q_UNUSED(index);
0365     extender->setGeometry(option.rect);
0366 }
0367 
0368 void KExtendableItemDelegatePrivate::deleteExtenders()
0369 {
0370     for (QWidget *ext : std::as_const(extenders)) {
0371         ext->hide();
0372         ext->deleteLater();
0373     }
0374     deletionQueue.unite(extenderIndices);
0375     extenders.clear();
0376     extenderIndices.clear();
0377 }
0378 
0379 // make the view re-ask for sizeHint() and redisplay items with their new size
0380 //### starting from Qt 4.4 we could emit sizeHintChanged() instead
0381 void KExtendableItemDelegatePrivate::scheduleUpdateViewLayout()
0382 {
0383     QAbstractItemView *aiv = qobject_cast<QAbstractItemView *>(q->parent());
0384     // prevent crashes during destruction of the view
0385     if (aiv) {
0386         // dirty hack to call aiv's protected scheduleDelayedItemsLayout()
0387         aiv->setRootIndex(aiv->rootIndex());
0388     }
0389 }
0390 
0391 void KExtendableItemDelegate::setExtendPixmap(const QPixmap &pixmap)
0392 {
0393     d->extendPixmap = pixmap;
0394 }
0395 
0396 void KExtendableItemDelegate::setContractPixmap(const QPixmap &pixmap)
0397 {
0398     d->contractPixmap = pixmap;
0399 }
0400 
0401 QPixmap KExtendableItemDelegate::extendPixmap()
0402 {
0403     return d->extendPixmap;
0404 }
0405 
0406 QPixmap KExtendableItemDelegate::contractPixmap()
0407 {
0408     return d->contractPixmap;
0409 }
0410 
0411 #include "moc_kextendableitemdelegate.cpp"