// Copyright (C) 2023 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.Window import QtQuick.Controls import QtQuick.Layouts import QtQuick.Dialogs import QtCore import QtQuick3D.MaterialEditor ApplicationWindow { id: window height: 720 width: 1024 visible: true title: qsTr("Custom Material Editor") // Context property (see main.cpp) property url projectFolder: _qtProjectDir // qmllint disable unqualified Settings { id: settings property alias windowX: window.x property alias windowY: window.y property alias windowWidth: window.width property alias windowHeight: window.height property alias windowVisibility: window.visibility } Component.onCompleted: { mainSplitView.restoreState(settings.value("ui/mainSplitView")) editorView.restoreState(settings.value("ui/editorView")) } Component.onDestruction: { settings.setValue("ui/mainSplitView", mainSplitView.saveState()) settings.setValue("ui/editorView", editorView.saveState()) } QtObject { id: resourceStore objectName: "QtQuick3DResourceStorePrivate" } FileDialog { id: openMaterialDialog title: "Open a Material Project File" nameFilters: [ "Material Editor Project (*.qmp)"] currentFolder: window.projectFolder onAccepted: { if (openMaterialDialog.selectedFile !== null) materialAdapter.loadMaterial(openMaterialDialog.selectedFile); } } FileDialog { id: saveAsDialog fileMode: FileDialog.SaveFile currentFolder: window.projectFolder nameFilters: [ "Material Editor Project (*.qmp)"] onAccepted: materialAdapter.saveMaterial(selectedFile) } FileDialog { id: fragmentShaderImportDialog title: "Fragment Shader to import" nameFilters: [ "Fragment Shader (*.frag *.fs *.glsl)" ] currentFolder: window.projectFolder onAccepted: { if (fragmentShaderImportDialog.selectedFile !== null) { materialAdapter.importFragmentShader(fragmentShaderImportDialog.selectedFile) } } } FileDialog { id: vertexShaderImportDialog title: "Vertex Shader to import" nameFilters: [ "Vertex Shader (*.vert *.vs *.glsl)" ] currentFolder: window.projectFolder onAccepted: { if (vertexShaderImportDialog.selectedFile !== null) { materialAdapter.importVertexShader(vertexShaderImportDialog.selectedFile) } } } FileDialog { id: saveCompFileDialog title: "Choose file" nameFilters: [ "QML Componen (*.qml)" ] fileMode: FileDialog.SaveFile currentFolder: window.projectFolder onAccepted: { if (selectedFile !== null) componentFilePath.text = selectedFile } } RegularExpressionValidator { id: nameValidator regularExpression: /[a-zA-Z0-9_-]*/ } Dialog { id: exportMaterialDialog title: "Export material" anchors.centerIn: parent ColumnLayout { id: exportFiles anchors.fill: parent spacing: 1 RowLayout { Text { text: qsTr("Component") color: palette.text } TextField { id: componentFilePath readOnly: true } Button { text: qsTr("Choose...") onClicked: { saveCompFileDialog.open() exportMaterialDialog.aboutToHide() } } } RowLayout { Text { text: qsTr("Vertex:") color: palette.text } TextField { id: vertexFilename enabled: (editorView.vertexEditor.text !== "") validator: nameValidator } } RowLayout { Text { text: qsTr("Fragment:") color: palette.text } TextField { id: fragmentFilename enabled: (editorView.fragmentEditor.text !== "") validator: nameValidator } } DialogButtonBox { Button { text: qsTr("Export") enabled: (componentFilePath.text !== "" && (!vertexFilename.enabled || (vertexFilename.enabled && vertexFilename.text !== "")) && (!fragmentFilename.enabled || (fragmentFilename.enabled && fragmentFilename.text !== ""))) DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole onClicked: exportMaterialDialog.accept() } Button { text: qsTr("Cancel") DialogButtonBox.buttonRole: DialogButtonBox.DestructiveRole onClicked: exportMaterialDialog.reject() } } } onAccepted: { materialAdapter.exportQmlComponent(componentFilePath.text, vertexFilename.text, fragmentFilename.text) } } SaveChangesDialog { id: saveChangesDialog materialAdapter: materialAdapter saveAsDialog: saveAsDialog anchors.centerIn: parent } AboutDialog { id: aboutDialog parent: Overlay.overlay anchors.centerIn: parent } function saveAction() { // 1. No file name(s) given (call saveAs) let materialSaveFileUrl = new URL(materialAdapter.materialSaveFile) if (materialSaveFileUrl.toString().length > 0) materialAdapter.save() else saveAsAction() } function openAction() { openMaterialDialog.open() } function newAction() { saveChangesDialog.doIfChangesSavedOrDiscarded(() => { materialAdapter.reset() }); materialAdapter.reset() } function saveAsAction() { saveAsDialog.open() } function quitAction() { Qt.quit() } function aboutAction() { aboutDialog.open() } function importFragmentShader() { fragmentShaderImportDialog.open() } function importVertexShader() { vertexShaderImportDialog.open() } function exportMaterial() { exportMaterialDialog.open() } menuBar: MenuBar { Menu { title: qsTr("&File") Action { text: qsTr("&New..."); onTriggered: window.newAction(); } Action { text: qsTr("&Open..."); onTriggered: window.openAction(); } Action { text: qsTr("&Save"); onTriggered: window.saveAction(); } Action { text: qsTr("Save &As..."); onTriggered: window.saveAsAction(); } MenuSeparator { } Menu { title: qsTr("Import") Action { text: qsTr("Fragment Shader"); onTriggered: window.importFragmentShader(); } Action { text: qsTr("Vertex Shader"); onTriggered: window.importVertexShader(); } } Action { text: qsTr("Export"); onTriggered: window.exportMaterial(); } MenuSeparator { } Action { text: qsTr("&Quit"); onTriggered: window.quitAction(); } } Menu { title: qsTr("&Help") Action { text: qsTr("&About"); onTriggered: window.aboutAction(); } } } SplitView { id: mainSplitView anchors.fill: parent orientation: Qt.Horizontal EditorView { id: editorView vertexTabText: "Vertex Shader" fragmentTabText: "Fragment Shader" SplitView.preferredWidth: window.width * 0.5 SplitView.fillWidth: true materialAdapter: materialAdapter instanceEntry: preview.instanceEntry targetModel: preview.modelInstance } Preview { id: preview implicitWidth: parent.width * 0.5 currentMaterial: materialAdapter.material } } function outputLine(lineText) { // Prepend editorView.outputTextItem.text = lineText + "\n" + editorView.outputTextItem.text; } function printShaderStatusError(stage, msg) { let outputString = "" outputString += msg.filename + " => " + msg.message if (msg.identifier !== null && msg.identifier !== "") outputString += " '" + msg.identifier + "'"; if (msg.line >= 0) outputString += ", on line: " + msg.line outputLine(outputString) } MaterialAdapter { id: materialAdapter vertexShader: editorView.vertexEditor.text fragmentShader: editorView.fragmentEditor.text rootNode: preview.rootNode uniformModel: editorView.uniformModel onVertexStatusChanged: { if (vertexStatus.status !== ShaderConstants.Success) { editorView.tabBarInfoView.currentIndex = 1 window.printShaderStatusError(ShaderConstants.Vertex, vertexStatus) } else if (fragmentStatus.status === ShaderConstants.Success){ // both work, clear editorView.outputTextItem.text = ""; } } onFragmentStatusChanged: { if (fragmentStatus.status !== ShaderConstants.Success) { editorView.tabBarInfoView.currentIndex = 1 window.printShaderStatusError(ShaderConstants.Fragment, fragmentStatus) } else if (vertexStatus.status === ShaderConstants.Success) { // both work, clear editorView.outputTextItem.text = ""; } } onVertexShaderChanged: { editorView.vertexEditor.text = materialAdapter.vertexShader } onFragmentShaderChanged: { editorView.fragmentEditor.text = materialAdapter.fragmentShader } } }