Skip to content

Commit 6ed5dd1

Browse files
committed
core/popupanchor: add item-relative anchor rect support
1 parent adcef7f commit 6ed5dd1

File tree

5 files changed

+116
-40
lines changed

5 files changed

+116
-40
lines changed

src/core/popupanchor.cpp

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include <qcontainerfwd.h>
55
#include <qlogging.h>
66
#include <qobject.h>
7+
#include <qquickitem.h>
78
#include <qsize.h>
89
#include <qtmetamacros.h>
910
#include <qwindow.h>
@@ -25,14 +26,11 @@ bool PopupAnchor::isDirty() const {
2526
void PopupAnchor::markClean() { this->lastState = this->state; }
2627
void PopupAnchor::markDirty() { this->lastState.reset(); }
2728

28-
QObject* PopupAnchor::window() const { return this->mWindow; }
29-
ProxyWindowBase* PopupAnchor::proxyWindow() const { return this->mProxyWindow; }
30-
3129
QWindow* PopupAnchor::backingWindow() const {
3230
return this->mProxyWindow ? this->mProxyWindow->backingWindow() : nullptr;
3331
}
3432

35-
void PopupAnchor::setWindow(QObject* window) {
33+
void PopupAnchor::setWindowInternal(QObject* window) {
3634
if (window == this->mWindow) return;
3735

3836
if (this->mWindow) {
@@ -78,25 +76,69 @@ void PopupAnchor::setWindow(QObject* window) {
7876
}
7977
}
8078

79+
void PopupAnchor::setWindow(QObject* window) {
80+
this->setItem(nullptr);
81+
this->setWindowInternal(window);
82+
}
83+
84+
void PopupAnchor::setItem(QQuickItem* item) {
85+
if (item == this->mItem) return;
86+
87+
if (this->mItem) {
88+
QObject::disconnect(this->mItem, nullptr, this, nullptr);
89+
}
90+
91+
this->mItem = item;
92+
this->onItemWindowChanged();
93+
94+
if (item) {
95+
QObject::connect(item, &QObject::destroyed, this, &PopupAnchor::onItemDestroyed);
96+
QObject::connect(item, &QQuickItem::windowChanged, this, &PopupAnchor::onItemWindowChanged);
97+
}
98+
}
99+
81100
void PopupAnchor::onWindowDestroyed() {
82101
this->mWindow = nullptr;
83102
this->mProxyWindow = nullptr;
84103
emit this->windowChanged();
85104
emit this->backingWindowVisibilityChanged();
86105
}
87106

88-
Box PopupAnchor::rect() const { return this->state.rect; }
107+
void PopupAnchor::onItemDestroyed() {
108+
this->mItem = nullptr;
109+
emit this->itemChanged();
110+
this->setWindowInternal(nullptr);
111+
}
112+
113+
void PopupAnchor::onItemWindowChanged() {
114+
if (auto* window = qobject_cast<ProxiedWindow*>(this->mItem->window())) {
115+
this->setWindowInternal(window->proxy());
116+
} else {
117+
this->setWindowInternal(nullptr);
118+
}
119+
}
89120

90121
void PopupAnchor::setRect(Box rect) {
91-
if (rect == this->state.rect) return;
92122
if (rect.w <= 0) rect.w = 1;
93123
if (rect.h <= 0) rect.h = 1;
124+
if (rect == this->mUserRect) return;
94125

95-
this->state.rect = rect;
126+
this->mUserRect = rect;
96127
emit this->rectChanged();
128+
129+
this->setWindowRect(rect);
130+
}
131+
132+
void PopupAnchor::setWindowRect(Box rect) {
133+
if (rect.w <= 0) rect.w = 1;
134+
if (rect.h <= 0) rect.h = 1;
135+
if (rect == this->state.rect) return;
136+
137+
this->state.rect = rect;
138+
emit this->windowRectChanged();
97139
}
98140

99-
Edges::Flags PopupAnchor::edges() const { return this->state.edges; }
141+
void PopupAnchor::resetRect() { this->mUserRect = Box(); }
100142

101143
void PopupAnchor::setEdges(Edges::Flags edges) {
102144
if (edges == this->state.edges) return;
@@ -110,8 +152,6 @@ void PopupAnchor::setEdges(Edges::Flags edges) {
110152
emit this->edgesChanged();
111153
}
112154

113-
Edges::Flags PopupAnchor::gravity() const { return this->state.gravity; }
114-
115155
void PopupAnchor::setGravity(Edges::Flags gravity) {
116156
if (gravity == this->state.gravity) return;
117157

@@ -124,8 +164,6 @@ void PopupAnchor::setGravity(Edges::Flags gravity) {
124164
emit this->gravityChanged();
125165
}
126166

127-
PopupAdjustment::Flags PopupAnchor::adjustment() const { return this->state.adjustment; }
128-
129167
void PopupAnchor::setAdjustment(PopupAdjustment::Flags adjustment) {
130168
if (adjustment == this->state.adjustment) return;
131169
this->state.adjustment = adjustment;
@@ -137,6 +175,19 @@ void PopupAnchor::updatePlacement(const QPoint& anchorpoint, const QSize& size)
137175
this->state.size = size;
138176
}
139177

178+
void PopupAnchor::updateAnchor() {
179+
if (this->mItem && this->mProxyWindow) {
180+
auto rect = this->mProxyWindow->contentItem()->mapRectFromItem(
181+
this->mItem,
182+
this->mUserRect.isEmpty() ? this->mItem->boundingRect() : this->mUserRect.qrect()
183+
);
184+
185+
this->setWindowRect(rect);
186+
}
187+
188+
emit this->anchoring();
189+
}
190+
140191
static PopupPositioner* POSITIONER = nullptr; // NOLINT
141192

142193
void PopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool onlyIfDirty) {
@@ -148,15 +199,15 @@ void PopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool only
148199
auto parentGeometry = parentWindow->geometry();
149200
auto windowGeometry = window->geometry();
150201

151-
emit anchor->anchoring();
202+
anchor->updateAnchor();
152203
anchor->updatePlacement(parentGeometry.topLeft(), windowGeometry.size());
153204

154205
if (onlyIfDirty && !anchor->isDirty()) return;
155206
anchor->markClean();
156207

157208
auto adjustment = anchor->adjustment();
158209
auto screenGeometry = parentWindow->screen()->geometry();
159-
auto anchorRectGeometry = anchor->rect().qrect().translated(parentGeometry.topLeft());
210+
auto anchorRectGeometry = anchor->windowRect().qrect().translated(parentGeometry.topLeft());
160211

161212
auto anchorEdges = anchor->edges();
162213
auto anchorGravity = anchor->gravity();

src/core/popupanchor.hpp

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <qobject.h>
88
#include <qpoint.h>
99
#include <qqmlintegration.h>
10+
#include <qquickitem.h>
1011
#include <qsize.h>
1112
#include <qtclasshelpermacros.h>
1213
#include <qtmetamacros.h>
@@ -72,25 +73,29 @@ struct PopupAnchorState {
7273
class PopupAnchor: public QObject {
7374
Q_OBJECT;
7475
// clang-format off
75-
/// The window to anchor / attach the popup to.
76+
/// The window to anchor / attach the popup to. Setting this property unsets @@item.
7677
Q_PROPERTY(QObject* window READ window WRITE setWindow NOTIFY windowChanged);
77-
/// The anchorpoints the popup will attach to. Which anchors will be used is
78-
/// determined by the @@edges, @@gravity, and @@adjustment.
78+
/// The item to anchor / attach the popup to. Setting this property unsets @@window.
79+
///
80+
/// The popup's position relative to its parent window is only calculated when it is
81+
/// initially shown (directly before @@anchoring(s) is emitted), meaning its anchor
82+
/// rectangle will be set relative to the item's position in the window at that time.
83+
/// @@updateAnchor() can be called to update the anchor rectangle if the item's position
84+
/// has changed.
85+
///
86+
/// > [!NOTE] If a more flexible way to position a popup relative to an item is needed,
87+
/// > set @@window to the item's parent window, and handle the @@anchoring signal to
88+
/// > position the popup relative to the window's contentItem.
89+
Q_PROPERTY(QQuickItem* item READ item WRITE setItem NOTIFY itemChanged);
90+
/// The anchorpoints the popup will attach to, relative to @@item or @@window.
91+
/// Which anchors will be used is determined by the @@edges, @@gravity, and @@adjustment.
7992
///
8093
/// If you leave @@edges, @@gravity and @@adjustment at their default values,
8194
/// setting more than `x` and `y` does not matter. The anchor rect cannot
8295
/// be smaller than 1x1 pixels.
8396
///
84-
/// > [!INFO] To position a popup relative to an item inside a window,
85-
/// > you can use [coordinate mapping functions] (note the warning below).
86-
///
87-
/// > [!WARNING] Using [coordinate mapping functions] in a binding to
88-
/// > this property will position the anchor incorrectly.
89-
/// > If you want to use them, do so in @@anchoring(s), or use
90-
/// > @@TransformWatcher if you need real-time updates to mapped coordinates.
91-
///
9297
/// [coordinate mapping functions]: https://doc.qt.io/qt-6/qml-qtquick-item.html#mapFromItem-method
93-
Q_PROPERTY(Box rect READ rect WRITE setRect NOTIFY rectChanged);
98+
Q_PROPERTY(Box rect READ rect WRITE setRect RESET resetRect NOTIFY rectChanged);
9499
/// The point on the anchor rectangle the popup should anchor to.
95100
/// Opposing edges suchs as `Edges.Left | Edges.Right` are not allowed.
96101
///
@@ -113,25 +118,40 @@ class PopupAnchor: public QObject {
113118
public:
114119
explicit PopupAnchor(QObject* parent): QObject(parent) {}
115120

121+
/// Update the popup's anchor rect relative to its parent window.
122+
///
123+
/// If anchored to an item, popups anchors will not automatically follow
124+
/// the item if its position changes. This function can be called to
125+
/// recalculate the anchors.
126+
Q_INVOKABLE void updateAnchor();
127+
116128
[[nodiscard]] bool isDirty() const;
117129
void markClean();
118130
void markDirty();
119131

120-
[[nodiscard]] QObject* window() const;
121-
[[nodiscard]] ProxyWindowBase* proxyWindow() const;
132+
[[nodiscard]] QObject* window() const { return this->mWindow; }
133+
[[nodiscard]] ProxyWindowBase* proxyWindow() const { return this->mProxyWindow; }
122134
[[nodiscard]] QWindow* backingWindow() const;
135+
void setWindowInternal(QObject* window);
123136
void setWindow(QObject* window);
124137

125-
[[nodiscard]] Box rect() const;
138+
[[nodiscard]] QQuickItem* item() const { return this->mItem; }
139+
void setItem(QQuickItem* item);
140+
141+
[[nodiscard]] Box windowRect() const { return this->state.rect; }
142+
void setWindowRect(Box rect);
143+
144+
[[nodiscard]] Box rect() const { return this->mUserRect; }
126145
void setRect(Box rect);
146+
void resetRect();
127147

128-
[[nodiscard]] Edges::Flags edges() const;
148+
[[nodiscard]] Edges::Flags edges() const { return this->state.edges; }
129149
void setEdges(Edges::Flags edges);
130150

131-
[[nodiscard]] Edges::Flags gravity() const;
151+
[[nodiscard]] Edges::Flags gravity() const { return this->state.gravity; }
132152
void setGravity(Edges::Flags gravity);
133153

134-
[[nodiscard]] PopupAdjustment::Flags adjustment() const;
154+
[[nodiscard]] PopupAdjustment::Flags adjustment() const { return this->state.adjustment; }
135155
void setAdjustment(PopupAdjustment::Flags adjustment);
136156

137157
void updatePlacement(const QPoint& anchorpoint, const QSize& size);
@@ -144,19 +164,25 @@ class PopupAnchor: public QObject {
144164
void anchoring();
145165

146166
void windowChanged();
167+
void itemChanged();
147168
QSDOC_HIDE void backingWindowVisibilityChanged();
169+
QSDOC_HIDE void windowRectChanged();
148170
void rectChanged();
149171
void edgesChanged();
150172
void gravityChanged();
151173
void adjustmentChanged();
152174

153175
private slots:
154176
void onWindowDestroyed();
177+
void onItemDestroyed();
178+
void onItemWindowChanged();
155179

156180
private:
157181
QObject* mWindow = nullptr;
182+
QQuickItem* mItem = nullptr;
158183
ProxyWindowBase* mProxyWindow = nullptr;
159184
PopupAnchorState state;
185+
Box mUserRect;
160186
std::optional<PopupAnchorState> lastState;
161187
};
162188

src/wayland/popupanchor.cpp

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
#include <private/qwayland-xdg-shell.h>
55
#include <private/qwaylandwindow_p.h>
66
#include <private/wayland-xdg-shell-client-protocol.h>
7-
#include <qtmetamacros.h>
87
#include <qvariant.h>
98
#include <qwindow.h>
109

@@ -21,7 +20,7 @@ void WaylandPopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bo
2120
auto* waylandWindow = dynamic_cast<QWaylandWindow*>(window->handle());
2221
auto* popupRole = waylandWindow ? waylandWindow->surfaceRole<::xdg_popup>() : nullptr;
2322

24-
emit anchor->anchoring();
23+
anchor->updateAnchor();
2524

2625
// If a popup becomes invisble after creation ensure the _q properties will
2726
// be set and not ignored because the rest is the same.
@@ -44,7 +43,7 @@ void WaylandPopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bo
4443

4544
positioner.set_constraint_adjustment(anchor->adjustment().toInt());
4645

47-
auto anchorRect = anchor->rect();
46+
auto anchorRect = anchor->windowRect();
4847

4948
if (auto* p = window->transientParent()) {
5049
anchorRect.x = QHighDpi::toNativePixels(anchorRect.x, p);
@@ -104,8 +103,8 @@ void WaylandPopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bo
104103
bool WaylandPopupPositioner::shouldRepositionOnMove() const { return true; }
105104

106105
void WaylandPopupPositioner::setFlags(PopupAnchor* anchor, QWindow* window) {
107-
emit anchor->anchoring();
108-
auto anchorRect = anchor->rect();
106+
anchor->updateAnchor();
107+
auto anchorRect = anchor->windowRect();
109108

110109
if (auto* p = window->transientParent()) {
111110
anchorRect.x = QHighDpi::toNativePixels(anchorRect.x, p);

src/window/popupwindow.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ ProxyPopupWindow::ProxyPopupWindow(QObject* parent): ProxyWindowBase(parent) {
1414
this->mVisible = false;
1515
// clang-format off
1616
QObject::connect(&this->mAnchor, &PopupAnchor::windowChanged, this, &ProxyPopupWindow::parentWindowChanged);
17-
QObject::connect(&this->mAnchor, &PopupAnchor::rectChanged, this, &ProxyPopupWindow::reposition);
17+
QObject::connect(&this->mAnchor, &PopupAnchor::windowRectChanged, this, &ProxyPopupWindow::reposition);
1818
QObject::connect(&this->mAnchor, &PopupAnchor::edgesChanged, this, &ProxyPopupWindow::reposition);
1919
QObject::connect(&this->mAnchor, &PopupAnchor::gravityChanged, this, &ProxyPopupWindow::reposition);
2020
QObject::connect(&this->mAnchor, &PopupAnchor::adjustmentChanged, this, &ProxyPopupWindow::reposition);

src/window/popupwindow.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ class ProxyPopupWindow: public ProxyWindowBase {
5757
///
5858
/// The Y position of the popup relative to the parent window.
5959
Q_PROPERTY(qint32 relativeY READ relativeY WRITE setRelativeY NOTIFY relativeYChanged);
60-
/// The popup's anchor / positioner relative to another window. The popup will not be
61-
/// shown until it has a valid anchor relative to a window and @@visible is true.
60+
/// The popup's anchor / positioner relative to another item or window. The popup will
61+
/// not be shown until it has a valid anchor relative to a window and @@visible is true.
6262
///
6363
/// You can set properties of the anchor like so:
6464
/// ```qml

0 commit comments

Comments
 (0)