forked from qt-creator/qt-creator
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:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user