// Copyright (C) 2024 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 StudioControls as StudioControls import StudioTheme as StudioTheme import HelperWidgets as HelperWidgets import EffectComposerBackend Item { id: root height: column.implicitHeight + (root.verticalSpacing * 4) readonly property real horizontalSpacing: 10 readonly property real verticalSpacing: 5 readonly property real labelWidth: 140 readonly property real controlWidth: (root.width - labelWidth - root.horizontalSpacing - (StudioTheme.Values.popupMargin * 2)) property var backendModel: EffectComposerBackend.effectComposerModel property string effectNodeName property int typeIndex: 1 property alias displayName: nameText.text property alias uniName: uniNameText.text property alias description: descriptionEdit.text property var minValue: 0 property var maxValue: 1 property var defaultValue: 0 property bool showMinMax: false property var minPossibleValue: 0 property var maxPossibleValue: 0 property bool userAdded: false property bool uniNameEdited: false property real minFloat: root.backendModel.valueLimit("float", false) property real maxFloat: root.backendModel.valueLimit("float", true) property int minInt: root.backendModel.valueLimit("int", false) property int maxInt: root.backendModel.valueLimit("int", true) property var reservedDispNames: [] property var reservedUniNames: [] property bool propNameError: root.reservedDispNames.includes(nameText.text) property bool uniNameError: root.reservedUniNames.includes(uniNameText.text) property var typeList: [ {"type": "int", "controlType": "int"}, {"type": "float", "controlType": "float"}, {"type": "bool", "controlType": "bool"}, {"type": "vec2", "controlType": "vec2"}, {"type": "vec3", "controlType": "vec3"}, {"type": "vec4", "controlType": "vec4"}, {"type": "color", "controlType": "color"}, {"type": "int", "controlType": "channel"}, {"type": "sampler2D", "controlType": "sampler2D"}, {"type": "define", "controlType": "int"}, {"type": "define", "controlType": "bool"} ] property var displayTypes: [ qsTr("Integer"), qsTr("Float"), qsTr("Boolean"), qsTr("Vector 2D"), qsTr("Vector 3D"), qsTr("Vector 4D"), qsTr("Color"), qsTr("Color channel"), qsTr("Texture sampler"), qsTr("Define (integer)"), qsTr("Define (boolean)") ] property vector4d compareMatches: Qt.vector4d(0, 0, 0, 0) // Backend value for sampler controls (UrlChooser needs this to function) property var urlChooserBackendValue: QtObject { id: urlChooserValue property bool isBound: false property var expression property string valueToString property var value signal valueChangedQml() onValueChanged: { urlChooserValue.valueToString = value.toString() urlChooserValue.valueChangedQml() } } signal accepted() signal canceled() // If vector is assigned to variant, it just creates reference by default, which breaks the // min/max logic. This function creates a deep copy of vectors. function copyValue(v) { if (root.typeList[root.typeIndex].type === "vec2") return Qt.vector2d(v.x, v.y) else if (root.typeList[root.typeIndex].type === "vec3") return Qt.vector3d(v.x, v.y, v.z) else if (root.typeList[root.typeIndex].type === "vec4") return Qt.vector4d(v.x, v.y, v.z, v.w) return v } // For vectors, copies values of previous comparison matched subcomponents only function copyMatchedSubcomponents(v, orig) { let isVec2 = root.typeList[root.typeIndex].type === "vec2" let isVec3 = root.typeList[root.typeIndex].type === "vec3" let isVec4 = root.typeList[root.typeIndex].type === "vec4" if (isVec2 || isVec3 || isVec4) { let vx = root.compareMatches.x > 0 ? v.x : orig.x let vy = root.compareMatches.y > 0 ? v.y : orig.y if (isVec3 || isVec4) { let vz = root.compareMatches.z > 0 ? v.z : orig.z if (isVec4) { let vw = root.compareMatches.w > 0 ? v.w : orig.w return Qt.vector4d(vx, vy, vz, vw) } else { return Qt.vector3d(vx, vy, vz) } } else { return Qt.vector2d(vx, vy) } } return v } function showForAdd() { if (typeCombo.currentIndex === 0) reloadType() else typeCombo.currentIndex = 0 root.displayName = root.backendModel.getUniqueDisplayName(root.reservedDispNames) let generatedUniName = root.backendModel.generateUniformName(root.effectNodeName, root.displayName, "") root.initiateUniName(generatedUniName) root.description = "" root.userAdded = true titleLabel.text = qsTr("Add property") root.visible = true } function showForEdit(type, controlType, dispName, name, desc, defVal, min, max, user) { root.uniNameEdited = true let targetIndex = typeList.findIndex(function(element) { return element.type === type && element.controlType === controlType; }) if (targetIndex < 0) return; root.userAdded = user if (typeCombo.currentIndex === targetIndex) reloadType() else typeCombo.currentIndex = targetIndex root.displayName = dispName root.description = desc root.uniName = name if (root.showMinMax) { root.minValue = root.copyValue(root.minPossibleValue) root.maxValue = root.copyValue(root.maxPossibleValue) root.maxValue = root.copyValue(max) root.minValue = root.copyValue(min) minValueLoader.uniformValue = root.copyValue(min) maxValueLoader.uniformValue = root.copyValue(max) } root.defaultValue = root.copyValue(defVal) if (root.urlChooserBackendValue) root.urlChooserBackendValue.value = root.copyValue(root.defaultValue) defaultValueLoader.uniformValue = root.copyValue(defVal) titleLabel.text = qsTr("Edit property") root.visible = true } function propertyData() { let propData = { "type": root.typeList[root.typeIndex].type, "controlType": root.typeList[root.typeIndex].controlType, "displayName": root.displayName, "name": root.uniName, "description": root.description, "minValue": root.minValue, "maxValue": root.maxValue, "defaultValue": root.defaultValue, "userAdded": root.userAdded } return propData; } // Returns true if variant a is considered larger than variant b. // In case of vectors, any subcomponent being larger qualifies. // root.compareMatches subproperties are set to non-zero to indicate which subproperties // were matched. function compareValues(a, b) { let isVec2 = root.typeList[root.typeIndex].type === "vec2" let isVec3 = root.typeList[root.typeIndex].type === "vec3" let isVec4 = root.typeList[root.typeIndex].type === "vec4" if (isVec2 || isVec3 || isVec4) { root.compareMatches = Qt.vector4d(0, 0, 0, 0) let match = false if (a.x > b.x) { match = true root.compareMatches.x = 1 } if (a.y > b.y) { match = true root.compareMatches.y = 1 } if (isVec3 || isVec4) { if (a.z > b.z) { match = true root.compareMatches.z = 1 } if (isVec4) { if (a.w > b.w) { match = true root.compareMatches.w = 1 } } } return match } else { return a > b } } function reloadType() { defaultValueLoader.source = "" minValueLoader.source = "" maxValueLoader.source = "" defaultValueLoader.uniformBackendValue = null var sourceQml let hasMinMax = false if (root.typeList[root.typeIndex].controlType === "int") { root.minPossibleValue = root.minInt root.maxPossibleValue = root.maxInt root.defaultValue = 0 root.minValue = 0 root.maxValue = 100 hasMinMax = true sourceQml = "ValueInt.qml" } else if (root.typeList[root.typeIndex].controlType === "channel") { root.minPossibleValue = root.minInt root.maxPossibleValue = root.maxInt root.defaultValue = 3 root.minValue = 0 root.maxValue = 3 hasMinMax = false // Color channels have hardcoded min/max, so don't show controls sourceQml= "ValueChannel.qml" } else if (root.typeList[root.typeIndex].controlType === "vec2") { root.minPossibleValue = Qt.vector2d(root.minFloat, root.minFloat) root.maxPossibleValue = Qt.vector2d(root.maxFloat, root.maxFloat) root.defaultValue = Qt.vector2d(0, 0) root.minValue = Qt.vector2d(0, 0) root.maxValue = Qt.vector2d(1, 1) hasMinMax = true sourceQml = "ValueVec2.qml" } else if (root.typeList[root.typeIndex].controlType === "vec3") { root.minPossibleValue = Qt.vector3d(root.minFloat, root.minFloat, root.minFloat) root.maxPossibleValue = Qt.vector3d(root.maxFloat, root.maxFloat, root.maxFloat) root.defaultValue = Qt.vector3d(0, 0, 0) root.minValue = Qt.vector3d(0, 0, 0) root.maxValue = Qt.vector3d(1, 1, 1) hasMinMax = true sourceQml = "ValueVec3.qml" } else if (root.typeList[root.typeIndex].controlType === "vec4") { root.minPossibleValue = Qt.vector4d(root.minFloat, root.minFloat, root.minFloat, root.minFloat) root.maxPossibleValue = Qt.vector4d(root.maxFloat, root.maxFloat, root.maxFloat, root.maxFloat) root.defaultValue = Qt.vector4d(0, 0, 0, 0) root.minValue = Qt.vector4d(0, 0, 0, 0) root.maxValue = Qt.vector4d(1, 1, 1, 1) hasMinMax = true sourceQml = "ValueVec4.qml" } else if (root.typeList[root.typeIndex].controlType === "bool") { root.defaultValue = false sourceQml = "ValueBool.qml" } else if (root.typeList[root.typeIndex].controlType === "color") { root.defaultValue = Qt.rgba(0, 0, 0, 1) sourceQml = "ValueColor.qml" } else if (root.typeList[root.typeIndex].controlType === "sampler2D") { root.defaultValue = "" sourceQml = "ValueImage.qml" defaultValueLoader.uniformBackendValue = root.urlChooserBackendValue root.urlChooserBackendValue.value = root.copyValue(root.defaultValue) } else { root.minPossibleValue = root.minFloat root.maxPossibleValue = root.maxFloat root.defaultValue = 0 root.minValue = 0 root.maxValue = 1 hasMinMax = true sourceQml = "ValueFloat.qml" } if (hasMinMax) { minValueLoader.uniformValue = root.copyValue(root.minValue) maxValueLoader.uniformValue = root.copyValue(root.maxValue) minValueLoader.source = sourceQml maxValueLoader.source = sourceQml } defaultValueLoader.uniformValue = root.copyValue(root.defaultValue) defaultValueLoader.source = sourceQml root.showMinMax = hasMinMax } function initiateUniName(name: string) { root.uniName = name root.uniNameEdited = false } onMinValueChanged: { if (!root.showMinMax) return if (root.compareValues(root.minValue, root.maxValue)) { root.maxValue = root.copyMatchedSubcomponents(root.minValue, root.maxValue) maxValueLoader.uniformValue = root.copyValue(root.maxValue) } if (root.compareValues(root.minValue, root.defaultValue)) { root.defaultValue = root.copyMatchedSubcomponents(root.minValue, root.defaultValue) defaultValueLoader.uniformValue = root.copyValue(root.defaultValue) } } onMaxValueChanged: { if (!root.showMinMax) return if (root.compareValues(root.minValue, root.maxValue)) { root.minValue = root.copyMatchedSubcomponents(root.maxValue, root.minValue) minValueLoader.uniformValue = root.copyValue(root.minValue) } if (root.compareValues(root.defaultValue, root.maxValue)) { root.defaultValue = root.copyMatchedSubcomponents(root.maxValue, root.defaultValue) defaultValueLoader.uniformValue = root.copyValue(root.defaultValue) } } onTypeIndexChanged: reloadType() Rectangle { anchors.fill: parent anchors.topMargin: 8 anchors.rightMargin: 8 border.width: 1 border.color: StudioTheme.Values.themeControlOutline color: StudioTheme.Values.themeSectionHeadBackground Column { id: column anchors.fill: parent anchors.topMargin: root.verticalSpacing anchors.bottomMargin: root.verticalSpacing spacing: root.verticalSpacing Text { id: titleLabel color: StudioTheme.Values.themeControlOutlineInteraction font.bold: true font.pixelSize: StudioTheme.Values.baseFontSize width: parent.width height: 30 horizontalAlignment: Text.AlignHCenter } Row { spacing: root.horizontalSpacing width: parent.width Text { id: nameLabel color: StudioTheme.Values.themeTextColor text: qsTr("Display Name") font.pixelSize: StudioTheme.Values.baseFontSize anchors.verticalCenter: parent.verticalCenter width: root.labelWidth elide: Text.ElideRight horizontalAlignment: Text.AlignRight StudioControls.ToolTipArea { anchors.fill: parent text: qsTr("Sets the display name of the property.") } } StudioControls.TextField { id: nameText width: root.controlWidth anchors.verticalCenter: parent.verticalCenter actionIndicatorVisible: false translationIndicatorVisible: false KeyNavigation.tab: uniNameText onTextChanged: { if (!root.uniNameEdited) { let generatedUniName = root.backendModel.generateUniformName( root.effectNodeName, root.displayName, root.uniName) root.initiateUniName(generatedUniName) } } } } // Error line (for invalid name) Row { width: parent.width spacing: root.horizontalSpacing Item { // Spacer width: root.labelWidth height: 1 } Text { text: qsTr("Display name of the property has to be unique.") font.pixelSize: StudioTheme.Values.baseFontSize anchors.verticalCenter: parent.verticalCenter width: root.controlWidth elide: Text.ElideRight horizontalAlignment: Text.AlignLeft visible: root.propNameError color: StudioTheme.Values.themeWarning } } Row { spacing: root.horizontalSpacing width: parent.width Text { color: StudioTheme.Values.themeTextColor text: qsTr("Uniform Name") font.pixelSize: StudioTheme.Values.baseFontSize anchors.verticalCenter: parent.verticalCenter width: root.labelWidth elide: Text.ElideRight horizontalAlignment: Text.AlignRight StudioControls.ToolTipArea { anchors.fill: parent text: qsTr("Sets the uniform name of the property.") } } StudioControls.TextField { id: uniNameText width: root.controlWidth anchors.verticalCenter: parent.verticalCenter actionIndicatorVisible: false translationIndicatorVisible: false text: root.uniName KeyNavigation.tab: descriptionEdit onTextChanged: root.uniNameEdited = true } } // Error line (for invalid uniform name) Row { width: parent.width spacing: root.horizontalSpacing Item { // Spacer width: root.labelWidth height: 1 } Text { text: qsTr("Uniform name has to be unique.") font.pixelSize: StudioTheme.Values.baseFontSize anchors.verticalCenter: parent.verticalCenter width: root.controlWidth elide: Text.ElideRight horizontalAlignment: Text.AlignLeft visible: root.uniNameError color: StudioTheme.Values.themeWarning } } Row { spacing: root.horizontalSpacing width: parent.width Text { color: StudioTheme.Values.themeTextColor text: qsTr("Description") font.pixelSize: StudioTheme.Values.baseFontSize anchors.verticalCenter: parent.verticalCenter width: root.labelWidth elide: Text.ElideRight horizontalAlignment: Text.AlignRight StudioControls.ToolTipArea { anchors.fill: parent text: qsTr("Sets the property description.") } } Rectangle { color: StudioTheme.Values.controlStyle.background.idle border.color: StudioTheme.Values.controlStyle.border.idle border.width: StudioTheme.Values.controlStyle.borderWidth height: descriptionEdit.height width: root.controlWidth clip: true TextEdit { id: descriptionEdit padding: StudioTheme.Values.controlStyle.inputHorizontalPadding width: parent.width height: descriptionEdit.contentHeight + descriptionEdit.topPadding + descriptionEdit.bottomPadding font.pixelSize: StudioTheme.Values.baseFontSize color: StudioTheme.Values.themeTextColor wrapMode: TextEdit.WordWrap KeyNavigation.tab: typeCombo } } } Row { width: parent.width spacing: root.horizontalSpacing Text { color: StudioTheme.Values.themeTextColor text: qsTr("Type") font.pixelSize: StudioTheme.Values.baseFontSize anchors.verticalCenter: parent.verticalCenter width: root.labelWidth elide: Text.ElideRight horizontalAlignment: Text.AlignRight StudioControls.ToolTipArea { anchors.fill: parent text: qsTr("Sets the property type.") } } StudioControls.ComboBox { id: typeCombo width: root.controlWidth model: root.displayTypes actionIndicatorVisible: false onCurrentIndexChanged: root.typeIndex = currentIndex } } Row { width: parent.width spacing: root.horizontalSpacing Text { color: StudioTheme.Values.themeTextColor text: qsTr("Default Value") font.pixelSize: StudioTheme.Values.baseFontSize anchors.verticalCenter: parent.verticalCenter width: root.labelWidth elide: Text.ElideRight horizontalAlignment: Text.AlignRight StudioControls.ToolTipArea { anchors.fill: parent text: qsTr("Sets the default value of the property.") } } Loader { id: defaultValueLoader property var uniformValue // No need to deep copy min/max as these bindings do not change the value property var uniformMinValue: root.minValue property var uniformMaxValue: root.maxValue // These are needed for sampler controls (ValueImage type) property var uniformBackendValue property string uniformName: root.uniName property var uniformDefaultValue width: root.controlWidth Connections { target: defaultValueLoader.item function onValueChanged() { root.defaultValue = root.copyValue(defaultValueLoader.uniformValue) } } } } Row { width: parent.width spacing: root.horizontalSpacing visible: root.showMinMax Text { color: StudioTheme.Values.themeTextColor text: qsTr("Min Value") font.pixelSize: StudioTheme.Values.baseFontSize anchors.verticalCenter: parent.verticalCenter width: root.labelWidth elide: Text.ElideRight horizontalAlignment: Text.AlignRight StudioControls.ToolTipArea { anchors.fill: parent text: qsTr("Sets the minimum value of the property.") } } Loader { id: minValueLoader property var uniformValue // No need to deep copy min/max as these bindings do not change the value property var uniformMinValue: root.minPossibleValue property var uniformMaxValue: root.maxValue width: root.controlWidth Connections { target: minValueLoader.item function onValueChanged() { root.minValue = root.copyValue(minValueLoader.uniformValue) } } onLoaded: { if (root.typeList[root.typeIndex].controlType === "int" || root.typeList[root.typeIndex].controlType === "float") { item.hideSlider = true } } } } Row { width: parent.width spacing: root.horizontalSpacing visible: root.showMinMax Text { color: StudioTheme.Values.themeTextColor text: qsTr("Max Value") font.pixelSize: StudioTheme.Values.baseFontSize anchors.verticalCenter: parent.verticalCenter width: root.labelWidth elide: Text.ElideRight horizontalAlignment: Text.AlignRight StudioControls.ToolTipArea { anchors.fill: parent text: qsTr("Sets the maximum value of the property.") } } Loader { id: maxValueLoader property var uniformValue // No need to deep copy min/max as these bindings do not change the value property var uniformMinValue: root.minValue property var uniformMaxValue: root.maxPossibleValue width: root.controlWidth Connections { target: maxValueLoader.item function onValueChanged() { root.maxValue = root.copyValue(maxValueLoader.uniformValue) } } onLoaded: { if (root.typeList[root.typeIndex].controlType === "int" || root.typeList[root.typeIndex].controlType === "float") { item.hideSlider = true } } } } Row { id: buttonRow width: acceptButton.width + root.horizontalSpacing + cancelButton.width spacing: root.horizontalSpacing x: (parent.width - buttonRow.width) / 2 + 6 height: 30 HelperWidgets.Button { id: cancelButton width: 100 height: 30 text: qsTr("Cancel") padding: 4 onClicked: { root.canceled() root.visible = false } } HelperWidgets.Button { id: acceptButton width: 100 height: 30 text: qsTr("Apply") padding: 4 enabled: !root.propNameError && !root.uniNameError onClicked: { // Remove the focus from the editing control. It fixes a mac bug where a // control's value doesn't get applied when the Apply button is clicked acceptButton.forceActiveFocus() root.accepted() root.visible = false } } } } } }