mirror of
https://bitbucket.org/smil3y/kdelibs.git
synced 2025-02-23 18:32:49 +00:00
451 lines
15 KiB
C++
451 lines
15 KiB
C++
/* This file is part of the KDE libraries
|
|
Copyright (C) 2006,2007 Andreas Hartmetz (ahartmetz@gmail.com)
|
|
Copyright (C) 2008 Urs Wolfer (uwolfer @ kde.org)
|
|
|
|
This library is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU Library General Public
|
|
License as published by the Free Software Foundation; either
|
|
version 2 of the License, or (at your option) any later version.
|
|
|
|
This library is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
Library General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Library General Public License
|
|
along with this library; see the file COPYING.LIB. If not, write to
|
|
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
Boston, MA 02110-1301, USA.
|
|
*/
|
|
|
|
#include "kextendableitemdelegate.h"
|
|
|
|
#include <QtCore/qabstractitemmodel.h>
|
|
#include <QScrollBar>
|
|
#include <QTreeView>
|
|
#include <QPainter>
|
|
#include <QApplication>
|
|
|
|
|
|
class KExtendableItemDelegate::Private {
|
|
public:
|
|
Private(KExtendableItemDelegate *parent) :
|
|
q(parent),
|
|
stateTick(0),
|
|
cachedStateTick(-1),
|
|
cachedRow(-20), //Qt uses -1 for invalid indices
|
|
extender(0),
|
|
extenderHeight(0)
|
|
|
|
{}
|
|
|
|
void _k_extenderDestructionHandler(QObject *destroyed);
|
|
void _k_verticalScroll();
|
|
|
|
QSize maybeExtendedSize(const QStyleOptionViewItem &option, const QModelIndex &index) const;
|
|
QModelIndex indexOfExtendedColumnInSameRow(const QModelIndex &index) const;
|
|
void scheduleUpdateViewLayout();
|
|
|
|
KExtendableItemDelegate *q;
|
|
|
|
/**
|
|
* Delete all active extenders
|
|
*/
|
|
void deleteExtenders();
|
|
|
|
//this will trigger a lot of auto-casting QModelIndex <-> QPersistentModelIndex
|
|
QHash<QPersistentModelIndex, QWidget *> extenders;
|
|
QHash<QWidget *, QPersistentModelIndex> extenderIndices;
|
|
QHash<QWidget *, QPersistentModelIndex> deletionQueue;
|
|
QPixmap extendPixmap;
|
|
QPixmap contractPixmap;
|
|
int stateTick;
|
|
int cachedStateTick;
|
|
int cachedRow;
|
|
QModelIndex cachedParentIndex;
|
|
QWidget *extender;
|
|
int extenderHeight;
|
|
};
|
|
|
|
|
|
KExtendableItemDelegate::KExtendableItemDelegate(QAbstractItemView* parent)
|
|
: QStyledItemDelegate(parent),
|
|
d(new Private(this))
|
|
{
|
|
connect(parent->verticalScrollBar(), SIGNAL(valueChanged(int)),
|
|
this, SLOT(_k_verticalScroll()));
|
|
}
|
|
|
|
|
|
KExtendableItemDelegate::~KExtendableItemDelegate()
|
|
{
|
|
delete d;
|
|
}
|
|
|
|
|
|
void KExtendableItemDelegate::extendItem(QWidget *ext, const QModelIndex &index)
|
|
{
|
|
// kDebug() << "Creating extender at " << ext << " for item " << index.model()->data(index,Qt::DisplayRole).toString();
|
|
|
|
if (!ext || !index.isValid()) {
|
|
return;
|
|
}
|
|
//maintain the invariant "zero or one extender per row"
|
|
d->stateTick++;
|
|
contractItem(d->indexOfExtendedColumnInSameRow(index));
|
|
d->stateTick++;
|
|
//reparent, as promised in the docs
|
|
QAbstractItemView *aiv = qobject_cast<QAbstractItemView *>(parent());
|
|
if (!aiv) {
|
|
return;
|
|
}
|
|
ext->setParent(aiv->viewport());
|
|
d->extenders.insert(index, ext);
|
|
d->extenderIndices.insert(ext, index);
|
|
connect(ext, SIGNAL(destroyed(QObject*)), this, SLOT(_k_extenderDestructionHandler(QObject*)));
|
|
emit extenderCreated(ext, index);
|
|
d->scheduleUpdateViewLayout();
|
|
}
|
|
|
|
|
|
void KExtendableItemDelegate::contractItem(const QModelIndex& index)
|
|
{
|
|
QWidget *extender = d->extenders.value(index);
|
|
if (!extender) {
|
|
return;
|
|
}
|
|
// kDebug() << "Collapse extender at " << extender << " for item " << index.model()->data(index,Qt::DisplayRole).toString();
|
|
extender->hide();
|
|
extender->deleteLater();
|
|
|
|
QPersistentModelIndex persistentIndex = d->extenderIndices.take(extender);
|
|
d->extenders.remove(persistentIndex);
|
|
|
|
d->deletionQueue.insert(extender, persistentIndex);
|
|
|
|
d->scheduleUpdateViewLayout();
|
|
}
|
|
|
|
|
|
void KExtendableItemDelegate::contractAll()
|
|
{
|
|
d->deleteExtenders();
|
|
}
|
|
|
|
|
|
//slot
|
|
void KExtendableItemDelegate::Private::_k_extenderDestructionHandler(QObject *destroyed)
|
|
{
|
|
// kDebug() << "Removing extender at " << destroyed;
|
|
|
|
QWidget *extender = static_cast<QWidget *>(destroyed);
|
|
stateTick++;
|
|
|
|
QPersistentModelIndex persistentIndex = deletionQueue.take(extender);
|
|
if (persistentIndex.isValid() &&
|
|
q->receivers(SIGNAL(extenderDestroyed(QWidget*,QModelIndex)))) {
|
|
|
|
QModelIndex index = persistentIndex;
|
|
emit q->extenderDestroyed(extender, index);
|
|
}
|
|
|
|
scheduleUpdateViewLayout();
|
|
}
|
|
|
|
|
|
//slot
|
|
void KExtendableItemDelegate::Private::_k_verticalScroll()
|
|
{
|
|
foreach (QWidget *extender, extenders) {
|
|
// Fast scrolling can lead to artifacts where extenders stay in the viewport
|
|
// of the parent's scroll area even though their items are scrolled out.
|
|
// Therefore we hide all extenders when scrolling.
|
|
// In paintEvent() show() will be called on actually visible extenders and
|
|
// Qt's double buffering takes care of eliminating flicker.
|
|
// ### This scales badly to many extenders. There are probably better ways to
|
|
// avoid the artifacts.
|
|
extender->hide();
|
|
}
|
|
}
|
|
|
|
|
|
bool KExtendableItemDelegate::isExtended(const QModelIndex &index) const
|
|
{
|
|
return d->extenders.value(index);
|
|
}
|
|
|
|
|
|
QSize KExtendableItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
|
|
{
|
|
QSize ret;
|
|
|
|
if (!d->extenders.isEmpty()) {
|
|
ret = d->maybeExtendedSize(option, index);
|
|
} else {
|
|
ret = QStyledItemDelegate::sizeHint(option, index);
|
|
}
|
|
|
|
bool showExtensionIndicator = index.model() ?
|
|
index.model()->data(index, ShowExtensionIndicatorRole).toBool() : false;
|
|
if (showExtensionIndicator) {
|
|
ret.rwidth() += d->extendPixmap.width();
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
void KExtendableItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
|
|
{
|
|
int indicatorX = 0;
|
|
int indicatorY = 0;
|
|
|
|
QStyleOptionViewItem indicatorOption(option);
|
|
initStyleOption(&indicatorOption, index);
|
|
if (index.column() == 0) {
|
|
indicatorOption.viewItemPosition = QStyleOptionViewItem::Beginning;
|
|
} else if (index.column() == index.model()->columnCount() - 1) {
|
|
indicatorOption.viewItemPosition = QStyleOptionViewItem::End;
|
|
} else {
|
|
indicatorOption.viewItemPosition = QStyleOptionViewItem::Middle;
|
|
}
|
|
|
|
QStyleOptionViewItem itemOption(option);
|
|
initStyleOption(&itemOption, index);
|
|
if (index.column() == 0) {
|
|
itemOption.viewItemPosition = QStyleOptionViewItem::Beginning;
|
|
} else if (index.column() == index.model()->columnCount() - 1) {
|
|
itemOption.viewItemPosition = QStyleOptionViewItem::End;
|
|
} else {
|
|
itemOption.viewItemPosition = QStyleOptionViewItem::Middle;
|
|
}
|
|
|
|
const bool showExtensionIndicator = index.model()->data(index, ShowExtensionIndicatorRole).toBool();
|
|
|
|
if (showExtensionIndicator) {
|
|
if (QApplication::isRightToLeft()) {
|
|
indicatorX = option.rect.right() - d->extendPixmap.width();
|
|
itemOption.rect.setRight(option.rect.right() - d->extendPixmap.width());
|
|
indicatorOption.rect.setLeft(option.rect.right() - d->extendPixmap.width());
|
|
} else {
|
|
indicatorX = option.rect.left();
|
|
indicatorOption.rect.setRight(option.rect.left() + d->extendPixmap.width());
|
|
itemOption.rect.setLeft(option.rect.left() + d->extendPixmap.width());
|
|
}
|
|
indicatorY = option.rect.top() + ((option.rect.height() - d->extendPixmap.height()) >> 1);
|
|
}
|
|
|
|
//fast path
|
|
if (d->extenders.isEmpty()) {
|
|
QStyledItemDelegate::paint(painter, itemOption, index);
|
|
if (showExtensionIndicator) {
|
|
painter->save();
|
|
QApplication::style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &indicatorOption,
|
|
painter);
|
|
painter->restore();
|
|
painter->drawPixmap(indicatorX, indicatorY, d->extendPixmap);
|
|
}
|
|
return;
|
|
}
|
|
|
|
int row = index.row();
|
|
QModelIndex parentIndex = index.parent();
|
|
|
|
//indexOfExtendedColumnInSameRow() is very expensive, try to avoid calling it.
|
|
if (row != d->cachedRow || d->cachedStateTick != d->stateTick
|
|
|| d->cachedParentIndex != parentIndex) {
|
|
d->extender = d->extenders.value(d->indexOfExtendedColumnInSameRow(index));
|
|
d->cachedStateTick = d->stateTick;
|
|
d->cachedRow = row;
|
|
d->cachedParentIndex = parentIndex;
|
|
if (d->extender) {
|
|
d->extenderHeight = d->extender->sizeHint().height();
|
|
}
|
|
}
|
|
|
|
if (!d->extender) {
|
|
QStyledItemDelegate::paint(painter, itemOption, index);
|
|
if (showExtensionIndicator) {
|
|
painter->save();
|
|
QApplication::style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &indicatorOption,
|
|
painter);
|
|
painter->restore();
|
|
painter->drawPixmap(indicatorX, indicatorY, d->extendPixmap);
|
|
}
|
|
return;
|
|
}
|
|
|
|
//an extender is present - make two rectangles: one to paint the original item, one for the extender
|
|
if (isExtended(index)) {
|
|
QStyleOptionViewItem extOption(option);
|
|
initStyleOption(&extOption, index);
|
|
extOption.rect = extenderRect(d->extender, option, index);
|
|
updateExtenderGeometry(d->extender, extOption, index);
|
|
//if we show it before, it will briefly flash in the wrong location.
|
|
//the downside is, of course, that an api user effectively can't hide it.
|
|
d->extender->show();
|
|
}
|
|
|
|
indicatorOption.rect.setHeight(option.rect.height() - d->extenderHeight);
|
|
itemOption.rect.setHeight(option.rect.height() - d->extenderHeight);
|
|
//tricky:make sure that the modified options' rect really has the
|
|
//same height as the unchanged option.rect if no extender is present
|
|
//(seems to work OK)
|
|
QStyledItemDelegate::paint(painter, itemOption, index);
|
|
|
|
if (showExtensionIndicator) {
|
|
//indicatorOption's height changed, change this too
|
|
indicatorY = indicatorOption.rect.top() + ((indicatorOption.rect.height() -
|
|
d->extendPixmap.height()) >> 1);
|
|
painter->save();
|
|
QApplication::style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &indicatorOption,
|
|
painter);
|
|
painter->restore();
|
|
|
|
if (d->extenders.contains(index)) {
|
|
painter->drawPixmap(indicatorX, indicatorY, d->contractPixmap);
|
|
} else {
|
|
painter->drawPixmap(indicatorX, indicatorY, d->extendPixmap);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
QRect KExtendableItemDelegate::extenderRect(QWidget *extender, const QStyleOptionViewItem &option, const QModelIndex &index) const
|
|
{
|
|
Q_ASSERT(extender);
|
|
QRect rect(option.rect);
|
|
rect.setTop(rect.bottom() + 1 - extender->sizeHint().height());
|
|
|
|
int indentation = 0;
|
|
if (QTreeView *tv = qobject_cast<QTreeView *>(parent())) {
|
|
int indentSteps = 0;
|
|
for (QModelIndex idx(index.parent()); idx.isValid(); idx = idx.parent()) {
|
|
indentSteps++;
|
|
}
|
|
if (tv->rootIsDecorated()) {
|
|
indentSteps++;
|
|
}
|
|
indentation = indentSteps * tv->indentation();
|
|
}
|
|
|
|
QAbstractScrollArea *container = qobject_cast<QAbstractScrollArea *>(parent());
|
|
Q_ASSERT(container);
|
|
if (qApp->isLeftToRight()) {
|
|
rect.setLeft(indentation);
|
|
rect.setRight(container->viewport()->width() - 1);
|
|
} else {
|
|
rect.setRight(container->viewport()->width() - 1 - indentation);
|
|
rect.setLeft(0);
|
|
}
|
|
return rect;
|
|
}
|
|
|
|
|
|
QSize KExtendableItemDelegate::Private::maybeExtendedSize(const QStyleOptionViewItem &option, const QModelIndex &index) const
|
|
{
|
|
QWidget *extender = extenders.value(index);
|
|
QSize size(q->QStyledItemDelegate::sizeHint(option, index));
|
|
if (!extender) {
|
|
return size;
|
|
}
|
|
//add extender height to maximum height of any column in our row
|
|
int itemHeight = size.height();
|
|
|
|
int row = index.row();
|
|
int thisColumn = index.column();
|
|
|
|
//this is quite slow, but Qt is smart about when to call sizeHint().
|
|
for (int column = 0; index.model()->columnCount() < column; column++) {
|
|
if (column == thisColumn) {
|
|
continue;
|
|
}
|
|
QModelIndex neighborIndex(index.sibling(row, column));
|
|
if (!neighborIndex.isValid()) {
|
|
break;
|
|
}
|
|
itemHeight = qMax(itemHeight, q->QStyledItemDelegate::sizeHint(option, neighborIndex).height());
|
|
}
|
|
|
|
//we only want to reserve vertical space, the horizontal extender layout is our private business.
|
|
size.rheight() = itemHeight + extender->sizeHint().height();
|
|
return size;
|
|
}
|
|
|
|
|
|
QModelIndex KExtendableItemDelegate::Private::indexOfExtendedColumnInSameRow(const QModelIndex &index) const
|
|
{
|
|
const QAbstractItemModel *const model = index.model();
|
|
const QModelIndex parentIndex(index.parent());
|
|
const int row = index.row();
|
|
const int columnCount = model->columnCount();
|
|
|
|
//slow, slow, slow
|
|
for (int column = 0; column < columnCount; column++) {
|
|
QModelIndex indexOfExt(model->index(row, column, parentIndex));
|
|
if (extenders.value(indexOfExt)) {
|
|
return indexOfExt;
|
|
}
|
|
}
|
|
|
|
return QModelIndex();
|
|
}
|
|
|
|
|
|
void KExtendableItemDelegate::updateExtenderGeometry(QWidget *extender, const QStyleOptionViewItem &option,
|
|
const QModelIndex &index) const
|
|
{
|
|
Q_UNUSED(index);
|
|
extender->setGeometry(option.rect);
|
|
}
|
|
|
|
|
|
void KExtendableItemDelegate::Private::deleteExtenders()
|
|
{
|
|
foreach (QWidget *ext, extenders) {
|
|
ext->hide();
|
|
ext->deleteLater();
|
|
}
|
|
deletionQueue.unite(extenderIndices);
|
|
extenders.clear();
|
|
extenderIndices.clear();
|
|
}
|
|
|
|
|
|
//make the view re-ask for sizeHint() and redisplay items with their new size
|
|
//### starting from Qt 4.4 we could emit sizeHintChanged() instead
|
|
void KExtendableItemDelegate::Private::scheduleUpdateViewLayout()
|
|
{
|
|
QAbstractItemView *aiv = qobject_cast<QAbstractItemView *>(q->parent());
|
|
//prevent crashes during destruction of the view
|
|
if (aiv) {
|
|
//dirty hack to call aiv's protected scheduleDelayedItemsLayout()
|
|
aiv->setRootIndex(aiv->rootIndex());
|
|
}
|
|
}
|
|
|
|
|
|
void KExtendableItemDelegate::setExtendPixmap(const QPixmap &pixmap)
|
|
{
|
|
d->extendPixmap = pixmap;
|
|
}
|
|
|
|
|
|
void KExtendableItemDelegate::setContractPixmap(const QPixmap &pixmap)
|
|
{
|
|
d->contractPixmap = pixmap;
|
|
}
|
|
|
|
|
|
QPixmap KExtendableItemDelegate::extendPixmap()
|
|
{
|
|
return d->extendPixmap;
|
|
}
|
|
|
|
|
|
QPixmap KExtendableItemDelegate::contractPixmap()
|
|
{
|
|
return d->contractPixmap;
|
|
}
|
|
|
|
#include "moc_kextendableitemdelegate.cpp"
|