QmlDesigner: Move material editor qml side as a property editor pane

Task-number: QDS-14624
Change-Id: Ibf277846bf99370cecb8ec3af28117872eaaef21
Reviewed-by: Miikka Heikkinen <miikka.heikkinen@qt.io>
Reviewed-by: Mahmoud Badri <mahmoud.badri@qt.io>
This commit is contained in:
Ali Kianian
2025-02-21 18:27:00 +02:00
parent 7222a14808
commit 0e32bb4bea
5 changed files with 604 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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