diff --git a/src/core/popupanchor.cpp b/src/core/popupanchor.cpp index f72d5c59..32d36dc5 100644 --- a/src/core/popupanchor.cpp +++ b/src/core/popupanchor.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -25,14 +26,11 @@ bool PopupAnchor::isDirty() const { 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) { +void PopupAnchor::setWindowInternal(QObject* window) { if (window == this->mWindow) return; if (this->mWindow) { @@ -78,6 +76,27 @@ setnull: } } +void PopupAnchor::setWindow(QObject* window) { + this->setItem(nullptr); + this->setWindowInternal(window); +} + +void PopupAnchor::setItem(QQuickItem* item) { + if (item == this->mItem) return; + + if (this->mItem) { + QObject::disconnect(this->mItem, nullptr, this, nullptr); + } + + this->mItem = item; + this->onItemWindowChanged(); + + if (item) { + QObject::connect(item, &QObject::destroyed, this, &PopupAnchor::onItemDestroyed); + QObject::connect(item, &QQuickItem::windowChanged, this, &PopupAnchor::onItemWindowChanged); + } +} + void PopupAnchor::onWindowDestroyed() { this->mWindow = nullptr; this->mProxyWindow = nullptr; @@ -85,18 +104,41 @@ void PopupAnchor::onWindowDestroyed() { 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(); +void PopupAnchor::onItemDestroyed() { + this->mItem = nullptr; + emit this->itemChanged(); + this->setWindowInternal(nullptr); } -Edges::Flags PopupAnchor::edges() const { return this->state.edges; } +void PopupAnchor::onItemWindowChanged() { + if (auto* window = qobject_cast(this->mItem->window())) { + this->setWindowInternal(window->proxy()); + } else { + this->setWindowInternal(nullptr); + } +} + +void PopupAnchor::setRect(Box rect) { + if (rect.w <= 0) rect.w = 1; + if (rect.h <= 0) rect.h = 1; + if (rect == this->mUserRect) return; + + this->mUserRect = rect; + emit this->rectChanged(); + + this->setWindowRect(rect); +} + +void PopupAnchor::setWindowRect(Box rect) { + if (rect.w <= 0) rect.w = 1; + if (rect.h <= 0) rect.h = 1; + if (rect == this->state.rect) return; + + this->state.rect = rect; + emit this->windowRectChanged(); +} + +void PopupAnchor::resetRect() { this->mUserRect = Box(); } void PopupAnchor::setEdges(Edges::Flags edges) { if (edges == this->state.edges) return; @@ -110,8 +152,6 @@ void PopupAnchor::setEdges(Edges::Flags 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; @@ -124,8 +164,6 @@ void PopupAnchor::setGravity(Edges::Flags 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; @@ -137,6 +175,19 @@ void PopupAnchor::updatePlacement(const QPoint& anchorpoint, const QSize& size) this->state.size = size; } +void PopupAnchor::updateAnchor() { + if (this->mItem && this->mProxyWindow) { + auto rect = this->mProxyWindow->contentItem()->mapRectFromItem( + this->mItem, + this->mUserRect.isEmpty() ? this->mItem->boundingRect() : this->mUserRect.qrect() + ); + + this->setWindowRect(rect); + } + + emit this->anchoring(); +} + static PopupPositioner* POSITIONER = nullptr; // NOLINT void PopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool onlyIfDirty) { @@ -148,7 +199,7 @@ void PopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool only auto parentGeometry = parentWindow->geometry(); auto windowGeometry = window->geometry(); - emit anchor->anchoring(); + anchor->updateAnchor(); anchor->updatePlacement(parentGeometry.topLeft(), windowGeometry.size()); if (onlyIfDirty && !anchor->isDirty()) return; @@ -156,7 +207,7 @@ void PopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool only auto adjustment = anchor->adjustment(); auto screenGeometry = parentWindow->screen()->geometry(); - auto anchorRectGeometry = anchor->rect().qrect().translated(parentGeometry.topLeft()); + auto anchorRectGeometry = anchor->windowRect().qrect().translated(parentGeometry.topLeft()); auto anchorEdges = anchor->edges(); auto anchorGravity = anchor->gravity(); diff --git a/src/core/popupanchor.hpp b/src/core/popupanchor.hpp index 90ba697f..49668398 100644 --- a/src/core/popupanchor.hpp +++ b/src/core/popupanchor.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -72,25 +73,29 @@ struct PopupAnchorState { class PopupAnchor: public QObject { Q_OBJECT; // clang-format off - /// The window to anchor / attach the popup to. + /// The window to anchor / attach the popup to. Setting this property unsets @@item. 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. + /// The item to anchor / attach the popup to. Setting this property unsets @@window. + /// + /// The popup's position relative to its parent window is only calculated when it is + /// initially shown (directly before @@anchoring(s) is emitted), meaning its anchor + /// rectangle will be set relative to the item's position in the window at that time. + /// @@updateAnchor() can be called to update the anchor rectangle if the item's position + /// has changed. + /// + /// > [!NOTE] If a more flexible way to position a popup relative to an item is needed, + /// > set @@window to the item's parent window, and handle the @@anchoring signal to + /// > position the popup relative to the window's contentItem. + Q_PROPERTY(QQuickItem* item READ item WRITE setItem NOTIFY itemChanged); + /// The anchorpoints the popup will attach to, relative to @@item or @@window. + /// 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. The anchor rect cannot /// be smaller than 1x1 pixels. /// - /// > [!INFO] To position a popup relative to an item inside a window, - /// > you can use [coordinate mapping functions] (note the warning below). - /// - /// > [!WARNING] Using [coordinate mapping functions] in a binding to - /// > this property will position the anchor incorrectly. - /// > If you want to use them, do so in @@anchoring(s), or use - /// > @@TransformWatcher if you need real-time updates to mapped coordinates. - /// /// [coordinate mapping functions]: https://doc.qt.io/qt-6/qml-qtquick-item.html#mapFromItem-method - Q_PROPERTY(Box rect READ rect WRITE setRect NOTIFY rectChanged); + Q_PROPERTY(Box rect READ rect WRITE setRect RESET resetRect NOTIFY rectChanged); /// The point on the anchor rectangle the popup should anchor to. /// Opposing edges suchs as `Edges.Left | Edges.Right` are not allowed. /// @@ -113,25 +118,40 @@ class PopupAnchor: public QObject { public: explicit PopupAnchor(QObject* parent): QObject(parent) {} + /// Update the popup's anchor rect relative to its parent window. + /// + /// If anchored to an item, popups anchors will not automatically follow + /// the item if its position changes. This function can be called to + /// recalculate the anchors. + Q_INVOKABLE void updateAnchor(); + [[nodiscard]] bool isDirty() const; void markClean(); void markDirty(); - [[nodiscard]] QObject* window() const; - [[nodiscard]] ProxyWindowBase* proxyWindow() const; + [[nodiscard]] QObject* window() const { return this->mWindow; } + [[nodiscard]] ProxyWindowBase* proxyWindow() const { return this->mProxyWindow; } [[nodiscard]] QWindow* backingWindow() const; + void setWindowInternal(QObject* window); void setWindow(QObject* window); - [[nodiscard]] Box rect() const; - void setRect(Box rect); + [[nodiscard]] QQuickItem* item() const { return this->mItem; } + void setItem(QQuickItem* item); - [[nodiscard]] Edges::Flags edges() const; + [[nodiscard]] Box windowRect() const { return this->state.rect; } + void setWindowRect(Box rect); + + [[nodiscard]] Box rect() const { return this->mUserRect; } + void setRect(Box rect); + void resetRect(); + + [[nodiscard]] Edges::Flags edges() const { return this->state.edges; } void setEdges(Edges::Flags edges); - [[nodiscard]] Edges::Flags gravity() const; + [[nodiscard]] Edges::Flags gravity() const { return this->state.gravity; } void setGravity(Edges::Flags gravity); - [[nodiscard]] PopupAdjustment::Flags adjustment() const; + [[nodiscard]] PopupAdjustment::Flags adjustment() const { return this->state.adjustment; } void setAdjustment(PopupAdjustment::Flags adjustment); void updatePlacement(const QPoint& anchorpoint, const QSize& size); @@ -144,7 +164,9 @@ signals: void anchoring(); void windowChanged(); + void itemChanged(); QSDOC_HIDE void backingWindowVisibilityChanged(); + QSDOC_HIDE void windowRectChanged(); void rectChanged(); void edgesChanged(); void gravityChanged(); @@ -152,11 +174,15 @@ signals: private slots: void onWindowDestroyed(); + void onItemDestroyed(); + void onItemWindowChanged(); private: QObject* mWindow = nullptr; + QQuickItem* mItem = nullptr; ProxyWindowBase* mProxyWindow = nullptr; PopupAnchorState state; + Box mUserRect; std::optional lastState; }; diff --git a/src/core/types.hpp b/src/core/types.hpp index 0adc85c0..5c7bd2b1 100644 --- a/src/core/types.hpp +++ b/src/core/types.hpp @@ -44,6 +44,7 @@ public: qint32 h = 0; [[nodiscard]] QRect qrect() const; + [[nodiscard]] bool isEmpty() const { return this->w == 0 && this->h == 0; } }; QDebug operator<<(QDebug debug, const Box& box); diff --git a/src/wayland/popupanchor.cpp b/src/wayland/popupanchor.cpp index ca90dc13..1c9e1ccb 100644 --- a/src/wayland/popupanchor.cpp +++ b/src/wayland/popupanchor.cpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include @@ -21,7 +20,7 @@ void WaylandPopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bo auto* waylandWindow = dynamic_cast(window->handle()); auto* popupRole = waylandWindow ? waylandWindow->surfaceRole<::xdg_popup>() : nullptr; - emit anchor->anchoring(); + anchor->updateAnchor(); // If a popup becomes invisble after creation ensure the _q properties will // be set and not ignored because the rest is the same. @@ -44,7 +43,7 @@ void WaylandPopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bo positioner.set_constraint_adjustment(anchor->adjustment().toInt()); - auto anchorRect = anchor->rect(); + auto anchorRect = anchor->windowRect(); if (auto* p = window->transientParent()) { anchorRect.x = QHighDpi::toNativePixels(anchorRect.x, p); @@ -104,8 +103,8 @@ void WaylandPopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bo bool WaylandPopupPositioner::shouldRepositionOnMove() const { return true; } void WaylandPopupPositioner::setFlags(PopupAnchor* anchor, QWindow* window) { - emit anchor->anchoring(); - auto anchorRect = anchor->rect(); + anchor->updateAnchor(); + auto anchorRect = anchor->windowRect(); if (auto* p = window->transientParent()) { anchorRect.x = QHighDpi::toNativePixels(anchorRect.x, p); diff --git a/src/window/popupwindow.cpp b/src/window/popupwindow.cpp index 99c471c7..ec2be7ed 100644 --- a/src/window/popupwindow.cpp +++ b/src/window/popupwindow.cpp @@ -14,7 +14,7 @@ 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::windowRectChanged, 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); diff --git a/src/window/popupwindow.hpp b/src/window/popupwindow.hpp index 63ae97ff..e00495cc 100644 --- a/src/window/popupwindow.hpp +++ b/src/window/popupwindow.hpp @@ -57,8 +57,8 @@ class ProxyPopupWindow: public ProxyWindowBase { /// /// 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. + /// The popup's anchor / positioner relative to another item or 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