From b0f3701987f80effb6b0a91216bca427488c94b9 Mon Sep 17 00:00:00 2001 From: Davide Faconti Date: Sun, 26 Apr 2026 23:20:27 +0200 Subject: [PATCH] Wayland: render drag preview and overlays as child widgets Wayland refuses client-driven placement of top-level windows in screen coordinates. CFloatingDragPreview, CDockOverlay, and CDockOverlayCross were all top-level Qt::Tool windows positioned via move(globalCoords) every mouse event, so under native Wayland the preview drifted off the cursor and the drop indicators rendered over the wrong dock area. Reparent all three to the dock manager's top-level window so they render as child widgets inside an existing surface, and drop the Linux X11 bypass-window-manager hint that only ever applied to top-levels. In moveFloating() / showOverlay() translate the screen-coord target into parent-local coordinates via mapFromGlobal() before move(). Replace windowHandle()->devicePixelRatio() with devicePixelRatioF() in updateOverlayIcons() - the former returns nullptr for a child widget and segfaulted as soon as the cross attempted to refresh icon DPR after reparenting. Add WA_TransparentForMouseEvents on preview and overlays so a release directly on a visible drop indicator is no longer intercepted by the translucent preview rectangle. This is an alternative to PR #789's Qt::ToolTip approach, which the author confirms still has unresolved overlay positioning issues on Wayland (see PR #789 thread, comment from 2025-11-29). --- src/DockOverlay.cpp | 43 ++++++++++++++++--------------------- src/FloatingDragPreview.cpp | 41 ++++++++++++++++++++++++----------- 2 files changed, 48 insertions(+), 36 deletions(-) diff --git a/src/DockOverlay.cpp b/src/DockOverlay.cpp index 3ae43a2d7..60baa28de 100644 --- a/src/DockOverlay.cpp +++ b/src/DockOverlay.cpp @@ -397,21 +397,20 @@ int DockOverlayPrivate::sideBarMouseZone(SideBarLocation sideBarLocation) //============================================================================ +// Wayland: parent the overlay to the manager's top-level window and drop +// the Qt::Tool flags. Top-level placement in screen coordinates is unreliable +// under Wayland, which left the indicators stuck over the wrong dock area. CDockOverlay::CDockOverlay(QWidget* parent, eMode Mode) : - QFrame(parent), + QFrame(parent ? parent->window() : nullptr), d(new DockOverlayPrivate(this)) { d->Mode = Mode; d->Cross = new CDockOverlayCross(this); -#if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS) - setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::X11BypassWindowManagerHint); -#else - setWindowFlags(Qt::Tool | Qt::FramelessWindowHint); -#endif setWindowOpacity(1); setWindowTitle("DockOverlay"); setAttribute(Qt::WA_NoSystemBackground); setAttribute(Qt::WA_TranslucentBackground); + setAttribute(Qt::WA_TransparentForMouseEvents); d->Cross->setVisible(false); setVisible(false); @@ -570,9 +569,10 @@ DockWidgetArea CDockOverlay::showOverlay(QWidget* target) // Move it over the target. hide(); resize(target->size()); - QPoint TopLeft = target->mapToGlobal(target->rect().topLeft()); - move(TopLeft); + const QPoint TopLeftGlobal = target->mapToGlobal(target->rect().topLeft()); + move(parentWidget() ? parentWidget()->mapFromGlobal(TopLeftGlobal) : TopLeftGlobal); show(); + raise(); d->Cross->updatePosition(); d->Cross->updateOverlayIcons(); return dropAreaUnderCursor(); @@ -733,18 +733,15 @@ QPoint DockOverlayCrossPrivate::areaGridPosition(const DockWidgetArea area) //============================================================================ +// Wayland: see CDockOverlay above for the rationale. CDockOverlayCross::CDockOverlayCross(CDockOverlay* overlay) : - QWidget(overlay->parentWidget()), + QWidget(overlay->parentWidget() ? overlay->parentWidget()->window() : nullptr), d(new DockOverlayCrossPrivate(this)) { d->DockOverlay = overlay; -#if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS) - setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::X11BypassWindowManagerHint); -#else - setWindowFlags(Qt::Tool | Qt::FramelessWindowHint); -#endif setWindowTitle("DockOverlayCross"); setAttribute(Qt::WA_TranslucentBackground); + setAttribute(Qt::WA_TransparentForMouseEvents); d->GridLayout = new QGridLayout(); d->GridLayout->setSpacing(0); @@ -784,7 +781,10 @@ void CDockOverlayCross::setupOverlayCross(CDockOverlay::eMode Mode) //============================================================================ void CDockOverlayCross::updateOverlayIcons() { - if (windowHandle()->devicePixelRatio() == d->LastDevicePixelRatio) + // Wayland: devicePixelRatioF() walks the parent chain; the original + // windowHandle()->devicePixelRatio() segfaults for a child widget. + const qreal CurrentRatio = devicePixelRatioF(); + if (CurrentRatio == d->LastDevicePixelRatio) { return; } @@ -793,11 +793,7 @@ void CDockOverlayCross::updateOverlayIcons() { d->updateDropIndicatorIcon(Widget); } -#if QT_VERSION >= 0x050600 - d->LastDevicePixelRatio = devicePixelRatioF(); -#else - d->LastDevicePixelRatio = devicePixelRatio(); -#endif + d->LastDevicePixelRatio = CurrentRatio; } @@ -911,11 +907,10 @@ void CDockOverlayCross::showEvent(QShowEvent*) void CDockOverlayCross::updatePosition() { resize(d->DockOverlay->size()); - QPoint TopLeft = d->DockOverlay->pos(); - QPoint Offest((this->width() - d->DockOverlay->width()) / 2, + const QPoint Offset((this->width() - d->DockOverlay->width()) / 2, (this->height() - d->DockOverlay->height()) / 2); - QPoint CrossTopLeft = TopLeft - Offest; - move(CrossTopLeft); + move(d->DockOverlay->pos() - Offset); + raise(); } diff --git a/src/FloatingDragPreview.cpp b/src/FloatingDragPreview.cpp index f7a45a2b2..ced26431b 100644 --- a/src/FloatingDragPreview.cpp +++ b/src/FloatingDragPreview.cpp @@ -276,8 +276,14 @@ void FloatingDragPreviewPrivate::createFloatingWidget() //============================================================================ +// Wayland: in the frameless config, parent the preview to the manager's +// top-level window so it renders as a translucent child instead of a Qt::Tool +// top-level. Wayland refuses to honour client-driven move() of top-levels in +// screen coordinates; child widgets work on every backend. CFloatingDragPreview::CFloatingDragPreview(QWidget* Content, QWidget* parent) : - QWidget(parent), + QWidget((parent && !CDockManager::testConfigFlag(CDockManager::DragPreviewHasWindowFrame)) + ? parent->window() + : parent), d(new FloatingDragPreviewPrivate(this)) { d->Content = Content; @@ -287,20 +293,21 @@ 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 { - setWindowFlags(Qt::Tool | Qt::FramelessWindowHint); setAttribute(Qt::WA_NoSystemBackground); setAttribute(Qt::WA_TranslucentBackground); + // Let releases over visible drop indicators fall through to them. + setAttribute(Qt::WA_TransparentForMouseEvents); } -#if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS) - 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 // operations @@ -352,10 +359,20 @@ CFloatingDragPreview::~CFloatingDragPreview() //============================================================================ void CFloatingDragPreview::moveFloating() { - int BorderSize = (frameSize().width() - size().width()) / 2; - const QPoint moveToPos = QCursor::pos() - d->DragStartMousePosition + const int BorderSize = (frameSize().width() - size().width()) / 2; + const QPoint TargetGlobal = QCursor::pos() - d->DragStartMousePosition - QPoint(BorderSize, 0); - move(moveToPos); + // Wayland: when we're a child widget (frameless config), translate + // the screen-coord target into parent-local space so move() actually + // places us under the cursor. + if (isWindow() || !parentWidget()) + { + move(TargetGlobal); + } + else + { + move(parentWidget()->mapFromGlobal(TargetGlobal)); + } d->updateDropOverlays(QCursor::pos()); } @@ -370,7 +387,7 @@ void CFloatingDragPreview::startFloating(const QPoint &DragStartMousePos, d->DragStartMousePosition = DragStartMousePos; moveFloating(); show(); - + raise(); }