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 }