core/popupanchor: rework popup anchoring and add PopupAnchor

This commit is contained in:
outfoxxed 2024-07-23 22:12:27 -07:00
parent 14910b1b60
commit ebfa8ec448
Signed by: outfoxxed
GPG key ID: 4C88A185FB89301E
14 changed files with 770 additions and 108 deletions

View file

@ -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}")

View file

@ -24,5 +24,7 @@ headers = [
"objectrepeater.hpp",
"qsmenu.hpp",
"retainable.hpp",
"popupanchor.hpp",
"types.hpp",
]
-----

275
src/core/popupanchor.cpp Normal file
View file

@ -0,0 +1,275 @@
#include "popupanchor.hpp"
#include <qlogging.h>
#include <qobject.h>
#include <qtmetamacros.h>
#include <qwindow.h>
#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<ProxyWindowBase*>(window)) {
this->mProxyWindow = proxy;
} else if (auto* interface = qobject_cast<WindowInterface*>(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;
}

157
src/core/popupanchor.hpp Normal file
View file

@ -0,0 +1,157 @@
#pragma once
#include <optional>
#include <QtQmlIntegration/qqmlintegration.h>
#include <qflags.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qpoint.h>
#include <qqmlintegration.h>
#include <qtclasshelpermacros.h>
#include <qtmetamacros.h>
#include <qwindow.h>
#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<PopupAnchorState> 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);
};

View file

@ -4,86 +4,66 @@
#include <qnamespace.h>
#include <qobject.h>
#include <qquickwindow.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qwindow.h>
#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<ProxyWindowBase*>(parent)) {
this->mParentProxyWindow = proxy;
} else if (auto* interface = qobject_cast<WindowInterface*>(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);
}

View file

@ -7,6 +7,7 @@
#include <qtypes.h>
#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;
};

23
src/core/types.cpp Normal file
View file

@ -0,0 +1,23 @@
#include "types.hpp"
#include <qdebug.h>
#include <qnamespace.h>
#include <qrect.h>
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);
}

54
src/core/types.hpp Normal file
View file

@ -0,0 +1,54 @@
#pragma once
#include <qdebug.h>
#include <qnamespace.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
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);

View file

@ -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

View file

@ -4,12 +4,14 @@
#include <qtenvironmentvariables.h>
#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

View file

@ -0,0 +1,99 @@
#include "popupanchor.hpp"
#include <private/qwayland-xdg-shell.h>
#include <private/qwaylandwindow_p.h>
#include <private/wayland-xdg-shell-client-protocol.h>
#include <qvariant.h>
#include <qwindow.h>
#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<QWaylandWindow*>(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()); }

View file

@ -0,0 +1,16 @@
#pragma once
#include <qwindow.h>
#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();

14
src/wayland/xdgshell.cpp Normal file
View file

@ -0,0 +1,14 @@
#include "xdgshell.hpp"
#include <qwaylandclientextension.h>
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

20
src/wayland/xdgshell.hpp Normal file
View file

@ -0,0 +1,20 @@
#pragma once
#include <private/qwayland-xdg-shell.h>
#include <qwaylandclientextension.h>
namespace qs::wayland::xdg_shell {
// Hack that binds xdg_wm_base twice as QtWaylandXdgShell headers are not exported anywhere.
class XdgWmBase
: public QWaylandClientExtensionTemplate<XdgWmBase>
, public QtWayland::xdg_wm_base {
public:
static XdgWmBase* instance();
private:
explicit XdgWmBase();
};
} // namespace qs::wayland::xdg_shell