Files
qt-creator/src/plugins/qmldesigner/components/materialeditor/materialeditorview.cpp
Marco Bubke 0be4de69d8 QmlDesigner: Remove parent from abstract view
We apply not very often the parent to views. So it can lead to dangling
pointer if the parent is used by other objects and does not handle the
null pointer case. It can lead to double deletion if the parent is
deleted before the object when it is on the stack or handled by smart
pointer.

If you really want to use it there is still setParent.

Change-Id: I1fc6b145a50f037a0e9d415fb36e7970ea7296ed
Reviewed-by: Tim Jenssen <tim.jenssen@qt.io>
2022-09-22 19:08:12 +00:00

1130 lines
40 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 <qmldesignerplugin.h>
#include <utils/fileutils.h>
#include <utils/qtcassert.h>
#include <propertyeditorqmlbackend.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()
: 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()) {
executeInTransaction("MaterialEditorView::MaterialEditorView", [this] {
ensureMaterialLibraryNode();
});
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::delayedResetView()
{
// TODO: it seems the delayed reset is not needed. Leaving it commented out for now just in case it
// turned out to be needed. Otherwise will be removed after a small testing period.
// if (m_timerId)
// killTimer(m_timerId);
// m_timerId = startTimer(50);
resetView();
}
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 (qEnvironmentVariableIsSet("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) {
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()->setHasMaterialRoot(m_hasMaterialRoot);
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
static const QStringList basicTypes {"DefaultMaterial", "PrincipledMaterial", "CustomMaterial"};
QStringList allTypes = basicTypes;
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) {
QString typeName = QString::fromLatin1(entry.typeName().split('.').last());
if (!allTypes.contains(typeName))
allTypes.append(typeName);
}
}
}
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();
}
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;
}
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());
delayedResetView();
}
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));
delayedResetView();
}
}
void MaterialEditorView::rootNodeTypeChanged(const QString &type, int, int)
{
if (rootModelNode() == m_selectedMaterial) {
m_qmlBackEnd->contextObject()->setCurrentType(type);
delayedResetView();
}
}
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);
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);
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());
// set name and id
QString newName = sourceMat.modelNode().variantProperty("objectName").value().toString() + " copy";
duplicateMat.modelNode().variantProperty("objectName").setValue(newName);
duplicateMat.modelNode().setIdWithoutRefactoring(model()->generateIdFromName(newName, "material"));
// sync properties
const QList<AbstractProperty> props = material.properties();
for (const AbstractProperty &prop : props) {
if (prop.name() == "objectName")
continue;
if (prop.isVariantProperty())
duplicateMat.setVariantProperty(prop.name(), prop.toVariantProperty().value());
else if (prop.isBindingProperty())
duplicateMat.setBindingProperty(prop.name(), prop.toBindingProperty().expression());
}
matLib.defaultNodeListProperty().reparentHere(duplicateMat);
});
}
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 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 QObject::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