File indexing completed on 2024-05-19 16:49:49

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 }