QmlDesigner: Handle multiselection for Materials and textures

* Material Preview is removed for multiselected nodes
* Name field shows `multiselection`
* Showing material type in multiselection is handled
* Setting a type for multiselected materials is handled
* Renaming a material/texture changes the id as well

Task-number: QDS-14895
Change-Id: I61b6ebf8bf320f5a9e27e318f66d6823d9e91272
Reviewed-by: Miikka Heikkinen <miikka.heikkinen@qt.io>
Reviewed-by: Mahmoud Badri <mahmoud.badri@qt.io>
This commit is contained in:
Ali Kianian
2025-03-11 16:46:22 +02:00
parent fe2f389c38
commit c5f2f7984c
10 changed files with 150 additions and 54 deletions

View File

@@ -44,7 +44,6 @@ Rectangle {
image.source = "image://nodeInstance/preview" image.source = "image://nodeInstance/preview"
} }
Connections { Connections {
target: root.backend target: root.backend

View File

@@ -15,13 +15,15 @@ StudioControls.SplitView {
property Component previewComponent: null property Component previewComponent: null
width: parent.width width: parent.width
implicitHeight: showImage ? previewLoader.implicitHeight + nameSection.implicitHeight : nameSection.implicitHeight implicitHeight: showImage ? previewLoader.activeHeight + nameSection.implicitHeight : nameSection.implicitHeight
orientation: Qt.Vertical orientation: Qt.Vertical
Loader { Loader {
id: previewLoader id: previewLoader
property real activeHeight: previewLoader.active ? implicitHeight : 0
SplitView.fillWidth: true SplitView.fillWidth: true
SplitView.minimumWidth: 152 SplitView.minimumWidth: 152
SplitView.preferredHeight: previewLoader.visible ? Math.min(root.width * 0.75, 400) : 0 SplitView.preferredHeight: previewLoader.visible ? Math.min(root.width * 0.75, 400) : 0
@@ -58,6 +60,7 @@ StudioControls.SplitView {
placeholderText: qsTr("Material name") placeholderText: qsTr("Material name")
showTranslateCheckBox: false showTranslateCheckBox: false
showExtendedFunctionButton: false showExtendedFunctionButton: false
enabled: !hasMultiSelection
Timer { Timer {
running: true running: true

View File

@@ -6,6 +6,7 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import HelperWidgets 2.0 import HelperWidgets 2.0
import StudioControls 1.0 as StudioControls import StudioControls 1.0 as StudioControls
import StudioTheme 1.0 as StudioTheme
import "Material" as Material import "Material" as Material
Item { Item {
@@ -58,7 +59,7 @@ Item {
active: splitView.isHorizontal active: splitView.isHorizontal
visible: leftSideView.active && leftSideView.item visible: leftSideView.active && leftSideView.item
sourceComponent: PreviewComponent {} sourceComponent: hasMultiSelection ? blankPreview : preview
} }
PropertyEditorPane { PropertyEditorPane {
@@ -77,8 +78,8 @@ Item {
Component.onCompleted: topSection.restoreState(settings.topSection) Component.onCompleted: topSection.restoreState(settings.topSection)
Component.onDestruction: settings.topSection = topSection.saveState() Component.onDestruction: settings.topSection = topSection.saveState()
previewComponent: PreviewComponent {} previewComponent: preview
showImage: !splitView.isHorizontal showImage: !hasMultiSelection && !splitView.isHorizontal
} }
DynamicPropertiesSection { DynamicPropertiesSection {
@@ -119,7 +120,10 @@ Item {
} }
} }
component PreviewComponent : Material.Preview { Component {
id: preview
Material.Preview {
id: previewItem id: previewItem
pinned: settings.dockMode pinned: settings.dockMode
@@ -134,4 +138,13 @@ Item {
} }
} }
} }
}
Component {
id: blankPreview
Rectangle {
color: StudioTheme.Values.themePanelBackground
}
}
} }

View File

@@ -191,18 +191,15 @@ void PropertyEditorContextObject::changeTypeName(const QString &typeName)
QTC_ASSERT(!rewriterView->selectedModelNodes().isEmpty(), return); QTC_ASSERT(!rewriterView->selectedModelNodes().isEmpty(), return);
try { auto changeNodeTypeName = [&](ModelNode &selectedNode) {
auto transaction = RewriterTransaction(rewriterView, "PropertyEditorContextObject:changeTypeName");
ModelNode selectedNode = rewriterView->selectedModelNodes().constFirst();
// Check if the requested type is the same as already set // Check if the requested type is the same as already set
if (selectedNode.simplifiedTypeName() == typeName) if (selectedNode.simplifiedTypeName() == typeName)
return; return;
NodeMetaInfo metaInfo = m_model->metaInfo(typeName.toLatin1()); NodeMetaInfo metaInfo = m_model->metaInfo(typeName.toLatin1());
if (!metaInfo.isValid()) { if (!metaInfo.isValid()) {
Core::AsynchronousMessageBox::warning(tr("Invalid Type"), tr("%1 is an invalid type.").arg(typeName)); Core::AsynchronousMessageBox::warning(tr("Invalid Type"),
tr("%1 is an invalid type.").arg(typeName));
return; return;
} }
@@ -210,7 +207,9 @@ void PropertyEditorContextObject::changeTypeName(const QString &typeName)
auto propertiesAndSignals = Utils::transform<PropertyNameList>( auto propertiesAndSignals = Utils::transform<PropertyNameList>(
PropertyEditorUtils::filteredProperties(metaInfo), &PropertyMetaInfo::name); PropertyEditorUtils::filteredProperties(metaInfo), &PropertyMetaInfo::name);
// Add signals to the list // Add signals to the list
for (const auto &signal : metaInfo.signalNames()) {
const PropertyNameList &signalNames = metaInfo.signalNames();
for (const PropertyName &signal : signalNames) {
if (signal.isEmpty()) if (signal.isEmpty())
continue; continue;
@@ -222,7 +221,8 @@ void PropertyEditorContextObject::changeTypeName(const QString &typeName)
} }
// Add dynamic properties and respective change signals // Add dynamic properties and respective change signals
for (const auto &property : selectedNode.properties()) { const QList<AbstractProperty> &nodeProperties = selectedNode.properties();
for (const AbstractProperty &property : nodeProperties) {
if (!property.isDynamic()) if (!property.isDynamic())
continue; continue;
@@ -239,7 +239,7 @@ void PropertyEditorContextObject::changeTypeName(const QString &typeName)
// Compare current properties and signals with the once available for change type // Compare current properties and signals with the once available for change type
QList<PropertyName> incompatibleProperties; QList<PropertyName> incompatibleProperties;
for (const auto &property : selectedNode.properties()) { for (const AbstractProperty &property : nodeProperties) {
if (!propertiesAndSignals.contains(property.name())) if (!propertiesAndSignals.contains(property.name()))
incompatibleProperties.append(property.name().toByteArray()); incompatibleProperties.append(property.name().toByteArray());
} }
@@ -259,11 +259,11 @@ void PropertyEditorContextObject::changeTypeName(const QString &typeName)
msgBox.setTextFormat(Qt::RichText); msgBox.setTextFormat(Qt::RichText);
msgBox.setIcon(QMessageBox::Question); msgBox.setIcon(QMessageBox::Question);
msgBox.setWindowTitle("Change Type"); msgBox.setWindowTitle("Change Type");
msgBox.setText(QString("Changing the type from %1 to %2 can't be done without removing incompatible properties.<br><br>%3") msgBox.setText(QString("Changing the type from %1 to %2 can't be done without removing "
.arg(selectedNode.simplifiedTypeName()) "incompatible properties.<br><br>%3")
.arg(typeName) .arg(selectedNode.simplifiedTypeName(), typeName, detailedText));
.arg(detailedText)); msgBox.setInformativeText(
msgBox.setInformativeText("Do you want to continue by removing incompatible properties?"); "Do you want to continue by removing incompatible properties?");
msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
msgBox.setDefaultButton(QMessageBox::Ok); msgBox.setDefaultButton(QMessageBox::Ok);
@@ -281,10 +281,23 @@ void PropertyEditorContextObject::changeTypeName(const QString &typeName)
selectedNode.changeType(typeName.toUtf8(), -1, -1); selectedNode.changeType(typeName.toUtf8(), -1, -1);
#else #else
if (selectedNode.isRootNode()) if (selectedNode.isRootNode())
rewriterView->changeRootNodeType(metaInfo.typeName(), metaInfo.majorVersion(), metaInfo.minorVersion()); rewriterView->changeRootNodeType(metaInfo.typeName(),
metaInfo.majorVersion(),
metaInfo.minorVersion());
else else
selectedNode.changeType(metaInfo.typeName(), metaInfo.majorVersion(), metaInfo.minorVersion()); selectedNode.changeType(metaInfo.typeName(),
metaInfo.majorVersion(),
metaInfo.minorVersion());
#endif #endif
};
try {
auto transaction = RewriterTransaction(rewriterView, "PropertyEditorContextObject:changeTypeName");
ModelNodes selectedNodes = rewriterView->selectedModelNodes(); // TODO: replace it by PropertyEditorView::currentNodes()
for (ModelNode &selectedNode : selectedNodes)
changeNodeTypeName(selectedNode);
transaction.commit(); transaction.commit();
} catch (const Exception &e) { } catch (const Exception &e) {
e.showException(); e.showException();

View File

@@ -535,6 +535,31 @@ void QmlDesigner::PropertyEditorQmlBackend::createPropertyEditorValues(const Qml
#endif #endif
} }
PropertyEditorValue *PropertyEditorQmlBackend::insertValue(const QString &name,
const QVariant &value,
const ModelNode &modelNode)
{
auto valueObject = qobject_cast<PropertyEditorValue *>(
variantToQObject(m_backendValuesPropertyMap.value(name)));
if (!valueObject)
valueObject = new PropertyEditorValue(&m_backendValuesPropertyMap);
valueObject->setName(name.toLatin1());
if (modelNode)
valueObject->setModelNode(modelNode);
if (value.isValid())
valueObject->setValue(value);
QObject::connect(valueObject,
&PropertyEditorValue::valueChanged,
&backendValuesPropertyMap(),
&DesignerPropertyMap::valueChanged);
m_backendValuesPropertyMap.insert(name, QVariant::fromValue(valueObject));
return valueObject;
}
void PropertyEditorQmlBackend::updateInstanceImage() void PropertyEditorQmlBackend::updateInstanceImage()
{ {
m_view->instanceImageProvider()->invalidate(); m_view->instanceImageProvider()->invalidate();
@@ -564,29 +589,12 @@ void PropertyEditorQmlBackend::setup(const QmlObjectNode &qmlObjectNode, const Q
m_backendMaterialNode.setup(qmlObjectNode); m_backendMaterialNode.setup(qmlObjectNode);
m_backendTextureNode.setup(qmlObjectNode); m_backendTextureNode.setup(qmlObjectNode);
// className insertValue(Constants::PROPERTY_EDITOR_CLASSNAME_PROPERTY,
auto valueObject = qobject_cast<PropertyEditorValue *>(variantToQObject( m_backendModelNode.simplifiedTypeName(),
m_backendValuesPropertyMap.value(Constants::PROPERTY_EDITOR_CLASSNAME_PROPERTY))); qmlObjectNode.modelNode());
if (!valueObject)
valueObject = new PropertyEditorValue(&m_backendValuesPropertyMap);
valueObject->setName(Constants::PROPERTY_EDITOR_CLASSNAME_PROPERTY);
valueObject->setModelNode(qmlObjectNode.modelNode());
valueObject->setValue(m_backendModelNode.simplifiedTypeName());
QObject::connect(valueObject,
&PropertyEditorValue::valueChanged,
&backendValuesPropertyMap(),
&DesignerPropertyMap::valueChanged);
m_backendValuesPropertyMap.insert(Constants::PROPERTY_EDITOR_CLASSNAME_PROPERTY,
QVariant::fromValue(valueObject));
// id insertValue("id"_L1, m_backendModelNode.nodeId());
valueObject = qobject_cast<PropertyEditorValue*>(variantToQObject(m_backendValuesPropertyMap.value(QLatin1String("id")))); insertValue("objectName"_L1, m_backendModelNode.nodeObjectName());
if (!valueObject)
valueObject = new PropertyEditorValue(&m_backendValuesPropertyMap);
valueObject->setName("id");
valueObject->setValue(m_backendModelNode.nodeId());
QObject::connect(valueObject, &PropertyEditorValue::valueChanged, &backendValuesPropertyMap(), &DesignerPropertyMap::valueChanged);
m_backendValuesPropertyMap.insert(QLatin1String("id"), QVariant::fromValue(valueObject));
QmlItemNode itemNode(qmlObjectNode.modelNode()); QmlItemNode itemNode(qmlObjectNode.modelNode());

View File

@@ -113,6 +113,10 @@ private:
const NodeMetaInfo &type); const NodeMetaInfo &type);
void createPropertyEditorValues(const QmlObjectNode &qmlObjectNode, PropertyEditorView *propertyEditor); void createPropertyEditorValues(const QmlObjectNode &qmlObjectNode, PropertyEditorView *propertyEditor);
PropertyEditorValue *insertValue(const QString &name,
const QVariant &value = {},
const ModelNode &modelNode = {});
static QUrl fileToUrl(const QString &filePath); static QUrl fileToUrl(const QString &filePath);
static QString fileFromUrl(const QUrl &url); static QString fileFromUrl(const QUrl &url);
#ifndef QDS_USE_PROJECTSTORAGE #ifndef QDS_USE_PROJECTSTORAGE

View File

@@ -135,6 +135,18 @@ void PropertyEditorView::changeValue(const QString &name)
return; return;
} }
if (propertyName == "objectName" && currentNodes().size() == 1) {
if (activeNode().metaInfo().isQtQuick3DMaterial()
|| activeNode().metaInfo().isQtQuick3DTexture()) {
PropertyEditorValue *value = m_qmlBackEndForCurrentType->propertyValueForName(
"objectName");
const QString &newObjectName = value->value().toString();
QmlObjectNode objectNode(activeNode());
objectNode.setNameAndId(newObjectName, QString::fromLatin1(activeNode().type()));
return;
}
}
PropertyName underscoreName(propertyName); PropertyName underscoreName(propertyName);
underscoreName.replace('.', '_'); underscoreName.replace('.', '_');
PropertyEditorValue *value = m_qmlBackEndForCurrentType->propertyValueForName(QString::fromLatin1(underscoreName)); PropertyEditorValue *value = m_qmlBackEndForCurrentType->propertyValueForName(QString::fromLatin1(underscoreName));

View File

@@ -16,6 +16,19 @@ namespace QmlDesigner {
using namespace Qt::StringLiterals; using namespace Qt::StringLiterals;
static bool allMaterialTypesAre(const ModelNodes &materials, const QString &materialType)
{
if (materials.isEmpty())
return false;
for (const ModelNode &material : materials) {
if (material.simplifiedTypeName() != materialType)
return false;
}
return true;
}
QmlMaterialNodeProxy::QmlMaterialNodeProxy() QmlMaterialNodeProxy::QmlMaterialNodeProxy()
: QObject() : QObject()
, m_previewUpdateTimer(this) , m_previewUpdateTimer(this)
@@ -62,9 +75,27 @@ void QmlMaterialNodeProxy::updatePossibleTypes()
"SpecularGlossyMaterial", "SpecularGlossyMaterial",
}; };
if (!materialNode()) {
setPossibleTypes({});
return;
}
const QString &matType = materialNode().simplifiedTypeName(); const QString &matType = materialNode().simplifiedTypeName();
setPossibleTypes(basicTypes.contains(matType) ? basicTypes : QStringList{matType}); const ModelNodes selectedNodes = materialView()->selectedModelNodes(); // TODO: replace it by PropertyEditorView::currentNodes()
bool allAreBasic = Utils::allOf(selectedNodes, [&](const ModelNode &node) {
return basicTypes.contains(node.simplifiedTypeName());
});
if (allAreBasic) {
setPossibleTypes(basicTypes);
setCurrentType(matType); setCurrentType(matType);
} else if (allMaterialTypesAre(selectedNodes, matType)) {
setPossibleTypes(QStringList{matType});
setCurrentType(matType);
} else {
setPossibleTypes(QStringList{"multiselection"});
setCurrentType("multiselection");
}
} }
void QmlMaterialNodeProxy::setCurrentType(const QString &type) void QmlMaterialNodeProxy::setCurrentType(const QString &type)

View File

@@ -80,6 +80,17 @@ QString QmlModelNodeProxy::nodeId() const
return m_qmlObjectNode.id(); return m_qmlObjectNode.id();
} }
QString QmlModelNodeProxy::nodeObjectName() const
{
if (!m_qmlObjectNode.isValid())
return {};
if (multiSelection())
return tr("multiselection");
return m_qmlObjectNode.modelNode().variantProperty("objectName").value().toString();
}
QString QmlModelNodeProxy::simplifiedTypeName() const QString QmlModelNodeProxy::simplifiedTypeName() const
{ {
if (!m_qmlObjectNode.isValid()) if (!m_qmlObjectNode.isValid())

View File

@@ -38,6 +38,8 @@ public:
QString nodeId() const; QString nodeId() const;
QString nodeObjectName() const;
QString simplifiedTypeName() const; QString simplifiedTypeName() const;
Q_INVOKABLE QList<int> allChildren(int internalId = -1) const; Q_INVOKABLE QList<int> allChildren(int internalId = -1) const;