From 0e32bb4beaaac456b3e2d988d89971e7a53b6418 Mon Sep 17 00:00:00 2001 From: Ali Kianian Date: Fri, 21 Feb 2025 18:27:00 +0200 Subject: [PATCH] QmlDesigner: Move material editor qml side as a property editor pane Task-number: QDS-14624 Change-Id: Ibf277846bf99370cecb8ec3af28117872eaaef21 Reviewed-by: Miikka Heikkinen Reviewed-by: Mahmoud Badri --- .../QtQuick3D/Material/ColorEditorPopup.qml | 62 ++++ .../QtQuick3D/Material/Preview.qml | 266 ++++++++++++++++++ .../QtQuick3D/Material/Toolbar.qml | 50 ++++ .../QtQuick3D/Material/TopSection.qml | 93 ++++++ .../QtQuick3D/MaterialPane.qml | 133 +++++++++ 5 files changed, 604 insertions(+) create mode 100644 share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick3D/Material/ColorEditorPopup.qml create mode 100644 share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick3D/Material/Preview.qml create mode 100644 share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick3D/Material/Toolbar.qml create mode 100644 share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick3D/Material/TopSection.qml create mode 100644 share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick3D/MaterialPane.qml diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick3D/Material/ColorEditorPopup.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick3D/Material/ColorEditorPopup.qml new file mode 100644 index 00000000000..6ea3428a71b --- /dev/null +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick3D/Material/ColorEditorPopup.qml @@ -0,0 +1,62 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import QtQuick +import StudioControls as StudioControls + +StudioControls.PopupDialog { + id: colorPopup + + property QtObject loaderItem: loader.item + property color originalColor + required property color currentColor + + signal activateColor(color : color) + + width: 260 + + onOriginalColorChanged: loader.updateOriginalColor() + onClosing: loader.active = false + + function open(showItem) { + loader.ensureActive() + colorPopup.show(showItem) + + loader.updateOriginalColor() + } + + Loader { + id: loader + + function ensureActive() { + if (!loader.active) + loader.active = true + } + + function updateOriginalColor() { + if (loader.status === Loader.Ready) + loader.item.originalColor = colorPopup.originalColor + } + + sourceComponent: StudioControls.ColorEditorPopup { + width: colorPopup.contentWidth + visible: colorPopup.visible + + onActivateColor: (color) => { + colorPopup.activateColor(color) + } + } + + Binding { + target: loader.item + property: "color" + value: colorPopup.currentColor + when: loader.status === Loader.Ready + } + + onLoaded: { + loader.updateOriginalColor() + colorPopup.titleBar = loader.item.titleBarContent + } + } +} diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick3D/Material/Preview.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick3D/Material/Preview.qml new file mode 100644 index 00000000000..8dd04372b06 --- /dev/null +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick3D/Material/Preview.qml @@ -0,0 +1,266 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import QtQuick +import HelperWidgets 2.0 as HelperWidgets +import StudioControls as StudioControls +import StudioTheme as StudioTheme + +Rectangle { + id: root + + property HelperWidgets.QmlMaterialNodeProxy backend: materialNodeBackend + property alias pinned: pinButton.checked + property alias showPinButton: pinButton.visible + + property StudioTheme.ControlStyle buttonStyle: StudioTheme.ViewBarButtonStyle { + // This is how you can override stuff from the control styles + baseIconFontSize: StudioTheme.Values.bigIconFontSize + } + + Connections { + target: HelperWidgets.Controller + + function onCloseContextMenu() { + root.closeContextMenu() + } + } + + implicitHeight: image.height + + clip: true + color: "#000000" + + // Called from C++ to close context menu on focus out + function closeContextMenu() + { + modelMenu.close() + envMenu.close() + } + + function refreshPreview() + { + image.source = "" + image.source = "image://nodeInstance/preview" + } + + + Connections { + target: root.backend + + function onPreviewEnvChanged() { + envMenu.updateEnvParams(backend.previewEnv) + root.refreshPreview() + } + + function onPreviewModelChanged() { + root.refreshPreview() + } + } + + Image { + id: image + + anchors.fill: parent + fillMode: Image.PreserveAspectFit + + source: "image://nodeInstance/preview" + cache: false + smooth: true + + sourceSize.width: image.width + sourceSize.height: image.height + + Rectangle { + id: toolbarRect + + radius: 10 + color: StudioTheme.Values.themeToolbarBackground + width: optionsToolbar.width + 2 * toolbarRect.radius + height: optionsToolbar.height + toolbarRect.radius + anchors.left: parent.left + anchors.leftMargin: -toolbarRect.radius + anchors.verticalCenter: parent.verticalCenter + + Column { + id: optionsToolbar + + spacing: 5 + anchors.centerIn: parent + anchors.horizontalCenterOffset: optionsToolbar.spacing + + HelperWidgets.AbstractButton { + id: pinButton + + style: root.buttonStyle + buttonIcon: pinButton.checked ? StudioTheme.Constants.pin : StudioTheme.Constants.unpin + checkable: true + } + + HelperWidgets.AbstractButton { + id: previewEnvMenuButton + + style: root.buttonStyle + buttonIcon: StudioTheme.Constants.textures_medium + tooltip: qsTr("Select preview environment.") + onClicked: envMenu.popup() + } + + HelperWidgets.AbstractButton { + id: previewModelMenuButton + + style: root.buttonStyle + buttonIcon: StudioTheme.Constants.cube_medium + tooltip: qsTr("Select preview model.") + onClicked: modelMenu.popup() + } + } + } + } + + StudioControls.Menu { + id: modelMenu + + closePolicy: StudioControls.Menu.CloseOnEscape | StudioControls.Menu.CloseOnPressOutside + + ListModel { + id: modelMenuModel + ListElement { + modelName: qsTr("Cone") + modelStr: "#Cone" + } + ListElement { + modelName: qsTr("Cube") + modelStr: "#Cube" + } + ListElement { + modelName: qsTr("Cylinder") + modelStr: "#Cylinder" + } + ListElement { + modelName: qsTr("Sphere") + modelStr: "#Sphere" + } + } + + Repeater { + model: modelMenuModel + StudioControls.MenuItemWithIcon { + text: modelName + onClicked: root.backend.previewModel = modelStr + checkable: true + checked: root.backend.previewModel === modelStr + } + } + } + + StudioControls.Menu { + id: envMenu + + property string previewEnvName + property string previewEnvValue + + signal envParametersChanged() + + closePolicy: StudioControls.Menu.CloseOnEscape | StudioControls.Menu.CloseOnPressOutside + + Component.onCompleted: envMenu.updateEnvParams(root.backend.previewEnv) + + function updateEnvParams(str: string) { + let eqFound = str.lastIndexOf("=") + let newEnvName = (eqFound > 0) ? str.substr(0, eqFound) : str + let newEnvValue = (eqFound > 0) ? str.substr(eqFound + 1, str.length - eqFound) : "" + + if (envMenu.previewEnvName !== newEnvName + || envMenu.previewEnvValue !== newEnvValue) { + envMenu.previewEnvName = newEnvName + envMenu.previewEnvValue = newEnvValue + envMenu.envParametersChanged() + } + } + + EnvMenuItem { + envName: qsTr("Basic") + envStr: "Basic" + } + + EnvMenuItem { + id: colorItem + + property color color + property bool colorIsValid: false + + envName: qsTr("Color") + envStr: "Color" + checked: false + + Component.onCompleted: update() + onColorIsValidChanged: updatePopupOriginalColor() + + onClicked: { + colorItem.updatePopupOriginalColor() + colorPopup.open(colorItem) + } + + onColorChanged: { + colorItem.envStr = colorItem.checked + ? "Color=" + color.toString() + : "Color" + colorItem.commit() + } + + function updatePopupOriginalColor() { + if (colorItem.colorIsValid) + colorPopup.originalColor = colorItem.color + } + + function update() { + colorItem.checked = envMenu.previewEnvName === "Color" + if (colorItem.checked && envMenu.previewEnvValue) { + colorItem.color = envMenu.previewEnvValue + colorItem.colorIsValid = true + } else { + colorItem.colorIsValid = false + } + } + + Connections { + target: envMenu + function onEnvParametersChanged() { + colorItem.update(); + } + } + } + + EnvMenuItem { + envName: qsTr("Studio") + envStr: "SkyBox=preview_studio" + } + + EnvMenuItem { + envName: qsTr("Landscape") + envStr: "SkyBox=preview_landscape" + } + } + + ColorEditorPopup { + id: colorPopup + + currentColor: colorItem.color + onActivateColor: (color) => colorItem.color = color + } + + component EnvMenuItem: StudioControls.MenuItemWithIcon { + required property string envName + property string envStr + + function commit() { + root.backend.previewEnv = envStr + } + + text: envName + onClicked: commit() + checkable: false + checked: root.backend.previewEnv === envStr + } +} diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick3D/Material/Toolbar.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick3D/Material/Toolbar.qml new file mode 100644 index 00000000000..b3e5a7e147c --- /dev/null +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick3D/Material/Toolbar.qml @@ -0,0 +1,50 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import QtQuick +import HelperWidgets 2.0 +import HelperWidgets 2.0 as HelperWidgets +import StudioTheme as StudioTheme +Rectangle { + id: root + + property HelperWidgets.QmlMaterialNodeProxy backend: materialNodeBackend + + color: StudioTheme.Values.themeToolbarBackground + height: StudioTheme.Values.toolbarHeight + + Row { + id: row + spacing: StudioTheme.Values.toolbarSpacing + anchors.verticalCenter: parent.verticalCenter + leftPadding: 6 + + HelperWidgets.AbstractButton { + style: StudioTheme.Values.viewBarButtonStyle + buttonIcon: StudioTheme.Constants.apply_medium + tooltip: qsTr("Apply material to selected model.") + onClicked: root.backend.toolBarAction(QmlMaterialNodeProxy.ApplyToSelected) + } + + HelperWidgets.AbstractButton { + style: StudioTheme.Values.viewBarButtonStyle + buttonIcon: StudioTheme.Constants.create_medium + tooltip: qsTr("Create new material.") + onClicked: backend.toolBarAction(QmlMaterialNodeProxy.AddNewMaterial) + } + + HelperWidgets.AbstractButton { + style: StudioTheme.Values.viewBarButtonStyle + buttonIcon: StudioTheme.Constants.delete_medium + tooltip: qsTr("Delete current material.") + onClicked: backend.toolBarAction(QmlMaterialNodeProxy.DeleteCurrentMaterial) + } + + HelperWidgets.AbstractButton { + style: StudioTheme.Values.viewBarButtonStyle + buttonIcon: StudioTheme.Constants.materialBrowser_medium + tooltip: qsTr("Open material browser.") + onClicked: backend.toolBarAction(QmlMaterialNodeProxy.OpenMaterialBrowser) + } + } +} diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick3D/Material/TopSection.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick3D/Material/TopSection.qml new file mode 100644 index 00000000000..b52befa0a0f --- /dev/null +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick3D/Material/TopSection.qml @@ -0,0 +1,93 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import QtQuick +import QtQuick.Controls +import HelperWidgets 2.0 as HelperWidgets +import StudioControls as StudioControls +import StudioTheme as StudioTheme + +StudioControls.SplitView { + id: root + + property HelperWidgets.QmlMaterialNodeProxy backend: materialNodeBackend + property alias showImage: previewLoader.active + property Component previewComponent: null + + width: parent.width + implicitHeight: showImage ? previewLoader.implicitHeight + nameSection.implicitHeight : nameSection.implicitHeight + + orientation: Qt.Vertical + + Loader { + id: previewLoader + + SplitView.fillWidth: true + SplitView.minimumWidth: 152 + SplitView.preferredHeight: previewLoader.visible ? Math.min(root.width * 0.75, 400) : 0 + SplitView.minimumHeight: previewLoader.visible ? 150 : 0 + SplitView.maximumHeight: previewLoader.visible ? 600 : 0 + + visible: previewLoader.active && previewLoader.item + + sourceComponent: root.previewComponent + } + + HelperWidgets.Section { + id: nameSection + + // Section with hidden header is used so properties are aligned with the other sections' properties + hideHeader: true + SplitView.fillWidth: true + SplitView.preferredHeight: implicitHeight + SplitView.maximumHeight: implicitHeight + bottomPadding: StudioTheme.Values.sectionPadding * 2 + collapsible: false + + HelperWidgets.SectionLayout { + HelperWidgets.PropertyLabel { text: qsTr("Name") } + + HelperWidgets.SecondColumnLayout { + HelperWidgets.Spacer { implicitWidth: StudioTheme.Values.actionIndicatorWidth } + + HelperWidgets.LineEdit { + id: matName + + implicitWidth: StudioTheme.Values.singleControlColumnWidth + width: StudioTheme.Values.singleControlColumnWidth + placeholderText: qsTr("Material name") + showTranslateCheckBox: false + showExtendedFunctionButton: false + + Timer { + running: true + interval: 0 + onTriggered: matName.backendValue = backendValues.objectName + // backendValues.objectName is not available yet without the Timer + } + + // allow only alphanumeric characters, underscores, no space at start, and 1 space between words + validator: RegularExpressionValidator { regularExpression: /^(\w+\s)*\w+$/ } + } + + HelperWidgets.ExpandingSpacer {} + } + + HelperWidgets.PropertyLabel { text: qsTr("Type") } + + HelperWidgets.SecondColumnLayout { + HelperWidgets.Spacer { implicitWidth: StudioTheme.Values.actionIndicatorWidth } + + HelperWidgets.ComboBox { + currentIndex: backend.possibleTypeIndex + model: backend.possibleTypes + showExtendedFunctionButton: false + implicitWidth: StudioTheme.Values.singleControlColumnWidth + enabled: backend.possibleTypes.length > 1 + + onActivated: changeTypeName(currentValue) + } + } + } + } +} diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick3D/MaterialPane.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick3D/MaterialPane.qml new file mode 100644 index 00000000000..13e41c8506b --- /dev/null +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick3D/MaterialPane.qml @@ -0,0 +1,133 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import QtCore +import QtQuick +import QtQuick.Controls +import HelperWidgets 2.0 +import StudioControls 1.0 as StudioControls +import "Material" as Material + +Item { + id: root + + width: 420 + height: 420 + + // invoked from C++ to refresh material preview image + signal refreshPreview() + + // Called from C++ to close context menu on focus out + function closeContextMenu() + { + Controller.closeContextMenu() + } + + Material.Toolbar { + id: toolbar + + width: parent.width + } + + Settings { + id: settings + + property var topSection + property bool dockMode + } + + StudioControls.SplitView { + id: splitView + + readonly property bool isHorizontal: splitView.orientation == Qt.Horizontal + + anchors.top: toolbar.bottom + anchors.bottom: parent.bottom + width: parent.width + orientation: splitView.width > 1000 ? Qt.Horizontal : Qt.Vertical + clip: true + + Loader { + id: leftSideView + + SplitView.fillWidth: leftSideView.visible + SplitView.fillHeight: leftSideView.visible + SplitView.minimumWidth: leftSideView.visible ? 300 : 0 + SplitView.minimumHeight: leftSideView.visible ? 300 : 0 + + active: splitView.isHorizontal + visible: leftSideView.active && leftSideView.item + + sourceComponent: PreviewComponent {} + } + + PropertyEditorPane { + id: itemPane + + clip: true + SplitView.fillWidth: !leftSideView.visible + SplitView.fillHeight: true + SplitView.minimumWidth: leftSideView.visible ? 400 : 0 + SplitView.maximumWidth: leftSideView.visible ? 800 : -1 + + headerDocked: !leftSideView.visible && settings.dockMode + + headerComponent: Material.TopSection { + id: topSection + + Component.onCompleted: topSection.restoreState(settings.topSection) + Component.onDestruction: settings.topSection = topSection.saveState() + previewComponent: PreviewComponent {} + showImage: !splitView.isHorizontal + } + + DynamicPropertiesSection { + propertiesModel: SelectionDynamicPropertiesModel {} + visible: !hasMultiSelection + } + + Loader { + id: specificsTwo + + property string theSource: specificQmlData + + width: itemPane.width + visible: specificsTwo.theSource !== "" + sourceComponent: specificQmlComponent + + onTheSourceChanged: { + specificsTwo.active = false + specificsTwo.active = true + } + } + + Item { // spacer + width: 1 + height: 10 + visible: specificsTwo.visible + } + + Loader { + id: specificsOne + width: itemPane.width + source: specificsUrl + } + } + } + + component PreviewComponent : Material.Preview { + id: previewItem + + pinned: settings.dockMode + showPinButton: !leftSideView.visible + onPinnedChanged: settings.dockMode = previewItem.pinned + + Connections { + target: root + + function onRefreshPreview() { + previewItem.refreshPreview() + } + } + } +}