popups: add popup windows

This commit is contained in:
outfoxxed 2024-03-11 05:44:56 -07:00
parent 8cf0659444
commit b675b3676c
Signed by: outfoxxed
GPG key ID: 4C88A185FB89301E
20 changed files with 586 additions and 71 deletions

View file

@ -46,6 +46,7 @@ set(QT_FPDEPS Gui Qml Quick QuickControls2)
if (BUILD_TESTING)
enable_testing()
add_definitions(-DQS_TEST)
list(APPEND QT_FPDEPS Test)
endif()

2
docs

@ -1 +1 @@
Subproject commit b218d3ec30f8ff2c51d4caf17509b9d21cf0c088
Subproject commit 2d0b15bbd52ea61bd79880b89fae0a589010d1f3

@ -1 +1 @@
Subproject commit f76b43db25fb06a016ccf64ec2b28079c325c346
Subproject commit 9437c6a840faf7180ab7dfb5425a402ca8a4b58c

View file

@ -15,9 +15,14 @@ qt_add_executable(quickshell
windowinterface.cpp
floatingwindow.cpp
panelinterface.cpp
popupwindow.cpp
)
set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}")
qt_add_qml_module(quickshell URI Quickshell VERSION 0.1)
target_link_libraries(quickshell PRIVATE ${QT_DEPS})
if (BUILD_TESTING)
add_subdirectory(test)
endif()

View file

@ -9,3 +9,6 @@
// make the type visible in the docs even if not a QML_ELEMENT
#define QSDOC_ELEMENT
#define QSDOC_NAMED_ELEMENT(name)
// overridden properties
#define QSDOC_PROPERTY_OVERRIDE(...)

View file

@ -42,6 +42,7 @@ void FloatingWindowInterface::onReload(QObject* oldInstance) {
}
QQmlListProperty<QObject> FloatingWindowInterface::data() { return this->window->data(); }
ProxyWindowBase* FloatingWindowInterface::proxyWindow() const { return this->window; }
QQuickItem* FloatingWindowInterface::contentItem() const { return this->window->contentItem(); }
// NOLINTBEGIN
@ -57,6 +58,4 @@ proxyPair(QColor, color, setColor);
proxyPair(PendingRegion*, mask, setMask);
#undef proxyPair
#undef proxySet
#undef proxyGet
// NOLINTEND

View file

@ -27,6 +27,7 @@ public:
void onReload(QObject* oldInstance) override;
[[nodiscard]] ProxyWindowBase* proxyWindow() const override;
[[nodiscard]] QQuickItem* contentItem() const override;
// NOLINTBEGIN

View file

@ -12,5 +12,6 @@ headers = [
"windowinterface.hpp",
"panelinterface.hpp",
"floatingwindow.hpp",
"popupwindow.hpp",
]
-----

151
src/core/popupwindow.cpp Normal file
View file

@ -0,0 +1,151 @@
#include "popupwindow.hpp"
#include <qlogging.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qquickwindow.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "proxywindow.hpp"
#include "qmlscreen.hpp"
#include "windowinterface.hpp"
ProxyPopupWindow::ProxyPopupWindow(QObject* parent): ProxyWindowBase(parent) {
this->mVisible = false;
}
void ProxyPopupWindow::setupWindow() {
this->ProxyWindowBase::setupWindow();
this->window->setFlag(Qt::ToolTip);
this->updateTransientParent();
}
qint32 ProxyPopupWindow::x() const {
return this->ProxyWindowBase::x() + 1; // QTBUG-121550
}
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::windowConnected, this, &ProxyPopupWindow::onParentConnected);
// clang-format on
}
this->updateTransientParent();
}
QObject* ProxyPopupWindow::parentWindow() const { return this->mParentWindow; }
void ProxyPopupWindow::updateTransientParent() {
if (this->window == nullptr) return;
this->updateX();
this->updateY();
this->window->setTransientParent(
this->mParentProxyWindow == nullptr ? nullptr : this->mParentProxyWindow->backingWindow()
);
this->updateVisible();
}
void ProxyPopupWindow::onParentConnected() { 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";
}
void ProxyPopupWindow::setVisible(bool visible) {
if (visible == this->wantsVisible) return;
this->wantsVisible = visible;
this->updateVisible();
}
void ProxyPopupWindow::updateVisible() {
auto target = this->wantsVisible && this->mParentWindow != nullptr;
if (target && this->window != nullptr && !this->window->isVisible()) {
this->updateX(); // QTBUG-121550
}
this->ProxyWindowBase::setVisible(target);
}
void ProxyPopupWindow::setRelativeX(qint32 x) {
if (x == this->mRelativeX) return;
this->mRelativeX = x;
this->updateX();
}
qint32 ProxyPopupWindow::relativeX() const { return this->mRelativeX; }
void ProxyPopupWindow::setRelativeY(qint32 y) {
if (y == this->mRelativeY) return;
this->mRelativeY = y;
this->updateY();
}
qint32 ProxyPopupWindow::relativeY() const { return this->mRelativeY; }
void ProxyPopupWindow::updateX() {
if (this->mParentWindow == nullptr || this->window == nullptr) return;
// use the backing window's x to account for popups in popups with overridden x positions
auto target = this->mParentProxyWindow->backingWindow()->x() + this->relativeX();
auto reshow = this->window->isVisible() && (this->window->x() != target && this->x() != target);
if (reshow) this->window->setVisible(false);
this->window->setX(target - 1); // -1 due to QTBUG-121550
if (reshow && this->wantsVisible) this->window->setVisible(true);
}
void ProxyPopupWindow::updateY() {
if (this->mParentWindow == nullptr || this->window == nullptr) return;
auto target = this->mParentProxyWindow->y() + this->relativeY();
auto reshow = this->window->isVisible() && this->window->y() != target;
if (reshow) {
this->window->setVisible(false);
this->updateX(); // QTBUG-121550
}
this->window->setY(target);
if (reshow && this->wantsVisible) this->window->setVisible(true);
}

