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 }