KDDockWidgets API Documentation 2.0
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 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 "Logging_p.h"
14#include "Utils_p.h"
15#include "WidgetResizeHandler_p.h"
16#include "Config.h"
17#include "WindowZOrder_x11_p.h"
18
19#include "core/DockRegistry.h"
20#include "core/Window_p.h"
21#include "core/MDILayout.h"
22#include "core/DropArea.h"
23#include "core/TitleBar.h"
24#include "core/Platform.h"
25#include "core/Group.h"
26#include "core/FloatingWindow.h"
27#include "core/DockWidget_p.h"
28#include "core/ScopedValueRollback_p.h"
29
30#ifdef KDDW_FRONTEND_QT
31#include "../qtcommon/DragControllerWayland_p.h"
32#ifdef KDDW_FRONTEND_QTWIDGETS
33#include "kddockwidgets/qtcommon/Platform.h"
34#include <QWidget>
35#include <QApplication>
36#endif
37#endif
38
39#include <algorithm>
40#include <cstdlib>
41
42#if defined(Q_OS_WIN)
43#include <windows.h>
44#endif
45
46using namespace KDDockWidgets;
47using namespace KDDockWidgets::Core;
48
49namespace KDDockWidgets::Core {
51class FallbackMouseGrabber : public Core::Object,
53{
54public:
55 explicit FallbackMouseGrabber(Core::Object *parent)
56 : Core::Object(parent)
57 {
58 }
59
60 ~FallbackMouseGrabber() override;
61
62 void grabMouse(View *target)
63 {
64 m_target = target;
65 m_guard = target;
67 }
68
69 void releaseMouse()
70 {
71 // Ungrab harder if QtQuick.
72 // QtQuick has the habit of grabbing the MouseArea internally, then doesn't ungrab it since
73 // we're consuming the events. So explicitly ungrab if any QQuickWindow::mouseGrabberItem()
74 // is still set. Done via platform now, so it's generic. Should be a no-op for QtWidgets.
76
77 m_target = nullptr;
78 m_guard.clear();
80 }
81
82 bool onMouseEvent(View *, MouseEvent *me) override
83 {
84 if (m_reentrancyGuard || !m_guard)
85 return false;
86
87 m_reentrancyGuard = true;
88 Platform::instance()->sendEvent(m_target, me);
89 m_reentrancyGuard = false;
90 return true;
91 }
92
93 bool m_reentrancyGuard = false;
94 View *m_target = nullptr;
95 ViewGuard m_guard = nullptr;
96};
97
98FallbackMouseGrabber::~FallbackMouseGrabber()
99{
100}
101
102}
103
104State::State(MinimalStateMachine *parent)
105 : Core::Object(parent)
106 , m_machine(parent)
107{
108}
109
110State::~State() = default;
111
112bool State::isCurrentState() const
113{
114 return m_machine->currentState() == this;
115}
116
117MinimalStateMachine::MinimalStateMachine(Core::Object *parent)
118 : Core::Object(parent)
119{
120}
121
122template<typename Signal>
123void State::addTransition(Signal &signal, State *dest)
124{
125 signal.connect([this, dest] {
126 if (isCurrentState()) {
127 m_machine->setCurrentState(dest);
128 }
129 });
130}
131
132State *MinimalStateMachine::currentState() const
133{
134 return m_currentState;
135}
136
137void MinimalStateMachine::setCurrentState(State *state)
138{
139 if (state != m_currentState) {
140 if (m_currentState)
141 m_currentState->onExit();
142
143 m_currentState = state;
144
145 if (state)
146 state->onEntry();
147
148 currentStateChanged.emit();
149 }
150}
151
152StateBase::StateBase(DragController *parent)
153 : State(parent)
154 , q(parent)
155{
156}
157
158bool StateBase::isActiveState() const
159{
160 return q->activeState() == this;
161}
162
163StateBase::~StateBase() = default;
164
165StateNone::StateNone(DragController *parent)
166 : StateBase(parent)
167{
168}
169
170void StateNone::onEntry()
171{
172 KDDW_DEBUG("StateNone entered");
173 q->m_pressPos = Point();
174 q->m_offset = Point();
175 q->m_draggable = nullptr;
176 q->m_draggableGuard.clear();
177 q->m_windowBeingDragged.reset();
178
179 WidgetResizeHandler::s_disableAllHandlers = false; // Re-enable resize handlers
180
181 q->m_nonClientDrag = false;
182 q->m_inProgrammaticDrag = false;
183
184 if (q->m_currentDropArea) {
185 q->m_currentDropArea->removeHover();
186 q->m_currentDropArea = nullptr;
187 }
188
191 q->isDraggingChanged.emit();
192}
193
194// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
195bool StateNone::handleMouseButtonPress(Draggable *draggable, Point globalPos, Point pos)
196{
197 KDDW_DEBUG("StateNone::handleMouseButtonPress: draggable={} ; globalPos={}", ( void * )draggable,
198 globalPos);
199
200 if (!draggable) {
201 KDDW_ERROR("StateNone::handleMouseButtonPress: null draggable");
202 return false;
203 }
204
205 if (!q->m_inProgrammaticDrag && !draggable->isPositionDraggable(pos))
206 return false;
207
208 q->m_draggable = draggable;
209 q->m_draggableGuard = draggable->asView();
210 q->m_pressPos = globalPos;
211 q->m_offset = draggable->mapToWindow(pos);
212 q->mousePressed.emit();
213
214 return false;
215}
216
217StateNone::~StateNone() = default;
218
219
220StatePreDrag::StatePreDrag(DragController *parent)
221 : StateBase(parent)
222{
223}
224
225StatePreDrag::~StatePreDrag() = default;
226
227void StatePreDrag::onEntry()
228{
229 KDDW_DEBUG("StatePreDrag entered {}", q->m_draggableGuard.isNull());
230 WidgetResizeHandler::s_disableAllHandlers = true; // Disable the resize handler during dragging
231}
232
233bool StatePreDrag::handleMouseMove(Point globalPos)
234{
235 if (!q->m_draggableGuard) {
236 KDDW_ERROR("Draggable was destroyed, canceling the drag");
237 q->dragCanceled.emit();
238 return false;
239 }
240
241 if (!q->m_draggable->dragCanStart(q->m_pressPos, globalPos))
242 return false;
243
244 if (auto func = Config::self().dragAboutToStartFunc()) {
245 if (!func(q->m_draggable))
246 return false;
247 }
248
249 if (q->m_draggable->isMDI())
250 q->manhattanLengthMoveMDI.emit();
251 else
252 q->manhattanLengthMove.emit();
253
254 return true;
255}
256
257bool StatePreDrag::handleMouseButtonRelease(Point)
258{
259 q->dragCanceled.emit();
260 return false;
261}
262
263bool StatePreDrag::handleMouseDoubleClick()
264{
265 // This is only needed for QtQuick.
266 // With QtQuick, when double clicking, we get: Press, Release, Press, Double-click. and never
267 // receive the last Release event.
268 q->dragCanceled.emit();
269 return false;
270}
271
272StateDragging::StateDragging(DragController *parent)
273 : StateBase(parent)
274{
275#if defined(KDDW_FRONTEND_QT_WINDOWS) && !defined(DOCKS_DEVELOPER_MODE)
276 m_maybeCancelDrag.setInterval(100);
277 QObject::connect(&m_maybeCancelDrag, &QTimer::timeout, this, [this] {
278 // Workaround bug #166 , where Qt doesn't agree with Window's mouse button state.
279 // Looking in the Qt bug tracker there's many hits, so do a quick workaround here:
280
281 const bool mouseButtonIsReallyDown = (GetKeyState(VK_LBUTTON) & 0x8000);
282 if (!mouseButtonIsReallyDown && Platform::instance()->isLeftMouseButtonPressed()) {
283 KDDW_DEBUG("Canceling drag, Qt thinks mouse button is pressed"
284 "but Windows knows it's not");
285 handleMouseButtonRelease(Platform::instance()->cursorPos());
286 q->dragCanceled.emit();
287 }
288 });
289#endif
290}
291
292StateDragging::~StateDragging() = default;
293
294void StateDragging::onEntry()
295{
296#if defined(KDDW_FRONTEND_QT_WINDOWS) && !defined(DOCKS_DEVELOPER_MODE)
297 m_maybeCancelDrag.start();
298#endif
299
300 if (!q->m_draggableGuard) {
301 KDDW_ERROR("Draggable was destroyed, canceling the drag");
302 q->dragCanceled.emit();
303 return;
304 }
305
306 if (DockWidget *dw = q->m_draggable->singleDockWidget()) {
307 // When we start to drag a floating window which has a single dock widget, we save the
308 // position
309 if (dw->isFloating())
310 dw->d->saveLastFloatingGeometry();
311 }
312
313 const bool needsUndocking = !q->m_draggable->isWindow();
314 q->m_windowBeingDragged = q->m_draggable->makeWindow();
315 if (q->m_windowBeingDragged) {
316#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) && defined(KDDW_FRONTEND_QT_WINDOWS)
317 if (!q->m_nonClientDrag && KDDockWidgets::usesNativeDraggingAndResizing()) {
318 // Started as a client move, as the dock widget was docked,
319 // but now that we're dragging it as a floating window, switch to native drag, so we can
320 // still get aero-snap
321 FloatingWindow *fw = q->m_windowBeingDragged->floatingWindow();
322 q->m_nonClientDrag = true;
323 q->m_windowBeingDragged.reset();
324 q->m_windowBeingDragged = fw->makeWindow();
325
326 Window::Ptr window = fw->view()->window();
327
328 if (needsUndocking) {
329 // Position the window before the drag start, otherwise if you move mouse too fast
330 // there will be an offset Only required when we've undocked/detached a window.
331 const Point cursorPos = Platform::instance()->cursorPos();
332 window->setPosition(cursorPos - q->m_offset);
333 }
334
335 // Start the native move
336 window->startSystemMove();
337 }
338#else
339 KDDW_UNUSED(needsUndocking);
340#endif
341
342 KDDW_DEBUG("StateDragging entered. m_draggable={}; m_windowBeingDragged={}", ( void * )q->m_draggable, ( void * )q->m_windowBeingDragged->floatingWindow());
343
344 auto fw = q->m_windowBeingDragged->floatingWindow();
345#ifdef Q_OS_LINUX
346 if (fw->view()->isMaximized()) {
347 // When dragging a maximized window on linux we need to restore its normal size
348 // On Windows this works already. On macOS I don't see this feature at all
349
350 const Rect normalGeometry = fw->view()->normalGeometry();
351
352 // distance to the left edge of the window:
353 const int leftOffset = q->m_offset.x();
354
355 // distance to the right edge of the window:
356 const int rightOffset = fw->width() - q->m_offset.x();
357
358 const bool leftEdgeIsNearest = leftOffset <= rightOffset;
359
360 fw->view()->showNormal();
361
362 if (!normalGeometry.contains(q->m_pressPos)) {
363 if ((leftEdgeIsNearest && leftOffset > normalGeometry.width())
364 || (!leftEdgeIsNearest && rightOffset > normalGeometry.width())) {
365 // Case #1: The window isn't under the cursor anymore
366 // Let's just put its middle under the cursor
367 q->m_offset.setX(normalGeometry.width() / 2);
368 } else if (!leftEdgeIsNearest) {
369 // Case #2: The new geometry is still under the cursor, but instead of moving
370 // its right edge left we'll move the left edge right, since initially the press
371 // position was closer to the right edge
372 q->m_offset.setX(normalGeometry.width() - rightOffset);
373 }
374 }
375 } else
376#endif
377
378 if (!fw->geometry().contains(q->m_pressPos)) {
379 // The window shrunk when the drag started, this can happen if it has max-size
380 // constraints we make the floating window smaller. Has the downside that it might not
381 // be under the mouse cursor anymore, so make the change
382 if (fw->width() < q->m_offset.x()) { // make sure it shrunk
383 q->m_offset.setX(fw->width() / 2);
384 }
385 }
386 } else {
387 // Shouldn't happen
388 KDDW_ERROR("No window being dragged for {} {}", ( void * )q->m_draggable, ( void * )q->m_draggable->asController());
389 q->dragCanceled.emit();
390 }
391
392 q->isDraggingChanged.emit();
393}
394
395void StateDragging::onExit()
396{
397#if defined(KDDW_FRONTEND_QT_WINDOWS) && !defined(DOCKS_DEVELOPER_MODE)
398 m_maybeCancelDrag.stop();
399#endif
400
401 if (auto callback = Config::self().dragEndedFunc()) {
402 // this user is interested in knowing the drag ended
403 callback();
404 }
405}
406
407bool StateDragging::handleMouseButtonRelease(Point globalPos)
408{
409 KDDW_DEBUG("StateDragging: handleMouseButtonRelease");
410
411 FloatingWindow *floatingWindow = q->m_windowBeingDragged->floatingWindow();
412 if (!floatingWindow) {
413 // It was deleted externally
414 KDDW_DEBUG("StateDragging: Bailling out, deleted externally");
415 q->dragCanceled.emit();
416 return true;
417 }
418
419 if (floatingWindow->anyNonDockable()) {
420 KDDW_DEBUG("StateDragging: Ignoring floating window with non dockable widgets");
421 q->dragCanceled.emit();
422 return true;
423 }
424
425 if (q->m_currentDropArea) {
426 if (q->m_currentDropArea->drop(q->m_windowBeingDragged.get(), globalPos)) {
427 q->dropped.emit();
428 } else {
429 KDDW_DEBUG("StateDragging: Bailling out, drop not accepted");
430 q->dragCanceled.emit();
431 }
432 } else {
433 KDDW_DEBUG("StateDragging: Bailling out, not over a drop area");
434 q->dragCanceled.emit();
435 }
436 return true;
437}
438
439bool StateDragging::handleMouseMove(Point globalPos)
440{
441 FloatingWindow *fw = q->m_windowBeingDragged->floatingWindow();
442 if (!fw) {
443 KDDW_DEBUG("Canceling drag, window was deleted");
444 q->dragCanceled.emit();
445 return true;
446 }
447
448 if (fw->beingDeleted()) {
449 // Ignore, we're in the middle of recurrency. We're inside
450 // StateDragging::handleMouseButtonRelease too
451 return true;
452 }
453
454#ifdef Q_OS_LINUX
455 if (fw->lastWindowManagerState() == WindowState::Maximized) {
456 // The window was maximized, we dragged it, which triggers a show normal.
457 // But we can only start moving the window *after* the (async) window manager acknowledges.
458 // See QTBUG-102430.
459 // Since #286 was only implemented and needed on Linux, then this counter-part is also
460 // ifdefed for Linux, Probably the ifdef could be removed, but don't want to be testing N
461 // platforms, who's undocumented behaviour can change between releases, so narrow the scope
462 // and workaround for linux only.
463 return true;
464 }
465#endif
466
467 if (!q->m_nonClientDrag)
468 fw->view()->window()->setFramePosition(globalPos - q->m_offset);
469
470 if (fw->anyNonDockable()) {
471 KDDW_DEBUG("StateDragging: Ignoring non dockable floating window");
472 return true;
473 }
474
475 DropArea *dropArea = q->dropAreaUnderCursor();
476 if (q->m_currentDropArea && dropArea != q->m_currentDropArea)
477 q->m_currentDropArea->removeHover();
478
479 if (dropArea) {
480 if (FloatingWindow *targetFw = dropArea->floatingWindow()) {
481 if (targetFw->anyNonDockable()) {
482 KDDW_DEBUG("StateDragging: Ignoring non dockable target floating window");
483 return false;
484 }
485 }
486
487 dropArea->hover(q->m_windowBeingDragged.get(), globalPos);
488 }
489
490 q->m_currentDropArea = dropArea;
491
492 return true;
493}
494
495bool StateDragging::handleMouseDoubleClick()
496{
497 // See comment in StatePreDrag::handleMouseDoubleClick().
498 // Very unlikely that we're in this state though, due to manhattan length
499 q->dragCanceled.emit();
500 return false;
501}
502
503StateInternalMDIDragging::StateInternalMDIDragging(DragController *parent)
504 : StateBase(parent)
505{
506}
507
508StateInternalMDIDragging::~StateInternalMDIDragging()
509{
510}
511
512void StateInternalMDIDragging::onEntry()
513{
514 KDDW_DEBUG("StateInternalMDIDragging entered. draggable={}", ( void * )q->m_draggable);
515
516 if (!q->m_draggableGuard) {
517 KDDW_ERROR("Draggable was destroyed, canceling the drag");
518 q->dragCanceled.emit();
519 return;
520 }
521
522 // Raise the dock widget being dragged
523 if (auto tb = q->m_draggable->asView()->asTitleBarController()) {
524 if (Group *f = tb->group())
525 f->view()->raise();
526 }
527
528 q->isDraggingChanged.emit();
529}
530
531bool StateInternalMDIDragging::handleMouseButtonRelease(Point)
532{
533 q->dragCanceled.emit();
534 return false;
535}
536
537bool StateInternalMDIDragging::handleMouseMove(Point globalPos)
538{
539 if (!q->m_draggableGuard) {
540 KDDW_ERROR("Draggable was destroyed, canceling the drag");
541 q->dragCanceled.emit();
542 return false;
543 }
544
545 // for MDI we only support dragging via the title bar, other cases don't make sense conceptually
546 auto tb = q->m_draggable->asView()->asTitleBarController();
547 if (!tb) {
548 KDDW_ERROR("expected a title bar, not {}", ( void * )q->m_draggable);
549 q->dragCanceled.emit();
550 return false;
551 }
552
553 Group *group = tb->group();
554 if (!group) {
555 // Doesn't happen.
556 KDDW_ERROR("null group.");
557 q->dragCanceled.emit();
558 return false;
559 }
560
561 const Size parentSize = group->view()->d->parentSize();
562 const Point oldPos = group->mapToGlobal(Point(0, 0));
563 const Point delta = globalPos - oldPos;
564 const Point newLocalPos = group->pos() + delta - q->m_offset;
565
566 // Let's not allow the MDI window to go outside of its parent
567
568 Point newLocalPosBounded = { std::max(0, newLocalPos.x()), std::max(0, newLocalPos.y()) };
569 newLocalPosBounded.setX(std::min(newLocalPosBounded.x(), parentSize.width() - group->width()));
570 newLocalPosBounded.setY(std::min(newLocalPosBounded.y(), parentSize.height() - group->height()));
571
572 auto layout = group->mdiLayout();
573 assert(layout);
574 layout->moveDockWidget(group, newLocalPosBounded);
575
576 // Check if we need to pop out the MDI window (make it float)
577 // If we drag the window against an edge, and move behind the edge some threshold, we float it
578 const int threshold = Config::self().mdiPopupThreshold();
579 if (threshold != -1) {
580 const Point overflow = newLocalPosBounded - newLocalPos;
581 if (std::abs(overflow.x()) > threshold || std::abs(overflow.y()) > threshold)
582 q->mdiPopOut.emit();
583 }
584
585 return false;
586}
587
588bool StateInternalMDIDragging::handleMouseDoubleClick()
589{
590 q->dragCanceled.emit();
591 return false;
592}
593
594namespace {
595
596StateDragging *createDraggingState(DragController *parent)
597{
598#ifdef KDDW_FRONTEND_QT
599 return isWayland() ? new StateDraggingWayland(parent) : new StateDragging(parent);
600#else
601 return new StateDragging(parent);
602#endif
603}
604
605}
606
607DragController::DragController(Core::Object *parent)
608 : MinimalStateMachine(parent)
609 , m_stateNone(new StateNone(this))
610 , m_statePreDrag(new StatePreDrag(this))
611 , m_stateDragging(createDraggingState(this))
612 , m_stateDraggingMDI(new StateInternalMDIDragging(this))
613{
614 KDDW_TRACE("DragController CTOR");
615
616 m_stateNone->addTransition(mousePressed, m_statePreDrag);
617 m_statePreDrag->addTransition(dragCanceled, m_stateNone);
618 m_statePreDrag->addTransition(manhattanLengthMove, m_stateDragging);
619 m_statePreDrag->addTransition(manhattanLengthMoveMDI, m_stateDraggingMDI);
620 m_stateDragging->addTransition(dragCanceled, m_stateNone);
621 m_stateDragging->addTransition(dropped, m_stateNone);
622
623 m_stateDraggingMDI->addTransition(dragCanceled, m_stateNone);
624 m_stateDraggingMDI->addTransition(mdiPopOut, m_stateDragging);
625
626 if (Platform::instance()->usesFallbackMouseGrabber())
627 enableFallbackMouseGrabber();
628
629 setCurrentState(m_stateNone);
630}
631
632DragController *DragController::instance()
633{
634 static DragController dragController;
635 return &dragController;
636}
637
638void DragController::registerDraggable(Draggable *drg)
639{
640 m_draggables.push_back(drg);
641 drg->asView()->installViewEventFilter(this);
642}
643
644void DragController::unregisterDraggable(Draggable *drg)
645{
646 m_draggables.removeOne(drg);
647 drg->asView()->removeViewEventFilter(this);
648}
649
650bool DragController::isDragging() const
651{
652 return m_windowBeingDragged != nullptr || activeState() == m_stateDraggingMDI;
653}
654
655bool DragController::isInNonClientDrag() const
656{
657 return isDragging() && m_nonClientDrag;
658}
659
660bool DragController::isInClientDrag() const
661{
662 return isDragging() && !m_nonClientDrag;
663}
664
665bool DragController::isInProgrammaticDrag() const
666{
667 return m_inProgrammaticDrag;
668}
669
670bool DragController::isIdle() const
671{
672 return activeState() == m_stateNone;
673}
674
675void DragController::grabMouseFor(View *target)
676{
677 if (isWayland())
678 return; // No grabbing supported on wayland
679
680 if (m_fallbackMouseGrabber) {
681 m_fallbackMouseGrabber->grabMouse(target);
682 } else {
683 target->grabMouse();
684 }
685}
686
687void DragController::releaseMouse(View *target)
688{
689 if (isWayland())
690 return; // No grabbing supported on wayland
691
692 if (m_fallbackMouseGrabber) {
693 m_fallbackMouseGrabber->releaseMouse();
694 } else {
695 target->releaseMouse();
696 }
697}
698
699FloatingWindow *DragController::floatingWindowBeingDragged() const
700{
701 return m_windowBeingDragged ? m_windowBeingDragged->floatingWindow() : nullptr;
702}
703
704void DragController::enableFallbackMouseGrabber()
705{
706 if (!m_fallbackMouseGrabber)
707 m_fallbackMouseGrabber = new FallbackMouseGrabber(this);
708}
709
710WindowBeingDragged *DragController::windowBeingDragged() const
711{
712 return m_windowBeingDragged.get();
713}
714
715bool DragController::onDnDEvent(View *view, Event *e)
716{
717 if (!isWayland())
718 return false;
719
720 // Wayland is very different. It uses QDrag for the dragging of a window.
721 if (view) {
722 KDDW_DEBUG("DragController::onDnDEvent: ev={}, dropArea=", int(e->type()), ( void * )view->asDropAreaController());
723 if (auto dropArea = view->asDropAreaController()) {
724 switch (int(e->type())) {
725 case Event::DragEnter:
726 if (activeState()->handleDragEnter(static_cast<DragMoveEvent *>(e), dropArea))
727 return true;
728 break;
729 case Event::DragLeave:
730 if (activeState()->handleDragLeave(dropArea))
731 return true;
732 break;
733 case Event::DragMove:
734 if (activeState()->handleDragMove(static_cast<DragMoveEvent *>(e), dropArea))
735 return true;
736 break;
737 case Event::Drop:
738 if (activeState()->handleDrop(static_cast<DropEvent *>(e), dropArea))
739 return true;
740 break;
741 }
742 }
743 } else if (e->type() == Event::DragEnter && isDragging()) {
744 // We're dragging a window. Be sure user code doesn't accept DragEnter events.
745 KDDW_DEBUG("DragController::onDnDEvent: Eating DragEnter.");
746 return true;
747 } else {
748 KDDW_DEBUG("DragController::onDnDEvent: No view. ev={}", int(e->type()));
749 }
750
751 return false;
752}
753
754bool DragController::onMoveEvent(View *)
755{
756 if (m_nonClientDrag) {
757 // On Windows, non-client mouse moves are only sent at the end, so we must fake it:
758 KDDW_TRACE("DragController::onMoveEvent");
759 activeState()
760 ->handleMouseMove(Platform::instance()->cursorPos());
761 }
762
763 return false;
764}
765
766bool DragController::onMouseEvent(View *w, MouseEvent *me)
767{
768 if (!w)
769 return false;
770
771 KDDW_TRACE("DragController::onMouseEvent e={} ; nonClientDrag={}", int(me->type()), m_nonClientDrag);
772
773 switch (me->type()) {
774 case Event::NonClientAreaMouseButtonPress: {
775 if (auto fw = w->asFloatingWindowController()) {
776 if (KDDockWidgets::usesNativeTitleBar()
777 || fw->isInDragArea(Qt5Qt6Compat::eventGlobalPos(me))) {
778 m_nonClientDrag = true;
779 return activeState()->handleMouseButtonPress(
780 draggableForView(w), Qt5Qt6Compat::eventGlobalPos(me), me->pos());
781 }
782 }
783 return false;
784 }
785 case Event::MouseButtonPress:
786 // We don't care about the secondary button
787 if (me->buttons() & Qt::RightButton)
788 break;
789
790 // For top-level windows that support native dragging all goes through the NonClient*
791 // events. This also forbids dragging a FloatingWindow simply by pressing outside of the
792 // title area, in the background
793 if (KDDockWidgets::usesNativeDraggingAndResizing() && w->isRootView())
794 break;
795
796 assert(activeState());
797 return activeState()->handleMouseButtonPress(
798 draggableForView(w), Qt5Qt6Compat::eventGlobalPos(me), me->pos());
799
800 case Event::MouseButtonRelease:
801 case Event::NonClientAreaMouseButtonRelease: {
802 ViewGuard guard(w);
803 const bool inProgrammaticDrag = m_inProgrammaticDrag;
804 const bool result = activeState()->handleMouseButtonRelease(Qt5Qt6Compat::eventGlobalPos(me));
805
806 if (!guard) {
807 // Always consume the event if the view was deleted during a DND. For example
808 // tabbing A into B will destroy tabwidget A. Qt would then try to deliver event to A and crash.
809 return true;
810 }
811
812 // In normal KDDW operation, we consume the mouse release (true is returned), however,
813 // if using programmattic drag (via DockWidget::startDragging()), we do not want to consume the release event.
814 // User might have clicked a button to start the drag. Button needs to be released when it's over, otherwise
815 // it will look visually pressed.
816 return result && !inProgrammaticDrag;
817 }
818
819 case Event::NonClientAreaMouseMove:
820 case Event::MouseMove:
821 return activeState()->handleMouseMove(Qt5Qt6Compat::eventGlobalPos(me));
822 case Event::MouseButtonDblClick:
823 case Event::NonClientAreaMouseButtonDblClick:
824 return activeState()->handleMouseDoubleClick();
825 default:
826 break;
827 }
828
829 return false;
830}
831
832StateBase *DragController::activeState() const
833{
834 return static_cast<StateBase *>(currentState());
835}
836
837DropLocation DragController::currentDropLocation() const
838{
839 if (auto dropArea = dropAreaUnderCursor())
840 return dropArea->currentDropLocation();
841
842 return DropLocation_None;
843}
844
845bool DragController::programmaticStartDrag(Draggable *draggable, Point globalPos, Point offset)
846{
847 // Here we manually force state machine states instead of having a 2nd/parallel API.
848 // As sharing 99.99% of the code path gives us some comfort.
849
850 if (!draggable) {
851 KDDW_WARN("DragController::programmaticStartDrag: draggable is null");
852 return false;
853 }
854
855 if (isDragging()) {
856 KDDW_WARN("DragController::programmaticStartDrag: Dragging already ongoing");
857 return false;
858 }
859
860 m_inProgrammaticDrag = true;
861 m_stateNone->handleMouseButtonPress(draggable, globalPos, offset);
862 if (activeState() != m_statePreDrag) {
863 m_inProgrammaticDrag = false;
864 KDDW_WARN("DragController::programmaticStartDrag: Expected to be in pre-drag state");
865 return false;
866 }
867
868 if (auto func = Config::self().dragAboutToStartFunc()) {
869 if (!func(m_draggable))
870 return false;
871 }
872
873 manhattanLengthMove.emit();
874 if (activeState() != m_stateDragging && !isWayland()) { // wayland blocks on a QDrag::exec(). When it reaches here we're already done
875 KDDW_WARN("DragController::programmaticStartDrag: Expected to be in drag state");
876 return false;
877 }
878
879 // Also fake the 1st mouse move, so code that positions the frame gets run
880 m_stateDragging->handleMouseMove(globalPos);
881
882 return true;
883}
884
885void DragController::programmaticStopDrag()
886{
887 dragCanceled.emit();
888}
889
890#if defined(KDDW_FRONTEND_QT_WINDOWS)
891static std::shared_ptr<View> qtTopLevelForHWND(HWND hwnd)
892{
893 const Window::List windows = Platform::instance()->windows();
894 for (Window::Ptr window : windows) {
895 if (!window->isVisible())
896 continue;
897
898 if (hwnd == ( HWND )window->handle()) {
899 if (auto result = window->rootView())
900 return result;
901#ifdef KDDW_FRONTEND_QTWIDGETS
902 if (Platform::instance()->isQtWidgets()) {
903 // It's not a KDDW window, but we still return something, as the KDDW main window
904 // might be embedded into another non-kddw QMainWindow
905 // Case not supported for QtQuick.
906 const QWidgetList widgets = qApp->topLevelWidgets();
907 for (QWidget *widget : widgets) {
908 if (!widget->window()) {
909 // Don't call winId on windows that don't have it, as that will force all
910 // its children to have it, and that's not very stable. a top level might
911 // not have one because it's being destroyed, or because it's a top-level
912 // just because it has't been reparented I guess.
913 continue;
914 }
915 if (hwnd == ( HWND )widget->winId()) {
917 }
918 }
919 }
920#endif
921 }
922 }
923
924 KDDW_TRACE("Couldn't find hwnd for top-level hwnd={}", ( void * )hwnd);
925 return nullptr;
926}
927
928#endif
929
930static std::shared_ptr<View> qtTopLevelUnderCursor_impl(Point globalPos,
931 const Window::List &windows,
932 View *rootViewBeingDragged)
933{
934 for (auto i = windows.size() - 1; i >= 0; --i) {
935 const Window::Ptr &window = windows.at(i);
936 auto tl = window->rootView();
937
938 if (!tl->isVisible() || tl->equals(rootViewBeingDragged) || tl->isMinimized())
939 continue;
940
941 if (rootViewBeingDragged && rootViewBeingDragged->window()->equals(window))
942 continue;
943
944 if (window->geometry().contains(globalPos)) {
945 KDDW_TRACE("Found top-level {}", ( void * )tl.get());
946 return tl;
947 }
948 }
949
950 return nullptr;
951}
952
953std::shared_ptr<View> DragController::qtTopLevelUnderCursor() const
954{
955 Point globalPos = Platform::instance()->cursorPos();
956
957 if (KDDockWidgets::isWindows()) { // So -platform offscreen on Windows doesn't use this
958#if defined(KDDW_FRONTEND_QT_WINDOWS)
959 POINT globalNativePos;
960 if (!GetCursorPos(&globalNativePos))
961 return nullptr;
962
963 // There might be windows that don't belong to our app in between, so use win32 to travel by
964 // z-order. Another solution is to set a parent on all top-levels. But this code is
965 // orthogonal.
966 HWND hwnd = HWND(m_windowBeingDragged->floatingWindow()->view()->window()->handle());
967 while (hwnd) {
968 hwnd = GetWindow(hwnd, GW_HWNDNEXT);
969 RECT r;
970 if (!GetWindowRect(hwnd, &r) || !IsWindowVisible(hwnd))
971 continue;
972
973 if (!PtInRect(&r, globalNativePos)) // Check if window is under cursor
974 continue;
975
976 if (auto tl = qtTopLevelForHWND(hwnd)) {
977 const Rect windowGeometry = tl->d->windowGeometry();
978
979 if (windowGeometry.contains(globalPos)
980 && tl->viewName() != QStringLiteral("_docks_IndicatorWindow_Overlay")) {
981 KDDW_TRACE("Found top-level {}", ( void * )tl.get());
982 return tl;
983 }
984 } else {
985#ifdef KDDW_FRONTEND_QTWIDGETS
986 if (Platform::instance()->isQtWidgets()) {
987 // Maybe it's embedded in a QWinWidget:
988 auto topLevels = qApp->topLevelWidgets();
989 for (auto topLevel : topLevels) {
990 if (QLatin1String(topLevel->metaObject()->className())
991 == QLatin1String("QWinWidget")) {
992 if (hwnd == GetParent(HWND(topLevel->window()->winId()))) {
993 if (topLevel->rect().contains(topLevel->mapFromGlobal(globalPos))
994 && topLevel->objectName()
995 != QStringLiteral("_docks_IndicatorWindow_Overlay")) {
996 KDDW_TRACE("Found top-level {}", ( void * )topLevel);
998 }
999 }
1000 }
1001 }
1002 }
1003#endif // QtWidgets A window belonging to another app is below the cursor
1004 KDDW_TRACE("Window from another app is under cursor {}", ( void * )hwnd);
1005 return nullptr;
1006 }
1007 }
1008#endif // KDDW_FRONTEND_QT_WINDOWS
1009 } else if (linksToXLib() && isXCB()) {
1010 bool ok = false;
1011 const Window::List orderedWindows = KDDockWidgets::orderedWindows(ok);
1012 FloatingWindow *tlwBeingDragged = m_windowBeingDragged->floatingWindow();
1013 if (auto tl =
1014 qtTopLevelUnderCursor_impl(globalPos, orderedWindows, tlwBeingDragged->view()))
1015 return tl;
1016
1017 if (!ok) {
1018 KDDW_TRACE("No top-level found. Some windows weren't seen by XLib");
1019 }
1020 } else {
1021 // !Windows: Linux, macOS, offscreen (offscreen on Windows too), etc.
1022
1023 // On Linux we don't have API to check the z-order of top-levels. So first check the
1024 // floating windows and check the MainWindow last, as the MainWindow will have lower z-order
1025 // as it's a parent (TODO: How will it work with multiple MainWindows ?) The floating window
1026 // list is sorted by z-order, as we catch QEvent::Expose and move it to last of the list
1027
1028 View *tlwBeingDragged = m_windowBeingDragged->floatingWindow()->view();
1029 if (auto tl = qtTopLevelUnderCursor_impl(
1030 globalPos, DockRegistry::self()->floatingQWindows(), tlwBeingDragged))
1031 return tl;
1032
1034 globalPos, DockRegistry::self()->topLevels(/*excludeFloatingDocks=*/true), tlwBeingDragged);
1035 }
1036
1037 KDDW_TRACE("No top-level found");
1038 return nullptr;
1039}
1040
1041static DropArea *deepestDropAreaInTopLevel(std::shared_ptr<View> topLevel, Point globalPos,
1042 const Vector<QString> &affinities)
1043{
1044 const auto localPos = topLevel->mapFromGlobal(globalPos);
1045 auto view = topLevel->childViewAt(localPos);
1046
1047 while (view) {
1048 if (auto dt = view->asDropAreaController()) {
1049 if (DockRegistry::self()->affinitiesMatch(dt->affinities(), affinities))
1050 return dt;
1051 }
1052 view = view->parentView();
1053 }
1054
1055 return nullptr;
1056}
1057
1058DropArea *DragController::dropAreaUnderCursor() const
1059{
1060 if (!m_windowBeingDragged)
1061 return nullptr;
1062
1063 std::shared_ptr<View> topLevel = qtTopLevelUnderCursor();
1064 if (!topLevel) {
1065 KDDW_DEBUG("DragController::dropAreaUnderCursor: No drop area under cursor");
1066 return nullptr;
1067 }
1068
1069 const Vector<QString> affinities = m_windowBeingDragged->floatingWindow()->affinities();
1070
1071 if (auto fw = topLevel->asFloatingWindowController()) {
1072 if (DockRegistry::self()->affinitiesMatch(fw->affinities(), affinities)) {
1073 KDDW_DEBUG("DragController::dropAreaUnderCursor: Found drop area in floating window");
1074 return fw->dropArea();
1075 }
1076 }
1077
1078 if (topLevel->viewName() == QStringLiteral("_docks_IndicatorWindow")) {
1079 KDDW_ERROR("Indicator window should be hidden {} isVisible={}", ( void * )topLevel.get(), topLevel->isVisible());
1080 assert(false);
1081 }
1082
1083 if (auto dt = deepestDropAreaInTopLevel(topLevel, Platform::instance()->cursorPos(), affinities)) {
1084 KDDW_DEBUG("DragController::dropAreaUnderCursor: Found drop area {} {}", ( void * )dt, ( void * )dt->view()->rootView().get());
1085 return dt;
1086 }
1087
1088 KDDW_DEBUG("DragController::dropAreaUnderCursor: null2");
1089 return nullptr;
1090}
1091
1092Draggable *DragController::draggableForView(View *view) const
1093{
1094 for (auto draggable : m_draggables)
1095 if (draggable->asView()->equals(view)) {
1096 return draggable;
1097 }
1098
1099 return nullptr;
1100}
1101
1102bool DragController::isInQDrag() const
1103{
1104 return m_inQDrag;
1105}
Application-wide config to tune certain behaviours of the framework.
static DropArea * deepestDropAreaInTopLevel(std::shared_ptr< View > topLevel, Point globalPos, const Vector< QString > &affinities)
static std::shared_ptr< View > qtTopLevelUnderCursor_impl(Point globalPos, const Window::List &windows, View *rootViewBeingDragged)
#define KDDW_UNUSED(name)
int mdiPopupThreshold() const
Definition Config.cpp:391
static Config & self()
returns the singleton Config instance
Definition Config.cpp:88
View * view() const
Returns the view associated with this controller, if any.
Point mapToGlobal(Point) const
The DockWidget base-class. DockWidget and Core::DockWidget are only split in two so we can share some...
DropLocation currentDropLocation() const
DropLocation hover(WindowBeingDragged *draggedWindow, Point globalPos)
std::unique_ptr< WindowBeingDragged > makeWindow() override
bool beingDeleted() const
Returns whether a deleteLater has already been issued.
bool isInDragArea(Point globalPoint) const
Returns whether globalPoint is inside the title bar (or, when there's no title-bar,...
MDILayout * mdiLayout() const
Returns the MDI layout. Or nullptr if this group isn't in a MDI layout.
Core::FloatingWindow * floatingWindow() const
Definition Layout.cpp:95
virtual Vector< std::shared_ptr< Core::Window > > windows() const =0
Returns all windows.
virtual bool isLeftMouseButtonPressed() const =0
Returns whether the left mouse button is pressed.
static Platform * instance()
Returns the platform singleton.
void removeGlobalEventFilter(EventFilterInterface *)
Removes a global event filter.
virtual void ungrabMouse()=0
Releases the mouse grab, if any.
virtual void sendEvent(View *, Event *) const =0
Sends the specified event to the specified view.
virtual Point cursorPos() const =0
Returns the mouse cursor position in screen coordinates.
void installGlobalEventFilter(EventFilterInterface *)
Installs a global event filter Events will be forwarded to the specified EventFilterInterface.
This class provides a weak reference to a view i.e., it becomes null automatically once a View is des...
Definition ViewGuard.h:27
virtual bool isRootView() const =0
virtual void grabMouse()=0
virtual std::shared_ptr< View > parentView() const =0
Returns the gui element's parent. Like QWidget::parentWidget()
virtual Rect normalGeometry() const =0
Core::FloatingWindow * asFloatingWindowController() const
asFooController() are deprecated. Use asController<T>() instead
virtual bool isMaximized() const =0
virtual std::shared_ptr< Core::Window > window() const =0
Returns the window this view is inside For the Qt frontend, this wraps a QWindow. Like QWidget::windo...
Core::DropArea * asDropAreaController() const
virtual void releaseMouse()=0
virtual void showNormal()=0
virtual std::shared_ptr< View > childViewAt(Point localPos) const =0
static DockRegistry * self()
virtual std::shared_ptr< Core::View > qobjectAsView(QObject *) const =0
Returns the specified QObject casted to View Nullptr if it's not a view.
A MultiSplitter with support for drop indicators when hovering over.
Class to abstract QAction, so code still works with QtQuick and Flutter.
DropLocation
Enum describing the different drop indicator types.
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
RightButton
void timeout()

© 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 by doxygen 1.9.8