diff options
author | Marco Bubke <marco.bubke@qt.io> | 2023-08-23 23:45:52 +0200 |
---|---|---|
committer | Marco Bubke <marco.bubke@qt.io> | 2023-10-02 20:22:10 +0000 |
commit | 1177bb4b1f3f913006d5ff0f2f05dfef2dc3a1de (patch) | |
tree | 64eea90c2a96037a9c13d3094d653397ebb1173f | |
parent | 3a0eff9e7959c53a0d068497ce645df387dab93b (diff) |
QmlDesigner: Add PropertyComponentGenerator
Task-number: QDS-10578
Change-Id: I2c20f5aaa6ebfe7736012a02d4c4596589007866
Reviewed-by: Qt CI Patch Build Bot <ci_patchbuild_bot@qt.io>
Reviewed-by: Thomas Hartmann <thomas.hartmann@qt.io>
Reviewed-by: <github-actions-qt-creator@cristianadam.eu>
27 files changed, 1689 insertions, 15 deletions
diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/PropertyTemplates/TemplateTypes.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/PropertyTemplates/TemplateTypes.qml index a6ee7eb476..03e42a974f 100644 --- a/share/qtcreator/qmldesigner/propertyEditorQmlSources/PropertyTemplates/TemplateTypes.qml +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/PropertyTemplates/TemplateTypes.qml @@ -9,72 +9,93 @@ AutoTypes { Type { typeNames: ["int"] + module: "QML" sourceFile: "IntEditorTemplate.template" } Type { typeNames: ["real", "double", "qreal"] + module: "QML" sourceFile: "RealEditorTemplate.template" } Type { typeNames: ["string", "QString"] + module: "QML" sourceFile: "StringEditorTemplate.template" } Type { - typeNames: ["QUrl", "url"] + typeNames: ["url", "QUrl"] + module: "QML" sourceFile: "UrlEditorTemplate.template" } Type { typeNames: ["bool", "boolean"] + module: "QML" sourceFile: "BooleanEditorTemplate.template" } Type { typeNames: ["color", "QColor"] + module: "QtQuick" sourceFile: "ColorEditorTemplate.template" } Type { typeNames: ["Text"] + module: "QtQuick" sourceFile: "TextEditorTemplate.template" separateSection: true } Type { typeNames: ["font", "QFont"] + module: "QtQuick" sourceFile: "FontEditorTemplate.template" separateSection: true } Type { typeNames: ["Rectangle"] + module: "QtQuick" sourceFile: "RectangleEditorTemplate.template" separateSection: true } Type { typeNames: ["Image"] + module: "QtQuick" sourceFile: "ImageEditorTemplate.template" separateSection: true } Type { - typeNames: ["TextureInput", "Texture"] + typeNames: ["TextureInput"] + module: "QtQuick3D" + sourceFile: "3DItemFilterComboBoxEditorTemplate.template" + needsTypeArg: true + } + + Type { + typeNames: ["Texture"] + module: "QtQuick3D" sourceFile: "3DItemFilterComboBoxEditorTemplate.template" needsTypeArg: true } Type { typeNames: ["vector2d"] + module: "QtQuick3D" sourceFile: "Vector2dEditorTemplate.template" } Type { typeNames: ["vector3d"] + module: "QtQuick3D" sourceFile: "Vector3dEditorTemplate.template" } Type { typeNames: ["vector4d"] + module: "QtQuick3D" sourceFile: "Vector4dEditorTemplate.template" } } diff --git a/src/libs/qmljs/qmljssimplereader.cpp b/src/libs/qmljs/qmljssimplereader.cpp index 761131b3a2..d4b8a21bf7 100644 --- a/src/libs/qmljs/qmljssimplereader.cpp +++ b/src/libs/qmljs/qmljssimplereader.cpp @@ -89,7 +89,10 @@ static Q_LOGGING_CATEGORY(simpleReaderLog, "qtc.qmljs.simpleReader", QtWarningMs return newNode; } - const SimpleReaderNode::List SimpleReaderNode::children() const { return m_children; } + const SimpleReaderNode::List &SimpleReaderNode::children() const + { + return m_children; + } void SimpleReaderNode::setProperty(const QString &name, const SourceLocation &nameLocation, diff --git a/src/libs/qmljs/qmljssimplereader.h b/src/libs/qmljs/qmljssimplereader.h index de57bb62da..e14f49d65d 100644 --- a/src/libs/qmljs/qmljssimplereader.h +++ b/src/libs/qmljs/qmljssimplereader.h @@ -34,6 +34,7 @@ public: SourceLocation valueLocation; bool isValid() const { return !value.isNull() && value.isValid(); } + explicit operator bool() const { return isValid(); } bool isDefaultValue() const { return !value.isNull() && !nameLocation.isValid() && !valueLocation.isValid(); @@ -53,7 +54,7 @@ public: WeakPtr parent() const; QString name() const; SourceLocation nameLocation() const; - const List children() const; + const List &children() const; protected: SimpleReaderNode(); diff --git a/src/plugins/qmldesigner/CMakeLists.txt b/src/plugins/qmldesigner/CMakeLists.txt index 8ea79bcdcf..6df5a63247 100644 --- a/src/plugins/qmldesigner/CMakeLists.txt +++ b/src/plugins/qmldesigner/CMakeLists.txt @@ -238,6 +238,7 @@ extend_qtc_library(QmlDesignerCore modelmerger.h modelnode.h modelnodepositionstorage.h + module.h nodeabstractproperty.h nodeinstance.h nodelistproperty.h diff --git a/src/plugins/qmldesigner/components/componentcore/propertycomponentgenerator.cpp b/src/plugins/qmldesigner/components/componentcore/propertycomponentgenerator.cpp new file mode 100644 index 0000000000..7e5487909a --- /dev/null +++ b/src/plugins/qmldesigner/components/componentcore/propertycomponentgenerator.cpp @@ -0,0 +1,272 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "propertycomponentgenerator.h" + +#include <utils/algorithm.h> +#include <utils/environment.h> + +#include <model.h> + +#include <QFile> +#include <QString> + +#include <memory> + +using namespace Qt::StringLiterals; + +namespace QmlDesigner { + +namespace { + +QmlJS::SimpleReaderNode::Ptr createTemplateConfiguration(const QString &propertyEditorResourcesPath) +{ + QmlJS::SimpleReader reader; + const QString fileName = propertyEditorResourcesPath + u"/PropertyTemplates/TemplateTypes.qml"; + auto templateConfiguration = reader.readFile(fileName); + + if (!templateConfiguration) + qWarning().nospace() << "template definitions:" << reader.errors(); + + return templateConfiguration; +} + +template<typename Type> +Type getProperty(const QmlJS::SimpleReaderNode *node, const QString &name) +{ + if (auto property = node->property(name)) { + const auto &value = property.value; + if (value.type() == QVariant::List) { + auto list = value.toList(); + if (list.size()) + return list.front().value<Type>(); + } else { + return property.value.value<Type>(); + } + } + + return {}; +} + +QString getContent(const QString &path) +{ + QFile file{path}; + + if (file.open(QIODevice::ReadOnly)) + return QString::fromUtf8(file.readAll()); + + return {}; +} + +QString generateComponentText(Utils::SmallStringView propertyName, + QStringView source, + Utils::SmallStringView typeName, + bool needsTypeArg) +{ + QString underscoreName{propertyName}; + underscoreName.replace('.', '_'); + if (needsTypeArg) + return source.arg(QString{propertyName}, underscoreName, QString{typeName}); + + return source.arg(QString{propertyName}, underscoreName); +} + +PropertyComponentGenerator::Property generate(const PropertyMetaInfo &property, + const PropertyComponentGenerator::Entry &entry) +{ + auto propertyName = property.name(); + auto component = generateComponentText(propertyName, + entry.propertyTemplate, + entry.typeName, + entry.needsTypeArg); + + if (entry.separateSection) + return PropertyComponentGenerator::ComplexProperty{propertyName, component}; + + return PropertyComponentGenerator::BasicProperty{propertyName, component}; +} + +auto getRandomExportedName(const NodeMetaInfo &metaInfo) +{ + const auto &names = metaInfo.allExportedTypeNames(); + + using Result = decltype(names.front().name); + + if (!names.empty()) + return names.front().name; + + return Result{}; +} + +} // namespace + +QString PropertyComponentGenerator::generateSubComponentText(Utils::SmallStringView propertyBaseName, + const PropertyMetaInfo &subProperty) const +{ + auto propertyType = subProperty.propertyType(); + if (auto entry = findEntry(propertyType)) { + auto propertyName = Utils::SmallString::join({propertyBaseName, subProperty.name()}); + return generateComponentText(propertyName, + entry->propertyTemplate, + entry->typeName, + entry->needsTypeArg); + } + + return {}; +} + +QString PropertyComponentGenerator::generateComplexComponentText(Utils::SmallStringView propertyName, + const NodeMetaInfo &propertyType) const +{ + auto subProperties = propertyType.properties(); + + if (std::empty(subProperties)) + return {}; + + auto propertyTypeName = getRandomExportedName(propertyType); + static QString templateText = QStringLiteral( + R"xy( + Section { + caption: %1 - %2 + anchors.left: parent.left + anchors.right: parent.right + leftPadding: 8 + rightPadding: 0 + expanded: false + level: 1 + SectionLayout { + )xy"); + + auto component = templateText.arg(QString{propertyName}, QString{propertyTypeName}); + + auto propertyBaseName = Utils::SmallString::join({propertyName, "."}); + + bool subPropertiesHaveContent = false; + for (const auto &subProperty : propertyType.properties()) { + auto subPropertyContent = generateSubComponentText(propertyBaseName, subProperty); + subPropertiesHaveContent = subPropertiesHaveContent || subPropertyContent.size(); + component += subPropertyContent; + } + + component += "}\n}\n"_L1; + + if (subPropertiesHaveContent) + return component; + + return {}; +} + +PropertyComponentGenerator::Property PropertyComponentGenerator::generateComplexComponent( + const PropertyMetaInfo &property, const NodeMetaInfo &propertyType) const +{ + auto propertyName = property.name(); + auto component = generateComplexComponentText(propertyName, propertyType); + + if (component.isEmpty()) + return {}; + + return ComplexProperty{propertyName, component}; +} + +namespace { + +std::optional<PropertyComponentGenerator::Entry> createEntry(QmlJS::SimpleReaderNode *node, + Model *model, + const QString &templatesPath) +{ + auto moduleName = getProperty<QByteArray>(node, "module"); + if (moduleName.isEmpty()) + return {}; + + auto module = model->module(moduleName); + + auto typeName = getProperty<QByteArray>(node, "typeNames"); + + auto type = model->metaInfo(module, typeName); + if (!type) + return {}; + + auto path = getProperty<QString>(node, "sourceFile"); + if (path.isEmpty()) + return {}; + + auto content = getContent(templatesPath + path); + if (content.isEmpty()) + return {}; + + bool needsTypeArg = getProperty<bool>(node, "needsTypeArg"); + + bool separateSection = getProperty<bool>(node, "separateSection"); + + return PropertyComponentGenerator::Entry{std::move(type), + std::move(typeName), + std::move(content), + separateSection, + needsTypeArg}; +} + +PropertyComponentGenerator::Entries createEntries(QmlJS::SimpleReaderNode::Ptr templateConfiguration, + Model *model, + const QString &templatesPath) +{ + PropertyComponentGenerator::Entries entries; + entries.reserve(32); + + const auto &nodes = templateConfiguration->children(); + for (const QmlJS::SimpleReaderNode::Ptr &node : nodes) { + if (auto entry = createEntry(node.get(), model, templatesPath)) + entries.push_back(*entry); + } + + return entries; +} + +QStringList createImports(QmlJS::SimpleReaderNode::Ptr templateConfiguration) +{ + auto property = templateConfiguration->property("imports"); + return Utils::transform<QStringList>(property.value.toList(), + [](const auto &entry) { return entry.toString(); }); +} + +} // namespace + +PropertyComponentGenerator::PropertyComponentGenerator(const QString &propertyEditorResourcesPath, + Model *model) + : m_entries(createEntries(createTemplateConfiguration(propertyEditorResourcesPath), + model, + propertyEditorResourcesPath + "/PropertyTemplates/")) +{ + auto templateConfiguration = createTemplateConfiguration(propertyEditorResourcesPath); + + m_entries = createEntries(templateConfiguration, + model, + propertyEditorResourcesPath + "/PropertyTemplates/"); + + m_imports = createImports(templateConfiguration); +} + +PropertyComponentGenerator::Property PropertyComponentGenerator::create(const PropertyMetaInfo &property) const +{ + auto propertyType = property.propertyType(); + if (auto entry = findEntry(propertyType)) + return generate(property, *entry); + + if (property.isWritable() && property.isPointer()) + return {}; + + return generateComplexComponent(property, propertyType); +} + +const PropertyComponentGenerator::Entry *PropertyComponentGenerator::findEntry(const NodeMetaInfo &type) const +{ + auto found = std::find_if(m_entries.begin(), m_entries.end(), [&](const auto &entry) { + return entry.type == type; + }); + + if (found != m_entries.end()) + return std::addressof(*found); + + return nullptr; +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/componentcore/propertycomponentgenerator.h b/src/plugins/qmldesigner/components/componentcore/propertycomponentgenerator.h new file mode 100644 index 0000000000..67239c16a7 --- /dev/null +++ b/src/plugins/qmldesigner/components/componentcore/propertycomponentgenerator.h @@ -0,0 +1,53 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "propertycomponentgeneratorinterface.h" + +#include <modelfwd.h> +#include <nodemetainfo.h> + +#include <qmljs/qmljssimplereader.h> + +#include <optional> +#include <variant> +#include <vector> + +namespace QmlDesigner { + +class PropertyComponentGenerator final : public PropertyComponentGeneratorInterface +{ +public: + PropertyComponentGenerator(const QString &propertyEditorResourcesPath, Model *model); + + Property create(const PropertyMetaInfo &property) const override; + + struct Entry + { + NodeMetaInfo type; + Utils::SmallString typeName; + QString propertyTemplate; + bool separateSection = false; + bool needsTypeArg = false; + }; + + using Entries = std::vector<Entry>; + + QStringList imports() const override { return m_imports; } + +private: + const Entry *findEntry(const NodeMetaInfo &type) const; + QString generateSubComponentText(Utils::SmallStringView propertyBaseName, + const PropertyMetaInfo &subProperty) const; + QString generateComplexComponentText(Utils::SmallStringView propertyName, + const NodeMetaInfo &propertyType) const; + Property generateComplexComponent(const PropertyMetaInfo &property, + const NodeMetaInfo &propertyType) const; + +private: + Entries m_entries; + QStringList m_imports; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/componentcore/propertycomponentgeneratorinterface.h b/src/plugins/qmldesigner/components/componentcore/propertycomponentgeneratorinterface.h new file mode 100644 index 0000000000..ab4047c957 --- /dev/null +++ b/src/plugins/qmldesigner/components/componentcore/propertycomponentgeneratorinterface.h @@ -0,0 +1,46 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include <modelfwd.h> +#include <nodemetainfo.h> + +#include <qmljs/qmljssimplereader.h> + +#include <optional> +#include <variant> +#include <vector> + +namespace QmlDesigner { + +class PropertyComponentGeneratorInterface +{ +public: + PropertyComponentGeneratorInterface() = default; + PropertyComponentGeneratorInterface(const PropertyComponentGeneratorInterface &) = delete; + PropertyComponentGeneratorInterface &operator=(const PropertyComponentGeneratorInterface &) = delete; + + struct BasicProperty + { + Utils::SmallString propertyName; + QString component; + }; + + struct ComplexProperty + { + Utils::SmallString propertyName; + QString component; + }; + + using Property = std::variant<std::monostate, BasicProperty, ComplexProperty>; + + virtual Property create(const PropertyMetaInfo &property) const = 0; + + virtual QStringList imports() const = 0; + +protected: + ~PropertyComponentGeneratorInterface() = default; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/componentcore/propertyeditorcomponentgenerator.cpp b/src/plugins/qmldesigner/components/componentcore/propertyeditorcomponentgenerator.cpp new file mode 100644 index 0000000000..272130b8f3 --- /dev/null +++ b/src/plugins/qmldesigner/components/componentcore/propertyeditorcomponentgenerator.cpp @@ -0,0 +1,177 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "propertyeditorcomponentgenerator.h" + +#include <utils/environment.h> +#include <utils/span.h> + +#include <modelnode.h> + +namespace QmlDesigner { + +namespace { + +using GeneratorProperty = PropertyComponentGeneratorInterface::Property; +using BasicProperty = PropertyComponentGeneratorInterface::BasicProperty; +using ComplexProperty = PropertyComponentGeneratorInterface::ComplexProperty; +using GeneratorProperties = std::vector<GeneratorProperty>; + +QString createImports(const QStringList &imports) +{ + QString importsContent; + importsContent.reserve(512); + + for (const QString &import : imports) { + importsContent += import; + importsContent += '\n'; + } + + return importsContent; +} + +QString componentButton(bool isComponent) +{ + if (isComponent) + return "ComponentButton {}"; + + return {}; +} + +void createBasicPropertySections(QString &components, + Utils::span<GeneratorProperty> generatorProperties) +{ + components += R"xy( + Column { + width: parent.width + leftPadding: 8 + bottomPadding: 10 + SectionLayout {)xy"; + + for (const auto &generatorProperty : generatorProperties) + components += std::get_if<BasicProperty>(&generatorProperty)->component; + + components += R"xy( + } + })xy"; +} + +void createComplexPropertySections(QString &components, + Utils::span<GeneratorProperty> generatorProperties) +{ + for (const auto &generatorProperty : generatorProperties) + components += std::get_if<ComplexProperty>(&generatorProperty)->component; +} + +Utils::SmallStringView propertyName(const GeneratorProperty &property) +{ + return std::visit( + [](const auto &p) -> Utils::SmallStringView { + if constexpr (!std::is_same_v<std::decay_t<decltype(p)>, std::monostate>) + return p.propertyName; + else + return {}; + }, + property); +} + +PropertyMetaInfos getUnmangedProperties(const NodeMetaInfo &nodeInfo) +{ + PropertyMetaInfos properties; + properties.reserve(128); + + auto prototypes = nodeInfo.selfAndPrototypes(); + + for (const auto &prototype : prototypes) { + if (prototype.propertyEditorPathId()) + break; + + auto localProperties = prototype.localProperties(); + + properties.insert(properties.end(), localProperties.begin(), localProperties.end()); + } + + return properties; +} + +GeneratorProperties createSortedGeneratorProperties( + const PropertyMetaInfos &properties, const PropertyComponentGeneratorType &propertyGenerator) +{ + GeneratorProperties generatorProperties; + generatorProperties.reserve(properties.size()); + + for (const auto &property : properties) { + auto generatedProperty = propertyGenerator.create(property); + if (!std::holds_alternative<std::monostate>(generatedProperty)) + generatorProperties.push_back(std::move(generatedProperty)); + } + std::sort(generatorProperties.begin(), + generatorProperties.end(), + [](const auto &first, const auto &second) { + // this is sensitive to the order of the variant types but the test should catch any error + return std::forward_as_tuple(first.index(), propertyName(first)) + < std::forward_as_tuple(second.index(), propertyName(second)); + }); + + return generatorProperties; +} + +QString createPropertySections(const PropertyComponentGeneratorType &propertyGenerator, + const NodeMetaInfo &nodeInfo) +{ + QString propertyComponents; + propertyComponents.reserve(100000); + + auto generatorProperties = createSortedGeneratorProperties(getUnmangedProperties(nodeInfo), + propertyGenerator); + + const auto begin = generatorProperties.begin(); + const auto end = generatorProperties.end(); + + auto point = std::partition_point(begin, end, [](const auto &p) { + return !std::holds_alternative<ComplexProperty>(p); + }); + + if (begin != point) + createBasicPropertySections(propertyComponents, Utils::span<GeneratorProperty>{begin, point}); + + if (point != end) + createComplexPropertySections(propertyComponents, Utils::span<GeneratorProperty>{point, end}); + + return propertyComponents; +} + +} // namespace + +PropertyEditorTemplateGenerator::PropertyEditorTemplateGenerator( + const PropertyComponentGeneratorType &propertyGenerator) + : m_propertyGenerator{propertyGenerator} +{} + +QString PropertyEditorTemplateGenerator::create(const NodeMetaInfo &nodeInfo, bool isComponent) +{ + return QString{R"xy( + %1 + Column { + width: parent.width + %2 + Section { + caption: "%3" + anchors.left: parent.left + anchors.right: parent.right + leftPadding: 0 + rightPadding: 0 + bottomPadding: 0 + Column { + width: parent.width + %4 + } + } + })xy"} + .arg(createImports(m_propertyGenerator.imports()), + componentButton(isComponent), + QObject::tr("Exposed Custom Properties"), + createPropertySections(m_propertyGenerator, nodeInfo)); +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/componentcore/propertyeditorcomponentgenerator.h b/src/plugins/qmldesigner/components/componentcore/propertyeditorcomponentgenerator.h new file mode 100644 index 0000000000..37dd824d43 --- /dev/null +++ b/src/plugins/qmldesigner/components/componentcore/propertyeditorcomponentgenerator.h @@ -0,0 +1,29 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "propertycomponentgenerator.h" + +#include <nodemetainfo.h> + +namespace QmlDesigner { + +#ifdef UNIT_TESTS +using PropertyComponentGeneratorType = PropertyComponentGeneratorInterface; +#else +using PropertyComponentGeneratorType = PropertyComponentGenerator; +#endif +class PropertyEditorTemplateGenerator +{ + +public: + PropertyEditorTemplateGenerator(const PropertyComponentGeneratorType &propertyGenerator); + + QString create(const NodeMetaInfo &nodeInfo, bool isComponent); + +private: + const PropertyComponentGeneratorType &m_propertyGenerator; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/designercore/include/model.h b/src/plugins/qmldesigner/designercore/include/model.h index c404525a9c..ac8182c076 100644 --- a/src/plugins/qmldesigner/designercore/include/model.h +++ b/src/plugins/qmldesigner/designercore/include/model.h @@ -7,7 +7,9 @@ #include <documentmessage.h> #include <model/modelresourcemanagementinterface.h> +#include <module.h> #include <projectstorage/projectstoragefwd.h> +#include <projectstorage/projectstorageinfotypes.h> #include <projectstorageids.h> #include <QMimeData> @@ -125,7 +127,11 @@ public: const MetaInfo metaInfo() const; MetaInfo metaInfo(); + Module module(Utils::SmallStringView moduleName); NodeMetaInfo metaInfo(const TypeName &typeName, int majorVersion = -1, int minorVersion = -1) const; + NodeMetaInfo metaInfo(Module module, + Utils::SmallStringView typeName, + Storage::Version version = Storage::Version{}) const; bool hasNodeMetaInfo(const TypeName &typeName, int majorVersion = -1, int minorVersion = -1) const; void setMetaInfo(const MetaInfo &metaInfo); diff --git a/src/plugins/qmldesigner/designercore/include/module.h b/src/plugins/qmldesigner/designercore/include/module.h new file mode 100644 index 0000000000..5078f3ff8f --- /dev/null +++ b/src/plugins/qmldesigner/designercore/include/module.h @@ -0,0 +1,35 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include <modelfwd.h> +#include <projectstorageids.h> + +namespace QmlDesigner { + +class Module +{ +public: + Module() = default; + + Module(ModuleId moduleId, NotNullPointer<const ProjectStorageType> projectStorage) + : m_projectStorage{projectStorage} + , m_id{moduleId} + {} + + ModuleId id() const { return m_id; } + + bool isValid() const { return bool(m_id); } + + explicit operator bool() const { return isValid(); } + + const ProjectStorageType &projectStorage() const { return *m_projectStorage; } + + friend bool operator==(Module first, Module second) { return first.m_id == second.m_id; } + +private: + NotNullPointer<const ProjectStorageType> m_projectStorage = {}; + ModuleId m_id; +}; +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/designercore/model/model.cpp b/src/plugins/qmldesigner/designercore/model/model.cpp index 74afaaad86..c0393d524c 100644 --- a/src/plugins/qmldesigner/designercore/model/model.cpp +++ b/src/plugins/qmldesigner/designercore/model/model.cpp @@ -2436,6 +2436,16 @@ NodeMetaInfo Model::metaInfo(const TypeName &typeName, int majorVersion, int min } } +NodeMetaInfo Model::metaInfo(Module module, Utils::SmallStringView typeName, Storage::Version version) const +{ + if constexpr (useProjectStorage()) { + return NodeMetaInfo(d->projectStorage->typeId(module.id(), typeName, version), + d->projectStorage); + } else { + return {}; + } +} + /*! \brief Returns list of QML types available within the model. */ @@ -2444,6 +2454,15 @@ MetaInfo Model::metaInfo() return d->metaInfo(); } +Module Model::module(Utils::SmallStringView moduleName) +{ + if constexpr (useProjectStorage()) { + return Module(d->projectStorage->moduleId(moduleName), d->projectStorage); + } + + return {}; +} + /*! \name View related functions */ //\{ diff --git a/tests/unit/tests/matchers/CMakeLists.txt b/tests/unit/tests/matchers/CMakeLists.txt index f050cb83c8..32a7dad99c 100644 --- a/tests/unit/tests/matchers/CMakeLists.txt +++ b/tests/unit/tests/matchers/CMakeLists.txt @@ -7,6 +7,7 @@ add_qtc_library(TestMatchers OBJECT SOURCES info_exportedtypenames-matcher.h import-matcher.h + strippedstring-matcher.h unittest-matchers.h version-matcher.h qvariant-matcher.h diff --git a/tests/unit/tests/matchers/strippedstring-matcher.h b/tests/unit/tests/matchers/strippedstring-matcher.h new file mode 100644 index 0000000000..3c491cba8f --- /dev/null +++ b/tests/unit/tests/matchers/strippedstring-matcher.h @@ -0,0 +1,81 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "version-matcher.h" + +#include <projectstorage/projectstoragetypes.h> + +#include <QRegularExpression> + +#include <algorithm> + +namespace Internal { +class StippedStringEqMatcher +{ +public: + explicit StippedStringEqMatcher(QString content) + : m_content(strip(std::move(content))) + {} + + bool MatchAndExplain(const QString &s, testing::MatchResultListener *listener) const + { + auto strippedContent = strip(s); + bool isEqual = m_content == strippedContent; + + if (!isEqual) { + auto [found1, found2] = std::mismatch(strippedContent.begin(), + strippedContent.end(), + m_content.begin(), + m_content.end()); + if (found1 != strippedContent.end() && found2 != m_content.end()) { + *listener << "\ncurrent value mismatch start: \n" + << QStringView{found1, strippedContent.end()}; + *listener << "\nexpected value mismatch start: \n" + << QStringView{found2, m_content.end()}; + } + } + + return isEqual; + } + + static QString strip(QString s) + { + s.replace('\n', ' '); + + auto newEnd = std::unique(s.begin(), s.end(), [](QChar first, QChar second) { + return first.isSpace() && second.isSpace(); + }); + + s.erase(newEnd, s.end()); + + static QRegularExpression regex1{R"xx((\W)\s)xx"}; + s.replace(regex1, R"xx(\1)xx"); + + static QRegularExpression regex2{R"xx(\s(\W))xx"}; + s.replace(regex2, R"xx(\1)xx"); + + return s.trimmed(); + } + + void DescribeTo(::std::ostream *os) const + { + *os << "has stripped content " << testing::PrintToString(m_content); + } + + void DescribeNegationTo(::std::ostream *os) const + { + *os << "doesn't have stripped content " << testing::PrintToString(m_content); + } + +private: + const QString m_content; +}; + +} // namespace Internal + +inline auto StrippedStringEq(const QStringView &content) +{ + return ::testing::PolymorphicMatcher(Internal::StippedStringEqMatcher(content.toString())); +} diff --git a/tests/unit/tests/mocks/CMakeLists.txt b/tests/unit/tests/mocks/CMakeLists.txt index 5323f09e40..bd34e15c68 100644 --- a/tests/unit/tests/mocks/CMakeLists.txt +++ b/tests/unit/tests/mocks/CMakeLists.txt @@ -2,6 +2,7 @@ add_qtc_library(TestMocks OBJECT EXCLUDE_FROM_INSTALL PROPERTIES SKIP_AUTOGEN ON PUBLIC_INCLUDES ${CMAKE_CURRENT_LIST_DIR} + INCLUDES ../../../../src/plugins/qmldesigner/components/componentcore DEPENDS Qt::Core Qt::Widgets Googletest Sqlite TestDesignerCore SOURCES @@ -20,6 +21,7 @@ add_qtc_library(TestMocks OBJECT mocktimer.h mocktimestampprovider.h modelresourcemanagementmock.h + propertycomponentgeneratormock.h projectstoragemock.cpp projectstoragemock.h projectstoragepathwatchermock.h diff --git a/tests/unit/tests/mocks/projectstoragemock.cpp b/tests/unit/tests/mocks/projectstoragemock.cpp index 105b8c7346..b2445cd82a 100644 --- a/tests/unit/tests/mocks/projectstoragemock.cpp +++ b/tests/unit/tests/mocks/projectstoragemock.cpp @@ -112,6 +112,16 @@ QmlDesigner::ImportId ProjectStorageMock::createImportId(QmlDesigner::ModuleId m return importId; } +void ProjectStorageMock::addExportedTypeName(QmlDesigner::TypeId typeId, + QmlDesigner::ModuleId moduleId, + Utils::SmallStringView typeName) +{ + ON_CALL(*this, typeId(Eq(moduleId), Eq(typeName), _)).WillByDefault(Return(typeId)); + ON_CALL(*this, fetchTypeIdByModuleIdAndExportedName(Eq(moduleId), Eq(typeName))) + .WillByDefault(Return(typeId)); + exportedTypeName[typeId].emplace_back(moduleId, typeName); +} + PropertyDeclarationId ProjectStorageMock::createProperty(TypeId typeId, Utils::SmallString name, PropertyDeclarationTraits traits, @@ -159,6 +169,12 @@ void ProjectStorageMock::createFunction(QmlDesigner::TypeId typeId, Utils::Small ON_CALL(*this, functionDeclarationNames(Eq(typeId))).WillByDefault(Return(functionNames)); } +void ProjectStorageMock::setPropertyEditorPathId(QmlDesigner::TypeId typeId, + QmlDesigner::SourceId sourceId) +{ + ON_CALL(*this, propertyEditorPathId(Eq(typeId))).WillByDefault(Return(sourceId)); +} + namespace { void addBaseProperties(TypeId typeId, TypeIds baseTypeIds, ProjectStorageMock &storage) { @@ -192,6 +208,10 @@ TypeId ProjectStorageMock::createType(ModuleId moduleId, ON_CALL(*this, typeId(Eq(moduleId), Eq(typeName), _)).WillByDefault(Return(typeId)); ON_CALL(*this, fetchTypeIdByModuleIdAndExportedName(Eq(moduleId), Eq(typeName))) .WillByDefault(Return(typeId)); + addExportedTypeName(typeId, moduleId, typeName); + ON_CALL(*this, exportedTypeNames(Eq(typeId))).WillByDefault([&](TypeId id) { + return exportedTypeName[id]; + }); PropertyDeclarationId defaultPropertyDeclarationId; if (defaultPropertyName.size()) { if (!defaultPropertyTypeId) { @@ -280,6 +300,7 @@ void ProjectStorageMock::setupQtQuick() auto intId = createValue(qmlModuleId, "int"); createValue(qmlNativeModuleId, "uint"); auto doubleId = createValue(qmlModuleId, "double"); + addExportedTypeName(doubleId, qmlModuleId, "real"); createValue(qmlNativeModuleId, "float"); createValue(qmlModuleId, "date"); auto stringId = createValue(qmlModuleId, "string"); @@ -307,7 +328,7 @@ void ProjectStorageMock::setupQtQuick() createProperty(fontValueTypeId, "overline", boolId); createProperty(fontValueTypeId, "pointSize", doubleId); createProperty(fontValueTypeId, "pixelSize", intId); - createValue(qtQuickModuleId, "font", {fontValueTypeId}); + auto fontId = createValue(qtQuickModuleId, "font", {fontValueTypeId}); auto itemId = createObject(qtQuickModuleId, "Item", @@ -315,6 +336,8 @@ void ProjectStorageMock::setupQtQuick() PropertyDeclarationTraits::IsList, qtObjectId, {qtObjectId}); + createProperty(itemId, "x", doubleId); + createProperty(itemId, "font", fontId); auto inputDeviceId = createObject(qtQuickModuleId, "InputDevice", {qtObjectId}); createProperty(inputDeviceId, "seatName", stringId); diff --git a/tests/unit/tests/mocks/projectstoragemock.h b/tests/unit/tests/mocks/projectstoragemock.h index 22e316ede3..5c189af519 100644 --- a/tests/unit/tests/mocks/projectstoragemock.h +++ b/tests/unit/tests/mocks/projectstoragemock.h @@ -9,6 +9,7 @@ #include <projectstorage/commontypecache.h> #include <projectstorage/filestatus.h> +#include <projectstorage/projectstorageinfotypes.h> #include <projectstorage/projectstorageinterface.h> #include <projectstorage/sourcepathcache.h> @@ -40,15 +41,18 @@ public: QmlDesigner::SourceId sourceId, QmlDesigner::Storage::Version version = QmlDesigner::Storage::Version{}); - QmlDesigner::TypeId createType( - QmlDesigner::ModuleId moduleId, - Utils::SmallStringView typeName, - Utils::SmallStringView defaultPropertyName, - QmlDesigner::Storage::PropertyDeclarationTraits defaultPropertyTraits, - QmlDesigner::TypeId defaultPropertyTypeId, - QmlDesigner::Storage::TypeTraits typeTraits, - QmlDesigner::TypeIds baseTypeIds = {}, - QmlDesigner::SourceId sourceId = QmlDesigner::SourceId{}); + void addExportedTypeName(QmlDesigner::TypeId typeId, + QmlDesigner::ModuleId moduleId, + Utils::SmallStringView typeName); + + QmlDesigner::TypeId createType(QmlDesigner::ModuleId moduleId, + Utils::SmallStringView typeName, + Utils::SmallStringView defaultPropertyName, + QmlDesigner::Storage::PropertyDeclarationTraits defaultPropertyTraits, + QmlDesigner::TypeId defaultPropertyTypeId, + QmlDesigner::Storage::TypeTraits typeTraits, + QmlDesigner::TypeIds baseTypeIds = {}, + QmlDesigner::SourceId sourceId = QmlDesigner::SourceId{}); QmlDesigner::TypeId createType(QmlDesigner::ModuleId moduleId, Utils::SmallStringView typeName, @@ -85,6 +89,7 @@ public: void createSignal(QmlDesigner::TypeId typeId, Utils::SmallString name); void createFunction(QmlDesigner::TypeId typeId, Utils::SmallString name); + void setPropertyEditorPathId(QmlDesigner::TypeId typeId, QmlDesigner::SourceId sourceId); MOCK_METHOD(void, synchronize, @@ -280,6 +285,7 @@ public: (const, override)); QmlDesigner::Storage::Info::CommonTypeCache<QmlDesigner::ProjectStorageInterface> typeCache{*this}; + std::map<QmlDesigner::TypeId, QmlDesigner::Storage::Info::ExportedTypeNames> exportedTypeName; }; class ProjectStorageMockWithQtQtuick : public ProjectStorageMock diff --git a/tests/unit/tests/mocks/propertycomponentgeneratormock.h b/tests/unit/tests/mocks/propertycomponentgeneratormock.h new file mode 100644 index 0000000000..043b64b464 --- /dev/null +++ b/tests/unit/tests/mocks/propertycomponentgeneratormock.h @@ -0,0 +1,22 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "../utils/googletest.h" + +#include <propertycomponentgeneratorinterface.h> + +class PropertyComponentGeneratorMock : public QmlDesigner::PropertyComponentGeneratorInterface +{ + using Property = QmlDesigner::PropertyComponentGeneratorInterface::Property; + +public: + virtual ~PropertyComponentGeneratorMock() = default; + MOCK_METHOD(Property, + create, + (const QmlDesigner::PropertyMetaInfo &), + + (const, override)); + MOCK_METHOD(QStringList, imports, (), (const, override)); +}; diff --git a/tests/unit/tests/printers/gtest-qt-printing.cpp b/tests/unit/tests/printers/gtest-qt-printing.cpp index 532aef525f..f432d87398 100644 --- a/tests/unit/tests/printers/gtest-qt-printing.cpp +++ b/tests/unit/tests/printers/gtest-qt-printing.cpp @@ -33,6 +33,11 @@ std::ostream &operator<<(std::ostream &out, const QString &text) return out << text.toUtf8(); } +std::ostream &operator<<(std::ostream &out, QStringView text) +{ + return out << text.toString(); +} + std::ostream &operator<<(std::ostream &out, const QVariant &variant) { QString output; diff --git a/tests/unit/tests/printers/gtest-qt-printing.h b/tests/unit/tests/printers/gtest-qt-printing.h index c84697c17b..ceec63817c 100644 --- a/tests/unit/tests/printers/gtest-qt-printing.h +++ b/tests/unit/tests/printers/gtest-qt-printing.h @@ -11,12 +11,14 @@ QT_BEGIN_NAMESPACE class QVariant; class QString; +class QStringView; class QTextCharFormat; class QImage; class QIcon; std::ostream &operator<<(std::ostream &out, const QVariant &QVariant); std::ostream &operator<<(std::ostream &out, const QString &text); +std::ostream &operator<<(std::ostream &out, QStringView text); std::ostream &operator<<(std::ostream &out, const QByteArray &byteArray); std::ostream &operator<<(std::ostream &out, const QTextCharFormat &format); std::ostream &operator<<(std::ostream &out, const QImage &image); diff --git a/tests/unit/tests/testdesignercore/CMakeLists.txt b/tests/unit/tests/testdesignercore/CMakeLists.txt index 9a10b6d744..0847caa323 100644 --- a/tests/unit/tests/testdesignercore/CMakeLists.txt +++ b/tests/unit/tests/testdesignercore/CMakeLists.txt @@ -65,6 +65,7 @@ add_qtc_library(TestDesignerCore OBJECT include/metainfo.h include/metainforeader.h include/modelnode.h + include/module.h include/nodeabstractproperty.h include/nodelistproperty.h include/nodemetainfo.h diff --git a/tests/unit/tests/unittests/CMakeLists.txt b/tests/unit/tests/unittests/CMakeLists.txt index 5de9ef141c..7088bedcc0 100644 --- a/tests/unit/tests/unittests/CMakeLists.txt +++ b/tests/unit/tests/unittests/CMakeLists.txt @@ -38,6 +38,7 @@ function(unittest_copy_data_folder) ) endfunction(unittest_copy_data_folder) +add_subdirectory(componentcore) add_subdirectory(listmodeleditor) add_subdirectory(imagecache) add_subdirectory(metainfo) diff --git a/tests/unit/tests/unittests/componentcore/CMakeLists.txt b/tests/unit/tests/unittests/componentcore/CMakeLists.txt new file mode 100644 index 0000000000..913fe21bea --- /dev/null +++ b/tests/unit/tests/unittests/componentcore/CMakeLists.txt @@ -0,0 +1,15 @@ +extend_qtc_test(unittest + SOURCES + propertycomponentgenerator-test.cpp + propertyeditorcomponentgenerator-test.cpp +) + +extend_qtc_test(unittest + SOURCES_PREFIX ../../../../../src/plugins/qmldesigner/components/componentcore + SOURCES + propertyeditorcomponentgenerator.cpp propertyeditorcomponentgenerator.h + propertycomponentgenerator.cpp propertycomponentgenerator.h + propertycomponentgeneratorinterface.h +) + + diff --git a/tests/unit/tests/unittests/componentcore/propertycomponentgenerator-test.cpp b/tests/unit/tests/unittests/componentcore/propertycomponentgenerator-test.cpp new file mode 100644 index 0000000000..353efb91f6 --- /dev/null +++ b/tests/unit/tests/unittests/componentcore/propertycomponentgenerator-test.cpp @@ -0,0 +1,322 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include <googletest.h> +#include <mocks/abstractviewmock.h> +#include <mocks/modelresourcemanagementmock.h> +#include <mocks/projectstoragemock.h> +#include <mocks/sourcepathcachemock.h> +#include <strippedstring-matcher.h> + +#include <propertycomponentgenerator.h> + +using namespace Qt::StringLiterals; + +namespace QmlDesigner { + +std::ostream &operator<<(std::ostream &out, const PropertyComponentGenerator::BasicProperty &property) +{ + return out << "BasicProperty(" << property.propertyName << ", " << property.component << ")"; +} + +std::ostream &operator<<(std::ostream &out, const PropertyComponentGenerator::ComplexProperty &property) +{ + return out << "ComplexPropertiesFromTemplates(" << property.propertyName << ", " + << property.component << ")"; +} + +} // namespace QmlDesigner + +namespace { + +using BasicProperty = QmlDesigner::PropertyComponentGenerator::BasicProperty; +using ComplexProperty = QmlDesigner::PropertyComponentGenerator::ComplexProperty; + +template<typename Matcher> +auto IsBasicProperty(const Matcher &matcher) +{ + return VariantWith<BasicProperty>(Field(&BasicProperty::component, matcher)); +} + +template<typename Matcher> +auto IsComplexProperty(const Matcher &matcher) +{ + return VariantWith<ComplexProperty>(Field(&ComplexProperty::component, matcher)); +} + +constexpr Utils::SmallStringView sourcesPath = UNITTEST_DIR + "/../../../../share/qtcreator/qmldesigner/propertyEditorQmlSources"; + +class PropertyComponentGenerator : public ::testing::Test +{ +protected: + static void SetUpTestSuite() + { + simpleReaderNode = createTemplateConfiguration(QString{sourcesPath}); + } + + static void TearDownTestSuite() { simpleReaderNode.reset(); } + + static QmlJS::SimpleReaderNode::Ptr createTemplateConfiguration(const QString &propertyEditorResourcesPath) + { + QmlJS::SimpleReader reader; + const QString fileName = propertyEditorResourcesPath + u"/PropertyTemplates/TemplateTypes.qml"; + auto templateConfiguration = reader.readFile(fileName); + + if (!templateConfiguration) + qWarning().nospace() << "template definitions:" << reader.errors(); + + return templateConfiguration; + } + + template<typename Type> + static Type getProperty(const QmlJS::SimpleReaderNode *node, const QString &name) + { + if (auto property = node->property(name)) { + const auto &value = property.value; + if (value.type() == QVariant::List) { + auto list = value.toList(); + if (list.size()) + return list.front().value<Type>(); + } else { + return property.value.value<Type>(); + } + } + + return {}; + } + + static QString getSource(const QString &name) + { + const auto &nodes = simpleReaderNode->children(); + for (const QmlJS::SimpleReaderNode::Ptr &node : nodes) { + if (auto typeName = getProperty<QString>(node.get(), "typeNames"); typeName != name) + continue; + + auto sourcePath = getProperty<QString>(node.get(), "sourceFile"); + + return getContent(QString{sourcesPath} + "/PropertyTemplates/" + sourcePath); + } + + return {}; + } + + static QString getContent(const QString &path) + { + QFile file{path}; + + if (file.open(QIODevice::ReadOnly)) + return QString::fromUtf8(file.readAll()); + + return {}; + } + + static QString getExpectedContent(const QString &typeName, + const QString &propertyName, + const QString &binding) + { + auto source = getSource(typeName); + + return source.arg(propertyName, binding); + } + + QmlDesigner::NodeMetaInfo createTypeWithProperties(Utils::SmallStringView name, + QmlDesigner::TypeId propertyTypeId) + { + auto typeId = projectStorageMock.createValue(qmlModuleId, name); + projectStorageMock.createProperty(typeId, "sub1", propertyTypeId); + projectStorageMock.createProperty(typeId, "sub2", propertyTypeId); + + return {typeId, &projectStorageMock}; + } + + QmlDesigner::NodeMetaInfo createType(Utils::SmallStringView name) + { + auto typeId = projectStorageMock.createValue(qmlModuleId, name); + + return {typeId, &projectStorageMock}; + } + + QmlDesigner::PropertyMetaInfo createProperty(QmlDesigner::TypeId typeId, + Utils::SmallString name, + QmlDesigner::Storage::PropertyDeclarationTraits traits, + QmlDesigner::TypeId propertyTypeId) + { + auto propertyId = projectStorageMock.createProperty(typeId, name, traits, propertyTypeId); + + return {propertyId, &projectStorageMock}; + } + + QString toString(const QmlDesigner::PropertyComponentGenerator::Property &propertyComponent, + const QString &baseName, + const QString &propertyName) + { + auto text = std::visit( + [](const auto &p) -> QString { + if constexpr (!std::is_same_v<std::decay_t<decltype(p)>, std::monostate>) + return p.component; + else + return {}; + }, + propertyComponent); + + text.replace(propertyName, baseName + "."_L1 + propertyName); + text.replace("backendValues."_L1 + baseName + "."_L1 + propertyName, + "backendValues."_L1 + baseName + "_"_L1 + propertyName); + + return text; + } + +protected: + inline static QSharedPointer<const QmlJS::SimpleReaderNode> simpleReaderNode; + NiceMock<AbstractViewMock> viewMock; + NiceMock<SourcePathCacheMockWithPaths> pathCacheMock{"/path/foo.qml"}; + NiceMock<ProjectStorageMockWithQtQtuick> projectStorageMock{pathCacheMock.sourceId}; + NiceMock<ModelResourceManagementMock> resourceManagementMock; + QmlDesigner::Model model{{projectStorageMock, pathCacheMock}, + "Item", + -1, + -1, + nullptr, + std::make_unique<ModelResourceManagementMockWrapper>( + resourceManagementMock)}; + QmlDesigner::PropertyComponentGenerator generator{QString{sourcesPath}, &model}; + QmlDesigner::NodeMetaInfo itemMetaInfo = model.qtQuickItemMetaInfo(); + QmlDesigner::ModuleId qmlModuleId = projectStorageMock.createModule("QML"); +}; + +TEST_F(PropertyComponentGenerator, + basic_property_is_returned_for_property_with_a_template_with_no_separate_section) +{ + QString expected = getExpectedContent("real", "x", "x"); + auto xProperty = itemMetaInfo.property("x"); + + auto propertyComponent = generator.create(xProperty); + + ASSERT_THAT(propertyComponent, IsBasicProperty(StrippedStringEq(expected))); +} + +TEST_F(PropertyComponentGenerator, + complex_property_is_returned_for_property_with_a_template_with_a_separate_section) +{ + QString expected = getExpectedContent("font", "font", "font"); + auto fontProperty = itemMetaInfo.property("font"); + + auto propertyComponent = generator.create(fontProperty); + + ASSERT_THAT(propertyComponent, IsComplexProperty(StrippedStringEq(expected))); +} + +TEST_F(PropertyComponentGenerator, + complex_property_is_returned_for_property_without_a_template_and_subproperties) +{ + auto stringId = projectStorageMock.createValue(qmlModuleId, "string"); + auto fooNodeInfo = createTypeWithProperties("Foo", stringId); + auto sub1Text = toString(generator.create(fooNodeInfo.property("sub1")), "foo", "sub1"); + auto sub2Text = toString(generator.create(fooNodeInfo.property("sub2")), "foo", "sub2"); + auto fooProperty = createProperty(itemMetaInfo.id(), "foo", {}, fooNodeInfo.id()); + QString expectedText = QStringLiteral( + R"xy( + Section { + caption: foo - Foo + anchors.left: parent.left + anchors.right: parent.right + leftPadding: 8 + rightPadding: 0 + expanded: false + level: 1 + SectionLayout { + )xy"); + expectedText += sub1Text; + expectedText += sub2Text; + expectedText += "}}"; + + auto propertyComponent = generator.create(fooProperty); + + ASSERT_THAT(propertyComponent, IsComplexProperty(StrippedStringEq(expectedText))); +} + +TEST_F(PropertyComponentGenerator, + pointer_readonly_complex_property_is_returned_for_property_without_a_template_and_subproperties) +{ + auto stringId = projectStorageMock.createValue(qmlModuleId, "string"); + auto fooNodeInfo = createTypeWithProperties("Foo", stringId); + auto sub1Text = toString(generator.create(fooNodeInfo.property("sub1")), "foo", "sub1"); + auto sub2Text = toString(generator.create(fooNodeInfo.property("sub2")), "foo", "sub2"); + auto fooProperty = createProperty(itemMetaInfo.id(), + "foo", + QmlDesigner::Storage::PropertyDeclarationTraits::IsPointer + | QmlDesigner::Storage::PropertyDeclarationTraits::IsReadOnly, + fooNodeInfo.id()); + QString expectedText = QStringLiteral( + R"xy( + Section { + caption: foo - Foo + anchors.left: parent.left + anchors.right: parent.right + leftPadding: 8 + rightPadding: 0 + expanded: false + level: 1 + SectionLayout { + )xy"); + expectedText += sub1Text; + expectedText += sub2Text; + expectedText += "}}"; + + auto propertyComponent = generator.create(fooProperty); + + ASSERT_THAT(propertyComponent, IsComplexProperty(StrippedStringEq(expectedText))); +} + +TEST_F(PropertyComponentGenerator, basic_property_without_template_is_returning_monostate) +{ + auto fooNodeInfo = createType("Foo"); + auto fooProperty = createProperty(itemMetaInfo.id(), "foo", {}, fooNodeInfo.id()); + + auto propertyComponent = generator.create(fooProperty); + + ASSERT_THAT(propertyComponent, VariantWith<std::monostate>(std::monostate{})); +} + +TEST_F(PropertyComponentGenerator, + complex_property_without_templates_and_writeable_is_returning_monostate) +{ + auto barTypeId = projectStorageMock.createValue(qmlModuleId, "bar"); + auto fooNodeInfo = createTypeWithProperties("Foo", barTypeId); + auto fooProperty = createProperty(itemMetaInfo.id(), "foo", {}, fooNodeInfo.id()); + + auto propertyComponent = generator.create(fooProperty); + + ASSERT_THAT(propertyComponent, VariantWith<std::monostate>(std::monostate{})); +} + +TEST_F(PropertyComponentGenerator, + pointer_writeable_complex_property_without_templates_and_with_only_subproperties_without_templates_is_returning_monostate) +{ + auto stringId = projectStorageMock.createValue(qmlModuleId, "string"); + auto fooNodeInfo = createTypeWithProperties("Foo", stringId); + auto sub1Text = toString(generator.create(fooNodeInfo.property("sub1")), "foo", "sub1"); + auto sub2Text = toString(generator.create(fooNodeInfo.property("sub2")), "foo", "sub2"); + auto fooProperty = createProperty(itemMetaInfo.id(), + "foo", + QmlDesigner::Storage::PropertyDeclarationTraits::IsPointer, + fooNodeInfo.id()); + + auto propertyComponent = generator.create(fooProperty); + + ASSERT_THAT(propertyComponent, VariantWith<std::monostate>(std::monostate{})); +} + +TEST_F(PropertyComponentGenerator, get_imports) +{ + auto imports = generator.imports(); + + ASSERT_THAT(imports, + ElementsAre(Eq("import HelperWidgets 2.0"), + Eq("import QtQuick 2.15"), + Eq("import QtQuick.Layouts 1.15"), + Eq("import StudioTheme 1.0 as StudioTheme"))); +} + +} // namespace diff --git a/tests/unit/tests/unittests/componentcore/propertyeditorcomponentgenerator-test.cpp b/tests/unit/tests/unittests/componentcore/propertyeditorcomponentgenerator-test.cpp new file mode 100644 index 0000000000..d02b2b12cb --- /dev/null +++ b/tests/unit/tests/unittests/componentcore/propertyeditorcomponentgenerator-test.cpp @@ -0,0 +1,487 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include <googletest.h> +#include <mocks/projectstoragemock.h> +#include <mocks/propertycomponentgeneratormock.h> +#include <strippedstring-matcher.h> + +#include <propertyeditorcomponentgenerator.h> + +namespace { + +using BasicProperty = QmlDesigner::PropertyComponentGenerator::BasicProperty; +using ComplexProperty = QmlDesigner::PropertyComponentGenerator::ComplexProperty; +using QmlDesigner::PropertyMetaInfo; + +class PropertyEditorTemplateGenerator : public ::testing::Test +{ +protected: + QmlDesigner::NodeMetaInfo createType(Utils::SmallStringView name, + QmlDesigner::TypeIds baseTypeIds = {}) + { + auto typeId = projectStorageMock.createValue(qtQuickModuleId, name, baseTypeIds); + + return {typeId, &projectStorageMock}; + } + + QmlDesigner::PropertyMetaInfo createProperty(QmlDesigner::TypeId typeId, + Utils::SmallString name, + QmlDesigner::Storage::PropertyDeclarationTraits traits, + QmlDesigner::TypeId propertyTypeId) + { + auto propertyId = projectStorageMock.createProperty(typeId, name, traits, propertyTypeId); + + return {propertyId, &projectStorageMock}; + } + + void setImports(const QStringList &imports) + { + ON_CALL(propertyGeneratorMock, imports()).WillByDefault(Return(imports)); + } + + void addBasicProperty(const PropertyMetaInfo &property, + Utils::SmallStringView name, + const QString &componentContent) + { + ON_CALL(propertyGeneratorMock, create(property)) + .WillByDefault(Return(BasicProperty{name, componentContent})); + } + + void addComplexProperty(const PropertyMetaInfo &property, + Utils::SmallStringView name, + const QString &componentContent) + { + ON_CALL(propertyGeneratorMock, create(property)) + .WillByDefault(Return(ComplexProperty{name, componentContent})); + } + + QmlDesigner::PropertyMetaInfo createBasicProperty(QmlDesigner::TypeId typeId, + Utils::SmallString name, + QmlDesigner::Storage::PropertyDeclarationTraits traits, + QmlDesigner::TypeId propertyTypeId, + const QString &componentContent) + { + auto propertyInfo = createProperty(typeId, name, traits, propertyTypeId); + addBasicProperty(propertyInfo, name, componentContent); + + return propertyInfo; + } + + QmlDesigner::PropertyMetaInfo createComplexProperty( + QmlDesigner::TypeId typeId, + Utils::SmallString name, + QmlDesigner::Storage::PropertyDeclarationTraits traits, + QmlDesigner::TypeId propertyTypeId, + const QString &componentContent) + { + auto propertyInfo = createProperty(typeId, name, traits, propertyTypeId); + addComplexProperty(propertyInfo, name, componentContent); + + return propertyInfo; + } + +protected: + QmlDesigner::SourceId sourceId = QmlDesigner::SourceId::create(10); + NiceMock<ProjectStorageMockWithQtQtuick> projectStorageMock{sourceId}; + NiceMock<PropertyComponentGeneratorMock> propertyGeneratorMock; + QmlDesigner::PropertyEditorTemplateGenerator generator{propertyGeneratorMock}; + QmlDesigner::ModuleId qtQuickModuleId = projectStorageMock.createModule("QtQuick"); + QmlDesigner::NodeMetaInfo fooTypeInfo = createType("Foo"); + QmlDesigner::TypeId dummyTypeId = projectStorageMock.commonTypeCache().builtinTypeId<double>(); +}; + +TEST_F(PropertyEditorTemplateGenerator, no_properties_and_no_imports) +{ + QString expectedText{ + R"xy( + Column { + width: parent.width + Section { + caption: "Exposed Custom Properties" + anchors.left: parent.left + anchors.right: parent.right + leftPadding: 0 + rightPadding: 0 + bottomPadding: 0 + Column { + width: parent.width + } + } + })xy"}; + + auto text = generator.create(fooTypeInfo, false); + + ASSERT_THAT(text, StrippedStringEq(expectedText)); +} + +TEST_F(PropertyEditorTemplateGenerator, properties_without_component_are_not_shows) +{ + QString expectedText{ + R"xy( + Column { + width: parent.width + Section { + caption: "Exposed Custom Properties" + anchors.left: parent.left + anchors.right: parent.right + leftPadding: 0 + rightPadding: 0 + bottomPadding: 0 + Column { + width: parent.width + } + } + })xy"}; + createProperty(fooTypeInfo.id(), "x", {}, dummyTypeId); + + auto text = generator.create(fooTypeInfo, false); + + ASSERT_THAT(text, StrippedStringEq(expectedText)); +} + +TEST_F(PropertyEditorTemplateGenerator, show_component_button_for_a_component_node) +{ + QString expectedText{ + R"xy( + Column { + width: parent.width + ComponentButton {} + Section { + caption: "Exposed Custom Properties" + anchors.left: parent.left + anchors.right: parent.right + leftPadding: 0 + rightPadding: 0 + bottomPadding: 0 + Column { + width: parent.width + } + } + })xy"}; + + auto text = generator.create(fooTypeInfo, true); + + ASSERT_THAT(text, StrippedStringEq(expectedText)); +} + +TEST_F(PropertyEditorTemplateGenerator, imports) +{ + QString expectedText{ + R"xy( + import QtQtuick + import Studio 2.1 + Column { + width: parent.width + Section { + caption: "Exposed Custom Properties" + anchors.left: parent.left + anchors.right: parent.right + leftPadding: 0 + rightPadding: 0 + bottomPadding: 0 + Column { + width: parent.width + } + } + })xy"}; + setImports({"import QtQtuick", "import Studio 2.1"}); + + auto text = generator.create(fooTypeInfo, false); + + ASSERT_THAT(text, StrippedStringEq(expectedText)); +} + +TEST_F(PropertyEditorTemplateGenerator, basic_property) +{ + QString expectedText{ + R"xy( + Column { + width: parent.width + Section { + caption: "Exposed Custom Properties" + anchors.left: parent.left + anchors.right: parent.right + leftPadding: 0 + rightPadding: 0 + bottomPadding: 0 + Column { + width: parent.width + Column { + width: parent.width + leftPadding: 8 + bottomPadding: 10 + SectionLayout { + Double{} + } + } + } + } + })xy"}; + createBasicProperty(fooTypeInfo.id(), "value", {}, dummyTypeId, "Double{}"); + + auto text = generator.create(fooTypeInfo, false); + + ASSERT_THAT(text, StrippedStringEq(expectedText)); +} + +TEST_F(PropertyEditorTemplateGenerator, basic_properties_with_base_type) +{ + QString expectedText{ + R"xy( + Column { + width: parent.width + Section { + caption: "Exposed Custom Properties" + anchors.left: parent.left + anchors.right: parent.right + leftPadding: 0 + rightPadding: 0 + bottomPadding: 0 + Column { + width: parent.width + Column { + width: parent.width + leftPadding: 8 + bottomPadding: 10 + SectionLayout { + SuperDouble{} + Double{} + } + } + } + } + })xy"}; + createBasicProperty(fooTypeInfo.id(), "x", {}, dummyTypeId, "Double{}"); + auto superFooInfo = createType("SuperFoo", {fooTypeInfo.id()}); + createBasicProperty(superFooInfo.id(), "value", {}, dummyTypeId, "SuperDouble{}"); + + auto text = generator.create(superFooInfo, false); + + ASSERT_THAT(text, StrippedStringEq(expectedText)); +} + +TEST_F(PropertyEditorTemplateGenerator, + only_handle_basic_properties_for_types_without_specifics_or_panes) +{ + QString expectedText{ + R"xy( + Column { + width: parent.width + Section { + caption: "Exposed Custom Properties" + anchors.left: parent.left + anchors.right: parent.right + leftPadding: 0 + rightPadding: 0 + bottomPadding: 0 + Column { + width: parent.width + Column { + width: parent.width + leftPadding: 8 + bottomPadding: 10 + SectionLayout { + SuperDouble{} + } + } + } + } + })xy"}; + createBasicProperty(fooTypeInfo.id(), "x", {}, dummyTypeId, "Double{}"); + auto superFooInfo = createType("SuperFoo", {fooTypeInfo.id()}); + createBasicProperty(superFooInfo.id(), "value", {}, dummyTypeId, "SuperDouble{}"); + projectStorageMock.setPropertyEditorPathId(fooTypeInfo.id(), sourceId); + + auto text = generator.create(superFooInfo, false); + + ASSERT_THAT(text, StrippedStringEq(expectedText)); +} + +TEST_F(PropertyEditorTemplateGenerator, order_basic_property) +{ + QString expectedText{ + R"xy( + Column { + width: parent.width + Section { + caption: "Exposed Custom Properties" + anchors.left: parent.left + anchors.right: parent.right + leftPadding: 0 + rightPadding: 0 + bottomPadding: 0 + Column { + width: parent.width + Column { + width: parent.width + leftPadding: 8 + bottomPadding: 10 + SectionLayout { + AnotherX{} + AndY{} + SomeZ{} + } + } + } + } + })xy"}; + createBasicProperty(fooTypeInfo.id(), "z", {}, dummyTypeId, "SomeZ{}"); + createBasicProperty(fooTypeInfo.id(), "x", {}, dummyTypeId, "AnotherX{}"); + createBasicProperty(fooTypeInfo.id(), "y", {}, dummyTypeId, "AndY{}"); + + auto text = generator.create(fooTypeInfo, false); + + ASSERT_THAT(text, StrippedStringEq(expectedText)); +} + +TEST_F(PropertyEditorTemplateGenerator, complex_property) +{ + QString expectedText{ + R"xy( + Column { + width: parent.width + Section { + caption: "Exposed Custom Properties" + anchors.left: parent.left + anchors.right: parent.right + leftPadding: 0 + rightPadding: 0 + bottomPadding: 0 + Column { + width: parent.width + Complex{} + } + } + })xy"}; + createComplexProperty(fooTypeInfo.id(), "value", {}, dummyTypeId, "Complex{}"); + + auto text = generator.create(fooTypeInfo, false); + + ASSERT_THAT(text, StrippedStringEq(expectedText)); +} + +TEST_F(PropertyEditorTemplateGenerator, complex_properties_with_base_type) +{ + QString expectedText{ + R"xy( + Column { + width: parent.width + Section { + caption: "Exposed Custom Properties" + anchors.left: parent.left + anchors.right: parent.right + leftPadding: 0 + rightPadding: 0 + bottomPadding: 0 + Column { + width: parent.width + Complex{} + SuperComplex{} + } + } + })xy"}; + createComplexProperty(fooTypeInfo.id(), "delegate", {}, dummyTypeId, "Complex{}"); + auto superFooInfo = createType("SuperFoo", {fooTypeInfo.id()}); + createComplexProperty(superFooInfo.id(), "value", {}, dummyTypeId, "SuperComplex{}"); + + auto text = generator.create(superFooInfo, false); + + ASSERT_THAT(text, StrippedStringEq(expectedText)); +} + +TEST_F(PropertyEditorTemplateGenerator, + only_handle_complex_properties_for_types_without_specifics_or_panes) +{ + QString expectedText{ + R"xy( + Column { + width: parent.width + Section { + caption: "Exposed Custom Properties" + anchors.left: parent.left + anchors.right: parent.right + leftPadding: 0 + rightPadding: 0 + bottomPadding: 0 + Column { + width: parent.width + SuperComplex{} + } + } + })xy"}; + createComplexProperty(fooTypeInfo.id(), "delegate", {}, dummyTypeId, "Complex{}"); + auto superFooInfo = createType("SuperFoo", {fooTypeInfo.id()}); + createComplexProperty(superFooInfo.id(), "value", {}, dummyTypeId, "SuperComplex{}"); + projectStorageMock.setPropertyEditorPathId(fooTypeInfo.id(), sourceId); + + auto text = generator.create(superFooInfo, false); + + ASSERT_THAT(text, StrippedStringEq(expectedText)); +} + +TEST_F(PropertyEditorTemplateGenerator, ordered_complex_property) +{ + QString expectedText{ + R"xy( + Column { + width: parent.width + Section { + caption: "Exposed Custom Properties" + anchors.left: parent.left + anchors.right: parent.right + leftPadding: 0 + rightPadding: 0 + bottomPadding: 0 + Column { + width: parent.width + Anchors{} + Delegate{} + ComplexFont{} + } + } + })xy"}; + createComplexProperty(fooTypeInfo.id(), "delegate", {}, dummyTypeId, "Delegate{}"); + createComplexProperty(fooTypeInfo.id(), "font", {}, dummyTypeId, "ComplexFont{}"); + createComplexProperty(fooTypeInfo.id(), "anchors", {}, dummyTypeId, "Anchors{}"); + + auto text = generator.create(fooTypeInfo, false); + + ASSERT_THAT(text, StrippedStringEq(expectedText)); +} + +TEST_F(PropertyEditorTemplateGenerator, basic_is_placed_before_complex_components) +{ + QString expectedText{ + R"xy( + Column { + width: parent.width + Section { + caption: "Exposed Custom Properties" + anchors.left: parent.left + anchors.right: parent.right + leftPadding: 0 + rightPadding: 0 + bottomPadding: 0 + Column { + width: parent.width + Column { + width: parent.width + leftPadding: 8 + bottomPadding: 10 + SectionLayout { + Double{} + } + } + Font{} + } + } + })xy"}; + createBasicProperty(fooTypeInfo.id(), "x", {}, dummyTypeId, "Double{}"); + createComplexProperty(fooTypeInfo.id(), "font", {}, dummyTypeId, "Font{}"); + + auto text = generator.create(fooTypeInfo, false); + + ASSERT_THAT(text, StrippedStringEq(expectedText)); +} +} // namespace diff --git a/tests/unit/tests/unittests/metainfo/nodemetainfo-test.cpp b/tests/unit/tests/unittests/metainfo/nodemetainfo-test.cpp index c1f3b4f8d7..c21d325936 100644 --- a/tests/unit/tests/unittests/metainfo/nodemetainfo-test.cpp +++ b/tests/unit/tests/unittests/metainfo/nodemetainfo-test.cpp @@ -409,7 +409,7 @@ TEST_F(NodeMetaInfo, get_invalid_property_if_not_exists) { auto metaInfo = model.qtQuickItemMetaInfo(); - auto property = metaInfo.property("x"); + auto property = metaInfo.property("foo"); ASSERT_THAT(property, PropertyId(IsFalse())); } diff --git a/tests/unit/tests/unittests/model/model-test.cpp b/tests/unit/tests/unittests/model/model-test.cpp index 17374d4314..b705c15ffa 100644 --- a/tests/unit/tests/unittests/model/model-test.cpp +++ b/tests/unit/tests/unittests/model/model-test.cpp @@ -869,6 +869,49 @@ TEST_F(Model, meta_info_of_not_existing_type_is_invalid) ASSERT_THAT(meta_info, IsFalse()); } +TEST_F(Model, module_is_valid) +{ + auto module = model.module("QML"); + + ASSERT_THAT(module, IsTrue()); +} + +TEST_F(Model, module_returns_always_the_same) +{ + auto oldModule = model.module("QML"); + + auto module = model.module("QML"); + + ASSERT_THAT(module, oldModule); +} + +TEST_F(Model, get_meta_info_by_module) +{ + auto module = model.module("QML"); + + auto metaInfo = model.metaInfo(module, "QtObject"); + + ASSERT_THAT(metaInfo, model.qmlQtObjectMetaInfo()); +} + +TEST_F(Model, get_invalid_meta_info_by_module_for_wrong_name) +{ + auto module = model.module("QML"); + + auto metaInfo = model.metaInfo(module, "Object"); + + ASSERT_THAT(metaInfo, IsFalse()); +} + +TEST_F(Model, get_invalid_meta_info_by_module_for_wrong_module) +{ + auto module = model.module("Qml"); + + auto metaInfo = model.metaInfo(module, "Object"); + + ASSERT_THAT(metaInfo, IsFalse()); +} + TEST_F(Model, add_refresh_callback_to_project_storage) { EXPECT_CALL(projectStorageMock, addRefreshCallback(_)); |