File indexing completed on 2024-05-19 13:22:13
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 0041 namespace Oxygen 0042 { 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::LeftButton, Qt::LeftButton, Qt::NoModifier); 0321 qApp->sendEvent(child, &localMouseEvent); 0322 0323 // never eat event 0324 return false; 0325 } 0326 0327 //_____________________________________________________________ 0328 bool WindowManager::mouseMoveEvent(QObject *object, QEvent *event) 0329 { 0330 Q_UNUSED(object); 0331 QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event); 0332 0333 if (mouseEvent->source() != Qt::MouseEventNotSynthesized) { 0334 return false; 0335 } 0336 0337 // stop timer 0338 if (_dragTimer.isActive()) 0339 _dragTimer.stop(); 0340 0341 // cast event and check drag distance 0342 if (!_dragInProgress) { 0343 if (_dragAboutToStart) { 0344 if (mouseEvent->pos() == _dragPoint) { 0345 // start timer, 0346 _dragAboutToStart = false; 0347 if (_dragTimer.isActive()) 0348 _dragTimer.stop(); 0349 _dragTimer.start(_dragDelay, this); 0350 0351 } else 0352 resetDrag(); 0353 0354 } else if (QPoint(mouseEvent->globalPos() - _globalDragPoint).manhattanLength() >= _dragDistance) { 0355 _dragTimer.start(0, this); 0356 } 0357 0358 return true; 0359 0360 } else if (!useWMMoveResize() && _target) { 0361 // use QWidget::move for the grabbing 0362 /* this works only if the sending object and the target are identical */ 0363 QWidget *window(_target.data()->window()); 0364 window->move(window->pos() + mouseEvent->pos() - _dragPoint); 0365 return true; 0366 0367 } else 0368 return false; 0369 } 0370 0371 //_____________________________________________________________ 0372 bool WindowManager::mouseReleaseEvent(QObject *object, QEvent *event) 0373 { 0374 Q_UNUSED(object); 0375 Q_UNUSED(event); 0376 resetDrag(); 0377 return false; 0378 } 0379 0380 //_____________________________________________________________ 0381 bool WindowManager::isDragable(QWidget *widget) 0382 { 0383 // check widget 0384 if (!widget) 0385 return false; 0386 0387 // accepted default types 0388 if ((qobject_cast<QDialog *>(widget) && widget->isWindow()) || (qobject_cast<QMainWindow *>(widget) && widget->isWindow()) 0389 || qobject_cast<QGroupBox *>(widget)) { 0390 return true; 0391 } 0392 0393 // more accepted types, provided they are not dock widget titles 0394 if ((qobject_cast<QMenuBar *>(widget) || qobject_cast<QTabBar *>(widget) || qobject_cast<QStatusBar *>(widget) || qobject_cast<QToolBar *>(widget)) 0395 && !isDockWidgetTitle(widget)) { 0396 return true; 0397 } 0398 0399 if (widget->inherits("KScreenSaver") && widget->inherits("KCModule")) { 0400 return true; 0401 } 0402 0403 if (isWhiteListed(widget)) { 0404 return true; 0405 } 0406 0407 // flat toolbuttons 0408 if (QToolButton *toolButton = qobject_cast<QToolButton *>(widget)) { 0409 if (toolButton->autoRaise()) 0410 return true; 0411 } 0412 0413 // viewports 0414 /* 0415 one needs to check that 0416 1/ the widget parent is a scrollarea 0417 2/ it matches its parent viewport 0418 3/ the parent is not blacklisted 0419 */ 0420 if (QListView *listView = qobject_cast<QListView *>(widget->parentWidget())) { 0421 if (listView->viewport() == widget && !isBlackListed(listView)) 0422 return true; 0423 } 0424 0425 if (QTreeView *treeView = qobject_cast<QTreeView *>(widget->parentWidget())) { 0426 if (treeView->viewport() == widget && !isBlackListed(treeView)) 0427 return true; 0428 } 0429 0430 /* 0431 catch labels in status bars. 0432 this is because of kstatusbar 0433 who captures buttonPress/release events 0434 */ 0435 if (QLabel *label = qobject_cast<QLabel *>(widget)) { 0436 if (label->textInteractionFlags().testFlag(Qt::TextSelectableByMouse)) 0437 return false; 0438 0439 QWidget *parent = label->parentWidget(); 0440 while (parent) { 0441 if (qobject_cast<QStatusBar *>(parent)) 0442 return true; 0443 parent = parent->parentWidget(); 0444 } 0445 } 0446 0447 return false; 0448 } 0449 0450 //_____________________________________________________________ 0451 bool WindowManager::isBlackListed(QWidget *widget) 0452 { 0453 // check against noAnimations propery 0454 QVariant propertyValue(widget->property(PropertyNames::noWindowGrab)); 0455 if (propertyValue.isValid() && propertyValue.toBool()) 0456 return true; 0457 0458 // list-based blacklisted widgets 0459 QString appName(qApp->applicationName()); 0460 for (const ExceptionId &id : std::as_const(_blackList)) { 0461 if (!id.appName().isEmpty() && id.appName() != appName) 0462 continue; 0463 if (id.className() == QStringLiteral("*") && !id.appName().isEmpty()) { 0464 // if application name matches and all classes are selected 0465 // disable the grabbing entirely 0466 setEnabled(false); 0467 return true; 0468 } 0469 if (widget->inherits(id.className().toLatin1().data())) 0470 return true; 0471 } 0472 0473 return false; 0474 } 0475 0476 //_____________________________________________________________ 0477 bool WindowManager::isWhiteListed(QWidget *widget) const 0478 { 0479 QString appName(qApp->applicationName()); 0480 for (const ExceptionId &id : std::as_const(_whiteList)) { 0481 if (!id.appName().isEmpty() && id.appName() != appName) 0482 continue; 0483 if (widget->inherits(id.className().toLatin1().data())) 0484 return true; 0485 } 0486 0487 return false; 0488 } 0489 0490 //_____________________________________________________________ 0491 bool WindowManager::canDrag(QWidget *widget) 0492 { 0493 // check if enabled 0494 if (!enabled()) 0495 return false; 0496 0497 // assume isDragable widget is already passed 0498 // check some special cases where drag should not be effective 0499 0500 // check mouse grabber 0501 if (QWidget::mouseGrabber()) 0502 return false; 0503 0504 /* 0505 check cursor shape. 0506 Assume that a changed cursor means that some action is in progress 0507 and should prevent the drag 0508 */ 0509 if (widget->cursor().shape() != Qt::ArrowCursor) 0510 return false; 0511 0512 // accept 0513 return true; 0514 } 0515 0516 //_____________________________________________________________ 0517 bool WindowManager::canDrag(QWidget *widget, QWidget *child, const QPoint &position) 0518 { 0519 // retrieve child at given position and check cursor again 0520 if (child && child->cursor().shape() != Qt::ArrowCursor) 0521 return false; 0522 0523 /* 0524 check against children from which drag should never be enabled, 0525 even if mousePress/Move has been passed to the parent 0526 */ 0527 if (child && (qobject_cast<QComboBox *>(child) || qobject_cast<QProgressBar *>(child) || qobject_cast<QScrollBar *>(child))) { 0528 return false; 0529 } 0530 0531 // tool buttons 0532 if (QToolButton *toolButton = qobject_cast<QToolButton *>(widget)) { 0533 if (dragMode() == StyleConfigData::WD_MINIMAL && !qobject_cast<QToolBar *>(widget->parentWidget())) 0534 return false; 0535 return toolButton->autoRaise() && !toolButton->isEnabled(); 0536 } 0537 0538 // check menubar 0539 if (QMenuBar *menuBar = qobject_cast<QMenuBar *>(widget)) { 0540 // do not drag from menubars embedded in Mdi windows 0541 if (findParent<QMdiSubWindow *>(widget)) 0542 return false; 0543 0544 // check if there is an active action 0545 if (menuBar->activeAction() && menuBar->activeAction()->isEnabled()) 0546 return false; 0547 0548 // check if action at position exists and is enabled 0549 if (QAction *action = menuBar->actionAt(position)) { 0550 if (action->isSeparator()) 0551 return true; 0552 if (action->isEnabled()) 0553 return false; 0554 } 0555 0556 // return true in all other cases 0557 return true; 0558 } 0559 0560 /* 0561 in MINIMAL mode, anything that has not been already accepted 0562 and does not come from a toolbar is rejected 0563 */ 0564 if (dragMode() == StyleConfigData::WD_MINIMAL) { 0565 if (qobject_cast<QToolBar *>(widget)) 0566 return true; 0567 else 0568 return false; 0569 } 0570 0571 /* following checks are relevant only for WD_FULL mode */ 0572 0573 // tabbar. Make sure no tab is under the cursor 0574 if (QTabBar *tabBar = qobject_cast<QTabBar *>(widget)) { 0575 return tabBar->tabAt(position) == -1; 0576 } 0577 0578 /* 0579 check groupboxes 0580 prevent drag if unchecking grouboxes 0581 */ 0582 if (QGroupBox *groupBox = qobject_cast<QGroupBox *>(widget)) { 0583 // non checkable group boxes are always ok 0584 if (!groupBox->isCheckable()) 0585 return true; 0586 0587 // gather options to retrieve checkbox subcontrol rect 0588 QStyleOptionGroupBox opt; 0589 opt.initFrom(groupBox); 0590 if (groupBox->isFlat()) 0591 opt.features |= QStyleOptionFrame::Flat; 0592 opt.lineWidth = 1; 0593 opt.midLineWidth = 0; 0594 opt.text = groupBox->title(); 0595 opt.textAlignment = groupBox->alignment(); 0596 opt.subControls = (QStyle::SC_GroupBoxFrame | QStyle::SC_GroupBoxCheckBox); 0597 if (!groupBox->title().isEmpty()) 0598 opt.subControls |= QStyle::SC_GroupBoxLabel; 0599 0600 opt.state |= (groupBox->isChecked() ? QStyle::State_On : QStyle::State_Off); 0601 0602 // check against groupbox checkbox 0603 if (groupBox->style()->subControlRect(QStyle::CC_GroupBox, &opt, QStyle::SC_GroupBoxCheckBox, groupBox).contains(position)) { 0604 return false; 0605 } 0606 0607 // check against groupbox label 0608 if (!groupBox->title().isEmpty() 0609 && groupBox->style()->subControlRect(QStyle::CC_GroupBox, &opt, QStyle::SC_GroupBoxLabel, groupBox).contains(position)) { 0610 return false; 0611 } 0612 0613 return true; 0614 } 0615 0616 // labels 0617 if (QLabel *label = qobject_cast<QLabel *>(widget)) { 0618 if (label->textInteractionFlags().testFlag(Qt::TextSelectableByMouse)) 0619 return false; 0620 } 0621 0622 // abstract item views 0623 QAbstractItemView *itemView(nullptr); 0624 if ((itemView = qobject_cast<QListView *>(widget->parentWidget())) || (itemView = qobject_cast<QTreeView *>(widget->parentWidget()))) { 0625 if (widget == itemView->viewport()) { 0626 // QListView 0627 if (itemView->frameShape() != QFrame::NoFrame) 0628 return false; 0629 else if (itemView->selectionMode() != QAbstractItemView::NoSelection && itemView->selectionMode() != QAbstractItemView::SingleSelection 0630 && itemView->model() && itemView->model()->rowCount()) 0631 return false; 0632 else if (itemView->model() && itemView->indexAt(position).isValid()) 0633 return false; 0634 } 0635 0636 } else if ((itemView = qobject_cast<QAbstractItemView *>(widget->parentWidget()))) { 0637 if (widget == itemView->viewport()) { 0638 // QAbstractItemView 0639 if (itemView->frameShape() != QFrame::NoFrame) 0640 return false; 0641 else if (itemView->indexAt(position).isValid()) 0642 return false; 0643 } 0644 0645 } else if (QGraphicsView *graphicsView = qobject_cast<QGraphicsView *>(widget->parentWidget())) { 0646 if (widget == graphicsView->viewport()) { 0647 // QGraphicsView 0648 if (graphicsView->frameShape() != QFrame::NoFrame) 0649 return false; 0650 else if (graphicsView->dragMode() != QGraphicsView::NoDrag) 0651 return false; 0652 else if (graphicsView->itemAt(position)) 0653 return false; 0654 } 0655 } 0656 0657 return true; 0658 } 0659 0660 //____________________________________________________________ 0661 void WindowManager::resetDrag(void) 0662 { 0663 if ((!useWMMoveResize()) && _target && _cursorOverride) { 0664 qApp->restoreOverrideCursor(); 0665 _cursorOverride = false; 0666 } 0667 0668 _target.clear(); 0669 _quickTarget.clear(); 0670 if (_dragTimer.isActive()) 0671 _dragTimer.stop(); 0672 _dragPoint = QPoint(); 0673 _globalDragPoint = QPoint(); 0674 _dragAboutToStart = false; 0675 _dragInProgress = false; 0676 } 0677 0678 //____________________________________________________________ 0679 void WindowManager::startDrag(QWindow *window) 0680 { 0681 if (!(enabled() && window)) 0682 return; 0683 if (QWidget::mouseGrabber()) 0684 return; 0685 0686 _dragInProgress = window->startSystemMove(); 0687 0688 return; 0689 } 0690 0691 //____________________________________________________________ 0692 bool WindowManager::isDockWidgetTitle(const QWidget *widget) const 0693 { 0694 if (!widget) 0695 return false; 0696 if (const QDockWidget *dockWidget = qobject_cast<const QDockWidget *>(widget->parent())) { 0697 return widget == dockWidget->titleBarWidget(); 0698 0699 } else 0700 return false; 0701 } 0702 }