// Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "materialeditorcontextobject.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace QmlDesigner { MaterialEditorContextObject::MaterialEditorContextObject(QQmlContext *context, QObject *parent) : QObject(parent) , m_qmlContext(context) { qmlRegisterUncreatableType("MaterialToolBarAction", 1, 0, "ToolBarAction", "Enum type"); } QQmlComponent *MaterialEditorContextObject::specificQmlComponent() { if (m_specificQmlComponent) return m_specificQmlComponent; m_specificQmlComponent = new QQmlComponent(m_qmlContext->engine(), this); m_specificQmlComponent->setData(m_specificQmlData.toUtf8(), QUrl::fromLocalFile("specifics.qml")); return m_specificQmlComponent; } QString MaterialEditorContextObject::convertColorToString(const QVariant &color) { QString colorString; QColor theColor; if (color.canConvert(QVariant::Color)) { theColor = color.value(); } else if (color.canConvert(QVariant::Vector3D)) { auto vec = color.value(); theColor = QColor::fromRgbF(vec.x(), vec.y(), vec.z()); } colorString = theColor.name(); if (theColor.alpha() != 255) { QString hexAlpha = QString("%1").arg(theColor.alpha(), 2, 16, QLatin1Char('0')); colorString.remove(0, 1); colorString.prepend(hexAlpha); colorString.prepend(QStringLiteral("#")); } return colorString; } // TODO: this method is used by the ColorEditor helper widget, check if at all needed? QColor MaterialEditorContextObject::colorFromString(const QString &colorString) { return colorString; } void MaterialEditorContextObject::changeTypeName(const QString &typeName) { QTC_ASSERT(m_model && m_model->rewriterView(), return); QTC_ASSERT(m_selectedMaterial.isValid(), return); if (m_selectedMaterial.simplifiedTypeName() == typeName) return; // Ideally we should not misuse the rewriterView // If we add more code here we have to forward the material editor view RewriterView *rewriterView = m_model->rewriterView(); rewriterView->executeInTransaction("MaterialEditorContextObject::changeTypeName", [&] { NodeMetaInfo metaInfo = m_model->metaInfo(typeName.toLatin1()); QTC_ASSERT(metaInfo.isValid(), return); // Create a list of properties available for the new type PropertyNameList propertiesAndSignals = Utils::transform( metaInfo.properties(), [](const auto &property) { return property.name(); }); // Add signals to the list const PropertyNameList signalNames = metaInfo.signalNames(); for (const PropertyName &signal : signalNames) { if (signal.isEmpty()) continue; PropertyName name = signal; QChar firstChar = QChar(signal.at(0)).toUpper().toLatin1(); name[0] = firstChar.toLatin1(); name.prepend("on"); propertiesAndSignals.append(name); } // Add dynamic properties and respective change signals const QList matProps = m_selectedMaterial.properties(); for (const auto &property : matProps) { if (!property.isDynamic()) continue; // Add dynamic property propertiesAndSignals.append(property.name()); // Add its change signal PropertyName name = property.name(); QChar firstChar = QChar(property.name().at(0)).toUpper().toLatin1(); name[0] = firstChar.toLatin1(); name.prepend("on"); name.append("Changed"); propertiesAndSignals.append(name); } // Compare current properties and signals with the ones available for change type QList incompatibleProperties; for (const auto &property : matProps) { if (!propertiesAndSignals.contains(property.name())) incompatibleProperties.append(property.name()); } // When switching between material types, copy base (diffuse) color and map properties of // source type into corresponding properties of the target type. const QList baseColors = {"baseColor", "diffuseColor", "albedoColor"}; const QList baseMaps = {"baseColorMap", "diffuseMap", "albedoMap"}; int sourceIndex = -1; int targetIndex = -1; NodeMetaInfo oldMetaInfo = m_selectedMaterial.metaInfo(); struct CopyData { CopyData() {}; CopyData(PropertyName n) : name(n) {} PropertyName name; QVariant value; bool isBinding = false; }; QHash copyMap; if (oldMetaInfo.isQtQuick3DPrincipledMaterial()) sourceIndex = 0; else if (oldMetaInfo.isQtQuick3DDefaultMaterial()) sourceIndex = 1; else if (oldMetaInfo.isQtQuick3DSpecularGlossyMaterial()) sourceIndex = 2; if (metaInfo.isQtQuick3DPrincipledMaterial()) targetIndex = 0; else if (metaInfo.isQtQuick3DDefaultMaterial()) targetIndex = 1; else if (metaInfo.isQtQuick3DSpecularGlossyMaterial()) targetIndex = 2; if (sourceIndex >= 0 && targetIndex >= 0) { if (incompatibleProperties.contains(baseColors[sourceIndex])) { copyMap.insert(baseColors[sourceIndex], baseColors[targetIndex]); incompatibleProperties.removeOne(baseColors[sourceIndex]); } if (incompatibleProperties.contains(baseMaps[sourceIndex])) { copyMap.insert(baseMaps[sourceIndex], baseMaps[targetIndex]); incompatibleProperties.removeOne(baseMaps[sourceIndex]); } } const auto ©Keys = copyMap.keys(); Utils::sort(incompatibleProperties); // Create a dialog showing incompatible properties and signals if (!incompatibleProperties.empty()) { QString detailedText = tr("Incompatible properties:
"); for (const auto &p : std::as_const(incompatibleProperties)) detailedText.append("- " + QString::fromUtf8(p) + "
"); detailedText.chop(QString("
").size()); QMessageBox msgBox; msgBox.setTextFormat(Qt::RichText); msgBox.setIcon(QMessageBox::Question); msgBox.setWindowTitle(tr("Change Type")); msgBox.setText(tr("Changing the type from %1 to %2 can't be done without removing incompatible properties.

