581 lines
18 KiB
C++
581 lines
18 KiB
C++
/*
|
|
This file is part of KDDockWidgets.
|
|
|
|
SPDX-FileCopyrightText: 2019-2020 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 "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 <QMouseEvent>
|
|
#include <QApplication>
|
|
#include <QCursor>
|
|
#include <QWindow>
|
|
|
|
#if defined(Q_OS_WIN)
|
|
# include <QWindow>
|
|
# include <Windows.h>
|
|
#endif
|
|
|
|
using namespace KDDockWidgets;
|
|
|
|
namespace KDDockWidgets {
|
|
///@brief Custom mouse grabber, as platforms like wayland 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()
|
|
{
|
|
m_target = nullptr;
|
|
qApp->removeEventFilter(this);
|
|
}
|
|
|
|
bool eventFilter(QObject *, QEvent *ev) override
|
|
{
|
|
if (m_reentrancyGuard || !m_target)
|
|
return false;
|
|
|
|
if (ev->type() == QEvent::MouseButtonPress ||
|
|
ev->type() == QEvent::MouseButtonRelease ||
|
|
ev->type() == QEvent::MouseMove ||
|
|
ev->type() == QEvent::NonClientAreaMouseButtonPress ||
|
|
ev->type() == QEvent::NonClientAreaMouseButtonRelease ||
|
|
ev->type() == QEvent::NonClientAreaMouseMove) {
|
|
m_reentrancyGuard = true;
|
|
qApp->sendEvent(m_target, ev);
|
|
m_reentrancyGuard = false;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool m_reentrancyGuard = false;
|
|
QPointer<QWidgetOrQuick> m_target;
|
|
};
|
|
|
|
FallbackMouseGrabber::~FallbackMouseGrabber() {}
|
|
|
|
}
|
|
|
|
StateBase::StateBase(DragController *parent)
|
|
: QState(parent)
|
|
, q(parent)
|
|
{
|
|
}
|
|
|
|
StateBase::~StateBase() = default;
|
|
|
|
StateNone::StateNone(DragController *parent)
|
|
: StateBase(parent)
|
|
{
|
|
}
|
|
|
|
void StateNone::onEntry(QEvent *)
|
|
{
|
|
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(QEvent *)
|
|
{
|
|
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;
|
|
}
|
|
|
|
StateDragging::StateDragging(DragController *parent)
|
|
: StateBase(parent)
|
|
{
|
|
}
|
|
|
|
StateDragging::~StateDragging() = default;
|
|
|
|
void StateDragging::onEntry(QEvent *)
|
|
{
|
|
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) {
|
|
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(floatingWindow, 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(fw, globalPos);
|
|
}
|
|
|
|
q->m_currentDropArea = dropArea;
|
|
|
|
return true;
|
|
}
|
|
|
|
DragController::DragController(QObject *)
|
|
{
|
|
qCDebug(creation) << "DragController()";
|
|
|
|
auto stateNone = new StateNone(this);
|
|
auto statepreDrag = new StatePreDrag(this);
|
|
auto stateDragging = 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);
|
|
|
|
setInitialState(stateNone);
|
|
start();
|
|
}
|
|
|
|
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 (m_fallbackMouseGrabber) {
|
|
m_fallbackMouseGrabber->grabMouse(target);
|
|
} else {
|
|
target->grabMouse();
|
|
}
|
|
}
|
|
|
|
void DragController::releaseMouse(QWidgetOrQuick *target)
|
|
{
|
|
if (m_fallbackMouseGrabber) {
|
|
m_fallbackMouseGrabber->releaseMouse();
|
|
} else {
|
|
target->releaseMouse();
|
|
}
|
|
}
|
|
|
|
static QMouseEvent *mouseEvent(QEvent *e)
|
|
{
|
|
switch (e->type()) {
|
|
case QEvent::MouseButtonPress:
|
|
case QEvent::MouseButtonRelease:
|
|
case QEvent::MouseMove:
|
|
case QEvent::NonClientAreaMouseButtonPress:
|
|
case QEvent::NonClientAreaMouseButtonRelease:
|
|
case QEvent::NonClientAreaMouseMove:
|
|
return static_cast<QMouseEvent *>(e);
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
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 QStateMachine::eventFilter(o, e);
|
|
}
|
|
|
|
QMouseEvent *me = mouseEvent(e);
|
|
if (!me)
|
|
return QStateMachine::eventFilter(o, e);
|
|
|
|
auto w = qobject_cast<QWidget*>(o);
|
|
if (!w)
|
|
return QStateMachine::eventFilter(o, e);
|
|
|
|
qCDebug(mouseevents) << "DragController::eventFilter e=" << e->type() << "; o=" << o;
|
|
|
|
switch (e->type()) {
|
|
case QEvent::NonClientAreaMouseButtonPress: {
|
|
if (auto fw = qobject_cast<FloatingWindow*>(o)) {
|
|
if (fw->isInDragArea(Qt5Qt6Compat::eventGlobalPos(me))) {
|
|
m_nonClientDrag = true;
|
|
return activeState()->handleMouseButtonPress(draggableForQObject(o), Qt5Qt6Compat::eventGlobalPos(me), me->pos());
|
|
}
|
|
}
|
|
return QStateMachine::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())
|
|
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));
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return QStateMachine::eventFilter(o, e);
|
|
}
|
|
|
|
StateBase *DragController::activeState() const
|
|
{
|
|
auto set = configuration();
|
|
if (set.isEmpty())
|
|
return nullptr;
|
|
return static_cast<StateBase *>(*(set.begin()));
|
|
}
|
|
|
|
#if defined(Q_OS_WIN)
|
|
static QWidget *qtTopLevelForHWND(HWND hwnd)
|
|
{
|
|
auto topLevels = qApp->topLevelWidgets();
|
|
for (auto topLevel : topLevels) {
|
|
if (hwnd == (HWND)topLevel->winId())
|
|
return topLevel;
|
|
}
|
|
|
|
qCDebug(toplevels) << Q_FUNC_INFO << "Couldn't find hwnd for top-level" << hwnd;
|
|
return nullptr;
|
|
}
|
|
#endif
|
|
template <typename T>
|
|
static QWidget* qtTopLevelUnderCursor_impl(QPoint globalPos, const QVector<T> &topLevels, T windowBeingDragged)
|
|
{
|
|
for (int i = topLevels.size() -1; i >= 0; --i) {
|
|
auto tl = topLevels.at(i);
|
|
if (!tl->isVisible() || tl == windowBeingDragged || tl->isMinimized())
|
|
continue;
|
|
|
|
if (windowBeingDragged && windowBeingDragged->window() == tl->window())
|
|
continue;
|
|
|
|
if (tl->geometry().contains(globalPos)) {
|
|
qCDebug(toplevels) << Q_FUNC_INFO << "Found top-level" << tl;
|
|
return tl;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
QWidgetOrQuick *DragController::qtTopLevelUnderCursor() const
|
|
{
|
|
#ifdef KDDOCKWIDGETS_QTWIDGETS
|
|
|
|
QPoint globalPos = QCursor::pos();
|
|
|
|
if (qApp->platformName() == QLatin1String("windows")) { // So -platform offscreen on Windows doesn't use this
|
|
# if defined(Q_OS_WIN)
|
|
auto topLevels = qApp->topLevelWidgets();
|
|
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 {
|
|
// Maybe it's embedded in a QWinWidget:
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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
|
|
} 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()->nestedwindows(), tlwBeingDragged))
|
|
return tl;
|
|
|
|
return qtTopLevelUnderCursor_impl<QWidget*>(globalPos,
|
|
DockRegistry::self()->topLevels(/*excludeFloating=*/true),
|
|
tlwBeingDragged);
|
|
}
|
|
#else
|
|
// QtQuick:
|
|
qWarning() << Q_FUNC_INFO << "Implement me!";
|
|
#endif
|
|
|
|
qCDebug(toplevels) << Q_FUNC_INFO << "No top-level found";
|
|
return nullptr;
|
|
}
|
|
|
|
static DropArea* deepestDropAreaInTopLevel(QWidgetOrQuick *topLevel, QPoint globalPos,
|
|
const QStringList &affinities)
|
|
{
|
|
auto w = topLevel->childAt(topLevel->mapFromGlobal(globalPos));
|
|
while (w) {
|
|
if (auto dt = qobject_cast<DropArea *>(w)) {
|
|
if (DockRegistry::self()->affinitiesMatch(dt->affinities(), affinities))
|
|
return dt;
|
|
}
|
|
w = w->parentWidget();
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
DropArea *DragController::dropAreaUnderCursor() const
|
|
{
|
|
auto topLevel = qtTopLevelUnderCursor();
|
|
if (!topLevel)
|
|
return nullptr;
|
|
|
|
const QStringList affinities = m_windowBeingDragged->floatingWindow()->affinities();
|
|
|
|
if (auto dt = qobject_cast<DropArea *>(topLevel)) {
|
|
if (DockRegistry::self()->affinitiesMatch(dt->affinities(), affinities))
|
|
return dt;
|
|
}
|
|
|
|
if (auto fw = qobject_cast<FloatingWindow *>(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 dock = qobject_cast<DockWidgetBase *>(topLevel)) {
|
|
FloatingWindow *fw = dock->morphIntoFloatingWindow();
|
|
m_windowBeingDragged->floatingWindow()->raise();
|
|
if (DockRegistry::self()->affinitiesMatch(fw->affinities(), affinities))
|
|
return fw->dropArea();
|
|
}
|
|
|
|
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;
|
|
}
|