Skip to content

Commit 3c0456a

Browse files
committed
core/boundcomponent: add BoundComponent
1 parent d64bf59 commit 3c0456a

File tree

4 files changed

+385
-0
lines changed

4 files changed

+385
-0
lines changed

src/core/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ qt_add_library(quickshell-core STATIC
2525
iconimageprovider.cpp
2626
imageprovider.cpp
2727
transformwatcher.cpp
28+
boundcomponent.cpp
2829
)
2930

3031
set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}")

src/core/boundcomponent.cpp

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
#include "boundcomponent.hpp"
2+
#include <utility>
3+
4+
#include <qcontainerfwd.h>
5+
#include <qlogging.h>
6+
#include <qmetaobject.h>
7+
#include <qobject.h>
8+
#include <qobjectdefs.h>
9+
#include <qqmlcomponent.h>
10+
#include <qqmlcontext.h>
11+
#include <qqmlengine.h>
12+
#include <qqmlerror.h>
13+
#include <qquickitem.h>
14+
#include <qtmetamacros.h>
15+
16+
#include "incubator.hpp"
17+
18+
QObject* BoundComponent::item() const { return this->object; }
19+
QQmlComponent* BoundComponent::sourceComponent() const { return this->mComponent; }
20+
21+
void BoundComponent::setSourceComponent(QQmlComponent* component) {
22+
if (component == this->mComponent) return;
23+
24+
if (this->componentCompleted) {
25+
qWarning() << "BoundComponent.component cannot be set after creation";
26+
return;
27+
}
28+
this->disconnectComponent();
29+
30+
this->ownsComponent = false;
31+
this->mComponent = component;
32+
if (component != nullptr) {
33+
QObject::connect(component, &QObject::destroyed, this, &BoundComponent::onComponentDestroyed);
34+
}
35+
36+
emit this->sourceComponentChanged();
37+
}
38+
39+
void BoundComponent::disconnectComponent() {
40+
if (this->mComponent == nullptr) return;
41+
42+
if (this->ownsComponent) {
43+
delete this->mComponent;
44+
} else {
45+
QObject::disconnect(this->mComponent, nullptr, this, nullptr);
46+
}
47+
48+
this->mComponent = nullptr;
49+
}
50+
51+
void BoundComponent::onComponentDestroyed() { this->mComponent = nullptr; }
52+
QString BoundComponent::source() const { return this->mSource; }
53+
54+
void BoundComponent::setSource(QString source) {
55+
if (source == this->mSource) return;
56+
57+
if (this->componentCompleted) {
58+
qWarning() << "BoundComponent.url cannot be set after creation";
59+
return;
60+
}
61+
62+
auto* context = QQmlEngine::contextForObject(this);
63+
auto* component = new QQmlComponent(context->engine(), context->resolvedUrl(source), this);
64+
65+
if (component->isError()) {
66+
qWarning() << component->errorString().toStdString().c_str();
67+
delete component;
68+
} else {
69+
this->disconnectComponent();
70+
this->ownsComponent = true;
71+
this->mSource = std::move(source);
72+
this->mComponent = component;
73+
74+
emit this->sourceChanged();
75+
emit this->sourceComponentChanged();
76+
}
77+
}
78+
79+
bool BoundComponent::bindValues() const { return this->mBindValues; }
80+
81+
void BoundComponent::setBindValues(bool bindValues) {
82+
if (this->componentCompleted) {
83+
qWarning() << "BoundComponent.bindValues cannot be set after creation";
84+
return;
85+
}
86+
87+
this->mBindValues = bindValues;
88+
emit this->bindValuesChanged();
89+
}
90+
91+
void BoundComponent::componentComplete() {
92+
this->QQuickItem::componentComplete();
93+
this->componentCompleted = true;
94+
this->tryCreate();
95+
}
96+
97+
void BoundComponent::tryCreate() {
98+
if (this->mComponent == nullptr) {
99+
qWarning() << "BoundComponent has no component";
100+
return;
101+
}
102+
103+
auto initialProperties = QVariantMap();
104+
105+
const auto* metaObject = this->metaObject();
106+
for (auto i = metaObject->propertyOffset(); i < metaObject->propertyCount(); i++) {
107+
const auto prop = metaObject->property(i);
108+
109+
if (prop.isReadable()) {
110+
initialProperties.insert(prop.name(), prop.read(this));
111+
}
112+
}
113+
114+
this->incubator = new QsQmlIncubator(QsQmlIncubator::AsynchronousIfNested, this);
115+
this->incubator->setInitialProperties(initialProperties);
116+
117+
// clang-format off
118+
QObject::connect(this->incubator, &QsQmlIncubator::completed, this, &BoundComponent::onIncubationCompleted);
119+
QObject::connect(this->incubator, &QsQmlIncubator::failed, this, &BoundComponent::onIncubationFailed);
120+
// clang-format on
121+
122+
this->mComponent->create(*this->incubator, QQmlEngine::contextForObject(this));
123+
}
124+
125+
void BoundComponent::onIncubationCompleted() {
126+
this->object = this->incubator->object();
127+
delete this->incubator;
128+
this->disconnectComponent();
129+
130+
this->object->setParent(this);
131+
this->mItem = qobject_cast<QQuickItem*>(this->object);
132+
133+
const auto* metaObject = this->metaObject();
134+
const auto* objectMetaObject = this->object->metaObject();
135+
136+
if (this->mBindValues) {
137+
for (auto i = metaObject->propertyOffset(); i < metaObject->propertyCount(); i++) {
138+
const auto prop = metaObject->property(i);
139+
140+
if (prop.isReadable() && prop.hasNotifySignal()) {
141+
const auto objectPropIndex = objectMetaObject->indexOfProperty(prop.name());
142+
143+
if (objectPropIndex == -1) {
144+
qWarning() << "property" << prop.name()
145+
<< "defined on BoundComponent but not on its contained object.";
146+
continue;
147+
}
148+
149+
const auto objectProp = objectMetaObject->property(objectPropIndex);
150+
if (objectProp.isWritable()) {
151+
auto* proxy = new BoundComponentPropertyProxy(this, this->object, prop, objectProp);
152+
proxy->onNotified(); // any changes that might've happened before connection
153+
} else {
154+
qWarning() << "property" << prop.name()
155+
<< "defined on BoundComponent is not writable for its contained object.";
156+
}
157+
}
158+
}
159+
}
160+
161+
for (auto i = metaObject->methodOffset(); i < metaObject->methodCount(); i++) {
162+
const auto method = metaObject->method(i);
163+
164+
if (method.name().startsWith("on") && method.name().length() > 2) {
165+
auto sig = QString(method.methodSignature()).sliced(2);
166+
if (!sig[0].isUpper()) continue;
167+
sig[0] = sig[0].toLower();
168+
auto name = sig.sliced(0, sig.indexOf('('));
169+
170+
auto mostViableSignal = QMetaMethod();
171+
for (auto i = 0; i < objectMetaObject->methodCount(); i++) {
172+
const auto method = objectMetaObject->method(i);
173+
if (method.methodSignature() == sig) {
174+
mostViableSignal = method;
175+
break;
176+
}
177+
178+
if (method.name() == name) {
179+
if (mostViableSignal.isValid()) {
180+
qWarning() << "Multiple candidates, so none will be attached for signal" << name;
181+
goto next;
182+
}
183+
184+
mostViableSignal = method;
185+
}
186+
}
187+
188+
if (!mostViableSignal.isValid()) {
189+
qWarning() << "Function" << method.name() << "appears to be a signal handler for" << name
190+
<< "but it does not match any signals on the target object";
191+
goto next;
192+
}
193+
194+
QMetaObject::connect(
195+
this->object,
196+
mostViableSignal.methodIndex(),
197+
this,
198+
method.methodIndex()
199+
);
200+
}
201+
202+
next:;
203+
}
204+
205+
if (this->mItem != nullptr) {
206+
this->mItem->setParentItem(this);
207+
208+
// clang-format off
209+
QObject::connect(this, &QQuickItem::widthChanged, this, &BoundComponent::updateSize);
210+
QObject::connect(this, &QQuickItem::heightChanged, this, &BoundComponent::updateSize);
211+
QObject::connect(this->mItem, &QQuickItem::implicitWidthChanged, this, &BoundComponent::updateImplicitSize);
212+
QObject::connect(this->mItem, &QQuickItem::implicitHeightChanged, this, &BoundComponent::updateImplicitSize);
213+
// clang-format on
214+
215+
this->updateImplicitSize();
216+
this->updateSize();
217+
}
218+
219+
emit this->loaded();
220+
}
221+
222+
void BoundComponent::onIncubationFailed() {
223+
qWarning() << "Failed to create BoundComponent";
224+
225+
for (auto& error: this->incubator->errors()) {
226+
qWarning() << error;
227+
}
228+
229+
delete this->incubator;
230+
this->disconnectComponent();
231+
}
232+
233+
void BoundComponent::updateSize() { this->mItem->setSize(this->size()); }
234+
235+
void BoundComponent::updateImplicitSize() {
236+
this->setImplicitWidth(this->mItem->implicitWidth());
237+
this->setImplicitHeight(this->mItem->implicitHeight());
238+
}
239+
240+
BoundComponentPropertyProxy::BoundComponentPropertyProxy(
241+
QObject* from,
242+
QObject* to,
243+
QMetaProperty fromProperty,
244+
QMetaProperty toProperty
245+
)
246+
: QObject(from)
247+
, from(from)
248+
, to(to)
249+
, fromProperty(fromProperty)
250+
, toProperty(toProperty) {
251+
const auto* metaObject = this->metaObject();
252+
auto method = metaObject->indexOfSlot("onNotified()");
253+
QMetaObject::connect(from, fromProperty.notifySignal().methodIndex(), this, method);
254+
}
255+
256+
void BoundComponentPropertyProxy::onNotified() {
257+
this->toProperty.write(this->to, this->fromProperty.read(this->from));
258+
}

