Files
qt-creator/src/plugins/qmldesigner/components/materialeditor/materialeditorview.cpp
Miikka Heikkinen e3a817ec77 QmlDesigner: Hide or disable material browser and editor if no library
If material library is missing, material browser and editor UI should
be disabled, except for material editor main pane in case of root
material node.

Change-Id: I3d2bd545de0649fb90d3fe1f751d46b7c7054bbf
Reviewed-by: Mahmoud Badri <mahmoud.badri@qt.io>
2022-11-22 10:05:55 +00:00

1192 lines
43 KiB
C++

// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
#include "materialeditorview.h"
#include "materialeditorqmlbackend.h"
#include "materialeditorcontextobject.h"
#include "materialeditordynamicpropertiesproxymodel.h"
#include "propertyeditorvalue.h"
#include "materialeditortransaction.h"
#include "assetslibrarywidget.h"
#include <auxiliarydataproperties.h>
#include <bindingproperty.h>
#include <dynamicpropertiesmodel.h>
#include <metainfo.h>
#include <nodeinstanceview.h>
#include <nodelistproperty.h>
#include <nodemetainfo.h>
#include <nodeproperty.h>
#include <rewritingexception.h>
#include <variantproperty.h>
#include <qmldesignerconstants.h>
#include <qmldesignerplugin.h>
#include <qmltimeline.h>
#include <theme.h>
#include <coreplugin/icore.h>
#include <coreplugin/messagebox.h>
#include <designmodewidget.h>
#include <propertyeditorqmlbackend.h>
#include <utils/environment.h>
#include <utils/fileutils.h>
#include <utils/qtcassert.h>
#include <qmldesignerplugin.h>
#include <QApplication>
#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QQuickWidget>
#include <QQuickItem>
#include <QScopedPointer>
#include <QStackedWidget>
#include <QShortcut>
#include <QTimer>
#include <QColorDialog>
namespace QmlDesigner {
MaterialEditorView::MaterialEditorView(ExternalDependenciesInterface &externalDependencies)
: AbstractView{externalDependencies}
, m_stackedWidget(new QStackedWidget)
, m_dynamicPropertiesModel(new Internal::DynamicPropertiesModel(true, this))
{
m_updateShortcut = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_F7), m_stackedWidget);
connect(m_updateShortcut, &QShortcut::activated, this, &MaterialEditorView::reloadQml);
m_ensureMatLibTimer.callOnTimeout([this] {
if (model() && model()->rewriterView() && !model()->rewriterView()->hasIncompleteTypeInformation()
&& model()->rewriterView()->errors().isEmpty()) {
ensureMaterialLibraryNode();
if (m_qmlBackEnd && m_qmlBackEnd->contextObject())
m_qmlBackEnd->contextObject()->setHasMaterialLibrary(materialLibraryNode().isValid());
m_ensureMatLibTimer.stop();
}
});
m_typeUpdateTimer.setSingleShot(true);
m_typeUpdateTimer.setInterval(500);
connect(&m_typeUpdateTimer, &QTimer::timeout, this, &MaterialEditorView::updatePossibleTypes);
m_stackedWidget->setStyleSheet(Theme::replaceCssColors(
QString::fromUtf8(Utils::FileReader::fetchQrc(":/qmldesigner/stylesheet.css"))));
m_stackedWidget->setMinimumWidth(250);
QmlDesignerPlugin::trackWidgetFocusTime(m_stackedWidget, Constants::EVENT_MATERIALEDITOR_TIME);
MaterialEditorDynamicPropertiesProxyModel::registerDeclarativeType();
}
MaterialEditorView::~MaterialEditorView()
{
qDeleteAll(m_qmlBackendHash);
}
// from material editor to model
void MaterialEditorView::changeValue(const QString &name)
{
PropertyName propertyName = name.toUtf8();
if (propertyName.isNull() || locked() || noValidSelection() || propertyName == "id"
|| propertyName == Constants::PROPERTY_EDITOR_CLASSNAME_PROPERTY) {
return;
}
if (propertyName == "objectName") {
renameMaterial(m_selectedMaterial, m_qmlBackEnd->propertyValueForName("objectName")->value().toString());
return;
}
PropertyName underscoreName(propertyName);
underscoreName.replace('.', '_');
PropertyEditorValue *value = m_qmlBackEnd->propertyValueForName(QString::fromLatin1(underscoreName));
if (!value)
return;
if (propertyName.endsWith("__AUX")) {
commitAuxValueToModel(propertyName, value->value());
return;
}
const NodeMetaInfo metaInfo = m_selectedMaterial.metaInfo();
QVariant castedValue;
if (auto property = metaInfo.property(propertyName)) {
castedValue = property.castedValue(value->value());
} else {
qWarning() << __FUNCTION__ << propertyName << "cannot be casted (metainfo)";
return;
}
if (value->value().isValid() && !castedValue.isValid()) {
qWarning() << __FUNCTION__ << propertyName << "not properly casted (metainfo)";
return;
}
bool propertyTypeUrl = false;
if (auto property = metaInfo.property(propertyName)) {
if (property.propertyType().isUrl()) {
// turn absolute local file paths into relative paths
propertyTypeUrl = true;
QString filePath = castedValue.toUrl().toString();
QFileInfo fi(filePath);
if (fi.exists() && fi.isAbsolute()) {
QDir fileDir(QFileInfo(model()->fileUrl().toLocalFile()).absolutePath());
castedValue = QUrl(fileDir.relativeFilePath(filePath));
}
}
}
if (name == "state" && castedValue.toString() == "base state")
castedValue = "";
if (castedValue.type() == QVariant::Color) {
QColor color = castedValue.value<QColor>();
QColor newColor = QColor(color.name());
newColor.setAlpha(color.alpha());
castedValue = QVariant(newColor);
}
if (!value->value().isValid() || (propertyTypeUrl && value->value().toString().isEmpty())) { // reset
removePropertyFromModel(propertyName);
} else {
// QVector*D(0, 0, 0) detects as null variant though it is valid value
if (castedValue.isValid()
&& (!castedValue.isNull() || castedValue.type() == QVariant::Vector2D
|| castedValue.type() == QVariant::Vector3D
|| castedValue.type() == QVariant::Vector4D)) {
commitVariantValueToModel(propertyName, castedValue);
}
}
requestPreviewRender();
}
static bool isTrueFalseLiteral(const QString &expression)
{
return (expression.compare("false", Qt::CaseInsensitive) == 0)
|| (expression.compare("true", Qt::CaseInsensitive) == 0);
}
void MaterialEditorView::changeExpression(const QString &propertyName)
{
PropertyName name = propertyName.toUtf8();
if (name.isNull() || locked() || noValidSelection())
return;
executeInTransaction("MaterialEditorView::changeExpression", [this, name] {
PropertyName underscoreName(name);
underscoreName.replace('.', '_');
QmlObjectNode qmlObjectNode(m_selectedMaterial);
PropertyEditorValue *value = m_qmlBackEnd->propertyValueForName(QString::fromLatin1(underscoreName));
if (!value) {
qWarning() << __FUNCTION__ << "no value for " << underscoreName;
return;
}
if (auto property = m_selectedMaterial.metaInfo().property(name)) {
auto propertyTypeName = property.propertyType().typeName();
if (propertyTypeName == "QColor") {
if (QColor(value->expression().remove('"')).isValid()) {
qmlObjectNode.setVariantProperty(name, QColor(value->expression().remove('"')));
return;
}
} else if (propertyTypeName == "bool") {
if (isTrueFalseLiteral(value->expression())) {
if (value->expression().compare("true", Qt::CaseInsensitive) == 0)
qmlObjectNode.setVariantProperty(name, true);
else
qmlObjectNode.setVariantProperty(name, false);
return;
}
} else if (propertyTypeName == "int") {
bool ok;
int intValue = value->expression().toInt(&ok);
if (ok) {
qmlObjectNode.setVariantProperty(name, intValue);
return;
}
} else if (propertyTypeName == "qreal") {
bool ok;
qreal realValue = value->expression().toDouble(&ok);
if (ok) {
qmlObjectNode.setVariantProperty(name, realValue);
return;
}
} else if (propertyTypeName == "QVariant") {
bool ok;
qreal realValue = value->expression().toDouble(&ok);
if (ok) {
qmlObjectNode.setVariantProperty(name, realValue);
return;
} else if (isTrueFalseLiteral(value->expression())) {
if (value->expression().compare("true", Qt::CaseInsensitive) == 0)
qmlObjectNode.setVariantProperty(name, true);
else
qmlObjectNode.setVariantProperty(name, false);
return;
}
}
}
if (value->expression().isEmpty()) {
value->resetValue();
return;
}
if (qmlObjectNode.expression(name) != value->expression() || !qmlObjectNode.propertyAffectedByCurrentState(name))
qmlObjectNode.setBindingProperty(name, value->expression());
requestPreviewRender();
}); // end of transaction
}
void MaterialEditorView::exportPropertyAsAlias(const QString &name)
{
if (name.isNull() || locked() || noValidSelection())
return;
executeInTransaction("MaterialEditorView::exportPopertyAsAlias", [this, name] {
const QString id = m_selectedMaterial.validId();
QString upperCasePropertyName = name;
upperCasePropertyName.replace(0, 1, upperCasePropertyName.at(0).toUpper());
QString aliasName = id + upperCasePropertyName;
aliasName.replace(".", ""); //remove all dots
PropertyName propertyName = aliasName.toUtf8();
if (rootModelNode().hasProperty(propertyName)) {
Core::AsynchronousMessageBox::warning(tr("Cannot Export Property as Alias"),
tr("Property %1 does already exist for root component.").arg(aliasName));
return;
}
rootModelNode().bindingProperty(propertyName).setDynamicTypeNameAndExpression("alias", id + "." + name);
});
}
void MaterialEditorView::removeAliasExport(const QString &name)
{
if (name.isNull() || locked() || noValidSelection())
return;
executeInTransaction("MaterialEditorView::removeAliasExport", [this, name] {
const QString id = m_selectedMaterial.validId();
const QList<BindingProperty> bindingProps = rootModelNode().bindingProperties();
for (const BindingProperty &property : bindingProps) {
if (property.expression() == (id + "." + name)) {
rootModelNode().removeProperty(property.name());
break;
}
}
});
}
bool MaterialEditorView::locked() const
{
return m_locked;
}
void MaterialEditorView::currentTimelineChanged(const ModelNode &)
{
m_qmlBackEnd->contextObject()->setHasActiveTimeline(QmlTimeline::hasActiveTimeline(this));
}
Internal::DynamicPropertiesModel *MaterialEditorView::dynamicPropertiesModel() const
{
return m_dynamicPropertiesModel;
}
MaterialEditorView *MaterialEditorView::instance()
{
static MaterialEditorView *s_instance = nullptr;
if (s_instance)
return s_instance;
const auto views = QmlDesignerPlugin::instance()->viewManager().views();
for (auto *view : views) {
MaterialEditorView *myView = qobject_cast<MaterialEditorView *>(view);
if (myView)
s_instance = myView;
}
QTC_ASSERT(s_instance, return nullptr);
return s_instance;
}
void MaterialEditorView::timerEvent(QTimerEvent *timerEvent)
{
if (m_timerId == timerEvent->timerId())
resetView();
}
void MaterialEditorView::resetView()
{
if (!model())
return;
m_locked = true;
if (m_timerId)
killTimer(m_timerId);
setupQmlBackend();
if (m_qmlBackEnd)
m_qmlBackEnd->emitSelectionChanged();
QTimer::singleShot(0, this, &MaterialEditorView::requestPreviewRender);
m_locked = false;
if (m_timerId)
m_timerId = 0;
}
// static
QString MaterialEditorView::materialEditorResourcesPath()
{
#ifdef SHARE_QML_PATH
if (Utils::qtcEnvironmentVariableIsSet("LOAD_QML_FROM_SOURCE"))
return QLatin1String(SHARE_QML_PATH) + "/materialEditorQmlSources";
#endif
return Core::ICore::resourcePath("qmldesigner/materialEditorQmlSources").toString();
}
void MaterialEditorView::applyMaterialToSelectedModels(const ModelNode &material, bool add)
{
if (m_selectedModels.isEmpty())
return;
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("MaterialEditorView::applyMaterialToSelectedModels", [&] {
for (const ModelNode &node : std::as_const(m_selectedModels)) {
QmlObjectNode qmlObjNode(node);
if (add) {
QStringList matList = expToList(qmlObjNode.expression("materials"));
matList.append(material.id());
QString updatedExp = listToExp(matList);
qmlObjNode.setBindingProperty("materials", updatedExp);
} else {
qmlObjNode.setBindingProperty("materials", material.id());
}
}
});
}
void MaterialEditorView::handleToolBarAction(int action)
{
QTC_ASSERT(m_hasQuick3DImport, return);
switch (action) {
case MaterialEditorContextObject::ApplyToSelected: {
applyMaterialToSelectedModels(m_selectedMaterial);
break;
}
case MaterialEditorContextObject::ApplyToSelectedAdd: {
applyMaterialToSelectedModels(m_selectedMaterial, true);
break;
}
case MaterialEditorContextObject::AddNewMaterial: {
if (!model())
break;
executeInTransaction("MaterialEditorView:handleToolBarAction", [&] {
ModelNode matLib = materialLibraryNode();
if (!matLib.isValid())
return;
NodeMetaInfo metaInfo = model()->metaInfo("QtQuick3D.DefaultMaterial");
ModelNode newMatNode = createModelNode("QtQuick3D.DefaultMaterial", metaInfo.majorVersion(),
metaInfo.minorVersion());
renameMaterial(newMatNode, "New Material");
matLib.defaultNodeListProperty().reparentHere(newMatNode);
});
break;
}
case MaterialEditorContextObject::DeleteCurrentMaterial: {
if (m_selectedMaterial.isValid())
m_selectedMaterial.destroy();
break;
}
case MaterialEditorContextObject::OpenMaterialBrowser: {
QmlDesignerPlugin::instance()->mainWidget()->showDockWidget("MaterialBrowser", true);
break;
}
}
}
void MaterialEditorView::handlePreviewEnvChanged(const QString &envAndValue)
{
Q_UNUSED(envAndValue);
if (envAndValue.isEmpty() || m_initializingPreviewData)
return;
QTC_ASSERT(m_hasQuick3DImport, return);
QTC_ASSERT(model(), return);
QTC_ASSERT(model()->nodeInstanceView(), return);
QStringList parts = envAndValue.split('=');
QString env = parts[0];
QString value;
if (parts.size() > 1)
value = parts[1];
auto renderPreviews = [=](const QString &auxEnv, const QString &auxValue) {
rootModelNode().setAuxiliaryData(materialPreviewEnvDocProperty, auxEnv);
rootModelNode().setAuxiliaryData(materialPreviewEnvProperty, auxEnv);
rootModelNode().setAuxiliaryData(materialPreviewEnvValueDocProperty, auxValue);
rootModelNode().setAuxiliaryData(materialPreviewEnvValueProperty, auxValue);
QTimer::singleShot(0, this, &MaterialEditorView::requestPreviewRender);
emitCustomNotification("refresh_material_browser", {});
};
if (env == "Color") {
m_colorDialog.clear();
// Store color to separate property to persist selection over non-color env changes
auto oldColorPropVal = rootModelNode().auxiliaryData(materialPreviewColorDocProperty);
auto oldEnvPropVal = rootModelNode().auxiliaryData(materialPreviewEnvDocProperty);
auto oldValuePropVal = rootModelNode().auxiliaryData(materialPreviewEnvValueDocProperty);
QString oldColor = oldColorPropVal ? oldColorPropVal->toString() : "";
QString oldEnv = oldEnvPropVal ? oldEnvPropVal->toString() : "";
QString oldValue = oldValuePropVal ? oldValuePropVal->toString() : "";
m_colorDialog = new QColorDialog(Core::ICore::dialogParent());
m_colorDialog->setModal(true);
m_colorDialog->setAttribute(Qt::WA_DeleteOnClose);
m_colorDialog->setCurrentColor(QColor(oldColor));
m_colorDialog->show();
QObject::connect(m_colorDialog, &QColorDialog::currentColorChanged,
m_colorDialog, [=](const QColor &color) {
renderPreviews(env, color.name());
});
QObject::connect(m_colorDialog, &QColorDialog::colorSelected,
m_colorDialog, [=](const QColor &color) {
renderPreviews(env, color.name());
rootModelNode().setAuxiliaryData(materialPreviewColorDocProperty, color.name());
});
QObject::connect(m_colorDialog, &QColorDialog::rejected,
m_colorDialog, [=]() {
renderPreviews(oldEnv, oldValue);
initPreviewData();
});
return;
}
renderPreviews(env, value);
}
void MaterialEditorView::handlePreviewModelChanged(const QString &modelStr)
{
Q_UNUSED(modelStr);
if (modelStr.isEmpty() || m_initializingPreviewData)
return;
QTC_ASSERT(m_hasQuick3DImport, return);
QTC_ASSERT(model(), return);
QTC_ASSERT(model()->nodeInstanceView(), return);
rootModelNode().setAuxiliaryData(materialPreviewModelDocProperty, modelStr);
rootModelNode().setAuxiliaryData(materialPreviewModelProperty, modelStr);
QTimer::singleShot(0, this, &MaterialEditorView::requestPreviewRender);
emitCustomNotification("refresh_material_browser", {});
}
void MaterialEditorView::setupQmlBackend()
{
QUrl qmlPaneUrl;
QUrl qmlSpecificsUrl;
QString specificQmlData;
QString currentTypeName;
if (m_selectedMaterial.isValid() && m_hasQuick3DImport && (materialLibraryNode().isValid() || m_hasMaterialRoot)) {
qmlPaneUrl = QUrl::fromLocalFile(materialEditorResourcesPath() + "/MaterialEditorPane.qml");
TypeName diffClassName;
if (NodeMetaInfo metaInfo = m_selectedMaterial.metaInfo()) {
diffClassName = metaInfo.typeName();
for (const NodeMetaInfo &metaInfo : metaInfo.classHierarchy()) {
if (PropertyEditorQmlBackend::checkIfUrlExists(qmlSpecificsUrl))
break;
qmlSpecificsUrl = PropertyEditorQmlBackend::getQmlFileUrl(metaInfo.typeName()
+ "Specifics", metaInfo);
diffClassName = metaInfo.typeName();
}
if (diffClassName != m_selectedMaterial.type()) {
specificQmlData = PropertyEditorQmlBackend::templateGeneration(metaInfo,
model()->metaInfo(
diffClassName),
m_selectedMaterial);
}
}
currentTypeName = QString::fromLatin1(m_selectedMaterial.type());
} else {
qmlPaneUrl = QUrl::fromLocalFile(materialEditorResourcesPath() + "/EmptyMaterialEditorPane.qml");
}
MaterialEditorQmlBackend *currentQmlBackend = m_qmlBackendHash.value(qmlPaneUrl.toString());
QString currentStateName = currentState().isBaseState() ? currentState().name() : "invalid state";
if (!currentQmlBackend) {
currentQmlBackend = new MaterialEditorQmlBackend(this);
m_stackedWidget->addWidget(currentQmlBackend->widget());
m_qmlBackendHash.insert(qmlPaneUrl.toString(), currentQmlBackend);
currentQmlBackend->setup(m_selectedMaterial, currentStateName, qmlSpecificsUrl, this);
currentQmlBackend->setSource(qmlPaneUrl);
QObject *rootObj = currentQmlBackend->widget()->rootObject();
QObject::connect(rootObj, SIGNAL(toolBarAction(int)),
this, SLOT(handleToolBarAction(int)));
QObject::connect(rootObj, SIGNAL(previewEnvChanged(QString)),
this, SLOT(handlePreviewEnvChanged(QString)));
QObject::connect(rootObj, SIGNAL(previewModelChanged(QString)),
this, SLOT(handlePreviewModelChanged(QString)));
} else {
currentQmlBackend->setup(m_selectedMaterial, currentStateName, qmlSpecificsUrl, this);
}
currentQmlBackend->widget()->installEventFilter(this);
currentQmlBackend->contextObject()->setHasQuick3DImport(m_hasQuick3DImport);
currentQmlBackend->contextObject()->setHasMaterialLibrary(materialLibraryNode().isValid());
currentQmlBackend->contextObject()->setSpecificQmlData(specificQmlData);
currentQmlBackend->contextObject()->setCurrentType(currentTypeName);
m_qmlBackEnd = currentQmlBackend;
if (m_hasMaterialRoot)
m_dynamicPropertiesModel->setSelectedNode(m_selectedMaterial);
else
m_dynamicPropertiesModel->reset();
delayedTypeUpdate();
initPreviewData();
m_stackedWidget->setCurrentWidget(m_qmlBackEnd->widget());
}
void MaterialEditorView::commitVariantValueToModel(const PropertyName &propertyName, const QVariant &value)
{
m_locked = true;
executeInTransaction("MaterialEditorView:commitVariantValueToModel", [&] {
QmlObjectNode(m_selectedMaterial).setVariantProperty(propertyName, value);
});
m_locked = false;
}
void MaterialEditorView::commitAuxValueToModel(const PropertyName &propertyName, const QVariant &value)
{
m_locked = true;
PropertyName name = propertyName;
name.chop(5);
try {
if (value.isValid())
m_selectedMaterial.setAuxiliaryData(AuxiliaryDataType::Document, name, value);
else
m_selectedMaterial.removeAuxiliaryData(AuxiliaryDataType::Document, name);
}
catch (const Exception &e) {
e.showException();
}
m_locked = false;
}
void MaterialEditorView::removePropertyFromModel(const PropertyName &propertyName)
{
m_locked = true;
executeInTransaction("MaterialEditorView:removePropertyFromModel", [&] {
QmlObjectNode(m_selectedMaterial).removeProperty(propertyName);
});
m_locked = false;
}
bool MaterialEditorView::noValidSelection() const
{
QTC_ASSERT(m_qmlBackEnd, return true);
return !QmlObjectNode::isValidQmlObjectNode(m_selectedMaterial);
}
void MaterialEditorView::initPreviewData()
{
if (model() && m_qmlBackEnd) {
auto envPropVal = rootModelNode().auxiliaryData(materialPreviewEnvDocProperty);
auto envValuePropVal = rootModelNode().auxiliaryData(materialPreviewEnvValueDocProperty);
auto modelStrPropVal = rootModelNode().auxiliaryData(materialPreviewModelDocProperty);
QString env = envPropVal ? envPropVal->toString() : "";
QString envValue = envValuePropVal ? envValuePropVal->toString() : "";
QString modelStr = modelStrPropVal ? modelStrPropVal->toString() : "";
// Initialize corresponding instance aux values used by puppet
QTimer::singleShot(0, this, [this, env, envValue, modelStr]() {
if (model()) {
rootModelNode().setAuxiliaryData(materialPreviewEnvProperty, env);
rootModelNode().setAuxiliaryData(materialPreviewEnvValueProperty, envValue);
rootModelNode().setAuxiliaryData(materialPreviewModelProperty, modelStr);
}
});
if (!envValue.isEmpty() && env != "Color" && env != "Basic") {
env += '=';
env += envValue;
}
if (env.isEmpty())
env = "SkyBox=preview_studio";
if (modelStr.isEmpty())
modelStr = "#Sphere";
m_initializingPreviewData = true;
QMetaObject::invokeMethod(m_qmlBackEnd->widget()->rootObject(),
"initPreviewData",
Q_ARG(QVariant, env), Q_ARG(QVariant, modelStr));
m_initializingPreviewData = false;
}
}
void MaterialEditorView::delayedTypeUpdate()
{
m_typeUpdateTimer.start();
}
static Import entryToImport(const ItemLibraryEntry &entry)
{
if (entry.majorVersion() == -1 && entry.minorVersion() == -1)
return Import::createFileImport(entry.requiredImport());
return Import::createLibraryImport(entry.requiredImport(),
QString::number(entry.majorVersion()) + QLatin1Char('.') +
QString::number(entry.minorVersion()));
}
void MaterialEditorView::updatePossibleTypes()
{
QTC_ASSERT(model(), return);
if (!m_qmlBackEnd)
return;
// Ensure basic types are always first
QStringList nonQuick3dTypes;
QStringList allTypes;
const QList<ItemLibraryEntry> itemLibEntries = m_itemLibraryInfo->entries();
for (const ItemLibraryEntry &entry : itemLibEntries) {
NodeMetaInfo metaInfo = model()->metaInfo(entry.typeName());
bool valid = metaInfo.isValid()
&& (metaInfo.majorVersion() >= entry.majorVersion()
|| metaInfo.majorVersion() < 0);
if (valid && metaInfo.isQtQuick3DMaterial()) {
bool addImport = entry.requiredImport().isEmpty();
if (!addImport) {
Import import = entryToImport(entry);
addImport = model()->hasImport(import, true, true);
}
if (addImport) {
const QList<QByteArray> typeSplit = entry.typeName().split('.');
const QString typeName = QString::fromLatin1(typeSplit.last());
if (typeSplit.size() == 2 && typeSplit.first() == "QtQuick3D") {
if (!allTypes.contains(typeName))
allTypes.append(typeName);
} else if (!nonQuick3dTypes.contains(typeName)) {
nonQuick3dTypes.append(typeName);
}
}
}
}
allTypes.sort();
nonQuick3dTypes.sort();
allTypes.append(nonQuick3dTypes);
m_qmlBackEnd->contextObject()->setPossibleTypes(allTypes);
}
void MaterialEditorView::modelAttached(Model *model)
{
AbstractView::modelAttached(model);
m_locked = true;
m_hasQuick3DImport = model->hasImport("QtQuick3D");
m_hasMaterialRoot = rootModelNode().metaInfo().isQtQuick3DMaterial();
if (m_hasMaterialRoot) {
m_selectedMaterial = rootModelNode();
} else if (m_hasQuick3DImport) {
// Creating the material library node on model attach causes errors as long as the type
// information is not complete yet, so we keep checking until type info is complete.
m_ensureMatLibTimer.start(500);
}
if (m_itemLibraryInfo.data() != model->metaInfo().itemLibraryInfo()) {
if (m_itemLibraryInfo) {
disconnect(m_itemLibraryInfo.data(), &ItemLibraryInfo::entriesChanged,
this, &MaterialEditorView::delayedTypeUpdate);
}
m_itemLibraryInfo = model->metaInfo().itemLibraryInfo();
if (m_itemLibraryInfo) {
connect(m_itemLibraryInfo.data(), &ItemLibraryInfo::entriesChanged,
this, &MaterialEditorView::delayedTypeUpdate);
}
}
if (!m_setupCompleted) {
reloadQml();
m_setupCompleted = true;
}
resetView();
m_locked = false;
}
void MaterialEditorView::modelAboutToBeDetached(Model *model)
{
AbstractView::modelAboutToBeDetached(model);
m_dynamicPropertiesModel->reset();
m_qmlBackEnd->materialEditorTransaction()->end();
m_qmlBackEnd->contextObject()->setHasMaterialLibrary(false);
}
void MaterialEditorView::propertiesRemoved(const QList<AbstractProperty> &propertyList)
{
if (noValidSelection())
return;
bool changed = false;
for (const AbstractProperty &property : propertyList) {
ModelNode node(property.parentModelNode());
if (node.isRootNode())
m_qmlBackEnd->contextObject()->setHasAliasExport(QmlObjectNode(m_selectedMaterial).isAliasExported());
if (node == m_selectedMaterial || QmlObjectNode(m_selectedMaterial).propertyChangeForCurrentState() == node) {
setValue(m_selectedMaterial, property.name(), QmlObjectNode(m_selectedMaterial).instanceValue(property.name()));
changed = true;
}
dynamicPropertiesModel()->dispatchPropertyChanges(property);
}
if (changed)
requestPreviewRender();
}
void MaterialEditorView::variantPropertiesChanged(const QList<VariantProperty> &propertyList, PropertyChangeFlags /*propertyChange*/)
{
if (noValidSelection())
return;
bool changed = false;
for (const VariantProperty &property : propertyList) {
ModelNode node(property.parentModelNode());
if (node == m_selectedMaterial || QmlObjectNode(m_selectedMaterial).propertyChangeForCurrentState() == node) {
if (property.isDynamic())
m_dynamicPropertiesModel->variantPropertyChanged(property);
if (m_selectedMaterial.property(property.name()).isBindingProperty())
setValue(m_selectedMaterial, property.name(), QmlObjectNode(m_selectedMaterial).instanceValue(property.name()));
else
setValue(m_selectedMaterial, property.name(), QmlObjectNode(m_selectedMaterial).modelValue(property.name()));
changed = true;
}
if (!changed && node.metaInfo().isQtQuick3DTexture()
&& m_selectedMaterial.bindingProperties().size() > 0) {
// update preview when editing texture properties if the material has binding properties
changed = true;
}
dynamicPropertiesModel()->dispatchPropertyChanges(property);
}
if (changed)
requestPreviewRender();
}
void MaterialEditorView::bindingPropertiesChanged(const QList<BindingProperty> &propertyList, PropertyChangeFlags /*propertyChange*/)
{
if (noValidSelection())
return;
bool changed = false;
for (const BindingProperty &property : propertyList) {
ModelNode node(property.parentModelNode());
if (property.isAliasExport())
m_qmlBackEnd->contextObject()->setHasAliasExport(QmlObjectNode(m_selectedMaterial).isAliasExported());
if (node == m_selectedMaterial || QmlObjectNode(m_selectedMaterial).propertyChangeForCurrentState() == node) {
if (property.isDynamic())
m_dynamicPropertiesModel->bindingPropertyChanged(property);
if (QmlObjectNode(m_selectedMaterial).modelNode().property(property.name()).isBindingProperty())
setValue(m_selectedMaterial, property.name(), QmlObjectNode(m_selectedMaterial).instanceValue(property.name()));
else
setValue(m_selectedMaterial, property.name(), QmlObjectNode(m_selectedMaterial).modelValue(property.name()));
changed = true;
}
dynamicPropertiesModel()->dispatchPropertyChanges(property);
}
if (changed)
requestPreviewRender();
}
void MaterialEditorView::auxiliaryDataChanged(const ModelNode &node,
AuxiliaryDataKeyView key,
const QVariant &)
{
if (noValidSelection() || !node.isSelected())
return;
m_qmlBackEnd->setValueforAuxiliaryProperties(m_selectedMaterial, key);
}
void MaterialEditorView::propertiesAboutToBeRemoved(const QList<AbstractProperty> &propertyList)
{
for (const auto &property : propertyList) {
if (property.isBindingProperty())
m_dynamicPropertiesModel->bindingRemoved(property.toBindingProperty());
else if (property.isVariantProperty())
m_dynamicPropertiesModel->variantRemoved(property.toVariantProperty());
}
}
// request render image for the selected material node
void MaterialEditorView::requestPreviewRender()
{
if (model() && model()->nodeInstanceView() && m_selectedMaterial.isValid())
model()->nodeInstanceView()->previewImageDataForGenericNode(m_selectedMaterial, {});
}
bool MaterialEditorView::hasWidget() const
{
return true;
}
WidgetInfo MaterialEditorView::widgetInfo()
{
return createWidgetInfo(m_stackedWidget,
"MaterialEditor",
WidgetInfo::RightPane,
0,
tr("Material Editor"));
}
void MaterialEditorView::selectedNodesChanged(const QList<ModelNode> &selectedNodeList,
[[maybe_unused]] const QList<ModelNode> &lastSelectedNodeList)
{
m_selectedModels.clear();
for (const ModelNode &node : selectedNodeList) {
if (node.metaInfo().isQtQuick3DModel())
m_selectedModels.append(node);
}
m_qmlBackEnd->contextObject()->setHasModelSelection(!m_selectedModels.isEmpty());
}
void MaterialEditorView::currentStateChanged(const ModelNode &node)
{
QmlModelState newQmlModelState(node);
Q_ASSERT(newQmlModelState.isValid());
resetView();
}
void MaterialEditorView::instancePropertyChanged(const QList<QPair<ModelNode, PropertyName> > &propertyList)
{
if (!m_selectedMaterial.isValid() || !m_qmlBackEnd)
return;
m_locked = true;
bool changed = false;
for (const QPair<ModelNode, PropertyName> &propertyPair : propertyList) {
const ModelNode modelNode = propertyPair.first;
const QmlObjectNode qmlObjectNode(modelNode);
const PropertyName propertyName = propertyPair.second;
if (qmlObjectNode.isValid() && modelNode == m_selectedMaterial && qmlObjectNode.currentState().isValid()) {
const AbstractProperty property = modelNode.property(propertyName);
if (!modelNode.hasProperty(propertyName) || modelNode.property(property.name()).isBindingProperty())
setValue(modelNode, property.name(), qmlObjectNode.instanceValue(property.name()));
else
setValue(modelNode, property.name(), qmlObjectNode.modelValue(property.name()));
changed = true;
}
}
if (changed)
requestPreviewRender();
m_locked = false;
}
void MaterialEditorView::nodeTypeChanged(const ModelNode &node, const TypeName &typeName, int, int)
{
if (node == m_selectedMaterial) {
m_qmlBackEnd->contextObject()->setCurrentType(QString::fromLatin1(typeName));
resetView();
}
}
void MaterialEditorView::rootNodeTypeChanged(const QString &type, int, int)
{
if (rootModelNode() == m_selectedMaterial) {
m_qmlBackEnd->contextObject()->setCurrentType(type);
resetView();
}
}
void MaterialEditorView::modelNodePreviewPixmapChanged(const ModelNode &node, const QPixmap &pixmap)
{
if (node == m_selectedMaterial)
m_qmlBackEnd->updateMaterialPreview(pixmap);
}
void MaterialEditorView::importsChanged([[maybe_unused]] const QList<Import> &addedImports,
[[maybe_unused]] const QList<Import> &removedImports)
{
m_hasQuick3DImport = model()->hasImport("QtQuick3D");
m_qmlBackEnd->contextObject()->setHasQuick3DImport(m_hasQuick3DImport);
if (m_hasQuick3DImport)
m_ensureMatLibTimer.start(500);
resetView();
}
void MaterialEditorView::renameMaterial(ModelNode &material, const QString &newName)
{
QTC_ASSERT(material.isValid(), return);
QVariant objName = material.variantProperty("objectName").value();
if (objName.isValid() && objName.toString() == newName)
return;
executeInTransaction("MaterialEditorView:renameMaterial", [&] {
material.setIdWithRefactoring(model()->generateIdFromName(newName, "material"));
VariantProperty objNameProp = material.variantProperty("objectName");
objNameProp.setValue(newName);
});
}
void MaterialEditorView::duplicateMaterial(const ModelNode &material)
{
QTC_ASSERT(material.isValid(), return);
if (!model())
return;
TypeName matType = material.type();
QmlObjectNode sourceMat(material);
ModelNode duplicateMatNode;
QList<AbstractProperty> dynamicProps;
executeInTransaction(__FUNCTION__, [&] {
ModelNode matLib = materialLibraryNode();
if (!matLib.isValid())
return;
// create the duplicate material
NodeMetaInfo metaInfo = model()->metaInfo(matType);
QmlObjectNode duplicateMat = createModelNode(matType, metaInfo.majorVersion(), metaInfo.minorVersion());
duplicateMatNode = duplicateMat.modelNode();
// set name and id
QString newName = sourceMat.modelNode().variantProperty("objectName").value().toString() + " copy";
duplicateMatNode.variantProperty("objectName").setValue(newName);
duplicateMatNode.setIdWithoutRefactoring(model()->generateIdFromName(newName, "material"));
// sync properties. Only the base state is duplicated.
const QList<AbstractProperty> props = material.properties();
for (const AbstractProperty &prop : props) {
if (prop.name() == "objectName" || prop.name() == "data")
continue;
if (prop.isVariantProperty()) {
if (prop.isDynamic()) {
dynamicProps.append(prop);
} else {
duplicateMatNode.variantProperty(prop.name())
.setValue(prop.toVariantProperty().value());
}
} else if (prop.isBindingProperty()) {
if (prop.isDynamic()) {
dynamicProps.append(prop);
} else {
duplicateMatNode.bindingProperty(prop.name())
.setExpression(prop.toBindingProperty().expression());
}
}
}
matLib.defaultNodeListProperty().reparentHere(duplicateMat);
});
// For some reason, creating dynamic properties in the same transaction doesn't work, so
// let's do it in separate transaction.
// TODO: Fix the issue and merge transactions (QDS-8094)
if (!dynamicProps.isEmpty()) {
executeInTransaction(__FUNCTION__, [&] {
for (const AbstractProperty &prop : std::as_const(dynamicProps)) {
if (prop.isVariantProperty()) {
duplicateMatNode.variantProperty(prop.name())
.setDynamicTypeNameAndValue(prop.dynamicTypeName(),
prop.toVariantProperty().value());
} else if (prop.isBindingProperty()) {
duplicateMatNode.bindingProperty(prop.name())
.setDynamicTypeNameAndExpression(prop.dynamicTypeName(),
prop.toBindingProperty().expression());
}
}
});
}
}
void MaterialEditorView::customNotification([[maybe_unused]] const AbstractView *view,
const QString &identifier,
const QList<ModelNode> &nodeList,
const QList<QVariant> &data)
{
if (identifier == "selected_material_changed") {
if (!m_hasMaterialRoot) {
m_selectedMaterial = nodeList.first();
m_dynamicPropertiesModel->setSelectedNode(m_selectedMaterial);
QTimer::singleShot(0, this, &MaterialEditorView::resetView);
}
} else if (identifier == "apply_to_selected_triggered") {
applyMaterialToSelectedModels(nodeList.first(), data.first().toBool());
} else if (identifier == "rename_material") {
if (m_selectedMaterial == nodeList.first())
renameMaterial(m_selectedMaterial, data.first().toString());
} else if (identifier == "add_new_material") {
handleToolBarAction(MaterialEditorContextObject::AddNewMaterial);
} else if (identifier == "duplicate_material") {
duplicateMaterial(nodeList.first());
}
}
void MaterialEditorView::nodeReparented(const ModelNode &node,
const NodeAbstractProperty &newPropertyParent,
const NodeAbstractProperty &oldPropertyParent,
PropertyChangeFlags propertyChange)
{
if (node.id() == Constants::MATERIAL_LIB_ID && m_qmlBackEnd && m_qmlBackEnd->contextObject())
m_qmlBackEnd->contextObject()->setHasMaterialLibrary(true);
}
void MaterialEditorView::nodeAboutToBeRemoved(const ModelNode &removedNode)
{
if (removedNode.id() == Constants::MATERIAL_LIB_ID && m_qmlBackEnd && m_qmlBackEnd->contextObject())
m_qmlBackEnd->contextObject()->setHasMaterialLibrary(false);
}
void QmlDesigner::MaterialEditorView::highlightSupportedProperties(bool highlight)
{
if (!m_selectedMaterial.isValid())
return;
DesignerPropertyMap &propMap = m_qmlBackEnd->backendValuesPropertyMap();
const QStringList propNames = propMap.keys();
NodeMetaInfo metaInfo = m_selectedMaterial.metaInfo();
QTC_ASSERT(metaInfo.isValid(), return);
for (const QString &propName : propNames) {
if (metaInfo.property(propName.toUtf8()).propertyType().isQtQuick3DTexture()) {
QObject *propEditorValObj = propMap.value(propName).value<QObject *>();
PropertyEditorValue *propEditorVal = qobject_cast<PropertyEditorValue *>(propEditorValObj);
propEditorVal->setHasActiveDrag(highlight);
}
}
}
void MaterialEditorView::dragStarted(QMimeData *mimeData)
{
if (!mimeData->hasFormat(Constants::MIME_TYPE_ASSETS))
return;
const QString assetPath = QString::fromUtf8(mimeData->data(Constants::MIME_TYPE_ASSETS)).split(',')[0];
QString assetType = AssetsLibraryWidget::getAssetTypeAndData(assetPath).first;
if (assetType != Constants::MIME_TYPE_ASSET_IMAGE) // currently only image assets have dnd-supported properties
return;
highlightSupportedProperties();
}
void MaterialEditorView::dragEnded()
{
highlightSupportedProperties(false);
}
// from model to material editor
void MaterialEditorView::setValue(const QmlObjectNode &qmlObjectNode, const PropertyName &name, const QVariant &value)
{
m_locked = true;
m_qmlBackEnd->setValue(qmlObjectNode, name, value);
m_locked = false;
}
bool MaterialEditorView::eventFilter(QObject *obj, QEvent *event)
{
if (event->type() == QEvent::FocusOut) {
if (m_qmlBackEnd && m_qmlBackEnd->widget() == obj)
QMetaObject::invokeMethod(m_qmlBackEnd->widget()->rootObject(), "closeContextMenu");
}
return AbstractView::eventFilter(obj, event);
}
void MaterialEditorView::reloadQml()
{
m_qmlBackendHash.clear();
while (QWidget *widget = m_stackedWidget->widget(0)) {
m_stackedWidget->removeWidget(widget);
delete widget;
}
m_qmlBackEnd = nullptr;
resetView();
}
} // namespace QmlDesigner