102
src/core/popupwindow.hpp Normal file
View file

@ -0,0 +1,102 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
#include <qquickwindow.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "doc.hpp"
#include "proxywindow.hpp"
#include "qmlscreen.hpp"
#include "windowinterface.hpp"
///! Popup window.
/// Popup window that can display in a position relative to a floating
/// or panel window.
///
/// #### Example
/// The following snippet creates a panel with a popup centered over it.
///
/// ```qml
/// PanelWindow {
/// id: toplevel
///
/// anchors {
/// bottom: true
/// left: true
/// right: true
/// }
///
/// PopupWindow {
/// parentWindow: toplevel
/// relativeX: parentWindow.width / 2 - width / 2
/// relativeY: parentWindow.height
/// width: 500
/// height: 500
/// visible: true
/// }
/// }
/// ```
class ProxyPopupWindow: public ProxyWindowBase {
QSDOC_BASECLASS(WindowInterface);
Q_OBJECT;
// clang-format off
/// The parent window of this popup.
///
/// Changing this property reparents the popup.
Q_PROPERTY(QObject* parentWindow READ parentWindow WRITE setParentWindow NOTIFY parentWindowChanged);
/// The X position of the popup relative to the parent window.
Q_PROPERTY(qint32 relativeX READ relativeX WRITE setRelativeX NOTIFY relativeXChanged);
/// The Y position of the popup relative to the parent window.
Q_PROPERTY(qint32 relativeY READ relativeY WRITE setRelativeY NOTIFY relativeYChanged);
/// If the window is shown or hidden. Defaults to false.
QSDOC_PROPERTY_OVERRIDE(bool visible READ isVisible WRITE setVisible NOTIFY visibleChanged);
/// The screen that the window currently occupies.
///
/// This may be modified to move the window to the given screen.
QSDOC_PROPERTY_OVERRIDE(QuickshellScreenInfo* screen READ screen NOTIFY screenChanged);
// clang-format on
QML_NAMED_ELEMENT(PopupWindow);
public:
explicit ProxyPopupWindow(QObject* parent = nullptr);
void setupWindow() override;
void setScreen(QuickshellScreenInfo* screen) override;
void setVisible(bool visible) override;
[[nodiscard]] qint32 x() const override;
[[nodiscard]] QObject* parentWindow() const;
void setParentWindow(QObject* parent);
[[nodiscard]] qint32 relativeX() const;
void setRelativeX(qint32 x);
[[nodiscard]] qint32 relativeY() const;
void setRelativeY(qint32 y);
signals:
void parentWindowChanged();
void relativeXChanged();
void relativeYChanged();
private slots:
void onParentConnected();
void onParentDestroyed();
void updateX();
void updateY();
private:
QQuickWindow* parentBackingWindow();
void updateTransientParent();
void updateVisible();
QObject* mParentWindow = nullptr;
ProxyWindowBase* mParentProxyWindow = nullptr;
qint32 mRelativeX = 0;
qint32 mRelativeY = 0;
bool wantsVisible = false;
};

