forked from quickshell/quickshell
		
	core/boundcomponent: add BoundComponent
This commit is contained in:
		
							parent
							
								
									d64bf59bb0
								
							
						
					
					
						commit
						3c0456a3c0
					
				
					 4 changed files with 385 additions and 0 deletions
				
			
		| 
						 | 
				
			
			@ -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}")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										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",
 | 
			
		||||
	"easingcurve.hpp",
 | 
			
		||||
	"transformwatcher.hpp",
 | 
			
		||||
	"boundcomponent.hpp",
 | 
			
		||||
]
 | 
			
		||||
-----
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue