diff --git a/src/libs/qmlpuppetcommunication/interfaces/nodeinstanceglobal.h b/src/libs/qmlpuppetcommunication/interfaces/nodeinstanceglobal.h index 6ade6a92751..ed0104ae54d 100644 --- a/src/libs/qmlpuppetcommunication/interfaces/nodeinstanceglobal.h +++ b/src/libs/qmlpuppetcommunication/interfaces/nodeinstanceglobal.h @@ -20,7 +20,8 @@ enum class AuxiliaryDataType { Temporary, Document, NodeInstancePropertyOverwrite, - NodeInstanceAuxiliary + NodeInstanceAuxiliary, + Persistent }; enum class View3DActionType { diff --git a/src/plugins/qmldesigner/CMakeLists.txt b/src/plugins/qmldesigner/CMakeLists.txt index 33b4df0292f..1ee5b7ca139 100644 --- a/src/plugins/qmldesigner/CMakeLists.txt +++ b/src/plugins/qmldesigner/CMakeLists.txt @@ -361,6 +361,7 @@ extend_qtc_library(QmlDesignerCore abstractview.cpp anchorline.cpp annotation.cpp + auxiliarypropertystorageview.cpp auxiliarypropertystorageview.h bindingproperty.cpp componenttextmodifier.cpp documentmessage.cpp diff --git a/src/plugins/qmldesigner/components/componentcore/viewmanager.cpp b/src/plugins/qmldesigner/components/componentcore/viewmanager.cpp index 1ab5a012b2f..2e6b2b52dbb 100644 --- a/src/plugins/qmldesigner/components/componentcore/viewmanager.cpp +++ b/src/plugins/qmldesigner/components/componentcore/viewmanager.cpp @@ -20,17 +20,19 @@ #include #include #include +#include #include #include #include +#include #include #include #include #include -#include #include +#include #include #include @@ -49,6 +51,7 @@ public: ViewManagerData(AsynchronousImageCache &imageCache, ExternalDependenciesInterface &externalDependencies) : debugView{externalDependencies} + , auxiliaryDataKeyView{auxiliaryDataDatabase, externalDependencies} , designerActionManagerView{externalDependencies} , nodeInstanceView(QCoreApplication::arguments().contains("-capture-puppet-stream") ? capturingConnectionManager @@ -78,6 +81,11 @@ public: CapturingConnectionManager capturingConnectionManager; QmlModelState savedState; Internal::DebugView debugView; + Sqlite::Database auxiliaryDataDatabase{ + Utils::PathString{Core::ICore::userResourcePath("auxiliary_data.db").toString()}, + Sqlite::JournalMode::Wal, + Sqlite::LockingMode::Normal}; + AuxiliaryPropertyStorageView auxiliaryDataKeyView; DesignerActionManagerView designerActionManagerView; NodeInstanceView nodeInstanceView; ContentLibraryView contentLibraryView; @@ -201,7 +209,8 @@ QList ViewManager::views() const QList ViewManager::standardViews() const { #ifndef QTC_USE_QML_DESIGNER_LITE - QList list = {&d->edit3DView, + QList list = {&d->auxiliaryDataKeyView, + &d->edit3DView, &d->formEditorView, &d->textEditorView, &d->assetsLibraryView, diff --git a/src/plugins/qmldesigner/components/debugview/debugview.cpp b/src/plugins/qmldesigner/components/debugview/debugview.cpp index e05001e4774..b6f61c9e2d4 100644 --- a/src/plugins/qmldesigner/components/debugview/debugview.cpp +++ b/src/plugins/qmldesigner/components/debugview/debugview.cpp @@ -244,6 +244,9 @@ QTextStream &operator<<(QTextStream &stream, AuxiliaryDataType type) case AuxiliaryDataType::Temporary: stream << "Temporary"; break; + case AuxiliaryDataType::Persistent: + stream << "Persistent"; + break; } return stream; diff --git a/src/plugins/qmldesigner/designercore/include/model.h b/src/plugins/qmldesigner/designercore/include/model.h index 39b5cdaa811..98de7bb587f 100644 --- a/src/plugins/qmldesigner/designercore/include/model.h +++ b/src/plugins/qmldesigner/designercore/include/model.h @@ -249,6 +249,7 @@ public: const QList &warnings); QList selectedNodes(AbstractView *view) const; + void setSelectedModelNodes(const QList &selectedNodeList); void clearMetaInfoCache(); diff --git a/src/plugins/qmldesigner/designercore/include/modelnode.h b/src/plugins/qmldesigner/designercore/include/modelnode.h index 85b4198b43a..63d4190116a 100644 --- a/src/plugins/qmldesigner/designercore/include/modelnode.h +++ b/src/plugins/qmldesigner/designercore/include/modelnode.h @@ -152,10 +152,10 @@ public: void destroy(); QString id() const; - void ensureIdExists(); - [[nodiscard]] QString validId(); - void setIdWithRefactoring(const QString &id); - void setIdWithoutRefactoring(const QString &id); + void ensureIdExists() const; + [[nodiscard]] QString validId() const; + void setIdWithRefactoring(const QString &id) const; + void setIdWithoutRefactoring(const QString &id) const; static bool isValidId(const QString &id); static QString getIdValidityErrorMessage(const QString &id); @@ -192,6 +192,7 @@ public: void removeAuxiliaryData(AuxiliaryDataType type, Utils::SmallStringView name) const; bool hasAuxiliaryData(AuxiliaryDataKeyView key) const; bool hasAuxiliaryData(AuxiliaryDataType type, Utils::SmallStringView name) const; + bool hasAuxiliaryData(AuxiliaryDataType type) const; AuxiliaryDatasForType auxiliaryData(AuxiliaryDataType type) const; AuxiliaryDatasView auxiliaryData() const; diff --git a/src/plugins/qmldesigner/designercore/model/abstractview.cpp b/src/plugins/qmldesigner/designercore/model/abstractview.cpp index 254f9536f9c..2f3ddb86c03 100644 --- a/src/plugins/qmldesigner/designercore/model/abstractview.cpp +++ b/src/plugins/qmldesigner/designercore/model/abstractview.cpp @@ -427,14 +427,7 @@ QList toInternalNodeList(const QList */ void AbstractView::setSelectedModelNodes(const QList &selectedNodeList) { - QList unlockedNodes; - - for (const auto &modelNode : selectedNodeList) { - if (!ModelUtils::isThisOrAncestorLocked(modelNode)) - unlockedNodes.push_back(modelNode); - } - - model()->d->setSelectedNodes(toInternalNodeList(unlockedNodes)); + model()->setSelectedModelNodes(selectedNodeList); } void AbstractView::setSelectedModelNode(const ModelNode &modelNode) diff --git a/src/plugins/qmldesigner/designercore/model/auxiliarypropertystorageview.cpp b/src/plugins/qmldesigner/designercore/model/auxiliarypropertystorageview.cpp new file mode 100644 index 00000000000..23d66b29a4b --- /dev/null +++ b/src/plugins/qmldesigner/designercore/model/auxiliarypropertystorageview.cpp @@ -0,0 +1,201 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "auxiliarypropertystorageview.h" + +#include + +#include +#include + +namespace QmlDesigner { + +namespace { +struct Initializer +{ + Initializer(Sqlite::Database &database, bool isInitialized) + { + if (!isInitialized) { + createAuxiliaryPropertiesTable(database); + } + database.setIsInitialized(true); + } + + void createAuxiliaryPropertiesTable(Sqlite::Database &database) + { + Sqlite::StrictTable table; + table.setUseIfNotExists(true); + table.setUseWithoutRowId(true); + table.setName("auxiliaryProperties"); + auto &filePathColumn = table.addColumn("filePath", Sqlite::StrictColumnType::Text); + auto &idColumn = table.addColumn("id", Sqlite::StrictColumnType::Text); + auto &keyColumn = table.addColumn("key", Sqlite::StrictColumnType::Text); + table.addColumn("value", Sqlite::StrictColumnType::Blob); + + table.addPrimaryKeyContraint({filePathColumn, idColumn, keyColumn}); + + table.initialize(database); + } +}; + +struct Entry +{ + Entry(Utils::SmallStringView nodeId, Utils::SmallStringView key, Sqlite::BlobView value) + : nodeId{nodeId} + , key{key} + , value{value} + {} + + Utils::SmallStringView nodeId; + Utils::SmallStringView key; + Sqlite::BlobView value; +}; + +struct ChangeEntry +{ + ChangeEntry(Utils::SmallStringView id, Utils::SmallStringView name) + : id{id} + , name{name} + {} + + friend bool operator<(const ChangeEntry &first, const ChangeEntry &second) + { + return std::tie(first.id, first.name) < std::tie(second.id, second.name); + } + + Utils::SmallString id; + Utils::SmallString name; +}; + +} // namespace + +struct AuxiliaryPropertyStorageView::Data +{ + Data(Sqlite::Database &database) + : database{database} + { + exclusiveTransaction.commit(); + } + + Sqlite::Database &database; + Sqlite::ExclusiveNonThrowingDestructorTransaction exclusiveTransaction{database}; + Initializer initializer{database, database.isInitialized()}; + Sqlite::WriteStatement<4> upsertValue{ + "INSERT INTO auxiliaryProperties(filePath, id, key, value) " + "VALUES(?1, ?2, ?3, ?4) " + "ON CONFLICT DO UPDATE SET value=excluded.value", + database}; + Sqlite::ReadStatement<3, 1> selectValues{ + "SELECT id, key, value FROM auxiliaryProperties WHERE filePath=?1", database}; + Sqlite::WriteStatement<0> removeValues{"DELETE FROM auxiliaryProperties", database}; + Sqlite::WriteStatement<3> removeValue{ + "DELETE FROM auxiliaryProperties WHERE filePath=?1 AND id=?2 AND key=?3", database}; + std::set> changeEntries; +}; + +AuxiliaryPropertyStorageView::AuxiliaryPropertyStorageView( + Sqlite::Database &database, ExternalDependenciesInterface &externalDependencies) + : AbstractView{externalDependencies} + , d{std::make_unique(database)} +{} + +AuxiliaryPropertyStorageView::~AuxiliaryPropertyStorageView() = default; + +void AuxiliaryPropertyStorageView::modelAttached(Model *model) +{ + auto entryRange = d->selectValues.rangeWithTransaction( + Utils::PathString{model->fileUrl().path()}); + + for (const auto entry : entryRange) { + auto node = model->modelNodeForId(QString{entry.nodeId}); + + if (!node) + continue; + + auto array = QByteArray::fromRawData(entry.value.cdata(), entry.value.ssize()); + QDataStream dataStream{array}; + + QVariant value; + dataStream >> value; + + node.setAuxiliaryDataWithoutLock({AuxiliaryDataType::Persistent, entry.key}, value); + } + + AbstractView::modelAttached(model); +} + +void AuxiliaryPropertyStorageView::modelAboutToBeDetached(Model *model) +{ + Utils::PathString filePath{model->fileUrl().path()}; + + auto update = [&] { + for (const auto &[id, name] : d->changeEntries) { + QByteArray array; + QDataStream dataStream{&array, QIODevice::WriteOnly}; + auto node = model->modelNodeForId(id.toQString()); + + auto value = node.auxiliaryData(AuxiliaryDataType::Persistent, name); + if (value) { + dataStream << *value; + + Sqlite::BlobView blob{array}; + + d->upsertValue.write(filePath, id, name, blob); + + } else { + d->removeValue.write(filePath, id, name); + } + } + }; + + if (d->changeEntries.size()) { + Sqlite::withImmediateTransaction(d->database, update); + + d->changeEntries.clear(); + } + + AbstractView::modelAboutToBeDetached(model); +} + +void AuxiliaryPropertyStorageView::nodeIdChanged(const ModelNode &node, + const QString &newIdArg, + const QString &oldIdArg) +{ + Utils::SmallString newId = newIdArg; + Utils::SmallString oldId = oldIdArg; + for (auto entry : node.auxiliaryData()) { + if (entry.first.type != AuxiliaryDataType::Persistent) + continue; + + d->changeEntries.emplace(oldId, entry.first.name); + d->changeEntries.emplace(newId, entry.first.name); + } +} + +void AuxiliaryPropertyStorageView::nodeAboutToBeRemoved(const ModelNode &removedNode) +{ + if (removedNode.hasId()) { + Utils::SmallString id = removedNode.id(); + for (auto entry : removedNode.auxiliaryData()) { + if (entry.first.type != AuxiliaryDataType::Persistent) + continue; + + d->changeEntries.emplace(id, entry.first.name); + } + } +} + +void AuxiliaryPropertyStorageView::auxiliaryDataChanged(const ModelNode &node, + AuxiliaryDataKeyView key, + const QVariant &) +{ + if (isAttached() && key.type == AuxiliaryDataType::Persistent) + d->changeEntries.emplace(Utils::SmallString{node.id()}, key.name); +} + +void AuxiliaryPropertyStorageView::resetForTestsOnly() +{ + Sqlite::withImmediateTransaction(d->database, [&] { d->removeValues.execute(); }); +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/designercore/model/auxiliarypropertystorageview.h b/src/plugins/qmldesigner/designercore/model/auxiliarypropertystorageview.h new file mode 100644 index 00000000000..2096afe9596 --- /dev/null +++ b/src/plugins/qmldesigner/designercore/model/auxiliarypropertystorageview.h @@ -0,0 +1,34 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include + +#include + +namespace QmlDesigner { +class AuxiliaryPropertyStorageView final : public AbstractView +{ +public: + AuxiliaryPropertyStorageView(Sqlite::Database &database, + ExternalDependenciesInterface &externalDependencies); + ~AuxiliaryPropertyStorageView(); + + void modelAttached(Model *model) override; + void modelAboutToBeDetached(Model *model) override; + + void nodeIdChanged(const ModelNode &node, const QString &newId, const QString &oldId) override; + + void nodeAboutToBeRemoved(const ModelNode &removedNode) override; + + void auxiliaryDataChanged(const ModelNode &node, + AuxiliaryDataKeyView type, + const QVariant &data) override; + + struct Data; + + void resetForTestsOnly(); + +private: + std::unique_ptr d; +}; +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/designercore/model/internalnode.cpp b/src/plugins/qmldesigner/designercore/model/internalnode.cpp index bf59383747c..d4a7c1ba584 100644 --- a/src/plugins/qmldesigner/designercore/model/internalnode.cpp +++ b/src/plugins/qmldesigner/designercore/model/internalnode.cpp @@ -51,6 +51,14 @@ auto find(Type &&auxiliaryDatas, AuxiliaryDataKeyView key) }); } +template +auto find(Type &&auxiliaryDatas, AuxiliaryDataType type) +{ + return std::find_if(auxiliaryDatas.begin(), auxiliaryDatas.end(), [&](const auto &element) { + return element.first.type == type; + }); +} + } // namespace std::optional InternalNode::auxiliaryData(AuxiliaryDataKeyView key) const @@ -99,6 +107,13 @@ bool InternalNode::hasAuxiliaryData(AuxiliaryDataKeyView key) const return found != m_auxiliaryDatas.end(); } +bool InternalNode::hasAuxiliaryData(AuxiliaryDataType type) const +{ + auto found = find(m_auxiliaryDatas, type); + + return found != m_auxiliaryDatas.end(); +} + AuxiliaryDatasForType InternalNode::auxiliaryData(AuxiliaryDataType type) const { AuxiliaryDatasForType data; diff --git a/src/plugins/qmldesigner/designercore/model/internalnode_p.h b/src/plugins/qmldesigner/designercore/model/internalnode_p.h index f3fe55f2315..0bc0b9ce997 100644 --- a/src/plugins/qmldesigner/designercore/model/internalnode_p.h +++ b/src/plugins/qmldesigner/designercore/model/internalnode_p.h @@ -76,6 +76,7 @@ public: bool setAuxiliaryData(AuxiliaryDataKeyView key, const QVariant &data); bool removeAuxiliaryData(AuxiliaryDataKeyView key); bool hasAuxiliaryData(AuxiliaryDataKeyView key) const; + bool hasAuxiliaryData(AuxiliaryDataType type) const; AuxiliaryDatasForType auxiliaryData(AuxiliaryDataType type) const; AuxiliaryDatasView auxiliaryData() const { return std::as_const(m_auxiliaryDatas); } diff --git a/src/plugins/qmldesigner/designercore/model/model.cpp b/src/plugins/qmldesigner/designercore/model/model.cpp index f92aca3b8d6..0fddce99b09 100644 --- a/src/plugins/qmldesigner/designercore/model/model.cpp +++ b/src/plugins/qmldesigner/designercore/model/model.cpp @@ -4,6 +4,7 @@ #include "model.h" #include "internalnode_p.h" #include "model_p.h" +#include "modelutils.h" #include #include "../projectstorage/sourcepath.h" @@ -2039,6 +2040,18 @@ QList Model::selectedNodes(AbstractView *view) const return d->toModelNodeList(d->selectedNodes(), view); } +void Model::setSelectedModelNodes(const QList &selectedNodeList) +{ + QList unlockedNodes; + + for (const auto &modelNode : selectedNodeList) { + if (!ModelUtils::isThisOrAncestorLocked(modelNode)) + unlockedNodes.push_back(modelNode); + } + + d->setSelectedNodes(toInternalNodeList(unlockedNodes)); +} + void Model::clearMetaInfoCache() { d->m_nodeMetaInfoCache.clear(); @@ -2064,7 +2077,6 @@ SourceId Model::fileUrlSourceId() const */ void Model::setFileUrl(const QUrl &url) { - QTC_ASSERT(url.isValid() && url.isLocalFile(), qDebug() << "url:" << url; return); Internal::WriteLocker locker(d.get()); d->setFileUrl(url); } diff --git a/src/plugins/qmldesigner/designercore/model/modelnode.cpp b/src/plugins/qmldesigner/designercore/model/modelnode.cpp index 5e7defd1456..c4fc203fd2b 100644 --- a/src/plugins/qmldesigner/designercore/model/modelnode.cpp +++ b/src/plugins/qmldesigner/designercore/model/modelnode.cpp @@ -80,13 +80,13 @@ QString ModelNode::id() const return m_internalNode->id; } -void ModelNode::ensureIdExists() +void ModelNode::ensureIdExists() const { if (!hasId()) setIdWithoutRefactoring(model()->generateNewId(simplifiedTypeName())); } -QString ModelNode::validId() +QString ModelNode::validId() const { ensureIdExists(); @@ -162,7 +162,7 @@ bool ModelNode::hasId() const return !m_internalNode->id.isEmpty(); } -void ModelNode::setIdWithRefactoring(const QString &id) +void ModelNode::setIdWithRefactoring(const QString &id) const { if (isValid()) { if (model()->rewriterView() && !id.isEmpty() @@ -174,7 +174,7 @@ void ModelNode::setIdWithRefactoring(const QString &id) } } -void ModelNode::setIdWithoutRefactoring(const QString &id) +void ModelNode::setIdWithoutRefactoring(const QString &id) const { Internal::WriteLocker locker(m_model.data()); if (!isValid()) @@ -601,7 +601,8 @@ static QList descendantNodes(const ModelNode &node) static void removeModelNodeFromSelection(const ModelNode &node) { // remove nodes from the active selection - QList selectedList = node.view()->selectedModelNodes(); + auto model = node.model(); + QList selectedList = model->selectedNodes(node.view()); const QList descendants = descendantNodes(node); for (const ModelNode &descendantNode : descendants) @@ -609,7 +610,7 @@ static void removeModelNodeFromSelection(const ModelNode &node) selectedList.removeAll(node); - node.view()->setSelectedModelNodes(selectedList); + model->setSelectedModelNodes(selectedList); } /*! \brief complete removes this ModelNode from the Model @@ -995,6 +996,8 @@ void ModelNode::setAuxiliaryData(AuxiliaryDataType type, void ModelNode::setAuxiliaryData(AuxiliaryDataKeyView key, const QVariant &data) const { if (isValid()) { + if (key.type == AuxiliaryDataType::Persistent) + ensureIdExists(); Internal::WriteLocker locker(m_model.data()); m_model->d->setAuxiliaryData(internalNode(), key, data); } @@ -1002,21 +1005,32 @@ void ModelNode::setAuxiliaryData(AuxiliaryDataKeyView key, const QVariant &data) void ModelNode::setAuxiliaryDataWithoutLock(AuxiliaryDataKeyView key, const QVariant &data) const { - if (isValid()) + if (isValid()) { + if (key.type == AuxiliaryDataType::Persistent) + ensureIdExists(); + m_model->d->setAuxiliaryData(internalNode(), key, data); + } } void ModelNode::setAuxiliaryDataWithoutLock(AuxiliaryDataType type, Utils::SmallStringView name, const QVariant &data) const { - if (isValid()) + if (isValid()) { + if (type == AuxiliaryDataType::Persistent) + ensureIdExists(); + m_model->d->setAuxiliaryData(internalNode(), {type, name}, data); + } } void ModelNode::removeAuxiliaryData(AuxiliaryDataKeyView key) const { if (isValid()) { + if (key.type == AuxiliaryDataType::Persistent) + ensureIdExists(); + Internal::WriteLocker locker(m_model.data()); m_model->d->removeAuxiliaryData(internalNode(), key); } @@ -1040,6 +1054,14 @@ bool ModelNode::hasAuxiliaryData(AuxiliaryDataType type, Utils::SmallStringView return hasAuxiliaryData({type, name}); } +bool ModelNode::hasAuxiliaryData(AuxiliaryDataType type) const +{ + if (!isValid()) + return false; + + return m_internalNode->hasAuxiliaryData(type); +} + AuxiliaryDatasForType ModelNode::auxiliaryData(AuxiliaryDataType type) const { if (!isValid()) diff --git a/tests/unit/tests/printers/gtest-creator-printing.cpp b/tests/unit/tests/printers/gtest-creator-printing.cpp index 4f3c44419ab..d1ad492e00d 100644 --- a/tests/unit/tests/printers/gtest-creator-printing.cpp +++ b/tests/unit/tests/printers/gtest-creator-printing.cpp @@ -534,6 +534,42 @@ std::ostream &operator<<(std::ostream &out, FlagIs flagIs) return out; } +std::ostream &operator<<(std::ostream &out, const BasicAuxiliaryDataKey &key) +{ + return out << "(" << key.name << ", " << key.type << ")"; +} + +std::ostream &operator<<(std::ostream &out, const BasicAuxiliaryDataKey &key) +{ + return out << "(" << key.name << ", " << key.type << ")"; +} + +std::ostream &operator<<(std::ostream &out, AuxiliaryDataType type) +{ + switch (type) { + case AuxiliaryDataType::None: + out << "None"; + break; + case AuxiliaryDataType::Temporary: + out << "Temporary"; + break; + case AuxiliaryDataType::Document: + out << "Document"; + break; + case AuxiliaryDataType::NodeInstancePropertyOverwrite: + out << "NodeInstancePropertyOverwrite"; + break; + case AuxiliaryDataType::NodeInstanceAuxiliary: + out << "NodeInstanceAuxiliary"; + break; + case AuxiliaryDataType::Persistent: + out << "Persistent"; + break; + } + + return out; +} + namespace Cache { std::ostream &operator<<(std::ostream &out, const SourceContext &sourceContext) diff --git a/tests/unit/tests/printers/gtest-creator-printing.h b/tests/unit/tests/printers/gtest-creator-printing.h index 3fb862d915f..7d5bfe6606a 100644 --- a/tests/unit/tests/printers/gtest-creator-printing.h +++ b/tests/unit/tests/printers/gtest-creator-printing.h @@ -4,6 +4,7 @@ #pragma once #include +#include #include #include @@ -126,6 +127,8 @@ class NodeMetaInfo; class PropertyMetaInfo; struct CompoundPropertyMetaInfo; enum class FlagIs : unsigned int; +template +class BasicAuxiliaryDataKey; std::ostream &operator<<(std::ostream &out, const ModelNode &node); std::ostream &operator<<(std::ostream &out, const VariantProperty &property); @@ -142,6 +145,9 @@ std::ostream &operator<<(std::ostream &out, const NodeMetaInfo &metaInfo); std::ostream &operator<<(std::ostream &out, const PropertyMetaInfo &metaInfo); std::ostream &operator<<(std::ostream &out, const CompoundPropertyMetaInfo &metaInfo); std::ostream &operator<<(std::ostream &out, FlagIs flagIs); +std::ostream &operator<<(std::ostream &out, const BasicAuxiliaryDataKey &key); +std::ostream &operator<<(std::ostream &out, const BasicAuxiliaryDataKey &key); +std::ostream &operator<<(std::ostream &out, AuxiliaryDataType type); namespace Cache { class SourceContext; diff --git a/tests/unit/tests/printers/gtest-std-printing.h b/tests/unit/tests/printers/gtest-std-printing.h index af62ddfb1ee..f2ad83ef619 100644 --- a/tests/unit/tests/printers/gtest-std-printing.h +++ b/tests/unit/tests/printers/gtest-std-printing.h @@ -4,6 +4,8 @@ #pragma once #include +#include +#include #include namespace std { @@ -24,4 +26,41 @@ ostream &operator<<(ostream &out, const vector &vector) return out; } +template +ostream &operator<<(ostream &out, const span &span) +{ + out << "["; + + for (auto current = span.begin(); current != span.end(); ++current) { + out << *current; + + if (std::next(current) != span.end()) + out << ", "; + } + + out << "]"; + + return out; +} + +template +ostream &operator<<(ostream &out, const pair &pair) +{ + out << "{"; + + out << pair.first; + + out << ", "; + + out << pair.second; + + out << "}"; + + return out; +} + +inline ostream &operator<<(ostream &out, std::byte byte) +{ + return out << std::hex << static_cast(byte) << std::dec; +} } // namespace std diff --git a/tests/unit/tests/testdesignercore/CMakeLists.txt b/tests/unit/tests/testdesignercore/CMakeLists.txt index c446af81a35..423ce7ff8a5 100644 --- a/tests/unit/tests/testdesignercore/CMakeLists.txt +++ b/tests/unit/tests/testdesignercore/CMakeLists.txt @@ -80,6 +80,7 @@ add_qtc_library(TestDesignerCore OBJECT metainfo/nodemetainfo.cpp model/abstractproperty.cpp model/abstractview.cpp + model/auxiliarypropertystorageview.cpp model/auxiliarypropertystorageview.h model/annotation.cpp model/bindingproperty.cpp model/import.cpp diff --git a/tests/unit/tests/unittests/model/CMakeLists.txt b/tests/unit/tests/unittests/model/CMakeLists.txt index 43d6154c504..08b2a7daa59 100644 --- a/tests/unit/tests/unittests/model/CMakeLists.txt +++ b/tests/unit/tests/unittests/model/CMakeLists.txt @@ -1,6 +1,7 @@ # qmldesigner/designercore/model extend_qtc_test(unittest SOURCES + auxiliarypropertystorageview-test.cpp import-test.cpp model-test.cpp modelnode-test.cpp diff --git a/tests/unit/tests/unittests/model/auxiliarypropertystorageview-test.cpp b/tests/unit/tests/unittests/model/auxiliarypropertystorageview-test.cpp new file mode 100644 index 00000000000..62f99e89d57 --- /dev/null +++ b/tests/unit/tests/unittests/model/auxiliarypropertystorageview-test.cpp @@ -0,0 +1,230 @@ +// 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 + +#include +#include +#include +#include + +#include + +namespace { + +using QmlDesigner::AuxiliaryDataKey; +using QmlDesigner::AuxiliaryDataType; +using QmlDesigner::ModelNode; + +template +auto AuxiliaryProperty(AuxiliaryDataType type, const NameMatcher &nameMatcher, const QVariant &value) +{ + return Pair(AllOf(Field(&AuxiliaryDataKey::type, type), + Field(&AuxiliaryDataKey::name, nameMatcher)), + value); +} + +class AuxiliaryPropertyStorageView : public ::testing::Test +{ +protected: + struct StaticData + { + Sqlite::Database database{":memory:", Sqlite::JournalMode::Memory}; + }; + + ~AuxiliaryPropertyStorageView() { view.resetForTestsOnly(); } + + static void SetUpTestSuite() { staticData = std::make_unique(); } + + static void TearDownTestSuite() { staticData.reset(); } + + inline static std::unique_ptr staticData; + Sqlite::Database &database = staticData->database; + NiceMock pathCacheMock{"/path/foo.qml"}; + NiceMock projectStorageMock{pathCacheMock.sourceId}; + NiceMock resourceManagementMock; + QmlDesigner::Imports imports = {QmlDesigner::Import::createLibraryImport("QtQuick")}; + QmlDesigner::Model model{{projectStorageMock, pathCacheMock}, + "Item", + imports, + pathCacheMock.path.toQString(), + std::make_unique( + resourceManagementMock)}; + QmlDesigner::Model model2{{projectStorageMock, pathCacheMock}, + "Item", + imports, + pathCacheMock.path.toQString(), + std::make_unique( + resourceManagementMock)}; + NiceMock externalDependenciesMock; + QmlDesigner::AuxiliaryPropertyStorageView view{database, externalDependenciesMock}; + ModelNode rootNode = model.rootModelNode(); + ModelNode rootNode2 = model2.rootModelNode(); + QmlDesigner::AuxiliaryDataKeyView persistentFooKey{AuxiliaryDataType::Persistent, "foo"}; + QmlDesigner::AuxiliaryDataKeyView temporaryFooKey{AuxiliaryDataType::Temporary, "foo"}; +}; + +TEST_F(AuxiliaryPropertyStorageView, store_persistent_auxiliary_property) +{ + // arrange + model.attachView(&view); + rootNode.setIdWithoutRefactoring("root"); + rootNode2.setIdWithoutRefactoring("root"); + + // act + rootNode.setAuxiliaryData(persistentFooKey, "text"); + + // assert + model.detachView(&view); + model2.attachView(&view); + ASSERT_THAT(rootNode2.auxiliaryData(), + Contains(AuxiliaryProperty(AuxiliaryDataType::Persistent, "foo", "text"))); +} + +TEST_F(AuxiliaryPropertyStorageView, store_color_auxiliary_property) +{ + // arrange + model.attachView(&view); + rootNode.setIdWithoutRefactoring("root"); + rootNode2.setIdWithoutRefactoring("root"); + QColor color{Qt::red}; + + // act + rootNode.setAuxiliaryData(persistentFooKey, color); + + // assert + model.detachView(&view); + model2.attachView(&view); + ASSERT_THAT(rootNode2.auxiliaryData(), + Contains(AuxiliaryProperty(AuxiliaryDataType::Persistent, "foo", color))); +} + +TEST_F(AuxiliaryPropertyStorageView, do_not_store_temporary_auxiliary_property) +{ + // arrange + model.attachView(&view); + rootNode.setIdWithoutRefactoring("root"); + rootNode2.setIdWithoutRefactoring("root"); + + // act + rootNode.setAuxiliaryData(temporaryFooKey, "text"); + + // assert + model.detachView(&view); + model2.attachView(&view); + ASSERT_THAT(rootNode2.auxiliaryData(), + Not(Contains(AuxiliaryProperty(AuxiliaryDataType::Temporary, "foo", "text")))); +} + +TEST_F(AuxiliaryPropertyStorageView, do_not_load_node_without_id) +{ + // arrange + model.attachView(&view); + rootNode.setIdWithoutRefactoring("root"); + + // act + rootNode.setAuxiliaryData(persistentFooKey, "text"); + + // assert + model.detachView(&view); + model2.attachView(&view); + ASSERT_THAT(rootNode2.auxiliaryData(), + Not(Contains(AuxiliaryProperty(AuxiliaryDataType::Persistent, "foo", "text")))); +} + +TEST_F(AuxiliaryPropertyStorageView, do_not_store_node_without_id) +{ + // arrange + model.attachView(&view); + rootNode2.setIdWithoutRefactoring("root"); + + // act + rootNode.setAuxiliaryData(persistentFooKey, "text"); + + // assert + model.detachView(&view); + model2.attachView(&view); + ASSERT_THAT(rootNode2.auxiliaryData(), + Not(Contains(AuxiliaryProperty(AuxiliaryDataType::Persistent, "foo", "text")))); +} + +TEST_F(AuxiliaryPropertyStorageView, do_not_load_from_different_document) +{ + // arrange + model2.setFileUrl(QUrl{"/path/foo2.qml"}); + model.attachView(&view); + rootNode.setIdWithoutRefactoring("root"); + rootNode2.setIdWithoutRefactoring("root"); + + // act + rootNode.setAuxiliaryData(persistentFooKey, "text"); + + // assert + model.detachView(&view); + model2.attachView(&view); + ASSERT_THAT(rootNode2.auxiliaryData(), + Not(Contains(AuxiliaryProperty(AuxiliaryDataType::Persistent, "foo", "text")))); +} + +TEST_F(AuxiliaryPropertyStorageView, removing_node_erases_entries) +{ + // arrange + auto node = model.createModelNode("Item"); + node.setIdWithoutRefactoring("node1"); + node.setAuxiliaryData(persistentFooKey, "some value"); + auto node2 = model2.createModelNode("Item"); + node2.setIdWithoutRefactoring("node1"); + model.attachView(&view); + rootNode.setIdWithoutRefactoring("root"); + rootNode2.setIdWithoutRefactoring("root"); + + // act + node.destroy(); + + // assert + model.detachView(&view); + model2.attachView(&view); + ASSERT_THAT(node2.auxiliaryData(), + Not(Contains(AuxiliaryProperty(AuxiliaryDataType::Persistent, "foo", "some value")))); +} + +TEST_F(AuxiliaryPropertyStorageView, changing_node_id_is_saving_data_under_new_id) +{ + // arrange + model.attachView(&view); + rootNode.setIdWithoutRefactoring("root"); + rootNode2.setIdWithoutRefactoring("root2"); + rootNode.setAuxiliaryData(persistentFooKey, "text"); + model.detachView(&view); + model.attachView(&view); + + // act + rootNode.setIdWithoutRefactoring("root2"); + + // assert + model.detachView(&view); + model2.attachView(&view); + ASSERT_THAT(rootNode2.auxiliaryData(), + Contains(AuxiliaryProperty(AuxiliaryDataType::Persistent, "foo", "text"))); +} + +TEST_F(AuxiliaryPropertyStorageView, changing_node_id_is_removing_data_from_old_id) +{ + // arrange + model.attachView(&view); + rootNode.setIdWithoutRefactoring("root"); + rootNode2.setIdWithoutRefactoring("root"); + rootNode.setAuxiliaryData(persistentFooKey, "text"); + model.detachView(&view); + model.attachView(&view); + + // act + rootNode.setIdWithoutRefactoring("root2"); + + // assert + model.detachView(&view); + model2.attachView(&view); + ASSERT_THAT(rootNode2.auxiliaryData(), + Not(Contains(AuxiliaryProperty(AuxiliaryDataType::Persistent, "foo", "text")))); +} +} // namespace diff --git a/tests/unit/tests/utils/googletest.h b/tests/unit/tests/utils/googletest.h index 7da860f7ffa..2692eecece3 100644 --- a/tests/unit/tests/utils/googletest.h +++ b/tests/unit/tests/utils/googletest.h @@ -9,10 +9,12 @@ #include "conditionally-disabled-tests.h" -#include "../printers/gtest-creator-printing.h" -#include "../printers/gtest-qt-printing.h" #include "../printers/gtest-std-printing.h" +#include "../printers/gtest-qt-printing.h" + +#include "../printers/gtest-creator-printing.h" + #include "../utils/google-using-declarations.h" #include "../matchers/unittest-matchers.h"