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 <tim.jenssen@qt.io>
This commit is contained in:
Thomas Hartmann
2018-02-21 18:41:31 +01:00
parent 5179dbbe68
commit cf82b8e685
7 changed files with 304 additions and 2 deletions

View File

@@ -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());

View File

@@ -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);

View File

@@ -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<QString, QString> &idRenamingHash)
{
foreach (const BindingProperty &bindingProperty, inputNode.bindingProperties()) {
@@ -138,6 +145,7 @@ static ModelNode createNodeFromNode(const ModelNode &modelNode,const QHash<QStri
NodeMetaInfo nodeMetaInfo = view->model()->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<QString, QString> idRenamingHash;
setupIdRenamingHash(modelNode, idRenamingHash, view());
syncAuxiliaryProperties(rootNode, modelNode);
syncVariantProperties(rootNode, modelNode);
syncBindingProperties(rootNode, modelNode, idRenamingHash);
syncId(rootNode, modelNode, idRenamingHash);

View File

@@ -42,11 +42,17 @@
#include <qmljs/parser/qmljsengine_p.h>
#include <qmljs/qmljsmodelmanagerinterface.h>
#include <qmljs/qmljssimplereader.h>
#include <utils/changeset.h>
#include <utils/qtcassert.h>
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<DocumentMessage> &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<PropertyName, QVariant> 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

View File

@@ -57,6 +57,8 @@ public:
bool isModificationGroupActive() const;
void setModificationGroupActive(bool active);
void applyModificationGroupChanges();
using RewriterView::auxiliaryDataAsQML;
};
} // QmlDesigner

View File

@@ -59,6 +59,7 @@
#include <utils/fileutils.h>
#include <qmljs/qmljsinterpreter.h>
#include <qmljs/qmljssimplereader.h>
#include <extensionsystem/pluginmanager.h>
#include <QPlainTextEdit>
@@ -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(Model::create("QtQuick.Item", 2, 1));
QVERIFY(model.data());
QScopedPointer<TestRewriterView> 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(Model::create("QtQuick.Item", 2, 1));
QVERIFY(model.data());
QScopedPointer<TestRewriterView> 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);

View File

@@ -229,4 +229,8 @@ private slots:
// Object bindings as properties:
void loadGradient();
void changeGradientId();
// QMLAnnotations
void writeAnnotations();
void readAnnotations();
};