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()
}
tooltip: qsTr("Change the type of this item.")
enabled: !modelNodeBackend.multiSelection
}
ExpressionTextField {
@@ -118,15 +119,18 @@ Rectangle {
Layout.fillWidth: true
showTranslateCheckBox: false
showExtendedFunctionButton: false
enabled: !modelNodeBackend.multiSelection
}
// workaround: without this item the lineedit does not shrink to the
// right size after resizing to a wider width
Image {
visible: !modelNodeBackend.multiSelection
Layout.preferredWidth: 16
Layout.preferredHeight: 16
source: hasAliasExport ? "image://icons/alias-export-checked" : "image://icons/alias-export-unchecked"
ToolTipArea {
enabled: !modelNodeBackend.multiSelection
anchors.fill: parent
onClicked: toogleExportAlias()
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 <qmljs/qmljssimplereader.h>
#include <utils/qtcassert.h>
#include <utils/algorithm.h>
#include <utils/fileutils.h>
@@ -281,13 +282,17 @@ void PropertyEditorQmlBackend::setup(const QmlObjectNode &qmlObjectNode, const Q
setupLayoutAttachedProperties(qmlObjectNode, propertyEditor);
// model node
m_backendModelNode.setup(qmlObjectNode.modelNode());
context()->setContextProperty(QLatin1String("modelNodeBackend"), &m_backendModelNode);
// className
auto valueObject = qobject_cast<PropertyEditorValue*>(variantToQObject(m_backendValuesPropertyMap.value(QLatin1String("className"))));
if (!valueObject)
valueObject = new PropertyEditorValue(&m_backendValuesPropertyMap);
valueObject->setName("className");
valueObject->setModelNode(qmlObjectNode.modelNode());
valueObject->setValue(qmlObjectNode.modelNode().simplifiedTypeName());
valueObject->setValue(m_backendModelNode.simplifiedTypeName());
QObject::connect(valueObject, &PropertyEditorValue::valueChanged, &backendValuesPropertyMap(), &DesignerPropertyMap::valueChanged);
m_backendValuesPropertyMap.insert(QLatin1String("className"), QVariant::fromValue(valueObject));
@@ -296,7 +301,7 @@ void PropertyEditorQmlBackend::setup(const QmlObjectNode &qmlObjectNode, const Q
if (!valueObject)
valueObject = new PropertyEditorValue(&m_backendValuesPropertyMap);
valueObject->setName("id");
valueObject->setValue(qmlObjectNode.id());
valueObject->setValue(m_backendModelNode.nodeId());
QObject::connect(valueObject, &PropertyEditorValue::valueChanged, &backendValuesPropertyMap(), &DesignerPropertyMap::valueChanged);
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();
// model node
m_backendModelNode.setup(qmlObjectNode.modelNode());
context()->setContextProperty(QLatin1String("modelNodeBackend"), &m_backendModelNode);
qCInfo(propertyEditorBenchmark) << "context:" << time.elapsed();
contextObject()->setSpecificsUrl(qmlSpecificsFile);
@@ -402,7 +403,7 @@ QString PropertyEditorQmlBackend::propertyEditorResourcesPath() {
QString PropertyEditorQmlBackend::templateGeneration(const NodeMetaInfo &type,
const NodeMetaInfo &superType,
const QmlObjectNode &objectNode)
const QmlObjectNode &node)
{
if (!templateConfiguration() || !templateConfiguration()->isValid())
return QString();
@@ -411,7 +412,7 @@ QString PropertyEditorQmlBackend::templateGeneration(const NodeMetaInfo &type,
QString qmlTemplate = imports.join(QLatin1Char('\n')) + QLatin1Char('\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");
QList<PropertyName> orderedList = type.propertyNames();
@@ -429,8 +430,8 @@ QString PropertyEditorQmlBackend::templateGeneration(const NodeMetaInfo &type,
TypeName typeName = type.propertyTypeName(name);
//alias resolution only possible with instance
if (typeName == "alias" && objectNode.isValid())
typeName = objectNode.instanceType(name);
if (typeName == "alias" && node.isValid())
typeName = node.instanceType(name);
if (!superType.hasProperty(name) && type.propertyIsWritable(name) && !name.contains(".")) {
foreach (const QmlJS::SimpleReaderNode::Ptr &node, templateConfiguration()->children())
@@ -469,6 +470,34 @@ TypeName PropertyEditorQmlBackend::fixTypeNameForPanes(const TypeName &typeName)
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)
{
const TypeName fixedTypeName = fixTypeNameForPanes(nodeInfo.typeName());
@@ -526,10 +555,10 @@ void PropertyEditorQmlBackend::setValueforLayoutAttachedProperties(const QmlObje
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()) {
foreach (const NodeMetaInfo &info, modelNode.metaInfo().classHierarchy()) {
if (metaInfo.isValid()) {
foreach (const NodeMetaInfo &info, metaInfo.classHierarchy()) {
QUrl fileUrl = fileToUrl(locateQmlFile(info, QString::fromUtf8(qmlFileName(info))));
if (fileUrl.isValid()) {
className = info.typeName();

View File

@@ -68,11 +68,10 @@ public:
PropertyEditorValue *propertyValueForName(const QString &propertyName);
static QString propertyEditorResourcesPath();
static QString templateGeneration(const NodeMetaInfo &type, const NodeMetaInfo &superType,
const QmlObjectNode &objectNode);
static QString templateGeneration(const NodeMetaInfo &type, const NodeMetaInfo &superType, const QmlObjectNode &node);
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);
@@ -83,6 +82,8 @@ public:
void setupLayoutAttachedProperties(const QmlObjectNode &qmlObjectNode, PropertyEditorView *propertyEditor);
static NodeMetaInfo findCommonAncestor(const ModelNode &node);
private:
void createPropertyEditorValue(const QmlObjectNode &qmlObjectNode,
const PropertyName &name, const QVariant &value,

View File

@@ -213,20 +213,13 @@ void PropertyEditorView::changeValue(const QString &name)
castedValue = QVariant(newColor);
}
try {
if (!value->value().isValid()) { //reset
qmlObjectNode.removeProperty(propertyName);
removePropertyFromModel(propertyName);
} else {
if (castedValue.isValid() && !castedValue.isNull()) {
m_locked = true;
qmlObjectNode.setVariantProperty(propertyName, castedValue);
m_locked = false;
commitVariantValueToModel(propertyName, castedValue);
}
}
}
catch (const RewritingException &e) {
e.showException();
}
}
void PropertyEditorView::changeExpression(const QString &propertyName)
@@ -446,13 +439,16 @@ void PropertyEditorView::resetView()
void PropertyEditorView::setupQmlBackend()
{
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;
TypeName diffClassName;
if (m_selectedNode.isValid()) {
diffClassName = m_selectedNode.metaInfo().typeName();
foreach (const NodeMetaInfo &metaInfo, m_selectedNode.metaInfo().classHierarchy()) {
if (commonAncestor.isValid()) {
diffClassName = commonAncestor.typeName();
foreach (const NodeMetaInfo &metaInfo, commonAncestor.classHierarchy()) {
if (PropertyEditorQmlBackend::checkIfUrlExists(qmlSpecificsFile))
break;
qmlSpecificsFile = PropertyEditorQmlBackend::getQmlFileUrl(metaInfo.typeName() + "Specifics", metaInfo);
@@ -465,8 +461,8 @@ void PropertyEditorView::setupQmlBackend()
QString specificQmlData;
if (m_selectedNode.isValid() && m_selectedNode.metaInfo().isValid() && diffClassName != m_selectedNode.type())
specificQmlData = PropertyEditorQmlBackend::templateGeneration(m_selectedNode.metaInfo(), model()->metaInfo(diffClassName), m_selectedNode);
if (commonAncestor.isValid() && m_selectedNode.metaInfo().isValid() && diffClassName != m_selectedNode.type())
specificQmlData = PropertyEditorQmlBackend::templateGeneration(commonAncestor, model()->metaInfo(diffClassName), m_selectedNode);
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,
const QList<ModelNode> &lastSelectedNodeList)
{
Q_UNUSED(lastSelectedNodeList);
if (selectedNodeList.isEmpty() || selectedNodeList.count() > 1)
if (selectedNodeList.isEmpty())
select(ModelNode());
else if (m_selectedNode != selectedNodeList.constFirst())
else
select(selectedNodeList.constFirst());
}

View File

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

View File

@@ -23,6 +23,7 @@
**
****************************************************************************/
#include "abstractview.h"
#include "qmlmodelnodeproxy.h"
#include <QtQml>
@@ -66,4 +67,34 @@ ModelNode QmlModelNodeProxy::modelNode() const
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

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