From 3c0456a3c0aaed53e06b680192caae286eee8e2e Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 1 May 2024 02:14:32 -0700 Subject: [PATCH] core/boundcomponent: add BoundComponent --- src/core/CMakeLists.txt | 1 + src/core/boundcomponent.cpp | 258 ++++++++++++++++++++++++++++++++++++ src/core/boundcomponent.hpp | 125 +++++++++++++++++ src/core/module.md | 1 + 4 files changed, 385 insertions(+) create mode 100644 src/core/boundcomponent.cpp create mode 100644 src/core/boundcomponent.hpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 294e8f92..b40b807f 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -25,6 +25,7 @@ qt_add_library(quickshell-core STATIC iconimageprovider.cpp imageprovider.cpp transformwatcher.cpp + boundcomponent.cpp ) set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}") diff --git a/src/core/boundcomponent.cpp b/src/core/boundcomponent.cpp new file mode 100644 index 00000000..8b1c8284 --- /dev/null +++ b/src/core/boundcomponent.cpp @@ -0,0 +1,258 @@ +#include "boundcomponent.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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(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)); +} diff --git a/src/core/boundcomponent.hpp b/src/core/boundcomponent.hpp new file mode 100644 index 00000000..d47121df --- /dev/null +++ b/src/core/boundcomponent.hpp @@ -0,0 +1,125 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#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; +}; diff --git a/src/core/module.md b/src/core/module.md index d18c4638..8eb9b638 100644 --- a/src/core/module.md +++ b/src/core/module.md @@ -17,5 +17,6 @@ headers = [ "lazyloader.hpp", "easingcurve.hpp", "transformwatcher.hpp", + "boundcomponent.hpp", ] -----