forked from quickshell/quickshell
core/boundcomponent: add BoundComponent
This commit is contained in:
parent
d64bf59bb0
commit
3c0456a3c0
|
@ -25,6 +25,7 @@ qt_add_library(quickshell-core STATIC
|
||||||
iconimageprovider.cpp
|
iconimageprovider.cpp
|
||||||
imageprovider.cpp
|
imageprovider.cpp
|
||||||
transformwatcher.cpp
|
transformwatcher.cpp
|
||||||
|
boundcomponent.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}")
|
set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}")
|
||||||
|
|
258
src/core/boundcomponent.cpp
Normal file
258
src/core/boundcomponent.cpp
Normal file
|
@ -0,0 +1,258 @@
|
||||||
|
#include "boundcomponent.hpp"
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <qcontainerfwd.h>
|
||||||
|
#include <qlogging.h>
|
||||||
|
#include <qmetaobject.h>
|
||||||
|
#include <qobject.h>
|
||||||
|
#include <qobjectdefs.h>
|
||||||
|
#include <qqmlcomponent.h>
|
||||||
|
#include <qqmlcontext.h>
|
||||||
|
#include <qqmlengine.h>
|
||||||
|
#include <qqmlerror.h>
|
||||||
|
#include <qquickitem.h>
|
||||||
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
|
#include "incubator.hpp"
|
||||||
|
|
||||||
|
QObject* BoundComponent::item() const { return this->object; }
|
||||||
|
QQmlComponent* BoundComponent::sourceComponent() const { return this->mComponent; }
|
||||||
|
|
||||||
|
void BoundComponent::setSourceComponent(QQmlComponent* component) {
|
||||||
|
if (component == this->mComponent) return;
|
||||||
|
|
||||||
|
if (this->componentCompleted) {
|
||||||
|
qWarning() << "BoundComponent.component cannot be set after creation";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->disconnectComponent();
|
||||||
|
|
||||||
|
this->ownsComponent = false;
|
||||||
|
this->mComponent = component;
|
||||||
|
if (component != nullptr) {
|
||||||
|
QObject::connect(component, &QObject::destroyed, this, &BoundComponent::onComponentDestroyed);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit this->sourceComponentChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoundComponent::disconnectComponent() {
|
||||||
|
if (this->mComponent == nullptr) return;
|
||||||
|
|
||||||
|
if (this->ownsComponent) {
|
||||||
|
delete this->mComponent;
|
||||||
|
} else {
|
||||||
|
QObject::disconnect(this->mComponent, nullptr, this, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
this->mComponent = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoundComponent::onComponentDestroyed() { this->mComponent = nullptr; }
|
||||||
|
QString BoundComponent::source() const { return this->mSource; }
|
||||||
|
|
||||||
|
void BoundComponent::setSource(QString source) {
|
||||||
|
if (source == this->mSource) return;
|
||||||
|
|
||||||
|
if (this->componentCompleted) {
|
||||||
|
qWarning() << "BoundComponent.url cannot be set after creation";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* context = QQmlEngine::contextForObject(this);
|
||||||
|
auto* component = new QQmlComponent(context->engine(), context->resolvedUrl(source), this);
|
||||||
|
|
||||||
|
if (component->isError()) {
|
||||||
|
qWarning() << component->errorString().toStdString().c_str();
|
||||||
|
delete component;
|
||||||
|
} else {
|
||||||
|
this->disconnectComponent();
|
||||||
|
this->ownsComponent = true;
|
||||||
|
this->mSource = std::move(source);
|
||||||
|
this->mComponent = component;
|
||||||
|
|
||||||
|
emit this->sourceChanged();
|
||||||
|
emit this->sourceComponentChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BoundComponent::bindValues() const { return this->mBindValues; }
|
||||||
|
|
||||||
|
void BoundComponent::setBindValues(bool bindValues) {
|
||||||
|
if (this->componentCompleted) {
|
||||||
|
qWarning() << "BoundComponent.bindValues cannot be set after creation";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->mBindValues = bindValues;
|
||||||
|
emit this->bindValuesChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoundComponent::componentComplete() {
|
||||||
|
this->QQuickItem::componentComplete();
|
||||||
|
this->componentCompleted = true;
|
||||||
|
this->tryCreate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoundComponent::tryCreate() {
|
||||||
|
if (this->mComponent == nullptr) {
|
||||||
|
qWarning() << "BoundComponent has no component";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto initialProperties = QVariantMap();
|
||||||
|
|
||||||
|
const auto* metaObject = this->metaObject();
|
||||||
|
for (auto i = metaObject->propertyOffset(); i < metaObject->propertyCount(); i++) {
|
||||||
|
const auto prop = metaObject->property(i);
|
||||||
|
|
||||||
|
if (prop.isReadable()) {
|
||||||
|
initialProperties.insert(prop.name(), prop.read(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this->incubator = new QsQmlIncubator(QsQmlIncubator::AsynchronousIfNested, this);
|
||||||
|
this->incubator->setInitialProperties(initialProperties);
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
QObject::connect(this->incubator, &QsQmlIncubator::completed, this, &BoundComponent::onIncubationCompleted);
|
||||||
|
QObject::connect(this->incubator, &QsQmlIncubator::failed, this, &BoundComponent::onIncubationFailed);
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
this->mComponent->create(*this->incubator, QQmlEngine::contextForObject(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoundComponent::onIncubationCompleted() {
|
||||||
|
this->object = this->incubator->object();
|
||||||
|
delete this->incubator;
|
||||||
|
this->disconnectComponent();
|
||||||
|
|
||||||
|
this->object->setParent(this);
|
||||||
|
this->mItem = qobject_cast<QQuickItem*>(this->object);
|
||||||
|
|
||||||
|
const auto* metaObject = this->metaObject();
|
||||||
|
const auto* objectMetaObject = this->object->metaObject();
|
||||||
|
|
||||||
|
if (this->mBindValues) {
|
||||||
|
for (auto i = metaObject->propertyOffset(); i < metaObject->propertyCount(); i++) {
|
||||||
|
const auto prop = metaObject->property(i);
|
||||||
|
|
||||||
|
if (prop.isReadable() && prop.hasNotifySignal()) {
|
||||||
|
const auto objectPropIndex = objectMetaObject->indexOfProperty(prop.name());
|
||||||
|
|
||||||
|
if (objectPropIndex == -1) {
|
||||||
|
qWarning() << "property" << prop.name()
|
||||||
|
<< "defined on BoundComponent but not on its contained object.";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto objectProp = objectMetaObject->property(objectPropIndex);
|
||||||
|
if (objectProp.isWritable()) {
|
||||||
|
auto* proxy = new BoundComponentPropertyProxy(this, this->object, prop, objectProp);
|
||||||
|
proxy->onNotified(); // any changes that might've happened before connection
|
||||||
|
} else {
|
||||||
|
qWarning() << "property" << prop.name()
|
||||||
|
<< "defined on BoundComponent is not writable for its contained object.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto i = metaObject->methodOffset(); i < metaObject->methodCount(); i++) {
|
||||||
|
const auto method = metaObject->method(i);
|
||||||
|
|
||||||
|
if (method.name().startsWith("on") && method.name().length() > 2) {
|
||||||
|
auto sig = QString(method.methodSignature()).sliced(2);
|
||||||
|
if (!sig[0].isUpper()) continue;
|
||||||
|
sig[0] = sig[0].toLower();
|
||||||
|
auto name = sig.sliced(0, sig.indexOf('('));
|
||||||
|
|
||||||
|
auto mostViableSignal = QMetaMethod();
|
||||||
|
for (auto i = 0; i < objectMetaObject->methodCount(); i++) {
|
||||||
|
const auto method = objectMetaObject->method(i);
|
||||||
|
if (method.methodSignature() == sig) {
|
||||||
|
mostViableSignal = method;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method.name() == name) {
|
||||||
|
if (mostViableSignal.isValid()) {
|
||||||
|
qWarning() << "Multiple candidates, so none will be attached for signal" << name;
|
||||||
|
goto next;
|
||||||
|
}
|
||||||
|
|
||||||
|
mostViableSignal = method;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mostViableSignal.isValid()) {
|
||||||
|
qWarning() << "Function" << method.name() << "appears to be a signal handler for" << name
|
||||||
|
<< "but it does not match any signals on the target object";
|
||||||
|
goto next;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMetaObject::connect(
|
||||||
|
this->object,
|
||||||
|
mostViableSignal.methodIndex(),
|
||||||
|
this,
|
||||||
|
method.methodIndex()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
next:;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->mItem != nullptr) {
|
||||||
|
this->mItem->setParentItem(this);
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
QObject::connect(this, &QQuickItem::widthChanged, this, &BoundComponent::updateSize);
|
||||||
|
QObject::connect(this, &QQuickItem::heightChanged, this, &BoundComponent::updateSize);
|
||||||
|
QObject::connect(this->mItem, &QQuickItem::implicitWidthChanged, this, &BoundComponent::updateImplicitSize);
|
||||||
|
QObject::connect(this->mItem, &QQuickItem::implicitHeightChanged, this, &BoundComponent::updateImplicitSize);
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
this->updateImplicitSize();
|
||||||
|
this->updateSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
emit this->loaded();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoundComponent::onIncubationFailed() {
|
||||||
|
qWarning() << "Failed to create BoundComponent";
|
||||||
|
|
||||||
|
for (auto& error: this->incubator->errors()) {
|
||||||
|
qWarning() << error;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete this->incubator;
|
||||||
|
this->disconnectComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoundComponent::updateSize() { this->mItem->setSize(this->size()); }
|
||||||
|
|
||||||
|
void BoundComponent::updateImplicitSize() {
|
||||||
|
this->setImplicitWidth(this->mItem->implicitWidth());
|
||||||
|
this->setImplicitHeight(this->mItem->implicitHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
BoundComponentPropertyProxy::BoundComponentPropertyProxy(
|
||||||
|
QObject* from,
|
||||||
|
QObject* to,
|
||||||
|
QMetaProperty fromProperty,
|
||||||
|
QMetaProperty toProperty
|
||||||
|
)
|
||||||
|
: QObject(from)
|
||||||
|
, from(from)
|
||||||
|
, to(to)
|
||||||
|
, fromProperty(fromProperty)
|
||||||
|
, toProperty(toProperty) {
|
||||||
|
const auto* metaObject = this->metaObject();
|
||||||
|
auto method = metaObject->indexOfSlot("onNotified()");
|
||||||
|
QMetaObject::connect(from, fromProperty.notifySignal().methodIndex(), this, method);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoundComponentPropertyProxy::onNotified() {
|
||||||
|
this->toProperty.write(this->to, this->fromProperty.read(this->from));
|
||||||
|
}
|
125
src/core/boundcomponent.hpp
Normal file
125
src/core/boundcomponent.hpp
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <qmetaobject.h>
|
||||||
|
#include <qobject.h>
|
||||||
|
#include <qqmlcomponent.h>
|
||||||
|
#include <qqmlparserstatus.h>
|
||||||
|
#include <qquickitem.h>
|
||||||
|
#include <qsignalmapper.h>
|
||||||
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
|
#include "incubator.hpp"
|
||||||
|
|
||||||
|
///! Component loader that allows setting initial properties.
|
||||||
|
/// Component loader that allows setting initial properties, primarily useful for
|
||||||
|
/// escaping cyclic dependency errors.
|
||||||
|
///
|
||||||
|
/// Properties defined on the BoundComponent will be applied to its loaded component,
|
||||||
|
/// including required properties, and will remain reactive. Functions created with
|
||||||
|
/// the names of signal handlers will also be attached to signals of the loaded component.
|
||||||
|
///
|
||||||
|
/// ```qml {filename="MyComponent.qml"}
|
||||||
|
/// MouseArea {
|
||||||
|
/// required property color color;
|
||||||
|
/// width: 100
|
||||||
|
/// height: 100
|
||||||
|
///
|
||||||
|
/// Rectangle {
|
||||||
|
/// anchors.fill: parent
|
||||||
|
/// color: parent.color
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```qml
|
||||||
|
/// BoundComponent {
|
||||||
|
/// source: "MyComponent.qml"
|
||||||
|
///
|
||||||
|
/// // this is the same as assigning to `color` on MyComponent if loaded normally.
|
||||||
|
/// property color color: "red";
|
||||||
|
///
|
||||||
|
/// // this will be triggered when the `clicked` signal from the MouseArea is sent.
|
||||||
|
/// function onClicked() {
|
||||||
|
/// color = "blue";
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
class BoundComponent: public QQuickItem {
|
||||||
|
Q_OBJECT;
|
||||||
|
// clang-format off
|
||||||
|
/// The loaded component. Will be null until it has finished loading.
|
||||||
|
Q_PROPERTY(QObject* item READ item NOTIFY loaded);
|
||||||
|
/// The source to load, as a Component.
|
||||||
|
Q_PROPERTY(QQmlComponent* sourceComponent READ sourceComponent WRITE setSourceComponent NOTIFY sourceComponentChanged);
|
||||||
|
/// The source to load, as a Url.
|
||||||
|
Q_PROPERTY(QString source READ source WRITE setSource NOTIFY sourceChanged);
|
||||||
|
/// If property values should be bound after they are initially set. Defaults to `true`.
|
||||||
|
Q_PROPERTY(bool bindValues READ bindValues WRITE setBindValues NOTIFY bindValuesChanged);
|
||||||
|
Q_PROPERTY(qreal implicitWidth READ implicitWidth NOTIFY implicitWidthChanged);
|
||||||
|
Q_PROPERTY(qreal implicitHeight READ implicitHeight NOTIFY implicitHeightChanged);
|
||||||
|
// clang-format on
|
||||||
|
QML_ELEMENT;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit BoundComponent(QQuickItem* parent = nullptr): QQuickItem(parent) {}
|
||||||
|
|
||||||
|
void componentComplete() override;
|
||||||
|
|
||||||
|
[[nodiscard]] QObject* item() const;
|
||||||
|
|
||||||
|
[[nodiscard]] QQmlComponent* sourceComponent() const;
|
||||||
|
void setSourceComponent(QQmlComponent* sourceComponent);
|
||||||
|
|
||||||
|
[[nodiscard]] QString source() const;
|
||||||
|
void setSource(QString source);
|
||||||
|
|
||||||
|
[[nodiscard]] bool bindValues() const;
|
||||||
|
void setBindValues(bool bindValues);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void loaded();
|
||||||
|
void sourceComponentChanged();
|
||||||
|
void sourceChanged();
|
||||||
|
void bindValuesChanged();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onComponentDestroyed();
|
||||||
|
void onIncubationCompleted();
|
||||||
|
void onIncubationFailed();
|
||||||
|
void updateSize();
|
||||||
|
void updateImplicitSize();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void disconnectComponent();
|
||||||
|
void tryCreate();
|
||||||
|
|
||||||
|
QString mSource;
|
||||||
|
bool mBindValues = true;
|
||||||
|
QQmlComponent* mComponent = nullptr;
|
||||||
|
bool ownsComponent = false;
|
||||||
|
QsQmlIncubator* incubator = nullptr;
|
||||||
|
QObject* object = nullptr;
|
||||||
|
QQuickItem* mItem = nullptr;
|
||||||
|
bool componentCompleted = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class BoundComponentPropertyProxy: public QObject {
|
||||||
|
Q_OBJECT;
|
||||||
|
|
||||||
|
public:
|
||||||
|
BoundComponentPropertyProxy(
|
||||||
|
QObject* from,
|
||||||
|
QObject* to,
|
||||||
|
QMetaProperty fromProperty,
|
||||||
|
QMetaProperty toProperty
|
||||||
|
);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void onNotified();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QObject* from;
|
||||||
|
QObject* to;
|
||||||
|
QMetaProperty fromProperty;
|
||||||
|
QMetaProperty toProperty;
|
||||||
|
};
|
|
@ -17,5 +17,6 @@ headers = [
|
||||||
"lazyloader.hpp",
|
"lazyloader.hpp",
|
||||||
"easingcurve.hpp",
|
"easingcurve.hpp",
|
||||||
"transformwatcher.hpp",
|
"transformwatcher.hpp",
|
||||||
|
"boundcomponent.hpp",
|
||||||
]
|
]
|
||||||
-----
|
-----
|
||||||
|
|
Loading…
Reference in a new issue