aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorLuca Di Sera <luca.disera@qt.io>2024-03-11 16:33:35 +0100
committerLuca Di Sera <luca.disera@qt.io>2024-03-20 16:59:30 +0100
commit361b601620073351013b516e9f7cf9f5173b9ce7 (patch)
tree8a46f93f4349dfe8dc64493834def517bf2daa8c /tools
parentcf15ba9b085ac783fd3315f1b103c012947cb7db (diff)
qmltc: Allow setting initial values when creating a compiled type
`qmltc`-generated types currently do not allow setting any initial values for the property of a component during creation. For example, some component `Foo` with a property `bar`, will have no way to set `bar` to a specific value from the C++ side of the code before an instance of `Foo` is obtained by the user. This lack of control prohibits the user from interacting with certain processes that are part of the component creation. For example, if a component provides am `onCompleted` binding that depends on some of the values of its properties, the user is inhibited from varying the per-instance values that `onCompleted` depends on, as the user would be able to vary those values only after the component is created and the `onCompleted` signal is emitted. This differs, from example, from the `QQmlComponent` interface, where the user is able to provide some initialization values as part of the creation of an instance of the component. To allow the user to have more control in the initialization of the instance of a component, before it is fully created, `qmltc` generated code now allows the user to provide an initialization callback that is processed as part of the creation cycle of an instance of the component. The callback provides the user with a generated proxy object, `PropertyInitializer`, that only knows how to set the writable, non-private properties of the component. The generated code for the public constructor of a `qmltc`-generated type was modified to provide an optional `initializer` parameter that stores the callback. The private `QML_init` method that `qmltc` generates for each type, that performs the required setup to create an instance of a component, was modified to allow for the same optional parameter, which is passed on by the public constructor. The body of `QML_init` was modified to call the new parameter, after having performed the general set-up for the created instance but before the instance is completed and before setting up "complex bindings" such as an `onPropertyChanged` handler. The callback is called with an instance of the generated proxy object that is built on-site. The proxy-object keeps track of the properties that were actually initialized by the callback. This information is now passed down to `QML_setComplexBindings`, which avoids setting up any unnecessary bindings for the properties that were initialized. A representation for the proxy object was added to the internal IR that is used by `qmltc` when generating code. The representation for a compiled C++ type was modified to store an instance of the proxy object. The newly stored instance is now populated as part of the general compilation of a type, by the `compilePropertyInitializer` free-function in "qmltccompiler.cpp". The component responsible for the final code-generation, `QmltcCodeWriter`, was modified to be able to generate code for the new proxy object representation. The documentation for `QmltcCodeGenerator::generate_initCode`, which sets up the body for `QML_init`, was updated to reflect the new body. A pre-existing issue in the documentation of the method, which failed to enumerate all generated steps for the body, was fixed as part of the change. The preamble that `qmltc` generates for all generated header files was modified to include "QtCore/qxpfunction.h", to have access to `qxp::function_ref`, which is used to store the new callback parameter. A pre-existing snapshot test had its snapshot file, "src/qml/doc/snippets/qmltc/special/HelloWorld.qml.cpp", updated to reflect the changes to the generated code. A new basic, non-exhaustive test case was added to the available corpus of `qmltc` tests to test the basic workflow of providing an initialization callback. The documentation for `qmltc` was modified to mention the new parameter. Task-number: QTBUG-120700 Change-Id: I246c1c3634982580d66b31fd891382559a9cc3ae Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
Diffstat (limited to 'tools')
-rw-r--r--tools/qmltc/qmltccodewriter.cpp46
-rw-r--r--tools/qmltc/qmltccodewriter.h1
-rw-r--r--tools/qmltc/qmltccompiler.cpp111
-rw-r--r--tools/qmltc/qmltccompilerpieces.cpp3
-rw-r--r--tools/qmltc/qmltccompilerpieces.h25
-rw-r--r--tools/qmltc/qmltcoutputir.h27
6 files changed, 199 insertions, 14 deletions
diff --git a/tools/qmltc/qmltccodewriter.cpp b/tools/qmltc/qmltccodewriter.cpp
index 67ffcd6b0b..d4eb57606c 100644
--- a/tools/qmltc/qmltccodewriter.cpp
+++ b/tools/qmltc/qmltccodewriter.cpp
@@ -118,6 +118,7 @@ void QmltcCodeWriter::writeGlobalHeader(QmltcOutputWrapper &code, const QString
code.rawAppendToHeader(u"#include <QtCore/qproperty.h>");
code.rawAppendToHeader(u"#include <QtCore/qobject.h>");
code.rawAppendToHeader(u"#include <QtCore/qcoreapplication.h>");
+ code.rawAppendToHeader(u"#include <QtCore/qxpfunctional.h>");
code.rawAppendToHeader(u"#include <QtQml/qqmlengine.h>");
code.rawAppendToHeader(u"#include <QtCore/qurl.h>"); // used in engine execution
code.rawAppendToHeader(u"#include <QtQml/qqml.h>"); // used for attached properties
@@ -160,6 +161,48 @@ void QmltcCodeWriter::writeGlobalHeader(QmltcOutputWrapper &code, const QString
}
}
+void QmltcCodeWriter::write(QmltcOutputWrapper &code,
+ const QmltcPropertyInitializer &propertyInitializer,
+ const QmltcType &wrappedType)
+{
+ code.rawAppendToHeader(u"class " + propertyInitializer.name + u" {");
+
+ {
+ {
+ [[maybe_unused]] QmltcOutputWrapper::HeaderIndentationScope headerIndent(&code);
+
+ code.rawAppendToHeader(u"friend class " + wrappedType.cppType + u";");
+ }
+
+ code.rawAppendToHeader(u"public:"_s);
+
+ [[maybe_unused]] QmltcOutputWrapper::MemberNameScope typeScope(&code, propertyInitializer.name);
+ {
+ [[maybe_unused]] QmltcOutputWrapper::HeaderIndentationScope headerIndent(&code);
+
+ write(code, propertyInitializer.constructor);
+ code.rawAppendToHeader(u""); // blank line
+
+ for (const auto &propertySetter : propertyInitializer.propertySetters) {
+ write(code, propertySetter);
+ }
+ }
+
+ code.rawAppendToHeader(u""); // blank line
+ code.rawAppendToHeader(u"private:"_s);
+
+ {
+ [[maybe_unused]] QmltcOutputWrapper::HeaderIndentationScope headerIndent(&code);
+
+ write(code, propertyInitializer.component);
+ write(code, propertyInitializer.initializedCache);
+ }
+ }
+
+ code.rawAppendToHeader(u"};"_s);
+ code.rawAppendToHeader(u""); // blank line
+}
+
void QmltcCodeWriter::writeGlobalFooter(QmltcOutputWrapper &code, const QString &sourcePath,
const QString &outNamespace)
{
@@ -291,6 +334,9 @@ void QmltcCodeWriter::write(QmltcOutputWrapper &code, const QmltcType &type,
code.rawAppendToHeader(u"/* External C++ API */");
code.rawAppendToHeader(u"public:", -1);
+ if (!type.propertyInitializer.name.isEmpty())
+ write(code, type.propertyInitializer, type);
+
// NB: when non-document root, the externalCtor won't be public - but we
// really don't care about the output format of such types
if (!type.ignoreInit && type.externalCtor.access == QQmlJSMetaMethod::Public) {
diff --git a/tools/qmltc/qmltccodewriter.h b/tools/qmltc/qmltccodewriter.h
index 04446f4c4e..b45773eef9 100644
--- a/tools/qmltc/qmltccodewriter.h
+++ b/tools/qmltc/qmltccodewriter.h
@@ -27,6 +27,7 @@ struct QmltcCodeWriter
static void write(QmltcOutputWrapper &code, const QmltcDtor &dtor);
static void write(QmltcOutputWrapper &code, const QmltcVariable &var);
static void write(QmltcOutputWrapper &code, const QmltcProperty &prop);
+ static void write(QmltcOutputWrapper &code, const QmltcPropertyInitializer &propertyInitializer, const QmltcType& wrappedType);
private:
static void writeUrl(QmltcOutputWrapper &code, const QmltcMethod &urlMethod); // special
diff --git a/tools/qmltc/qmltccompiler.cpp b/tools/qmltc/qmltccompiler.cpp
index 1ad0fdaf20..f0371bcaf1 100644
--- a/tools/qmltc/qmltccompiler.cpp
+++ b/tools/qmltc/qmltccompiler.cpp
@@ -248,6 +248,17 @@ void QmltcCompiler::compileType(
current.finalizeComponent.access = QQmlJSMetaMethod::Protected;
current.handleOnCompleted.access = QQmlJSMetaMethod::Protected;
+ current.propertyInitializer.name = u"PropertyInitializer"_s;
+ current.propertyInitializer.constructor.access = QQmlJSMetaMethod::Public;
+ current.propertyInitializer.constructor.name = current.propertyInitializer.name;
+ current.propertyInitializer.constructor.parameterList = {
+ QmltcVariable(u"%1&"_s.arg(current.cppType), u"component"_s)
+ };
+ current.propertyInitializer.component.cppType = current.cppType + u"&";
+ current.propertyInitializer.component.name = u"component"_s;
+ current.propertyInitializer.initializedCache.cppType = u"QSet<QString>"_s;
+ current.propertyInitializer.initializedCache.name = u"initializedCache"_s;
+
current.baselineCtor.name = current.cppType;
current.externalCtor.name = current.cppType;
current.init.name = u"QML_init"_s;
@@ -267,16 +278,26 @@ void QmltcCompiler::compileType(
QmltcVariable creator(u"QQmltcObjectCreationHelper*"_s, u"creator"_s);
QmltcVariable engine(u"QQmlEngine*"_s, u"engine"_s);
QmltcVariable parent(u"QObject*"_s, u"parent"_s, u"nullptr"_s);
+ QmltcVariable initializedCache(
+ u"[[maybe_unused]] const QSet<QString>&"_s,
+ u"initializedCache"_s,
+ u"{}"_s
+ );
QmltcVariable ctxtdata(u"const QQmlRefPointer<QQmlContextData>&"_s, u"parentContext"_s);
QmltcVariable finalizeFlag(u"bool"_s, u"canFinalize"_s);
current.baselineCtor.parameterList = { parent };
current.endInit.parameterList = { creator, engine };
- current.setComplexBindings.parameterList = { creator, engine };
+ current.setComplexBindings.parameterList = { creator, engine, initializedCache };
current.handleOnCompleted.parameterList = { creator };
if (documentRoot || inlineComponent) {
- current.externalCtor.parameterList = { engine, parent };
- current.init.parameterList = { creator, engine, ctxtdata, finalizeFlag };
+ QmltcVariable initializer(
+ u"[[maybe_unused]] qxp::function_ref<void(%1&)>"_s.arg(current.propertyInitializer.name),
+ u"initializer"_s,
+ u"[](%1&){}"_s.arg(current.propertyInitializer.name));
+
+ current.externalCtor.parameterList = { engine, parent, initializer };
+ current.init.parameterList = { creator, engine, ctxtdata, finalizeFlag, initializer };
current.beginClass.parameterList = { creator, finalizeFlag };
current.completeComponent.parameterList = { creator, finalizeFlag };
current.finalizeComponent.parameterList = { creator, finalizeFlag };
@@ -314,7 +335,7 @@ void QmltcCompiler::compileType(
// now call init
current.externalCtor.body << current.init.name
+ u"(&creator, engine, QQmlContextData::get(engine->rootContext()), /* "
- u"endInit */ true);";
+ u"endInit */ true, initializer);";
} else {
current.externalCtor.body << u"// not document root:"_s;
// just call init, we don't do any setup here otherwise
@@ -362,6 +383,82 @@ static Iterator partitionBindings(Iterator first, Iterator last)
});
}
+// Populates the propertyInitializer of the current type based on the
+// available properties.
+//
+// A propertyInitializer is a generated class that provides a
+// restricted interface that only allows setting property values and
+// internally keep tracks of which properties where actually set,
+// intended to be used to allow the user to set up the initial values
+// when creating an instance of a component.
+//
+// For each property of the current type that is known, is not private
+// and is writable, a setter method is generated.
+// Each setter method knows how to set a specific property, so as to
+// provide a strongly typed interface to property setting, as if the
+// relevant C++ type was used directly.
+//
+// Each setter uses the write method for the proprerty when available
+// and otherwise falls back to a the more generic
+// `QObject::setProperty` for properties where a WRITE method is not
+// available or in scope.
+static void compilePropertyInitializer(QmltcType &current, const QQmlJSScope::ConstPtr &type) {
+ static auto isFromExtension = [](const QQmlJSMetaProperty &property, const QQmlJSScope::ConstPtr &scope) {
+ return scope->ownerOfProperty(scope, property.propertyName()).extensionSpecifier != QQmlJSScope::NotExtension;
+ };
+
+ current.propertyInitializer.constructor.initializerList << u"component{component}"_s;
+
+ auto properties = type->properties().values();
+ for (auto& property: properties) {
+ if (property.index() == -1) continue;
+ if (property.isPrivate()) continue;
+ if (!property.isWritable() || qIsReferenceTypeList(property)) continue;
+
+ const QString name = property.propertyName();
+
+ current.propertyInitializer.propertySetters.emplace_back();
+ auto& compiledSetter = current.propertyInitializer.propertySetters.back();
+
+ compiledSetter.userVisible = true;
+ compiledSetter.returnType = u"void"_s;
+ compiledSetter.name = QmltcPropertyData(property).write;
+ compiledSetter.parameterList.emplaceBack(
+ QQmlJSUtils::constRefify(getUnderlyingType(property)), name + u"_", QString()
+ );
+
+ if (QQmlJSUtils::bindablePropertyHasDefaultAccessor(property, QQmlJSUtils::PropertyAccessor_Write)) {
+ compiledSetter.body << u"%1.%2().setValue(%3_);"_s.arg(
+ current.propertyInitializer.component.name, property.bindable(), name);
+ } else if (property.write().isEmpty() || isFromExtension(property, type)) {
+ // We can end here if a WRITE method is not available or
+ // if the method is available but not in this scope, so
+ // that we fallback to the string-based setters..
+ //
+ // For example, types that makes use of QML_EXTENDED
+ // types, will have the extension types properties
+ // available and with a WRITE method, but the WRITE method
+ // will not be available to the extended type, from C++,
+ // as the type does not directly inherit from the
+ // extension type.
+ //
+ // We specifically scope `setProperty` to `QObject` as
+ // certain types might have shadowed the method.
+ // For example, in QtQuick, some types have a property
+ // called `property` with a `setProperty` WRITE method
+ // that will produce the shadowing.
+ compiledSetter.body << u"%1.QObject::setProperty(\"%2\", QVariant::fromValue(%2_));"_s.arg(
+ current.propertyInitializer.component.name, name);
+ } else {
+ compiledSetter.body << u"%1.%2(%3_);"_s.arg(
+ current.propertyInitializer.component.name, property.write(), name);
+ }
+
+ compiledSetter.body << u"%1.insert(\"%2\");"_s.arg(
+ current.propertyInitializer.initializedCache.name, name);
+ }
+}
+
void QmltcCompiler::compileTypeElements(QmltcType &current, const QQmlJSScope::ConstPtr &type)
{
// compile components of a type:
@@ -404,6 +501,7 @@ void QmltcCompiler::compileTypeElements(QmltcType &current, const QQmlJSScope::C
auto bindings = type->ownPropertyBindingsInQmlIROrder();
partitionBindings(bindings.begin(), bindings.end());
+ compilePropertyInitializer(current, type);
compileBinding(current, bindings.begin(), bindings.end(), type, { type });
}
@@ -1777,9 +1875,12 @@ void QmltcCompiler::compileScriptBinding(QmltcType &current,
current.children << compileScriptBindingPropertyChangeHandler(
binding, objectType, m_urlMethodName, bindingFunctorName, objectClassName);
+ current.setComplexBindings.body << u"if (!%1.contains(\"%2\"))"_s.arg(
+ current.propertyInitializer.initializedCache.name, propertyName);
+
// TODO: this could be dropped if QQmlEngine::setContextForObject() is
// done before currently generated C++ object is constructed
- current.setComplexBindings.body << bindingSymbolName + u".reset(new QPropertyChangeHandler<"
+ current.setComplexBindings.body << u" "_s + bindingSymbolName + u".reset(new QPropertyChangeHandler<"
+ bindingFunctorName + u">("
+ QmltcCodeGenerator::wrap_privateClass(accessor.name, *actualProperty)
+ u"->" + bindableString + u"().onValueChanged(" + bindingFunctorName + u"("
diff --git a/tools/qmltc/qmltccompilerpieces.cpp b/tools/qmltc/qmltccompilerpieces.cpp
index 4c6c93eaa2..bdb71cff4c 100644
--- a/tools/qmltc/qmltccompilerpieces.cpp
+++ b/tools/qmltc/qmltccompilerpieces.cpp
@@ -237,7 +237,8 @@ void QmltcCodeGenerator::generate_createBindingOnProperty(
}
*block += prologue;
- *block << value + u"->" + bindable + u"().setBinding(" + createBindingForBindable + u");";
+ *block << u"if (!initializedCache.contains(\"%1\"))"_s.arg(p.propertyName());
+ *block << u" "_s + value + u"->" + bindable + u"().setBinding(" + createBindingForBindable + u");";
*block += epilogue;
} else {
QString createBindingForNonBindable =
diff --git a/tools/qmltc/qmltccompilerpieces.h b/tools/qmltc/qmltccompilerpieces.h
index 06708b2165..3252f19e86 100644
--- a/tools/qmltc/qmltccompilerpieces.h
+++ b/tools/qmltc/qmltccompilerpieces.h
@@ -152,14 +152,17 @@ struct QmltcCodeGenerator
/*!
\internal
- Generates \a{current.init}'s code. The init method sets up a QQmlContext for
- the object and (in case \a type is a document root) calls other object
- creation methods in a well-defined order:
+ Generates \a{current.init}'s code. The init method sets up a
+ QQmlContext for the object and (in case \a type is a document
+ root) calls other object creation methods, and a user-provided
+ initialization callback, in a well-defined order:
1. current.beginClass
2. current.endInit
- 3. current.completeComponent
- 4. current.finalizeComponent
- 5. current.handleOnCompleted
+ 3. user-provided initialization function
+ 4. current.setComplexBindings
+ 5. current.completeComponent
+ 6. current.finalizeComponent
+ 7. current.handleOnCompleted
This function returns a QScopeGuard with the final instructions that have to
be generated at a later point, once everything else is compiled.
@@ -294,8 +297,14 @@ inline decltype(auto) QmltcCodeGenerator::generate_initCode(QmltcType &current,
.arg(current.beginClass.name);
current.init.body << QStringLiteral(" %1(creator, engine);")
.arg(current.endInit.name);
- current.init.body << QStringLiteral(" %1(creator, engine);")
- .arg(current.setComplexBindings.name);
+
+ current.init.body << QStringLiteral(" {");
+ current.init.body << QStringLiteral(" PropertyInitializer propertyInitializer(*this);");
+ current.init.body << QStringLiteral(" initializer(propertyInitializer);");
+ current.init.body << QStringLiteral(" %1(creator, engine, propertyInitializer.initializedCache);").arg(current.setComplexBindings.name);
+ current.init.body << QStringLiteral(" }");
+
+
current.init.body << QStringLiteral(" %1(creator, /* finalize */ true);")
.arg(current.completeComponent.name);
current.init.body << QStringLiteral(" %1(creator, /* finalize */ true);")
diff --git a/tools/qmltc/qmltcoutputir.h b/tools/qmltc/qmltcoutputir.h
index bed5a2037f..eb608f8749 100644
--- a/tools/qmltc/qmltcoutputir.h
+++ b/tools/qmltc/qmltcoutputir.h
@@ -92,6 +92,29 @@ struct QmltcDtor : QmltcMethodBase
{
};
+// Represents a generated class that knows how to set the public,
+// writable properties of a compiled QML -> C++ type.
+// This is generally intended to be available for the root of the
+// document to allow the user to set the initial values for
+// properties, when creating a component, with support for strong
+// typing.
+struct QmltcPropertyInitializer {
+ QString name;
+
+ QmltcCtor constructor;
+
+ // A member containing a reference to the object for which the
+ // properties should be set.
+ QmltcVariable component;
+
+ // A member containing a cache of properties that were actually
+ // set that can be referenced later..
+ QmltcVariable initializedCache;
+
+ // Setter methods for each property.
+ QList<QmltcMethod> propertySetters;
+};
+
// Represents QML -> C++ compiled type
struct QmltcType
{
@@ -131,6 +154,10 @@ struct QmltcType
// needed for singletons
std::optional<QmltcMethod> staticCreate{};
+
+ // A proxy class that provides a restricted interface that only
+ // allows setting the properties of the type.
+ QmltcPropertyInitializer propertyInitializer{};
};
// Represents whole QML program, compiled to C++