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:
Ali Kianian
2024-07-18 16:38:41 +03:00
parent 3e05959468
commit 7c17349296
10 changed files with 432 additions and 81 deletions

View File

@@ -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

View File

@@ -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());

View File

@@ -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

View File

@@ -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

View File

@@ -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();

View File

@@ -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;

View File

@@ -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());

View File

@@ -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

View File

@@ -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

View File

@@ -548,4 +548,157 @@ TEST(ModelUtils, empty_is_not_banned_Qml_id)
ASSERT_THAT(isBannedQmlId, IsFalse());
}
TEST(ModelUtils, expressionToList_empty_expression_returns_empty_list)
{
QString expression = "";
QStringList list = QmlDesigner::ModelUtils::expressionToList(expression);
ASSERT_THAT(list, IsEmpty());
}
TEST(ModelUtils, expressionToList_empty_array_returns_empty_list)
{
QString expression = "[]";
QStringList list = QmlDesigner::ModelUtils::expressionToList(expression);
ASSERT_THAT(list, IsEmpty());
}
TEST(ModelUtils, expressionToList_comma_only_array_returns_empty_list)
{
QString expression = "[,,,]";
QStringList list = QmlDesigner::ModelUtils::expressionToList(expression);
ASSERT_THAT(list, IsEmpty());
}
TEST(ModelUtils, expressionToList_space_only_array_returns_empty_list)
{
QString expression = "[ , , , ]";
QStringList list = QmlDesigner::ModelUtils::expressionToList(expression);
ASSERT_THAT(list, IsEmpty());
}
TEST(ModelUtils, expressionToList_single_expression_returns_single_item_list)
{
QString expression = "aaa";
QStringList list = QmlDesigner::ModelUtils::expressionToList(expression);
ASSERT_THAT(list, UnorderedElementsAre("aaa"));
}
TEST(ModelUtils, expressionToList_single_expression_keeps_middle_spaces)
{
QString expression = "aa a b";
QStringList list = QmlDesigner::ModelUtils::expressionToList(expression);
ASSERT_THAT(list, UnorderedElementsAre("aa a b"));
}
TEST(ModelUtils, expressionToList_single_expression_omites_side_spaces)
{
QString expression = " aa a b ";
QStringList list = QmlDesigner::ModelUtils::expressionToList(expression);
ASSERT_THAT(list, UnorderedElementsAre("aa a b"));
}
TEST(ModelUtils, expressionToList_single_item_array_returns_single_item_list)
{
QString expression = "[aaa]";
QStringList list = QmlDesigner::ModelUtils::expressionToList(expression);
ASSERT_THAT(list, UnorderedElementsAre("aaa"));
}
TEST(ModelUtils, expressionToList_array_with_multiple_items_returns_all)
{
QString expression = "[bbb,aaa,ccc]";
QStringList list = QmlDesigner::ModelUtils::expressionToList(expression);
ASSERT_THAT(list, UnorderedElementsAre("bbb", "aaa", "ccc"));
}
TEST(ModelUtils, expressionToList_array_with_empty_items_returns_clean_list)
{
QString expression = "[,aaa,,bbb,ccc,,]";
QStringList list = QmlDesigner::ModelUtils::expressionToList(expression);
ASSERT_THAT(list, UnorderedElementsAre("aaa", "bbb", "ccc"));
}
TEST(ModelUtils, expressionToList_keeps_middle_spaces_in_tokens)
{
QString expression = "[aaa,,a bb b,ccc]";
QStringList list = QmlDesigner::ModelUtils::expressionToList(expression);
ASSERT_THAT(list, UnorderedElementsAre("aaa", "a bb b", "ccc"));
}
TEST(ModelUtils, expressionToList_omits_side_spaces_in_tokens)
{
QString expression = "[aaa,, a bb b ,ccc]";
QStringList list = QmlDesigner::ModelUtils::expressionToList(expression);
ASSERT_THAT(list, UnorderedElementsAre("aaa", "a bb b", "ccc"));
}
TEST(ModelUtils, expressionToList_unicodes_supported)
{
QString expression = "[你好, foo]";
QStringList list = QmlDesigner::ModelUtils::expressionToList(expression);
ASSERT_THAT(list, UnorderedElementsAre("你好", "foo"));
}
TEST(ModelUtils, listToExpression_returns_non_array_expression_for_single_items)
{
QStringList list = {"aaa"};
QString expression = QmlDesigner::ModelUtils::listToExpression(list);
ASSERT_THAT(expression, Eq("aaa"));
}
TEST(ModelUtils, listToExpression_returns_expression)
{
QStringList list = {"aaa", "bbb", "ccc"};
QString expression = QmlDesigner::ModelUtils::listToExpression(list);
ASSERT_THAT(expression, Eq("[aaa,bbb,ccc]"));
}
TEST(ModelUtils, listToExpression_returns_expression_with_empty_items)
{
QStringList list = {"aaa", "bbb", "", "ccc"};
QString expression = QmlDesigner::ModelUtils::listToExpression(list);
ASSERT_THAT(expression, Eq("[aaa,bbb,,ccc]"));
}
TEST(ModelUtils, listToExpression_returns_empty_for_empty_expressions)
{
QStringList list = {};
QString expression = QmlDesigner::ModelUtils::listToExpression(list);
ASSERT_THAT(expression, IsEmpty());
}
} // namespace