KDDockWidgets API Documentation 1.7
Loading...
Searching...
No Matches
DragController.cpp
Go to the documentation of this file.
1/*
2 This file is part of KDDockWidgets.
3
4 SPDX-FileCopyrightText: 2019-2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
5 Author: SĂ©rgio Martins <sergio.martins@kdab.com>
6
7 SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only
8
9 Contact KDAB at <info@kdab.com> for commercial licensing options.
10*/
11
12#include "DragController_p.h"
13#include "DockRegistry_p.h"
14#include "DockWidgetBase_p.h"
15#include "DropArea_p.h"
16#include "FloatingWindow_p.h"
17#include "Frame_p.h"
18#include "Logging_p.h"
19#include "Qt5Qt6Compat_p.h"
20#include "Utils_p.h"
21#include "WidgetResizeHandler_p.h"
22#include "Config.h"
23#include "MDILayoutWidget_p.h"
24#include "WindowZOrder_x11_p.h"
25
26#include <QMouseEvent>
27#include <QGuiApplication>
28#include <QCursor>
29#include <QWindow>
30#include <QDrag>
31#include <QScopedValueRollback>
32
33#if defined(Q_OS_WIN)
34#include <windows.h>
35#endif
36
37using namespace KDDockWidgets;
38
39namespace KDDockWidgets {
41class FallbackMouseGrabber : public QObject
42{
43public:
44 FallbackMouseGrabber(QObject *parent)
46 {
47 }
48
49 ~FallbackMouseGrabber() override;
50
51 void grabMouse(QWidgetOrQuick *target)
52 {
53 m_target = target;
54 qApp->installEventFilter(this);
55 }
56
57 void releaseMouse()
58 {
59#ifdef KDDOCKWIDGETS_QTQUICK
60 // Ungrab harder if QtQuick.
61 // QtQuick has the habit og grabbing the MouseArea internally, then doesn't ungrab it since
62 // we're consuming the events. So explicitly ungrab if any QQuickWindow::mouseGrabberItem()
63 // is still set.
64
65 QQuickView *view = m_target ? m_target->quickView()
66 : nullptr;
67 QQuickItem *grabber = view ? view->mouseGrabberItem()
68 : nullptr;
69 if (grabber)
70 grabber->ungrabMouse();
71#endif
72
73 m_target = nullptr;
74 qApp->removeEventFilter(this);
75 }
76
77 bool eventFilter(QObject *, QEvent *ev) override
78 {
79 if (m_reentrancyGuard || !m_target)
80 return false;
81
82 if (QMouseEvent *me = mouseEvent(ev)) {
83 m_reentrancyGuard = true;
84 qApp->sendEvent(m_target, me);
85 m_reentrancyGuard = false;
86 return true;
87 }
88
89 return false;
90 }
91
92 bool m_reentrancyGuard = false;
94};
95
96FallbackMouseGrabber::~FallbackMouseGrabber()
97{
98}
99
100}
101
102State::State(MinimalStateMachine *parent)
103 : QObject(parent)
104 , m_machine(parent)
105{
106}
107
108State::~State() = default;
109
110bool State::isCurrentState() const
111{
112 return m_machine->currentState() == this;
113}
114
115MinimalStateMachine::MinimalStateMachine(QObject *parent)
116 : QObject(parent)
117{
118}
119
120template<typename Obj, typename Signal>
121void State::addTransition(Obj *obj, Signal signal, State *dest)
122{
123 connect(obj, signal, this, [this, dest] {
124 if (isCurrentState()) {
125 m_machine->setCurrentState(dest);
126 }
127 });
128}
129
130
131State *MinimalStateMachine::currentState() const
132{
133 return m_currentState;
134}
135
136void MinimalStateMachine::setCurrentState(State *state)
137{
138 if (state != m_currentState) {
139 if (m_currentState)
140 m_currentState->onExit();
141
142 m_currentState = state;
143
144 if (state)
145 state->onEntry();
146
147 Q_EMIT currentStateChanged();
148 }
149}
150
151StateBase::StateBase(DragController *parent)
152 : State(parent)
153 , q(parent)
154{
155}
156
157bool StateBase::isActiveState() const
158{
159 return q->activeState() == this;
160}
161
162StateBase::~StateBase() = default;
163
164StateNone::StateNone(DragController *parent)
165 : StateBase(parent)
166{
167}
168
169void StateNone::onEntry()
170{
171 qCDebug(state) << "StateNone entered";
172 q->m_pressPos = QPoint();
173 q->m_offset = QPoint();
174 q->m_draggable = nullptr;
175 q->m_draggableGuard.clear();
176 q->m_windowBeingDragged.reset();
177 WidgetResizeHandler::s_disableAllHandlers = false; // Re-enable resize handlers
178
179 q->m_nonClientDrag = false;
180 if (q->m_currentDropArea) {
181 q->m_currentDropArea->removeHover();
182 q->m_currentDropArea = nullptr;
183 }
184
185 Q_EMIT q->isDraggingChanged();
186}
187
188bool StateNone::handleMouseButtonPress(Draggable *draggable, QPoint globalPos, QPoint pos)
189{
190 qCDebug(state) << "StateNone::handleMouseButtonPress: draggable"
191 << draggable->asWidget() << "; globalPos" << globalPos;
192
193 if (!draggable->isPositionDraggable(pos))
194 return false;
195
196 q->m_draggable = draggable;
197 q->m_draggableGuard = draggable->asWidget();
198 q->m_pressPos = globalPos;
199 q->m_offset = draggable->mapToWindow(pos);
200 Q_EMIT q->mousePressed();
201 return false;
202}
203
204StateNone::~StateNone() = default;
205
206
207StatePreDrag::StatePreDrag(DragController *parent)
208 : StateBase(parent)
209{
210}
211
212StatePreDrag::~StatePreDrag() = default;
213
214void StatePreDrag::onEntry()
215{
216 qCDebug(state) << "StatePreDrag entered" << q->m_draggableGuard.data();
217 WidgetResizeHandler::s_disableAllHandlers = true; // Disable the resize handler during dragging
218}
219
220bool StatePreDrag::handleMouseMove(QPoint globalPos)
221{
222 if (!q->m_draggableGuard) {
223 qWarning() << Q_FUNC_INFO << "Draggable was destroyed, canceling the drag";
224 Q_EMIT q->dragCanceled();
225 return false;
226 }
227
228 if (q->m_draggable->dragCanStart(q->m_pressPos, globalPos)) {
229 if (q->m_draggable->isMDI())
230 Q_EMIT q->manhattanLengthMoveMDI();
231 else
232 Q_EMIT q->manhattanLengthMove();
233 return true;
234 }
235 return false;
236}
237
238bool StatePreDrag::handleMouseButtonRelease(QPoint)
239{
240 Q_EMIT q->dragCanceled();
241 return false;
242}
243
244bool StatePreDrag::handleMouseDoubleClick()
245{
246 // This is only needed for QtQuick.
247 // With QtQuick, when double clicking, we get: Press, Release, Press, Double-click. and never
248 // receive the last Release event.
249 Q_EMIT q->dragCanceled();
250 return false;
251}
252
253StateDragging::StateDragging(DragController *parent)
254 : StateBase(parent)
255{
256#if defined(Q_OS_WIN) && !defined(DOCKS_DEVELOPER_MODE)
257 m_maybeCancelDrag.setInterval(100);
258 QObject::connect(&m_maybeCancelDrag, &QTimer::timeout, this, [this] {
259 // Workaround bug #166 , where Qt doesn't agree with Window's mouse button state.
260 // Looking in the Qt bug tracker there's many hits, so do a quick workaround here:
261
262 const bool mouseButtonIsReallyDown = (GetKeyState(VK_LBUTTON) & 0x8000);
263 if (!mouseButtonIsReallyDown && isLeftButtonPressed()) {
264 qCDebug(state) << "Canceling drag, Qt thinks mouse button is pressed"
265 << "but Windows knows it's not";
266 handleMouseButtonRelease(QCursor::pos());
267 Q_EMIT q->dragCanceled();
268 }
269 });
270#endif
271}
272
273StateDragging::~StateDragging() = default;
274
275void StateDragging::onEntry()
276{
277 m_maybeCancelDrag.start();
278
279 if (DockWidgetBase *dw = q->m_draggable->singleDockWidget()) {
280 // When we start to drag a floating window which has a single dock widget, we save the position
281 if (dw->isFloating())
282 dw->d->saveLastFloatingGeometry();
283 }
284
285 const bool needsUndocking = !q->m_draggable->isWindow();
286 q->m_windowBeingDragged = q->m_draggable->makeWindow();
287 if (q->m_windowBeingDragged) {
288#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) && defined(Q_OS_WIN)
289 if (!q->m_nonClientDrag && KDDockWidgets::usesNativeDraggingAndResizing()) {
290 // Started as a client move, as the dock widget was docked,
291 // but now that we're dragging it as a floating window, switch to native drag, so we can still get aero-snap
292 FloatingWindow *fw = q->m_windowBeingDragged->floatingWindow();
293 q->m_nonClientDrag = true;
294 q->m_windowBeingDragged.reset();
295 q->m_windowBeingDragged = fw->makeWindow();
296
297 QWindow *window = fw->windowHandle();
298
299 if (needsUndocking) {
300 // Position the window before the drag start, otherwise if you move mouse too fast there will be an offset
301 // Only required when we've undocked/detached a window.
302 window->setPosition(QCursor::pos() - q->m_offset);
303 }
304
305 // Start the native move
306 window->startSystemMove();
307 }
308#else
309 Q_UNUSED(needsUndocking);
310#endif
311
312 qCDebug(state) << "StateDragging entered. m_draggable=" << q->m_draggable->asWidget()
313 << "; m_windowBeingDragged=" << q->m_windowBeingDragged->floatingWindow();
314
315 auto fw = q->m_windowBeingDragged->floatingWindow();
316#ifdef Q_OS_LINUX
317 if (fw->isMaximizedOverride()) {
318 // When dragging a maximized window on linux we need to restore its normal size
319 // On Windows this works already. On macOS I don't see this feature at all
320
321 const QRect normalGeometry = fw->normalGeometry();
322
323 // distance to the left edge of the window:
324 const int leftOffset = q->m_offset.x();
325
326 // distance to the right edge of the window:
327 const int rightOffset = fw->width() - q->m_offset.x();
328
329 const bool leftEdgeIsNearest = leftOffset <= rightOffset;
330
331 fw->showNormal();
332
333 if (!normalGeometry.contains(q->m_pressPos)) {
334 if ((leftEdgeIsNearest && leftOffset > normalGeometry.width()) || (!leftEdgeIsNearest && rightOffset > normalGeometry.width())) {
335 // Case #1: The window isn't under the cursor anymore
336 // Let's just put its middle under the cursor
337 q->m_offset.setX(normalGeometry.width() / 2);
338 } else if (!leftEdgeIsNearest) {
339 // Case #2: The new geometry is still under the cursor, but instead of moving its right edge left
340 // we'll move the left edge right, since initially the press position was closer to the right edge
341 q->m_offset.setX(normalGeometry.width() - rightOffset);
342 }
343 }
344 } else
345#endif
346 if (!fw->geometry().contains(q->m_pressPos)) {
347 // The window shrunk when the drag started, this can happen if it has max-size constraints
348 // we make the floating window smaller. Has the downside that it might not be under the mouse
349 // cursor anymore, so make the change
350 if (fw->width() < q->m_offset.x()) { // make sure it shrunk
351 q->m_offset.setX(fw->width() / 2);
352 }
353 }
354 } else {
355 // Shouldn't happen
356 qWarning() << Q_FUNC_INFO << "No window being dragged for " << q->m_draggable->asWidget();
357 Q_EMIT q->dragCanceled();
358 }
359
360 Q_EMIT q->isDraggingChanged();
361}
362
363void StateDragging::onExit()
364{
365 m_maybeCancelDrag.stop();
366}
367
368bool StateDragging::handleMouseButtonRelease(QPoint globalPos)
369{
370 qCDebug(state) << "StateDragging: handleMouseButtonRelease";
371
372 FloatingWindow *floatingWindow = q->m_windowBeingDragged->floatingWindow();
373 if (!floatingWindow) {
374 // It was deleted externally
375 qCDebug(state) << "StateDragging: Bailling out, deleted externally";
376 Q_EMIT q->dragCanceled();
377 return true;
378 }
379
380 if (floatingWindow->anyNonDockable()) {
381 qCDebug(state) << "StateDragging: Ignoring floating window with non dockable widgets";
382 Q_EMIT q->dragCanceled();
383 return true;
384 }
385
386 if (q->m_currentDropArea) {
387 if (q->m_currentDropArea->drop(q->m_windowBeingDragged.get(), globalPos)) {
388 Q_EMIT q->dropped();
389 } else {
390 qCDebug(state) << "StateDragging: Bailling out, drop not accepted";
391 Q_EMIT q->dragCanceled();
392 }
393 } else {
394 qCDebug(state) << "StateDragging: Bailling out, not over a drop area";
395 Q_EMIT q->dragCanceled();
396 }
397 return true;
398}
399
400bool StateDragging::handleMouseMove(QPoint globalPos)
401{
402 FloatingWindow *fw = q->m_windowBeingDragged->floatingWindow();
403 if (!fw) {
404 qCDebug(state) << "Canceling drag, window was deleted";
405 Q_EMIT q->dragCanceled();
406 return true;
407 }
408
409 if (fw->beingDeleted()) {
410 // Ignore, we're in the middle of recurrency. We're inside StateDragging::handleMouseButtonRelease too
411 return true;
412 }
413
414#ifdef Q_OS_LINUX
415 if (fw->lastWindowManagerState() == Qt::WindowMaximized) {
416 // The window was maximized, we dragged it, which triggers a show normal.
417 // But we can only start moving the window *after* the (async) window manager acknowledges.
418 // See QTBUG-102430.
419 // Since #286 was only implemented and needed on Linux, then this counter-part is also ifdefed for Linux,
420 // Probably the ifdef could be removed, but don't want to be testing N platforms, who's undocumented behaviour
421 // can change between releases, so narrow the scope and workaround for linux only.
422 return true;
423 }
424#endif
425
426 if (!q->m_nonClientDrag)
427 fw->windowHandle()->setPosition(globalPos - q->m_offset);
428
429 if (fw->anyNonDockable()) {
430 qCDebug(state) << "StateDragging: Ignoring non dockable floating window";
431 return true;
432 }
433
434 DropArea *dropArea = q->dropAreaUnderCursor();
435 if (q->m_currentDropArea && dropArea != q->m_currentDropArea)
436 q->m_currentDropArea->removeHover();
437
438 if (dropArea) {
439 if (FloatingWindow *targetFw = dropArea->floatingWindow()) {
440 if (targetFw->anyNonDockable()) {
441 qCDebug(state) << "StateDragging: Ignoring non dockable target floating window";
442 return false;
443 }
444 }
445
446 dropArea->hover(q->m_windowBeingDragged.get(), globalPos);
447 }
448
449 q->m_currentDropArea = dropArea;
450
451 return true;
452}
453
454bool StateDragging::handleMouseDoubleClick()
455{
456 // See comment in StatePreDrag::handleMouseDoubleClick().
457 // Very unlikely that we're in this state though, due to manhattan length
458 Q_EMIT q->dragCanceled();
459 return false;
460}
461
462StateInternalMDIDragging::StateInternalMDIDragging(DragController *parent)
463 : StateBase(parent)
464{
465}
466
467StateInternalMDIDragging::~StateInternalMDIDragging()
468{
469}
470
471void StateInternalMDIDragging::onEntry()
472{
473 qCDebug(state) << "StateInternalMDIDragging entered. draggable="
474 << q->m_draggable->asWidget();
475
476 // Raise the dock widget being dragged
477 if (auto tb = qobject_cast<TitleBar *>(q->m_draggable->asWidget())) {
478 if (Frame *f = tb->frame())
479 f->raise();
480 }
481
482 Q_EMIT q->isDraggingChanged();
483}
484
485bool StateInternalMDIDragging::handleMouseButtonRelease(QPoint)
486{
487 Q_EMIT q->dragCanceled();
488 return false;
489}
490
491bool StateInternalMDIDragging::handleMouseMove(QPoint globalPos)
492{
493 // for MDI we only support dragging via the title bar, other cases don't make sense conceptually
494 auto tb = qobject_cast<TitleBar *>(q->m_draggable->asWidget());
495 if (!tb) {
496 qWarning() << Q_FUNC_INFO << "expected a title bar, not" << q->m_draggable->asWidget();
497 Q_EMIT q->dragCanceled();
498 return false;
499 }
500
501 Frame *frame = tb->frame();
502 if (!frame) {
503 // Doesn't happen.
504 qWarning() << Q_FUNC_INFO << "null frame.";
505 Q_EMIT q->dragCanceled();
506 return false;
507 }
508
509 const QSize parentSize = frame->QWidgetAdapter::parentWidget()->size();
510 const QPoint oldPos = frame->mapToGlobal(QPoint(0, 0));
511 const QPoint delta = globalPos - oldPos;
512 const QPoint newLocalPos = frame->pos() + delta - q->m_offset;
513
514 // Let's not allow the MDI window to go outside of its parent
515
516 QPoint newLocalPosBounded = { qMax(0, newLocalPos.x()), qMax(0, newLocalPos.y()) };
517 newLocalPosBounded.setX(qMin(newLocalPosBounded.x(), parentSize.width() - frame->width()));
518 newLocalPosBounded.setY(qMin(newLocalPosBounded.y(), parentSize.height() - frame->height()));
519
520 auto layout = frame->mdiLayoutWidget();
521 Q_ASSERT(layout);
522 layout->moveDockWidget(frame, newLocalPosBounded);
523
524 // Check if we need to pop out the MDI window (make it float)
525 // If we drag the window against an edge, and move behind the edge some threshold, we float it
526 const int threshold = Config::self().mdiPopupThreshold();
527 if (threshold != -1) {
528 const QPoint overflow = newLocalPosBounded - newLocalPos;
529 if (qAbs(overflow.x()) > threshold || qAbs(overflow.y()) > threshold)
530 Q_EMIT q->mdiPopOut();
531 }
532
533 return false;
534}
535
536bool StateInternalMDIDragging::handleMouseDoubleClick()
537{
538 Q_EMIT q->dragCanceled();
539 return false;
540}
541
542StateDraggingWayland::StateDraggingWayland(DragController *parent)
543 : StateDragging(parent)
544{
545}
546
547StateDraggingWayland::~StateDraggingWayland()
548{
549}
550
551void StateDraggingWayland::onEntry()
552{
553 qCDebug(state) << "StateDragging entered";
554
555 if (m_inQDrag) {
556 // Maybe we can exit the state due to the nested event loop of QDrag::Exec();
557 qWarning() << Q_FUNC_INFO << "Impossible!";
558 return;
559 }
560
561 QScopedValueRollback<bool> guard(m_inQDrag, true);
562 q->m_windowBeingDragged = std::unique_ptr<WindowBeingDragged>(new WindowBeingDraggedWayland(q->m_draggable));
563
564 auto mimeData = new WaylandMimeData();
565 QDrag drag(this);
566 drag.setMimeData(mimeData);
567 drag.setPixmap(q->m_windowBeingDragged->pixmap());
568
569 qApp->installEventFilter(q);
570 const Qt::DropAction result = drag.exec();
571 qApp->removeEventFilter(q);
572 if (result == Qt::IgnoreAction)
573 Q_EMIT q->dragCanceled();
574}
575
576bool StateDraggingWayland::handleMouseButtonRelease(QPoint /*globalPos*/)
577{
578 qCDebug(state) << Q_FUNC_INFO;
579 Q_EMIT q->dragCanceled();
580 return true;
581}
582
583bool StateDraggingWayland::handleMouseMove(QPoint)
584{
585 // Wayland uses QDrag to drag stuff while other platforms use mouse.
586 // So override handleMouseMove() just so the regular mouse stuff doesn't run.
587
588 return false;
589}
590
591bool StateDraggingWayland::handleDragEnter(QDragEnterEvent *ev, DropArea *dropArea)
592{
593 auto mimeData = qobject_cast<const WaylandMimeData *>(ev->mimeData());
594 qCDebug(state) << Q_FUNC_INFO << mimeData << dropArea << q->m_windowBeingDragged.get();
595 if (!mimeData || !q->m_windowBeingDragged)
596 return false; // Not for us, some other user drag.
597
598 if (q->m_windowBeingDragged->contains(dropArea)) {
599 ev->ignore();
600 return true;
601 }
602
603 dropArea->hover(q->m_windowBeingDragged.get(), dropArea->mapToGlobal(Qt5Qt6Compat::eventPos(ev)));
604
605 ev->accept();
606 return true;
607}
608
609bool StateDraggingWayland::handleDragLeave(DropArea *dropArea)
610{
611 qCDebug(state) << Q_FUNC_INFO;
612 dropArea->removeHover();
613 return true;
614}
615
616bool StateDraggingWayland::handleDrop(QDropEvent *ev, DropArea *dropArea)
617{
618 qCDebug(state) << Q_FUNC_INFO;
619 auto mimeData = qobject_cast<const WaylandMimeData *>(ev->mimeData());
620 if (!mimeData || !q->m_windowBeingDragged)
621 return false; // Not for us, some other user drag.
622
623 if (dropArea->drop(q->m_windowBeingDragged.get(), dropArea->mapToGlobal(Qt5Qt6Compat::eventPos(ev)))) {
625 ev->accept();
626 Q_EMIT q->dropped();
627 } else {
628 Q_EMIT q->dragCanceled();
629 }
630
631 dropArea->removeHover();
632 return true;
633}
634
635bool StateDraggingWayland::handleDragMove(QDragMoveEvent *ev, DropArea *dropArea)
636{
637 auto mimeData = qobject_cast<const WaylandMimeData *>(ev->mimeData());
638 if (!mimeData || !q->m_windowBeingDragged)
639 return false; // Not for us, some other user drag.
640
641 dropArea->hover(q->m_windowBeingDragged.get(), dropArea->mapToGlobal(Qt5Qt6Compat::eventPos(ev)));
642
643 return true;
644}
645
646DragController::DragController(QObject *parent)
647 : MinimalStateMachine(parent)
648{
649 qCDebug(creation) << "DragController()";
650
651 m_stateNone = new StateNone(this);
652 auto statepreDrag = new StatePreDrag(this);
653 auto stateDragging = isWayland() ? new StateDraggingWayland(this)
654 : new StateDragging(this);
655 m_stateDraggingMDI = new StateInternalMDIDragging(this);
656
657 m_stateNone->addTransition(this, &DragController::mousePressed, statepreDrag);
658 statepreDrag->addTransition(this, &DragController::dragCanceled, m_stateNone);
659 statepreDrag->addTransition(this, &DragController::manhattanLengthMove, stateDragging);
660 statepreDrag->addTransition(this, &DragController::manhattanLengthMoveMDI, m_stateDraggingMDI);
661 stateDragging->addTransition(this, &DragController::dragCanceled, m_stateNone);
662 stateDragging->addTransition(this, &DragController::dropped, m_stateNone);
663
664 m_stateDraggingMDI->addTransition(this, &DragController::dragCanceled, m_stateNone);
665 m_stateDraggingMDI->addTransition(this, &DragController::mdiPopOut, stateDragging);
666
667 if (usesFallbackMouseGrabber())
668 enableFallbackMouseGrabber();
669
670 setCurrentState(m_stateNone);
671}
672
673DragController *DragController::instance()
674{
675 static DragController dragController;
676 return &dragController;
677}
678
679void DragController::registerDraggable(Draggable *drg)
680{
681 m_draggables << drg;
682 drg->asWidget()->installEventFilter(this);
683}
684
685void DragController::unregisterDraggable(Draggable *drg)
686{
687 m_draggables.removeOne(drg);
688 drg->asWidget()->removeEventFilter(this);
689}
690
691bool DragController::isDragging() const
692{
693 return m_windowBeingDragged != nullptr || activeState() == m_stateDraggingMDI;
694}
695
696bool DragController::isInNonClientDrag() const
697{
698 return isDragging() && m_nonClientDrag;
699}
700
701bool DragController::isInClientDrag() const
702{
703 return isDragging() && !m_nonClientDrag;
704}
705
706bool DragController::isIdle() const
707{
708 return activeState() == m_stateNone;
709}
710
711void DragController::grabMouseFor(QWidgetOrQuick *target)
712{
713 if (isWayland())
714 return; // No grabbing supported on wayland
715
716 if (m_fallbackMouseGrabber) {
717 m_fallbackMouseGrabber->grabMouse(target);
718 } else {
719 target->grabMouse();
720 }
721}
722
723void DragController::releaseMouse(QWidgetOrQuick *target)
724{
725 if (isWayland())
726 return; // No grabbing supported on wayland
727
728 if (m_fallbackMouseGrabber) {
729 m_fallbackMouseGrabber->releaseMouse();
730 } else {
731 target->releaseMouse();
732 }
733}
734
735FloatingWindow *DragController::floatingWindowBeingDragged() const
736{
737 return m_windowBeingDragged ? m_windowBeingDragged->floatingWindow()
738 : nullptr;
739}
740
741void DragController::enableFallbackMouseGrabber()
742{
743 if (!m_fallbackMouseGrabber)
744 m_fallbackMouseGrabber = new FallbackMouseGrabber(this);
745}
746
747WindowBeingDragged *DragController::windowBeingDragged() const
748{
749 return m_windowBeingDragged.get();
750}
751
752bool DragController::eventFilter(QObject *o, QEvent *e)
753{
754 if (m_nonClientDrag && e->type() == QEvent::Move) {
755 // On Windows, non-client mouse moves are only sent at the end, so we must fake it:
756 qCDebug(mouseevents) << "DragController::eventFilter e=" << e->type() << "; o=" << o;
757 activeState()->handleMouseMove(QCursor::pos());
758 return MinimalStateMachine::eventFilter(o, e);
759 }
760
761 if (isWayland()) {
762 // Wayland is very different. It uses QDrag for the dragging of a window.
763 if (auto dropArea = qobject_cast<DropArea *>(o)) {
764 switch (int(e->type())) {
766 if (activeState()->handleDragEnter(static_cast<QDragEnterEvent *>(e), dropArea))
767 return true;
768 break;
770 if (activeState()->handleDragLeave(dropArea))
771 return true;
772 break;
773 case QEvent::DragMove:
774 if (activeState()->handleDragMove(static_cast<QDragMoveEvent *>(e), dropArea))
775 return true;
776 break;
777 case QEvent::Drop:
778 if (activeState()->handleDrop(static_cast<QDropEvent *>(e), dropArea))
779 return true;
780 break;
781 }
782 } else if (e->type() == QEvent::DragEnter && isDragging()) {
783 // We're dragging a window. Be sure user code doesn't accept DragEnter events.
784 return true;
785 }
786 }
787
788 QMouseEvent *me = mouseEvent(e);
789 if (!me)
790 return MinimalStateMachine::eventFilter(o, e);
791
792 auto w = qobject_cast<QWidgetOrQuick *>(o);
793 if (!w)
794 return MinimalStateMachine::eventFilter(o, e);
795
796 qCDebug(mouseevents) << "DragController::eventFilter e=" << e->type() << "; o=" << o
797 << "; m_nonClientDrag=" << m_nonClientDrag;
798
799 switch (e->type()) {
801 if (auto fw = qobject_cast<FloatingWindow *>(o)) {
802 if (KDDockWidgets::usesNativeTitleBar() || fw->isInDragArea(Qt5Qt6Compat::eventGlobalPos(me))) {
803 m_nonClientDrag = true;
804 return activeState()->handleMouseButtonPress(draggableForQObject(o), Qt5Qt6Compat::eventGlobalPos(me), me->pos());
805 }
806 }
807 return MinimalStateMachine::eventFilter(o, e);
808 }
810 // For top-level windows that support native dragging all goes through the NonClient* events.
811 // This also forbids dragging a FloatingWindow simply by pressing outside of the title area, in the background
812 if (!KDDockWidgets::usesNativeDraggingAndResizing() || !w->isWindow()) {
813 Q_ASSERT(activeState());
814 return activeState()->handleMouseButtonPress(draggableForQObject(o), Qt5Qt6Compat::eventGlobalPos(me), me->pos());
815 } else
816 break;
819 return activeState()->handleMouseButtonRelease(Qt5Qt6Compat::eventGlobalPos(me));
822 return activeState()->handleMouseMove(Qt5Qt6Compat::eventGlobalPos(me));
825 return activeState()->handleMouseDoubleClick();
826 default:
827 break;
828 }
829
830 return MinimalStateMachine::eventFilter(o, e);
831}
832
833StateBase *DragController::activeState() const
834{
835 return static_cast<StateBase *>(currentState());
836}
837
838#if defined(Q_OS_WIN)
839static QWidgetOrQuick *qtTopLevelForHWND(HWND hwnd)
840{
841 const QList<QWindow *> windows = qApp->topLevelWindows();
842 for (QWindow *window : windows) {
843 if (!window->isVisible())
844 continue;
845
846 if (hwnd == ( HWND )window->winId()) {
847 if (auto result = DockRegistry::self()->topLevelForHandle(window))
848 return result;
849#ifdef KDDOCKWIDGETS_QTWIDGETS
850 // It's not a KDDW window, but we still return something, as the KDDW main window
851 // might be embedded into another non-kddw QMainWindow
852 // Case not supported for QtQuick.
853 const QWidgetList widgets = qApp->topLevelWidgets();
854 for (QWidget *widget : widgets) {
855 if (!widget->windowHandle()) {
856 // Don't call winId on windows that don't have it, as that will force all its children to have it,
857 // and that's not very stable. a top level might not have one because it's being destroyed, or because
858 // it's a top-level just because it has't been reparented I guess.
859 continue;
860 }
861 if (hwnd == ( HWND )widget->winId()) {
862 return widget;
863 }
864 }
865#endif
866 }
867 }
868
869 qCDebug(toplevels) << Q_FUNC_INFO << "Couldn't find hwnd for top-level" << hwnd;
870 return nullptr;
871}
872
873static QRect topLevelGeometry(const QWidgetOrQuick *topLevel)
874{
875 if (auto mainWindow = qobject_cast<const MainWindowBase *>(topLevel))
876 return mainWindow->windowGeometry();
877
878 return topLevel->geometry();
879}
880
881#endif
882
883template<typename T>
884static WidgetType *qtTopLevelUnderCursor_impl(QPoint globalPos, const QVector<QWindow *> &windows, T windowBeingDragged)
885{
886 for (auto i = windows.size() - 1; i >= 0; --i) {
887 QWindow *window = windows.at(i);
888 auto tl = KDDockWidgets::Private::widgetForWindow(window);
889
890 if (!tl->isVisible() || tl == windowBeingDragged || KDDockWidgets::Private::isMinimized(tl))
891 continue;
892
893 if (windowBeingDragged && KDDockWidgets::Private::windowForWidget(windowBeingDragged) == KDDockWidgets::Private::windowForWidget(tl))
894 continue;
895
896 if (window->geometry().contains(globalPos)) {
897 qCDebug(toplevels) << Q_FUNC_INFO << "Found top-level" << tl;
898 return tl;
899 }
900 }
901
902 return nullptr;
903}
904
905WidgetType *DragController::qtTopLevelUnderCursor() const
906{
907 QPoint globalPos = QCursor::pos();
908
909 if (qApp->platformName() == QLatin1String("windows")) { // So -platform offscreen on Windows doesn't use this
910#if defined(Q_OS_WIN)
911 POINT globalNativePos;
912 if (!GetCursorPos(&globalNativePos))
913 return nullptr;
914
915 // There might be windows that don't belong to our app in between, so use win32 to travel by z-order.
916 // Another solution is to set a parent on all top-levels. But this code is orthogonal.
917 HWND hwnd = HWND(m_windowBeingDragged->floatingWindow()->winId());
918 while (hwnd) {
919 hwnd = GetWindow(hwnd, GW_HWNDNEXT);
920 RECT r;
921 if (!GetWindowRect(hwnd, &r) || !IsWindowVisible(hwnd))
922 continue;
923
924 if (!PtInRect(&r, globalNativePos)) // Check if window is under cursor
925 continue;
926
927 if (auto tl = qtTopLevelForHWND(hwnd)) {
928 const QRect windowGeometry = topLevelGeometry(tl);
929
930 if (windowGeometry.contains(globalPos) && tl->objectName() != QStringLiteral("_docks_IndicatorWindow_Overlay")) {
931 qCDebug(toplevels) << Q_FUNC_INFO << "Found top-level" << tl;
932 return tl;
933 }
934 } else {
935#ifdef KDDOCKWIDGETS_QTWIDGETS // Maybe it's embedded in a QWinWidget:
936 auto topLevels = qApp->topLevelWidgets();
937 for (auto topLevel : topLevels) {
938 if (QLatin1String(topLevel->metaObject()->className()) == QLatin1String("QWinWidget")) {
939 if (hwnd == GetParent(HWND(topLevel->windowHandle()->winId()))) {
940 if (topLevel->rect().contains(topLevel->mapFromGlobal(globalPos)) && topLevel->objectName() != QStringLiteral("_docks_IndicatorWindow_Overlay")) {
941 qCDebug(toplevels) << Q_FUNC_INFO << "Found top-level" << topLevel;
942 return topLevel;
943 }
944 }
945 }
946 }
947#endif // QtWidgets A window belonging to another app is below the cursor
948 qCDebug(toplevels) << Q_FUNC_INFO << "Window from another app is under cursor" << hwnd;
949 return nullptr;
950 }
951 }
952#endif // Q_OS_WIN
953 } else if (linksToXLib() && isXCB()) {
954 bool ok = false;
955 const QVector<QWindow *> orderedWindows = KDDockWidgets::orderedWindows(ok);
956 FloatingWindow *tlwBeingDragged = m_windowBeingDragged->floatingWindow();
957 if (auto tl = qtTopLevelUnderCursor_impl(globalPos, orderedWindows, tlwBeingDragged))
958 return tl;
959
960 if (!ok) {
961 qCDebug(toplevels) << Q_FUNC_INFO << "No top-level found. Some windows weren't seen by XLib";
962 }
963
964 } else {
965 // !Windows: Linux, macOS, offscreen (offscreen on Windows too), etc.
966
967 // On Linux we don't have API to check the z-order of top-levels. So first check the floating windows
968 // and check the MainWindow last, as the MainWindow will have lower z-order as it's a parent (TODO: How will it work with multiple MainWindows ?)
969 // The floating window list is sorted by z-order, as we catch QEvent::Expose and move it to last of the list
970
971 FloatingWindow *tlwBeingDragged = m_windowBeingDragged->floatingWindow();
972 if (auto tl = qtTopLevelUnderCursor_impl(globalPos, DockRegistry::self()->floatingQWindows(), tlwBeingDragged))
973 return tl;
974
975 return qtTopLevelUnderCursor_impl<WidgetType *>(globalPos,
976 DockRegistry::self()->topLevels(/*excludeFloating=*/true),
977 tlwBeingDragged);
978 }
979
980 qCDebug(toplevels) << Q_FUNC_INFO << "No top-level found";
981 return nullptr;
982}
983
984static DropArea *deepestDropAreaInTopLevel(WidgetType *topLevel, QPoint globalPos,
985 const QStringList &affinities)
986{
987 const auto localPos = topLevel->mapFromGlobal(globalPos);
988 auto w = topLevel->childAt(localPos.x(), localPos.y());
989 while (w) {
990 if (auto dt = qobject_cast<DropArea *>(w)) {
991 if (DockRegistry::self()->affinitiesMatch(dt->affinities(), affinities))
992 return dt;
993 }
994 w = KDDockWidgets::Private::parentWidget(w);
995 }
996
997 return nullptr;
998}
999
1000DropArea *DragController::dropAreaUnderCursor() const
1001{
1002 WidgetType *topLevel = qtTopLevelUnderCursor();
1003 if (!topLevel) {
1004 qCDebug(state) << Q_FUNC_INFO << "No drop area under cursor";
1005 return nullptr;
1006 }
1007
1008 const QStringList affinities = m_windowBeingDragged->floatingWindow()->affinities();
1009
1010 if (auto fw = qobject_cast<FloatingWindow *>(topLevel)) {
1011 if (DockRegistry::self()->affinitiesMatch(fw->affinities(), affinities)) {
1012 qCDebug(state) << Q_FUNC_INFO << "Found drop area in floating window";
1013 return fw->dropArea();
1014 }
1015 }
1016
1017 if (topLevel->objectName() == QStringLiteral("_docks_IndicatorWindow")) {
1018 qWarning() << "Indicator window should be hidden " << topLevel << topLevel->isVisible();
1019 Q_ASSERT(false);
1020 }
1021
1022 if (auto dt = deepestDropAreaInTopLevel(topLevel, QCursor::pos(), affinities)) {
1023 qCDebug(state) << Q_FUNC_INFO << "Found drop area" << dt << dt->window();
1024 return dt;
1025 }
1026
1027 qCDebug(state) << "DragController::dropAreaUnderCursor: null2";
1028 return nullptr;
1029}
1030
1031Draggable *DragController::draggableForQObject(QObject *o) const
1032{
1033 for (auto draggable : m_draggables)
1034 if (draggable->asWidget() == o) {
1035 return draggable;
1036 }
1037
1038 return nullptr;
1039}
Application-wide config to tune certain behaviours of the framework.
static WidgetType * qtTopLevelUnderCursor_impl(QPoint globalPos, const QVector< QWindow * > &windows, T windowBeingDragged)
static DropArea * deepestDropAreaInTopLevel(WidgetType *topLevel, QPoint globalPos, const QStringList &affinities)
int mdiPopupThreshold() const
Definition Config.cpp:341
static Config & self()
returns the singleton Config instance
Definition Config.cpp:84
The DockWidget base-class. DockWidget and DockWidgetBase are only split in two so we can share some c...
bool isMinimized(QWindow *window)
QPoint pos()
const QMimeData * mimeData() const const
void setDropAction(Qt::DropAction action)
void accept()
QEvent::Type type() const const
const char * className() const const
QPoint pos() const const
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
void installEventFilter(QObject *filterObj)
virtual const QMetaObject * metaObject() const const
QObject * parent() const const
T qobject_cast(QObject *object)
void setX(int x)
void setY(int y)
int x() const const
int y() const const
bool contains(const QRect &rectangle, bool proper) const const
int width() const const
int x() const const
int height() const const
int width() const const
DropAction
WindowMaximized
void timeout()
const T & at(int i) const const
int size() const const
QWidget * childAt(int x, int y) const const
void grabMouse()
QPoint mapFromGlobal(const QPoint &pos) const const
void releaseMouse()
bool isVisible() const const
QWindow * windowHandle() const const
QRect geometry() const const
void setPosition(const QPoint &pt)
bool startSystemMove()
bool isVisible() const const
WId winId() const const

© 2019-2023 Klarälvdalens Datakonsult AB (KDAB)
"The Qt, C++ and OpenGL Experts"
https://www.kdab.com/
KDDockWidgets
Advanced Dock Widget Framework for Qt
https://www.kdab.com/development-resources/qt-tools/kddockwidgets/
Generated on Wed Nov 1 2023 00:02:31 for KDDockWidgets API Documentation by doxygen 1.9.8