12#include "DragController_p.h"
15#include "WidgetResizeHandler_p.h"
17#include "WindowZOrder_x11_p.h"
20#include "core/Window_p.h"
27#include "core/DockWidget_p.h"
28#include "core/ScopedValueRollback_p.h"
30#ifdef KDDW_FRONTEND_QT
31#include "../qtcommon/DragControllerWayland_p.h"
32#ifdef KDDW_FRONTEND_QTWIDGETS
33#include "kddockwidgets/qtcommon/Platform.h"
35#include <QApplication>
51class FallbackMouseGrabber :
public Core::Object,
55 explicit FallbackMouseGrabber(Core::Object *parent)
56 : Core::Object(parent)
60 ~FallbackMouseGrabber()
override;
62 void grabMouse(
View *target)
82 bool onMouseEvent(
View *, MouseEvent *me)
override
84 if (m_reentrancyGuard || !m_guard)
87 m_reentrancyGuard =
true;
89 m_reentrancyGuard =
false;
93 bool m_reentrancyGuard =
false;
94 View *m_target =
nullptr;
98FallbackMouseGrabber::~FallbackMouseGrabber()
104State::State(MinimalStateMachine *parent)
105 : Core::Object(parent)
110State::~State() =
default;
112bool State::isCurrentState()
const
114 return m_machine->currentState() ==
this;
117MinimalStateMachine::MinimalStateMachine(Core::Object *parent)
118 : Core::Object(parent)
122template<
typename Signal>
123void State::addTransition(Signal &signal, State *dest)
125 signal.connect([
this, dest] {
126 if (isCurrentState()) {
127 m_machine->setCurrentState(dest);
132State *MinimalStateMachine::currentState()
const
134 return m_currentState;
137void MinimalStateMachine::setCurrentState(State *state)
139 if (state != m_currentState) {
141 m_currentState->onExit();
143 m_currentState = state;
148 currentStateChanged.emit();
152StateBase::StateBase(DragController *parent)
158bool StateBase::isActiveState()
const
160 return q->activeState() ==
this;
163StateBase::~StateBase() =
default;
165StateNone::StateNone(DragController *parent)
170void StateNone::onEntry()
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();
179 WidgetResizeHandler::s_disableAllHandlers =
false;
181 q->m_nonClientDrag =
false;
182 q->m_inProgrammaticDrag =
false;
184 if (q->m_currentDropArea) {
185 q->m_currentDropArea->removeHover();
186 q->m_currentDropArea =
nullptr;
191 q->isDraggingChanged.emit();
195bool StateNone::handleMouseButtonPress(Draggable *draggable, Point globalPos, Point pos)
197 KDDW_DEBUG(
"StateNone::handleMouseButtonPress: draggable={} ; globalPos={}", (
void * )draggable,
201 KDDW_ERROR(
"StateNone::handleMouseButtonPress: null draggable");
205 if (!q->m_inProgrammaticDrag && !draggable->isPositionDraggable(pos))
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();
217StateNone::~StateNone() =
default;
220StatePreDrag::StatePreDrag(DragController *parent)
225StatePreDrag::~StatePreDrag() =
default;
227void StatePreDrag::onEntry()
229 KDDW_DEBUG(
"StatePreDrag entered {}", q->m_draggableGuard.isNull());
230 WidgetResizeHandler::s_disableAllHandlers =
true;
233bool StatePreDrag::handleMouseMove(Point globalPos)
235 if (!q->m_draggableGuard) {
236 KDDW_ERROR(
"Draggable was destroyed, canceling the drag");
237 q->dragCanceled.emit();
241 if (!q->m_draggable->dragCanStart(q->m_pressPos, globalPos))
244 if (
auto func =
Config::self().dragAboutToStartFunc()) {
245 if (!func(q->m_draggable))
249 if (q->m_draggable->isMDI())
250 q->manhattanLengthMoveMDI.emit();
252 q->manhattanLengthMove.emit();
257bool StatePreDrag::handleMouseButtonRelease(Point)
259 q->dragCanceled.emit();
263bool StatePreDrag::handleMouseDoubleClick()
268 q->dragCanceled.emit();
272StateDragging::StateDragging(DragController *parent)
275#if defined(KDDW_FRONTEND_QT_WINDOWS) && !defined(DOCKS_DEVELOPER_MODE)
276 m_maybeCancelDrag.setInterval(100);
281 const bool mouseButtonIsReallyDown = (GetKeyState(VK_LBUTTON) & 0x8000);
283 KDDW_DEBUG(
"Canceling drag, Qt thinks mouse button is pressed"
284 "but Windows knows it's not");
286 q->dragCanceled.emit();
292StateDragging::~StateDragging() =
default;
294void StateDragging::onEntry()
296#if defined(KDDW_FRONTEND_QT_WINDOWS) && !defined(DOCKS_DEVELOPER_MODE)
297 m_maybeCancelDrag.start();
300 if (!q->m_draggableGuard) {
301 KDDW_ERROR(
"Draggable was destroyed, canceling the drag");
302 q->dragCanceled.emit();
306 if (
DockWidget *dw = q->m_draggable->singleDockWidget()) {
309 if (dw->isFloating())
310 dw->d->saveLastFloatingGeometry();
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()) {
322 q->m_nonClientDrag =
true;
323 q->m_windowBeingDragged.reset();
328 if (needsUndocking) {
332 window->setPosition(cursorPos - q->m_offset);
336 window->startSystemMove();
342 KDDW_DEBUG(
"StateDragging entered. m_draggable={}; m_windowBeingDragged={}", (
void * )q->m_draggable, (
void * )q->m_windowBeingDragged->floatingWindow());
344 auto fw = q->m_windowBeingDragged->floatingWindow();
353 const int leftOffset = q->m_offset.x();
356 const int rightOffset = fw->
width() - q->m_offset.x();
358 const bool leftEdgeIsNearest = leftOffset <= rightOffset;
362 if (!normalGeometry.contains(q->m_pressPos)) {
363 if ((leftEdgeIsNearest && leftOffset > normalGeometry.width())
364 || (!leftEdgeIsNearest && rightOffset > normalGeometry.width())) {
367 q->m_offset.setX(normalGeometry.width() / 2);
368 }
else if (!leftEdgeIsNearest) {
372 q->m_offset.setX(normalGeometry.width() - rightOffset);
378 if (!fw->
geometry().contains(q->m_pressPos)) {
382 if (fw->
width() < q->m_offset.x()) {
383 q->m_offset.setX(fw->
width() / 2);
388 KDDW_ERROR(
"No window being dragged for {} {}", (
void * )q->m_draggable, (
void * )q->m_draggable->asController());
389 q->dragCanceled.emit();
392 q->isDraggingChanged.emit();
395void StateDragging::onExit()
397#if defined(KDDW_FRONTEND_QT_WINDOWS) && !defined(DOCKS_DEVELOPER_MODE)
398 m_maybeCancelDrag.stop();
407bool StateDragging::handleMouseButtonRelease(Point globalPos)
409 KDDW_DEBUG(
"StateDragging: handleMouseButtonRelease");
411 FloatingWindow *floatingWindow = q->m_windowBeingDragged->floatingWindow();
412 if (!floatingWindow) {
414 KDDW_DEBUG(
"StateDragging: Bailling out, deleted externally");
415 q->dragCanceled.emit();
420 KDDW_DEBUG(
"StateDragging: Ignoring floating window with non dockable widgets");
421 q->dragCanceled.emit();
425 if (q->m_currentDropArea) {
426 if (q->m_currentDropArea->drop(q->m_windowBeingDragged.get(), globalPos)) {
429 KDDW_DEBUG(
"StateDragging: Bailling out, drop not accepted");
430 q->dragCanceled.emit();
433 KDDW_DEBUG(
"StateDragging: Bailling out, not over a drop area");
434 q->dragCanceled.emit();
439bool StateDragging::handleMouseMove(Point globalPos)
443 KDDW_DEBUG(
"Canceling drag, window was deleted");
444 q->dragCanceled.emit();
467 if (!q->m_nonClientDrag)
468 fw->
view()->
window()->setFramePosition(globalPos - q->m_offset);
471 KDDW_DEBUG(
"StateDragging: Ignoring non dockable floating window");
475 DropArea *dropArea = q->dropAreaUnderCursor();
476 if (q->m_currentDropArea && dropArea != q->m_currentDropArea)
481 if (targetFw->anyNonDockable()) {
482 KDDW_DEBUG(
"StateDragging: Ignoring non dockable target floating window");
487 dropArea->
hover(q->m_windowBeingDragged.get(), globalPos);
490 q->m_currentDropArea = dropArea;
495bool StateDragging::handleMouseDoubleClick()
499 q->dragCanceled.emit();
503StateInternalMDIDragging::StateInternalMDIDragging(DragController *parent)
508StateInternalMDIDragging::~StateInternalMDIDragging()
512void StateInternalMDIDragging::onEntry()
514 KDDW_DEBUG(
"StateInternalMDIDragging entered. draggable={}", (
void * )q->m_draggable);
516 if (!q->m_draggableGuard) {
517 KDDW_ERROR(
"Draggable was destroyed, canceling the drag");
518 q->dragCanceled.emit();
523 if (
auto tb = q->m_draggable->asView()->asTitleBarController()) {
524 if (
Group *f = tb->group())
528 q->isDraggingChanged.emit();
531bool StateInternalMDIDragging::handleMouseButtonRelease(Point)
533 q->dragCanceled.emit();
537bool StateInternalMDIDragging::handleMouseMove(Point globalPos)
539 if (!q->m_draggableGuard) {
540 KDDW_ERROR(
"Draggable was destroyed, canceling the drag");
541 q->dragCanceled.emit();
546 auto tb = q->m_draggable->asView()->asTitleBarController();
548 KDDW_ERROR(
"expected a title bar, not {}", (
void * )q->m_draggable);
549 q->dragCanceled.emit();
553 Group *group = tb->group();
556 KDDW_ERROR(
"null group.");
557 q->dragCanceled.emit();
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;
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()));
574 layout->moveDockWidget(group, newLocalPosBounded);
579 if (threshold != -1) {
580 const Point overflow = newLocalPosBounded - newLocalPos;
581 if (std::abs(overflow.x()) > threshold || std::abs(overflow.y()) > threshold)
588bool StateInternalMDIDragging::handleMouseDoubleClick()
590 q->dragCanceled.emit();
596StateDragging *createDraggingState(DragController *parent)
598#ifdef KDDW_FRONTEND_QT
599 return isWayland() ?
new StateDraggingWayland(parent) : new StateDragging(parent);
601 return new StateDragging(parent);
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))
614 KDDW_TRACE(
"DragController CTOR");
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);
623 m_stateDraggingMDI->addTransition(dragCanceled, m_stateNone);
624 m_stateDraggingMDI->addTransition(mdiPopOut, m_stateDragging);
627 enableFallbackMouseGrabber();
629 setCurrentState(m_stateNone);
632DragController *DragController::instance()
634 static DragController dragController;
635 return &dragController;
638void DragController::registerDraggable(Draggable *drg)
640 m_draggables.push_back(drg);
641 drg->asView()->installViewEventFilter(
this);
644void DragController::unregisterDraggable(Draggable *drg)
646 m_draggables.removeOne(drg);
647 drg->asView()->removeViewEventFilter(
this);
650bool DragController::isDragging()
const
652 return m_windowBeingDragged !=
nullptr || activeState() == m_stateDraggingMDI;
655bool DragController::isInNonClientDrag()
const
657 return isDragging() && m_nonClientDrag;
660bool DragController::isInClientDrag()
const
662 return isDragging() && !m_nonClientDrag;
665bool DragController::isInProgrammaticDrag()
const
667 return m_inProgrammaticDrag;
670bool DragController::isIdle()
const
672 return activeState() == m_stateNone;
675void DragController::grabMouseFor(
View *target)
680 if (m_fallbackMouseGrabber) {
681 m_fallbackMouseGrabber->grabMouse(target);
687void DragController::releaseMouse(
View *target)
692 if (m_fallbackMouseGrabber) {
693 m_fallbackMouseGrabber->releaseMouse();
701 return m_windowBeingDragged ? m_windowBeingDragged->floatingWindow() :
nullptr;
704void DragController::enableFallbackMouseGrabber()
706 if (!m_fallbackMouseGrabber)
707 m_fallbackMouseGrabber =
new FallbackMouseGrabber(
this);
710WindowBeingDragged *DragController::windowBeingDragged()
const
712 return m_windowBeingDragged.get();
715bool DragController::onDnDEvent(
View *view, Event *e)
722 KDDW_DEBUG(
"DragController::onDnDEvent: ev={}, dropArea=",
int(e->type()), (
void * )view->
asDropAreaController());
724 switch (
int(e->type())) {
725 case Event::DragEnter:
726 if (activeState()->handleDragEnter(
static_cast<DragMoveEvent *
>(e), dropArea))
729 case Event::DragLeave:
730 if (activeState()->handleDragLeave(dropArea))
733 case Event::DragMove:
734 if (activeState()->handleDragMove(
static_cast<DragMoveEvent *
>(e), dropArea))
738 if (activeState()->handleDrop(
static_cast<DropEvent *
>(e), dropArea))
743 }
else if (e->type() == Event::DragEnter && isDragging()) {
745 KDDW_DEBUG(
"DragController::onDnDEvent: Eating DragEnter.");
748 KDDW_DEBUG(
"DragController::onDnDEvent: No view. ev={}",
int(e->type()));
754bool DragController::onMoveEvent(
View *)
756 if (m_nonClientDrag) {
758 KDDW_TRACE(
"DragController::onMoveEvent");
766bool DragController::onMouseEvent(
View *w, MouseEvent *me)
771 KDDW_TRACE(
"DragController::onMouseEvent e={} ; nonClientDrag={}",
int(me->type()), m_nonClientDrag);
773 switch (me->type()) {
774 case Event::NonClientAreaMouseButtonPress: {
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());
785 case Event::MouseButtonPress:
793 if (KDDockWidgets::usesNativeDraggingAndResizing() && w->
isRootView())
796 assert(activeState());
797 return activeState()->handleMouseButtonPress(
798 draggableForView(w), Qt5Qt6Compat::eventGlobalPos(me), me->pos());
800 case Event::MouseButtonRelease:
801 case Event::NonClientAreaMouseButtonRelease: {
803 const bool inProgrammaticDrag = m_inProgrammaticDrag;
804 const bool result = activeState()->handleMouseButtonRelease(Qt5Qt6Compat::eventGlobalPos(me));
816 return result && !inProgrammaticDrag;
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();
832StateBase *DragController::activeState()
const
834 return static_cast<StateBase *
>(currentState());
839 if (
auto dropArea = dropAreaUnderCursor())
845bool DragController::programmaticStartDrag(Draggable *draggable, Point globalPos, Point offset)
851 KDDW_WARN(
"DragController::programmaticStartDrag: draggable is null");
856 KDDW_WARN(
"DragController::programmaticStartDrag: Dragging already ongoing");
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");
868 if (
auto func =
Config::self().dragAboutToStartFunc()) {
869 if (!func(m_draggable))
873 manhattanLengthMove.emit();
874 if (activeState() != m_stateDragging && !isWayland()) {
875 KDDW_WARN(
"DragController::programmaticStartDrag: Expected to be in drag state");
880 m_stateDragging->handleMouseMove(globalPos);
885void DragController::programmaticStopDrag()
890#if defined(KDDW_FRONTEND_QT_WINDOWS)
891static std::shared_ptr<View> qtTopLevelForHWND(HWND hwnd)
894 for (Window::Ptr window : windows) {
895 if (!window->isVisible())
898 if (hwnd == ( HWND )window->handle()) {
899 if (
auto result = window->rootView())
901#ifdef KDDW_FRONTEND_QTWIDGETS
906 const QWidgetList widgets = qApp->topLevelWidgets();
907 for (
QWidget *widget : widgets) {
908 if (!widget->window()) {
915 if (hwnd == ( HWND )widget->winId()) {
924 KDDW_TRACE(
"Couldn't find hwnd for top-level hwnd={}", (
void * )hwnd);
931 const Window::List &windows,
932 View *rootViewBeingDragged)
934 for (
auto i = windows.size() - 1; i >= 0; --i) {
935 const Window::Ptr &window = windows.at(i);
936 auto tl = window->rootView();
938 if (!tl->isVisible() || tl->equals(rootViewBeingDragged) || tl->isMinimized())
941 if (rootViewBeingDragged && rootViewBeingDragged->
window()->equals(window))
944 if (window->geometry().contains(globalPos)) {
945 KDDW_TRACE(
"Found top-level {}", (
void * )tl.get());
953std::shared_ptr<View> DragController::qtTopLevelUnderCursor()
const
957 if (KDDockWidgets::isWindows()) {
958#if defined(KDDW_FRONTEND_QT_WINDOWS)
959 POINT globalNativePos;
960 if (!GetCursorPos(&globalNativePos))
966 HWND hwnd = HWND(m_windowBeingDragged->floatingWindow()->view()->window()->handle());
968 hwnd = GetWindow(hwnd, GW_HWNDNEXT);
970 if (!GetWindowRect(hwnd, &r) || !IsWindowVisible(hwnd))
973 if (!PtInRect(&r, globalNativePos))
976 if (
auto tl = qtTopLevelForHWND(hwnd)) {
977 const Rect windowGeometry = tl->d->windowGeometry();
979 if (windowGeometry.contains(globalPos)
980 && tl->viewName() != QStringLiteral(
"_docks_IndicatorWindow_Overlay")) {
981 KDDW_TRACE(
"Found top-level {}", (
void * )tl.get());
985#ifdef KDDW_FRONTEND_QTWIDGETS
988 auto topLevels = qApp->topLevelWidgets();
989 for (
auto topLevel : topLevels) {
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);
1004 KDDW_TRACE(
"Window from another app is under cursor {}", (
void * )hwnd);
1009 }
else if (linksToXLib() && isXCB()) {
1011 const Window::List orderedWindows = KDDockWidgets::orderedWindows(ok);
1012 FloatingWindow *tlwBeingDragged = m_windowBeingDragged->floatingWindow();
1018 KDDW_TRACE(
"No top-level found. Some windows weren't seen by XLib");
1028 View *tlwBeingDragged = m_windowBeingDragged->floatingWindow()->
view();
1037 KDDW_TRACE(
"No top-level found");
1044 const auto localPos = topLevel->mapFromGlobal(globalPos);
1058DropArea *DragController::dropAreaUnderCursor()
const
1060 if (!m_windowBeingDragged)
1063 std::shared_ptr<View> topLevel = qtTopLevelUnderCursor();
1065 KDDW_DEBUG(
"DragController::dropAreaUnderCursor: No drop area under cursor");
1069 const Vector<QString> affinities = m_windowBeingDragged->floatingWindow()->affinities();
1071 if (
auto fw = topLevel->asFloatingWindowController()) {
1073 KDDW_DEBUG(
"DragController::dropAreaUnderCursor: Found drop area in floating window");
1078 if (topLevel->viewName() == QStringLiteral(
"_docks_IndicatorWindow")) {
1079 KDDW_ERROR(
"Indicator window should be hidden {} isVisible={}", (
void * )topLevel.get(), topLevel->isVisible());
1084 KDDW_DEBUG(
"DragController::dropAreaUnderCursor: Found drop area {} {}", (
void * )dt, (
void * )dt->view()->rootView().get());
1088 KDDW_DEBUG(
"DragController::dropAreaUnderCursor: null2");
1092Draggable *DragController::draggableForView(
View *view)
const
1094 for (
auto draggable : m_draggables)
1095 if (draggable->asView()->equals(view)) {
1102bool DragController::isInQDrag()
const
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)
A MultiSplitter with support for drop indicators when hovering over.
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)