From 7cc7ae1221c4474a7b92d2bc7fad5ac7fd84a5e6 Mon Sep 17 00:00:00 2001 From: Sergio Martins Date: Thu, 30 Apr 2026 23:31:25 +0100 Subject: [PATCH] wip: wayland: Add xdg-toplevel-drag support Test with: ./bin/qtwidgets_dockwidgets -platform wayland --wayland-toplevel-drag Known bugs: - There's some offset, window doesn't snap to where mouse cursor is - When releasing mouse, outside of a drop-area, then next drag wont start (needs a second mouse click) --- code.code-workspace | 15 +++- examples/dockwidgets/main.cpp | 12 +++ src/Config.cpp | 2 +- src/Config.h | 1 + src/core/DragController.cpp | 9 ++- src/core/DragController_p.h | 1 + src/core/Utils_p.h | 12 ++- src/qtcommon/DragControllerWayland_p.cpp | 80 +++++++++++++++++++ src/qtcommon/DragControllerWayland_p.h | 9 +++ .../views/ClassicIndicatorsWindow.cpp | 1 - 10 files changed, 133 insertions(+), 9 deletions(-) diff --git a/code.code-workspace b/code.code-workspace index b62658439..7c4117978 100644 --- a/code.code-workspace +++ b/code.code-workspace @@ -66,10 +66,14 @@ "request": "launch", "program": "${command:cmake.launchTargetPath}", "args": [ - "tst_affinityWithPersistentCentralGroup" + "--wayland-toplevel-drag", + "-platform", + "wayland" ], "cwd": "${command:cmake.buildDirectory}", - "env": {} + "env": { + "XDG_RUNTIME_DIR": "/run/user/1000" + } }, { "name": "Current Target (GDB)", @@ -77,8 +81,13 @@ "request": "launch", "program": "${command:cmake.launchTargetPath}", "args": [ - "tst_affinityWithPersistentCentralGroup" + "--wayland-toplevel-drag", + "-platform", + "wayland" ], + "env": { + "XDG_RUNTIME_DIR": "/run/user/1000" + }, "cwd": "${command:cmake.buildDirectory}", "MIMode": "gdb", "setupCommands": [ diff --git a/examples/dockwidgets/main.cpp b/examples/dockwidgets/main.cpp index b4feb665a..030529082 100644 --- a/examples/dockwidgets/main.cpp +++ b/examples/dockwidgets/main.cpp @@ -236,6 +236,13 @@ int main(int argc, char **argv) QCoreApplication::translate("main", "Ctrl key toggles drop indicators")); parser.addOption(ctrlTogglesDropIndicators); +#ifdef Q_OS_LINUX + QCommandLineOption waylandToplevelDrag( + "wayland-toplevel-drag", + QCoreApplication::translate("main", "Use xdg-toplevel-drag on Wayland so the actual window drags instead of a pixmap")); + parser.addOption(waylandToplevelDrag); +#endif + #if defined(DOCKS_DEVELOPER_MODE) parser.addOption(centralFrame); @@ -369,6 +376,11 @@ int main(int argc, char **argv) if (parser.isSet(doubleClickMaximize)) flags |= KDDockWidgets::Config::Flag_DoubleClickMaximizes; +#ifdef Q_OS_LINUX + if (parser.isSet(waylandToplevelDrag)) + flags |= KDDockWidgets::Config::Flag_WaylandToplevelDrag; +#endif + if (parser.isSet(incompatibleMainWindows) && !parser.isSet(multipleMainWindows)) { qWarning() << "Error: Argument -i requires -m"; return 1; diff --git a/src/Config.cpp b/src/Config.cpp index d92ef7423..9069a554c 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -361,7 +361,7 @@ void Config::Private::fixFlags() } #if defined(Q_OS_LINUX) - if (KDDockWidgets::isWayland()) { + if (KDDockWidgets::isWayland() && !(m_flags & Config::Flag_WaylandToplevelDrag)) { // Native title bar is forced on Wayland. Needed for moving the window. // The inner KDDW title bar is used for DnD. m_flags |= Flag_NativeTitleBar; diff --git a/src/Config.h b/src/Config.h index d0b12cbda..49e6cfec9 100644 --- a/src/Config.h +++ b/src/Config.h @@ -132,6 +132,7 @@ class DOCKS_EXPORT Config ///< right clicking on the tab area Flag_AutoHideAsTabGroups = 0x100000, ///< If tabbed dockwidgets are sent to/from sidebar, they're all sent and restored together Flag_DisableDoubleClick = 0x200000, ///< Do not maximize of float if a title or tab is double-clicked. + Flag_WaylandToplevelDrag = 0x400000, ///< EXPERIMENTAL. Uses xdg-toplevel-drag on Wayland so the actual window drags instead of a pixmap. Requires compositor support. Flag_Default = Flag_AeroSnapWithClientDecos ///< The defaults }; Q_DECLARE_FLAGS(Flags, Flag) diff --git a/src/core/DragController.cpp b/src/core/DragController.cpp index 44d35a7e3..42c592273 100644 --- a/src/core/DragController.cpp +++ b/src/core/DragController.cpp @@ -596,10 +596,13 @@ namespace { StateDragging *createDraggingState(DragController *parent) { #ifdef KDDW_FRONTEND_QT - return isWayland() ? new StateDraggingWayland(parent) : new StateDragging(parent); -#else - return new StateDragging(parent); + if (isWayland()) { + if (Config::self().flags() & Config::Flag_WaylandToplevelDrag) + return new StateDraggingWaylandToplevel(parent); + return new StateDraggingWayland(parent); + } #endif + return new StateDragging(parent); } } diff --git a/src/core/DragController_p.h b/src/core/DragController_p.h index 66ce5ec2d..c651692b3 100644 --- a/src/core/DragController_p.h +++ b/src/core/DragController_p.h @@ -153,6 +153,7 @@ class DOCKS_EXPORT_FOR_UNIT_TESTS DragController : public MinimalStateMachine, p friend class StateInternalMDIDragging; friend class StateDropped; friend class StateDraggingWayland; + friend class StateDraggingWaylandToplevel; friend class ::TestQtWidgets; explicit DragController(Core::Object * = nullptr); diff --git a/src/core/Utils_p.h b/src/core/Utils_p.h index 4d66a0d1f..2b4c79859 100644 --- a/src/core/Utils_p.h +++ b/src/core/Utils_p.h @@ -30,6 +30,16 @@ inline bool isWayland() return Core::Platform::instance()->displayType() == Core::Platform::DisplayType::Wayland; } +inline bool isWaylandXDGTopLevelDrag() +{ + return isWayland() && (Config::self().flags() & Config::Flag_WaylandToplevelDrag); +} + +inline bool isWaylandNonXDGTopLevelDrag() +{ + return isWayland() && !isWaylandXDGTopLevelDrag(); +} + inline bool isOffscreen() { return Core::Platform::instance()->displayType() == Core::Platform::DisplayType::QtOffscreen; @@ -58,7 +68,7 @@ inline bool usesNativeTitleBar() inline bool usesClientTitleBar() { if (isWayland()) { - // Wayland has both client and native title bars, due to limitations. + // Wayland always has the client title-bar, since the native one doesn't emit events when moved return true; } diff --git a/src/qtcommon/DragControllerWayland_p.cpp b/src/qtcommon/DragControllerWayland_p.cpp index ff0a2716b..8310c44fa 100644 --- a/src/qtcommon/DragControllerWayland_p.cpp +++ b/src/qtcommon/DragControllerWayland_p.cpp @@ -14,6 +14,12 @@ #include "core/ScopedValueRollback_p.h" #include "kddockwidgets/core/Platform.h" #include "kddockwidgets/core/DropArea.h" +#include "kddockwidgets/core/FloatingWindow.h" +#include "qtcommon/Window_p.h" +#include "Config.h" + +#include +#include using namespace KDDockWidgets::Core; @@ -127,3 +133,77 @@ bool StateDraggingWayland::handleDragMove(DragMoveEvent *ev, DropArea *dropArea, return true; } + +StateDraggingWaylandToplevel::StateDraggingWaylandToplevel(DragController *parent) + : StateDraggingWayland(parent) +{ +} + +StateDraggingWaylandToplevel::~StateDraggingWaylandToplevel() +{ +} + +void StateDraggingWaylandToplevel::onEntry() +{ + KDDW_DEBUG("StateDraggingWaylandToplevel entered"); + + if (DragController::instance()->m_inQDrag) { + KDDW_ERROR("Impossible!"); + return; + } + + ScopedValueRollback guard(DragController::instance()->m_inQDrag, true); + + q->m_windowBeingDragged = q->m_draggable->makeWindow(); + if (!q->m_windowBeingDragged) { + KDDW_ERROR("StateDraggingWaylandToplevel: Failed to create window"); + q->dragCanceled.emit(); + return; + } + + auto fw = q->m_windowBeingDragged->floatingWindow(); + if (!fw) { + KDDW_ERROR("StateDraggingWaylandToplevel: No FloatingWindow"); + q->dragCanceled.emit(); + return; + } + + auto window = fw->view()->window(); + if (!window) { + KDDW_ERROR("StateDraggingWaylandToplevel: No window"); + q->dragCanceled.emit(); + return; + } + + auto qtWindow = static_cast(window.get())->qtWindow(); + if (!qtWindow) { + KDDW_ERROR("StateDraggingWaylandToplevel: No QWindow"); + q->dragCanceled.emit(); + return; + } + + auto serialize = [](const auto &object) { + QByteArray data; + QDataStream dataStream(&data, QIODevice::WriteOnly); + dataStream << object; + return data; + }; + + auto mimeData = new WaylandMimeData(); + mimeData->setData(QStringLiteral("application/x-qt-mainwindowdrag-window"), + serialize(reinterpret_cast(qtWindow))); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + mimeData->setData(QStringLiteral("application/x-qt-mainwindowdrag-position"), + serialize(QCursor::pos())); + + Drag drag(this); + drag.setMimeData(mimeData); + + Platform::instance()->installGlobalEventFilter(q); + KDDW_DEBUG("Started QDrag (toplevel)"); + const Qt::DropAction result = drag.exec(); + KDDW_DEBUG("QDrag (toplevel) finished with result={}", int(result)); + + Platform::instance()->removeGlobalEventFilter(q); + if (result == Qt::IgnoreAction) + q->dragCanceled.emit(); +} diff --git a/src/qtcommon/DragControllerWayland_p.h b/src/qtcommon/DragControllerWayland_p.h index 76a4c55ef..69420b133 100644 --- a/src/qtcommon/DragControllerWayland_p.h +++ b/src/qtcommon/DragControllerWayland_p.h @@ -44,5 +44,14 @@ class WaylandMimeData : public QMimeData public: }; +class StateDraggingWaylandToplevel : public StateDraggingWayland +{ + Q_OBJECT +public: + explicit StateDraggingWaylandToplevel(DragController *parent); + ~StateDraggingWaylandToplevel() override; + void onEntry() override; +}; + } } diff --git a/src/qtwidgets/views/ClassicIndicatorsWindow.cpp b/src/qtwidgets/views/ClassicIndicatorsWindow.cpp index 7ea64ef23..3bc4d5a92 100644 --- a/src/qtwidgets/views/ClassicIndicatorsWindow.cpp +++ b/src/qtwidgets/views/ClassicIndicatorsWindow.cpp @@ -152,7 +152,6 @@ QString Indicator::iconFileName(bool active) const static QWidget *parentForIndicatorWindow(ClassicDropIndicatorOverlay *classicIndicators_) { // On Wayland it can't be a top-level, as we have no way of positioning it - return isWayland() ? QtCommon::View_qt::asQWidget(classicIndicators_->view()) : nullptr; }