File indexing completed on 2024-05-19 09:28:04

0001 /*
0002     SPDX-FileCopyrightText: 2014 Hugo Pereira Da Costa <hugo.pereira@free.fr>
0003     SPDX-FileCopyrightText: 2014 Hugo Pereira Da Costa <hugo.pereira@free.fr>
0004     SPDX-FileCopyrightText: 2007 Thomas Luebking <thomas.luebking@web.de>
0005     SPDX-License-Identifier: GPL-2.0-or-later AND MIT
0006 */
0007 
0008 #include "oxygenwindowmanager.h"
0009 #include "oxygenhelper.h"
0010 #include "oxygenpropertynames.h"
0011 #include "oxygenstyleconfigdata.h"
0012 
0013 #include <QApplication>
0014 #include <QComboBox>
0015 #include <QDialog>
0016 #include <QDockWidget>
0017 #include <QGraphicsView>
0018 #include <QGroupBox>
0019 #include <QLabel>
0020 #include <QListView>
0021 #include <QMainWindow>
0022 #include <QMdiSubWindow>
0023 #include <QMenuBar>
0024 #include <QMouseEvent>
0025 #include <QProgressBar>
0026 #include <QQuickWindow>
0027 #include <QScrollBar>
0028 #include <QStatusBar>
0029 #include <QStyle>
0030 #include <QStyleOptionGroupBox>
0031 #include <QTabBar>
0032 #include <QTabWidget>
0033 #include <QToolBar>
0034 #include <QToolButton>
0035 #include <QTreeView>
0036 
0037 #include <QTextStream>
0038 // needed to deal with device pixel ratio
0039 #include <QWindow>
0040 #include <qnamespace.h>
0041 
0042 namespace Oxygen
0043 {
0044 
0045 //* provide application-wise event filter
0046 /**
0047 it us used to unlock dragging and make sure event look is properly restored
0048 after a drag has occurred
0049 */
0050 class AppEventFilter : public QObject
0051 {
0052 public:
0053     //* constructor
0054     explicit AppEventFilter(WindowManager *parent)
0055         : QObject(parent)
0056         , _parent(parent)
0057     {
0058     }
0059 
0060     //* event filter
0061     bool eventFilter(QObject *object, QEvent *event) override
0062     {
0063         if (event->type() == QEvent::MouseButtonRelease) {
0064             // stop drag timer
0065             if (_parent->_dragTimer.isActive()) {
0066                 _parent->resetDrag();
0067             }
0068 
0069             // unlock
0070             if (_parent->isLocked()) {
0071                 _parent->setLocked(false);
0072             }
0073         }
0074 
0075         if (!_parent->enabled())
0076             return false;
0077 
0078         /*
0079         if a drag is in progress, the widget will not receive any event
0080         we trigger on the first MouseMove or MousePress events that are received
0081         by any widget in the application to detect that the drag is finished
0082         */
0083         if (_parent->useWMMoveResize() && _parent->_dragInProgress && _parent->_target
0084             && (event->type() == QEvent::MouseMove || event->type() == QEvent::MouseButtonPress)) {
0085             return appMouseEvent(object, event);
0086         }
0087 
0088         return false;
0089     }
0090 
0091 protected:
0092     //* application-wise event.
0093     /** needed to catch end of XMoveResize events */
0094     bool appMouseEvent(QObject *, QEvent *event)
0095     {
0096         Q_UNUSED(event);
0097 
0098         /*
0099         post some mouseRelease event to the target, in order to counter balance
0100         the mouse press that triggered the drag. Note that it triggers a resetDrag
0101         */
0102         QMouseEvent mouseEvent(QEvent::MouseButtonRelease, _parent->_dragPoint, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
0103         qApp->sendEvent(_parent->_target.data(), &mouseEvent);
0104 
0105         return false;
0106     }
0107 
0108 private:
0109     //* parent
0110     WindowManager *_parent;
0111 };
0112 
0113 //_____________________________________________________________
0114 WindowManager::WindowManager(QObject *parent)
0115     : QObject(parent)
0116     , _enabled(true)
0117     , _useWMMoveResize(true)
0118     , _dragMode(StyleConfigData::WD_FULL)
0119     , _dragDistance(QApplication::startDragDistance())
0120     , _dragDelay(QApplication::startDragTime())
0121     , _dragAboutToStart(false)
0122     , _dragInProgress(false)
0123     , _locked(false)
0124     , _cursorOverride(false)
0125 {
0126     // install application wise event filter
0127     _appEventFilter = new AppEventFilter(this);
0128     qApp->installEventFilter(_appEventFilter);
0129 }
0130 
0131 //_____________________________________________________________
0132 void WindowManager::initialize(void)
0133 {
0134     setEnabled(StyleConfigData::windowDragMode() != StyleConfigData::WD_NONE);
0135     setDragMode(StyleConfigData::windowDragMode());
0136     setUseWMMoveResize(StyleConfigData::useWMMoveResize());
0137 
0138     setDragDistance(QApplication::startDragDistance());
0139     setDragDelay(QApplication::startDragTime());
0140 
0141     initializeWhiteList();
0142     initializeBlackList();
0143 }
0144 
0145 //_____________________________________________________________
0146 void WindowManager::registerWidget(QWidget *widget)
0147 {
0148     if (isBlackListed(widget) || isDragable(widget)) {
0149         /*
0150         install filter for dragable widgets.
0151         also install filter for blacklisted widgets
0152         to be able to catch the relevant events and prevent
0153         the drag to happen
0154         */
0155         widget->removeEventFilter(this);
0156         widget->installEventFilter(this);
0157     }
0158 }
0159 
0160 void WindowManager::registerQuickItem(QQuickItem *item)
0161 {
0162     if (!item)
0163         return;
0164 
0165     QQuickWindow *window = item->window();
0166     if (window) {
0167         QQuickItem *contentItem = window->contentItem();
0168         contentItem->setAcceptedMouseButtons(Qt::LeftButton);
0169         contentItem->removeEventFilter(this);
0170         contentItem->installEventFilter(this);
0171     }
0172 }
0173 
0174 //_____________________________________________________________
0175 void WindowManager::unregisterWidget(QWidget *widget)
0176 {
0177     if (widget) {
0178         widget->removeEventFilter(this);
0179     }
0180 }
0181 
0182 //_____________________________________________________________
0183 void WindowManager::initializeWhiteList(void)
0184 {
0185     _whiteList.clear();
0186 
0187     // add user specified whitelisted classnames
0188     _whiteList.insert(ExceptionId(QStringLiteral("MplayerWindow")));
0189     _whiteList.insert(ExceptionId(QStringLiteral("ViewSliders@kmix")));
0190     _whiteList.insert(ExceptionId(QStringLiteral("Sidebar_Widget@konqueror")));
0191 
0192     const QStringList whiteList = StyleConfigData::windowDragWhiteList();
0193     for (const QString &exception : whiteList) {
0194         ExceptionId id(exception);
0195         if (!id.className().isEmpty()) {
0196             _whiteList.insert(ExceptionId(exception));
0197         }
0198     }
0199 }
0200 
0201 //_____________________________________________________________
0202 void WindowManager::initializeBlackList(void)
0203 {
0204     _blackList.clear();
0205     _blackList.insert(ExceptionId(QStringLiteral("CustomTrackView@kdenlive")));
0206     _blackList.insert(ExceptionId(QStringLiteral("MuseScore")));
0207     _blackList.insert(ExceptionId(QStringLiteral("KGameCanvasWidget")));
0208 
0209     const QStringList blackList = StyleConfigData::windowDragBlackList();
0210     for (const QString &exception : blackList) {
0211         ExceptionId id(exception);
0212         if (!id.className().isEmpty()) {
0213             _blackList.insert(ExceptionId(exception));
0214         }
0215     }
0216 }
0217 
0218 //_____________________________________________________________
0219 bool WindowManager::eventFilter(QObject *object, QEvent *event)
0220 {
0221     if (!enabled())
0222         return false;
0223 
0224     switch (event->type()) {
0225     case QEvent::MouseButtonPress:
0226         return mousePressEvent(object, event);
0227         break;
0228 
0229     case QEvent::MouseMove:
0230         if (object == _target.data() || object == _quickTarget.data())
0231             return mouseMoveEvent(object, event);
0232         break;
0233 
0234     case QEvent::MouseButtonRelease:
0235         if (_target || _quickTarget)
0236             return mouseReleaseEvent(object, event);
0237 
0238         break;
0239 
0240     default:
0241         break;
0242     }
0243 
0244     return false;
0245 }
0246 
0247 //_____________________________________________________________
0248 void WindowManager::timerEvent(QTimerEvent *event)
0249 {
0250     if (event->timerId() == _dragTimer.timerId()) {
0251         _dragTimer.stop();
0252         if (_target) {
0253             startDrag(_target.data()->window()->windowHandle());
0254         } else if (_quickTarget) {
0255             startDrag(_quickTarget.data()->window());
0256         }
0257 
0258     } else {
0259         return QObject::timerEvent(event);
0260     }
0261 }
0262 
0263 //_____________________________________________________________
0264 bool WindowManager::mousePressEvent(QObject *object, QEvent *event)
0265 {
0266     // cast event and check buttons/modifiers
0267     QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
0268     if (!(mouseEvent->modifiers() == Qt::NoModifier && mouseEvent->button() == Qt::LeftButton)) {
0269         return false;
0270     }
0271     if (mouseEvent->source() != Qt::MouseEventNotSynthesized) {
0272         return false;
0273     }
0274 
0275     // check lock
0276     if (isLocked())
0277         return false;
0278     else
0279         setLocked(true);
0280 
0281     // check QQuickItem - we can immediately start drag, because QQuickWindow's contentItem
0282     // only receives mouse events that weren't handled by children
0283     if (QQuickItem *item = qobject_cast<QQuickItem *>(object)) {
0284         _quickTarget = item;
0285         _dragPoint = mouseEvent->pos();
0286         _globalDragPoint = mouseEvent->globalPos();
0287 
0288         if (_dragTimer.isActive())
0289             _dragTimer.stop();
0290         _dragTimer.start(_dragDelay, this);
0291 
0292         return true;
0293     }
0294 
0295     // cast to widget
0296     QWidget *widget = static_cast<QWidget *>(object);
0297 
0298     // check if widget can be dragged from current position
0299     if (isBlackListed(widget) || !canDrag(widget))
0300         return false;
0301 
0302     // retrieve widget's child at event position
0303     QPoint position(mouseEvent->pos());
0304     QWidget *child = widget->childAt(position);
0305     if (!canDrag(widget, child, position))
0306         return false;
0307 
0308     // save target and drag point
0309     _target = widget;
0310     _dragPoint = position;
0311     _globalDragPoint = mouseEvent->globalPos();
0312     _dragAboutToStart = true;
0313 
0314     // send a move event to the current child with same position
0315     // if received, it is caught to actually start the drag
0316     QPoint localPoint(_dragPoint);
0317     if (child)
0318         localPoint = child->mapFrom(widget, localPoint);
0319     else
0320         child = widget;
0321     QMouseEvent localMouseEvent(QEvent::MouseMove, localPoint, Qt::NoButton, Qt::LeftButton, Qt::NoModifier);
0322     localMouseEvent.setTimestamp(mouseEvent->timestamp());
0323     qApp->sendEvent(child, &localMouseEvent);
0324 
0325     // never eat event
0326     return false;
0327 }
0328 
0329 //_____________________________________________________________
0330 bool WindowManager::mouseMoveEvent(QObject *object, QEvent *event)
0331 {
0332     Q_UNUSED(object);
0333     QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
0334 
0335     if (mouseEvent->source() != Qt::MouseEventNotSynthesized) {
0336         return false;
0337     }
0338 
0339     // stop timer
0340     if (_dragTimer.isActive())
0341         _dragTimer.stop();
0342 
0343     // cast event and check drag distance
0344     if (!_dragInProgress) {
0345         if (_dragAboutToStart) {
0346             if (mouseEvent->pos() == _dragPoint) {
0347                 // start timer,
0348                 _dragAboutToStart = false;
0349                 if (_dragTimer.isActive())
0350                     _dragTimer.stop();
0351                 _dragTimer.start(_dragDelay, this);
0352 
0353             } else
0354                 resetDrag();
0355 
0356         } else if (QPoint(mouseEvent->globalPos() - _globalDragPoint).manhattanLength() >= _dragDistance) {
0357             _dragTimer.start(0, this);
0358         }
0359 
0360         return true;
0361 
0362     } else if (!useWMMoveResize() && _target) {
0363         // use QWidget::move for the grabbing
0364         /* this works only if the sending object and the target are identical */
0365         QWidget *window(_target.data()->window());
0366         window->move(window->pos() + mouseEvent->pos() - _dragPoint);
0367         return true;
0368 
0369     } else
0370         return false;
0371 }
0372 
0373 //_____________________________________________________________
0374 bool WindowManager::mouseReleaseEvent(QObject *object, QEvent *event)
0375 {
0376     Q_UNUSED(object);
0377     Q_UNUSED(event);
0378     resetDrag();
0379     return false;
0380 }
0381 
0382 //_____________________________________________________________
0383 bool WindowManager::isDragable(QWidget *widget)
0384 {
0385     // check widget
0386     if (!widget)
0387         return false;
0388 
0389     // accepted default types
0390     if ((qobject_cast<QDialog *>(widget) && widget->isWindow()) || (qobject_cast<QMainWindow *>(widget) && widget->isWindow())
0391         || qobject_cast<QGroupBox *>(widget)) {
0392         return true;
0393     }
0394 
0395     // more accepted types, provided they are not dock widget titles
0396     if ((qobject_cast<QMenuBar *>(widget) || qobject_cast<QTabBar *>(widget) || qobject_cast<QStatusBar *>(widget) || qobject_cast<QToolBar *>(widget))
0397         && !isDockWidgetTitle(widget)) {
0398         return true;
0399     }
0400 
0401     if (widget->inherits("KScreenSaver") && widget->inherits("KCModule")) {
0402         return true;
0403     }
0404 
0405     if (isWhiteListed(widget)) {
0406         return true;
0407     }
0408 
0409     // flat toolbuttons
0410     if (QToolButton *toolButton = qobject_cast<QToolButton *>(widget)) {
0411         if (toolButton->autoRaise())
0412             return true;
0413     }
0414 
0415     // viewports
0416     /*
0417     one needs to check that
0418     1/ the widget parent is a scrollarea
0419     2/ it matches its parent viewport
0420     3/ the parent is not blacklisted
0421     */
0422     if (QListView *listView = qobject_cast<QListView *>(widget->parentWidget())) {
0423         if (listView->viewport() == widget && !isBlackListed(listView))
0424             return true;
0425     }
0426 
0427     if (QTreeView *treeView = qobject_cast<QTreeView *>(widget->parentWidget())) {
0428         if (treeView->viewport() == widget && !isBlackListed(treeView))
0429             return true;
0430     }
0431 
0432     /*
0433     catch labels in status bars.
0434     this is because of kstatusbar
0435     who captures buttonPress/release events
0436     */
0437     if (QLabel *label = qobject_cast<QLabel *>(widget)) {
0438         if (label->textInteractionFlags().testFlag(Qt::TextSelectableByMouse))
0439             return false;
0440 
0441         QWidget *parent = label->parentWidget();
0442         while (parent) {
0443             if (qobject_cast<QStatusBar *>(parent))
0444                 return true;
0445             parent = parent->parentWidget();
0446         }
0447     }
0448 
0449     return false;
0450 }
0451 
0452 //_____________________________________________________________
0453 bool WindowManager::isBlackListed(QWidget *widget)
0454 {
0455     // check against noAnimations propery
0456     QVariant propertyValue(widget->property(PropertyNames::noWindowGrab));
0457     if (propertyValue.isValid() && propertyValue.toBool())
0458         return true;
0459 
0460     // list-based blacklisted widgets
0461     QString appName(qApp->applicationName());
0462     for (const ExceptionId &id : std::as_const(_blackList)) {
0463         if (!id.appName().isEmpty() && id.appName() != appName)
0464             continue;
0465         if (id.className() == QStringLiteral("*") && !id.appName().isEmpty()) {
0466             // if application name matches and all classes are selected
0467             // disable the grabbing entirely
0468             setEnabled(false);
0469             return true;
0470         }
0471         if (widget->inherits(id.className().toLatin1().data()))
0472             return true;
0473     }
0474 
0475     return false;
0476 }
0477 
0478 //_____________________________________________________________
0479 bool WindowManager::isWhiteListed(QWidget *widget) const
0480 {
0481     QString appName(qApp->applicationName());
0482     for (const ExceptionId &id : std::as_const(_whiteList)) {
0483         if (!id.appName().isEmpty() && id.appName() != appName)
0484             continue;
0485         if (widget->inherits(id.className().toLatin1().data()))
0486             return true;
0487     }
0488 
0489     return false;
0490 }
0491 
0492 //_____________________________________________________________
0493 bool WindowManager::canDrag(QWidget *widget)
0494 {
0495     // check if enabled
0496     if (!enabled())
0497         return false;
0498 
0499     // assume isDragable widget is already passed
0500     // check some special cases where drag should not be effective
0501 
0502     // check mouse grabber
0503     if (QWidget::mouseGrabber())
0504         return false;
0505 
0506     /*
0507     check cursor shape.
0508     Assume that a changed cursor means that some action is in progress
0509     and should prevent the drag
0510     */
0511     if (widget->cursor().shape() != Qt::ArrowCursor)
0512         return false;
0513 
0514     // accept
0515     return true;
0516 }
0517 
0518 //_____________________________________________________________
0519 bool WindowManager::canDrag(QWidget *widget, QWidget *child, const QPoint &position)
0520 {
0521     // retrieve child at given position and check cursor again
0522     if (child && child->cursor().shape() != Qt::ArrowCursor)
0523         return false;
0524 
0525     /*
0526     check against children from which drag should never be enabled,
0527     even if mousePress/Move has been passed to the parent
0528     */
0529     if (child && (qobject_cast<QComboBox *>(child) || qobject_cast<QProgressBar *>(child) || qobject_cast<QScrollBar *>(child))) {
0530         return false;
0531     }
0532 
0533     // tool buttons
0534     if (QToolButton *toolButton = qobject_cast<QToolButton *>(widget)) {
0535         if (dragMode() == StyleConfigData::WD_MINIMAL && !qobject_cast<QToolBar *>(widget->parentWidget()))
0536             return false;
0537         return toolButton->autoRaise() && !toolButton->isEnabled();
0538     }
0539 
0540     // check menubar
0541     if (QMenuBar *menuBar = qobject_cast<QMenuBar *>(widget)) {
0542         // do not drag from menubars embedded in Mdi windows
0543         if (findParent<QMdiSubWindow *>(widget))
0544             return false;
0545 
0546         // check if there is an active action
0547         if (menuBar->activeAction() && menuBar->activeAction()->isEnabled())
0548             return false;
0549 
0550         // check if action at position exists and is enabled
0551         if (QAction *action = menuBar->actionAt(position)) {
0552             if (action->isSeparator())
0553                 return true;
0554             if (action->isEnabled())
0555                 return false;
0556         }
0557 
0558         // return true in all other cases
0559         return true;
0560     }
0561 
0562     /*
0563     in MINIMAL mode, anything that has not been already accepted
0564     and does not come from a toolbar is rejected
0565     */
0566     if (dragMode() == StyleConfigData::WD_MINIMAL) {
0567         if (qobject_cast<QToolBar *>(widget))
0568             return true;
0569         else
0570             return false;
0571     }
0572 
0573     /* following checks are relevant only for WD_FULL mode */
0574 
0575     // tabbar. Make sure no tab is under the cursor
0576     if (QTabBar *tabBar = qobject_cast<QTabBar *>(widget)) {
0577         return tabBar->tabAt(position) == -1;
0578     }
0579 
0580     /*
0581     check groupboxes
0582     prevent drag if unchecking grouboxes
0583     */
0584     if (QGroupBox *groupBox = qobject_cast<QGroupBox *>(widget)) {
0585         // non checkable group boxes are always ok
0586         if (!groupBox->isCheckable())
0587             return true;
0588 
0589         // gather options to retrieve checkbox subcontrol rect
0590         QStyleOptionGroupBox opt;
0591         opt.initFrom(groupBox);
0592         if (groupBox->isFlat())
0593             opt.features |= QStyleOptionFrame::Flat;
0594         opt.lineWidth = 1;
0595         opt.midLineWidth = 0;
0596         opt.text = groupBox->title();
0597         opt.textAlignment = groupBox->alignment();
0598         opt.subControls = (QStyle::SC_GroupBoxFrame | QStyle::SC_GroupBoxCheckBox);
0599         if (!groupBox->title().isEmpty())
0600             opt.subControls |= QStyle::SC_GroupBoxLabel;
0601 
0602         opt.state |= (groupBox->isChecked() ? QStyle::State_On : QStyle::State_Off);
0603 
0604         // check against groupbox checkbox
0605         if (groupBox->style()->subControlRect(QStyle::CC_GroupBox, &opt, QStyle::SC_GroupBoxCheckBox, groupBox).contains(position)) {
0606             return false;
0607         }
0608 
0609         // check against groupbox label
0610         if (!groupBox->title().isEmpty()
0611             && groupBox->style()->subControlRect(QStyle::CC_GroupBox, &opt, QStyle::SC_GroupBoxLabel, groupBox).contains(position)) {
0612             return false;
0613         }
0614 
0615         return true;
0616     }
0617 
0618     // labels
0619     if (QLabel *label = qobject_cast<QLabel *>(widget)) {
0620         if (label->textInteractionFlags().testFlag(Qt::TextSelectableByMouse))
0621             return false;
0622     }
0623 
0624     // abstract item views
0625     QAbstractItemView *itemView(nullptr);
0626     if ((itemView = qobject_cast<QListView *>(widget->parentWidget())) || (itemView = qobject_cast<QTreeView *>(widget->parentWidget()))) {
0627         if (widget == itemView->viewport()) {
0628             // QListView
0629             if (itemView->frameShape() != QFrame::NoFrame)
0630                 return false;
0631             else if (itemView->selectionMode() != QAbstractItemView::NoSelection && itemView->selectionMode() != QAbstractItemView::SingleSelection
0632                      && itemView->model() && itemView->model()->rowCount())
0633                 return false;
0634             else if (itemView->model() && itemView->indexAt(position).isValid())
0635                 return false;
0636         }
0637 
0638     } else if ((itemView = qobject_cast<QAbstractItemView *>(widget->parentWidget()))) {
0639         if (widget == itemView->viewport()) {
0640             // QAbstractItemView
0641             if (itemView->frameShape() != QFrame::NoFrame)
0642                 return false;
0643             else if (itemView->indexAt(position).isValid())
0644                 return false;
0645         }
0646 
0647     } else if (QGraphicsView *graphicsView = qobject_cast<QGraphicsView *>(widget->parentWidget())) {
0648         if (widget == graphicsView->viewport()) {
0649             // QGraphicsView
0650             if (graphicsView->frameShape() != QFrame::NoFrame)
0651                 return false;
0652             else if (graphicsView->dragMode() != QGraphicsView::NoDrag)
0653                 return false;
0654             else if (graphicsView->itemAt(position))
0655                 return false;
0656         }
0657     }
0658 
0659     return true;
0660 }
0661 
0662 //____________________________________________________________
0663 void WindowManager::resetDrag(void)
0664 {
0665     if ((!useWMMoveResize()) && _target && _cursorOverride) {
0666         qApp->restoreOverrideCursor();
0667         _cursorOverride = false;
0668     }
0669 
0670     _target.clear();
0671     _quickTarget.clear();
0672     if (_dragTimer.isActive())
0673         _dragTimer.stop();
0674     _dragPoint = QPoint();
0675     _globalDragPoint = QPoint();
0676     _dragAboutToStart = false;
0677     _dragInProgress = false;
0678 }
0679 
0680 //____________________________________________________________
0681 void WindowManager::startDrag(QWindow *window)
0682 {
0683     if (!(enabled() && window))
0684         return;
0685     if (QWidget::mouseGrabber())
0686         return;
0687 
0688     _dragInProgress = window->startSystemMove();
0689 
0690     return;
0691 }
0692 
0693 //____________________________________________________________
0694 bool WindowManager::isDockWidgetTitle(const QWidget *widget) const
0695 {
0696     if (!widget)
0697         return false;
0698     if (const QDockWidget *dockWidget = qobject_cast<const QDockWidget *>(widget->parent())) {
0699         return widget == dockWidget->titleBarWidget();
0700 
0701     } else
0702         return false;
0703 }
0704 }