src/core/boundcomponent.hpp

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
#pragma once
2+
3+
#include <qmetaobject.h>
4+
#include <qobject.h>
5+
#include <qqmlcomponent.h>
6+
#include <qqmlparserstatus.h>
7+
#include <qquickitem.h>
8+
#include <qsignalmapper.h>
9+
#include <qtmetamacros.h>
10+
11+
#include "incubator.hpp"
12+
13+
///! Component loader that allows setting initial properties.
14+
/// Component loader that allows setting initial properties, primarily useful for
15+
/// escaping cyclic dependency errors.
16+
///
17+
/// Properties defined on the BoundComponent will be applied to its loaded component,
18+
/// including required properties, and will remain reactive. Functions created with
19+
/// the names of signal handlers will also be attached to signals of the loaded component.
20+
///
21+
/// ```qml {filename="MyComponent.qml"}
22+
/// MouseArea {
23+
/// required property color color;
24+
/// width: 100
25+
/// height: 100
26+
///
27+
/// Rectangle {
28+
/// anchors.fill: parent
29+
/// color: parent.color
30+
/// }
31+
/// }
32+
/// ```
33+
///
34+
/// ```qml
35+
/// BoundComponent {
36+
/// source: "MyComponent.qml"
37+
///
38+
/// // this is the same as assigning to `color` on MyComponent if loaded normally.
39+
/// property color color: "red";
40+
///
41+
/// // this will be triggered when the `clicked` signal from the MouseArea is sent.
42+
/// function onClicked() {
43+
/// color = "blue";
44+
/// }
45+
/// }
46+
/// ```
47+
class BoundComponent: public QQuickItem {
48+
Q_OBJECT;
49+
// clang-format off
50+
/// The loaded component. Will be null until it has finished loading.
51+
Q_PROPERTY(QObject* item READ item NOTIFY loaded);
52+
/// The source to load, as a Component.
53+
Q_PROPERTY(QQmlComponent* sourceComponent READ sourceComponent WRITE setSourceComponent NOTIFY sourceComponentChanged);
54+
/// The source to load, as a Url.
55+
Q_PROPERTY(QString source READ source WRITE setSource NOTIFY sourceChanged);
56+
/// If property values should be bound after they are initially set. Defaults to `true`.
57+
Q_PROPERTY(bool bindValues READ bindValues WRITE setBindValues NOTIFY bindValuesChanged);
58+
Q_PROPERTY(qreal implicitWidth READ implicitWidth NOTIFY implicitWidthChanged);
59+
Q_PROPERTY(qreal implicitHeight READ implicitHeight NOTIFY implicitHeightChanged);
60+
// clang-format on
61+
QML_ELEMENT;
62+
63+
public:
64+
explicit BoundComponent(QQuickItem* parent = nullptr): QQuickItem(parent) {}
65+
66+
void componentComplete() override;
67+
68+
[[nodiscard]] QObject* item() const;
69+
70+
[[nodiscard]] QQmlComponent* sourceComponent() const;
71+
void setSourceComponent(QQmlComponent* sourceComponent);
72+
73+
[[nodiscard]] QString source() const;
74+
void setSource(QString source);
75+
76+
[[nodiscard]] bool bindValues() const;
77+
void setBindValues(bool bindValues);
78+
79+
signals:
80+
void loaded();
81+
void sourceComponentChanged();
82+
void sourceChanged();
83+
void bindValuesChanged();
84+
85+
private slots:
86+
void onComponentDestroyed();
87+
void onIncubationCompleted();
88+
void onIncubationFailed();
89+
void updateSize();
90+
void updateImplicitSize();
91+
92+
private:
93+
void disconnectComponent();
94+
void tryCreate();
95+
96+
QString mSource;
97+
bool mBindValues = true;
98+
QQmlComponent* mComponent = nullptr;
99+
bool ownsComponent = false;
100+
QsQmlIncubator* incubator = nullptr;
101+
QObject* object = nullptr;
102+
QQuickItem* mItem = nullptr;
103+
bool componentCompleted = false;
104+
};
105+
106+
class BoundComponentPropertyProxy: public QObject {
107+
Q_OBJECT;
108+
109+
public:
110+
BoundComponentPropertyProxy(
111+
QObject* from,
112+
QObject* to,
113+
QMetaProperty fromProperty,
114+
QMetaProperty toProperty
115+
);
116+
117+
public slots:
118+
void onNotified();
119+
120+
private:
121+
QObject* from;
122+
QObject* to;
123+
QMetaProperty fromProperty;
124+
QMetaProperty toProperty;
125+
};

src/core/module.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@ headers = [
1717
"lazyloader.hpp",
1818
"easingcurve.hpp",
1919
"transformwatcher.hpp",
20+
"boundcomponent.hpp",
2021
]
2122
-----

0 commit comments

Comments
 (0)