From ebfa8ec448c7d4ee2f1bf6b463f1dbf2e65db060 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 23 Jul 2024 22:12:27 -0700 Subject: [PATCH] core/popupanchor: rework popup anchoring and add PopupAnchor --- src/core/CMakeLists.txt | 2 + src/core/module.md | 2 + src/core/popupanchor.cpp | 275 ++++++++++++++++++++++++++++++++++++ src/core/popupanchor.hpp | 157 ++++++++++++++++++++ src/core/popupwindow.cpp | 167 ++++++++++------------ src/core/popupwindow.hpp | 38 +++-- src/core/types.cpp | 23 +++ src/core/types.hpp | 54 +++++++ src/wayland/CMakeLists.txt | 2 + src/wayland/init.cpp | 9 +- src/wayland/popupanchor.cpp | 99 +++++++++++++ src/wayland/popupanchor.hpp | 16 +++ src/wayland/xdgshell.cpp | 14 ++ src/wayland/xdgshell.hpp | 20 +++ 14 files changed, 770 insertions(+), 108 deletions(-) create mode 100644 src/core/popupanchor.cpp create mode 100644 src/core/popupanchor.hpp create mode 100644 src/core/types.cpp create mode 100644 src/core/types.hpp create mode 100644 src/wayland/popupanchor.cpp create mode 100644 src/wayland/popupanchor.hpp create mode 100644 src/wayland/xdgshell.cpp create mode 100644 src/wayland/xdgshell.hpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 6eace03b..b70681bc 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -33,6 +33,8 @@ qt_add_library(quickshell-core STATIC platformmenu.cpp qsmenu.cpp retainable.cpp + popupanchor.cpp + types.cpp ) set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}") diff --git a/src/core/module.md b/src/core/module.md index f0d296a5..9bf7bf25 100644 --- a/src/core/module.md +++ b/src/core/module.md @@ -24,5 +24,7 @@ headers = [ "objectrepeater.hpp", "qsmenu.hpp", "retainable.hpp", + "popupanchor.hpp", + "types.hpp", ] ----- diff --git a/src/core/popupanchor.cpp b/src/core/popupanchor.cpp new file mode 100644 index 00000000..5700224f --- /dev/null +++ b/src/core/popupanchor.cpp @@ -0,0 +1,275 @@ +#include "popupanchor.hpp" + +#include +#include +#include +#include + +#include "proxywindow.hpp" +#include "types.hpp" +#include "windowinterface.hpp" + +bool PopupAnchorState::operator==(const PopupAnchorState& other) const { + return this->rect == other.rect && this->edges == other.edges && this->gravity == other.gravity + && this->adjustment == other.adjustment && this->anchorpoint == other.anchorpoint; +} + +bool PopupAnchor::isDirty() const { + return !this->lastState.has_value() || this->state != this->lastState.value(); +} + +void PopupAnchor::markClean() { this->lastState = this->state; } +void PopupAnchor::markDirty() { this->lastState.reset(); } + +QObject* PopupAnchor::window() const { return this->mWindow; } +ProxyWindowBase* PopupAnchor::proxyWindow() const { return this->mProxyWindow; } + +QWindow* PopupAnchor::backingWindow() const { + return this->mProxyWindow ? this->mProxyWindow->backingWindow() : nullptr; +} + +void PopupAnchor::setWindow(QObject* window) { + if (window == this->mWindow) return; + + if (this->mWindow) { + QObject::disconnect(this->mWindow, nullptr, this, nullptr); + QObject::disconnect(this->mProxyWindow, nullptr, this, nullptr); + } + + if (window) { + if (auto* proxy = qobject_cast(window)) { + this->mProxyWindow = proxy; + } else if (auto* interface = qobject_cast(window)) { + this->mProxyWindow = interface->proxyWindow(); + } else { + qWarning() << "Tried to set popup anchor window to" << window + << "which is not a quickshell window."; + goto setnull; + } + + this->mWindow = window; + + QObject::connect(this->mWindow, &QObject::destroyed, this, &PopupAnchor::onWindowDestroyed); + + QObject::connect( + this->mProxyWindow, + &ProxyWindowBase::backerVisibilityChanged, + this, + &PopupAnchor::backingWindowVisibilityChanged + ); + + emit this->windowChanged(); + emit this->backingWindowVisibilityChanged(); + + return; + } + +setnull: + if (this->mWindow) { + this->mWindow = nullptr; + this->mProxyWindow = nullptr; + + emit this->windowChanged(); + emit this->backingWindowVisibilityChanged(); + } +} + +void PopupAnchor::onWindowDestroyed() { + this->mWindow = nullptr; + this->mProxyWindow = nullptr; + emit this->windowChanged(); + emit this->backingWindowVisibilityChanged(); +} + +Box PopupAnchor::rect() const { return this->state.rect; } + +void PopupAnchor::setRect(Box rect) { + if (rect == this->state.rect) return; + if (rect.w <= 0) rect.w = 1; + if (rect.h <= 0) rect.h = 1; + + this->state.rect = rect; + emit this->rectChanged(); +} + +Edges::Flags PopupAnchor::edges() const { return this->state.edges; } + +void PopupAnchor::setEdges(Edges::Flags edges) { + if (edges == this->state.edges) return; + + if (Edges::isOpposing(edges)) { + qWarning() << "Cannot set opposing edges for anchor edges. Tried to set" << edges; + return; + } + + this->state.edges = edges; + emit this->edgesChanged(); +} + +Edges::Flags PopupAnchor::gravity() const { return this->state.gravity; } + +void PopupAnchor::setGravity(Edges::Flags gravity) { + if (gravity == this->state.gravity) return; + + if (Edges::isOpposing(gravity)) { + qWarning() << "Cannot set opposing edges for anchor gravity. Tried to set" << gravity; + return; + } + + this->state.gravity = gravity; + emit this->gravityChanged(); +} + +PopupAdjustment::Flags PopupAnchor::adjustment() const { return this->state.adjustment; } + +void PopupAnchor::setAdjustment(PopupAdjustment::Flags adjustment) { + if (adjustment == this->state.adjustment) return; + this->state.adjustment = adjustment; + emit this->adjustmentChanged(); +} + +void PopupAnchor::updateAnchorpoint(const QPoint& anchorpoint) { + this->state.anchorpoint = anchorpoint; +} + +static PopupPositioner* POSITIONER = nullptr; // NOLINT + +void PopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool onlyIfDirty) { + auto* parentWindow = window->transientParent(); + if (!parentWindow) { + qFatal() << "Cannot reposition popup that does not have a transient parent."; + } + + auto adjustment = anchor->adjustment(); + auto screenGeometry = parentWindow->screen()->geometry(); + auto parentGeometry = parentWindow->geometry(); + auto windowGeometry = window->geometry(); + auto anchorRectGeometry = anchor->rect().qrect().translated(parentGeometry.topLeft()); + + auto anchorEdges = anchor->edges(); + auto anchorGravity = anchor->gravity(); + + auto width = windowGeometry.width(); + auto height = windowGeometry.height(); + + auto anchorX = anchorEdges.testFlag(Edges::Left) ? anchorRectGeometry.left() + : anchorEdges.testFlag(Edges::Right) ? anchorRectGeometry.right() + : anchorRectGeometry.center().x(); + + auto anchorY = anchorEdges.testFlag(Edges::Top) ? anchorRectGeometry.top() + : anchorEdges.testFlag(Edges::Bottom) ? anchorRectGeometry.bottom() + : anchorRectGeometry.center().y(); + + anchor->updateAnchorpoint({anchorX, anchorY}); + if (onlyIfDirty && !anchor->isDirty()) return; + anchor->markClean(); + + auto calcEffectiveX = [&]() { + return anchorGravity.testFlag(Edges::Left) ? anchorX - windowGeometry.width() + 1 + : anchorGravity.testFlag(Edges::Right) ? anchorX + : anchorX - windowGeometry.width() / 2; + }; + + auto calcEffectiveY = [&]() { + return anchorGravity.testFlag(Edges::Top) ? anchorY - windowGeometry.height() + 1 + : anchorGravity.testFlag(Edges::Bottom) ? anchorY + : anchorY - windowGeometry.height() / 2; + }; + + auto effectiveX = calcEffectiveX(); + auto effectiveY = calcEffectiveY(); + + if (adjustment.testFlag(PopupAdjustment::FlipX)) { + if (anchorGravity.testFlag(Edges::Left)) { + if (effectiveX < screenGeometry.left()) { + anchorGravity = anchorGravity ^ Edges::Left | Edges::Right; + anchorX = anchorRectGeometry.right(); + effectiveX = calcEffectiveX(); + } + } else if (anchorGravity.testFlag(Edges::Right)) { + if (effectiveX + windowGeometry.width() > screenGeometry.right()) { + anchorGravity = anchorGravity ^ Edges::Right | Edges::Left; + anchorX = anchorRectGeometry.left(); + effectiveX = calcEffectiveX(); + } + } + } + + if (adjustment.testFlag(PopupAdjustment::FlipY)) { + if (anchorGravity.testFlag(Edges::Top)) { + if (effectiveY < screenGeometry.top()) { + anchorGravity = anchorGravity ^ Edges::Top | Edges::Bottom; + effectiveY = calcEffectiveY(); + } + } else if (anchorGravity.testFlag(Edges::Bottom)) { + if (effectiveY + windowGeometry.height() > screenGeometry.bottom()) { + anchorGravity = anchorGravity ^ Edges::Bottom | Edges::Top; + effectiveY = calcEffectiveY(); + } + } + } + + // Slide order is important for the case where the window is too large to fit on screen. + if (adjustment.testFlag(PopupAdjustment::SlideX)) { + if (effectiveX + windowGeometry.width() > screenGeometry.right()) { + effectiveX = screenGeometry.right() - windowGeometry.width() + 1; + } + + if (effectiveX < screenGeometry.left()) { + effectiveX = screenGeometry.left(); + } + } + + if (adjustment.testFlag(PopupAdjustment::SlideY)) { + if (effectiveY + windowGeometry.height() > screenGeometry.bottom()) { + effectiveY = screenGeometry.bottom() - windowGeometry.height() + 1; + } + + if (effectiveY < screenGeometry.top()) { + effectiveY = screenGeometry.top(); + } + } + + if (adjustment.testFlag(PopupAdjustment::ResizeX)) { + if (effectiveX < screenGeometry.left()) { + auto diff = screenGeometry.left() - effectiveX; + effectiveX = screenGeometry.left(); + width -= diff; + } + + auto effectiveX2 = effectiveX + windowGeometry.width(); + if (effectiveX2 > screenGeometry.right()) { + width -= effectiveX2 - screenGeometry.right() - 1; + } + } + + if (adjustment.testFlag(PopupAdjustment::ResizeY)) { + if (effectiveY < screenGeometry.top()) { + auto diff = screenGeometry.top() - effectiveY; + effectiveY = screenGeometry.top(); + height -= diff; + } + + auto effectiveY2 = effectiveY + windowGeometry.height(); + if (effectiveY2 > screenGeometry.bottom()) { + height -= effectiveY2 - screenGeometry.bottom() - 1; + } + } + + window->setGeometry({effectiveX, effectiveY, width, height}); +} + +bool PopupPositioner::shouldRepositionOnMove() const { return true; } + +PopupPositioner* PopupPositioner::instance() { + if (POSITIONER == nullptr) { + POSITIONER = new PopupPositioner(); + } + + return POSITIONER; +} + +void PopupPositioner::setInstance(PopupPositioner* instance) { + delete POSITIONER; + POSITIONER = instance; +} diff --git a/src/core/popupanchor.hpp b/src/core/popupanchor.hpp new file mode 100644 index 00000000..04d89f47 --- /dev/null +++ b/src/core/popupanchor.hpp @@ -0,0 +1,157 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "doc.hpp" +#include "proxywindow.hpp" +#include "types.hpp" + +///! Adjustment strategy for popups that do not fit on screen. +/// Adjustment strategy for popups. See @@PopupAnchor.adjustment. +/// +/// Adjustment flags can be combined with the `|` operator. +/// +/// `Flip` will be applied first, then `Slide`, then `Resize`. +namespace PopupAdjustment { // NOLINT +Q_NAMESPACE; +QML_ELEMENT; + +enum Enum { + None = 0, + /// If the X axis is constrained, the popup will slide along the X axis until it fits onscreen. + SlideX = 1, + /// If the Y axis is constrained, the popup will slide along the Y axis until it fits onscreen. + SlideY = 2, + /// Alias for `SlideX | SlideY`. + Slide = SlideX | SlideY, + /// If the X axis is constrained, the popup will invert its horizontal gravity if any. + FlipX = 4, + /// If the Y axis is constrained, the popup will invert its vertical gravity if any. + FlipY = 8, + /// Alias for `FlipX | FlipY`. + Flip = FlipX | FlipY, + /// If the X axis is constrained, the width of the popup will be reduced to fit on screen. + ResizeX = 16, + /// If the Y axis is constrained, the height of the popup will be reduced to fit on screen. + ResizeY = 32, + /// Alias for `ResizeX | ResizeY` + Resize = ResizeX | ResizeY, + /// Alias for `Flip | Slide | Resize`. + All = Slide | Flip | Resize, +}; +Q_ENUM_NS(Enum); +Q_DECLARE_FLAGS(Flags, Enum); + +} // namespace PopupAdjustment + +Q_DECLARE_OPERATORS_FOR_FLAGS(PopupAdjustment::Flags); + +struct PopupAnchorState { + bool operator==(const PopupAnchorState& other) const; + + Box rect = {0, 0, 1, 1}; + Edges::Flags edges = Edges::Top | Edges::Left; + Edges::Flags gravity = Edges::Bottom | Edges::Right; + PopupAdjustment::Flags adjustment = PopupAdjustment::Slide; + QPoint anchorpoint; +}; + +///! Anchorpoint or positioner for popup windows. +class PopupAnchor: public QObject { + Q_OBJECT; + // clang-format off + /// The window to anchor / attach the popup to. + Q_PROPERTY(QObject* window READ window WRITE setWindow NOTIFY windowChanged); + /// The anchorpoints the popup will attach to. Which anchors will be used is + /// determined by the @@edges, @@gravity, and @@adjustment. + /// + /// If you leave @@edges, @@gravity and @@adjustment at their default values, + /// setting more than `x` and `y` does not matter. + /// + /// > [!INFO] The anchor rect cannot be smaller than 1x1 pixels. + Q_PROPERTY(Box rect READ rect WRITE setRect NOTIFY rectChanged); + /// The point on the anchor rectangle the popup should anchor to. + /// Opposing edges suchs as `Edges.Left | Edges.Right` are not allowed. + /// + /// Defaults to `Edges.Top | Edges.Left`. + Q_PROPERTY(Edges::Flags edges READ edges WRITE setEdges NOTIFY edgesChanged); + /// The direction the popup should expand towards, relative to the anchorpoint. + /// Opposing edges suchs as `Edges.Left | Edges.Right` are not allowed. + /// + /// Defaults to `Edges.Bottom | Edges.Right`. + Q_PROPERTY(Edges::Flags gravity READ gravity WRITE setGravity NOTIFY gravityChanged); + /// The strategy used to adjust the popup's position if it would otherwise not fit on screen, + /// based on the anchor @@rect, preferred @@edges, and @@gravity. + /// + /// See the documentation for @@PopupAdjustment for details. + Q_PROPERTY(PopupAdjustment::Flags adjustment READ adjustment WRITE setAdjustment NOTIFY adjustmentChanged); + // clang-format on + QML_ELEMENT; + QML_UNCREATABLE(""); + +public: + explicit PopupAnchor(QObject* parent): QObject(parent) {} + + [[nodiscard]] bool isDirty() const; + void markClean(); + void markDirty(); + + [[nodiscard]] QObject* window() const; + [[nodiscard]] ProxyWindowBase* proxyWindow() const; + [[nodiscard]] QWindow* backingWindow() const; + void setWindow(QObject* window); + + [[nodiscard]] Box rect() const; + void setRect(Box rect); + + [[nodiscard]] Edges::Flags edges() const; + void setEdges(Edges::Flags edges); + + [[nodiscard]] Edges::Flags gravity() const; + void setGravity(Edges::Flags gravity); + + [[nodiscard]] PopupAdjustment::Flags adjustment() const; + void setAdjustment(PopupAdjustment::Flags adjustment); + + void updateAnchorpoint(const QPoint& anchorpoint); + +signals: + void windowChanged(); + QSDOC_HIDE void backingWindowVisibilityChanged(); + void rectChanged(); + void edgesChanged(); + void gravityChanged(); + void adjustmentChanged(); + +private slots: + void onWindowDestroyed(); + +private: + QObject* mWindow = nullptr; + ProxyWindowBase* mProxyWindow = nullptr; + PopupAnchorState state; + std::optional lastState; +}; + +class PopupPositioner { +public: + explicit PopupPositioner() = default; + virtual ~PopupPositioner() = default; + Q_DISABLE_COPY_MOVE(PopupPositioner); + + virtual void reposition(PopupAnchor* anchor, QWindow* window, bool onlyIfDirty = true); + [[nodiscard]] virtual bool shouldRepositionOnMove() const; + + static PopupPositioner* instance(); + static void setInstance(PopupPositioner* instance); +}; diff --git a/src/core/popupwindow.cpp b/src/core/popupwindow.cpp index 547bbe36..fa5d7892 100644 --- a/src/core/popupwindow.cpp +++ b/src/core/popupwindow.cpp @@ -4,86 +4,66 @@ #include #include #include -#include #include +#include +#include "popupanchor.hpp" #include "proxywindow.hpp" #include "qmlscreen.hpp" #include "windowinterface.hpp" ProxyPopupWindow::ProxyPopupWindow(QObject* parent): ProxyWindowBase(parent) { this->mVisible = false; + // clang-format off + QObject::connect(&this->mAnchor, &PopupAnchor::windowChanged, this, &ProxyPopupWindow::parentWindowChanged); + QObject::connect(&this->mAnchor, &PopupAnchor::rectChanged, this, &ProxyPopupWindow::reposition); + QObject::connect(&this->mAnchor, &PopupAnchor::edgesChanged, this, &ProxyPopupWindow::reposition); + QObject::connect(&this->mAnchor, &PopupAnchor::gravityChanged, this, &ProxyPopupWindow::reposition); + QObject::connect(&this->mAnchor, &PopupAnchor::adjustmentChanged, this, &ProxyPopupWindow::reposition); + QObject::connect(&this->mAnchor, &PopupAnchor::backingWindowVisibilityChanged, this, &ProxyPopupWindow::onParentUpdated); + // clang-format on } void ProxyPopupWindow::completeWindow() { this->ProxyWindowBase::completeWindow(); + QObject::connect( + this->window, + &QWindow::visibleChanged, + this, + &ProxyPopupWindow::onVisibleChanged + ); this->window->setFlag(Qt::ToolTip); - this->updateTransientParent(); } -void ProxyPopupWindow::postCompleteWindow() { this->ProxyWindowBase::setVisible(this->mVisible); } - -bool ProxyPopupWindow::deleteOnInvisible() const { - // Currently crashes in normal mode, do not have the time to debug it now. - return true; -} - -qint32 ProxyPopupWindow::x() const { - // QTBUG-121550 - auto basepos = this->mParentProxyWindow == nullptr ? 0 : this->mParentProxyWindow->x(); - return basepos + this->mRelativeX; -} +void ProxyPopupWindow::postCompleteWindow() { this->updateTransientParent(); } void ProxyPopupWindow::setParentWindow(QObject* parent) { - if (parent == this->mParentWindow) return; - - if (this->mParentWindow != nullptr) { - QObject::disconnect(this->mParentWindow, nullptr, this, nullptr); - QObject::disconnect(this->mParentProxyWindow, nullptr, this, nullptr); - } - - if (parent == nullptr) { - this->mParentWindow = nullptr; - this->mParentProxyWindow = nullptr; - } else { - if (auto* proxy = qobject_cast(parent)) { - this->mParentProxyWindow = proxy; - } else if (auto* interface = qobject_cast(parent)) { - this->mParentProxyWindow = interface->proxyWindow(); - } else { - qWarning() << "Tried to set popup parent window to something that is not a quickshell window:" - << parent; - this->mParentWindow = nullptr; - this->mParentProxyWindow = nullptr; - this->updateTransientParent(); - return; - } - - this->mParentWindow = parent; - - // clang-format off - QObject::connect(this->mParentWindow, &QObject::destroyed, this, &ProxyPopupWindow::onParentDestroyed); - - QObject::connect(this->mParentProxyWindow, &ProxyWindowBase::xChanged, this, &ProxyPopupWindow::updateX); - QObject::connect(this->mParentProxyWindow, &ProxyWindowBase::yChanged, this, &ProxyPopupWindow::updateY); - QObject::connect(this->mParentProxyWindow, &ProxyWindowBase::backerVisibilityChanged, this, &ProxyPopupWindow::onParentUpdated); - // clang-format on - } - - this->updateTransientParent(); + qWarning() << "PopupWindow.parentWindow is deprecated. Use PopupWindow.anchor.window."; + this->mAnchor.setWindow(parent); } -QObject* ProxyPopupWindow::parentWindow() const { return this->mParentWindow; } +QObject* ProxyPopupWindow::parentWindow() const { + qWarning() << "PopupWindow.parentWindow is deprecated. Use PopupWindow.anchor.window."; + return this->mAnchor.window(); +} void ProxyPopupWindow::updateTransientParent() { - this->updateX(); - this->updateY(); + auto* bw = this->mAnchor.backingWindow(); - if (this->window != nullptr) { - this->window->setTransientParent( - this->mParentProxyWindow == nullptr ? nullptr : this->mParentProxyWindow->backingWindow() - ); + if (this->window != nullptr && bw != this->window->transientParent()) { + if (this->window->transientParent()) { + QObject::disconnect(this->window->transientParent(), nullptr, this, nullptr); + } + + if (bw && PopupPositioner::instance()->shouldRepositionOnMove()) { + QObject::connect(bw, &QWindow::xChanged, this, &ProxyPopupWindow::reposition); + QObject::connect(bw, &QWindow::yChanged, this, &ProxyPopupWindow::reposition); + QObject::connect(bw, &QWindow::widthChanged, this, &ProxyPopupWindow::reposition); + QObject::connect(bw, &QWindow::heightChanged, this, &ProxyPopupWindow::reposition); + } + + this->window->setTransientParent(bw); } this->updateVisible(); @@ -91,13 +71,6 @@ void ProxyPopupWindow::updateTransientParent() { void ProxyPopupWindow::onParentUpdated() { this->updateTransientParent(); } -void ProxyPopupWindow::onParentDestroyed() { - this->mParentWindow = nullptr; - this->mParentProxyWindow = nullptr; - this->updateVisible(); - emit this->parentWindowChanged(); -} - void ProxyPopupWindow::setScreen(QuickshellScreenInfo* /*unused*/) { qWarning() << "Cannot set screen of popup window, as that is controlled by the parent window"; } @@ -109,53 +82,55 @@ void ProxyPopupWindow::setVisible(bool visible) { } void ProxyPopupWindow::updateVisible() { - auto target = this->wantsVisible && this->mParentWindow != nullptr - && this->mParentProxyWindow->isVisibleDirect(); + auto target = this->wantsVisible && this->mAnchor.window() != nullptr + && this->mAnchor.proxyWindow()->isVisibleDirect(); if (target && this->window != nullptr && !this->window->isVisible()) { - this->updateX(); // QTBUG-121550 + PopupPositioner::instance()->reposition(&this->mAnchor, this->window); } this->ProxyWindowBase::setVisible(target); } -void ProxyPopupWindow::setRelativeX(qint32 x) { - if (x == this->mRelativeX) return; - this->mRelativeX = x; - this->updateX(); +void ProxyPopupWindow::onVisibleChanged() { + // If the window was made invisible without its parent becoming invisible + // the compositor probably destroyed it. Without this the window won't ever + // be able to become visible again. + if (this->window->transientParent() && this->window->transientParent()->isVisible()) { + this->wantsVisible = this->window->isVisible(); + } } -qint32 ProxyPopupWindow::relativeX() const { return this->mRelativeX; } +void ProxyPopupWindow::setRelativeX(qint32 x) { + qWarning() << "PopupWindow.relativeX is deprecated. Use PopupWindow.anchor.rect.x."; + auto rect = this->mAnchor.rect(); + if (x == rect.x) return; + rect.x = x; + this->mAnchor.setRect(rect); +} + +qint32 ProxyPopupWindow::relativeX() const { + qWarning() << "PopupWindow.relativeX is deprecated. Use PopupWindow.anchor.rect.x."; + return this->mAnchor.rect().x; +} void ProxyPopupWindow::setRelativeY(qint32 y) { - if (y == this->mRelativeY) return; - this->mRelativeY = y; - this->updateY(); + qWarning() << "PopupWindow.relativeY is deprecated. Use PopupWindow.anchor.rect.y."; + auto rect = this->mAnchor.rect(); + if (y == rect.y) return; + rect.y = y; + this->mAnchor.setRect(rect); } -qint32 ProxyPopupWindow::relativeY() const { return this->mRelativeY; } - -void ProxyPopupWindow::updateX() { - if (this->mParentWindow == nullptr || this->window == nullptr) return; - - auto target = this->x() - 1; // QTBUG-121550 - - auto reshow = this->isVisibleDirect() && (this->window->x() != target && this->x() != target); - if (reshow) this->setVisibleDirect(false); - if (this->window != nullptr) this->window->setX(target); - if (reshow && this->wantsVisible) this->setVisibleDirect(true); +qint32 ProxyPopupWindow::relativeY() const { + qWarning() << "PopupWindow.relativeY is deprecated. Use PopupWindow.anchor.rect.y."; + return this->mAnchor.rect().y; } -void ProxyPopupWindow::updateY() { - if (this->mParentWindow == nullptr || this->window == nullptr) return; +PopupAnchor* ProxyPopupWindow::anchor() { return &this->mAnchor; } - auto target = this->mParentProxyWindow->y() + this->relativeY(); - - auto reshow = this->isVisibleDirect() && this->window->y() != target; - if (reshow) { - this->setVisibleDirect(false); - this->updateX(); // QTBUG-121550 +void ProxyPopupWindow::reposition() { + if (this->window != nullptr) { + PopupPositioner::instance()->reposition(&this->mAnchor, this->window); } - if (this->window != nullptr) this->window->setY(target); - if (reshow && this->wantsVisible) this->setVisibleDirect(true); } diff --git a/src/core/popupwindow.hpp b/src/core/popupwindow.hpp index 7815d400..47db4038 100644 --- a/src/core/popupwindow.hpp +++ b/src/core/popupwindow.hpp @@ -7,6 +7,7 @@ #include #include "doc.hpp" +#include "popupanchor.hpp" #include "proxywindow.hpp" #include "qmlscreen.hpp" #include "windowinterface.hpp" @@ -42,15 +43,37 @@ class ProxyPopupWindow: public ProxyWindowBase { QSDOC_BASECLASS(WindowInterface); Q_OBJECT; // clang-format off + /// > [!ERROR] Deprecated in favor of `anchor.window`. + /// /// The parent window of this popup. /// /// Changing this property reparents the popup. Q_PROPERTY(QObject* parentWindow READ parentWindow WRITE setParentWindow NOTIFY parentWindowChanged); + /// > [!ERROR] Deprecated in favor of `anchor.rect.x`. + /// /// The X position of the popup relative to the parent window. Q_PROPERTY(qint32 relativeX READ relativeX WRITE setRelativeX NOTIFY relativeXChanged); + /// > [!ERROR] Deprecated in favor of `anchor.rect.y`. + /// /// The Y position of the popup relative to the parent window. Q_PROPERTY(qint32 relativeY READ relativeY WRITE setRelativeY NOTIFY relativeYChanged); + /// The popup's anchor / positioner relative to another window. The popup will not be + /// shown until it has a valid anchor relative to a window and @@visible is true. + /// + /// You can set properties of the anchor like so: + /// ```qml + /// PopupWindow { + /// anchor.window: parentwindow + /// // or + /// anchor { + /// window: parentwindow + /// } + /// } + /// ``` + Q_PROPERTY(PopupAnchor* anchor READ anchor CONSTANT); /// If the window is shown or hidden. Defaults to false. + /// + /// The popup will not be shown until @@anchor is valid, regardless of this property. QSDOC_PROPERTY_OVERRIDE(bool visible READ isVisible WRITE setVisible NOTIFY visibleChanged); /// The screen that the window currently occupies. /// @@ -64,13 +87,10 @@ public: void completeWindow() override; void postCompleteWindow() override; - [[nodiscard]] bool deleteOnInvisible() const override; void setScreen(QuickshellScreenInfo* screen) override; void setVisible(bool visible) override; - [[nodiscard]] qint32 x() const override; - [[nodiscard]] QObject* parentWindow() const; void setParentWindow(QObject* parent); @@ -80,25 +100,23 @@ public: [[nodiscard]] qint32 relativeY() const; void setRelativeY(qint32 y); + [[nodiscard]] PopupAnchor* anchor(); + signals: void parentWindowChanged(); void relativeXChanged(); void relativeYChanged(); private slots: + void onVisibleChanged(); void onParentUpdated(); - void onParentDestroyed(); - void updateX(); - void updateY(); + void reposition(); private: QQuickWindow* parentBackingWindow(); void updateTransientParent(); void updateVisible(); - QObject* mParentWindow = nullptr; - ProxyWindowBase* mParentProxyWindow = nullptr; - qint32 mRelativeX = 0; - qint32 mRelativeY = 0; + PopupAnchor mAnchor {this}; bool wantsVisible = false; }; diff --git a/src/core/types.cpp b/src/core/types.cpp new file mode 100644 index 00000000..5ed63a02 --- /dev/null +++ b/src/core/types.cpp @@ -0,0 +1,23 @@ +#include "types.hpp" + +#include +#include +#include + +QRect Box::qrect() const { return {this->x, this->y, this->w, this->h}; } + +bool Box::operator==(const Box& other) const { + return this->x == other.x && this->y == other.y && this->w == other.w && this->h == other.h; +} + +QDebug operator<<(QDebug debug, const Box& box) { + auto saver = QDebugStateSaver(debug); + debug.nospace() << "Box(" << box.x << ',' << box.y << ' ' << box.w << 'x' << box.h << ')'; + return debug; +} + +Qt::Edges Edges::toQt(Edges::Flags edges) { return Qt::Edges(edges.toInt()); } + +bool Edges::isOpposing(Edges::Flags edges) { + return edges.testFlags(Edges::Top | Edges::Bottom) || edges.testFlags(Edges::Left | Edges::Right); +} diff --git a/src/core/types.hpp b/src/core/types.hpp new file mode 100644 index 00000000..11474f3d --- /dev/null +++ b/src/core/types.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include +#include + +class Box { + Q_GADGET; + Q_PROPERTY(qint32 x MEMBER x); + Q_PROPERTY(qint32 y MEMBER y); + Q_PROPERTY(qint32 w MEMBER w); + Q_PROPERTY(qint32 h MEMBER h); + Q_PROPERTY(qint32 width MEMBER w); + Q_PROPERTY(qint32 height MEMBER h); + QML_VALUE_TYPE(box); + +public: + explicit Box() = default; + Box(qint32 x, qint32 y, qint32 w, qint32 h): x(x), y(y), w(w), h(h) {} + bool operator==(const Box& other) const; + + qint32 x = 0; + qint32 y = 0; + qint32 w = 0; + qint32 h = 0; + + [[nodiscard]] QRect qrect() const; +}; + +QDebug operator<<(QDebug debug, const Box& box); + +///! Top Left Right Bottom flags. +/// Edge flags can be combined with the `|` operator. +namespace Edges { // NOLINT +Q_NAMESPACE; +QML_NAMED_ELEMENT(Edges); + +enum Enum { + None = 0, + Top = Qt::TopEdge, + Left = Qt::LeftEdge, + Right = Qt::RightEdge, + Bottom = Qt::BottomEdge, +}; +Q_ENUM_NS(Enum); +Q_DECLARE_FLAGS(Flags, Enum); + +Qt::Edges toQt(Flags edges); +bool isOpposing(Flags edges); + +}; // namespace Edges + +Q_DECLARE_OPERATORS_FOR_FLAGS(Edges::Flags); diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index a57c5579..d8702b7a 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -52,6 +52,8 @@ endfunction() qt_add_library(quickshell-wayland STATIC platformmenu.cpp + popupanchor.cpp + xdgshell.cpp ) # required to make sure the constructor is linked diff --git a/src/wayland/init.cpp b/src/wayland/init.cpp index b43179f7..1ad51cea 100644 --- a/src/wayland/init.cpp +++ b/src/wayland/init.cpp @@ -4,12 +4,14 @@ #include #include "../core/plugin.hpp" -#include "platformmenu.hpp" #ifdef QS_WAYLAND_WLR_LAYERSHELL #include "wlr_layershell.hpp" #endif +void installPlatformMenuHook(); +void installPopupPositioner(); + namespace { class WaylandPlugin: public QuickshellPlugin { @@ -27,7 +29,10 @@ class WaylandPlugin: public QuickshellPlugin { return isWayland; } - void init() override { installPlatformMenuHook(); } + void init() override { + installPlatformMenuHook(); + installPopupPositioner(); + } void registerTypes() override { #ifdef QS_WAYLAND_WLR_LAYERSHELL diff --git a/src/wayland/popupanchor.cpp b/src/wayland/popupanchor.cpp new file mode 100644 index 00000000..bf6f9850 --- /dev/null +++ b/src/wayland/popupanchor.cpp @@ -0,0 +1,99 @@ +#include "popupanchor.hpp" + +#include +#include +#include +#include +#include + +#include "../core/popupanchor.hpp" +#include "../core/types.hpp" +#include "xdgshell.hpp" + +using QtWaylandClient::QWaylandWindow; +using XdgPositioner = QtWayland::xdg_positioner; +using qs::wayland::xdg_shell::XdgWmBase; + +void WaylandPopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool onlyIfDirty) { + if (onlyIfDirty && !anchor->isDirty()) return; + + auto* waylandWindow = dynamic_cast(window->handle()); + auto* popupRole = waylandWindow ? waylandWindow->surfaceRole<::xdg_popup>() : nullptr; + + anchor->markClean(); + + if (popupRole) { + auto* xdgWmBase = XdgWmBase::instance(); + + if (xdgWmBase->QtWayland::xdg_wm_base::version() < XDG_POPUP_REPOSITION_SINCE_VERSION) { + window->setVisible(false); + WaylandPopupPositioner::setFlags(anchor, window); + window->setVisible(true); + return; + } + + auto positioner = XdgPositioner(xdgWmBase->create_positioner()); + + positioner.set_constraint_adjustment(anchor->adjustment().toInt()); + + auto anchorRect = anchor->rect(); + positioner.set_anchor_rect(anchorRect.x, anchorRect.y, anchorRect.w, anchorRect.h); + + XdgPositioner::anchor anchorFlag = XdgPositioner::anchor_none; + switch (anchor->edges()) { + case Edges::Top: anchorFlag = XdgPositioner::anchor_top; break; + case Edges::Top | Edges::Right: anchorFlag = XdgPositioner::anchor_top_right; break; + case Edges::Right: anchorFlag = XdgPositioner::anchor_right; break; + case Edges::Bottom | Edges::Right: anchorFlag = XdgPositioner::anchor_bottom_right; break; + case Edges::Bottom: anchorFlag = XdgPositioner::anchor_bottom; break; + case Edges::Bottom | Edges::Left: anchorFlag = XdgPositioner::anchor_bottom_left; break; + case Edges::Left: anchorFlag = XdgPositioner::anchor_left; break; + case Edges::Top | Edges::Left: anchorFlag = XdgPositioner::anchor_top_left; break; + default: break; + } + + positioner.set_anchor(anchorFlag); + + XdgPositioner::gravity gravity = XdgPositioner::gravity_none; + switch (anchor->gravity()) { + case Edges::Top: gravity = XdgPositioner::gravity_top; break; + case Edges::Top | Edges::Right: gravity = XdgPositioner::gravity_top_right; break; + case Edges::Right: gravity = XdgPositioner::gravity_right; break; + case Edges::Bottom | Edges::Right: gravity = XdgPositioner::gravity_bottom_right; break; + case Edges::Bottom: gravity = XdgPositioner::gravity_bottom; break; + case Edges::Bottom | Edges::Left: gravity = XdgPositioner::gravity_bottom_left; break; + case Edges::Left: gravity = XdgPositioner::gravity_left; break; + case Edges::Top | Edges::Left: gravity = XdgPositioner::gravity_top_left; break; + default: break; + } + + positioner.set_gravity(gravity); + auto geometry = waylandWindow->geometry(); + positioner.set_size(geometry.width(), geometry.height()); + + // Note: this needs to be set for the initial position as well but no compositor + // supports it enough to test + positioner.set_reactive(); + + xdg_popup_reposition(popupRole, positioner.object(), 0); + + positioner.destroy(); + } else { + WaylandPopupPositioner::setFlags(anchor, window); + } +} + +// Should be false but nobody supports set_reactive. +// This just tries its best when something like a bar gets resized. +bool WaylandPopupPositioner::shouldRepositionOnMove() const { return true; } + +void WaylandPopupPositioner::setFlags(PopupAnchor* anchor, QWindow* window) { + // clang-format off + window->setProperty("_q_waylandPopupConstraintAdjustment", anchor->adjustment().toInt()); + window->setProperty("_q_waylandPopupAnchorRect", anchor->rect().qrect()); + window->setProperty("_q_waylandPopupAnchor", QVariant::fromValue(Edges::toQt(anchor->edges()))); + window->setProperty("_q_waylandPopupGravity", QVariant::fromValue(Edges::toQt(anchor->gravity()))); + // clang-format on +} + +void installPopupPositioner() { PopupPositioner::setInstance(new WaylandPopupPositioner()); } diff --git a/src/wayland/popupanchor.hpp b/src/wayland/popupanchor.hpp new file mode 100644 index 00000000..3e84e4b8 --- /dev/null +++ b/src/wayland/popupanchor.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +#include "../core/popupanchor.hpp" + +class WaylandPopupPositioner: public PopupPositioner { +public: + void reposition(PopupAnchor* anchor, QWindow* window, bool onlyIfDirty = true) override; + [[nodiscard]] bool shouldRepositionOnMove() const override; + +private: + static void setFlags(PopupAnchor* anchor, QWindow* window); +}; + +void installPopupPositioner(); diff --git a/src/wayland/xdgshell.cpp b/src/wayland/xdgshell.cpp new file mode 100644 index 00000000..8677d1b5 --- /dev/null +++ b/src/wayland/xdgshell.cpp @@ -0,0 +1,14 @@ +#include "xdgshell.hpp" + +#include + +namespace qs::wayland::xdg_shell { + +XdgWmBase::XdgWmBase(): QWaylandClientExtensionTemplate(6) { this->initialize(); } + +XdgWmBase* XdgWmBase::instance() { + static auto* instance = new XdgWmBase(); // NOLINT + return instance; +} + +} // namespace qs::wayland::xdg_shell diff --git a/src/wayland/xdgshell.hpp b/src/wayland/xdgshell.hpp new file mode 100644 index 00000000..735ba679 --- /dev/null +++ b/src/wayland/xdgshell.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +namespace qs::wayland::xdg_shell { + +// Hack that binds xdg_wm_base twice as QtWaylandXdgShell headers are not exported anywhere. + +class XdgWmBase + : public QWaylandClientExtensionTemplate + , public QtWayland::xdg_wm_base { +public: + static XdgWmBase* instance(); + +private: + explicit XdgWmBase(); +}; + +} // namespace qs::wayland::xdg_shell