diff --git a/README.md b/README.md index 0ba30937..660a1bd8 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,7 @@ know it from Visual Studio. - [Windows](#windows) - [macOS](#macos) - [Linux](#linux) + - [Wayland](#wayland) - [Build](#build) - [Qt5 on Ubuntu 18.04 or 20.04](#qt5-on-ubuntu-1804-or-2004) - [Qt5 on Ubuntu 22.04](#qt5-on-ubuntu-2204) @@ -408,12 +409,11 @@ the library switches to `QWidget` based title bars. - **Kubuntu 18.04 and 19.10** - uses KWin - no native title bars - **Ubuntu 18.04, 19.10 and 20.04** - native title bars are supported -- **Ubuntu 22.04** - uses Wayland -> no native title bars +- **Ubuntu 22.04 and later** - uses Wayland -> native title bars (see [Wayland](#wayland) below) There are some requirements for the Linux distribution that have to be met: -- 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). -- 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). +- 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. Screenshot Kubuntu: ![Advanced Docking on Kubuntu Linux](doc/linux_kubuntu_1804.png) @@ -421,6 +421,27 @@ Screenshot Kubuntu: Screenshot Ubuntu: ![Advanced Docking on Ubuntu Linux](doc/linux_ubuntu_1910.png) +#### Wayland + +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). + +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: + +- Floating dock containers always use native window decorations (the custom + `QWidget` title bar is not available), because the custom title bar cannot + move the window. +- Undocking and re-docking use a compositor driven drag (the floating window + itself follows the cursor) instead of the translucent drag preview. +- The compositor controls the window stacking, so floating windows are not + forced to stay on top of the main window. +- Saved layouts restore the docked arrangement, the floating-window sizes and + their maximized/normal state, but **not** the on-screen position of floating + windows (the same applies to the application's own main window). Wayland does + not let a client position its top-level windows, so the compositor decides + where restored windows appear. Restoring positions requires compositor side + session management (the staging `xx-session-management-v1` protocol), which is + not yet available through Qt. + ## Build The Linux build requires private header files. Make sure that they are installed. diff --git a/src/DockAreaTitleBar.cpp b/src/DockAreaTitleBar.cpp index 7dd261f8..d2e94f54 100644 --- a/src/DockAreaTitleBar.cpp +++ b/src/DockAreaTitleBar.cpp @@ -158,6 +158,14 @@ struct DockAreaTitleBarPrivate */ IFloatingWidget* makeAreaFloating(const QPoint& Offset, eDragState DragState); + /** + * Wayland hybrid drag: drive the in-window drag preview from reliable event + * coordinates and, when the cursor leaves the source top-level window, + * convert the in-window drag into a native compositor platform drag + * (mirrors CDockWidgetTab). + */ + void waylandPreviewMove(QMouseEvent* ev); + /** * Helper function to create and initialize the menu entries for * the "Auto Hide Group To..." menu @@ -291,6 +299,10 @@ IFloatingWidget* DockAreaTitleBarPrivate::makeAreaFloating(const QPoint& Offset, { QSize Size = DockArea->size(); this->DragState = DragState; + // Wayland hybrid drag: start an in-window drag preview (driven by event + // coordinates, confined to the source container) rather than an immediate + // platform drag. The drag converts to a native compositor platform drag + // only when the cursor leaves the source window (see waylandPreviewMove()). bool CreateFloatingDockContainer = (DraggingFloatingWidget != DragState); CFloatingDockContainer* FloatingDockContainer = nullptr; IFloatingWidget* FloatingWidget; @@ -309,10 +321,17 @@ IFloatingWidget* DockAreaTitleBarPrivate::makeAreaFloating(const QPoint& Offset, { this->DragState = DraggingInactive; }); + if (internal::isWayland()) + { + // Confine the in-window preview to the source container; its + // position and the drop overlays are driven by event coordinates + // delivered to the title bar (see waylandPreviewMove()). + w->setSourceContainer(DockArea->dockContainer()); + } FloatingWidget = w; } - FloatingWidget->startFloating(Offset, Size, DragState, nullptr); + FloatingWidget->startFloating(Offset, Size, DragState, nullptr); if (FloatingDockContainer) { auto TopLevelDockWidget = FloatingDockContainer->topLevelDockWidget(); @@ -334,10 +353,49 @@ void DockAreaTitleBarPrivate::startFloating(const QPoint& Offset) DockArea->autoHideDockContainer()->hide(); } FloatingWidget = makeAreaFloating(Offset, DraggingFloatingWidget); + // On Wayland the hybrid drag now starts an in-window preview (it no longer + // blocks on a platform drag here), so the drag-start event is reported like + // on every other platform. qApp->postEvent(DockArea, new QEvent((QEvent::Type)internal::DockedWidgetDragStartEvent)); } +//============================================================================ +void DockAreaTitleBarPrivate::waylandPreviewMove(QMouseEvent* ev) +{ + const QPoint GlobalPos = internal::globalPositionOf(ev); + + // FloatingWidget is a CFloatingDragPreview during the in-window phase + // (DraggingFloatingWidget state on Wayland; see makeAreaFloating()). While + // the cursor stays inside the source window the preview just follows it. + auto Preview = static_cast(FloatingWidget); + if (CFloatingDockContainer::waylandMoveOrLeaveInWindowPreview( + Preview, _this->window(), GlobalPos)) + { + return; + } + + // Boundary cross: the preview was torn down, convert the gesture into a + // native compositor platform drag of a freshly created floating widget. + FloatingWidget = nullptr; + DragState = DraggingInactive; + + if (DockArea->autoHideDockContainer()) + { + DockArea->autoHideDockContainer()->cleanupAndDelete(); + } + auto FloatingDockContainer = new CFloatingDockContainer(DockArea); + auto TopLevelDockWidget = FloatingDockContainer->topLevelDockWidget(); + if (TopLevelDockWidget) + { + TopLevelDockWidget->emitTopLevelChanged(true); + } + CFloatingDockContainer::startPlatformDragForFloatingWidget( + FloatingDockContainer, DragStartMousePos, DockArea->size(), + _this->mapToGlobal(DragStartMousePos), _this); +} + + //============================================================================ CDockAreaTitleBar::CDockAreaTitleBar(CDockAreaWidget* parent) : QFrame(parent), @@ -691,7 +749,16 @@ void CDockAreaTitleBar::mouseMoveEvent(QMouseEvent* ev) // move floating window if (d->isDraggingState(DraggingFloatingWidget)) { - d->FloatingWidget->moveFloating(); + if (internal::isWayland()) + { + // Wayland hybrid drag: in-window preview until the cursor leaves + // the source window, then convert to a native platform drag. + d->waylandPreviewMove(ev); + } + else + { + d->FloatingWidget->moveFloating(); + } return; } @@ -699,9 +766,24 @@ void CDockAreaTitleBar::mouseMoveEvent(QMouseEvent* ev) // sense to move it to a new floating widget and leave this one // empty if (d->DockArea->dockContainer()->isFloating() - && d->DockArea->dockContainer()->visibleDockAreaCount() == 1 + && d->DockArea->dockContainer()->visibleDockAreaCount() == 1 && !d->DockArea->isAutoHide()) { + // On Wayland, dragging the title bar of the last dock area of a + // floating widget drags the existing floating widget, so the user + // can dock it into another container + int DragDistanceWayland = (d->DragStartMousePos - ev->pos()).manhattanLength(); + if (internal::isWayland() + && DragDistanceWayland >= CDockManager::startDragDistance()) + { + auto FloatingContainer = d->DockArea->dockContainer()->floatingWidget(); + if (FloatingContainer) + { + d->DragState = DraggingInactive; + CFloatingDockContainer::startPlatformDrag(FloatingContainer, + mapToGlobal(d->DragStartMousePos), this); + } + } return; } diff --git a/src/DockAreaWidget.cpp b/src/DockAreaWidget.cpp index 1f76c71b..61c94dcc 100644 --- a/src/DockAreaWidget.cpp +++ b/src/DockAreaWidget.cpp @@ -890,7 +890,12 @@ void CDockAreaWidget::updateTitleBarVisibility() if (!CDockManager::testConfigFlag(CDockManager::AlwaysShowTabs)) { bool Hidden = false; - if (!IsAutoHide) // Titlebar must always be visible when auto hidden so it can be dragged + // Wayland: the title bar of a floating container must always stay + // visible because it is the only handle that can drag the dock + // widget back into a dock container. The window decoration is owned + // by the compositor and only moves the window + bool IsWaylandFloating = internal::isWayland() && Container->isFloating(); + if (!IsAutoHide && !IsWaylandFloating) // Titlebar must always be visible when auto hidden so it can be dragged { if (Container->isFloating() || CDockManager::testConfigFlag(CDockManager::HideSingleCentralWidgetTitleBar)) { diff --git a/src/DockContainerWidget.cpp b/src/DockContainerWidget.cpp index 6639275a..3d61d5c1 100644 --- a/src/DockContainerWidget.cpp +++ b/src/DockContainerWidget.cpp @@ -42,6 +42,7 @@ #include #include #include +#include #include #include "DockManager.h" @@ -186,6 +187,13 @@ class DockContainerWidgetPrivate */ void dropIntoAutoHideSideBar(CFloatingDockContainer* FloatingWidget, DockWidgetArea area); + /** + * Shows the drop overlays for the given floating widget that is dragged + * over this container by a drag and drop operation (Wayland platform + * drag) + */ + void updateDropOverlays(const QPoint& GlobalPos, CFloatingDockContainer* FloatingWidget); + /** * Creates a new tab for a widget dropped into the center of a section */ @@ -521,6 +529,80 @@ void DockContainerWidgetPrivate::dropIntoContainer(CFloatingDockContainer* Float } +//============================================================================ +void DockContainerWidgetPrivate::updateDropOverlays(const QPoint& GlobalPos, + CFloatingDockContainer* FloatingWidget) +{ + if (!DockManager) + { + return; + } + + // This container received the drag move event, so it is the drop target - + // unlike the mouse tracked dragging, there is no need to search the + // container under the cursor + bool ContentPinnable = FloatingWidget->dockContainer()->features().testFlag( + CDockWidget::DockWidgetPinnable); + CDockContainerWidget::showDropOverlays(DockManager, _this, GlobalPos, ContentPinnable); +} + + +//============================================================================ +void CDockContainerWidget::showDropOverlays(CDockManager* DockManager, + CDockContainerWidget* TopContainer, const QPoint& GlobalPos, + bool ContentPinnable) +{ + auto ContainerOverlay = DockManager->containerOverlay(); + auto DockAreaOverlay = DockManager->dockAreaOverlay(); + int VisibleDockAreas = TopContainer->visibleDockAreaCount(); + DockWidgetAreas AllowedContainerAreas = (VisibleDockAreas > 1) ? OuterDockAreas : AllDockAreas; + auto DockArea = TopContainer->dockAreaAt(GlobalPos); + // If the dock container contains only one single DockArea, then we need + // to respect the allowed areas - only the center area is relevant here because + // all other allowed areas are from the container + if (VisibleDockAreas == 1 && DockArea) + { + AllowedContainerAreas.setFlag(CenterDockWidgetArea, DockArea->allowedAreas().testFlag(CenterDockWidgetArea)); + } + + if (ContentPinnable) + { + AllowedContainerAreas |= AutoHideDockAreas; + } + + ContainerOverlay->setAllowedAreas(AllowedContainerAreas); + + DockWidgetArea ContainerArea = ContainerOverlay->showOverlay(TopContainer, GlobalPos); + ContainerOverlay->enableDropPreview(ContainerArea != InvalidDockWidgetArea); + if (DockArea && DockArea->isVisible() && VisibleDockAreas > 0) + { + DockAreaOverlay->enableDropPreview(true); + DockAreaOverlay->setAllowedAreas( + (VisibleDockAreas == 1) ? NoDockWidgetArea : DockArea->allowedAreas()); + DockWidgetArea Area = DockAreaOverlay->showOverlay(DockArea, GlobalPos); + + // A CenterDockWidgetArea for the dockAreaOverlay() indicates that + // the mouse is in the title bar. If the ContainerArea is valid + // then we ignore the dock area of the dockAreaOverlay() and disable + // the drop preview + if ((Area == CenterDockWidgetArea) + && (ContainerArea != InvalidDockWidgetArea)) + { + DockAreaOverlay->enableDropPreview(false); + ContainerOverlay->enableDropPreview(true); + } + else + { + ContainerOverlay->enableDropPreview(InvalidDockWidgetArea == Area); + } + } + else + { + DockAreaOverlay->hideOverlay(); + } +} + + //============================================================================ void DockContainerWidgetPrivate::dropIntoAutoHideSideBar(CFloatingDockContainer* FloatingWidget, DockWidgetArea area) { @@ -1425,6 +1507,14 @@ CDockContainerWidget::CDockContainerWidget(CDockManager* DockManager, QWidget *p createRootSplitter(); createSideTabBarWidgets(); } + + // On Wayland, floating widgets are docked via drag and drop events + // because the mouse tracked dragging that is used on the other platforms + // requires the global cursor position, which Wayland does not provide + if (internal::isWayland()) + { + setAcceptDrops(true); + } } @@ -1537,6 +1627,105 @@ bool CDockContainerWidget::event(QEvent *e) } +//============================================================================ +/** + * Returns the drop position of the given drop event in global coordinates + */ +static QPoint dropEventGlobalPos(QDropEvent* e, QWidget* Widget) +{ +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + return Widget->mapToGlobal(e->position().toPoint()); +#else + return Widget->mapToGlobal(e->pos()); +#endif +} + + +//============================================================================ +CFloatingDockContainer* CDockContainerWidget::floatingWidgetFromDropEvent( + QDropEvent* e, CDockManager* DockManager) +{ + auto FloatingWidget = CFloatingDockContainer::floatingWidgetFromMimeData(e->mimeData()); + if (!FloatingWidget || !DockManager + || !DockManager->floatingWidgets().contains(FloatingWidget)) + { + return nullptr; + } + + return FloatingWidget; +} + + +//============================================================================ +void CDockContainerWidget::dragEnterEvent(QDragEnterEvent* e) +{ + auto FloatingWidget = floatingWidgetFromDropEvent(e, d->DockManager); + if (!FloatingWidget || FloatingWidget->dockContainer() == this) + { + QFrame::dragEnterEvent(e); + return; + } + + e->acceptProposedAction(); +} + + +//============================================================================ +void CDockContainerWidget::dragMoveEvent(QDragMoveEvent* e) +{ + auto FloatingWidget = floatingWidgetFromDropEvent(e, d->DockManager); + if (!FloatingWidget || FloatingWidget->dockContainer() == this) + { + QFrame::dragMoveEvent(e); + return; + } + + e->acceptProposedAction(); + const QPoint GlobalPos = dropEventGlobalPos(e, this); + d->updateDropOverlays(GlobalPos, FloatingWidget); + + // Record the drop candidate so the drag source can dock the floating + // widget if the compositor does not deliver a drop event (see + // CFloatingDockContainer::startPlatformDrag) + bool ValidDropArea = + (d->DockManager->containerOverlay()->visibleDropAreaUnderCursor(GlobalPos) != InvalidDockWidgetArea) + || (d->DockManager->dockAreaOverlay()->visibleDropAreaUnderCursor(GlobalPos) != InvalidDockWidgetArea); + CFloatingDockContainer::platformDragUpdateDropCandidate(this, GlobalPos, ValidDropArea); +} + + +//============================================================================ +void CDockContainerWidget::dragLeaveEvent(QDragLeaveEvent* e) +{ + Q_UNUSED(e) + if (!d->DockManager) + { + return; + } + + d->DockManager->containerOverlay()->hideOverlay(); + d->DockManager->dockAreaOverlay()->hideOverlay(); +} + + +//============================================================================ +void CDockContainerWidget::dropEvent(QDropEvent* e) +{ + auto FloatingWidget = floatingWidgetFromDropEvent(e, d->DockManager); + if (!FloatingWidget || FloatingWidget->dockContainer() == this) + { + QFrame::dropEvent(e); + return; + } + + CFloatingDockContainer::platformDragNotifyDropHandled(); + dropFloatingWidget(FloatingWidget, dropEventGlobalPos(e, this)); + d->DockManager->containerOverlay()->hideOverlay(); + d->DockManager->dockAreaOverlay()->hideOverlay(); + e->acceptProposedAction(); +} + + //============================================================================ QList CDockContainerWidget::autoHideWidgets() const { @@ -1726,7 +1915,7 @@ void CDockContainerWidget::dropFloatingWidget(CFloatingDockContainer* FloatingWi CDockWidget* SingleDroppedDockWidget = FloatingWidget->topLevelDockWidget(); CDockWidget* SingleDockWidget = topLevelDockWidget(); auto dropArea = InvalidDockWidgetArea; - auto ContainerDropArea = d->DockManager->containerOverlay()->dropAreaUnderCursor(); + auto ContainerDropArea = d->DockManager->containerOverlay()->dropAreaUnderCursor(TargetPos); bool Dropped = false; CDockAreaWidget* DockArea = dockAreaAt(TargetPos); @@ -1735,7 +1924,7 @@ void CDockContainerWidget::dropFloatingWidget(CFloatingDockContainer* FloatingWi { auto dropOverlay = d->DockManager->dockAreaOverlay(); dropOverlay->setAllowedAreas(DockArea->allowedAreas()); - dropArea = dropOverlay->showOverlay(DockArea); + dropArea = dropOverlay->showOverlay(DockArea, TargetPos); if (ContainerDropArea != InvalidDockWidgetArea && ContainerDropArea != dropArea) { diff --git a/src/DockContainerWidget.h b/src/DockContainerWidget.h index 1667b86e..177127ce 100644 --- a/src/DockContainerWidget.h +++ b/src/DockContainerWidget.h @@ -95,6 +95,54 @@ private Q_SLOTS: */ virtual bool event(QEvent *e) override; + /** + * The drag and drop events below implement docking of floating widgets + * on Wayland, where CFloatingDockContainer::startPlatformDrag() executes + * a compositor driven drag instead of the mouse tracked dragging that is + * used on the other platforms. + * Accepts the drag, if it transports a floating widget of the same + * dock manager + */ + virtual void dragEnterEvent(QDragEnterEvent* e) override; + + /** + * Updates the drop overlays for the drag position + */ + virtual void dragMoveEvent(QDragMoveEvent* e) override; + + /** + * Hides the drop overlays, if the drag leaves this container + */ + virtual void dragLeaveEvent(QDragLeaveEvent* e) override; + + /** + * Docks the dragged floating widget into the drop area under the drop + * position + */ + virtual void dropEvent(QDropEvent* e) override; + + /** + * Returns the floating widget transported by the given platform drag + * and drop event, if the drag was started by this application and the + * floating widget belongs to the given dock manager; otherwise nullptr. + * The floating widget pointer from the mime data is only dereferenced + * after it was found in the dock manager floating widget list, so drags + * from other applications cannot inject an invalid pointer + */ + static CFloatingDockContainer* floatingWidgetFromDropEvent(QDropEvent* e, + CDockManager* DockManager); + + /** + * Configures and shows the container and dock area drop overlays for a + * drag that hovers over TopContainer at the given global position. + * Shared by the mouse tracked dragging (CFloatingDragPreview / + * CFloatingDockContainer) and the Wayland platform drag and drop. + * ContentPinnable is true, if the dragged content can be auto hidden. + */ + static void showDropOverlays(CDockManager* DockManager, + CDockContainerWidget* TopContainer, const QPoint& GlobalPos, + bool ContentPinnable); + /** * Access function for the internal root splitter */ diff --git a/src/DockManager.cpp b/src/DockManager.cpp index 111f065c..22026edb 100644 --- a/src/DockManager.cpp +++ b/src/DockManager.cpp @@ -661,8 +661,14 @@ bool CDockManager::eventFilter(QObject *obj, QEvent *e) // Emulate Qt:Tool behaviour. // Required because on some WMs Tool windows can't be maximized. + // Wayland: skip the stays-on-top emulation. The compositor owns the + // window stacking, and changing the window flags of a shown window + // recreates its surface, which would detach a running platform drag. + // The minimize synchronization below does not change window flags and + // is kept. + // Window always on top of the MainWindow. - if (e->type() == QEvent::WindowActivate) + if (!internal::isWayland() && e->type() == QEvent::WindowActivate) { for (auto _window : d->FloatingWidgets) { @@ -684,7 +690,7 @@ bool CDockManager::eventFilter(QObject *obj, QEvent *e) } } } - else if (e->type() == QEvent::WindowDeactivate) + else if (!internal::isWayland() && e->type() == QEvent::WindowDeactivate) { for (auto _window : d->FloatingWidgets) { @@ -955,6 +961,35 @@ void CDockManager::showEvent(QShowEvent *event) } +//============================================================================ +void CDockManager::changeEvent(QEvent *event) +{ + Super::changeEvent(event); + + // Wayland: floating widgets have no parent widget, so a style sheet set on + // this dock manager or one of its ancestors does not reach them through the + // widget hierarchy (an application wide qApp style sheet is still applied + // automatically by Qt). A QEvent::StyleChange on the dock manager fires both + // for its own style sheet and - because the change propagates down to + // descendants - for an ancestor's, so re-apply the inherited style sheet to + // every floating widget here to keep them matching the docked content. + // Re-applying cannot recurse: the floating widgets are not children of this + // dock manager, so it does not receive their StyleChange events. + if (event->type() == QEvent::StyleChange && internal::isWayland()) + { + const QString StyleSheet = + CFloatingDockContainer::waylandInheritedStyleSheet(this); + for (auto FloatingWidget : d->FloatingWidgets) + { + if (FloatingWidget) + { + FloatingWidget->setStyleSheet(StyleSheet); + } + } + } +} + + //============================================================================ void CDockManager::restoreHiddenFloatingWidgets() { diff --git a/src/DockManager.h b/src/DockManager.h index 1ac9215f..6b0eca93 100644 --- a/src/DockManager.h +++ b/src/DockManager.h @@ -158,6 +158,14 @@ public Q_SLOTS: */ virtual void showEvent(QShowEvent *event) override; + /** + * Wayland: re-applies the inherited style sheet to all floating widgets + * when this dock manager or one of its ancestors changes its style sheet. + * Floating widgets have no parent widget on Wayland, so they do not + * receive style sheet changes that propagate down the widget hierarchy. + */ + virtual void changeEvent(QEvent *event) override; + /** * Access for the internal dock focus controller. * This function only returns a valid object, if the FocusHighlighting diff --git a/src/DockOverlay.cpp b/src/DockOverlay.cpp index 3ae43a2d..dc6e7ea6 100644 --- a/src/DockOverlay.cpp +++ b/src/DockOverlay.cpp @@ -65,6 +65,17 @@ struct DockOverlayPrivate CDockOverlay::eMode Mode = CDockOverlay::ModeDockAreaOverlay; QRect DropAreaRect; int TabIndex = InvalidTabIndex; + // Wayland: the global cursor position is not available, so the last drop + // position from the drag and drop events is cached here to drive the + // drop preview rectangle and the highlighted drop indicator + QPoint LastGlobalPos; + // Wayland: the overlay is temporarily reparented into whatever top level + // window it is shown over (see showOverlay), because a child widget cannot + // render over a foreign window. This is the stable home window (the dock + // manager's window) that the overlay is reparented back to when hidden, so + // it is never left as a child of a transient floating window that gets + // destroyed - which would delete this dock-manager-owned overlay with it. + QPointer HomeWindow; /** * Private data constructor @@ -398,20 +409,40 @@ int DockOverlayPrivate::sideBarMouseZone(SideBarLocation sideBarLocation) //============================================================================ CDockOverlay::CDockOverlay(QWidget* parent, eMode Mode) : - QFrame(parent), + // Wayland: the overlay must be a child widget of the target window. Top + // level windows cannot be positioned in screen coordinates on Wayland, + // so a Qt::Tool overlay would not align over the target dock area + QFrame(internal::isWayland() && parent ? parent->window() : parent), d(new DockOverlayPrivate(this)) { d->Mode = Mode; d->Cross = new CDockOverlayCross(this); + if (internal::isWayland()) + { + // Remember the stable home window so hideOverlay() can reparent the + // overlay back to it (see HomeWindow), keeping it off transient + // floating windows that may be destroyed. + d->HomeWindow = parentWidget(); + // A translucent child widget would become a native window + // (wl_subsurface) that takes part in the compositors drag and drop + // target picking and steals the drop focus from the dock container. + // A child widget composites with its parent without the attribute. + // WA_TransparentForMouseEvents lets a release on a drop indicator + // fall through to it + setAttribute(Qt::WA_TransparentForMouseEvents); + } + else + { #if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS) - setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::X11BypassWindowManagerHint); + setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::X11BypassWindowManagerHint); #else - setWindowFlags(Qt::Tool | Qt::FramelessWindowHint); + setWindowFlags(Qt::Tool | Qt::FramelessWindowHint); #endif - setWindowOpacity(1); + setWindowOpacity(1); + setAttribute(Qt::WA_TranslucentBackground); + } setWindowTitle("DockOverlay"); setAttribute(Qt::WA_NoSystemBackground); - setAttribute(Qt::WA_TranslucentBackground); d->Cross->setVisible(false); setVisible(false); @@ -458,6 +489,13 @@ DockWidgetAreas CDockOverlay::allowedAreas() const //============================================================================ DockWidgetArea CDockOverlay::dropAreaUnderCursor() const +{ + return dropAreaUnderCursor(QCursor::pos()); +} + + +//============================================================================ +DockWidgetArea CDockOverlay::dropAreaUnderCursor(const QPoint& GlobalPos) const { d->TabIndex = InvalidTabIndex; if (!d->TargetWidget) @@ -465,18 +503,18 @@ DockWidgetArea CDockOverlay::dropAreaUnderCursor() const return InvalidDockWidgetArea; } - DockWidgetArea Result = d->Cross->cursorLocation(); + DockWidgetArea Result = d->Cross->cursorLocation(GlobalPos); if (Result != InvalidDockWidgetArea) { return Result; } - auto CursorPos = QCursor::pos(); + auto CursorPos = GlobalPos; auto DockArea = qobject_cast(d->TargetWidget.data()); if (!DockArea && CDockManager::autoHideConfigFlags().testFlag(CDockManager::AutoHideFeatureEnabled)) { auto Rect = rect(); - const QPoint pos = mapFromGlobal(QCursor::pos()); + const QPoint pos = mapFromGlobal(GlobalPos); if ((pos.x() < d->sideBarMouseZone(SideBarLeft)) && d->AllowedAreas.testFlag(LeftAutoHideArea)) { @@ -537,6 +575,13 @@ int CDockOverlay::tabIndexUnderCursor() const //============================================================================ DockWidgetArea CDockOverlay::visibleDropAreaUnderCursor() const +{ + return visibleDropAreaUnderCursor(QCursor::pos()); +} + + +//============================================================================ +DockWidgetArea CDockOverlay::visibleDropAreaUnderCursor(const QPoint& GlobalPos) const { if (isHidden() || !d->DropPreviewEnabled) { @@ -544,7 +589,7 @@ DockWidgetArea CDockOverlay::visibleDropAreaUnderCursor() const } else { - return dropAreaUnderCursor(); + return dropAreaUnderCursor(GlobalPos); } } @@ -552,10 +597,33 @@ DockWidgetArea CDockOverlay::visibleDropAreaUnderCursor() const //============================================================================ DockWidgetArea CDockOverlay::showOverlay(QWidget* target) { + return showOverlay(target, QCursor::pos()); +} + + +//============================================================================ +DockWidgetArea CDockOverlay::showOverlay(QWidget* target, const QPoint& GlobalPos) +{ + // Cache the drag position that drives this overlay's drop preview. On + // Wayland paintEvent() reads it back because the global cursor position is + // not available during a platform drag; it must be current before the + // show()/repaint() below triggers a paint + d->LastGlobalPos = GlobalPos; + + // Wayland: a cross window drop (e.g. into a floating widget) requires the + // overlay to be a child of the target window because a widget cannot be + // positioned over a foreign window + if (internal::isWayland() && parentWidget() != target->window()) + { + setParent(target->window()); + d->Cross->setParent(target->window()); + d->TargetWidget = nullptr; + } + if (d->TargetWidget == target) { // Hint: We could update geometry of overlay here. - DockWidgetArea da = dropAreaUnderCursor(); + DockWidgetArea da = dropAreaUnderCursor(GlobalPos); if (da != d->LastLocation) { repaint(); @@ -571,11 +639,25 @@ DockWidgetArea CDockOverlay::showOverlay(QWidget* target) hide(); resize(target->size()); QPoint TopLeft = target->mapToGlobal(target->rect().topLeft()); - move(TopLeft); + // Wayland: the overlay is a child of the target window, so the target top + // left has to be mapped into the parent coordinate system + if (internal::isWayland() && parentWidget()) + { + move(parentWidget()->mapFromGlobal(TopLeft)); + } + else + { + move(TopLeft); + } show(); + if (internal::isWayland()) + { + // paint the child overlay above its sibling dock widgets + raise(); + } d->Cross->updatePosition(); d->Cross->updateOverlayIcons(); - return dropAreaUnderCursor(); + return dropAreaUnderCursor(GlobalPos); } @@ -583,6 +665,17 @@ DockWidgetArea CDockOverlay::showOverlay(QWidget* target) void CDockOverlay::hideOverlay() { hide(); + // Wayland: showOverlay() reparents this overlay (and its cross) into the + // top level window it is shown over. Reparent them back to the stable home + // window now that the overlay is hidden, so the dock-manager-owned overlay + // is never left as a child of a transient floating window - destroying that + // window would otherwise delete the overlay and leave the dock manager with + // a dangling pointer. + if (internal::isWayland() && d->HomeWindow && parentWidget() != d->HomeWindow) + { + setParent(d->HomeWindow); + d->Cross->setParent(d->HomeWindow); + } d->TargetWidget.clear(); d->LastLocation = InvalidDockWidgetArea; d->DropAreaRect = QRect(); @@ -617,7 +710,10 @@ void CDockOverlay::paintEvent(QPaintEvent* event) } QRect r = rect(); - const DockWidgetArea da = dropAreaUnderCursor(); + // Wayland: use the cached drag position because the global cursor + // position is not available during a platform drag + const DockWidgetArea da = internal::isWayland() + ? dropAreaUnderCursor(d->LastGlobalPos) : dropAreaUnderCursor(); double Factor = (CDockOverlay::ModeContainerOverlay == d->Mode) ? 3 : 2; @@ -734,17 +830,27 @@ QPoint DockOverlayCrossPrivate::areaGridPosition(const DockWidgetArea area) //============================================================================ CDockOverlayCross::CDockOverlayCross(CDockOverlay* overlay) : - QWidget(overlay->parentWidget()), + // Wayland: see CDockOverlay - the cross must be a child of the target + // window for the same reason as the overlay + QWidget(internal::isWayland() && overlay->parentWidget() + ? overlay->parentWidget()->window() : overlay->parentWidget()), d(new DockOverlayCrossPrivate(this)) { d->DockOverlay = overlay; + if (internal::isWayland()) + { + setAttribute(Qt::WA_TransparentForMouseEvents); + } + else + { #if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS) - setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::X11BypassWindowManagerHint); + setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::X11BypassWindowManagerHint); #else - setWindowFlags(Qt::Tool | Qt::FramelessWindowHint); + setWindowFlags(Qt::Tool | Qt::FramelessWindowHint); #endif + setAttribute(Qt::WA_TranslucentBackground); + } setWindowTitle("DockOverlayCross"); - setAttribute(Qt::WA_TranslucentBackground); d->GridLayout = new QGridLayout(); d->GridLayout->setSpacing(0); @@ -784,7 +890,11 @@ void CDockOverlayCross::setupOverlayCross(CDockOverlay::eMode Mode) //============================================================================ void CDockOverlayCross::updateOverlayIcons() { - if (windowHandle()->devicePixelRatio() == d->LastDevicePixelRatio) + // Wayland: the cross is a child widget and has no window handle, so the + // device pixel ratio is taken from the widget instead of the window + const qreal DevicePixelRatio = internal::isWayland() + ? devicePixelRatioF() : windowHandle()->devicePixelRatio(); + if (DevicePixelRatio == d->LastDevicePixelRatio) { return; } @@ -879,7 +989,14 @@ void CDockOverlayCross::setAreaWidgets(const QHash& wi //============================================================================ DockWidgetArea CDockOverlayCross::cursorLocation() const { - const QPoint pos = mapFromGlobal(QCursor::pos()); + return cursorLocation(QCursor::pos()); +} + + +//============================================================================ +DockWidgetArea CDockOverlayCross::cursorLocation(const QPoint& GlobalPos) const +{ + const QPoint pos = mapFromGlobal(GlobalPos); QHashIterator i(d->DropIndicatorWidgets); while (i.hasNext()) { @@ -916,6 +1033,11 @@ void CDockOverlayCross::updatePosition() (this->height() - d->DockOverlay->height()) / 2); QPoint CrossTopLeft = TopLeft - Offest; move(CrossTopLeft); + if (internal::isWayland()) + { + // paint the child cross above its sibling dock widgets + raise(); + } } diff --git a/src/DockOverlay.h b/src/DockOverlay.h index 36376f59..3dcd8e81 100644 --- a/src/DockOverlay.h +++ b/src/DockOverlay.h @@ -87,6 +87,14 @@ class ADS_EXPORT CDockOverlay : public QFrame */ DockWidgetArea dropAreaUnderCursor() const; + /** + * Returns the drop area under the given global position. + * Use this overload if the position does not come from the mouse cursor + * (e.g. from drag and drop events on Wayland, where the global cursor + * position is not available) + */ + DockWidgetArea dropAreaUnderCursor(const QPoint& GlobalPos) const; + /** * If the drop area is the CenterDockWidgetArea or a sidebar area, * then this function returns the index of the tab under cursor. @@ -105,11 +113,24 @@ class ADS_EXPORT CDockOverlay : public QFrame */ DockWidgetArea visibleDropAreaUnderCursor() const; + /** + * This function returns the same like dropAreaUnderCursor(GlobalPos) if + * this overlay is not hidden and if drop preview is enabled and returns + * InvalidDockWidgetArea if it is hidden or drop preview is disabled. + */ + DockWidgetArea visibleDropAreaUnderCursor(const QPoint& GlobalPos) const; + /** * Show the drop overly for the given target widget */ DockWidgetArea showOverlay(QWidget* target); + /** + * Show the drop overlay for the given target widget and evaluate the + * drop area for the given global position instead of the cursor position + */ + DockWidgetArea showOverlay(QWidget* target, const QPoint& GlobalPos); + /** * Hides the overlay */ @@ -238,6 +259,12 @@ class CDockOverlayCross : public QWidget */ DockWidgetArea cursorLocation() const; + /** + * The function checks, if the given global position is inside of any + * drop indicator widget and returns the corresponding DockWidgetArea. + */ + DockWidgetArea cursorLocation(const QPoint& GlobalPos) const; + /** * Sets up the overlay cross for the given overlay mode */ diff --git a/src/DockWidgetTab.cpp b/src/DockWidgetTab.cpp index 943a8c61..757973a6 100644 --- a/src/DockWidgetTab.cpp +++ b/src/DockWidgetTab.cpp @@ -109,6 +109,14 @@ struct DockWidgetTabPrivate */ bool startFloating(eDragState DraggingState = DraggingFloatingWidget); + /** + * Wayland hybrid drag: drive the in-window drag preview from reliable + * event coordinates and, when the cursor leaves the source top-level + * window, convert the in-window drag into a native compositor platform + * drag. + */ + void waylandPreviewMove(QMouseEvent* ev); + /** * Returns true if the given config flag is set */ @@ -311,12 +319,30 @@ bool DockWidgetTabPrivate::startFloating(eDragState DraggingState) && (dockContainer->visibleDockAreaCount() == 1) && (DockWidget->dockAreaWidget()->dockWidgetsCount() == 1)) { + // On Wayland, dragging the tab of the single dock widget of a + // floating widget drags the existing floating widget, so the user + // can dock it into another container + if (internal::isWayland() && (DraggingFloatingWidget == DraggingState)) + { + auto FloatingContainer = dockContainer->floatingWidget(); + if (FloatingContainer) + { + DragState = DraggingInactive; + CFloatingDockContainer::startPlatformDrag(FloatingContainer, + GlobalDragStartMousePosition, _this); + return true; + } + } return false; } ADS_PRINT("startFloating"); DragState = DraggingState; IFloatingWidget* FloatingWidget = nullptr; + // On Wayland the drag preview is an in-window child widget (driven by event + // coordinates, confined to the source container); it converts to a native + // compositor platform drag only when the cursor leaves the source window + // (see waylandPreviewMove()). So a drag uses a preview on every platform. bool CreateContainer = (DraggingFloatingWidget != DraggingState); // If section widget has multiple tabs, we take only one tab @@ -341,6 +367,15 @@ bool DockWidgetTabPrivate::startFloating(eDragState DraggingState) auto Overlay = DockManager->containerOverlay(); Overlay->setAllowedAreas(OuterDockAreas); this->FloatingWidget = FloatingWidget; + if (internal::isWayland()) + { + // Confine the in-window preview to the source container; its + // position and the drop overlays are driven by event coordinates + // delivered to the tab (see waylandPreviewMove()). FloatingWidget is + // a CFloatingDragPreview here because CreateContainer was false. + auto Preview = static_cast(FloatingWidget); + Preview->setSourceContainer(dockContainer); + } qApp->postEvent(DockWidget, new QEvent((QEvent::Type)internal::DockedWidgetDragStartEvent)); } else @@ -352,6 +387,43 @@ bool DockWidgetTabPrivate::startFloating(eDragState DraggingState) } +//============================================================================ +void DockWidgetTabPrivate::waylandPreviewMove(QMouseEvent* ev) +{ + const QPoint GlobalPos = internal::globalPositionOf(ev); + + // FloatingWidget is a CFloatingDragPreview during the in-window phase + // (DraggingFloatingWidget state on Wayland; see startFloating()). While the + // cursor stays inside the source window the preview just follows it. + auto Preview = static_cast(FloatingWidget); + if (CFloatingDockContainer::waylandMoveOrLeaveInWindowPreview( + Preview, _this->window(), GlobalPos)) + { + return; + } + + // Boundary cross: the preview was torn down, convert the gesture into a + // native compositor platform drag of a freshly created floating widget. + FloatingWidget = nullptr; + DragState = DraggingInactive; + + QSize Size; + IFloatingWidget* RealFloating = nullptr; + if (DockArea->dockWidgetsCount() > 1) + { + RealFloating = createFloatingWidget(DockWidget, true); + Size = DockWidget->size(); + } + else + { + RealFloating = createFloatingWidget(DockArea, true); + Size = DockArea->size(); + } + CFloatingDockContainer::startPlatformDragForFloatingWidget(RealFloating, + DragStartMousePosition, Size, GlobalDragStartMousePosition, _this); +} + + //============================================================================ CDockWidgetTab::CDockWidgetTab(CDockWidget* DockWidget, QWidget *parent) : QFrame(parent), @@ -457,7 +529,16 @@ void CDockWidgetTab::mouseMoveEvent(QMouseEvent* ev) // move floating window if (d->isDraggingState(DraggingFloatingWidget)) { - d->FloatingWidget->moveFloating(); + if (internal::isWayland()) + { + // Wayland hybrid drag: in-window preview until the cursor leaves + // the source window, then convert to a native platform drag. + d->waylandPreviewMove(ev); + } + else + { + d->FloatingWidget->moveFloating(); + } Super::mouseMoveEvent(ev); return; } @@ -478,10 +559,14 @@ void CDockWidgetTab::mouseMoveEvent(QMouseEvent* ev) { // If this is the last dock area in a dock container with only // one single dock widget it does not make sense to move it to a new - // floating widget and leave this one empty + // floating widget and leave this one empty. + // On Wayland we fall through, because dragging the tab drags the + // existing floating widget so the user can dock it into another + // container if (d->DockArea->dockContainer()->isFloating() && d->DockArea->openDockWidgetsCount() == 1 - && d->DockArea->dockContainer()->visibleDockAreaCount() == 1) + && d->DockArea->dockContainer()->visibleDockAreaCount() == 1 + && !internal::isWayland()) { return; } diff --git a/src/FloatingDockContainer.cpp b/src/FloatingDockContainer.cpp index 408bbbf0..de11bcf8 100644 --- a/src/FloatingDockContainer.cpp +++ b/src/FloatingDockContainer.cpp @@ -39,12 +39,17 @@ #include #include #include +#include +#include +#include +#include #include "DockContainerWidget.h" #include "DockAreaWidget.h" #include "DockManager.h" #include "DockWidget.h" #include "DockOverlay.h" +#include "FloatingDragPreview.h" #ifdef Q_OS_WIN #include @@ -583,62 +588,15 @@ void FloatingDockContainerPrivate::updateDropOverlays(const QPoint &GlobalPos) } DropContainer = TopContainer; - auto ContainerOverlay = DockManager->containerOverlay(); - auto DockAreaOverlay = DockManager->dockAreaOverlay(); - if (!TopContainer) { - ContainerOverlay->hideOverlay(); - DockAreaOverlay->hideOverlay(); + DockManager->containerOverlay()->hideOverlay(); + DockManager->dockAreaOverlay()->hideOverlay(); return; } - int VisibleDockAreas = TopContainer->visibleDockAreaCount(); - DockWidgetAreas AllowedContainerAreas = (VisibleDockAreas > 1) ? OuterDockAreas : AllDockAreas; - auto DockArea = TopContainer->dockAreaAt(GlobalPos); - // If the dock container contains only one single DockArea, then we need - // to respect the allowed areas - only the center area is relevant here because - // all other allowed areas are from the container - if (VisibleDockAreas == 1 && DockArea) - { - AllowedContainerAreas.setFlag(CenterDockWidgetArea, DockArea->allowedAreas().testFlag(CenterDockWidgetArea)); - } - - if (DockContainer->features().testFlag(CDockWidget::DockWidgetPinnable)) - { - AllowedContainerAreas |= AutoHideDockAreas; - } - - ContainerOverlay->setAllowedAreas(AllowedContainerAreas); - - DockWidgetArea ContainerArea = ContainerOverlay->showOverlay(TopContainer); - ContainerOverlay->enableDropPreview(ContainerArea != InvalidDockWidgetArea); - if (DockArea && DockArea->isVisible() && VisibleDockAreas > 0) - { - DockAreaOverlay->enableDropPreview(true); - DockAreaOverlay->setAllowedAreas( - (VisibleDockAreas == 1) ? NoDockWidgetArea : DockArea->allowedAreas()); - DockWidgetArea Area = DockAreaOverlay->showOverlay(DockArea); - - // A CenterDockWidgetArea for the dockAreaOverlay() indicates that - // the mouse is in the title bar. If the ContainerArea is valid - // then we ignore the dock area of the dockAreaOverlay() and disable - // the drop preview - if ((Area == CenterDockWidgetArea) - && (ContainerArea != InvalidDockWidgetArea)) - { - DockAreaOverlay->enableDropPreview(false); - ContainerOverlay->enableDropPreview(true); - } - else - { - ContainerOverlay->enableDropPreview(InvalidDockWidgetArea == Area); - } - } - else - { - DockAreaOverlay->hideOverlay(); - } + CDockContainerWidget::showDropOverlays(DockManager, TopContainer, GlobalPos, + DockContainer->features().testFlag(CDockWidget::DockWidgetPinnable)); } @@ -653,8 +611,16 @@ void FloatingDockContainerPrivate::handleEscapeKey() //============================================================================ +// Wayland: a floating widget that is a child of the dock manager makes Qt +// turn the dock manager and all of its child widgets into native windows +// when the floating widget is shown. Native child widgets are +// wl_subsurfaces that take part in the compositors drag and drop target +// picking and steal the drop focus from the dock container during a +// platform drag. The dock manager deletes the registered floating widgets +// in its destructor, so the floating widget does not need a parent for +// memory management. CFloatingDockContainer::CFloatingDockContainer(CDockManager *DockManager) : - tFloatingWidgetBase(DockManager), + tFloatingWidgetBase(internal::isWayland() ? nullptr : static_cast(DockManager)), d(new FloatingDockContainerPrivate(this)) { d->DockManager = DockManager; @@ -696,16 +662,14 @@ CFloatingDockContainer::CFloatingDockContainer(CDockManager *DockManager) : native_window = window_manager != "KWIN"; } - if (native_window) - { - // Native windows do not work if wayland is used. Ubuntu 22.04 uses wayland by default. To use - // native windows, switch to Xorg - QString XdgSessionType = qgetenv("XDG_SESSION_TYPE").toLower(); - if ("wayland" == XdgSessionType) - { - native_window = false; - } - } + // Wayland does not allow clients to move windows, so the custom title + // bar cannot work there - dragging it would not move the window. Always + // use a native window so that the user can move the floating widget + // via the window decoration provided by the compositor or by Qt. + if (internal::isWayland()) + { + native_window = true; + } if (native_window) { @@ -737,6 +701,21 @@ CFloatingDockContainer::CFloatingDockContainer(CDockManager *DockManager) : winId(); } + // Wayland: the floating widget has no parent widget (see above), so it + // does not inherit the dock manager's effective style sheet through the + // widget hierarchy. Apply the style sheets along the dock manager parent + // chain explicitly, so the floating widget looks like the docked content. + // CDockManager::changeEvent() keeps this up to date when the style sheet + // changes while the widget is already floating. + if (internal::isWayland()) + { + const QString StyleSheet = waylandInheritedStyleSheet(DockManager); + if (!StyleSheet.isEmpty()) + { + setStyleSheet(StyleSheet); + } + } + DockManager->registerFloatingWidget(this); } @@ -1250,6 +1229,250 @@ void CFloatingDockContainer::finishDragging() d->titleMouseReleaseEvent(); } + +//============================================================================ +// MIME type that transports the CFloatingDockContainer pointer between the +// drag source and the CDockContainerWidget drop targets within this +// application +static const char* const FloatingWidgetMimeType = "application/x-ads-floating-dock-container"; + +// MIME types that QWaylandDataDevice translates into an +// xdg_toplevel_drag_v1 request so that the compositor attaches the floating +// widget window to the drag cursor. The format matches +// QMainWindowLayout::performPlatformWidgetDrag() in qtbase +static const char* const PlatformDragWindowMimeType = "application/x-qt-mainwindowdrag-window"; +static const char* const PlatformDragPositionMimeType = "application/x-qt-mainwindowdrag-position"; + +// Maximum age of the recorded drop candidate that startPlatformDrag() still +// accepts as a drop. It must be longer than the interval between drag move +// events (so a drop right after the last move counts) but short enough to +// reject a release far away from the last drop area +static const int PlatformDragDropCandidateTimeoutMs = 400; + + +// Records the last drop candidate during a Wayland platform drag. Some +// compositors do not deliver a drop event when the dragged window overlaps +// the drop target (they send a leave event instead), so startPlatformDrag() +// falls back to this candidate when no drop event was delivered. +struct PlatformDragDropCandidate +{ + QPointer Container; + QPoint GlobalPos; + bool ValidDropArea = false; + bool DropHandled = false; + QElapsedTimer SinceLastUpdate; +}; +static PlatformDragDropCandidate s_PlatformDragDropCandidate; + + +//============================================================================ +void CFloatingDockContainer::platformDragUpdateDropCandidate( + CDockContainerWidget* Container, const QPoint& GlobalPos, bool ValidDropArea) +{ + s_PlatformDragDropCandidate.Container = Container; + s_PlatformDragDropCandidate.GlobalPos = GlobalPos; + s_PlatformDragDropCandidate.ValidDropArea = ValidDropArea; + s_PlatformDragDropCandidate.SinceLastUpdate.start(); +} + + +//============================================================================ +void CFloatingDockContainer::platformDragNotifyDropHandled() +{ + s_PlatformDragDropCandidate.DropHandled = true; +} + + +//============================================================================ +QString CFloatingDockContainer::waylandInheritedStyleSheet(QWidget* DockManager) +{ + QString StyleSheet; + for (QWidget* Widget = DockManager; Widget; Widget = Widget->parentWidget()) + { + const QString WidgetStyleSheet = Widget->styleSheet(); + if (!WidgetStyleSheet.isEmpty()) + { + StyleSheet = WidgetStyleSheet + QLatin1Char('\n') + StyleSheet; + } + } + return StyleSheet; +} + + +//============================================================================ +Qt::DropAction CFloatingDockContainer::startPlatformDrag( + CFloatingDockContainer* FloatingWidget, const QPoint& GlobalPressPos, + QWidget* DragSource, const QPoint* DragOffset) +{ + auto serialize = [](const auto &object) + { + QByteArray Data; + QDataStream DataStream(&Data, QIODevice::WriteOnly); + DataStream << object; + return Data; + }; + + // xdg_toplevel_drag_v1: the attach offset that places the new window under + // the cursor is only honoured by the compositor while the toplevel is still + // UNMAPPED. The protocol says the client "issues a xdg_toplevel_drag_v1.attach + // request before mapping it", and the offset "might only be used when an + // unmapped window is attached". Qt's QWaylandDataDevice::startDrag() performs + // the attach when the data drag starts, reading the xdg_toplevel surface role + // off the window handle. + // + // The caller already show()'d the floating widget, which creates the + // xdg_toplevel role, but the surface is only mapped (a buffer committed) + // once the event loop is pumped. We therefore deliberately do NOT wait for + // exposure here: pumping events would map the window before exec() runs the + // attach, after which the compositor ignores the offset and leaves the + // window at its default position (typically screen center). Proceeding + // straight to exec() keeps the toplevel created-but-unmapped so the attach + // offset positions it under the cursor. + + // The offset is the press position relative to the floating widget top + // left corner, so the compositor keeps the cursor at the same spot on + // the window during the drag. For a freshly created floating window the + // caller supplies it explicitly (DragOffset): on Wayland the new window has + // no meaningful geometry yet, so mapFromGlobal() would give a wrong/random + // offset and the compositor would place the window randomly. + QPoint DragStartOffset; + if (DragOffset) + { + DragStartOffset = *DragOffset; + // The new floating window is a native window on Wayland (forced in the + // constructor), so it has a title bar / decoration around the content - + // drawn either by the compositor or by Qt's client-side decoration. + // The supplied offset is relative to the source tab/area content, so + // shift it by the window frame's top and left margins to keep the + // grabbed content point - not the decoration - under the cursor. With + // server-side decorations the margins are 0 and no shift is applied. + if (FloatingWidget->windowHandle()) + { + const QMargins Frame = FloatingWidget->windowHandle()->frameMargins(); + DragStartOffset.rx() += Frame.left(); + DragStartOffset.ry() += Frame.top(); + } + } + else + { + DragStartOffset = FloatingWidget->mapFromGlobal(GlobalPressPos); + } + + auto Drag = new QDrag(DragSource); + auto MimeData = new QMimeData(); + MimeData->setData(QLatin1String(PlatformDragWindowMimeType), + serialize(reinterpret_cast(FloatingWidget->windowHandle()))); + MimeData->setData(QLatin1String(PlatformDragPositionMimeType), + serialize(DragStartOffset)); + MimeData->setData(QLatin1String(FloatingWidgetMimeType), + serialize(reinterpret_cast(FloatingWidget))); + Drag->setMimeData(MimeData); + + // The drop targets dock the floating widget in their dropEvent() handler, + // which deletes the floating widget before exec() returns + QPointer GuardedFloatingWidget(FloatingWidget); + auto DockManager = FloatingWidget->dockContainer()->dockManager(); + s_PlatformDragDropCandidate = PlatformDragDropCandidate(); + // The QDrag::exec() return value does not reliably encode whether the + // floating widget was docked on Wayland (e.g. compositors that send a + // leave event instead of a drop event report Qt::IgnoreAction even though + // the drag ended over a drop area). Track the docking outcome explicitly + // instead and translate it into the documented return value below. + Drag->exec(); + + // Some compositors (e.g. Mutter) do not deliver a drop event to the + // target container when the dragged window overlaps it - they send a + // leave event on release instead. If no drop event was delivered, but + // the drag ended over a valid drop area, dock the floating widget into + // the recorded candidate. A short timeout distinguishes a drop on a + // drop area from a release outside of any drop area to keep the widget + // floating + auto& Candidate = s_PlatformDragDropCandidate; + bool Docked = Candidate.DropHandled; + if (!Candidate.DropHandled && !GuardedFloatingWidget.isNull() + && Candidate.Container && Candidate.ValidDropArea + && Candidate.SinceLastUpdate.isValid() + && Candidate.SinceLastUpdate.elapsed() < PlatformDragDropCandidateTimeoutMs) + { + Candidate.Container->dropFloatingWidget(GuardedFloatingWidget, + Candidate.GlobalPos); + Docked = true; + } + s_PlatformDragDropCandidate = PlatformDragDropCandidate(); + DockManager->containerOverlay()->hideOverlay(); + DockManager->dockAreaOverlay()->hideOverlay(); + Drag->deleteLater(); + + if (GuardedFloatingWidget) + { + GuardedFloatingWidget->d->setState(DraggingInactive); + } + + return Docked ? Qt::MoveAction : Qt::IgnoreAction; +} + + +//============================================================================ +bool CFloatingDockContainer::waylandMoveOrLeaveInWindowPreview( + CFloatingDragPreview* Preview, QWidget* SourceWindow, const QPoint& GlobalPos) +{ + const QPoint InWindow = SourceWindow->mapFromGlobal(GlobalPos); + if (SourceWindow->rect().contains(InWindow)) + { + if (Preview) + { + Preview->moveFloating(GlobalPos); + } + return true; + } + + // Boundary cross: tear down the in-window preview without performing a + // drop. The caller converts the gesture into a native compositor platform + // drag while the implicit pointer grab from the original press is still + // active, which is what QDrag::exec() / xdg_toplevel_drag_v1 requires. + if (Preview) + { + Preview->cancelDraggingSilently(); + } + return false; +} + + +//============================================================================ +Qt::DropAction CFloatingDockContainer::startPlatformDragForFloatingWidget( + IFloatingWidget* FloatingWidget, const QPoint& GrabOffset, const QSize& Size, + const QPoint& GlobalGrabPos, QWidget* DragSource) +{ + FloatingWidget->startFloating(GrabOffset, Size, DraggingInactive, nullptr); + // Keep the grab point under the cursor across the conversion. A client + // cannot position a Wayland top-level and the new window has no usable + // geometry yet, so pass the surface-local grab offset explicitly rather + // than letting startPlatformDrag() derive it from the press via + // mapFromGlobal() (which would be random on Wayland). We must NOT move() + // the window here either: besides being ignored by the compositor, a + // geometry change between show() and exec() risks mapping the surface + // before the attach, which would discard the offset. + QPoint Offset = GrabOffset; + return startPlatformDrag(static_cast(FloatingWidget), + GlobalGrabPos, DragSource, &Offset); +} + + +//============================================================================ +CFloatingDockContainer* CFloatingDockContainer::floatingWidgetFromMimeData( + const QMimeData* MimeData) +{ + if (!MimeData || !MimeData->hasFormat(QLatin1String(FloatingWidgetMimeType))) + { + return nullptr; + } + + qintptr FloatingWidgetPtr = 0; + QDataStream DataStream(MimeData->data(QLatin1String(FloatingWidgetMimeType))); + DataStream >> FloatingWidgetPtr; + return reinterpret_cast(FloatingWidgetPtr); +} + #ifdef Q_OS_MACOS //============================================================================ bool CFloatingDockContainer::event(QEvent *e) @@ -1428,7 +1651,12 @@ void CFloatingDockContainer::resizeEvent(QResizeEvent *event) void CFloatingDockContainer::moveEvent(QMoveEvent *event) { Super::moveEvent(event); - if (!d->IsResizing && event->spontaneous() && d->MousePressed) + // On Wayland, the global cursor position is not available and the + // compositor does not report window moves, so the docking via window + // moves cannot work. Floating widgets are docked via the platform drag + // and drop implemented in CDockContainerWidget instead + if (!d->IsResizing && event->spontaneous() && d->MousePressed + && !internal::isWayland()) { d->setState(DraggingFloatingWidget); d->updateDropOverlays(QCursor::pos()); diff --git a/src/FloatingDockContainer.h b/src/FloatingDockContainer.h index 52991cc2..fcaa0bcf 100644 --- a/src/FloatingDockContainer.h +++ b/src/FloatingDockContainer.h @@ -42,6 +42,7 @@ #endif class CDockingStateReader; +class QMimeData; namespace ads { @@ -59,6 +60,7 @@ class CDockAreaTitleBar; struct DockAreaTitleBarPrivate; class CFloatingWidgetTitleBar; class CDockingStateReader; +class CFloatingDragPreview; /** * Pure virtual interface for floating widgets. @@ -117,6 +119,42 @@ class ADS_EXPORT CFloatingDockContainer : public tFloatingWidgetBase, public IFl friend class CDockWidget; friend class CDockAreaWidget; friend class CFloatingWidgetTitleBar; + friend class CDockContainerWidget; + + /** + * Returns the floating widget that is transported in the mime data of + * a drag and drop operation started by startPlatformDrag() and nullptr, + * if the mime data does not contain a floating widget + */ + static CFloatingDockContainer* floatingWidgetFromMimeData(const QMimeData* MimeData); + + /** + * Records the drop candidate while a platform drag is hovering over a + * dock container. Called from CDockContainerWidget::dragMoveEvent(). + * Some compositors (e.g. Mutter) do not deliver a drop event when the + * dragged window overlaps the drop target. startPlatformDrag() then docks + * the floating widget into the last recorded candidate, if the drag ended + * over a valid drop area + */ + static void platformDragUpdateDropCandidate(CDockContainerWidget* Container, + const QPoint& GlobalPos, bool ValidDropArea); + + /** + * Marks the platform drag drop as handled by a delivered drop event, so + * that startPlatformDrag() does not dock the floating widget a second time + */ + static void platformDragNotifyDropHandled(); + + /** + * Wayland: returns the concatenated style sheets along the dock manager + * parent chain. A floating widget has no parent widget on Wayland, so it + * does not inherit the dock manager's effective style sheet through the + * widget hierarchy and gets this style sheet applied explicitly instead + * (an application wide qApp style sheet is still applied automatically by + * Qt and is therefore not included here). Used at construction and when + * the dock manager re-applies the style sheet after a style change. + */ + static QString waylandInheritedStyleSheet(QWidget* DockManager); private Q_SLOTS: void onDockAreasAddedOrRemoved(); @@ -236,6 +274,57 @@ private Q_SLOTS: startFloating(DragStartMousePos, Size, DraggingFloatingWidget, MouseEventHandler); } + /** + * Starts a compositor driven drag of the given floating widget on + * Wayland. + * Wayland neither reports the global cursor position nor allows clients + * to move top level windows, so the mouse tracked dragging that is used + * on the other platforms cannot work. Instead, this function executes a + * QDrag with the MIME types that QWaylandDataDevice translates into an + * xdg_toplevel_drag_v1 request, so the compositor moves the floating + * widget with the cursor. The CDockContainerWidget drag and drop event + * handlers show the drop overlays and dock the widget on drop. + * GlobalPressPos is the mouse press position in global coordinates and + * DragSource is the widget that received the mouse press event. + * DragOffset, when non-null, is the surface-local point of the floating + * widget that should stay under the cursor (the xdg_toplevel_drag attach + * offset). It must be supplied for a freshly created floating window: on + * Wayland such a window has no meaningful geometry yet, so deriving the + * offset from GlobalPressPos via mapFromGlobal() would place it randomly. + * When null (e.g. dragging an already-mapped floating window) the offset is + * derived from GlobalPressPos. + * The function blocks until the user drops or cancels the drag. It + * returns Qt::MoveAction, if the floating widget was docked into a + * drop area and Qt::IgnoreAction, if the floating widget was dropped + * outside of any drop area and remains floating. + */ + static Qt::DropAction startPlatformDrag(CFloatingDockContainer* FloatingWidget, + const QPoint& GlobalPressPos, QWidget* DragSource, + const QPoint* DragOffset = nullptr); + + /** + * Wayland hybrid drag helper, shared by the tab and the title bar drag + * paths. While the cursor stays inside SourceWindow the in-window Preview + * is moved to GlobalPos and this returns true. Once the cursor leaves + * SourceWindow the Preview is torn down without performing a drop and this + * returns false - the caller then creates the real floating widget and + * hands it to startPlatformDragForFloatingWidget(). Preview may be null. + */ + static bool waylandMoveOrLeaveInWindowPreview(CFloatingDragPreview* Preview, + QWidget* SourceWindow, const QPoint& GlobalPos); + + /** + * Wayland hybrid drag helper, shared by the tab and the title bar drag + * paths. Floats the freshly created FloatingWidget at the given Size and + * starts the compositor driven platform drag, keeping GrabOffset (the + * surface-local grab point) under the cursor. See startPlatformDrag() for + * why the offset is passed explicitly instead of being derived from the + * press position on Wayland. + */ + static Qt::DropAction startPlatformDragForFloatingWidget( + IFloatingWidget* FloatingWidget, const QPoint& GrabOffset, + const QSize& Size, const QPoint& GlobalGrabPos, QWidget* DragSource); + /** * This function returns true, if it can be closed. * It can be closed, if all dock widgets in all dock areas can be closed diff --git a/src/FloatingDragPreview.cpp b/src/FloatingDragPreview.cpp index f7a45a2b..6b2c96d7 100644 --- a/src/FloatingDragPreview.cpp +++ b/src/FloatingDragPreview.cpp @@ -45,6 +45,22 @@ struct FloatingDragPreviewPrivate QPixmap ContentPreviewPixmap; bool Canceled = false; + // Wayland hybrid drag: during the in-window phase, hit-testing and overlays + // are confined to this container and driven by an explicit, event-supplied + // global position instead of the unreliable QCursor::pos(). + CDockContainerWidget* SourceContainer = nullptr; + QPoint LastGlobalPos; + bool HasLastGlobalPos = false; + + /** + * Returns the last event-supplied global position when available (Wayland + * hybrid drag), otherwise the live cursor position. + */ + QPoint cursorPos() const + { + return HasLastGlobalPos ? LastGlobalPos : QCursor::pos(); + } + /** * Private data constructor @@ -123,21 +139,38 @@ void FloatingDragPreviewPrivate::updateDropOverlays(const QPoint &GlobalPos) return; } - auto Containers = DockManager->dockContainers(); CDockContainerWidget *TopContainer = nullptr; - for (auto ContainerWidget : Containers) + if (SourceContainer) { - if (!ContainerWidget->isVisible()) + // Wayland in-window phase: the source container is the only candidate. + // This avoids relying on cross-window global coordinates (unreliable on + // Wayland) and keeps overlays from leaking onto other top-level windows. + if (SourceContainer->isVisible()) { - continue; + QPoint MappedPos = SourceContainer->mapFromGlobal(GlobalPos); + if (SourceContainer->rect().contains(MappedPos)) + { + TopContainer = SourceContainer; + } } - - QPoint MappedPos = ContainerWidget->mapFromGlobal(GlobalPos); - if (ContainerWidget->rect().contains(MappedPos)) + } + else + { + auto Containers = DockManager->dockContainers(); + for (auto ContainerWidget : Containers) { - if (!TopContainer || ContainerWidget->isInFrontOf(TopContainer)) + if (!ContainerWidget->isVisible()) + { + continue; + } + + QPoint MappedPos = ContainerWidget->mapFromGlobal(GlobalPos); + if (ContainerWidget->rect().contains(MappedPos)) { - TopContainer = ContainerWidget; + if (!TopContainer || ContainerWidget->isInFrontOf(TopContainer)) + { + TopContainer = ContainerWidget; + } } } } @@ -157,8 +190,8 @@ void FloatingDragPreviewPrivate::updateDropOverlays(const QPoint &GlobalPos) return; } - auto DockDropArea = DockAreaOverlay->dropAreaUnderCursor(); - auto ContainerDropArea = ContainerOverlay->dropAreaUnderCursor(); + auto DockDropArea = DockAreaOverlay->dropAreaUnderCursor(GlobalPos); + auto ContainerDropArea = ContainerOverlay->dropAreaUnderCursor(GlobalPos); int VisibleDockAreas = TopContainer->visibleDockAreaCount(); @@ -190,7 +223,7 @@ void FloatingDragPreviewPrivate::updateDropOverlays(const QPoint &GlobalPos) { DockAreaOverlay->enableDropPreview(true); DockAreaOverlay->setAllowedAreas( (VisibleDockAreas == 1) ? NoDockWidgetArea : DockArea->allowedAreas()); - DockWidgetArea Area = DockAreaOverlay->showOverlay(DockArea); + DockWidgetArea Area = DockAreaOverlay->showOverlay(DockArea, GlobalPos); // A CenterDockWidgetArea for the dockAreaOverlay() indicates that // the mouse is in the title bar. If the ContainerArea is valid @@ -205,7 +238,7 @@ void FloatingDragPreviewPrivate::updateDropOverlays(const QPoint &GlobalPos) { ContainerOverlay->enableDropPreview(InvalidDockWidgetArea == Area); } - ContainerOverlay->showOverlay(TopContainer); + ContainerOverlay->showOverlay(TopContainer, GlobalPos); } else { @@ -218,7 +251,7 @@ void FloatingDragPreviewPrivate::updateDropOverlays(const QPoint &GlobalPos) { ContainerOverlay->setAllowedAreas(AutoHideDockAreas); } - ContainerOverlay->showOverlay(TopContainer); + ContainerOverlay->showOverlay(TopContainer, GlobalPos); if (DockArea == ContentSourceArea && InvalidDockWidgetArea == ContainerDropArea) @@ -276,8 +309,19 @@ void FloatingDragPreviewPrivate::createFloatingWidget() //============================================================================ +// Wayland: in the frameless config, parent the preview to the source content's +// top-level window so it renders as a translucent child instead of a Qt::Tool +// top-level. Wayland refuses client-driven move() of top-levels in screen +// coordinates; child widgets composite correctly on every backend. We parent to +// Content->window() (the window the dragged widget/area currently lives in) and +// NOT to parent->window(): parent is the dock manager, whose window is always +// the main window, so a drag started in a floating window would otherwise show +// the preview on the main window. Non-Wayland behavior is unchanged. CFloatingDragPreview::CFloatingDragPreview(QWidget* Content, QWidget* parent) : - QWidget(parent), + QWidget((internal::isWayland() && Content + && !CDockManager::testConfigFlag(CDockManager::DragPreviewHasWindowFrame)) + ? Content->window() + : parent), d(new FloatingDragPreviewPrivate(this)) { d->Content = Content; @@ -287,19 +331,32 @@ CFloatingDragPreview::CFloatingDragPreview(QWidget* Content, QWidget* parent) : { setWindowFlags( Qt::Window | Qt::WindowMaximizeButtonHint | Qt::WindowCloseButtonHint); +#if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS) + auto Flags = windowFlags(); + Flags |= Qt::WindowStaysOnTopHint | Qt::X11BypassWindowManagerHint; + setWindowFlags(Flags); +#endif + } + else if (internal::isWayland()) + { + // Translucent child of the source window. Transparent for mouse events + // so a release over a drop indicator falls through to it; positioning + // is driven by the grabbing tab/title-bar, not by this preview. + setAttribute(Qt::WA_NoSystemBackground); + setAttribute(Qt::WA_TranslucentBackground); + setAttribute(Qt::WA_TransparentForMouseEvents); } else { setWindowFlags(Qt::Tool | Qt::FramelessWindowHint); setAttribute(Qt::WA_NoSystemBackground); setAttribute(Qt::WA_TranslucentBackground); - } - #if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS) - auto Flags = windowFlags(); - Flags |= Qt::WindowStaysOnTopHint | Qt::X11BypassWindowManagerHint; - setWindowFlags(Flags); + auto Flags = windowFlags(); + Flags |= Qt::WindowStaysOnTopHint | Qt::X11BypassWindowManagerHint; + setWindowFlags(Flags); #endif + } // Create a static image of the widget that should get undocked // This is like some kind preview image like it is uses in drag and drop @@ -352,11 +409,53 @@ CFloatingDragPreview::~CFloatingDragPreview() //============================================================================ void CFloatingDragPreview::moveFloating() { - int BorderSize = (frameSize().width() - size().width()) / 2; - const QPoint moveToPos = QCursor::pos() - d->DragStartMousePosition + moveFloating(QCursor::pos()); +} + + +//============================================================================ +void CFloatingDragPreview::moveFloating(const QPoint& GlobalPos) +{ + // Only stash the event-supplied position on Wayland, where QCursor::pos() + // is unreliable and finishDragging() must reuse it. On other platforms + // finishDragging() keeps using the live QCursor::pos() (unchanged behavior). + if (internal::isWayland()) + { + d->LastGlobalPos = GlobalPos; + d->HasLastGlobalPos = true; + } + const int BorderSize = (frameSize().width() - size().width()) / 2; + const QPoint TargetGlobal = GlobalPos - d->DragStartMousePosition - QPoint(BorderSize, 0); - move(moveToPos); - d->updateDropOverlays(QCursor::pos()); + // When parented as a child (Wayland frameless), translate the screen-coord + // target into parent-local space so move() places us under the cursor. + if (isWindow() || !parentWidget()) + { + move(TargetGlobal); + } + else + { + move(parentWidget()->mapFromGlobal(TargetGlobal)); + } + d->updateDropOverlays(GlobalPos); +} + + +//============================================================================ +void CFloatingDragPreview::setSourceContainer(CDockContainerWidget* Container) +{ + d->SourceContainer = Container; +} + + +//============================================================================ +void CFloatingDragPreview::cancelDraggingSilently() +{ + // Tear down without performing a drop and without emitting draggingCanceled. + d->Canceled = true; + d->DockManager->containerOverlay()->hideOverlay(); + d->DockManager->dockAreaOverlay()->hideOverlay(); + close(); } @@ -370,7 +469,14 @@ void CFloatingDragPreview::startFloating(const QPoint &DragStartMousePos, d->DragStartMousePosition = DragStartMousePos; moveFloating(); show(); - + if (internal::isWayland()) + { + // On Wayland the preview is a child widget of the source window; raise + // it above its siblings. On other platforms it is a Qt::Tool window + // that is already kept on top, so raising is unnecessary here (keeps + // non-Wayland behavior unchanged). + raise(); + } } @@ -379,8 +485,9 @@ void CFloatingDragPreview::finishDragging() { ADS_PRINT("CFloatingDragPreview::finishDragging"); - auto DockDropArea = d->DockManager->dockAreaOverlay()->visibleDropAreaUnderCursor(); - auto ContainerDropArea = d->DockManager->containerOverlay()->visibleDropAreaUnderCursor(); + const QPoint GlobalPos = d->cursorPos(); + auto DockDropArea = d->DockManager->dockAreaOverlay()->visibleDropAreaUnderCursor(GlobalPos); + auto ContainerDropArea = d->DockManager->containerOverlay()->visibleDropAreaUnderCursor(GlobalPos); bool ValidDropArea = (DockDropArea != InvalidDockWidgetArea) || (ContainerDropArea != InvalidDockWidgetArea); // Non floatable auto hide widgets should stay in its current auto hide @@ -396,7 +503,7 @@ void CFloatingDragPreview::finishDragging() } else if (DockDropArea != InvalidDockWidgetArea) { - d->DropContainer->dropWidget(d->Content, DockDropArea, d->DropContainer->dockAreaAt(QCursor::pos()), + d->DropContainer->dropWidget(d->Content, DockDropArea, d->DropContainer->dockAreaAt(GlobalPos), d->DockManager->dockAreaOverlay()->tabIndexUnderCursor()); } else if (ContainerDropArea != InvalidDockWidgetArea) @@ -406,7 +513,7 @@ void CFloatingDragPreview::finishDragging() // then we tabify the dropped widget into the only visible dock area if (d->DropContainer->visibleDockAreaCount() <= 1 && CenterDockWidgetArea == ContainerDropArea) { - DockArea = d->DropContainer->dockAreaAt(QCursor::pos()); + DockArea = d->DropContainer->dockAreaAt(GlobalPos); } d->DropContainer->dropWidget(d->Content, ContainerDropArea, DockArea, diff --git a/src/FloatingDragPreview.h b/src/FloatingDragPreview.h index 68e3e5cf..a0798809 100644 --- a/src/FloatingDragPreview.h +++ b/src/FloatingDragPreview.h @@ -17,6 +17,7 @@ namespace ads { class CDockWidget; class CDockAreaWidget; +class CDockContainerWidget; struct FloatingDragPreviewPrivate; /** @@ -85,6 +86,28 @@ private Q_SLOTS: */ virtual void moveFloating() override; + /** + * Wayland hybrid drag: move the preview using an explicit global position + * supplied by the grabbing tab/title-bar mouse event instead of + * QCursor::pos(), which is unreliable on Wayland. Also drives the drop + * overlays from the supplied position. + */ + void moveFloating(const QPoint& GlobalPos); + + /** + * Wayland hybrid drag: confine drop-target hit-testing and overlays to a + * single source container during the in-window phase, so nothing leaks to + * other top-level windows and no cross-window global coordinate is needed. + */ + void setSourceContainer(CDockContainerWidget* Container); + + /** + * Wayland hybrid drag: tear the preview down without performing a drop and + * without emitting draggingCanceled. Used when the in-window drag converts + * to a native platform drag on a window-boundary cross. + */ + void cancelDraggingSilently(); + /** * Finishes dragging. * Hides the dock overlays and executes the real undocking and docking diff --git a/src/ads_globals.cpp b/src/ads_globals.cpp index b019318e..13beca5b 100644 --- a/src/ads_globals.cpp +++ b/src/ads_globals.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include "DockSplitter.h" #include "DockManager.h" @@ -374,6 +375,14 @@ bool isSideBarArea(DockWidgetArea Area) } +//============================================================================ +bool isWayland() +{ + return QGuiApplication::platformName().startsWith( + QLatin1String("wayland"), Qt::CaseInsensitive); +} + + //============================================================================ QPixmap createTransparentPixmap(const QPixmap& Source, qreal Opacity) { diff --git a/src/ads_globals.h b/src/ads_globals.h index f7ccc8e3..42150912 100644 --- a/src/ads_globals.h +++ b/src/ads_globals.h @@ -229,6 +229,15 @@ class CDockInsertParam : public QPair CDockInsertParam dockAreaInsertParameters(DockWidgetArea Area); +/** + * Returns true, if the application runs on the Wayland display server + * protocol. Wayland does not allow clients to move top level windows in + * screen coordinates or to query the global cursor position, so docking + * requires a different implementation on Wayland. + */ +bool isWayland(); + + /** * Returns the SieBarLocation for the AutoHide dock widget areas */