Skip to content

Commit f2db7ca

Browse files
mliberty1Agent 01claude
authored
Wayland: dock widget drag-and-drop with in-window preview and native window drag (#844)
Qt Advanced Docking System could not dock on Wayland: a client cannot position top-level windows and the global cursor position is unreliable, so the mouse-tracked floating-widget drag never worked. This adds Wayland docking by combining two mechanisms, selected by whether the drag stays inside its source window. Native cross-window drag (xdg_toplevel_drag_v1): - CFloatingDockContainer::startPlatformDrag() runs a compositor-driven drag via QDrag with the Qt main-window-drag MIME types, so a floating window is moved by the compositor and can be dropped onto another window. - Drop targets handle the drag in CDockContainerWidget dragEnter/Move/Leave/drop events, with a recorded drop-candidate fallback for compositors that do not deliver a drop event over the dragged window. - The drop overlays (CDockOverlay) and the in-window drag preview are rendered as child widgets of the relevant top-level window, because a Qt::Tool top-level cannot be positioned in screen coordinates on Wayland. - Floating containers use a native window with no parent on Wayland, and the DockManager stays-on-top emulation that would recreate window surfaces is skipped. In-window preview, native only when leaving the window: - While the cursor stays inside the source top-level window, a drag uses the familiar in-window CFloatingDragPreview plus drop overlays (no new window), driven by the reliable event-supplied position from the grabbing tab or title bar and confined to the source container. - When the cursor leaves the window, the preview is torn down and a real CFloatingDockContainer is created and handed to startPlatformDrag() while the press's implicit pointer grab is still held - a single continuous gesture. This covers both the tab (CDockWidgetTab) and title-bar (CDockAreaTitleBar) drag paths and applies to rearranging widgets inside floating windows too. - The attach offset that places the new window under the cursor is only honored while the toplevel is unmapped, so startPlatformDrag() runs QDrag::exec() without waiting for exposure, and the caller passes the surface-local grab offset explicitly - shifted by the window frame top and left margins - instead of deriving it from the unmapped window's (meaningless) geometry, so the grabbed content point stays under the cursor on both axes. Style sheets and overlay lifetime: - A Wayland floating container has no parent widget, so it does not inherit the dock manager's effective style sheet through the widget hierarchy. The style sheets along the dock manager parent chain are applied explicitly when the window is created, and CDockManager re-applies them on QEvent::StyleChange so a floating window keeps matching the docked content when the style sheet changes at runtime (an application-wide qApp style sheet is still applied by Qt automatically). - The drop overlays are reparented into the top-level window they are shown over and reparented back to the dock manager's window when hidden, so a dock-manager-owned overlay is never left as a child of a transient floating window that gets destroyed. Saved layouts restore the docked arrangement, floating-window sizes and their maximized/normal state, but not floating-window or main-window positions: Wayland does not let a client position its own top-level windows, so the compositor decides where restored windows appear. Non-Wayland platforms (X11, Windows, macOS) are unaffected: every behavioral change is gated on internal::isWayland(), and the shared drag-preview code keeps using QCursor::pos() and the existing window positioning off Wayland. Co-authored-by: Agent 01 <agent@jetperch.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 4124dde commit f2db7ca

16 files changed

Lines changed: 1212 additions & 125 deletions

README.md

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ know it from Visual Studio.
183183
- [Windows](#windows)
184184
- [macOS](#macos)
185185
- [Linux](#linux)
186+
- [Wayland](#wayland)
186187
- [Build](#build)
187188
- [Qt5 on Ubuntu 18.04 or 20.04](#qt5-on-ubuntu-1804-or-2004)
188189
- [Qt5 on Ubuntu 22.04](#qt5-on-ubuntu-2204)
@@ -408,19 +409,39 @@ the library switches to `QWidget` based title bars.
408409

409410
- **Kubuntu 18.04 and 19.10** - uses KWin - no native title bars
410411
- **Ubuntu 18.04, 19.10 and 20.04** - native title bars are supported
411-
- **Ubuntu 22.04** - uses Wayland -> no native title bars
412+
- **Ubuntu 22.04 and later** - uses Wayland -> native title bars (see [Wayland](#wayland) below)
412413

413414
There are some requirements for the Linux distribution that have to be met:
414415

415-
- an X server that supports ARGB visuals and a compositing window manager. This is required to display the translucent dock overlays ([https://doc.qt.io/qt-5/qwidget.html#creating-translucent-windows](https://doc.qt.io/qt-5/qwidget.html#creating-translucent-windows)). If your Linux distribution does not support this, or if you disable this feature, you will very likely see issue [#95](https://github.com/githubuser0xFFFF/Qt-Advanced-Docking-System/issues/95).
416-
- Wayland is not properly supported by Qt yet. If you use Wayland, then you should set the session type to x11: `XDG_SESSION_TYPE=x11 ./AdvancedDockingSystemDemo`. You will find more details about this in issue [#288](https://github.com/githubuser0xFFFF/Qt-Advanced-Docking-System/issues/288).
416+
- an X server that supports ARGB visuals and a compositing window manager. This is required to display the translucent dock overlays ([https://doc.qt.io/qt-5/qwidget.html#creating-translucent-windows](https://doc.qt.io/qt-5/qwidget.html#creating-translucent-windows)). If your Linux distribution does not support this, or if you disable this feature, you will very likely see issue [#95](https://github.com/githubuser0xFFFF/Qt-Advanced-Docking-System/issues/95). On Wayland the dock overlays are rendered as child widgets and this requirement does not apply.
417417

418418
Screenshot Kubuntu:
419419
![Advanced Docking on Kubuntu Linux](doc/linux_kubuntu_1804.png)
420420

421421
Screenshot Ubuntu:
422422
![Advanced Docking on Ubuntu Linux](doc/linux_ubuntu_1910.png)
423423

424+
#### Wayland
425+
426+
Since Qt 6.6.3 docking is supported on Wayland. Earlier Qt versions do not implement the `xdg_toplevel_drag_v1` protocol that is required to drag a floating window with the cursor, so on those versions you should still set the session type to X11 (XWayland): `XDG_SESSION_TYPE=x11 ./AdvancedDockingSystemDemo`. You will find more details in issues [#288](https://github.com/githubuser0xFFFF/Qt-Advanced-Docking-System/issues/288) and [#714](https://github.com/githubuser0xFFFF/Qt-Advanced-Docking-System/issues/714).
427+
428+
Wayland does not allow a client to move its own top level windows in screen coordinates or to query the global cursor position, so docking is implemented differently than on the other platforms. This results in a few behavioral differences on Wayland:
429+
430+
- Floating dock containers always use native window decorations (the custom
431+
`QWidget` title bar is not available), because the custom title bar cannot
432+
move the window.
433+
- Undocking and re-docking use a compositor driven drag (the floating window
434+
itself follows the cursor) instead of the translucent drag preview.
435+
- The compositor controls the window stacking, so floating windows are not
436+
forced to stay on top of the main window.
437+
- Saved layouts restore the docked arrangement, the floating-window sizes and
438+
their maximized/normal state, but **not** the on-screen position of floating
439+
windows (the same applies to the application's own main window). Wayland does
440+
not let a client position its top-level windows, so the compositor decides
441+
where restored windows appear. Restoring positions requires compositor side
442+
session management (the staging `xx-session-management-v1` protocol), which is
443+
not yet available through Qt.
444+
424445
## Build
425446

426447
The Linux build requires private header files. Make sure that they are installed.

src/DockAreaTitleBar.cpp

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,14 @@ struct DockAreaTitleBarPrivate
158158
*/
159159
IFloatingWidget* makeAreaFloating(const QPoint& Offset, eDragState DragState);
160160

161+
/**
162+
* Wayland hybrid drag: drive the in-window drag preview from reliable event
163+
* coordinates and, when the cursor leaves the source top-level window,
164+
* convert the in-window drag into a native compositor platform drag
165+
* (mirrors CDockWidgetTab).
166+
*/
167+
void waylandPreviewMove(QMouseEvent* ev);
168+
161169
/**
162170
* Helper function to create and initialize the menu entries for
163171
* the "Auto Hide Group To..." menu
@@ -291,6 +299,10 @@ IFloatingWidget* DockAreaTitleBarPrivate::makeAreaFloating(const QPoint& Offset,
291299
{
292300
QSize Size = DockArea->size();
293301
this->DragState = DragState;
302+
// Wayland hybrid drag: start an in-window drag preview (driven by event
303+
// coordinates, confined to the source container) rather than an immediate
304+
// platform drag. The drag converts to a native compositor platform drag
305+
// only when the cursor leaves the source window (see waylandPreviewMove()).
294306
bool CreateFloatingDockContainer = (DraggingFloatingWidget != DragState);
295307
CFloatingDockContainer* FloatingDockContainer = nullptr;
296308
IFloatingWidget* FloatingWidget;
@@ -309,10 +321,17 @@ IFloatingWidget* DockAreaTitleBarPrivate::makeAreaFloating(const QPoint& Offset,
309321
{
310322
this->DragState = DraggingInactive;
311323
});
324+
if (internal::isWayland())
325+
{
326+
// Confine the in-window preview to the source container; its
327+
// position and the drop overlays are driven by event coordinates
328+
// delivered to the title bar (see waylandPreviewMove()).
329+
w->setSourceContainer(DockArea->dockContainer());
330+
}
312331
FloatingWidget = w;
313332
}
314333

315-
FloatingWidget->startFloating(Offset, Size, DragState, nullptr);
334+
FloatingWidget->startFloating(Offset, Size, DragState, nullptr);
316335
if (FloatingDockContainer)
317336
{
318337
auto TopLevelDockWidget = FloatingDockContainer->topLevelDockWidget();
@@ -334,10 +353,49 @@ void DockAreaTitleBarPrivate::startFloating(const QPoint& Offset)
334353
DockArea->autoHideDockContainer()->hide();
335354
}
336355
FloatingWidget = makeAreaFloating(Offset, DraggingFloatingWidget);
356+
// On Wayland the hybrid drag now starts an in-window preview (it no longer
357+
// blocks on a platform drag here), so the drag-start event is reported like
358+
// on every other platform.
337359
qApp->postEvent(DockArea, new QEvent((QEvent::Type)internal::DockedWidgetDragStartEvent));
338360
}
339361

340362

363+
//============================================================================
364+
void DockAreaTitleBarPrivate::waylandPreviewMove(QMouseEvent* ev)
365+
{
366+
const QPoint GlobalPos = internal::globalPositionOf(ev);
367+
368+
// FloatingWidget is a CFloatingDragPreview during the in-window phase
369+
// (DraggingFloatingWidget state on Wayland; see makeAreaFloating()). While
370+
// the cursor stays inside the source window the preview just follows it.
371+
auto Preview = static_cast<CFloatingDragPreview*>(FloatingWidget);
372+
if (CFloatingDockContainer::waylandMoveOrLeaveInWindowPreview(
373+
Preview, _this->window(), GlobalPos))
374+
{
375+
return;
376+
}
377+
378+
// Boundary cross: the preview was torn down, convert the gesture into a
379+
// native compositor platform drag of a freshly created floating widget.
380+
FloatingWidget = nullptr;
381+
DragState = DraggingInactive;
382+
383+
if (DockArea->autoHideDockContainer())
384+
{
385+
DockArea->autoHideDockContainer()->cleanupAndDelete();
386+
}
387+
auto FloatingDockContainer = new CFloatingDockContainer(DockArea);
388+
auto TopLevelDockWidget = FloatingDockContainer->topLevelDockWidget();
389+
if (TopLevelDockWidget)
390+
{
391+
TopLevelDockWidget->emitTopLevelChanged(true);
392+
}
393+
CFloatingDockContainer::startPlatformDragForFloatingWidget(
394+
FloatingDockContainer, DragStartMousePos, DockArea->size(),
395+
_this->mapToGlobal(DragStartMousePos), _this);
396+
}
397+
398+
341399
//============================================================================
342400
CDockAreaTitleBar::CDockAreaTitleBar(CDockAreaWidget* parent) :
343401
QFrame(parent),
@@ -691,17 +749,41 @@ void CDockAreaTitleBar::mouseMoveEvent(QMouseEvent* ev)
691749
// move floating window
692750
if (d->isDraggingState(DraggingFloatingWidget))
693751
{
694-
d->FloatingWidget->moveFloating();
752+
if (internal::isWayland())
753+
{
754+
// Wayland hybrid drag: in-window preview until the cursor leaves
755+
// the source window, then convert to a native platform drag.
756+
d->waylandPreviewMove(ev);
757+
}
758+
else
759+
{
760+
d->FloatingWidget->moveFloating();
761+
}
695762
return;
696763
}
697764

698765
// If this is the last dock area in a floating dock container it does not make
699766
// sense to move it to a new floating widget and leave this one
700767
// empty
701768
if (d->DockArea->dockContainer()->isFloating()
702-
&& d->DockArea->dockContainer()->visibleDockAreaCount() == 1
769+
&& d->DockArea->dockContainer()->visibleDockAreaCount() == 1
703770
&& !d->DockArea->isAutoHide())
704771
{
772+
// On Wayland, dragging the title bar of the last dock area of a
773+
// floating widget drags the existing floating widget, so the user
774+
// can dock it into another container
775+
int DragDistanceWayland = (d->DragStartMousePos - ev->pos()).manhattanLength();
776+
if (internal::isWayland()
777+
&& DragDistanceWayland >= CDockManager::startDragDistance())
778+
{
779+
auto FloatingContainer = d->DockArea->dockContainer()->floatingWidget();
780+
if (FloatingContainer)
781+
{
782+
d->DragState = DraggingInactive;
783+
CFloatingDockContainer::startPlatformDrag(FloatingContainer,
784+
mapToGlobal(d->DragStartMousePos), this);
785+
}
786+
}
705787
return;
706788
}
707789

src/DockAreaWidget.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -890,7 +890,12 @@ void CDockAreaWidget::updateTitleBarVisibility()
890890
if (!CDockManager::testConfigFlag(CDockManager::AlwaysShowTabs))
891891
{
892892
bool Hidden = false;
893-
if (!IsAutoHide) // Titlebar must always be visible when auto hidden so it can be dragged
893+
// Wayland: the title bar of a floating container must always stay
894+
// visible because it is the only handle that can drag the dock
895+
// widget back into a dock container. The window decoration is owned
896+
// by the compositor and only moves the window
897+
bool IsWaylandFloating = internal::isWayland() && Container->isFloating();
898+
if (!IsAutoHide && !IsWaylandFloating) // Titlebar must always be visible when auto hidden so it can be dragged
894899
{
895900
if (Container->isFloating() || CDockManager::testConfigFlag(CDockManager::HideSingleCentralWidgetTitleBar))
896901
{

0 commit comments

Comments
 (0)