forked from qt-creator/qt-creator
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:
@@ -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.")
|
||||
|
@@ -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 ¤tNode : 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();
|
||||
|
@@ -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,
|
||||
|
@@ -213,20 +213,13 @@ void PropertyEditorView::changeValue(const QString &name)
|
||||
castedValue = QVariant(newColor);
|
||||
}
|
||||
|
||||
try {
|
||||
if (!value->value().isValid()) { //reset
|
||||
qmlObjectNode.removeProperty(propertyName);
|
||||
} else {
|
||||
if (castedValue.isValid() && !castedValue.isNull()) {
|
||||
m_locked = true;
|
||||
qmlObjectNode.setVariantProperty(propertyName, castedValue);
|
||||
m_locked = false;
|
||||
}
|
||||
if (!value->value().isValid()) { //reset
|
||||
removePropertyFromModel(propertyName);
|
||||
} else {
|
||||
if (castedValue.isValid() && !castedValue.isNull()) {
|
||||
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());
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -35,7 +35,8 @@ class QmlModelNodeProxy : public QObject
|
||||
{
|
||||
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:
|
||||
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();
|
||||
|
Reference in New Issue
Block a user