forked from qt-creator/qt-creator
QmlDesigner: Repolish materials action in 3d context menu
1. Instead of a single action, a menu is shown. This menu contains all of the materials of the selected node. 2. If multiple nodes are selected, the intersection of the materials is shown. 3. Having several identical materials in a nodes are supported. 4. For each material, there are two options * Remove (Removes the material from the selected nodes) * Edit (Opens the material editor for the selected material) Task-number: QDS-12375 Change-Id: Icc19a4127dc490490e4464ce840e89e5379c5e8c Reviewed-by: Marco Bubke <marco.bubke@qt.io> Reviewed-by: Mahmoud Badri <mahmoud.badri@qt.io>
This commit is contained in:
@@ -676,6 +676,7 @@ extend_qtc_plugin(QmlDesigner
|
||||
edit3dwidget.cpp edit3dwidget.h
|
||||
edit3dcanvas.cpp edit3dcanvas.h
|
||||
edit3dactions.cpp edit3dactions.h
|
||||
edit3dmaterialsaction.cpp edit3dmaterialsaction.h
|
||||
edit3dtoolbarmenu.cpp edit3dtoolbarmenu.h
|
||||
backgroundcolorselection.cpp backgroundcolorselection.h
|
||||
bakelights.cpp bakelights.h
|
||||
|
||||
@@ -437,36 +437,14 @@ void ContentLibraryView::applyBundleMaterialToDropTarget(const ModelNode &bundle
|
||||
|
||||
executeInTransaction("ContentLibraryView::applyBundleMaterialToDropTarget", [&] {
|
||||
ModelNode newMatNode = typeName.size() ? createMaterial(typeName) : bundleMat;
|
||||
|
||||
// TODO: unify this logic as it exist elsewhere also
|
||||
auto expToList = [](const QString &exp) {
|
||||
QString copy = exp;
|
||||
copy = copy.remove("[").remove("]");
|
||||
|
||||
QStringList tmp = copy.split(',', Qt::SkipEmptyParts);
|
||||
for (QString &str : tmp)
|
||||
str = str.trimmed();
|
||||
|
||||
return tmp;
|
||||
};
|
||||
|
||||
auto listToExp = [](QStringList &stringList) {
|
||||
if (stringList.size() > 1)
|
||||
return QString("[" + stringList.join(",") + "]");
|
||||
|
||||
if (stringList.size() == 1)
|
||||
return stringList.first();
|
||||
|
||||
return QString();
|
||||
};
|
||||
|
||||
for (const ModelNode &target : std::as_const(m_bundleMaterialTargets)) {
|
||||
if (target.isValid() && target.metaInfo().isQtQuick3DModel()) {
|
||||
QmlObjectNode qmlObjNode(target);
|
||||
if (m_bundleMaterialAddToSelected) {
|
||||
QStringList matList = expToList(qmlObjNode.expression("materials"));
|
||||
QStringList matList = ModelUtils::expressionToList(
|
||||
qmlObjNode.expression("materials"));
|
||||
matList.append(newMatNode.id());
|
||||
QString updatedExp = listToExp(matList);
|
||||
QString updatedExp = ModelUtils::listToExpression(matList);
|
||||
qmlObjNode.setBindingProperty("materials", updatedExp);
|
||||
} else {
|
||||
qmlObjNode.setBindingProperty("materials", newMatNode.id());
|
||||
@@ -488,35 +466,14 @@ void ContentLibraryView::applyBundleMaterialToDropTarget(const ModelNode &bundle
|
||||
executeInTransaction("ContentLibraryView::applyBundleMaterialToDropTarget", [&] {
|
||||
ModelNode newMatNode = metaInfo.isValid() ? createMaterial(metaInfo) : bundleMat;
|
||||
|
||||
// TODO: unify this logic as it exist elsewhere also
|
||||
auto expToList = [](const QString &exp) {
|
||||
QString copy = exp;
|
||||
copy = copy.remove("[").remove("]");
|
||||
|
||||
QStringList tmp = copy.split(',', Qt::SkipEmptyParts);
|
||||
for (QString &str : tmp)
|
||||
str = str.trimmed();
|
||||
|
||||
return tmp;
|
||||
};
|
||||
|
||||
auto listToExp = [](QStringList &stringList) {
|
||||
if (stringList.size() > 1)
|
||||
return QString("[" + stringList.join(",") + "]");
|
||||
|
||||
if (stringList.size() == 1)
|
||||
return stringList.first();
|
||||
|
||||
return QString();
|
||||
};
|
||||
|
||||
for (const ModelNode &target : std::as_const(m_bundleMaterialTargets)) {
|
||||
if (target.isValid() && target.metaInfo().isQtQuick3DModel()) {
|
||||
QmlObjectNode qmlObjNode(target);
|
||||
if (m_bundleMaterialAddToSelected) {
|
||||
QStringList matList = expToList(qmlObjNode.expression("materials"));
|
||||
QStringList matList = ModelUtils::expressionToList(
|
||||
qmlObjNode.expression("materials"));
|
||||
matList.append(newMatNode.id());
|
||||
QString updatedExp = listToExp(matList);
|
||||
QString updatedExp = ModelUtils::listToExpression(matList);
|
||||
qmlObjNode.setBindingProperty("materials", updatedExp);
|
||||
} else {
|
||||
qmlObjNode.setBindingProperty("materials", newMatNode.id());
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
// Copyright (C) 2024 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
|
||||
#include "edit3dmaterialsaction.h"
|
||||
|
||||
#include <bindingproperty.h>
|
||||
#include <designmodewidget.h>
|
||||
#include <model.h>
|
||||
#include <modelnode.h>
|
||||
#include <modelutils.h>
|
||||
#include <qmldesignerplugin.h>
|
||||
#include <variantproperty.h>
|
||||
|
||||
#include <utils/qtcassert.h>
|
||||
|
||||
#include <QMenu>
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
namespace QmlDesigner {
|
||||
|
||||
static QString getMaterialName(const ModelNode &material, bool forceIncludeId = false)
|
||||
{
|
||||
QString materialName = material.variantProperty("objectName").value().toString();
|
||||
if (materialName.isEmpty() || forceIncludeId)
|
||||
materialName.append(QString("[%1]").arg(material.id()));
|
||||
return materialName;
|
||||
}
|
||||
|
||||
struct MaterialNameLessThan
|
||||
{
|
||||
bool operator()(const ModelNode &a, const ModelNode &b)
|
||||
{
|
||||
const QString aName = getMaterialName(a, true);
|
||||
const QString bName = getMaterialName(b, true);
|
||||
return aName.compare(bName, Qt::CaseSensitive) < 0;
|
||||
};
|
||||
};
|
||||
|
||||
static QList<ModelNode> getMaterials(const ModelNode &node)
|
||||
{
|
||||
BindingProperty matsProp = node.bindingProperty("materials");
|
||||
if (!matsProp.exists())
|
||||
return {};
|
||||
|
||||
Model *model = node.model();
|
||||
QList<ModelNode> materials;
|
||||
if (model->hasId(matsProp.expression()))
|
||||
materials.append(model->modelNodeForId(matsProp.expression()));
|
||||
else
|
||||
materials = matsProp.resolveToModelNodeList();
|
||||
|
||||
return materials;
|
||||
}
|
||||
|
||||
static QList<ModelNode> getSortedMaterials(const ModelNode &node)
|
||||
{
|
||||
QList<ModelNode> materials = getMaterials(node);
|
||||
std::sort(materials.begin(), materials.end(), MaterialNameLessThan{});
|
||||
return materials;
|
||||
}
|
||||
|
||||
static void removeMaterialFromNode(const ModelNode &node, const QString &materialId, int nthMaterial)
|
||||
{
|
||||
BindingProperty matsProp = node.bindingProperty("materials");
|
||||
if (!matsProp.exists())
|
||||
return;
|
||||
|
||||
const QString materialsExpression = matsProp.expression();
|
||||
Model *model = node.model();
|
||||
|
||||
if (matsProp.isList()) {
|
||||
matsProp.removeModelNodeFromArray(model->modelNodeForId(materialId));
|
||||
QStringList nodeMaterials = ModelUtils::expressionToList(materialsExpression);
|
||||
|
||||
int indexToBeRemoved = -1;
|
||||
do
|
||||
indexToBeRemoved = nodeMaterials.indexOf(materialId, indexToBeRemoved + 1);
|
||||
while (nthMaterial-- && indexToBeRemoved != -1);
|
||||
|
||||
if (indexToBeRemoved != -1)
|
||||
nodeMaterials.removeAt(indexToBeRemoved);
|
||||
|
||||
if (nodeMaterials.isEmpty())
|
||||
matsProp.parentModelNode().removeProperty(matsProp.name());
|
||||
else if (nodeMaterials.size() == 1)
|
||||
matsProp.setExpression(nodeMaterials.first());
|
||||
else
|
||||
matsProp.setExpression('[' + nodeMaterials.join(',') + ']');
|
||||
} else if (materialsExpression == materialId) {
|
||||
matsProp.parentModelNode().removeProperty(matsProp.name());
|
||||
}
|
||||
}
|
||||
|
||||
static QList<ModelNode> commonMaterialsOfNodes(const QList<ModelNode> &selectedNodes)
|
||||
{
|
||||
if (selectedNodes.isEmpty())
|
||||
return {};
|
||||
|
||||
if (selectedNodes.size() == 1)
|
||||
return getMaterials(selectedNodes.first());
|
||||
|
||||
QList<ModelNode> commonMaterials = getSortedMaterials(selectedNodes.first());
|
||||
for (const ModelNode &node : Utils::span(selectedNodes).subspan(1)) {
|
||||
const QList<ModelNode> materials = getSortedMaterials(node);
|
||||
QList<ModelNode> materialIntersection;
|
||||
std::set_intersection(commonMaterials.begin(),
|
||||
commonMaterials.end(),
|
||||
materials.begin(),
|
||||
materials.end(),
|
||||
std::back_inserter(materialIntersection),
|
||||
MaterialNameLessThan{});
|
||||
std::swap(commonMaterials, materialIntersection);
|
||||
if (commonMaterials.isEmpty())
|
||||
return {};
|
||||
}
|
||||
return commonMaterials;
|
||||
}
|
||||
|
||||
Edit3DMaterialsAction::Edit3DMaterialsAction(const QIcon &icon, QObject *parent)
|
||||
: QAction(icon, tr("Materials"), parent)
|
||||
{
|
||||
this->setMenu(new QMenu("Materials"));
|
||||
connect(this, &QObject::destroyed, this->menu(), &QObject::deleteLater);
|
||||
}
|
||||
|
||||
void Edit3DMaterialsAction::updateMenu(const QList<ModelNode> &selecedNodes)
|
||||
{
|
||||
QMenu *menu = this->menu();
|
||||
QTC_ASSERT(menu, return);
|
||||
|
||||
m_selectedNodes = selecedNodes;
|
||||
|
||||
menu->clear();
|
||||
const QList<ModelNode> materials = commonMaterialsOfNodes(m_selectedNodes);
|
||||
QHash<ModelNode, int> nthMaterialMap; // <material, n times repeated>
|
||||
|
||||
for (const ModelNode &material : materials) {
|
||||
int nthMaterialWithTheSameId = nthMaterialMap.value(material, -1) + 1;
|
||||
nthMaterialMap.insert(material, nthMaterialWithTheSameId);
|
||||
QAction *materialAction = createMaterialAction(material, menu, nthMaterialWithTheSameId);
|
||||
if (materialAction)
|
||||
menu->addAction(materialAction);
|
||||
}
|
||||
|
||||
setVisible(!menu->actions().isEmpty());
|
||||
setEnabled(isVisible());
|
||||
}
|
||||
|
||||
void Edit3DMaterialsAction::removeMaterial(const QString &materialId, int nthMaterial)
|
||||
{
|
||||
if (m_selectedNodes.isEmpty())
|
||||
return;
|
||||
|
||||
AbstractView *nodesView = m_selectedNodes.first().view();
|
||||
nodesView->executeInTransaction(__FUNCTION__, [&] {
|
||||
for (ModelNode &node : m_selectedNodes)
|
||||
removeMaterialFromNode(node, materialId, nthMaterial);
|
||||
});
|
||||
}
|
||||
|
||||
QAction *Edit3DMaterialsAction::createMaterialAction(const ModelNode &material,
|
||||
QMenu *parentMenu,
|
||||
int nthMaterial)
|
||||
{
|
||||
const QString materialId = material.id();
|
||||
if (materialId.isEmpty())
|
||||
return nullptr;
|
||||
|
||||
QString materialName = getMaterialName(material);
|
||||
|
||||
QAction *action = new QAction(materialName, parentMenu);
|
||||
QMenu *menu = new QMenu(materialName, parentMenu);
|
||||
connect(action, &QObject::destroyed, menu, &QObject::deleteLater);
|
||||
|
||||
QAction *removeMaterialAction = new QAction(tr("Remove"), menu);
|
||||
connect(removeMaterialAction,
|
||||
&QAction::triggered,
|
||||
menu,
|
||||
std::bind(&Edit3DMaterialsAction::removeMaterial, this, materialId, nthMaterial));
|
||||
|
||||
QAction *editMaterialAction = new QAction(tr("Edit"), menu);
|
||||
connect(editMaterialAction, &QAction::triggered, menu, [material] {
|
||||
QmlDesignerPlugin::instance()->mainWidget()->showDockWidget("MaterialEditor", true);
|
||||
if (auto materialView = material.view())
|
||||
materialView->emitCustomNotification("select_material", {material});
|
||||
});
|
||||
|
||||
menu->addAction(removeMaterialAction);
|
||||
menu->addAction(editMaterialAction);
|
||||
action->setMenu(menu);
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
}; // namespace QmlDesigner
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (C) 2024 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
#pragma once
|
||||
|
||||
#include <selectioncontext.h>
|
||||
|
||||
#include <QAction>
|
||||
#include <QList>
|
||||
|
||||
namespace QmlDesigner {
|
||||
|
||||
class ModelNode;
|
||||
|
||||
class Edit3DMaterialsAction : public QAction
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
Edit3DMaterialsAction(const QIcon &icon, QObject *parent);
|
||||
|
||||
void updateMenu(const QList<ModelNode> &selecedNodes);
|
||||
|
||||
private slots:
|
||||
void removeMaterial(const QString &materialId, int nthMaterial);
|
||||
|
||||
private:
|
||||
QAction *createMaterialAction(const ModelNode &material, QMenu *parentMenu, int nthMaterial);
|
||||
|
||||
QList<ModelNode> m_selectedNodes;
|
||||
};
|
||||
|
||||
} // namespace QmlDesigner
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "edit3dactions.h"
|
||||
#include "edit3dcanvas.h"
|
||||
#include "edit3dmaterialsaction.h"
|
||||
#include "edit3dtoolbarmenu.h"
|
||||
#include "edit3dview.h"
|
||||
|
||||
@@ -198,13 +199,8 @@ void Edit3DWidget::createContextMenu()
|
||||
DocumentManager::goIntoComponent(m_view->singleSelectedModelNode());
|
||||
});
|
||||
|
||||
m_editMaterialAction = m_contextMenu->addAction(
|
||||
contextIcon(DesignerIcons::MaterialIcon),
|
||||
tr("Edit Material"), [&] {
|
||||
SelectionContext selCtx(m_view);
|
||||
selCtx.setTargetNode(m_contextMenuTarget);
|
||||
ModelNodeOperations::editMaterial(selCtx);
|
||||
});
|
||||
m_materialsAction = new Edit3DMaterialsAction(contextIcon(DesignerIcons::MaterialIcon), this);
|
||||
m_contextMenu->addAction(m_materialsAction);
|
||||
|
||||
m_contextMenu->addSeparator();
|
||||
|
||||
@@ -649,7 +645,7 @@ void Edit3DWidget::showContextMenu(const QPoint &pos, const ModelNode &modelNode
|
||||
m_createSubMenu->setEnabled(!isSceneLocked());
|
||||
|
||||
m_editComponentAction->setEnabled(isSingleComponent);
|
||||
m_editMaterialAction->setEnabled(isModel);
|
||||
m_materialsAction->setEnabled(isModel);
|
||||
m_duplicateAction->setEnabled(selectionExcludingRoot);
|
||||
m_copyAction->setEnabled(selectionExcludingRoot);
|
||||
m_pasteAction->setEnabled(isPasteAvailable());
|
||||
@@ -663,6 +659,7 @@ void Edit3DWidget::showContextMenu(const QPoint &pos, const ModelNode &modelNode
|
||||
m_bakeLightsAction->setEnabled(view()->bakeLightsAction()->action()->isEnabled());
|
||||
m_addToContentLibAction->setEnabled(isNode && !isInBundle);
|
||||
m_exportBundleAction->setEnabled(isNode);
|
||||
m_materialsAction->updateMenu(view()->selectedModelNodes());
|
||||
|
||||
if (m_view) {
|
||||
int idx = m_view->activeSplit();
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace QmlDesigner {
|
||||
class Edit3DView;
|
||||
class Edit3DCanvas;
|
||||
class ToolBox;
|
||||
class Edit3DMaterialsAction;
|
||||
|
||||
struct ItemLibraryDetails {
|
||||
QString name;
|
||||
@@ -89,7 +90,6 @@ private:
|
||||
QPointer<QMenu> m_contextMenu;
|
||||
QPointer<QAction> m_bakeLightsAction;
|
||||
QPointer<QAction> m_editComponentAction;
|
||||
QPointer<QAction> m_editMaterialAction;
|
||||
QPointer<QAction> m_duplicateAction;
|
||||
QPointer<QAction> m_copyAction;
|
||||
QPointer<QAction> m_pasteAction;
|
||||
@@ -103,6 +103,7 @@ private:
|
||||
QPointer<QAction> m_importBundleAction;
|
||||
QPointer<QAction> m_exportBundleAction;
|
||||
QPointer<QAction> m_addToContentLibAction;
|
||||
QPointer<Edit3DMaterialsAction> m_materialsAction;
|
||||
QHash<int, QPointer<QAction>> m_matOverrideActions;
|
||||
QPointer<QMenu> m_createSubMenu;
|
||||
ModelNode m_contextMenuTarget;
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "qmldesignerplugin.h"
|
||||
#include "qmltimeline.h"
|
||||
#include "variantproperty.h"
|
||||
#include <modelutils.h>
|
||||
#include <uniquename.h>
|
||||
#include <utils3d.h>
|
||||
|
||||
@@ -380,34 +381,14 @@ void MaterialEditorView::applyMaterialToSelectedModels(const ModelNode &material
|
||||
|
||||
QTC_ASSERT(material.isValid(), return);
|
||||
|
||||
auto expToList = [](const QString &exp) {
|
||||
QString copy = exp;
|
||||
copy = copy.remove("[").remove("]");
|
||||
|
||||
QStringList tmp = copy.split(',', Qt::SkipEmptyParts);
|
||||
for (QString &str : tmp)
|
||||
str = str.trimmed();
|
||||
|
||||
return tmp;
|
||||
};
|
||||
|
||||
auto listToExp = [](QStringList &stringList) {
|
||||
if (stringList.size() > 1)
|
||||
return QString("[" + stringList.join(",") + "]");
|
||||
|
||||
if (stringList.size() == 1)
|
||||
return stringList.first();
|
||||
|
||||
return QString();
|
||||
};
|
||||
|
||||
executeInTransaction(__FUNCTION__, [&] {
|
||||
for (const ModelNode &node : std::as_const(m_selectedModels)) {
|
||||
QmlObjectNode qmlObjNode(node);
|
||||
if (add) {
|
||||
QStringList matList = expToList(qmlObjNode.expression("materials"));
|
||||
QStringList matList = ModelUtils::expressionToList(
|
||||
qmlObjNode.expression("materials"));
|
||||
matList.append(material.id());
|
||||
QString updatedExp = listToExp(matList);
|
||||
QString updatedExp = ModelUtils::listToExpression(matList);
|
||||
qmlObjNode.setBindingProperty("materials", updatedExp);
|
||||
} else {
|
||||
qmlObjNode.setBindingProperty("materials", material.id());
|
||||
|
||||
@@ -469,4 +469,33 @@ bool isValidQmlIdentifier(QStringView id)
|
||||
return id.contains(idExpr);
|
||||
}
|
||||
|
||||
QStringList expressionToList(QStringView expression)
|
||||
{
|
||||
QStringView cleanedExp = (expression.startsWith('[') && expression.endsWith(']'))
|
||||
? expression.sliced(1, expression.size() - 2)
|
||||
: expression;
|
||||
QList<QStringView> tokens = cleanedExp.split(',');
|
||||
|
||||
QStringList expList;
|
||||
expList.reserve(tokens.size());
|
||||
for (QStringView token : tokens) {
|
||||
token = token.trimmed();
|
||||
if (!token.isEmpty())
|
||||
expList.append(token.toString());
|
||||
}
|
||||
|
||||
return expList;
|
||||
}
|
||||
|
||||
QString listToExpression(const QStringList &stringList)
|
||||
{
|
||||
if (stringList.size() > 1)
|
||||
return QString("[" + stringList.join(",") + "]");
|
||||
|
||||
if (stringList.size() == 1)
|
||||
return stringList.first();
|
||||
|
||||
return QString();
|
||||
}
|
||||
|
||||
} // namespace QmlDesigner::ModelUtils
|
||||
|
||||
@@ -56,4 +56,9 @@ constexpr std::u16string_view toStdStringView(QStringView view)
|
||||
return {view.utf16(), Utils::usize(view)};
|
||||
}
|
||||
|
||||
QMLDESIGNERCORE_EXPORT QStringList expressionToList(QStringView exp);
|
||||
|
||||
QMLDESIGNERCORE_EXPORT QString listToExpression(const QStringList &stringList);
|
||||
;
|
||||
|
||||
} // namespace QmlDesigner::ModelUtils
|
||||
|
||||
Reference in New Issue
Block a user