From a06af243adfc7612853d31c24f64b49fe7d6e2da Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 17 Apr 2024 04:31:02 -0700 Subject: [PATCH] core/transformwatcher: add TransformWatcher --- .clang-tidy | 1 + docs | 2 +- examples | 2 +- src/core/CMakeLists.txt | 1 + src/core/module.md | 1 + src/core/test/CMakeLists.txt | 1 + src/core/test/transformwatcher.cpp | 105 ++++++++++++++++++++++ src/core/test/transformwatcher.hpp | 14 +++ src/core/transformwatcher.cpp | 136 +++++++++++++++++++++++++++++ src/core/transformwatcher.hpp | 84 ++++++++++++++++++ 10 files changed, 345 insertions(+), 2 deletions(-) create mode 100644 src/core/test/transformwatcher.cpp create mode 100644 src/core/test/transformwatcher.hpp create mode 100644 src/core/transformwatcher.cpp create mode 100644 src/core/transformwatcher.hpp diff --git a/.clang-tidy b/.clang-tidy index 6642fa7..ff820f6 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -12,6 +12,7 @@ Checks: > -cppcoreguidelines-pro-bounds-constant-array-index, -cppcoreguidelines-avoid-const-or-ref-data-members, -cppcoreguidelines-non-private-member-variables-in-classes, + -cppcoreguidelines-avoid-goto, google-build-using-namespace. google-explicit-constructor, google-global-names-in-headers, diff --git a/docs b/docs index adb293d..7c6cdbc 160000 --- a/docs +++ b/docs @@ -1 +1 @@ -Subproject commit adb293de95f09ed7aa80b7bea615e4e2c9203be3 +Subproject commit 7c6cdbcd7b172a354669c0d2cb64bd1fbe1ca75f diff --git a/examples b/examples index 57078cd..b9e744b 160000 --- a/examples +++ b/examples @@ -1 +1 @@ -Subproject commit 57078cd364cbceebff431ad136986234d7a538bc +Subproject commit b9e744b50673304dfddb68f3da2a2e906d028b96 diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index eaff747..31cf726 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -23,6 +23,7 @@ qt_add_library(quickshell-core STATIC lazyloader.cpp easingcurve.cpp iconimageprovider.cpp + transformwatcher.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 bb713ec..d18c463 100644 --- a/src/core/module.md +++ b/src/core/module.md @@ -16,5 +16,6 @@ headers = [ "singleton.hpp", "lazyloader.hpp", "easingcurve.hpp", + "transformwatcher.hpp", ] ----- diff --git a/src/core/test/CMakeLists.txt b/src/core/test/CMakeLists.txt index 766ba58..d0191ee 100644 --- a/src/core/test/CMakeLists.txt +++ b/src/core/test/CMakeLists.txt @@ -5,3 +5,4 @@ function (qs_test name) endfunction() qs_test(popupwindow popupwindow.cpp) +qs_test(transformwatcher transformwatcher.cpp) diff --git a/src/core/test/transformwatcher.cpp b/src/core/test/transformwatcher.cpp new file mode 100644 index 0000000..ac10cfb --- /dev/null +++ b/src/core/test/transformwatcher.cpp @@ -0,0 +1,105 @@ +#include "transformwatcher.hpp" + +#include +#include +#include +#include +#include + +#include "../transformwatcher.hpp" + +void TestTransformWatcher::aParentOfB() { // NOLINT + auto a = QQuickItem(); + a.setObjectName("a"); + auto b = QQuickItem(); + b.setObjectName("b"); + b.setParentItem(&a); + + auto watcher = TransformWatcher(); + watcher.setA(&a); + watcher.setB(&b); + + QCOMPARE(watcher.parentChain, {&a}); + QCOMPARE(watcher.childChain, {&b}); +} + +void TestTransformWatcher::bParentOfA() { // NOLINT + auto a = QQuickItem(); + a.setObjectName("a"); + auto b = QQuickItem(); + b.setObjectName("b"); + a.setParentItem(&b); + + auto watcher = TransformWatcher(); + watcher.setA(&a); + watcher.setB(&b); + + QCOMPARE(watcher.parentChain, (QList {&a, &b})); + QCOMPARE(watcher.childChain, {}); +} + +// a +// p1 b +// p2 c1 +// p3 +void TestTransformWatcher::aParentChainB() { // NOLINT + auto a = QQuickItem(); + a.setObjectName("a"); + auto b = QQuickItem(); + b.setObjectName("b"); + + auto p1 = QQuickItem(); + p1.setObjectName("p1"); + auto p2 = QQuickItem(); + p2.setObjectName("p2"); + auto p3 = QQuickItem(); + p3.setObjectName("p3"); + auto c1 = QQuickItem(); + c1.setObjectName("c1"); + + a.setParentItem(&p1); + p1.setParentItem(&p2); + p2.setParentItem(&p3); + + b.setParentItem(&c1); + c1.setParentItem(&p3); + + auto watcher = TransformWatcher(); + watcher.setA(&a); + watcher.setB(&b); + + QCOMPARE(watcher.parentChain, (QList {&a, &p1, &p2, &p3})); + QCOMPARE(watcher.childChain, (QList {&b, &c1})); +} + +void TestTransformWatcher::multiWindow() { // NOLINT + auto a = QQuickItem(); + a.setObjectName("a"); + auto b = QQuickItem(); + b.setObjectName("b"); + + auto p = QQuickItem(); + p.setObjectName("p"); + auto c = QQuickItem(); + c.setObjectName("c"); + + a.setParentItem(&p); + b.setParentItem(&c); + + auto aW = QQuickWindow(); + auto bW = QQuickWindow(); + + p.setParentItem(aW.contentItem()); + c.setParentItem(bW.contentItem()); + + auto watcher = TransformWatcher(); + watcher.setA(&a); + watcher.setB(&b); + + QCOMPARE(watcher.parentChain, (QList {&a, &p, aW.contentItem()})); + QCOMPARE(watcher.childChain, (QList {&b, &c, bW.contentItem()})); + QCOMPARE(watcher.parentWindow, &aW); + QCOMPARE(watcher.childWindow, &bW); +} + +QTEST_MAIN(TestTransformWatcher); diff --git a/src/core/test/transformwatcher.hpp b/src/core/test/transformwatcher.hpp new file mode 100644 index 0000000..e2bdfda --- /dev/null +++ b/src/core/test/transformwatcher.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include +#include + +class TestTransformWatcher: public QObject { + Q_OBJECT; + +private slots: + void aParentOfB(); + void bParentOfA(); + void aParentChainB(); + void multiWindow(); +}; diff --git a/src/core/transformwatcher.cpp b/src/core/transformwatcher.cpp new file mode 100644 index 0000000..697dfc5 --- /dev/null +++ b/src/core/transformwatcher.cpp @@ -0,0 +1,136 @@ +#include "transformwatcher.hpp" + +#include +#include +#include +#include +#include +#include +#include + +void TransformWatcher::resolveChains(QQuickItem* a, QQuickItem* b, QQuickItem* commonParent) { + if (a == nullptr || b == nullptr) return; + + auto aChain = QVector(); + auto bChain = QVector(); + + auto* aParent = a; + auto* bParent = b; + + // resolve the parent chain of b. if a is in the chain break early + while (bParent != nullptr) { + bChain.push_back(bParent); + + if (bParent == a) { + aChain.push_back(a); + goto chainResolved; + } + + if (bParent == commonParent) break; + bParent = bParent->parentItem(); + } + + // resolve the parent chain of a, breaking as soon as b is found + while (aParent != nullptr) { + aChain.push_back(aParent); + + for (auto bParent = bChain.begin(); bParent != bChain.end(); bParent++) { + if (*bParent == aParent) { + bParent++; + bChain.erase(bParent, bChain.end()); + goto chainResolved; + } + } + + if (aParent == commonParent) break; + aParent = aParent->parentItem(); + } + + if (commonParent != nullptr && aParent == commonParent) { + qWarning() << this << "failed to find a common parent between" << a << "and" << b + << "due to incorrectly set commonParent" << commonParent; + + return; + } + +chainResolved: + + this->parentChain = aChain; + if (bChain.last() == aChain.last()) bChain.removeLast(); + this->childChain = bChain; + + if (a->window() != b->window()) { + this->parentWindow = a->window(); + this->childWindow = b->window(); + } else { + this->parentWindow = nullptr; + this->childWindow = nullptr; + } +} + +void TransformWatcher::resolveChains() { + this->resolveChains(this->mA, this->mB, this->mCommonParent); +} + +void TransformWatcher::linkItem(QQuickItem* item) const { + QObject::connect(item, &QQuickItem::xChanged, this, &TransformWatcher::transformChanged); + QObject::connect(item, &QQuickItem::yChanged, this, &TransformWatcher::transformChanged); + QObject::connect(item, &QQuickItem::widthChanged, this, &TransformWatcher::transformChanged); + QObject::connect(item, &QQuickItem::heightChanged, this, &TransformWatcher::transformChanged); + QObject::connect(item, &QQuickItem::scaleChanged, this, &TransformWatcher::transformChanged); + QObject::connect(item, &QQuickItem::rotationChanged, this, &TransformWatcher::transformChanged); + + QObject::connect(item, &QQuickItem::parentChanged, this, &TransformWatcher::recalcChains); + QObject::connect(item, &QQuickItem::windowChanged, this, &TransformWatcher::recalcChains); + QObject::connect(item, &QObject::destroyed, this, &TransformWatcher::recalcChains); +} + +void TransformWatcher::linkChains() { + for (auto* item: this->parentChain) { + this->linkItem(item); + } + + for (auto* item: this->childChain) { + this->linkItem(item); + } +} + +void TransformWatcher::unlinkChains() { + for (auto* item: this->parentChain) { + QObject::disconnect(item, nullptr, this, nullptr); + } + + for (auto* item: this->childChain) { + QObject::disconnect(item, nullptr, this, nullptr); + } +} + +void TransformWatcher::recalcChains() { + this->unlinkChains(); + this->resolveChains(); + this->linkChains(); +} + +QQuickItem* TransformWatcher::a() const { return this->mA; } + +void TransformWatcher::setA(QQuickItem* a) { + if (this->mA == a) return; + this->mA = a; + this->recalcChains(); +} + +QQuickItem* TransformWatcher::b() const { return this->mB; } + +void TransformWatcher::setB(QQuickItem* b) { + if (this->mB == b) return; + this->mB = b; + this->recalcChains(); +} + +QQuickItem* TransformWatcher::commonParent() const { return this->mCommonParent; } + +void TransformWatcher::setCommonParent(QQuickItem* commonParent) { + if (this->mCommonParent == commonParent) return; + this->mCommonParent = commonParent; + this->resolveChains(); +} diff --git a/src/core/transformwatcher.hpp b/src/core/transformwatcher.hpp new file mode 100644 index 0000000..d7174e4 --- /dev/null +++ b/src/core/transformwatcher.hpp @@ -0,0 +1,84 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#ifdef QS_TEST +class TestTransformWatcher; +#endif + +///! Monitor of all geometry changes between two objects. +/// The TransformWatcher monitors all properties that affect the geometry +/// of two `Item`s relative to eachother. +/// +/// > [!INFO] The algorithm responsible for determining the relationship +/// > between `a` and `b` is biased towards `a` being a parent of `b`, +/// > or `a` being closer to the common parent of `a` and `b` than `b`. +class TransformWatcher: public QObject { + Q_OBJECT; + // clang-format off + Q_PROPERTY(QQuickItem* a READ a WRITE setA NOTIFY aChanged); + Q_PROPERTY(QQuickItem* b READ b WRITE setB NOTIFY bChanged); + /// Known common parent of both `a` and `b`. Defaults to `null`. + /// + /// This property can be used to optimize the algorithm that figures out + /// the relationship between `a` and `b`. Setting it to something that is not + /// a common parent of both `a` and `b` will prevent the path from being determined + /// correctly, and setting it to `null` will disable the optimization. + Q_PROPERTY(QQuickItem* commonParent READ commonParent WRITE setCommonParent NOTIFY commonParentChanged); + /// This property is updated whenever the geometry of any item in the path from `a` to `b` changes. + /// + /// Its value is undefined, and is intended to trigger an expression update. + Q_PROPERTY(QObject* transform READ transform NOTIFY transformChanged); + // clang-format on + QML_ELEMENT; + +public: + explicit TransformWatcher(QObject* parent = nullptr): QObject(parent) {} + + [[nodiscard]] QQuickItem* a() const; + void setA(QQuickItem* a); + + [[nodiscard]] QQuickItem* b() const; + void setB(QQuickItem* b); + + [[nodiscard]] QQuickItem* commonParent() const; + void setCommonParent(QQuickItem* commonParent); + + [[nodiscard]] QObject* transform() const { return nullptr; } // NOLINT + +signals: + void transformChanged(); + + void aChanged(); + void bChanged(); + void commonParentChanged(); + +private slots: + void recalcChains(); + +private: + void resolveChains(QQuickItem* a, QQuickItem* b, QQuickItem* commonParent); + void resolveChains(); + void linkItem(QQuickItem* item) const; + void linkChains(); + void unlinkChains(); + + QQuickItem* mA = nullptr; + QQuickItem* mB = nullptr; + QQuickItem* mCommonParent = nullptr; + + // a -> traverse parent chain -> parent window -> global scope -> child window -> traverse child chain -> b + QList parentChain; + QList childChain; + QQuickWindow* parentWindow = nullptr; + QQuickWindow* childWindow = nullptr; + +#ifdef QS_TEST + friend class TestTransformWatcher; +#endif +};