QmlDesigner: Add multi-selection to property editor

This implements basic multi selection for the property editor.
The property editor shows the most common type.
Values in the property editor show the values of the item that
was selected first.

Task-number: QDS-324
Change-Id: I5f03fa5aa9cfb0a0abaf285a29bf5f7e931635e5
Reviewed-by: Tim Jenssen <tim.jenssen@qt.io>
This commit is contained in:
Thomas Hartmann
2019-06-03 15:43:16 +02:00
parent c911be190f
commit 218c1e3769
7 changed files with 145 additions and 37 deletions

View File

@@ -76,6 +76,7 @@ Rectangle {
typeLineEdit.forceActiveFocus() typeLineEdit.forceActiveFocus()
} }
tooltip: qsTr("Change the type of this item.") tooltip: qsTr("Change the type of this item.")
enabled: !modelNodeBackend.multiSelection
} }
ExpressionTextField { ExpressionTextField {
@@ -118,15 +119,18 @@ Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
showTranslateCheckBox: false showTranslateCheckBox: false
showExtendedFunctionButton: false showExtendedFunctionButton: false
enabled: !modelNodeBackend.multiSelection
} }
// workaround: without this item the lineedit does not shrink to the // workaround: without this item the lineedit does not shrink to the
// right size after resizing to a wider width // right size after resizing to a wider width
Image { Image {
visible: !modelNodeBackend.multiSelection
Layout.preferredWidth: 16 Layout.preferredWidth: 16
Layout.preferredHeight: 16 Layout.preferredHeight: 16
source: hasAliasExport ? "image://icons/alias-export-checked" : "image://icons/alias-export-unchecked" source: hasAliasExport ? "image://icons/alias-export-checked" : "image://icons/alias-export-unchecked"
ToolTipArea { ToolTipArea {
enabled: !modelNodeBackend.multiSelection
anchors.fill: parent anchors.fill: parent
onClicked: toogleExportAlias() onClicked: toogleExportAlias()
tooltip: qsTr("Toggles whether this item is exported as an alias property of the root item.") tooltip: qsTr("Toggles whether this item is exported as an alias property of the root item.")

View File

@@ -40,6 +40,7 @@
#include <coreplugin/icore.h> #include <coreplugin/icore.h>
#include <qmljs/qmljssimplereader.h> #include <qmljs/qmljssimplereader.h>
#include <utils/qtcassert.h>
#include <utils/algorithm.h> #include <utils/algorithm.h>
#include <utils/fileutils.h> #include <utils/fileutils.h>
@@ -281,13 +282,17 @@ void PropertyEditorQmlBackend::setup(const QmlObjectNode &qmlObjectNode, const Q
setupLayoutAttachedProperties(qmlObjectNode, propertyEditor); setupLayoutAttachedProperties(qmlObjectNode, propertyEditor);
// model node
m_backendModelNode.setup(qmlObjectNode.modelNode());
context()->setContextProperty(QLatin1String("modelNodeBackend"), &m_backendModelNode);
// className // className
auto valueObject = qobject_cast<PropertyEditorValue*>(variantToQObject(m_backendValuesPropertyMap.value(QLatin1String("className")))); auto valueObject = qobject_cast<PropertyEditorValue*>(variantToQObject(m_backendValuesPropertyMap.value(QLatin1String("className"))));
if (!valueObject) if (!valueObject)
valueObject = new PropertyEditorValue(&m_backendValuesPropertyMap); valueObject = new PropertyEditorValue(&m_backendValuesPropertyMap);
valueObject->setName("className"); valueObject->setName("className");
valueObject->setModelNode(qmlObjectNode.modelNode()); valueObject->setModelNode(qmlObjectNode.modelNode());
valueObject->setValue(qmlObjectNode.modelNode().simplifiedTypeName()); valueObject->setValue(m_backendModelNode.simplifiedTypeName());
QObject::connect(valueObject, &PropertyEditorValue::valueChanged, &backendValuesPropertyMap(), &DesignerPropertyMap::valueChanged); QObject::connect(valueObject, &PropertyEditorValue::valueChanged, &backendValuesPropertyMap(), &DesignerPropertyMap::valueChanged);
m_backendValuesPropertyMap.insert(QLatin1String("className"), QVariant::fromValue(valueObject)); m_backendValuesPropertyMap.insert(QLatin1String("className"), QVariant::fromValue(valueObject));
@@ -296,7 +301,7 @@ void PropertyEditorQmlBackend::setup(const QmlObjectNode &qmlObjectNode, const Q
if (!valueObject) if (!valueObject)
valueObject = new PropertyEditorValue(&m_backendValuesPropertyMap); valueObject = new PropertyEditorValue(&m_backendValuesPropertyMap);
valueObject->setName("id"); valueObject->setName("id");
valueObject->setValue(qmlObjectNode.id()); valueObject->setValue(m_backendModelNode.nodeId());
QObject::connect(valueObject, &PropertyEditorValue::valueChanged, &backendValuesPropertyMap(), &DesignerPropertyMap::valueChanged); QObject::connect(valueObject, &PropertyEditorValue::valueChanged, &backendValuesPropertyMap(), &DesignerPropertyMap::valueChanged);
m_backendValuesPropertyMap.insert(QLatin1String("id"), QVariant::fromValue(valueObject)); m_backendValuesPropertyMap.insert(QLatin1String("id"), QVariant::fromValue(valueObject));
@@ -310,10 +315,6 @@ void PropertyEditorQmlBackend::setup(const QmlObjectNode &qmlObjectNode, const Q
qCInfo(propertyEditorBenchmark) << "anchors:" << time.elapsed(); qCInfo(propertyEditorBenchmark) << "anchors:" << time.elapsed();
// model node
m_backendModelNode.setup(qmlObjectNode.modelNode());
context()->setContextProperty(QLatin1String("modelNodeBackend"), &m_backendModelNode);
qCInfo(propertyEditorBenchmark) << "context:" << time.elapsed(); qCInfo(propertyEditorBenchmark) << "context:" << time.elapsed();
contextObject()->setSpecificsUrl(qmlSpecificsFile); contextObject()->setSpecificsUrl(qmlSpecificsFile);
@@ -402,7 +403,7 @@ QString PropertyEditorQmlBackend::propertyEditorResourcesPath() {
QString PropertyEditorQmlBackend::templateGeneration(const NodeMetaInfo &type, QString PropertyEditorQmlBackend::templateGeneration(const NodeMetaInfo &type,
const NodeMetaInfo &superType, const NodeMetaInfo &superType,
const QmlObjectNode &objectNode) const QmlObjectNode &node)
{ {
if (!templateConfiguration() || !templateConfiguration()->isValid()) if (!templateConfiguration() || !templateConfiguration()->isValid())
return QString(); return QString();
@@ -411,7 +412,7 @@ QString PropertyEditorQmlBackend::templateGeneration(const NodeMetaInfo &type,
QString qmlTemplate = imports.join(QLatin1Char('\n')) + QLatin1Char('\n'); QString qmlTemplate = imports.join(QLatin1Char('\n')) + QLatin1Char('\n');
qmlTemplate += QStringLiteral("Section {\n"); qmlTemplate += QStringLiteral("Section {\n");
qmlTemplate += QStringLiteral("caption: \"%1\"\n").arg(objectNode.modelNode().simplifiedTypeName()); qmlTemplate += QStringLiteral("caption: \"%1\"\n").arg(QString::fromUtf8(type.simplifiedTypeName()));
qmlTemplate += QStringLiteral("SectionLayout {\n"); qmlTemplate += QStringLiteral("SectionLayout {\n");
QList<PropertyName> orderedList = type.propertyNames(); QList<PropertyName> orderedList = type.propertyNames();
@@ -429,8 +430,8 @@ QString PropertyEditorQmlBackend::templateGeneration(const NodeMetaInfo &type,
TypeName typeName = type.propertyTypeName(name); TypeName typeName = type.propertyTypeName(name);
//alias resolution only possible with instance //alias resolution only possible with instance
if (typeName == "alias" && objectNode.isValid()) if (typeName == "alias" && node.isValid())
typeName = objectNode.instanceType(name); typeName = node.instanceType(name);
if (!superType.hasProperty(name) && type.propertyIsWritable(name) && !name.contains(".")) { if (!superType.hasProperty(name) && type.propertyIsWritable(name) && !name.contains(".")) {
foreach (const QmlJS::SimpleReaderNode::Ptr &node, templateConfiguration()->children()) foreach (const QmlJS::SimpleReaderNode::Ptr &node, templateConfiguration()->children())
@@ -469,6 +470,34 @@ TypeName PropertyEditorQmlBackend::fixTypeNameForPanes(const TypeName &typeName)
return fixedTypeName; return fixedTypeName;
} }
static NodeMetaInfo findCommonSuperClass(const NodeMetaInfo &first, const NodeMetaInfo &second)
{
for (const NodeMetaInfo &info : first.superClasses()) {
if (second.isSubclassOf(info.typeName()))
return info;
}
return first;
}
NodeMetaInfo PropertyEditorQmlBackend::findCommonAncestor(const ModelNode &node)
{
QTC_ASSERT(node.isValid(), return {});
QTC_ASSERT(node.metaInfo().isValid(), return {});
AbstractView *view = node.view();
if (view->selectedModelNodes().count() > 1) {
NodeMetaInfo commonClass = node.metaInfo();
for (const ModelNode &currentNode : view->selectedModelNodes()) {
if (currentNode.metaInfo().isValid() && !currentNode.isSubclassOf(commonClass.typeName(), -1, -1))
commonClass = findCommonSuperClass(currentNode.metaInfo(), commonClass);
}
return commonClass;
}
return node.metaInfo();
}
TypeName PropertyEditorQmlBackend::qmlFileName(const NodeMetaInfo &nodeInfo) TypeName PropertyEditorQmlBackend::qmlFileName(const NodeMetaInfo &nodeInfo)
{ {
const TypeName fixedTypeName = fixTypeNameForPanes(nodeInfo.typeName()); const TypeName fixedTypeName = fixTypeNameForPanes(nodeInfo.typeName());
@@ -526,10 +555,10 @@ void PropertyEditorQmlBackend::setValueforLayoutAttachedProperties(const QmlObje
setValue(qmlObjectNode, name, properDefaultLayoutAttachedProperties(qmlObjectNode, propertyName)); setValue(qmlObjectNode, name, properDefaultLayoutAttachedProperties(qmlObjectNode, propertyName));
} }
QUrl PropertyEditorQmlBackend::getQmlUrlForModelNode(const ModelNode &modelNode, TypeName &className) QUrl PropertyEditorQmlBackend::getQmlUrlForMetaInfo(const NodeMetaInfo &metaInfo, TypeName &className)
{ {
if (modelNode.isValid()) { if (metaInfo.isValid()) {
foreach (const NodeMetaInfo &info, modelNode.metaInfo().classHierarchy()) { foreach (const NodeMetaInfo &info, metaInfo.classHierarchy()) {
QUrl fileUrl = fileToUrl(locateQmlFile(info, QString::fromUtf8(qmlFileName(info)))); QUrl fileUrl = fileToUrl(locateQmlFile(info, QString::fromUtf8(qmlFileName(info))));
if (fileUrl.isValid()) { if (fileUrl.isValid()) {
className = info.typeName(); className = info.typeName();

View File

@@ -68,11 +68,10 @@ public:
PropertyEditorValue *propertyValueForName(const QString &propertyName); PropertyEditorValue *propertyValueForName(const QString &propertyName);
static QString propertyEditorResourcesPath(); static QString propertyEditorResourcesPath();
static QString templateGeneration(const NodeMetaInfo &type, const NodeMetaInfo &superType, static QString templateGeneration(const NodeMetaInfo &type, const NodeMetaInfo &superType, const QmlObjectNode &node);
const QmlObjectNode &objectNode);
static QUrl getQmlFileUrl(const TypeName &relativeTypeName, const NodeMetaInfo &info = NodeMetaInfo()); static QUrl getQmlFileUrl(const TypeName &relativeTypeName, const NodeMetaInfo &info = NodeMetaInfo());
static QUrl getQmlUrlForModelNode(const ModelNode &modelNode, TypeName &className); static QUrl getQmlUrlForMetaInfo(const NodeMetaInfo &modelNode, TypeName &className);
static bool checkIfUrlExists(const QUrl &url); static bool checkIfUrlExists(const QUrl &url);
@@ -83,6 +82,8 @@ public:
void setupLayoutAttachedProperties(const QmlObjectNode &qmlObjectNode, PropertyEditorView *propertyEditor); void setupLayoutAttachedProperties(const QmlObjectNode &qmlObjectNode, PropertyEditorView *propertyEditor);
static NodeMetaInfo findCommonAncestor(const ModelNode &node);
private: private:
void createPropertyEditorValue(const QmlObjectNode &qmlObjectNode, void createPropertyEditorValue(const QmlObjectNode &qmlObjectNode,
const PropertyName &name, const QVariant &value, const PropertyName &name, const QVariant &value,

View File

@@ -213,20 +213,13 @@ void PropertyEditorView::changeValue(const QString &name)
castedValue = QVariant(newColor); castedValue = QVariant(newColor);
} }
try { if (!value->value().isValid()) { //reset
if (!value->value().isValid()) { //reset removePropertyFromModel(propertyName);
qmlObjectNode.removeProperty(propertyName); } else {
} else { if (castedValue.isValid() && !castedValue.isNull()) {
if (castedValue.isValid() && !castedValue.isNull()) { commitVariantValueToModel(propertyName, castedValue);
m_locked = true;
qmlObjectNode.setVariantProperty(propertyName, castedValue);
m_locked = false;
}
} }
} }
catch (const RewritingException &e) {
e.showException();
}
} }
void PropertyEditorView::changeExpression(const QString &propertyName) void PropertyEditorView::changeExpression(const QString &propertyName)
@@ -446,13 +439,16 @@ void PropertyEditorView::resetView()
void PropertyEditorView::setupQmlBackend() void PropertyEditorView::setupQmlBackend()
{ {
TypeName specificsClassName; TypeName specificsClassName;
QUrl qmlFile(PropertyEditorQmlBackend::getQmlUrlForModelNode(m_selectedNode, specificsClassName));
const NodeMetaInfo commonAncestor = PropertyEditorQmlBackend::findCommonAncestor(m_selectedNode);
const QUrl qmlFile(PropertyEditorQmlBackend::getQmlUrlForMetaInfo(commonAncestor, specificsClassName));
QUrl qmlSpecificsFile; QUrl qmlSpecificsFile;
TypeName diffClassName; TypeName diffClassName;
if (m_selectedNode.isValid()) { if (commonAncestor.isValid()) {
diffClassName = m_selectedNode.metaInfo().typeName(); diffClassName = commonAncestor.typeName();
foreach (const NodeMetaInfo &metaInfo, m_selectedNode.metaInfo().classHierarchy()) { foreach (const NodeMetaInfo &metaInfo, commonAncestor.classHierarchy()) {
if (PropertyEditorQmlBackend::checkIfUrlExists(qmlSpecificsFile)) if (PropertyEditorQmlBackend::checkIfUrlExists(qmlSpecificsFile))
break; break;
qmlSpecificsFile = PropertyEditorQmlBackend::getQmlFileUrl(metaInfo.typeName() + "Specifics", metaInfo); qmlSpecificsFile = PropertyEditorQmlBackend::getQmlFileUrl(metaInfo.typeName() + "Specifics", metaInfo);
@@ -465,8 +461,8 @@ void PropertyEditorView::setupQmlBackend()
QString specificQmlData; QString specificQmlData;
if (m_selectedNode.isValid() && m_selectedNode.metaInfo().isValid() && diffClassName != m_selectedNode.type()) if (commonAncestor.isValid() && m_selectedNode.metaInfo().isValid() && diffClassName != m_selectedNode.type())
specificQmlData = PropertyEditorQmlBackend::templateGeneration(m_selectedNode.metaInfo(), model()->metaInfo(diffClassName), m_selectedNode); specificQmlData = PropertyEditorQmlBackend::templateGeneration(commonAncestor, model()->metaInfo(diffClassName), m_selectedNode);
PropertyEditorQmlBackend *currentQmlBackend = m_qmlBackendHash.value(qmlFile.toString()); PropertyEditorQmlBackend *currentQmlBackend = m_qmlBackendHash.value(qmlFile.toString());
@@ -515,14 +511,51 @@ void PropertyEditorView::setupQmlBackend()
} }
void PropertyEditorView::commitVariantValueToModel(const PropertyName &propertyName, const QVariant &value)
{
m_locked = true;
try {
RewriterTransaction transaction = beginRewriterTransaction("PropertyEditorView::commitVariantValueToMode");
for (const ModelNode &node : m_selectedNode.view()->selectedModelNodes()) {
if (QmlObjectNode::isValidQmlObjectNode(node))
QmlObjectNode(node).setVariantProperty(propertyName, value);
}
transaction.commit();
}
catch (const RewritingException &e) {
e.showException();
}
m_locked = false;
}
void PropertyEditorView::removePropertyFromModel(const PropertyName &propertyName)
{
m_locked = true;
try {
RewriterTransaction transaction = beginRewriterTransaction("PropertyEditorView::removePropertyFromModel");
for (const ModelNode &node : m_selectedNode.view()->selectedModelNodes()) {
if (QmlObjectNode::isValidQmlObjectNode(node))
QmlObjectNode(node).removeProperty(propertyName);
}
transaction.commit();
}
catch (const RewritingException &e) {
e.showException();
}
m_locked = false;
}
void PropertyEditorView::selectedNodesChanged(const QList<ModelNode> &selectedNodeList, void PropertyEditorView::selectedNodesChanged(const QList<ModelNode> &selectedNodeList,
const QList<ModelNode> &lastSelectedNodeList) const QList<ModelNode> &lastSelectedNodeList)
{ {
Q_UNUSED(lastSelectedNodeList); Q_UNUSED(lastSelectedNodeList);
if (selectedNodeList.isEmpty() || selectedNodeList.count() > 1) if (selectedNodeList.isEmpty())
select(ModelNode()); select(ModelNode());
else if (m_selectedNode != selectedNodeList.constFirst()) else
select(selectedNodeList.constFirst()); select(selectedNodeList.constFirst());
} }

View File

@@ -110,6 +110,9 @@ private: //functions
void delayedResetView(); void delayedResetView();
void setupQmlBackend(); void setupQmlBackend();
void commitVariantValueToModel(const PropertyName &propertyName, const QVariant &value);
void removePropertyFromModel(const PropertyName &propertyName);
private: //variables private: //variables
ModelNode m_selectedNode; ModelNode m_selectedNode;
QWidget *m_parent; QWidget *m_parent;

View File

@@ -23,6 +23,7 @@
** **
****************************************************************************/ ****************************************************************************/
#include "abstractview.h"
#include "qmlmodelnodeproxy.h" #include "qmlmodelnodeproxy.h"
#include <QtQml> #include <QtQml>
@@ -66,4 +67,34 @@ ModelNode QmlModelNodeProxy::modelNode() const
return m_qmlItemNode.modelNode(); return m_qmlItemNode.modelNode();
} }
bool QmlModelNodeProxy::multiSelection() const
{
if (!m_qmlItemNode.isValid())
return false;
return m_qmlItemNode.view()->selectedModelNodes().count() > 1;
}
QString QmlModelNodeProxy::nodeId() const
{
if (!m_qmlItemNode.isValid())
return {};
if (multiSelection())
return tr("multiselection");
return m_qmlItemNode.id();
}
QString QmlModelNodeProxy::simplifiedTypeName() const
{
if (!m_qmlItemNode.isValid())
return {};
if (multiSelection())
return tr("multiselection");
return m_qmlItemNode.simplifiedTypeName();
}
} }

View File

@@ -35,7 +35,8 @@ class QmlModelNodeProxy : public QObject
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(QmlDesigner::ModelNode modelNode READ modelNode NOTIFY modelNodeChanged) Q_PROPERTY(QmlDesigner::ModelNode modelNode READ modelNode NOTIFY modelNodeChanged)
Q_PROPERTY(bool multiSelection READ multiSelection NOTIFY modelNodeChanged)
public: public:
explicit QmlModelNodeProxy(QObject *parent = nullptr); explicit QmlModelNodeProxy(QObject *parent = nullptr);
@@ -51,6 +52,12 @@ public:
ModelNode modelNode() const; ModelNode modelNode() const;
bool multiSelection() const;
QString nodeId() const;
QString simplifiedTypeName() const;
signals: signals:
void modelNodeChanged(); void modelNodeChanged();
void selectionToBeChanged(); void selectionToBeChanged();