From cf82b8e685c112fef4170f72eacacb5ca5363b6c Mon Sep 17 00:00:00 2001 From: Thomas Hartmann Date: Wed, 21 Feb 2018 18:41:31 +0100 Subject: [PATCH] QmlDesigner: Allow annotations in comments This patch allows to store the auxiliary data of model nodes as meta data in the QML file. The meta data is encoded in a comment at the end of the QML file. By default such meta data is attached to the clipboard. Change-Id: I794d2c1297d270c5c1099c6c1be98b6b7a7f650b Reviewed-by: Tim Jenssen --- .../integration/designdocumentview.cpp | 5 +- .../designercore/include/rewriterview.h | 6 + .../designercore/model/modelmerger.cpp | 10 +- .../designercore/model/rewriterview.cpp | 160 ++++++++++++++++++ .../qmldesigner/coretests/testrewriterview.h | 2 + .../qmldesigner/coretests/tst_testcore.cpp | 119 +++++++++++++ .../qml/qmldesigner/coretests/tst_testcore.h | 4 + 7 files changed, 304 insertions(+), 2 deletions(-) diff --git a/src/plugins/qmldesigner/components/integration/designdocumentview.cpp b/src/plugins/qmldesigner/components/integration/designdocumentview.cpp index c30a392cef0..bc8930edb6d 100644 --- a/src/plugins/qmldesigner/components/integration/designdocumentview.cpp +++ b/src/plugins/qmldesigner/components/integration/designdocumentview.cpp @@ -130,8 +130,9 @@ QString DesignDocumentView::toText() const ModelNode rewriterNode(rewriterView->rootModelNode()); + rewriterView->writeAuxiliaryData(); + return rewriterView->extractText({rewriterNode}).value(rewriterNode) + rewriterView->getRawAuxiliaryData(); //get the text of the root item without imports - return rewriterView->extractText({rewriterNode}).value(rewriterNode); } void DesignDocumentView::fromText(QString text) @@ -151,6 +152,8 @@ void DesignDocumentView::fromText(QString text) rewriterView->setTextModifier(&modifier); inputModel->setRewriterView(rewriterView.data()); + rewriterView->restoreAuxiliaryData(); + if (rewriterView->errors().isEmpty() && rewriterView->rootModelNode().isValid()) { ModelMerger merger(this); merger.replaceModel(rewriterView->rootModelNode()); diff --git a/src/plugins/qmldesigner/designercore/include/rewriterview.h b/src/plugins/qmldesigner/designercore/include/rewriterview.h index 4d4cfee8e68..3c73ec1fabc 100644 --- a/src/plugins/qmldesigner/designercore/include/rewriterview.h +++ b/src/plugins/qmldesigner/designercore/include/rewriterview.h @@ -163,6 +163,12 @@ public: void qmlTextChanged(); void delayedSetup(); + void writeAuxiliaryData(); + void restoreAuxiliaryData(); + + QString getRawAuxiliaryData() const; + QString auxiliaryDataAsQML() const; + protected: // functions void importAdded(const Import &import); void importRemoved(const Import &import); diff --git a/src/plugins/qmldesigner/designercore/model/modelmerger.cpp b/src/plugins/qmldesigner/designercore/model/modelmerger.cpp index a1569322373..161cf1f9f52 100644 --- a/src/plugins/qmldesigner/designercore/model/modelmerger.cpp +++ b/src/plugins/qmldesigner/designercore/model/modelmerger.cpp @@ -60,6 +60,13 @@ static void syncVariantProperties(ModelNode &outputNode, const ModelNode &inputN } } +static void syncAuxiliaryProperties(ModelNode &outputNode, const ModelNode &inputNode) +{ + auto tmp = inputNode.auxiliaryData(); + for (auto iter = tmp.begin(); iter != tmp.end(); ++iter) + outputNode.setAuxiliaryData(iter.key(), iter.value()); +} + static void syncBindingProperties(ModelNode &outputNode, const ModelNode &inputNode, const QHash &idRenamingHash) { foreach (const BindingProperty &bindingProperty, inputNode.bindingProperties()) { @@ -138,6 +145,7 @@ static ModelNode createNodeFromNode(const ModelNode &modelNode,const QHashmodel()->metaInfo(modelNode.type()); ModelNode newNode(view->createModelNode(modelNode.type(), nodeMetaInfo.majorVersion(), nodeMetaInfo.minorVersion(), propertyList, variantPropertyList, modelNode.nodeSource(), modelNode.nodeSourceType())); + syncAuxiliaryProperties(newNode, modelNode); syncBindingProperties(newNode, modelNode, idRenamingHash); syncId(newNode, modelNode, idRenamingHash); syncNodeProperties(newNode, modelNode, idRenamingHash, view); @@ -165,7 +173,6 @@ ModelNode ModelMerger::insertModel(const ModelNode &modelNode) return newNode; } - void ModelMerger::replaceModel(const ModelNode &modelNode) { view()->model()->changeImports(modelNode.model()->imports(), {}); @@ -182,6 +189,7 @@ void ModelMerger::replaceModel(const ModelNode &modelNode) QHash idRenamingHash; setupIdRenamingHash(modelNode, idRenamingHash, view()); + syncAuxiliaryProperties(rootNode, modelNode); syncVariantProperties(rootNode, modelNode); syncBindingProperties(rootNode, modelNode, idRenamingHash); syncId(rootNode, modelNode, idRenamingHash); diff --git a/src/plugins/qmldesigner/designercore/model/rewriterview.cpp b/src/plugins/qmldesigner/designercore/model/rewriterview.cpp index c4cb5bf7102..a5f036c49b3 100644 --- a/src/plugins/qmldesigner/designercore/model/rewriterview.cpp +++ b/src/plugins/qmldesigner/designercore/model/rewriterview.cpp @@ -42,11 +42,17 @@ #include #include +#include + +#include +#include using namespace QmlDesigner::Internal; namespace QmlDesigner { +const char annotationsEscapeSequence[] = "##^##"; + RewriterView::RewriterView(DifferenceHandling differenceHandling, QObject *parent): AbstractView(parent), m_differenceHandling(differenceHandling), @@ -442,6 +448,56 @@ void RewriterView::notifyErrorsAndWarnings(const QList &errors) emitDocumentMessage(errors, m_warnings); } +QString RewriterView::auxiliaryDataAsQML() const +{ + bool hasAuxData = false; + + QString str = "Designer {\n "; + + int columnCount = 0; + for (const auto node : allModelNodes()) { + QHash data = node.auxiliaryData(); + if (!data.isEmpty()) { + hasAuxData = true; + if (columnCount > 80) { + str += "\n"; + columnCount = 0; + } + const int startLen = str.length(); + str += "D{"; + str += "i:"; + str += QString::number(node.internalId()); + str += ";"; + + for (auto i = data.begin(); i != data.end(); ++i) { + const QVariant value = i.value(); + QString strValue = value.toString(); + if (value.type() == QMetaType::QString) + strValue = "\"" + strValue + "\""; + + if (!strValue.isEmpty()) { + str += QString::fromUtf8(i.key()) + ":"; + str += strValue; + str += ";"; + } + } + + if (str.back() == ';') + str.chop(1); + + str += "}"; + columnCount += str.length() - startLen; + } + } + + str += "\n}\n"; + + if (hasAuxData) + return str; + + return {}; +} + Internal::ModelNodePositionStorage *RewriterView::positionStorage() const { return m_positionStorage.data(); @@ -820,4 +876,108 @@ void RewriterView::delayedSetup() m_textToModelMerger->delayedSetup(); } +static QString annotationsEnd() +{ + const static QString end = QString(" %1*/\n").arg(annotationsEscapeSequence); + return end; +} + +static QString annotationsStart() +{ + const static QString start = QString("\n/*%1 ").arg(annotationsEscapeSequence); + return start; +} + +QString RewriterView::getRawAuxiliaryData() const +{ + QTC_ASSERT(m_textModifier, return {}); + + const QString oldText = m_textModifier->text(); + + QString newText = oldText; + + int startIndex = newText.indexOf(annotationsStart()); + int endIndex = newText.indexOf(annotationsEnd()); + + if (startIndex > 0 && endIndex > 0) + return newText.mid(startIndex, endIndex - startIndex + annotationsEnd().length()); + + return {}; +} + +void RewriterView::writeAuxiliaryData() +{ + QTC_ASSERT(m_textModifier, return); + + const QString oldText = m_textModifier->text(); + + QString newText = oldText; + + int startIndex = newText.indexOf(annotationsStart()); + int endIndex = newText.indexOf(annotationsEnd()); + + if (startIndex > 0 && endIndex > 0) + newText.remove(startIndex, endIndex - startIndex + annotationsEnd().length()); + + QString auxData = auxiliaryDataAsQML(); + + if (!auxData.isEmpty()) { + auxData.prepend(annotationsStart()); + auxData.append(annotationsEnd()); + newText.append(auxData); + + QTextCursor tc(m_textModifier->textDocument()); + Utils::ChangeSet changeSet; + changeSet.replace(0, oldText.length(), newText); + changeSet.apply(&tc); + } +} + +void checkNode(QmlJS::SimpleReaderNode::Ptr node, RewriterView *view); + +void static checkChildNodes(QmlJS::SimpleReaderNode::Ptr node, RewriterView *view) +{ + for (auto child : node->children()) + checkNode(child, view); +} +void static checkNode(QmlJS::SimpleReaderNode::Ptr node, RewriterView *view) +{ + if (!node) + return; + + if (!node->propertyNames().contains("i")) + return; + + const int internalId = node->property("i").toInt(); + const ModelNode modelNode = view->modelNodeForInternalId(internalId); + if (!modelNode.isValid()) + return; + + auto properties = node->properties(); + + for (auto i = properties.begin(); i != properties.end(); ++i) { + if (i.key() != "i") + modelNode.setAuxiliaryData(i.key().toUtf8(), i.value()); + } + + checkChildNodes(node, view); +} + +void RewriterView::restoreAuxiliaryData() +{ + QTC_ASSERT(m_textModifier, return); + + const QString text = m_textModifier->text(); + + int startIndex = text.indexOf(annotationsStart()); + int endIndex = text.indexOf(annotationsEnd()); + + if (startIndex > 0 && endIndex > 0) { + const QString auxSource = text.mid(startIndex + annotationsStart().length(), + endIndex - startIndex - annotationsStart().length()); + QmlJS::SimpleReader reader; + checkChildNodes(reader.readFromSource(auxSource), this); + } +} + } //QmlDesigner diff --git a/tests/auto/qml/qmldesigner/coretests/testrewriterview.h b/tests/auto/qml/qmldesigner/coretests/testrewriterview.h index 74b5fa384ac..46f7e1078f8 100644 --- a/tests/auto/qml/qmldesigner/coretests/testrewriterview.h +++ b/tests/auto/qml/qmldesigner/coretests/testrewriterview.h @@ -57,6 +57,8 @@ public: bool isModificationGroupActive() const; void setModificationGroupActive(bool active); void applyModificationGroupChanges(); + + using RewriterView::auxiliaryDataAsQML; }; } // QmlDesigner diff --git a/tests/auto/qml/qmldesigner/coretests/tst_testcore.cpp b/tests/auto/qml/qmldesigner/coretests/tst_testcore.cpp index 25eca2c3ce8..33b275046bf 100644 --- a/tests/auto/qml/qmldesigner/coretests/tst_testcore.cpp +++ b/tests/auto/qml/qmldesigner/coretests/tst_testcore.cpp @@ -59,6 +59,7 @@ #include #include +#include #include #include @@ -8208,5 +8209,123 @@ void tst_TestCore::changeGradientId() } } +void checkNode(QmlJS::SimpleReaderNode::Ptr node, TestRewriterView *view); + +void static checkChildNodes(QmlJS::SimpleReaderNode::Ptr node, TestRewriterView *view) +{ + for (auto child : node->children()) + checkNode(child, view); +} + +void static checkNode(QmlJS::SimpleReaderNode::Ptr node, TestRewriterView *view) +{ + QVERIFY(node); + QVERIFY(node->propertyNames().contains("i")); + const int internalId = node->property("i").toInt(); + const ModelNode modelNode = view->modelNodeForInternalId(internalId); + QVERIFY(modelNode.isValid()); + auto properties = node->properties(); + + for (auto i = properties.begin(); i != properties.end(); ++i) { + if (i.key() != "i") + QCOMPARE(i.value(), modelNode.auxiliaryData(i.key().toUtf8())); + } + + checkChildNodes(node, view); +} + +void tst_TestCore::writeAnnotations() +{ + const QLatin1String qmlCode("\n" + "import QtQuick 2.1\n" + "\n" + "Rectangle {\n" + " Item {\n" + " }\n" + "\n" + " MouseArea {\n" + " x: 3\n" + " y: 3\n" + " }\n" + "}"); + + const QLatin1String metaCode("\n/*##^## Designer {\n D{i:0;x:10}D{i:1;test:true;x:10;test2:\"string\"}" + "D{i:2;test:true;x:10;test2:\"string\"}\n}\n ##^##*/\n"); + + QPlainTextEdit textEdit; + textEdit.setPlainText(qmlCode); + NotIndentingTextEditModifier textModifier(&textEdit); + + QScopedPointer model(Model::create("QtQuick.Item", 2, 1)); + QVERIFY(model.data()); + + QScopedPointer testRewriterView(new TestRewriterView()); + testRewriterView->setTextModifier(&textModifier); + model->attachView(testRewriterView.data()); + + QVERIFY(model.data()); + ModelNode rootModelNode(testRewriterView->rootModelNode()); + QVERIFY(rootModelNode.isValid()); + + rootModelNode.setAuxiliaryData("x", 10); + for (const auto child : rootModelNode.allSubModelNodes()) { + child.setAuxiliaryData("x", 10); + child.setAuxiliaryData("test", true); + child.setAuxiliaryData("test2", "string"); + } + + const QString metaSource = testRewriterView->auxiliaryDataAsQML(); + + QmlJS::SimpleReader reader; + checkChildNodes(reader.readFromSource(metaSource), testRewriterView.data()); + + testRewriterView->writeAuxiliaryData(); + const QString textWithMeta = testRewriterView->textModifier()->text(); + testRewriterView->writeAuxiliaryData(); + QCOMPARE(textWithMeta.length(), testRewriterView->textModifier()->text().length()); +} + +void tst_TestCore::readAnnotations() +{ + const QLatin1String qmlCode("\n" + "import QtQuick 2.1\n" + "\n" + "Rectangle {\n" + " Item {\n" + " }\n" + "\n" + " MouseArea {\n" + " x: 3\n" + " y: 3\n" + " }\n" + "}"); + + const QLatin1String metaCode("\n/*##^## Designer {\n D{i:0;x:10}D{i:1;test:true;x:10;test2:\"string\"}" + "D{i:2;test:true;x:10;test2:\"string\"}\n}\n ##^##*/\n"); + + const QLatin1String metaCodeQmlCode("Designer {\n D{i:0;x:10}D{i:1;test2:\"string\";x:10;test:true}" + "D{i:2;test2:\"string\";x:10;test:true}\n}\n"); + + QPlainTextEdit textEdit; + textEdit.setPlainText(qmlCode + metaCode); + NotIndentingTextEditModifier textModifier(&textEdit); + + QScopedPointer model(Model::create("QtQuick.Item", 2, 1)); + QVERIFY(model.data()); + + QScopedPointer testRewriterView(new TestRewriterView()); + testRewriterView->setTextModifier(&textModifier); + model->attachView(testRewriterView.data()); + + QVERIFY(model.data()); + ModelNode rootModelNode(testRewriterView->rootModelNode()); + QVERIFY(rootModelNode.isValid()); + + testRewriterView->restoreAuxiliaryData(); + + const QString metaSource = testRewriterView->auxiliaryDataAsQML(); + QCOMPARE(metaSource.length(), QString(metaCodeQmlCode).length()); +} + QTEST_MAIN(tst_TestCore); diff --git a/tests/auto/qml/qmldesigner/coretests/tst_testcore.h b/tests/auto/qml/qmldesigner/coretests/tst_testcore.h index a3ce437947c..def024c63f3 100644 --- a/tests/auto/qml/qmldesigner/coretests/tst_testcore.h +++ b/tests/auto/qml/qmldesigner/coretests/tst_testcore.h @@ -229,4 +229,8 @@ private slots: // Object bindings as properties: void loadGradient(); void changeGradientId(); + + // QMLAnnotations + void writeAnnotations(); + void readAnnotations(); };