File indexing completed on 2024-04-21 03:55:21

0001 /*
0002     This file is part of the KDE project
0003     SPDX-FileCopyrightText: 2008 Rafael Fernández López <ereslibre@kde.org>
0004     SPDX-FileCopyrightText: 2022 Kai Uwe Broulik <kde@broulik.de>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-only
0007 */
0008 
0009 #ifndef KFILEPLACESVIEW_P_H
0010 #define KFILEPLACESVIEW_P_H
0011 
0012 #include <KIO/FileSystemFreeSpaceJob>
0013 #include <KIO/Global>
0014 
0015 #include <QAbstractItemDelegate>
0016 #include <QDateTime>
0017 #include <QDeadlineTimer>
0018 #include <QGestureEvent>
0019 #include <QMouseEvent>
0020 #include <QPointer>
0021 #include <QScroller>
0022 #include <QTimer>
0023 
0024 class KFilePlacesView;
0025 class QTimeLine;
0026 
0027 struct PlaceFreeSpaceInfo {
0028     QDeadlineTimer timeout;
0029     KIO::filesize_t used = 0;
0030     KIO::filesize_t size = 0;
0031     QPointer<KIO::FileSystemFreeSpaceJob> job;
0032 };
0033 
0034 class KFilePlacesViewDelegate : public QAbstractItemDelegate
0035 {
0036     Q_OBJECT
0037 public:
0038     explicit KFilePlacesViewDelegate(KFilePlacesView *parent);
0039     ~KFilePlacesViewDelegate() override;
0040     QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
0041     void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
0042 
0043     int iconSize() const;
0044     void setIconSize(int newSize);
0045 
0046     void paletteChange();
0047 
0048     void addAppearingItem(const QModelIndex &index);
0049     void setAppearingItemProgress(qreal value);
0050     void addDisappearingItem(const QModelIndex &index);
0051     void addDisappearingItemGroup(const QModelIndex &index);
0052     void setDisappearingItemProgress(qreal value);
0053     void setDeviceBusyAnimationRotation(qreal angle);
0054 
0055     void setShowHoverIndication(bool show);
0056     void setHoveredHeaderArea(const QModelIndex &index);
0057     void setHoveredAction(const QModelIndex &index);
0058 
0059     qreal contentsOpacity(const QModelIndex &index) const;
0060 
0061     bool pointIsHeaderArea(const QPoint &pos) const;
0062     bool pointIsTeardownAction(const QPoint &pos) const;
0063 
0064     void startDrag();
0065 
0066     int sectionHeaderHeight(const QModelIndex &index) const;
0067     bool indexIsSectionHeader(const QModelIndex &index) const;
0068     int actionIconSize() const;
0069 
0070     void checkFreeSpace();
0071     void checkFreeSpace(const QModelIndex &index) const;
0072     void startPollingFreeSpace() const;
0073     void stopPollingFreeSpace() const;
0074 
0075     void clearFreeSpaceInfo();
0076 
0077 protected:
0078     bool helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index) override;
0079 
0080 private:
0081     QString groupNameFromIndex(const QModelIndex &index) const;
0082     QModelIndex previousVisibleIndex(const QModelIndex &index) const;
0083     void drawSectionHeader(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;
0084 
0085     QColor textColor(const QStyleOption &option) const;
0086     QColor baseColor(const QStyleOption &option) const;
0087     QColor mixedColor(const QColor &c1, const QColor &c2, int c1Percent) const;
0088 
0089     KFilePlacesView *m_view;
0090     int m_iconSize;
0091 
0092     QList<QPersistentModelIndex> m_appearingItems;
0093     qreal m_appearingHeightScale;
0094     qreal m_appearingOpacity;
0095 
0096     QList<QPersistentModelIndex> m_disappearingItems;
0097     qreal m_disappearingHeightScale;
0098     qreal m_disappearingOpacity;
0099 
0100     qreal m_busyAnimationRotation = 0.0;
0101 
0102     bool m_showHoverIndication;
0103     QPersistentModelIndex m_hoveredHeaderArea;
0104     QPersistentModelIndex m_hoveredAction;
0105     mutable bool m_dragStarted;
0106 
0107     QMap<QPersistentModelIndex, QTimeLine *> m_timeLineMap;
0108     QMap<QTimeLine *, QPersistentModelIndex> m_timeLineInverseMap;
0109 
0110     mutable QTimer m_pollFreeSpace;
0111     mutable QMap<QPersistentModelIndex, PlaceFreeSpaceInfo> m_freeSpaceInfo;
0112     // constructing KColorScheme is expensive, cache the negative color
0113     mutable QColor m_warningCapacityBarColor;
0114 };
0115 
0116 class KFilePlacesEventWatcher : public QObject
0117 {
0118     Q_OBJECT
0119 
0120 public:
0121     explicit KFilePlacesEventWatcher(KFilePlacesView *parent = nullptr)
0122         : QObject(parent)
0123         , m_scroller(nullptr)
0124         , q(parent)
0125         , m_rubberBand(nullptr)
0126         , m_isTouchEvent(false)
0127         , m_mousePressed(false)
0128         , m_tapAndHoldActive(false)
0129         , m_lastMouseSource(Qt::MouseEventNotSynthesized)
0130     {
0131         m_rubberBand = new QRubberBand(QRubberBand::Rectangle, parent);
0132     }
0133 
0134     const QModelIndex hoveredHeaderAreaIndex()
0135     {
0136         return m_hoveredHeaderAreaIndex;
0137     }
0138 
0139     const QModelIndex hoveredActionIndex()
0140     {
0141         return m_hoveredActionIndex;
0142     }
0143 
0144     QScroller *m_scroller;
0145 
0146 public Q_SLOTS:
0147     void qScrollerStateChanged(const QScroller::State newState)
0148     {
0149         if (newState == QScroller::Inactive) {
0150             m_isTouchEvent = false;
0151         }
0152     }
0153 
0154 Q_SIGNALS:
0155     void entryMiddleClicked(const QModelIndex &index);
0156 
0157     void headerAreaEntered(const QModelIndex &index);
0158     void headerAreaLeft(const QModelIndex &index);
0159 
0160     void actionEntered(const QModelIndex &index);
0161     void actionLeft(const QModelIndex &index);
0162     void actionClicked(const QModelIndex &index);
0163 
0164     void windowActivated();
0165     void windowDeactivated();
0166 
0167     void paletteChanged();
0168 
0169 protected:
0170     bool eventFilter(QObject *watched, QEvent *event) override
0171     {
0172         switch (event->type()) {
0173         case QEvent::MouseMove: {
0174             if (m_isTouchEvent && !m_tapAndHoldActive) {
0175                 return true;
0176             }
0177 
0178             m_tapAndHoldActive = false;
0179             if (m_rubberBand->isVisible()) {
0180                 m_rubberBand->hide();
0181             }
0182 
0183             QAbstractItemView *view = qobject_cast<QAbstractItemView *>(watched->parent());
0184             const QPoint pos = static_cast<QMouseEvent *>(event)->pos();
0185             const QModelIndex index = view->indexAt(pos);
0186 
0187             QModelIndex headerAreaIndex;
0188             QModelIndex actionIndex;
0189             if (index.isValid()) {
0190                 if (auto *delegate = qobject_cast<KFilePlacesViewDelegate *>(view->itemDelegate())) {
0191                     if (delegate->pointIsHeaderArea(pos)) {
0192                         headerAreaIndex = index;
0193                     } else if (delegate->pointIsTeardownAction(pos)) {
0194                         actionIndex = index;
0195                     }
0196                 }
0197             }
0198 
0199             if (headerAreaIndex != m_hoveredHeaderAreaIndex) {
0200                 if (m_hoveredHeaderAreaIndex.isValid()) {
0201                     Q_EMIT headerAreaLeft(m_hoveredHeaderAreaIndex);
0202                 }
0203                 m_hoveredHeaderAreaIndex = headerAreaIndex;
0204                 if (headerAreaIndex.isValid()) {
0205                     Q_EMIT headerAreaEntered(headerAreaIndex);
0206                 }
0207             }
0208 
0209             if (actionIndex != m_hoveredActionIndex) {
0210                 if (m_hoveredActionIndex.isValid()) {
0211                     Q_EMIT actionLeft(m_hoveredActionIndex);
0212                 }
0213                 m_hoveredActionIndex = actionIndex;
0214                 if (actionIndex.isValid()) {
0215                     Q_EMIT actionEntered(actionIndex);
0216                 }
0217             }
0218 
0219             break;
0220         }
0221         case QEvent::Leave:
0222             if (m_hoveredHeaderAreaIndex.isValid()) {
0223                 Q_EMIT headerAreaLeft(m_hoveredHeaderAreaIndex);
0224             }
0225             m_hoveredHeaderAreaIndex = QModelIndex();
0226 
0227             if (m_hoveredActionIndex.isValid()) {
0228                 Q_EMIT actionLeft(m_hoveredActionIndex);
0229             }
0230             m_hoveredActionIndex = QModelIndex();
0231 
0232             break;
0233         case QEvent::MouseButtonPress: {
0234             QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
0235             m_mousePressed = true;
0236             m_lastMouseSource = mouseEvent->source();
0237 
0238             if (m_isTouchEvent) {
0239                 return true;
0240             }
0241             onPressed(mouseEvent);
0242             Q_FALLTHROUGH();
0243         }
0244         case QEvent::MouseButtonDblClick: {
0245             // Prevent the selection clearing by clicking on the viewport directly
0246             QAbstractItemView *view = qobject_cast<QAbstractItemView *>(watched->parent());
0247             if (!view->indexAt(static_cast<QMouseEvent *>(event)->pos()).isValid()) {
0248                 return true;
0249             }
0250             break;
0251         }
0252         case QEvent::MouseButtonRelease: {
0253             QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
0254             if (mouseEvent->button() == Qt::LeftButton || mouseEvent->button() == Qt::MiddleButton) {
0255                 QAbstractItemView *view = qobject_cast<QAbstractItemView *>(watched->parent());
0256                 const QModelIndex index = view->indexAt(mouseEvent->pos());
0257 
0258                 if (mouseEvent->button() == Qt::LeftButton) {
0259                     if (m_clickedActionIndex.isValid()) {
0260                         if (auto *delegate = qobject_cast<KFilePlacesViewDelegate *>(view->itemDelegate())) {
0261                             if (delegate->pointIsTeardownAction(mouseEvent->pos())) {
0262                                 if (m_clickedActionIndex == index) {
0263                                     Q_EMIT actionClicked(m_clickedActionIndex);
0264                                     // filter out, avoid QAbstractItemView::clicked being emitted
0265                                     return true;
0266                                 }
0267                             }
0268                         }
0269                     }
0270                     m_clickedActionIndex = index;
0271                 } else if (mouseEvent->button() == Qt::MiddleButton) {
0272                     if (m_middleClickedIndex.isValid() && m_middleClickedIndex == index) {
0273                         Q_EMIT entryMiddleClicked(m_middleClickedIndex);
0274                     }
0275                     m_middleClickedIndex = QPersistentModelIndex();
0276                 }
0277             }
0278             break;
0279         }
0280         case QEvent::WindowActivate:
0281             Q_EMIT windowActivated();
0282             break;
0283         case QEvent::WindowDeactivate:
0284             Q_EMIT windowDeactivated();
0285             break;
0286         case QEvent::PaletteChange:
0287             Q_EMIT paletteChanged();
0288             break;
0289         case QEvent::TouchBegin: {
0290             m_isTouchEvent = true;
0291             m_mousePressed = false;
0292             break;
0293         }
0294         case QEvent::Gesture: {
0295             gestureEvent(static_cast<QGestureEvent *>(event));
0296             event->accept();
0297             return true;
0298         }
0299         default:
0300             return false;
0301         }
0302 
0303         return false;
0304     }
0305 
0306     void onPressed(QMouseEvent *mouseEvent)
0307     {
0308         if (mouseEvent->button() == Qt::LeftButton || mouseEvent->button() == Qt::MiddleButton) {
0309             QAbstractItemView *view = qobject_cast<QAbstractItemView *>(q);
0310             const QModelIndex index = view->indexAt(mouseEvent->pos());
0311             if (index.isValid()) {
0312                 if (mouseEvent->button() == Qt::LeftButton) {
0313                     if (auto *delegate = qobject_cast<KFilePlacesViewDelegate *>(view->itemDelegate())) {
0314                         if (delegate->pointIsTeardownAction(mouseEvent->pos())) {
0315                             m_clickedActionIndex = index;
0316                         }
0317                     }
0318                 } else if (mouseEvent->button() == Qt::MiddleButton) {
0319                     m_middleClickedIndex = index;
0320                 }
0321             }
0322         }
0323     }
0324 
0325     void gestureEvent(QGestureEvent *event)
0326     {
0327         if (QGesture *gesture = event->gesture(Qt::TapGesture)) {
0328             tapTriggered(static_cast<QTapGesture *>(gesture));
0329         }
0330         if (QGesture *gesture = event->gesture(Qt::TapAndHoldGesture)) {
0331             tapAndHoldTriggered(static_cast<QTapAndHoldGesture *>(gesture));
0332         }
0333     }
0334 
0335     void tapAndHoldTriggered(QTapAndHoldGesture *tap)
0336     {
0337         if (tap->state() == Qt::GestureFinished) {
0338             if (!m_mousePressed) {
0339                 return;
0340             }
0341 
0342             // the TapAndHold gesture is triggerable with the mouse and stylus, we don't want this
0343             if (m_lastMouseSource == Qt::MouseEventNotSynthesized || !m_isTouchEvent) {
0344                 return;
0345             }
0346 
0347             m_tapAndHoldActive = true;
0348             m_scroller->stop();
0349 
0350             // simulate a mousePressEvent, to allow KFilePlacesView to select the items
0351             const QPointF tapGlobalPos = tap->position(); // QTapAndHoldGesture::position is global
0352             const QPointF tapViewportPos(q->viewport()->mapFromGlobal(tapGlobalPos));
0353             QMouseEvent fakeMousePress(QEvent::MouseButtonPress, tapViewportPos, tapGlobalPos, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
0354             onPressed(&fakeMousePress);
0355             q->mousePressEvent(&fakeMousePress);
0356 
0357             const QPoint tapIndicatorSize(80, 80);
0358             const QPoint pos(q->mapFromGlobal(tapGlobalPos.toPoint()));
0359             const QRect tapIndicatorRect(pos - (tapIndicatorSize / 2), pos + (tapIndicatorSize / 2));
0360             m_rubberBand->setGeometry(tapIndicatorRect.normalized());
0361             m_rubberBand->show();
0362         }
0363     }
0364 
0365     void tapTriggered(QTapGesture *tap)
0366     {
0367         static bool scrollerWasScrolling = false;
0368 
0369         if (tap->state() == Qt::GestureStarted) {
0370             m_tapAndHoldActive = false;
0371             // if QScroller state is Scrolling or Dragging, the user makes the tap to stop the scrolling
0372             auto const scrollerState = m_scroller->state();
0373             if (scrollerState == QScroller::Scrolling || scrollerState == QScroller::Dragging) {
0374                 scrollerWasScrolling = true;
0375             } else {
0376                 scrollerWasScrolling = false;
0377             }
0378         }
0379 
0380         if (tap->state() == Qt::GestureFinished && !scrollerWasScrolling) {
0381             m_isTouchEvent = false;
0382 
0383             // with touch you can touch multiple widgets at the same time, but only one widget will get a mousePressEvent.
0384             // we use this to select the right window
0385             if (!m_mousePressed) {
0386                 return;
0387             }
0388 
0389             if (m_rubberBand->isVisible()) {
0390                 m_rubberBand->hide();
0391             }
0392             // simulate a mousePressEvent, to allow KFilePlacesView to select the items
0393             const QPointF tapPosition = tap->position(); // QTapGesture::position is local
0394             const QPointF globalTapPosition = q->mapToGlobal(tapPosition);
0395             QMouseEvent fakeMousePress(QEvent::MouseButtonPress,
0396                                        tapPosition,
0397                                        globalTapPosition,
0398                                        m_tapAndHoldActive ? Qt::RightButton : Qt::LeftButton,
0399                                        m_tapAndHoldActive ? Qt::RightButton : Qt::LeftButton,
0400                                        Qt::NoModifier);
0401             onPressed(&fakeMousePress);
0402             q->mousePressEvent(&fakeMousePress);
0403 
0404             if (m_tapAndHoldActive) {
0405                 // simulate a contextMenuEvent
0406                 QContextMenuEvent fakeContextMenu(QContextMenuEvent::Mouse, tapPosition.toPoint(), globalTapPosition.toPoint());
0407                 q->contextMenuEvent(&fakeContextMenu);
0408             }
0409             m_tapAndHoldActive = false;
0410         }
0411     }
0412 
0413 private:
0414     QPersistentModelIndex m_hoveredHeaderAreaIndex;
0415     QPersistentModelIndex m_middleClickedIndex;
0416     QPersistentModelIndex m_hoveredActionIndex;
0417     QPersistentModelIndex m_clickedActionIndex;
0418 
0419     KFilePlacesView *const q;
0420 
0421     QRubberBand *m_rubberBand;
0422     bool m_isTouchEvent;
0423     bool m_mousePressed;
0424     bool m_tapAndHoldActive;
0425     Qt::MouseEventSource m_lastMouseSource;
0426 };
0427 
0428 #endif