/* This file is part of KDDockWidgets. SPDX-FileCopyrightText: 2019-2020 Klarälvdalens Datakonsult AB, a KDAB Group company Author: Sérgio Martins SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only Contact KDAB at for commercial licensing options. */ #include "DragController_p.h" #include "Frame_p.h" #include "Logging_p.h" #include "DropArea_p.h" #include "FloatingWindow_p.h" #include "WidgetResizeHandler_p.h" #include "Utils_p.h" #include "DockRegistry_p.h" #include "Qt5Qt6Compat_p.h" #include #include #include #include #include #include #if defined(Q_OS_WIN) # include # include #endif using namespace KDDockWidgets; namespace KDDockWidgets { ///@brief Custom mouse grabber, for platforms that don't support grabbing the mouse class FallbackMouseGrabber : public QObject /// clazy:exclude=missing-qobject-macro { public: FallbackMouseGrabber(QObject *parent) : QObject(parent) { } ~FallbackMouseGrabber() override; void grabMouse(QWidgetOrQuick *target) { m_target = target; qApp->installEventFilter(this); } void releaseMouse() { #ifdef KDDOCKWIDGETS_QTQUICK // Ungrab harder if QtQuick. // QtQuick has the habit og grabbing the MouseArea internally, then doesn't ungrab it since // we're consuming the events. So explicitly ungrab if any QQuickWindow::mouseGrabberItem() // is still set. QQuickView *view = m_target ? m_target->quickView() : nullptr; QQuickItem *grabber = view ? view->mouseGrabberItem() : nullptr; if (grabber) grabber->ungrabMouse(); #endif m_target = nullptr; qApp->removeEventFilter(this); } bool eventFilter(QObject *, QEvent *ev) override { if (m_reentrancyGuard || !m_target) return false; if (QMouseEvent *me = mouseEvent(ev)) { m_reentrancyGuard = true; qApp->sendEvent(m_target, me); m_reentrancyGuard = false; return true; } return false; } bool m_reentrancyGuard = false; QPointer m_target; }; FallbackMouseGrabber::~FallbackMouseGrabber() {} } State::State(MinimalStateMachine *parent) : QObject(parent) , m_machine(parent) { } State::~State() = default; bool State::isCurrentState() const { return m_machine->currentState() == this; } MinimalStateMachine::MinimalStateMachine(QObject *parent) : QObject(parent) { } template void State::addTransition(Obj *obj, Signal signal, State *dest) { connect(obj, signal, this, [this, dest] { if (isCurrentState()) { m_machine->setCurrentState(dest); } }); } State *MinimalStateMachine::currentState() const { return m_currentState; } void MinimalStateMachine::setCurrentState(State *state) { if (state != m_currentState) { m_currentState = state; if (state) state->onEntry(); } } StateBase::StateBase(DragController *parent) : State(parent) , q(parent) { } bool StateBase::isActiveState() const { return q->activeState() == this; } StateBase::~StateBase() = default; StateNone::StateNone(DragController *parent) : StateBase(parent) { } void StateNone::onEntry() { qCDebug(state) << "StateNone entered"; q->m_pressPos = QPoint(); q->m_offset = QPoint(); q->m_draggable = nullptr; q->m_windowBeingDragged.reset(); WidgetResizeHandler::s_disableAllHandlers = false; // Re-enable resize handlers q->m_nonClientDrag = false; if (q->m_currentDropArea) { q->m_currentDropArea->removeHover(); q->m_currentDropArea = nullptr; } } bool StateNone::handleMouseButtonPress(Draggable *draggable, QPoint globalPos, QPoint pos) { qCDebug(state) << "StateNone::handleMouseButtonPress: draggable" << draggable << "; globalPos" << globalPos; if (!draggable->isPositionDraggable(pos)) return false; q->m_draggable = draggable; q->m_pressPos = globalPos; q->m_offset = pos; Q_EMIT q->mousePressed(); return false; } StateNone::~StateNone() = default; StatePreDrag::StatePreDrag(DragController *parent) : StateBase(parent) { } StatePreDrag::~StatePreDrag() = default; void StatePreDrag::onEntry() { qCDebug(state) << "StatePreDrag entered"; WidgetResizeHandler::s_disableAllHandlers = true; // Disable the resize handler during dragging } bool StatePreDrag::handleMouseMove(QPoint globalPos) { if (q->m_draggable->dragCanStart(q->m_pressPos, globalPos)) { Q_EMIT q->manhattanLengthMove(); return true; } return false; } bool StatePreDrag::handleMouseButtonRelease(QPoint) { Q_EMIT q->dragCanceled(); return false; } bool StatePreDrag::handleMouseDoubleClick() { // This is only needed for QtQuick. // With QtQuick, when double clicking, we get: Press, Release, Press, Double-click. and never // receive the last Release event. Q_EMIT q->dragCanceled(); return false; } StateDragging::StateDragging(DragController *parent) : StateBase(parent) { } StateDragging::~StateDragging() = default; void StateDragging::onEntry() { if (DockWidgetBase *dw = q->m_draggable->singleDockWidget()) { // When we start to drag a floating window which has a single dock widget, we save the position if (dw->isFloating()) dw->saveLastFloatingGeometry(); } q->m_windowBeingDragged = q->m_draggable->makeWindow(); if (q->m_windowBeingDragged) { #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) # ifdef Q_OS_WIN if (!q->m_nonClientDrag && KDDockWidgets::usesNativeDraggingAndResizing()) { // Started as a client move, as the dock widget was docked, // but now that we're dragging it as a floating window, switch to native drag FloatingWindow *fw = q->m_windowBeingDragged->floatingWindow(); q->m_nonClientDrag = true; q->m_windowBeingDragged.reset(); const HWND hwnd = HWND(fw->windowHandle()->winId()); q->m_windowBeingDragged = fw->makeWindow(); QWindow *window = fw->windowHandle(); window->startSystemMove(); // Mouse press was done in another window, so we need to ungrab ReleaseCapture(); PostMessage(hwnd, WM_SYSCOMMAND, 0xF012, 0); // SC_DRAGMOVE } # endif #endif qCDebug(state) << "StateDragging entered. m_draggable=" << q->m_draggable << "; m_windowBeingDragged=" << q->m_windowBeingDragged->floatingWindow(); auto fw = q->m_windowBeingDragged->floatingWindow(); if (!fw->geometry().contains(q->m_pressPos)) { // The window shrunk when the drag started, this can happen if it has max-size constraints // we make the floating window smaller. Has the downside that it might not be under the mouse // cursor anymore, so make the change if (fw->width() < q->m_offset.x()) { // make sure it shrunk q->m_offset.setX(fw->width() / 2); } } } else { // Shouldn't happen qWarning() << Q_FUNC_INFO << "No window being dragged for " << q->m_draggable->asWidget(); Q_EMIT q->dragCanceled(); } } bool StateDragging::handleMouseButtonRelease(QPoint globalPos) { qCDebug(state) << "StateDragging: handleMouseButtonRelease"; FloatingWindow *floatingWindow = q->m_windowBeingDragged->floatingWindow(); if (!floatingWindow) { // It was deleted externally qCDebug(state) << "StateDragging: Bailling out, deleted externally"; Q_EMIT q->dragCanceled(); return true; } if (floatingWindow->anyNonDockable()) { qCDebug(state) << "StateDragging: Ignoring floating window with non dockable widgets"; Q_EMIT q->dragCanceled(); return true; } if (q->m_currentDropArea) { if (q->m_currentDropArea->drop(q->m_windowBeingDragged.get(), globalPos)) { Q_EMIT q->dropped(); } else { qCDebug(state) << "StateDragging: Bailling out, drop not accepted"; Q_EMIT q->dragCanceled(); } } else { qCDebug(state) << "StateDragging: Bailling out, not over a drop area"; Q_EMIT q->dragCanceled(); } return true; } bool StateDragging::handleMouseMove(QPoint globalPos) { FloatingWindow *fw = q->m_windowBeingDragged->floatingWindow(); if (!fw) { qCDebug(state) << "Canceling drag, window was deleted"; Q_EMIT q->dragCanceled(); return true; } if (fw->beingDeleted()) { // Ignore, we're in the middle of recurrency. We're inside StateDragging::handleMouseButtonRelease too return true; } if (!q->m_nonClientDrag) fw->windowHandle()->setPosition(globalPos - q->m_offset); if (fw->anyNonDockable()) { qCDebug(state) << "StateDragging: Ignoring non dockable floating window"; return true; } DropArea *dropArea = q->dropAreaUnderCursor(); if (q->m_currentDropArea && dropArea != q->m_currentDropArea) q->m_currentDropArea->removeHover(); if (dropArea) { if (FloatingWindow *targetFw = dropArea->floatingWindow()) { if (targetFw->anyNonDockable()) { qCDebug(state) << "StateDragging: Ignoring non dockable target floating window"; return false; } } dropArea->hover(q->m_windowBeingDragged.get(), globalPos); } q->m_currentDropArea = dropArea; return true; } bool StateDragging::handleMouseDoubleClick() { // See comment in StatePreDrag::handleMouseDoubleClick(). // Very unlikely that we're in this state though, due to manhattan length Q_EMIT q->dragCanceled(); return false; } StateDraggingWayland::StateDraggingWayland(DragController *parent) : StateDragging(parent) { } StateDraggingWayland::~StateDraggingWayland() { } void StateDraggingWayland::onEntry() { qCDebug(state) << "StateDragging entered"; if (m_inQDrag) { // Maybe we can exit the state due to the nested event loop of QDrag::Exec(); qWarning() << Q_FUNC_INFO << "Impossible!"; return; } QScopedValueRollback guard(m_inQDrag, true); q->m_windowBeingDragged = std::unique_ptr(new WindowBeingDraggedWayland(q->m_draggable)); auto mimeData = new WaylandMimeData(); QDrag drag(this); drag.setMimeData(mimeData); drag.setPixmap(q->m_windowBeingDragged->pixmap()); qApp->installEventFilter(q); const Qt::DropAction result = drag.exec(); qApp->removeEventFilter(q); if (result == Qt::IgnoreAction) Q_EMIT q->dragCanceled(); } bool StateDraggingWayland::handleMouseButtonRelease(QPoint /*globalPos*/) { qCDebug(state) << Q_FUNC_INFO; Q_EMIT q->dragCanceled(); return true; } bool StateDraggingWayland::handleDragEnter(QDragEnterEvent *ev, DropArea *dropArea) { auto mimeData = qobject_cast(ev->mimeData()); if (!mimeData || !q->m_windowBeingDragged) return false; // Not for us, some other user drag. if (q->m_windowBeingDragged->contains(dropArea)) { ev->ignore(); return true; } dropArea->hover(q->m_windowBeingDragged.get(), dropArea->mapToGlobal(Qt5Qt6Compat::eventPos(ev))); ev->accept(); return true; } bool StateDraggingWayland::handleDragLeave(DropArea *dropArea) { dropArea->removeHover(); return true; } bool StateDraggingWayland::handleDrop(QDropEvent *ev, DropArea *dropArea) { auto mimeData = qobject_cast(ev->mimeData()); if (!mimeData || !q->m_windowBeingDragged) return false; // Not for us, some other user drag. if (dropArea->drop(q->m_windowBeingDragged.get(), dropArea->mapToGlobal(Qt5Qt6Compat::eventPos(ev)))) { ev->setDropAction(Qt::MoveAction); ev->accept(); Q_EMIT q->dropped(); } else { Q_EMIT q->dragCanceled(); } dropArea->removeHover(); return true; } bool StateDraggingWayland::handleDragMove(QDragMoveEvent *ev, DropArea *dropArea) { auto mimeData = qobject_cast(ev->mimeData()); if (!mimeData || !q->m_windowBeingDragged) return false; // Not for us, some other user drag. dropArea->hover(q->m_windowBeingDragged.get(), dropArea->mapToGlobal(Qt5Qt6Compat::eventPos(ev))); return true; } DragController::DragController(QObject *parent) : MinimalStateMachine(parent) { qCDebug(creation) << "DragController()"; auto stateNone = new StateNone(this); auto statepreDrag = new StatePreDrag(this); auto stateDragging = isWayland() ? new StateDraggingWayland(this) : new StateDragging(this); stateNone->addTransition(this, &DragController::mousePressed, statepreDrag); statepreDrag->addTransition(this, &DragController::dragCanceled, stateNone); statepreDrag->addTransition(this, &DragController::manhattanLengthMove, stateDragging); stateDragging->addTransition(this, &DragController::dragCanceled, stateNone); stateDragging->addTransition(this, &DragController::dropped, stateNone); if (usesFallbackMouseGrabber()) enableFallbackMouseGrabber(); setCurrentState(stateNone); } DragController *DragController::instance() { static DragController dragController; return &dragController; } void DragController::registerDraggable(Draggable *drg) { m_draggables << drg; drg->asWidget()->installEventFilter(this); } void DragController::unregisterDraggable(Draggable *drg) { m_draggables.removeOne(drg); drg->asWidget()->removeEventFilter(this); } bool DragController::isDragging() const { return m_windowBeingDragged != nullptr; } bool DragController::isInNonClientDrag() const { return isDragging() && m_nonClientDrag; } bool DragController::isInClientDrag() const { return isDragging() && !m_nonClientDrag; } void DragController::grabMouseFor(QWidgetOrQuick *target) { if (isWayland()) return; // No grabbing supported on wayland if (m_fallbackMouseGrabber) { m_fallbackMouseGrabber->grabMouse(target); } else { target->grabMouse(); } } void DragController::releaseMouse(QWidgetOrQuick *target) { if (isWayland()) return; // No grabbing supported on wayland if (m_fallbackMouseGrabber) { m_fallbackMouseGrabber->releaseMouse(); } else { target->releaseMouse(); } } FloatingWindow *DragController::floatingWindowBeingDragged() const { return m_windowBeingDragged ? m_windowBeingDragged->floatingWindow() : nullptr; } void DragController::enableFallbackMouseGrabber() { if (!m_fallbackMouseGrabber) m_fallbackMouseGrabber = new FallbackMouseGrabber(this); } WindowBeingDragged *DragController::windowBeingDragged() const { return m_windowBeingDragged.get(); } bool DragController::eventFilter(QObject *o, QEvent *e) { if (m_nonClientDrag && e->type() == QEvent::Move) { // On Windows, non-client mouse moves are only sent at the end, so we must fake it: qCDebug(mouseevents) << "DragController::eventFilter e=" << e->type() << "; o=" << o; activeState()->handleMouseMove(QCursor::pos()); return MinimalStateMachine::eventFilter(o, e); } if (isWayland()) { // Wayland is very different. It uses QDrag for the dragging of a window. if (auto dropArea = qobject_cast(o)) { switch (int(e->type())) { case QEvent::DragEnter: if (activeState()->handleDragEnter(static_cast(e), dropArea)) return true; break; case QEvent::DragLeave: if (activeState()->handleDragLeave(dropArea)) return true; break; case QEvent::DragMove: if (activeState()->handleDragMove(static_cast(e), dropArea)) return true; break; case QEvent::Drop: if (activeState()->handleDrop(static_cast(e), dropArea)) return true; break; } } } QMouseEvent *me = mouseEvent(e); if (!me) return MinimalStateMachine::eventFilter(o, e); auto w = qobject_cast(o); if (!w) return MinimalStateMachine::eventFilter(o, e); qCDebug(mouseevents) << "DragController::eventFilter e=" << e->type() << "; o=" << o << "; m_nonClientDrag=" << m_nonClientDrag; switch (e->type()) { case QEvent::NonClientAreaMouseButtonPress: { if (auto fw = qobject_cast(o)) { if (fw->isInDragArea(Qt5Qt6Compat::eventGlobalPos(me))) { m_nonClientDrag = true; return activeState()->handleMouseButtonPress(draggableForQObject(o), Qt5Qt6Compat::eventGlobalPos(me), me->pos()); } } return MinimalStateMachine::eventFilter(o, e); } case QEvent::MouseButtonPress: // For top-level windows that support native dragging all goes through the NonClient* events. // This also forbids dragging a FloatingWindow simply by pressing outside of the title area, in the background if (!KDDockWidgets::usesNativeDraggingAndResizing() || !w->isWindow()) { Q_ASSERT(activeState()); return activeState()->handleMouseButtonPress(draggableForQObject(o), Qt5Qt6Compat::eventGlobalPos(me), me->pos()); } else break; case QEvent::MouseButtonRelease: case QEvent::NonClientAreaMouseButtonRelease: return activeState()->handleMouseButtonRelease(Qt5Qt6Compat::eventGlobalPos(me)); case QEvent::NonClientAreaMouseMove: case QEvent::MouseMove: return activeState()->handleMouseMove(Qt5Qt6Compat::eventGlobalPos(me)); case QEvent::MouseButtonDblClick: return activeState()->handleMouseDoubleClick(); default: break; } return MinimalStateMachine::eventFilter(o, e); } StateBase *DragController::activeState() const { return static_cast(currentState()); } #if defined(Q_OS_WIN) static QWidgetOrQuick *qtTopLevelForHWND(HWND hwnd) { const QList windows = qApp->topLevelWindows(); for (QWindow *window : windows) { if (hwnd == (HWND)window->winId()) { return DockRegistry::self()->topLevelForHandle(window); } } qCDebug(toplevels) << Q_FUNC_INFO << "Couldn't find hwnd for top-level" << hwnd; return nullptr; } #endif template static WidgetType* qtTopLevelUnderCursor_impl(QPoint globalPos, const QVector &windows, T windowBeingDragged) { for (auto i = windows.size() -1; i >= 0; --i) { QWindow *window = windows.at(i); auto tl = KDDockWidgets::Private::widgetForWindow(window); if (!tl->isVisible() || tl == windowBeingDragged || KDDockWidgets::Private::isMinimized(tl)) continue; if (windowBeingDragged && KDDockWidgets::Private::windowForWidget(windowBeingDragged) == KDDockWidgets::Private::windowForWidget(tl)) continue; if (window->geometry().contains(globalPos)) { qCDebug(toplevels) << Q_FUNC_INFO << "Found top-level" << tl; return tl; } } return nullptr; } WidgetType *DragController::qtTopLevelUnderCursor() const { QPoint globalPos = QCursor::pos(); if (qApp->platformName() == QLatin1String("windows")) { // So -platform offscreen on Windows doesn't use this #if defined(Q_OS_WIN) POINT globalNativePos; if (!GetCursorPos(&globalNativePos)) return nullptr; // There might be windows that don't belong to our app in between, so use win32 to travel by z-order. // Another solution is to set a parent on all top-levels. But this code is orthogonal. HWND hwnd = HWND(m_windowBeingDragged->floatingWindow()->winId()); while (hwnd) { hwnd = GetWindow(hwnd, GW_HWNDNEXT); RECT r; if (!GetWindowRect(hwnd, &r) || !IsWindowVisible(hwnd)) continue; if (!PtInRect(&r, globalNativePos)) // Check if window is under cursor continue; if (auto tl = qtTopLevelForHWND(hwnd)) { if (tl->geometry().contains(globalPos) && tl->objectName() != QStringLiteral("_docks_IndicatorWindow_Overlay")) { qCDebug(toplevels) << Q_FUNC_INFO << "Found top-level" << tl; return tl; } } else { # ifdef KDDOCKWIDGETS_QTWIDGETS // Maybe it's embedded in a QWinWidget: auto topLevels = qApp->topLevelWidgets(); for (auto topLevel : topLevels) { if (QLatin1String(topLevel->metaObject()->className()) == QLatin1String("QWinWidget")) { if (hwnd == GetParent(HWND(topLevel->windowHandle()->winId()))) { if (topLevel->rect().contains(topLevel->mapFromGlobal(globalPos)) && topLevel->objectName() != QStringLiteral("_docks_IndicatorWindow_Overlay")) { qCDebug(toplevels) << Q_FUNC_INFO << "Found top-level" << topLevel; return topLevel; } } } } # endif // QtWidgets // A window belonging to another app is below the cursor qCDebug(toplevels) << Q_FUNC_INFO << "Window from another app is under cursor" << hwnd; return nullptr; } } #endif // Q_OS_WIN } else { // !Windows: Linux, macOS, offscreen (offscreen on Windows too), etc. // On Linux we don't have API to check the z-order of top-levels. So first check the floating windows // 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 ?) // The floating window list is sorted by z-order, as we catch QEvent::Expose and move it to last of the list FloatingWindow *tlwBeingDragged = m_windowBeingDragged->floatingWindow(); if (auto tl = qtTopLevelUnderCursor_impl(globalPos, DockRegistry::self()->floatingQWindows(), tlwBeingDragged)) return tl; return qtTopLevelUnderCursor_impl(globalPos, DockRegistry::self()->topLevels(/*excludeFloating=*/true), tlwBeingDragged); } qCDebug(toplevels) << Q_FUNC_INFO << "No top-level found"; return nullptr; } static DropArea* deepestDropAreaInTopLevel(WidgetType *topLevel, QPoint globalPos, const QStringList &affinities) { const auto localPos = topLevel->mapFromGlobal(globalPos); auto w = topLevel->childAt(localPos.x(), localPos.y()); while (w) { if (auto dt = qobject_cast(w)) { if (DockRegistry::self()->affinitiesMatch(dt->affinities(), affinities)) return dt; } w = KDDockWidgets::Private::parentWidget(w); } return nullptr; } DropArea *DragController::dropAreaUnderCursor() const { WidgetType *topLevel = qtTopLevelUnderCursor(); if (!topLevel) return nullptr; const QStringList affinities = m_windowBeingDragged->floatingWindow()->affinities(); if (auto fw = qobject_cast(topLevel)) { if (DockRegistry::self()->affinitiesMatch(fw->affinities(), affinities)) return fw->dropArea(); } if (topLevel->objectName() == QStringLiteral("_docks_IndicatorWindow")) { qWarning() << "Indicator window should be hidden " << topLevel << topLevel->isVisible(); Q_ASSERT(false); } if (auto dt = deepestDropAreaInTopLevel(topLevel, QCursor::pos(), affinities)) { return dt; } qCDebug(state) << "DragController::dropAreaUnderCursor: null2"; return nullptr; } Draggable *DragController::draggableForQObject(QObject *o) const { for (auto draggable : m_draggables) if (draggable->asWidget() == o) { return draggable; } return nullptr; }