File indexing completed on 2024-04-28 13:41:57
0001 // SPDX-FileCopyrightText: 2020 Simon Persson <simon.persson@mykolab.com> 0002 // 0003 // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0004 0005 #include "versionlistdelegate.h" 0006 #include "versionlistmodel.h" 0007 0008 #include <KFormat> 0009 #include <KLocalizedString> 0010 0011 #include <QAbstractItemView> 0012 #include <QAbstractItemModel> 0013 #include <QApplication> 0014 #include <QMouseEvent> 0015 #include <QParallelAnimationGroup> 0016 #include <QPainter> 0017 #include <QGraphicsOpacityEffect> 0018 #include <QPropertyAnimation> 0019 #include <QPushButton> 0020 #include <QtMath> 0021 #include <utility> 0022 0023 static const int cMargin = 4; 0024 0025 Button::Button(QString pText, QWidget *pParent) { 0026 // intentionally don't make this QObject owned by pParent 0027 mStyleOption.initFrom(pParent); 0028 mStyleOption.features = QStyleOptionButton::None; 0029 mStyleOption.state = QStyle::State_Enabled; 0030 mStyleOption.text = std::move(pText); 0031 0032 const QSize lContentsSize = mStyleOption.fontMetrics.size(Qt::TextSingleLine, mStyleOption.text); 0033 mStyleOption.rect = QRect(QPoint(0, 0), 0034 QApplication::style()->sizeFromContents(QStyle::CT_PushButton, 0035 &mStyleOption, lContentsSize)); 0036 mPushed = false; 0037 mParent = pParent; 0038 } 0039 0040 void Button::setPosition(const QPoint &pTopRight) { 0041 mStyleOption.rect.moveTopRight(pTopRight); 0042 } 0043 0044 void Button::paint(QPainter *pPainter, float pOpacity) { 0045 pPainter->setOpacity(static_cast<qreal>(pOpacity)); 0046 QApplication::style()->drawControl(QStyle::CE_PushButton, &mStyleOption, pPainter); 0047 } 0048 0049 bool Button::event(QEvent *pEvent) { 0050 auto lMouseEvent = dynamic_cast<QMouseEvent *>(pEvent); 0051 bool lActivated = false; 0052 switch(pEvent->type()) { 0053 case QEvent::MouseMove: 0054 if(mStyleOption.rect.contains(lMouseEvent->pos())) { 0055 if(!(mStyleOption.state & QStyle::State_MouseOver)) { 0056 mStyleOption.state |= QStyle::State_MouseOver; 0057 if(mPushed) { 0058 mStyleOption.state |= QStyle::State_Sunken; 0059 mStyleOption.state &= ~QStyle::State_Raised; 0060 } 0061 mParent->update(mStyleOption.rect); 0062 } 0063 } else { 0064 if(mStyleOption.state & QStyle::State_MouseOver) { 0065 mStyleOption.state &= ~QStyle::State_MouseOver; 0066 if(mPushed) { 0067 mStyleOption.state &= ~QStyle::State_Sunken; 0068 mStyleOption.state |= QStyle::State_Raised; 0069 } 0070 mParent->update(mStyleOption.rect); 0071 } 0072 } 0073 break; 0074 case QEvent::MouseButtonPress: 0075 if(lMouseEvent->button() == Qt::LeftButton && !mPushed && 0076 (mStyleOption.state & QStyle::State_MouseOver)) { 0077 mPushed = true; 0078 mStyleOption.state |= QStyle::State_Sunken; 0079 mStyleOption.state &= ~QStyle::State_Raised; 0080 mParent->update(mStyleOption.rect); 0081 } 0082 break; 0083 case QEvent::MouseButtonRelease: 0084 if(lMouseEvent->button() == Qt::LeftButton) { 0085 if(mPushed && (mStyleOption.state & QStyle::State_MouseOver)) { 0086 lActivated = true; 0087 } 0088 mPushed = false; 0089 mStyleOption.state &= ~QStyle::State_Sunken; 0090 mStyleOption.state |= QStyle::State_Raised; 0091 mParent->update(mStyleOption.rect); 0092 } 0093 break; 0094 case QEvent::KeyPress: { 0095 auto lKeyEvent = dynamic_cast<QKeyEvent *>(pEvent); 0096 if((mStyleOption.state & QStyle::State_HasFocus)) { 0097 if(lKeyEvent->key() == Qt::Key_Left || lKeyEvent->key() == Qt::Key_Right) { 0098 mStyleOption.state &= ~QStyle::State_HasFocus; 0099 emit focusChangeRequested(lKeyEvent->key() == Qt::Key_Right); 0100 mParent->update(mStyleOption.rect); 0101 } else if(lKeyEvent->key() == Qt::Key_Space || lKeyEvent->key() == Qt::Key_Return 0102 || lKeyEvent->key() == Qt::Key_Enter) { 0103 lActivated = true; 0104 } 0105 } 0106 break; 0107 } 0108 default: 0109 break; 0110 } 0111 return lActivated; 0112 } 0113 0114 0115 VersionItemAnimation::VersionItemAnimation(QWidget *pParent) 0116 : QParallelAnimationGroup(pParent) 0117 { 0118 mParent = pParent; 0119 mExtraHeight = 0; 0120 mOpacity = 0; 0121 mOpenButton = new Button(xi18nc("@action:button", "Open"), pParent); 0122 connect(mOpenButton, SIGNAL(focusChangeRequested(bool)), SLOT(changeFocus(bool)), Qt::QueuedConnection); 0123 mRestoreButton = new Button(xi18nc("@action:button", "Restore"), pParent); 0124 connect(mRestoreButton, SIGNAL(focusChangeRequested(bool)), SLOT(changeFocus(bool)), Qt::QueuedConnection); 0125 auto lHeightAnimation = new QPropertyAnimation(this, "extraHeight", this); 0126 lHeightAnimation->setStartValue(0.0); 0127 lHeightAnimation->setEndValue(1.0); 0128 lHeightAnimation->setDuration(300); 0129 lHeightAnimation->setEasingCurve(QEasingCurve::InOutBack); 0130 addAnimation(lHeightAnimation); 0131 0132 auto lWidgetOpacityAnimation = new QPropertyAnimation(this, "opacity", this); 0133 lWidgetOpacityAnimation->setStartValue(0.0); 0134 lWidgetOpacityAnimation->setEndValue(1.0); 0135 lWidgetOpacityAnimation->setDuration(300); 0136 addAnimation(lWidgetOpacityAnimation); 0137 } 0138 0139 void VersionItemAnimation::setExtraHeight(qreal pExtraHeight) { 0140 mExtraHeight = pExtraHeight; 0141 emit sizeChanged(mIndex); 0142 } 0143 0144 void VersionItemAnimation::changeFocus(bool pForward) { 0145 Q_UNUSED(pForward) 0146 if(sender() == mOpenButton) { 0147 mRestoreButton->mStyleOption.state |= QStyle::State_HasFocus; 0148 mParent->update(mRestoreButton->mStyleOption.rect); 0149 } else if(sender() == mRestoreButton) { 0150 mOpenButton->mStyleOption.state |= QStyle::State_HasFocus; 0151 mParent->update(mOpenButton->mStyleOption.rect); 0152 } 0153 } 0154 0155 void VersionItemAnimation::setFocus(bool pFocused) { 0156 if(!pFocused) { 0157 mOpenButton->mStyleOption.state &= ~QStyle::State_HasFocus; 0158 mRestoreButton->mStyleOption.state &= ~QStyle::State_HasFocus; 0159 } else { 0160 mOpenButton->mStyleOption.state |= QStyle::State_HasFocus; 0161 mRestoreButton->mStyleOption.state &= ~QStyle::State_HasFocus; 0162 } 0163 mParent->update(mOpenButton->mStyleOption.rect); 0164 mParent->update(mRestoreButton->mStyleOption.rect); 0165 } 0166 0167 VersionListDelegate::VersionListDelegate(QAbstractItemView *pItemView, QObject *pParent) : 0168 QAbstractItemDelegate(pParent) 0169 { 0170 mView = pItemView; 0171 mModel = pItemView->model(); 0172 connect(pItemView->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), 0173 SLOT(updateCurrent(QModelIndex,QModelIndex))); 0174 connect(pItemView->model(), SIGNAL(modelReset()), SLOT(reset())); 0175 pItemView->viewport()->installEventFilter(this); //mouse events 0176 pItemView->installEventFilter(this); //keyboard events 0177 pItemView->viewport()->setMouseTracking(true); 0178 } 0179 0180 VersionListDelegate::~VersionListDelegate()=default; 0181 0182 void VersionListDelegate::paint(QPainter *pPainter, const QStyleOptionViewItem &pOption, const QModelIndex &pIndex) const { 0183 QStyle * lStyle = QApplication::style(); 0184 lStyle->drawPrimitive(QStyle::PE_PanelItemViewItem, &pOption, pPainter); 0185 pPainter->save(); 0186 pPainter->setPen(pOption.palette.color((pOption.state & QStyle::State_HasFocus) 0187 ? QPalette::HighlightedText: QPalette::Text)); 0188 QRect lMarginRect = pOption.rect.adjusted(cMargin, cMargin, -cMargin, -cMargin); 0189 0190 QRect lSizeDisplayBounds; 0191 if(!pIndex.data(VersionIsDirectoryRole).toBool()) { 0192 QString lSizeText = KFormat().formatByteSize(static_cast<double>(pIndex.data(VersionSizeRole).toULongLong())); 0193 pPainter->drawText(lMarginRect, Qt::AlignRight | Qt::AlignTop, lSizeText, &lSizeDisplayBounds); 0194 } 0195 QString lDateText = pOption.fontMetrics.elidedText(pIndex.data().toString(), Qt::ElideRight, 0196 lMarginRect.width() - lSizeDisplayBounds.width()); 0197 pPainter->drawText(lMarginRect, Qt::AlignLeft | Qt::AlignTop, lDateText); 0198 pPainter->restore(); 0199 0200 VersionItemAnimation *lAnimation = mActiveAnimations.value(pIndex); 0201 if(lAnimation != nullptr) { 0202 pPainter->save(); 0203 pPainter->setClipRect(pOption.rect); 0204 0205 lAnimation->mRestoreButton->setPosition(pOption.rect.topRight() + 0206 QPoint(-cMargin, 0207 pOption.fontMetrics.height() + 2*cMargin)); 0208 lAnimation->mRestoreButton->paint(pPainter, lAnimation->opacity()); 0209 0210 lAnimation->mOpenButton->setPosition(lAnimation->mRestoreButton->mStyleOption.rect.topLeft() + 0211 QPoint(-cMargin , 0)); 0212 lAnimation->mOpenButton->paint(pPainter, lAnimation->opacity()); 0213 pPainter->restore(); 0214 } 0215 } 0216 0217 QSize VersionListDelegate::sizeHint(const QStyleOptionViewItem &pOption, const QModelIndex &pIndex) const { 0218 int lExtraHeight = 0; 0219 int lExtraWidth = 0; 0220 VersionItemAnimation *lAnimation = mActiveAnimations.value(pIndex); 0221 if(lAnimation != nullptr) { 0222 int lButtonHeight = lAnimation->mOpenButton->mStyleOption.rect.height(); 0223 lExtraHeight = qCeil(lAnimation->extraHeight() * (lButtonHeight + cMargin)); 0224 lExtraWidth = lAnimation->mOpenButton->mStyleOption.rect.width() + 0225 lAnimation->mRestoreButton->mStyleOption.rect.width(); 0226 } 0227 return {lExtraWidth, cMargin*2 + pOption.fontMetrics.height() + lExtraHeight}; 0228 } 0229 0230 bool VersionListDelegate::eventFilter(QObject *pObject, QEvent *pEvent) { 0231 foreach (VersionItemAnimation *lAnimation, mActiveAnimations) { 0232 if(lAnimation->mOpenButton->event(pEvent)) { 0233 emit openRequested(lAnimation->mIndex); 0234 } 0235 if(lAnimation->mRestoreButton->event(pEvent)) { 0236 emit restoreRequested(lAnimation->mIndex); 0237 } 0238 } 0239 return QAbstractItemDelegate::eventFilter(pObject, pEvent); 0240 } 0241 0242 0243 void VersionListDelegate::updateCurrent(const QModelIndex &pCurrent, const QModelIndex &pPrevious) { 0244 if(pPrevious.isValid()) { 0245 VersionItemAnimation *lPrevAnim = mActiveAnimations.value(pPrevious); 0246 if(lPrevAnim != nullptr) { 0247 lPrevAnim->setDirection(QAbstractAnimation::Backward); 0248 lPrevAnim->start(); 0249 lPrevAnim->setFocus(false); 0250 } 0251 } 0252 if(pCurrent.isValid()) { 0253 VersionItemAnimation *lCurAnim = mActiveAnimations.value(pCurrent); 0254 if(lCurAnim == nullptr) { 0255 if(!mInactiveAnimations.isEmpty()) { 0256 lCurAnim = mInactiveAnimations.takeFirst(); 0257 } else { 0258 lCurAnim = new VersionItemAnimation(mView->viewport()); 0259 connect(lCurAnim, SIGNAL(sizeChanged(QModelIndex)), SIGNAL(sizeHintChanged(QModelIndex))); 0260 connect(lCurAnim, SIGNAL(finished()), SLOT(reclaimAnimation())); 0261 } 0262 lCurAnim->mIndex = pCurrent; 0263 mActiveAnimations.insert(pCurrent, lCurAnim); 0264 } 0265 lCurAnim->setDirection(QAbstractAnimation::Forward); 0266 lCurAnim->start(); 0267 lCurAnim->setFocus(true); 0268 } 0269 } 0270 0271 void VersionListDelegate::reset() { 0272 mInactiveAnimations.append(mActiveAnimations.values()); 0273 mActiveAnimations.clear(); 0274 } 0275 0276 void VersionListDelegate::reclaimAnimation() { 0277 auto lAnimation = qobject_cast<VersionItemAnimation *>(sender()); 0278 if(lAnimation->direction() == QAbstractAnimation::Backward) { 0279 mInactiveAnimations.append(lAnimation); 0280 foreach(const VersionItemAnimation *lActiveAnimation, mActiveAnimations) { 0281 if(lActiveAnimation == lAnimation) { 0282 mActiveAnimations.remove(lAnimation->mIndex); 0283 break; 0284 } 0285 } 0286 } 0287 }