View file

@ -61,6 +61,8 @@ QQuickWindow* ProxyWindowBase::createWindow(QObject* oldInstance) {
void ProxyWindowBase::setupWindow() {
// clang-format off
QObject::connect(this->window, &QWindow::visibilityChanged, this, &ProxyWindowBase::visibleChanged);
QObject::connect(this->window, &QWindow::xChanged, this, &ProxyWindowBase::xChanged);
QObject::connect(this->window, &QWindow::yChanged, this, &ProxyWindowBase::yChanged);
QObject::connect(this->window, &QWindow::widthChanged, this, &ProxyWindowBase::widthChanged);
QObject::connect(this->window, &QWindow::heightChanged, this, &ProxyWindowBase::heightChanged);
QObject::connect(this->window, &QWindow::screenChanged, this, &ProxyWindowBase::screenChanged);
@ -76,6 +78,10 @@ void ProxyWindowBase::setupWindow() {
this->setHeight(this->mHeight);
this->setColor(this->mColor);
this->updateMask();
// notify initial x and y positions
emit this->xChanged();
emit this->yChanged();
}
QQuickWindow* ProxyWindowBase::disownWindow() {
@ -103,6 +109,16 @@ void ProxyWindowBase::setVisible(bool visible) {
} else this->window->setVisible(visible);
}
qint32 ProxyWindowBase::x() const {
if (this->window == nullptr) return 0;
else return this->window->x();
}
qint32 ProxyWindowBase::y() const {
if (this->window == nullptr) return 0;
else return this->window->y();
}
qint32 ProxyWindowBase::width() const {
if (this->window == nullptr) return this->mWidth;
else return this->window->width();

View file

@ -34,7 +34,7 @@ class ProxyWindowBase: public Reloadable {
/// >
/// > Use **only** if you know what you are doing.
Q_PROPERTY(QQuickWindow* _backingWindow READ backingWindow);
Q_PROPERTY(QQuickItem* contentItem READ contentItem);
Q_PROPERTY(QQuickItem* contentItem READ contentItem CONSTANT);
Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibleChanged);
Q_PROPERTY(qint32 width READ width WRITE setWidth NOTIFY widthChanged);
Q_PROPERTY(qint32 height READ height WRITE setHeight NOTIFY heightChanged);
@ -67,6 +67,9 @@ public:
[[nodiscard]] virtual bool isVisible() const;
virtual void setVisible(bool visible);
[[nodiscard]] virtual qint32 x() const;
[[nodiscard]] virtual qint32 y() const;
[[nodiscard]] virtual qint32 width() const;
virtual void setWidth(qint32 width);
@ -87,6 +90,8 @@ public:
signals:
void windowConnected();
void visibleChanged();
void xChanged();
void yChanged();
void widthChanged();
void heightChanged();
void screenChanged();

View file

@ -0,0 +1,15 @@
function (qs_test name)
add_executable(${name} ${ARGN})
target_link_libraries(${name} PRIVATE ${QT_DEPS} Qt6::Test)
add_test(NAME ${name} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" COMMAND $<TARGET_FILE:${name}>)
endfunction()
qs_test(popupwindow
popupwindow.cpp
../popupwindow.cpp
../proxywindow.cpp
../qmlscreen.cpp
../region.cpp
../reload.cpp
../windowinterface.cpp
)

View file

@ -0,0 +1,182 @@
#include "popupwindow.hpp"
#include <qlogging.h>
#include <qquickwindow.h>
#include <qsignalspy.h>
#include <qtest.h>
#include <qtestcase.h>
#include <qwindow.h>
#include "../popupwindow.hpp"
#include "../proxywindow.hpp"
void TestPopupWindow::initiallyVisible() { // NOLINT
auto parent = ProxyWindowBase();
auto popup = ProxyPopupWindow();
popup.setParentWindow(&parent);
popup.setVisible(true);
parent.onReload(nullptr);
popup.onReload(nullptr);
QVERIFY(popup.isVisible());
QVERIFY(popup.backingWindow()->isVisible());
QCOMPARE(popup.backingWindow()->transientParent(), parent.backingWindow());
}
void TestPopupWindow::reloadReparent() { // NOLINT
// first generation
auto parent = ProxyWindowBase();
auto popup = ProxyPopupWindow();
auto* win2 = new QQuickWindow();
win2->setVisible(true);
parent.setVisible(true);
popup.setParentWindow(&parent);
popup.setVisible(true);
parent.onReload(nullptr);
popup.onReload(nullptr);
// second generation
auto newParent = ProxyWindowBase();
auto newPopup = ProxyPopupWindow();
newPopup.setParentWindow(&newParent);
newPopup.setVisible(true);
auto* oldWindow = popup.backingWindow();
auto* oldTransientParent = oldWindow->transientParent();
auto spy = QSignalSpy(oldWindow, &QWindow::visibleChanged);
qDebug() << "reload";
newParent.onReload(&parent);
newPopup.onReload(&popup);
QVERIFY(newPopup.isVisible());
QVERIFY(newPopup.backingWindow()->isVisible());
QCOMPARE(newPopup.backingWindow()->transientParent(), oldTransientParent);
QCOMPARE(newPopup.backingWindow()->transientParent(), newParent.backingWindow());
QCOMPARE(spy.length(), 0);
}
void TestPopupWindow::reloadUnparent() { // NOLINT
// first generation
auto parent = ProxyWindowBase();
auto popup = ProxyPopupWindow();
popup.setParentWindow(&parent);
popup.setVisible(true);
parent.onReload(nullptr);
popup.onReload(nullptr);
// second generation
auto newPopup = ProxyPopupWindow();
// parent not set
newPopup.setVisible(true);
newPopup.onReload(&popup);
QVERIFY(!newPopup.isVisible());
QVERIFY(!newPopup.backingWindow()->isVisible());
QCOMPARE(newPopup.backingWindow()->transientParent(), nullptr);
}
void TestPopupWindow::invisibleWithoutParent() { // NOLINT
auto popup = ProxyPopupWindow();
popup.setVisible(true);
popup.onReload(nullptr);
QVERIFY(!popup.isVisible());
}
void TestPopupWindow::moveWithParent() { // NOLINT
auto parent = ProxyWindowBase();
auto popup = ProxyPopupWindow();
popup.setParentWindow(&parent);
popup.setRelativeX(10);
popup.setRelativeY(10);
popup.setVisible(true);
parent.onReload(nullptr);
popup.onReload(nullptr);
QCOMPARE(popup.x(), parent.x() + 10);
QCOMPARE(popup.y(), parent.y() + 10);
parent.backingWindow()->setX(10);
parent.backingWindow()->setY(10);
QCOMPARE(popup.x(), parent.x() + 10);
QCOMPARE(popup.y(), parent.y() + 10);
}
void TestPopupWindow::attachParentLate() { // NOLINT
auto parent = ProxyWindowBase();
auto popup = ProxyPopupWindow();
popup.setVisible(true);
parent.onReload(nullptr);
popup.onReload(nullptr);
QVERIFY(!popup.isVisible());
popup.setParentWindow(&parent);
QVERIFY(popup.isVisible());
QVERIFY(popup.backingWindow()->isVisible());
QCOMPARE(popup.backingWindow()->transientParent(), parent.backingWindow());
}
void TestPopupWindow::reparentLate() { // NOLINT
auto parent = ProxyWindowBase();
auto popup = ProxyPopupWindow();
popup.setParentWindow(&parent);
popup.setVisible(true);
parent.onReload(nullptr);
popup.onReload(nullptr);
QCOMPARE(popup.x(), parent.x());
QCOMPARE(popup.y(), parent.y());
auto parent2 = ProxyWindowBase();
parent2.onReload(nullptr);
parent2.backingWindow()->setX(10);
parent2.backingWindow()->setY(10);
popup.setParentWindow(&parent2);
QVERIFY(popup.isVisible());
QVERIFY(popup.backingWindow()->isVisible());
QCOMPARE(popup.backingWindow()->transientParent(), parent2.backingWindow());
QCOMPARE(popup.x(), parent2.x());
QCOMPARE(popup.y(), parent2.y());
}
void TestPopupWindow::xMigrationFix() { // NOLINT
auto parent = ProxyWindowBase();
auto popup = ProxyPopupWindow();
popup.setParentWindow(&parent);
popup.setVisible(true);
parent.onReload(nullptr);
popup.onReload(nullptr);
QCOMPARE(popup.x(), parent.x());
popup.setVisible(false);
popup.setVisible(true);
QCOMPARE(popup.x(), parent.x());
}
QTEST_MAIN(TestPopupWindow);

View file

@ -0,0 +1,18 @@
#pragma once
#include <qobject.h>
#include <qtmetamacros.h>
class TestPopupWindow: public QObject {
Q_OBJECT;
private slots:
void initiallyVisible();
void reloadReparent();
void reloadUnparent();
void invisibleWithoutParent();
void moveWithParent();
void attachParentLate();
void reparentLate();
void xMigrationFix();
};

View file

@ -12,17 +12,19 @@
#include "region.hpp"
#include "reload.hpp"
class ProxyWindowBase;
class WindowInterface: public Reloadable {
Q_OBJECT;
// clang-format off
Q_PROPERTY(QQuickItem* contentItem READ contentItem);
Q_PROPERTY(QQuickItem* contentItem READ contentItem CONSTANT);
/// If the window is shown or hidden. Defaults to true.
Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibleChanged);
Q_PROPERTY(qint32 width READ width WRITE setWidth NOTIFY widthChanged);
Q_PROPERTY(qint32 height READ height WRITE setHeight NOTIFY heightChanged);
/// The screen that the window currently occupies.
///
/// > [!INFO] This cannot be changed after windowConnected.
/// This may be modified to move the window to the given screen.
Q_PROPERTY(QuickshellScreenInfo* screen READ screen WRITE setScreen NOTIFY screenChanged);
/// The background color of the window. Defaults to white.
///
@ -92,6 +94,7 @@ class WindowInterface: public Reloadable {
public:
explicit WindowInterface(QObject* parent = nullptr): Reloadable(parent) {}
[[nodiscard]] virtual ProxyWindowBase* proxyWindow() const = 0;
[[nodiscard]] virtual QQuickItem* contentItem() const = 0;
[[nodiscard]] virtual bool isVisible() const = 0;

View file

@ -1,4 +1,4 @@
#include "../datastream.hpp"
#include "datastream.hpp"
#include <qbytearray.h>
#include <qlist.h>
@ -7,12 +7,10 @@
#include <qsignalspy.h>
#include <qtest.h>
#include <qtestcase.h>
#include <qtmetamacros.h>
class TestSplitParser: public QObject {
Q_OBJECT;
private slots:
void splits_data() { // NOLINT
#include "../datastream.hpp"
void TestSplitParser::splits_data() { // NOLINT
QTest::addColumn<QString>("mark");
QTest::addColumn<QString>("buffer"); // max that can go in the buffer
QTest::addColumn<QString>("incoming"); // data that has to be tested on the end in one go
@ -42,9 +40,9 @@ private slots:
<< QList<QString>({ "foo", "bar" }) << "baz";
// clang-format on
// NOLINTEND
}
}
void splits() { // NOLINT
void TestSplitParser::splits() { // NOLINT
// NOLINTBEGIN
QFETCH(QString, mark);
QFETCH(QString, buffer);
@ -82,9 +80,9 @@ private slots:
QCOMPARE(actualResults, results);
QCOMPARE(buffer, remainder);
}
}
}
void initBuffer() { // NOLINT
void TestSplitParser::initBuffer() { // NOLINT
auto parser = SplitParser();
auto spy = QSignalSpy(&parser, &DataStreamParser::read);
@ -103,8 +101,6 @@ private slots:
qInfo() << "ACTUAL RESULTS" << actualResults;
QCOMPARE(actualResults, expected);
QCOMPARE(buf, "baz");
}
};
}
QTEST_MAIN(TestSplitParser)
#include "datastream.moc"
QTEST_MAIN(TestSplitParser);

View file

@ -0,0 +1,13 @@
#pragma once
#include <qobject.h>
#include <qtmetamacros.h>
class TestSplitParser: public QObject {
Q_OBJECT;
private slots:
void splits_data(); // NOLINT
void splits();
void initBuffer();
};

View file

@ -187,6 +187,7 @@ void WaylandPanelInterface::onReload(QObject* oldInstance) {
}
QQmlListProperty<QObject> WaylandPanelInterface::data() { return this->layer->data(); }
ProxyWindowBase* WaylandPanelInterface::proxyWindow() const { return this->layer; }
QQuickItem* WaylandPanelInterface::contentItem() const { return this->layer->contentItem(); }
// NOLINTBEGIN
@ -206,4 +207,6 @@ proxyPair(Anchors, anchors, setAnchors);
proxyPair(Margins, margins, setMargins);
proxyPair(qint32, exclusiveZone, setExclusiveZone);
proxyPair(ExclusionMode::Enum, exclusionMode, setExclusionMode);
#undef proxyPair
// NOLINTEND

View file

@ -121,6 +121,7 @@ public:
void onReload(QObject* oldInstance) override;
[[nodiscard]] ProxyWindowBase* proxyWindow() const override;
[[nodiscard]] QQuickItem* contentItem() const override;
// NOLINTBEGIN