core/transformwatcher: add TransformWatcher

This commit is contained in:
outfoxxed 2024-04-17 04:31:02 -07:00
parent fd5b73adbb
commit a06af243ad
Signed by: outfoxxed
GPG key ID: 4C88A185FB89301E
10 changed files with 345 additions and 2 deletions

View file

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

2
docs

@ -1 +1 @@
Subproject commit adb293de95f09ed7aa80b7bea615e4e2c9203be3
Subproject commit 7c6cdbcd7b172a354669c0d2cb64bd1fbe1ca75f

@ -1 +1 @@
Subproject commit 57078cd364cbceebff431ad136986234d7a538bc
Subproject commit b9e744b50673304dfddb68f3da2a2e906d028b96

View file

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

View file

@ -16,5 +16,6 @@ headers = [
"singleton.hpp",
"lazyloader.hpp",
"easingcurve.hpp",
"transformwatcher.hpp",
]
-----

View file

@ -5,3 +5,4 @@ function (qs_test name)
endfunction()
qs_test(popupwindow popupwindow.cpp)
qs_test(transformwatcher transformwatcher.cpp)

View file

@ -0,0 +1,105 @@
#include "transformwatcher.hpp"
#include <qlist.h>
#include <qquickitem.h>
#include <qquickwindow.h>
#include <qtest.h>
#include <qtestcase.h>
#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);

View file

@ -0,0 +1,14 @@
#pragma once
#include <qobject.h>
#include <qtmetamacros.h>
class TestTransformWatcher: public QObject {
Q_OBJECT;
private slots:
void aParentOfB();
void bParentOfA();
void aParentChainB();
void multiWindow();
};

View file

@ -0,0 +1,136 @@
#include "transformwatcher.hpp"
#include <qcontainerfwd.h>
#include <qdebug.h>
#include <qlist.h>
#include <qlogging.h>
#include <qobject.h>
#include <qquickitem.h>
#include <qquickwindow.h>
void TransformWatcher::resolveChains(QQuickItem* a, QQuickItem* b, QQuickItem* commonParent) {
if (a == nullptr || b == nullptr) return;
auto aChain = QVector<QQuickItem*>();
auto bChain = QVector<QQuickItem*>();
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();
}

View file

@ -0,0 +1,84 @@
#pragma once
#include <qlist.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qquickitem.h>
#include <qquickwindow.h>
#include <qtmetamacros.h>
#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<QQuickItem*> parentChain;
QList<QQuickItem*> childChain;
QQuickWindow* parentWindow = nullptr;
QQuickWindow* childWindow = nullptr;
#ifdef QS_TEST
friend class TestTransformWatcher;
#endif
};