AdaptixC2-Mod0/Libs/kddockwidgets/core/DragController.cpp
2026-04-06 00:20:51 -05:00

1110 lines
35 KiB
C++

/*
This file is part of KDDockWidgets.
SPDX-FileCopyrightText: 2019 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
Author: Sérgio Martins <sergio.martins@kdab.com>
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only
Contact KDAB at <info@kdab.com> for commercial licensing options.
*/
#include "DragController_p.h"
#include "Logging_p.h"
#include "Utils_p.h"
#include "WidgetResizeHandler_p.h"
#include "Config.h"
#include "WindowZOrder_x11_p.h"
#include "core/DockRegistry.h"
#include "core/Window_p.h"
#include "core/MDILayout.h"
#include "core/DropArea.h"
#include "core/TitleBar.h"
#include "core/Platform.h"
#include "core/Group.h"
#include "core/FloatingWindow.h"
#include "core/DockWidget_p.h"
#include "core/ScopedValueRollback_p.h"
#ifdef KDDW_FRONTEND_QT
#include "../qtcommon/DragControllerWayland_p.h"
#ifdef KDDW_FRONTEND_QTWIDGETS
#include "kddockwidgets/qtcommon/Platform.h"
#include <QWidget>
#include <QApplication>
#endif
#endif
#include <algorithm>
#include <cstdlib>
#if defined(Q_OS_WIN)
#include <windows.h>
#endif
using namespace KDDockWidgets;
using namespace KDDockWidgets::Core;
namespace KDDockWidgets::Core {
///@brief Custom mouse grabber, for platforms that don't support grabbing the mouse
class FallbackMouseGrabber : public Core::Object, /// clazy:exclude=missing-qobject-macro
public EventFilterInterface /// clazy:exclude=missing-qobject-macro
{
public:
explicit FallbackMouseGrabber(Core::Object *parent)
: Core::Object(parent)
{
}
~FallbackMouseGrabber() override;
void grabMouse(View *target)
{
m_target = target;
m_guard = target;
Platform::instance()->installGlobalEventFilter(this);
}
void releaseMouse()
{
// Ungrab harder if QtQuick.
// QtQuick has the habit of 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. Done via platform now, so it's generic. Should be a no-op for QtWidgets.
Platform::instance()->ungrabMouse();
m_target = nullptr;
m_guard.clear();
Platform::instance()->removeGlobalEventFilter(this);
}
bool onMouseEvent(View *, MouseEvent *me) override
{
if (m_reentrancyGuard || !m_guard)
return false;
m_reentrancyGuard = true;
Platform::instance()->sendEvent(m_target, me);
m_reentrancyGuard = false;
return true;
}
bool m_reentrancyGuard = false;
View *m_target = nullptr;
ViewGuard m_guard = nullptr;
};
FallbackMouseGrabber::~FallbackMouseGrabber()
{
}
}
State::State(MinimalStateMachine *parent)
: Core::Object(parent)
, m_machine(parent)
{
}
State::~State() = default;
bool State::isCurrentState() const
{
return m_machine->currentState() == this;
}
MinimalStateMachine::MinimalStateMachine(Core::Object *parent)
: Core::Object(parent)
{
}
template<typename Signal>
void State::addTransition(Signal &signal, State *dest)
{
signal.connect([this, dest] {
if (isCurrentState()) {
m_machine->setCurrentState(dest);
}
});
}
State *MinimalStateMachine::currentState() const
{
return m_currentState;
}
void MinimalStateMachine::setCurrentState(State *state)
{
if (state != m_currentState) {
if (m_currentState)
m_currentState->onExit();
m_currentState = state;
if (state)
state->onEntry();
currentStateChanged.emit();
}
}
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()
{
KDDW_DEBUG("StateNone entered");
q->m_pressPos = Point();
q->m_offset = Point();
q->m_draggable = nullptr;
q->m_draggableGuard.clear();
q->m_windowBeingDragged.reset();
WidgetResizeHandler::s_disableAllHandlers = false; // Re-enable resize handlers
q->m_nonClientDrag = false;
q->m_inProgrammaticDrag = false;
if (q->m_currentDropArea) {
q->m_currentDropArea->removeHover();
q->m_currentDropArea = nullptr;
}
/// Note that although this is unneedesly emitted at startup, there's nobody connected
/// to it, since we're in DragController ctor, so it's fine.
q->isDraggingChanged.emit();
}
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
bool StateNone::handleMouseButtonPress(Draggable *draggable, Point globalPos, Point pos)
{
KDDW_DEBUG("StateNone::handleMouseButtonPress: draggable={} ; globalPos={}", ( void * )draggable,
globalPos);
if (!draggable) {
KDDW_ERROR("StateNone::handleMouseButtonPress: null draggable");
return false;
}
if (!q->m_inProgrammaticDrag && !draggable->isPositionDraggable(pos))
return false;
q->m_draggable = draggable;
q->m_draggableGuard = draggable->asView();
q->m_pressPos = globalPos;
q->m_offset = draggable->mapToWindow(pos);
q->mousePressed.emit();
return false;
}
StateNone::~StateNone() = default;
StatePreDrag::StatePreDrag(DragController *parent)
: StateBase(parent)
{
}
StatePreDrag::~StatePreDrag() = default;
void StatePreDrag::onEntry()
{
KDDW_DEBUG("StatePreDrag entered {}", q->m_draggableGuard.isNull());
WidgetResizeHandler::s_disableAllHandlers = true; // Disable the resize handler during dragging
}
bool StatePreDrag::handleMouseMove(Point globalPos)
{
if (!q->m_draggableGuard) {
KDDW_ERROR("Draggable was destroyed, canceling the drag");
q->dragCanceled.emit();
return false;
}
if (!q->m_draggable->dragCanStart(q->m_pressPos, globalPos))
return false;
if (auto func = Config::self().dragAboutToStartFunc()) {
if (!func(q->m_draggable))
return false;
}
if (q->m_draggable->isMDI())
q->manhattanLengthMoveMDI.emit();
else
q->manhattanLengthMove.emit();
return true;
}
bool StatePreDrag::handleMouseButtonRelease(Point)
{
q->dragCanceled.emit();
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->dragCanceled.emit();
return false;
}
StateDragging::StateDragging(DragController *parent)
: StateBase(parent)
{
#if defined(KDDW_FRONTEND_QT_WINDOWS) && !defined(DOCKS_DEVELOPER_MODE)
m_maybeCancelDrag.setInterval(100);
QObject::connect(&m_maybeCancelDrag, &QTimer::timeout, this, [this] {
// Workaround bug #166 , where Qt doesn't agree with Window's mouse button state.
// Looking in the Qt bug tracker there's many hits, so do a quick workaround here:
const bool mouseButtonIsReallyDown = (GetKeyState(VK_LBUTTON) & 0x8000);
if (!mouseButtonIsReallyDown && Platform::instance()->isLeftMouseButtonPressed()) {
KDDW_DEBUG("Canceling drag, Qt thinks mouse button is pressed"
"but Windows knows it's not");
handleMouseButtonRelease(Platform::instance()->cursorPos());
q->dragCanceled.emit();
}
});
#endif
}
StateDragging::~StateDragging() = default;
void StateDragging::onEntry()
{
#if defined(KDDW_FRONTEND_QT_WINDOWS) && !defined(DOCKS_DEVELOPER_MODE)
m_maybeCancelDrag.start();
#endif
if (!q->m_draggableGuard) {
KDDW_ERROR("Draggable was destroyed, canceling the drag");
q->dragCanceled.emit();
return;
}
if (DockWidget *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->d->saveLastFloatingGeometry();
}
const bool needsUndocking = !q->m_draggable->isWindow();
q->m_windowBeingDragged = q->m_draggable->makeWindow();
if (q->m_windowBeingDragged) {
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) && defined(KDDW_FRONTEND_QT_WINDOWS)
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, so we can
// still get aero-snap
FloatingWindow *fw = q->m_windowBeingDragged->floatingWindow();
q->m_nonClientDrag = true;
q->m_windowBeingDragged.reset();
q->m_windowBeingDragged = fw->makeWindow();
Window::Ptr window = fw->view()->window();
if (needsUndocking) {
// Position the window before the drag start, otherwise if you move mouse too fast
// there will be an offset Only required when we've undocked/detached a window.
const Point cursorPos = Platform::instance()->cursorPos();
window->setPosition(cursorPos - q->m_offset);
}
// Start the native move
window->startSystemMove();
}
#else
KDDW_UNUSED(needsUndocking);
#endif
KDDW_DEBUG("StateDragging entered. m_draggable={}; m_windowBeingDragged={}", ( void * )q->m_draggable, ( void * )q->m_windowBeingDragged->floatingWindow());
auto fw = q->m_windowBeingDragged->floatingWindow();
#ifdef Q_OS_LINUX
if (fw->view()->isMaximized()) {
// When dragging a maximized window on linux we need to restore its normal size
// On Windows this works already. On macOS I don't see this feature at all
const Rect normalGeometry = fw->view()->normalGeometry();
// distance to the left edge of the window:
const int leftOffset = q->m_offset.x();
// distance to the right edge of the window:
const int rightOffset = fw->width() - q->m_offset.x();
const bool leftEdgeIsNearest = leftOffset <= rightOffset;
fw->view()->showNormal();
if (!normalGeometry.contains(q->m_pressPos)) {
if ((leftEdgeIsNearest && leftOffset > normalGeometry.width())
|| (!leftEdgeIsNearest && rightOffset > normalGeometry.width())) {
// Case #1: The window isn't under the cursor anymore
// Let's just put its middle under the cursor
q->m_offset.setX(normalGeometry.width() / 2);
} else if (!leftEdgeIsNearest) {
// Case #2: The new geometry is still under the cursor, but instead of moving
// its right edge left we'll move the left edge right, since initially the press
// position was closer to the right edge
q->m_offset.setX(normalGeometry.width() - rightOffset);
}
}
} else
#endif
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
KDDW_ERROR("No window being dragged for {} {}", ( void * )q->m_draggable, ( void * )q->m_draggable->asController());
q->dragCanceled.emit();
}
q->isDraggingChanged.emit();
}
void StateDragging::onExit()
{
#if defined(KDDW_FRONTEND_QT_WINDOWS) && !defined(DOCKS_DEVELOPER_MODE)
m_maybeCancelDrag.stop();
#endif
if (auto callback = Config::self().dragEndedFunc()) {
// this user is interested in knowing the drag ended
callback();
}
}
bool StateDragging::handleMouseButtonRelease(Point globalPos)
{
KDDW_DEBUG("StateDragging: handleMouseButtonRelease");
FloatingWindow *floatingWindow = q->m_windowBeingDragged->floatingWindow();
if (!floatingWindow) {
// It was deleted externally
KDDW_DEBUG("StateDragging: Bailling out, deleted externally");
q->dragCanceled.emit();
return true;
}
if (floatingWindow->anyNonDockable()) {
KDDW_DEBUG("StateDragging: Ignoring floating window with non dockable widgets");
q->dragCanceled.emit();
return true;
}
if (q->m_currentDropArea) {
if (q->m_currentDropArea->drop(q->m_windowBeingDragged.get(), globalPos)) {
q->dropped.emit();
} else {
KDDW_DEBUG("StateDragging: Bailling out, drop not accepted");
q->dragCanceled.emit();
}
} else {
KDDW_DEBUG("StateDragging: Bailling out, not over a drop area");
q->dragCanceled.emit();
}
return true;
}
bool StateDragging::handleMouseMove(Point globalPos)
{
FloatingWindow *fw = q->m_windowBeingDragged->floatingWindow();
if (!fw) {
KDDW_DEBUG("Canceling drag, window was deleted");
q->dragCanceled.emit();
return true;
}
if (fw->beingDeleted()) {
// Ignore, we're in the middle of recurrency. We're inside
// StateDragging::handleMouseButtonRelease too
return true;
}
#ifdef Q_OS_LINUX
if (fw->lastWindowManagerState() == WindowState::Maximized) {
// The window was maximized, we dragged it, which triggers a show normal.
// But we can only start moving the window *after* the (async) window manager acknowledges.
// See QTBUG-102430.
// Since #286 was only implemented and needed on Linux, then this counter-part is also
// ifdefed for Linux, Probably the ifdef could be removed, but don't want to be testing N
// platforms, who's undocumented behaviour can change between releases, so narrow the scope
// and workaround for linux only.
return true;
}
#endif
if (!q->m_nonClientDrag)
fw->view()->window()->setFramePosition(globalPos - q->m_offset);
if (fw->anyNonDockable()) {
KDDW_DEBUG("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()) {
KDDW_DEBUG("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->dragCanceled.emit();
return false;
}
StateInternalMDIDragging::StateInternalMDIDragging(DragController *parent)
: StateBase(parent)
{
}
StateInternalMDIDragging::~StateInternalMDIDragging()
{
}
void StateInternalMDIDragging::onEntry()
{
KDDW_DEBUG("StateInternalMDIDragging entered. draggable={}", ( void * )q->m_draggable);
if (!q->m_draggableGuard) {
KDDW_ERROR("Draggable was destroyed, canceling the drag");
q->dragCanceled.emit();
return;
}
// Raise the dock widget being dragged
if (auto tb = q->m_draggable->asView()->asTitleBarController()) {
if (Group *f = tb->group())
f->view()->raise();
}
q->isDraggingChanged.emit();
}
bool StateInternalMDIDragging::handleMouseButtonRelease(Point)
{
q->dragCanceled.emit();
return false;
}
bool StateInternalMDIDragging::handleMouseMove(Point globalPos)
{
if (!q->m_draggableGuard) {
KDDW_ERROR("Draggable was destroyed, canceling the drag");
q->dragCanceled.emit();
return false;
}
// for MDI we only support dragging via the title bar, other cases don't make sense conceptually
auto tb = q->m_draggable->asView()->asTitleBarController();
if (!tb) {
KDDW_ERROR("expected a title bar, not {}", ( void * )q->m_draggable);
q->dragCanceled.emit();
return false;
}
Group *group = tb->group();
if (!group) {
// Doesn't happen.
KDDW_ERROR("null group.");
q->dragCanceled.emit();
return false;
}
const Size parentSize = group->view()->d->parentSize();
const Point oldPos = group->mapToGlobal(Point(0, 0));
const Point delta = globalPos - oldPos;
const Point newLocalPos = group->pos() + delta - q->m_offset;
// Let's not allow the MDI window to go outside of its parent
Point newLocalPosBounded = { std::max(0, newLocalPos.x()), std::max(0, newLocalPos.y()) };
newLocalPosBounded.setX(std::min(newLocalPosBounded.x(), parentSize.width() - group->width()));
newLocalPosBounded.setY(std::min(newLocalPosBounded.y(), parentSize.height() - group->height()));
auto layout = group->mdiLayout();
assert(layout);
layout->moveDockWidget(group, newLocalPosBounded);
// Check if we need to pop out the MDI window (make it float)
// If we drag the window against an edge, and move behind the edge some threshold, we float it
const int threshold = Config::self().mdiPopupThreshold();
if (threshold != -1) {
const Point overflow = newLocalPosBounded - newLocalPos;
if (std::abs(overflow.x()) > threshold || std::abs(overflow.y()) > threshold)
q->mdiPopOut.emit();
}
return false;
}
bool StateInternalMDIDragging::handleMouseDoubleClick()
{
q->dragCanceled.emit();
return false;
}
namespace {
StateDragging *createDraggingState(DragController *parent)
{
#ifdef KDDW_FRONTEND_QT
return isWayland() ? new StateDraggingWayland(parent) : new StateDragging(parent);
#else
return new StateDragging(parent);
#endif
}
}
DragController::DragController(Core::Object *parent)
: MinimalStateMachine(parent)
, m_stateNone(new StateNone(this))
, m_statePreDrag(new StatePreDrag(this))
, m_stateDragging(createDraggingState(this))
, m_stateDraggingMDI(new StateInternalMDIDragging(this))
{
KDDW_TRACE("DragController CTOR");
m_stateNone->addTransition(mousePressed, m_statePreDrag);
m_statePreDrag->addTransition(dragCanceled, m_stateNone);
m_statePreDrag->addTransition(manhattanLengthMove, m_stateDragging);
m_statePreDrag->addTransition(manhattanLengthMoveMDI, m_stateDraggingMDI);
m_stateDragging->addTransition(dragCanceled, m_stateNone);
m_stateDragging->addTransition(dropped, m_stateNone);
m_stateDraggingMDI->addTransition(dragCanceled, m_stateNone);
m_stateDraggingMDI->addTransition(mdiPopOut, m_stateDragging);
if (Platform::instance()->usesFallbackMouseGrabber())
enableFallbackMouseGrabber();
setCurrentState(m_stateNone);
}
DragController *DragController::instance()
{
static DragController dragController;
return &dragController;
}
void DragController::registerDraggable(Draggable *drg)
{
m_draggables.push_back(drg);
drg->asView()->installViewEventFilter(this);
}
void DragController::unregisterDraggable(Draggable *drg)
{
m_draggables.removeOne(drg);
drg->asView()->removeViewEventFilter(this);
}
bool DragController::isDragging() const
{
return m_windowBeingDragged != nullptr || activeState() == m_stateDraggingMDI;
}
bool DragController::isInNonClientDrag() const
{
return isDragging() && m_nonClientDrag;
}
bool DragController::isInClientDrag() const
{
return isDragging() && !m_nonClientDrag;
}
bool DragController::isInProgrammaticDrag() const
{
return m_inProgrammaticDrag;
}
bool DragController::isIdle() const
{
return activeState() == m_stateNone;
}
void DragController::grabMouseFor(View *target)
{
if (isWayland())
return; // No grabbing supported on wayland
if (m_fallbackMouseGrabber) {
m_fallbackMouseGrabber->grabMouse(target);
} else {
target->grabMouse();
}
}
void DragController::releaseMouse(View *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::onDnDEvent(View *view, Event *e)
{
if (!isWayland())
return false;
// Wayland is very different. It uses QDrag for the dragging of a window.
if (view) {
KDDW_DEBUG("DragController::onDnDEvent: ev={}, dropArea=", int(e->type()), ( void * )view->asDropAreaController());
if (auto dropArea = view->asDropAreaController()) {
switch (int(e->type())) {
case Event::DragEnter:
if (activeState()->handleDragEnter(static_cast<DragMoveEvent *>(e), dropArea))
return true;
break;
case Event::DragLeave:
if (activeState()->handleDragLeave(dropArea))
return true;
break;
case Event::DragMove:
if (activeState()->handleDragMove(static_cast<DragMoveEvent *>(e), dropArea))
return true;
break;
case Event::Drop:
if (activeState()->handleDrop(static_cast<DropEvent *>(e), dropArea))
return true;
break;
default:
break;
}
}
} else if (e->type() == Event::DragEnter && isDragging()) {
// We're dragging a window. Be sure user code doesn't accept DragEnter events.
KDDW_DEBUG("DragController::onDnDEvent: Eating DragEnter.");
return true;
} else {
KDDW_DEBUG("DragController::onDnDEvent: No view. ev={}", int(e->type()));
}
return false;
}
bool DragController::onMoveEvent(View *)
{
if (m_nonClientDrag) {
// On Windows, non-client mouse moves are only sent at the end, so we must fake it:
KDDW_TRACE("DragController::onMoveEvent");
activeState()
->handleMouseMove(Platform::instance()->cursorPos());
}
return false;
}
bool DragController::onMouseEvent(View *w, MouseEvent *me)
{
if (!w)
return false;
KDDW_TRACE("DragController::onMouseEvent e={} ; nonClientDrag={}", int(me->type()), m_nonClientDrag);
switch (me->type()) {
case Event::NonClientAreaMouseButtonPress: {
if (auto fw = w->asFloatingWindowController()) {
if (KDDockWidgets::usesNativeTitleBar()
|| fw->isInDragArea(Qt5Qt6Compat::eventGlobalPos(me))) {
m_nonClientDrag = true;
return activeState()->handleMouseButtonPress(
draggableForView(w), Qt5Qt6Compat::eventGlobalPos(me), me->pos());
}
}
return false;
}
case Event::MouseButtonPress:
// We don't care about the secondary button
if (me->buttons() & Qt::RightButton)
break;
// 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->isRootView())
break;
assert(activeState());
return activeState()->handleMouseButtonPress(
draggableForView(w), Qt5Qt6Compat::eventGlobalPos(me), me->pos());
case Event::MouseButtonRelease:
case Event::NonClientAreaMouseButtonRelease: {
ViewGuard guard(w);
const bool inProgrammaticDrag = m_inProgrammaticDrag;
const bool result = activeState()->handleMouseButtonRelease(Qt5Qt6Compat::eventGlobalPos(me));
if (!guard) {
// Always consume the event if the view was deleted during a DND. For example
// tabbing A into B will destroy tabwidget A. Qt would then try to deliver event to A and crash.
return true;
}
// In normal KDDW operation, we consume the mouse release (true is returned), however,
// if using programmattic drag (via DockWidget::startDragging()), we do not want to consume the release event.
// User might have clicked a button to start the drag. Button needs to be released when it's over, otherwise
// it will look visually pressed.
return result && !inProgrammaticDrag;
}
case Event::NonClientAreaMouseMove:
case Event::MouseMove:
return activeState()->handleMouseMove(Qt5Qt6Compat::eventGlobalPos(me));
case Event::MouseButtonDblClick:
case Event::NonClientAreaMouseButtonDblClick:
return activeState()->handleMouseDoubleClick();
default:
break;
}
return false;
}
StateBase *DragController::activeState() const
{
return static_cast<StateBase *>(currentState());
}
DropLocation DragController::currentDropLocation() const
{
if (auto dropArea = dropAreaUnderCursor())
return dropArea->currentDropLocation();
return DropLocation_None;
}
bool DragController::programmaticStartDrag(Draggable *draggable, Point globalPos, Point offset)
{
// Here we manually force state machine states instead of having a 2nd/parallel API.
// As sharing 99.99% of the code path gives us some comfort.
if (!draggable) {
KDDW_WARN("DragController::programmaticStartDrag: draggable is null");
return false;
}
if (isDragging()) {
KDDW_WARN("DragController::programmaticStartDrag: Dragging already ongoing");
return false;
}
m_inProgrammaticDrag = true;
m_stateNone->handleMouseButtonPress(draggable, globalPos, offset);
if (activeState() != m_statePreDrag) {
m_inProgrammaticDrag = false;
KDDW_WARN("DragController::programmaticStartDrag: Expected to be in pre-drag state");
return false;
}
if (auto func = Config::self().dragAboutToStartFunc()) {
if (!func(m_draggable))
return false;
}
manhattanLengthMove.emit();
if (activeState() != m_stateDragging && !isWayland()) { // wayland blocks on a QDrag::exec(). When it reaches here we're already done
KDDW_WARN("DragController::programmaticStartDrag: Expected to be in drag state");
return false;
}
// Also fake the 1st mouse move, so code that positions the frame gets run
m_stateDragging->handleMouseMove(globalPos);
return true;
}
void DragController::programmaticStopDrag()
{
dragCanceled.emit();
}
#if defined(KDDW_FRONTEND_QT_WINDOWS)
static std::shared_ptr<View> qtTopLevelForHWND(HWND hwnd)
{
const Window::List windows = Platform::instance()->windows();
for (Window::Ptr window : windows) {
if (!window->isVisible())
continue;
if (hwnd == ( HWND )window->handle()) {
if (auto result = window->rootView())
return result;
#ifdef KDDW_FRONTEND_QTWIDGETS
if (Platform::instance()->isQtWidgets()) {
// It's not a KDDW window, but we still return something, as the KDDW main window
// might be embedded into another non-kddw QMainWindow
// Case not supported for QtQuick.
const QWidgetList widgets = qApp->topLevelWidgets();
for (QWidget *widget : widgets) {
if (!widget->window()) {
// Don't call winId on windows that don't have it, as that will force all
// its children to have it, and that's not very stable. a top level might
// not have one because it's being destroyed, or because it's a top-level
// just because it has't been reparented I guess.
continue;
}
if (hwnd == ( HWND )widget->winId()) {
return QtCommon::Platform_qt::instance()->qobjectAsView(widget);
}
}
}
#endif
}
}
KDDW_TRACE("Couldn't find hwnd for top-level hwnd={}", ( void * )hwnd);
return nullptr;
}
#endif
static std::shared_ptr<View> qtTopLevelUnderCursor_impl(Point globalPos,
const Window::List &windows,
View *rootViewBeingDragged)
{
for (auto i = windows.size() - 1; i >= 0; --i) {
const Window::Ptr &window = windows.at(i);
auto tl = window->rootView();
if (!tl->isVisible() || tl->equals(rootViewBeingDragged) || tl->isMinimized())
continue;
if (rootViewBeingDragged && rootViewBeingDragged->window()->equals(window))
continue;
if (window->geometry().contains(globalPos)) {
KDDW_TRACE("Found top-level {}", ( void * )tl.get());
return tl;
}
}
return nullptr;
}
std::shared_ptr<View> DragController::qtTopLevelUnderCursor() const
{
Point globalPos = Platform::instance()->cursorPos();
if (KDDockWidgets::isWindows()) { // So -platform offscreen on Windows doesn't use this
#if defined(KDDW_FRONTEND_QT_WINDOWS)
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()->view()->window()->handle());
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)) {
const Rect windowGeometry = tl->d->windowGeometry();
if (windowGeometry.contains(globalPos)
&& tl->viewName() != QStringLiteral("_docks_IndicatorWindow_Overlay")) {
KDDW_TRACE("Found top-level {}", ( void * )tl.get());
return tl;
}
} else {
#ifdef KDDW_FRONTEND_QTWIDGETS
if (Platform::instance()->isQtWidgets()) {
// 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->window()->winId()))) {
if (topLevel->rect().contains(topLevel->mapFromGlobal(globalPos))
&& topLevel->objectName()
!= QStringLiteral("_docks_IndicatorWindow_Overlay")) {
KDDW_TRACE("Found top-level {}", ( void * )topLevel);
return QtCommon::Platform_qt::instance()->qobjectAsView(topLevel);
}
}
}
}
}
#endif // QtWidgets A window belonging to another app is below the cursor
KDDW_TRACE("Window from another app is under cursor {}", ( void * )hwnd);
return nullptr;
}
}
#endif // KDDW_FRONTEND_QT_WINDOWS
} else if (linksToXLib() && isXCB()) {
bool ok = false;
const Window::List orderedWindows = KDDockWidgets::orderedWindows(ok);
FloatingWindow *tlwBeingDragged = m_windowBeingDragged->floatingWindow();
if (auto tl =
qtTopLevelUnderCursor_impl(globalPos, orderedWindows, tlwBeingDragged->view()))
return tl;
if (!ok) {
KDDW_TRACE("No top-level found. Some windows weren't seen by XLib");
}
} 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 *floatingWindow = m_windowBeingDragged->floatingWindow();
if (floatingWindow) {
if (auto tl = qtTopLevelUnderCursor_impl(
globalPos, DockRegistry::self()->floatingQWindows(), floatingWindow->view()))
return tl;
return qtTopLevelUnderCursor_impl(
globalPos, DockRegistry::self()->topLevels(/*excludeFloatingDocks=*/true), floatingWindow->view());
}
}
KDDW_TRACE("No top-level found");
return nullptr;
}
static DropArea *deepestDropAreaInTopLevel(std::shared_ptr<View> topLevel, Point globalPos,
const Vector<QString> &affinities)
{
const auto localPos = topLevel->mapFromGlobal(globalPos);
auto view = topLevel->childViewAt(localPos);
while (view) {
if (auto dt = view->asDropAreaController()) {
if (DockRegistry::self()->affinitiesMatch(dt->affinities(), affinities))
return dt;
}
view = view->parentView();
}
return nullptr;
}
DropArea *DragController::dropAreaUnderCursor() const
{
if (!m_windowBeingDragged)
return nullptr;
std::shared_ptr<View> topLevel = qtTopLevelUnderCursor();
if (!topLevel) {
KDDW_DEBUG("DragController::dropAreaUnderCursor: No drop area under cursor");
return nullptr;
}
const Vector<QString> affinities = m_windowBeingDragged->floatingWindow()->affinities();
if (auto fw = topLevel->asFloatingWindowController()) {
if (DockRegistry::self()->affinitiesMatch(fw->affinities(), affinities)) {
KDDW_DEBUG("DragController::dropAreaUnderCursor: Found drop area in floating window");
return fw->dropArea();
}
}
if (topLevel->viewName() == QStringLiteral("_docks_IndicatorWindow")) {
KDDW_ERROR("Indicator window should be hidden {} isVisible={}", ( void * )topLevel.get(), topLevel->isVisible());
assert(false);
}
if (auto dt = deepestDropAreaInTopLevel(topLevel, Platform::instance()->cursorPos(), affinities)) {
KDDW_DEBUG("DragController::dropAreaUnderCursor: Found drop area {} {}", ( void * )dt, ( void * )dt->view()->rootView().get());
return dt;
}
KDDW_DEBUG("DragController::dropAreaUnderCursor: null2");
return nullptr;
}
Draggable *DragController::draggableForView(View *view) const
{
for (auto draggable : m_draggables)
if (draggable->asView()->equals(view)) {
return draggable;
}
return nullptr;
}
bool DragController::isInQDrag() const
{
return m_inQDrag;
}