File indexing completed on 2024-05-05 05:35:32

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