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"