%3") .arg(m_selectedMaterial.simplifiedTypeName(), typeName, detailedText)); msgBox.setInformativeText(tr("Do you want to continue by removing incompatible properties?")); msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); msgBox.setDefaultButton(QMessageBox::Ok); if (msgBox.exec() == QMessageBox::Cancel) { updatePossibleTypeIndex(); return; } for (const auto &p : std::as_const(incompatibleProperties)) m_selectedMaterial.removeProperty(p); } if (!copyKeys.isEmpty()) { // Copy mapped properties to new name. Note that this will only copy the base // property value and adjust the keyframe groups. Any other bindings related // to the property will be ignored. const QList timeLines = QmlObjectNode(m_selectedMaterial).allTimelines(); for (const auto &key : std::as_const(copyKeys)) { CopyData ©Data = copyMap[key]; for (const auto &timeLineNode : timeLines) { QmlTimeline timeLine(timeLineNode); if (timeLine.hasKeyframeGroup(m_selectedMaterial, key)) { QmlTimelineKeyframeGroup group = timeLine.keyframeGroup(m_selectedMaterial, key); group.setPropertyName(copyData.name); } } // Property value itself cannot be copied until type has been changed, so store it AbstractProperty prop = m_selectedMaterial.property(key); if (prop.isValid()) { if (prop.isBindingProperty()) { copyData.isBinding = true; copyData.value = prop.toBindingProperty().expression(); } else { copyData.value = prop.toVariantProperty().value(); } } m_selectedMaterial.removeProperty(key); } } if (m_selectedMaterial.isRootNode()) rewriterView->changeRootNodeType(metaInfo.typeName(), metaInfo.majorVersion(), metaInfo.minorVersion()); else m_selectedMaterial.changeType(metaInfo.typeName(), metaInfo.majorVersion(), metaInfo.minorVersion()); for (const auto &key : copyKeys) { const CopyData ©Data = copyMap[key]; if (copyData.isBinding) m_selectedMaterial.bindingProperty(copyData.name).setExpression(copyData.value.toString()); else m_selectedMaterial.variantProperty(copyData.name).setValue(copyData.value); } }); } void MaterialEditorContextObject::insertKeyframe(const QString &propertyName) { QTC_ASSERT(m_model && m_model->rewriterView(), return); QTC_ASSERT(m_selectedMaterial.isValid(), return); // Ideally we should not missuse the rewriterView // If we add more code here we have to forward the material editor view RewriterView *rewriterView = m_model->rewriterView(); QmlTimeline timeline = rewriterView->currentTimeline(); QTC_ASSERT(timeline.isValid(), return); rewriterView->executeInTransaction("MaterialEditorContextObject::insertKeyframe", [&] { timeline.insertKeyframe(m_selectedMaterial, propertyName.toUtf8()); }); } int MaterialEditorContextObject::majorVersion() const { return m_majorVersion; } void MaterialEditorContextObject::setMajorVersion(int majorVersion) { if (m_majorVersion == majorVersion) return; m_majorVersion = majorVersion; emit majorVersionChanged(); } bool MaterialEditorContextObject::hasActiveTimeline() const { return m_hasActiveTimeline; } void MaterialEditorContextObject::setHasActiveTimeline(bool b) { if (b == m_hasActiveTimeline) return; m_hasActiveTimeline = b; emit hasActiveTimelineChanged(); } bool MaterialEditorContextObject::hasQuick3DImport() const { return m_hasQuick3DImport; } void MaterialEditorContextObject::setHasQuick3DImport(bool b) { if (b == m_hasQuick3DImport) return; m_hasQuick3DImport = b; emit hasQuick3DImportChanged(); } bool MaterialEditorContextObject::hasMaterialLibrary() const { return m_hasMaterialLibrary; } void MaterialEditorContextObject::setHasMaterialLibrary(bool b) { if (b == m_hasMaterialLibrary) return; m_hasMaterialLibrary = b; emit hasMaterialLibraryChanged(); } bool MaterialEditorContextObject::hasModelSelection() const { return m_hasModelSelection; } void MaterialEditorContextObject::setHasModelSelection(bool b) { if (b == m_hasModelSelection) return; m_hasModelSelection = b; emit hasModelSelectionChanged(); } void MaterialEditorContextObject::setSelectedMaterial(const ModelNode &matNode) { m_selectedMaterial = matNode; } void MaterialEditorContextObject::setSpecificsUrl(const QUrl &newSpecificsUrl) { if (newSpecificsUrl == m_specificsUrl) return; m_specificsUrl = newSpecificsUrl; emit specificsUrlChanged(); } void MaterialEditorContextObject::setSpecificQmlData(const QString &newSpecificQmlData) { if (newSpecificQmlData == m_specificQmlData) return; m_specificQmlData = newSpecificQmlData; delete m_specificQmlComponent; m_specificQmlComponent = nullptr; emit specificQmlComponentChanged(); emit specificQmlDataChanged(); } void MaterialEditorContextObject::setStateName(const QString &newStateName) { if (newStateName == m_stateName) return; m_stateName = newStateName; emit stateNameChanged(); } void MaterialEditorContextObject::setAllStateNames(const QStringList &allStates) { if (allStates == m_allStateNames) return; m_allStateNames = allStates; emit allStateNamesChanged(); } void MaterialEditorContextObject::setPossibleTypes(const QStringList &types) { if (types == m_possibleTypes) return; m_possibleTypes = types; emit possibleTypesChanged(); updatePossibleTypeIndex(); } void MaterialEditorContextObject::setCurrentType(const QString &type) { m_currentType = type.split('.').last(); updatePossibleTypeIndex(); } void MaterialEditorContextObject::setIsBaseState(bool newIsBaseState) { if (newIsBaseState == m_isBaseState) return; m_isBaseState = newIsBaseState; emit isBaseStateChanged(); } void MaterialEditorContextObject::setSelectionChanged(bool newSelectionChanged) { if (newSelectionChanged == m_selectionChanged) return; m_selectionChanged = newSelectionChanged; emit selectionChangedChanged(); } void MaterialEditorContextObject::setBackendValues(QQmlPropertyMap *newBackendValues) { if (newBackendValues == m_backendValues) return; m_backendValues = newBackendValues; emit backendValuesChanged(); } void MaterialEditorContextObject::setModel(Model *model) { m_model = model; } void MaterialEditorContextObject::triggerSelectionChanged() { setSelectionChanged(!m_selectionChanged); } void MaterialEditorContextObject::setHasAliasExport(bool hasAliasExport) { if (m_aliasExport == hasAliasExport) return; m_aliasExport = hasAliasExport; emit hasAliasExportChanged(); } void MaterialEditorContextObject::updatePossibleTypeIndex() { int newIndex = -1; if (!m_currentType.isEmpty()) newIndex = m_possibleTypes.indexOf(m_currentType); // Emit valid possible type index change even if the index doesn't change, as currentIndex on // QML side will change to default internally if model is updated if (m_possibleTypeIndex != -1 || m_possibleTypeIndex != newIndex) { m_possibleTypeIndex = newIndex; emit possibleTypeIndexChanged(); } } void MaterialEditorContextObject::hideCursor() { if (QApplication::overrideCursor()) return; QApplication::setOverrideCursor(QCursor(Qt::BlankCursor)); if (QWidget *w = QApplication::activeWindow()) m_lastPos = QCursor::pos(w->screen()); } void MaterialEditorContextObject::restoreCursor() { if (!QApplication::overrideCursor()) return; QApplication::restoreOverrideCursor(); if (QWidget *w = QApplication::activeWindow()) QCursor::setPos(w->screen(), m_lastPos); } void MaterialEditorContextObject::holdCursorInPlace() { if (!QApplication::overrideCursor()) return; if (QWidget *w = QApplication::activeWindow()) QCursor::setPos(w->screen(), m_lastPos); } int MaterialEditorContextObject::devicePixelRatio() { if (QWidget *w = QApplication::activeWindow()) return w->devicePixelRatio(); return 1; } QStringList MaterialEditorContextObject::allStatesForId(const QString &id) { if (m_model && m_model->rewriterView()) { const QmlObjectNode node = m_model->rewriterView()->modelNodeForId(id); if (node.isValid()) return node.allStateNames(); } return {}; } bool MaterialEditorContextObject::isBlocked(const QString &propName) const { if (!m_selectedMaterial.isValid()) return false; if (!m_model || !m_model->rewriterView()) return false; if (QmlObjectNode(m_selectedMaterial).isBlocked(propName.toUtf8())) return true; return false; } void MaterialEditorContextObject::goIntoComponent() { QTC_ASSERT(m_model, return); DocumentManager::goIntoComponent(m_selectedMaterial); } } // QmlDesigner