import QtQuick import QtQuick.Layouts import QtQuick.Controls import QtQuick.Dialogs import MaterialIcons 2.2 import Utils 1.0 import Controls 1.0 import "AttributeControls" as AttributeControls /** * Instantiate a control to visualize and edit an Attribute based on its type. */ RowLayout { id: root property variant attribute: null property bool readOnly: false // Whether the attribute's value can be modified property bool objectsHideable: true property string filterText: "" property alias label: parameterLabel // Accessor to the internal Label (attribute's name) property int labelWidth // Shortcut to set the fixed size of the Label readonly property bool editable: !attribute.isOutput && !attribute.isLink && !readOnly signal doubleClicked(var mouse, var attr) spacing: 2 function updateAttributeLabel() { background.color = attribute.validValue ? Qt.darker(palette.window, 1.1) : Qt.darker(Colors.red, 1.5) if (attribute.desc) { var tooltip = "" if (!attribute.validValue && attribute.desc.errorMessage !== "") tooltip += "Error: " + Format.plainToHtml(attribute.desc.errorMessage) + "

" tooltip += " " + attribute.desc.name + ": " + attribute.type + "
" + Format.plainToHtml(attribute.desc.description) parameterTooltip.text = tooltip } } Pane { background: Rectangle { id: background color: object != undefined && object.validValue ? Qt.darker(parent.palette.window, 1.1) : Qt.darker(Colors.red, 1.5) } padding: 0 Layout.preferredWidth: labelWidth || implicitWidth Layout.fillHeight: true RowLayout { spacing: 0 width: parent.width height: parent.height Label { id: parameterLabel Layout.fillHeight: true Layout.fillWidth: true horizontalAlignment: attribute.isOutput ? Qt.AlignRight : Qt.AlignLeft elide: Label.ElideRight padding: 5 wrapMode: Label.WrapAtWordBoundaryOrAnywhere text: object.label color: { if (object != undefined && (object.hasOutputConnections || object.isLink) && !object.enabled) return Colors.lightgrey else return palette.text } // Tooltip hint with attribute's description ToolTip { id: parameterTooltip // Position in y at mouse position y: parameterMA.mouseY + 10 text: { var tooltip = "" if (!object.validValue && object.desc.errorMessage !== "") tooltip += "Error: " + Format.plainToHtml(object.desc.errorMessage) + "

" tooltip += "" + object.desc.name + ": " + attribute.type + "
" + Format.plainToHtml(object.description) return tooltip } visible: parameterMA.containsMouse delay: 800 } // Make label bold if attribute's value is not the default one font.bold: !object.isOutput && !object.isDefault // Make label italic if attribute is a link font.italic: object.isLink MouseArea { id: parameterMA anchors.fill: parent hoverEnabled: true acceptedButtons: Qt.AllButtons onDoubleClicked: function(mouse) { root.doubleClicked(mouse, root.attribute) } property Component menuComp: Menu { id: paramMenu property bool isFileAttribute: attribute.type === "File" property bool isFilepath: isFileAttribute && Filepath.isFile(attribute.evalValue) MenuItem { text: "Reset To Default Value" enabled: root.editable && !attribute.isDefault onTriggered: { _reconstruction.resetAttribute(attribute) updateAttributeLabel() } } MenuItem { text: "Copy" enabled: attribute.value != "" onTriggered: { Clipboard.clear() Clipboard.setText(attribute.value) } } MenuItem { text: "Paste" enabled: Clipboard.getText() != "" && root.editable onTriggered: { _reconstruction.setAttribute(attribute, Clipboard.getText()) } } MenuSeparator { visible: paramMenu.isFileAttribute height: visible ? implicitHeight : 0 } MenuItem { visible: paramMenu.isFileAttribute height: visible ? implicitHeight : 0 text: paramMenu.isFilepath ? "Open Containing Folder" : "Open Folder" onClicked: paramMenu.isFilepath ? Qt.openUrlExternally(Filepath.dirname(attribute.evalValue)) : Qt.openUrlExternally(Filepath.stringToUrl(attribute.evalValue)) } MenuItem { visible: paramMenu.isFilepath height: visible ? implicitHeight : 0 text: "Open File" onClicked: Qt.openUrlExternally(Filepath.stringToUrl(attribute.value)) } } onClicked: function(mouse) { forceActiveFocus() if (mouse.button == Qt.RightButton) { var menu = menuComp.createObject(parameterLabel) menu.parent = parameterLabel menu.popup() } } } } MaterialLabel { visible: attribute.desc.advanced text: MaterialIcons.build color: palette.mid font.pointSize: 8 padding: 4 } } } function setTextFieldAttribute(value) { // editingFinished called even when TextField is readonly if (!editable) return switch (attribute.type) { case "IntParam": case "FloatParam": _reconstruction.setAttribute(root.attribute, Number(value)) updateAttributeLabel() break case "File": _reconstruction.setAttribute(root.attribute, value) break default: _reconstruction.setAttribute(root.attribute, value.trim()) updateAttributeLabel() break } } Loader { Layout.fillWidth: true sourceComponent: { // PushButtonParam always has value == undefined, so it needs to be excluded from this check if (attribute.type != "PushButtonParam" && attribute.value === undefined) { return notComputedComponent } switch (attribute.type) { case "PushButtonParam": return pushButtonComponent case "ChoiceParam": return attribute.desc.exclusive ? choiceComponent : choiceMultiComponent case "IntParam": return sliderComponent case "FloatParam": if (attribute.desc.semantic === 'color/hue') return colorHueComponent return sliderComponent case "BoolParam": return checkboxComponent case "ListAttribute": return listAttributeComponent case "GroupAttribute": return groupAttributeComponent case "StringParam": if (attribute.desc.semantic.includes('multiline')) return textAreaComponent return textFieldComponent case "ColorParam": return colorComponent default: return textFieldComponent } } Component { id: notComputedComponent MaterialLabel { anchors.fill: parent text: MaterialIcons.do_not_disturb_alt horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter padding: 4 background: Rectangle { anchors.fill: parent border.width: 0 radius: 20 color: Qt.darker(palette.window, 1.1) } } } Component { id: pushButtonComponent Button { text: attribute.label enabled: root.editable onClicked: { attribute.clicked() } } } Component { id: textFieldComponent TextField { id: textField readOnly: !root.editable text: attribute.value // Don't disable the component to keep interactive features (text selection, context menu...). // Only override the look by using the Disabled palette. SystemPalette { id: disabledPalette colorGroup: SystemPalette.Disabled } states: [ State { when: readOnly PropertyChanges { target: textField color: disabledPalette.text } } ] selectByMouse: true onEditingFinished: setTextFieldAttribute(text) persistentSelection: false onAccepted: { setTextFieldAttribute(text) parameterLabel.forceActiveFocus() } Keys.onPressed: function(event) { if ((event.key == Qt.Key_Escape)) { event.accepted = true parameterLabel.forceActiveFocus() } } Component.onDestruction: { if (activeFocus) setTextFieldAttribute(text) } DropArea { enabled: root.editable anchors.fill: parent onDropped: function(drop) { if (drop.hasUrls) setTextFieldAttribute(Filepath.urlToString(drop.urls[0])) else if (drop.hasText && drop.text != '') setTextFieldAttribute(drop.text) } } onPressed: (event) => { if(event.button == Qt.RightButton) { // Keep selection persistent while context menu is open to // visualize what is being copied or what will be replaced on paste. persistentSelection = true; const menu = textFieldMenuComponent.createObject(textField); menu.popup(); if(selectedText === "") { cursorPosition = positionAt(event.x, event.y); } } } Component { id: textFieldMenuComponent Menu { onOpened: { // Keep cursor visible to see where pasting would happen. textField.cursorVisible = true; } onClosed: { // Disable selection persistency behavior once menu is closed and // give focus back to the parent TextField. textField.persistentSelection = false; textField.forceActiveFocus(); destroy(); } MenuItem { text: "Copy" enabled: attribute.value != "" onTriggered: { const hasSelection = textField.selectionStart !== textField.selectionEnd; if(hasSelection) { // Use `TextField.copy` to copy only the current selection. textField.copy(); } else { Clipboard.setText(attribute.value); } } } MenuItem { text: "Paste" enabled: !readOnly onTriggered: { const clipboardText = Clipboard.getText(); if (clipboardText.length === 0) { return; } const before = textField.text.substr(0, textField.selectionStart); const after = textField.text.substr(textField.selectionEnd, textField.text.length); const updatedValue = before + clipboardText + after; setTextFieldAttribute(updatedValue); // Set the cursor at the end of the added text textField.cursorPosition = before.length + clipboardText.length; } } } } } } Component { id: textAreaComponent Rectangle { // Fixed background for the flickable object color: palette.base width: parent.width height: attribute.desc.semantic.includes("large") ? 400 : 70 Flickable { width: parent.width height: parent.height contentWidth: width contentHeight: height ScrollBar.vertical: MScrollBar {} TextArea.flickable: TextArea { wrapMode: Text.WordWrap padding: 0 rightPadding: 5 bottomPadding: 2 topPadding: 2 readOnly: !root.editable onEditingFinished: setTextFieldAttribute(text) text: attribute.value selectByMouse: true onPressed: { root.forceActiveFocus() } Component.onDestruction: { if (activeFocus) setTextFieldAttribute(text) } DropArea { enabled: root.editable anchors.fill: parent onDropped: { if (drop.hasUrls) setTextFieldAttribute(Filepath.urlToString(drop.urls[0])) else if (drop.hasText && drop.text != '') setTextFieldAttribute(drop.text) } } } } } } Component { id: colorComponent RowLayout { CheckBox { id: colorCheckbox Layout.alignment: Qt.AlignLeft checked: node && node.color === "" ? false : true checkable: root.editable text: "Custom Color" onClicked: { if (checked) { if (colorText.text == "") _reconstruction.setAttribute(attribute, "#0000FF") else _reconstruction.setAttribute(attribute, colorText.text) } else { _reconstruction.setAttribute(attribute, "") } } } TextField { id: colorText Layout.alignment: Qt.AlignLeft implicitWidth: 100 enabled: colorCheckbox.checked && root.editable visible: colorCheckbox.checked text: colorCheckbox.checked ? attribute.value : "" selectByMouse: true onEditingFinished: setTextFieldAttribute(text) onAccepted: setTextFieldAttribute(text) Component.onDestruction: { if (activeFocus) setTextFieldAttribute(text) } } Rectangle { height: colorText.height width: colorText.width / 2 Layout.alignment: Qt.AlignLeft visible: colorCheckbox.checked color: colorCheckbox.checked ? colorDialog.selectedColor : "" MouseArea { enabled: root.editable anchors.fill: parent onClicked: colorDialog.open() } } ColorDialog { id: colorDialog title: "Please choose a color" selectedColor: colorText.text onAccepted: { colorText.text = colorDialog.selectedColor // Artificially trigger change of attribute value colorText.editingFinished() close() } onRejected: close() } Item { // Dummy item to fill out the space if needed Layout.fillWidth: true } } } Component { id: choiceComponent AttributeControls.Choice { value: root.attribute.value values: root.attribute.values enabled: root.editable onEditingFinished: (value) => { _reconstruction.setAttribute(root.attribute, value) } } } Component { id: choiceMultiComponent AttributeControls.ChoiceMulti { value: root.attribute.value values: root.attribute.values enabled: root.editable customValueColor: Colors.orange onToggled: (value, checked) => { var currentValue = root.attribute.value; if (!checked) { currentValue.splice(currentValue.indexOf(value), 1); } else { currentValue.push(value); } _reconstruction.setAttribute(attribute, currentValue); } } } Component { id: sliderComponent RowLayout { TextField { IntValidator { id: intValidator } DoubleValidator { id: doubleValidator locale: 'C' // Use '.' decimal separator disregarding the system locale } implicitWidth: 100 Layout.fillWidth: !slider.active enabled: root.editable // Cast value to string to avoid intrusive scientific notations on numbers property string displayValue: String(slider.active && slider.item.pressed ? slider.item.formattedValue : attribute.value) text: displayValue selectByMouse: true // Note: Use autoScroll as a workaround for alignment // When the value change keep the text align to the left to be able to read the most important part // of the number. When we are editing (item is in focus), the content should follow the editing. autoScroll: activeFocus validator: attribute.type === "FloatParam" ? doubleValidator : intValidator onEditingFinished: setTextFieldAttribute(text) onAccepted: { setTextFieldAttribute(text) // When the text is too long, display the left part // (with the most important values and cut the floating point details) ensureVisible(0) } Component.onDestruction: { if (activeFocus) setTextFieldAttribute(text) } Component.onCompleted: { // When the text is too long, display the left part // (with the most important values and cut the floating point details) ensureVisible(0) } } Loader { id: slider Layout.fillWidth: true active: attribute.desc.range.length === 3 sourceComponent: Slider { readonly property int stepDecimalCount: stepSize < 1 ? String(stepSize).split(".").pop().length : 0 readonly property real formattedValue: value.toFixed(stepDecimalCount) enabled: root.editable value: attribute.value from: attribute.desc.range[0] to: attribute.desc.range[1] stepSize: attribute.desc.range[2] snapMode: Slider.SnapAlways onPressedChanged: { if (!pressed) { _reconstruction.setAttribute(attribute, formattedValue) updateAttributeLabel() } } } } } } Component { id: checkboxComponent Row { CheckBox { enabled: root.editable checked: attribute.value onToggled: _reconstruction.setAttribute(attribute, !attribute.value) } } } Component { id: listAttributeComponent ColumnLayout { id: listAttributeLayout width: parent.width property bool expanded: false RowLayout { spacing: 4 ToolButton { text: listAttributeLayout.expanded ? MaterialIcons.keyboard_arrow_down : MaterialIcons.keyboard_arrow_right font.family: MaterialIcons.fontFamily onClicked: listAttributeLayout.expanded = !listAttributeLayout.expanded } Label { Layout.alignment: Qt.AlignVCenter text: attribute.value.count + " elements" } ToolButton { text: MaterialIcons.add_circle_outline font.family: MaterialIcons.fontFamily font.pointSize: 11 padding: 2 enabled: root.editable onClicked: _reconstruction.appendAttribute(attribute, undefined) } } ListView { id: lv model: listAttributeLayout.expanded ? attribute.value : undefined visible: model !== undefined && count > 0 implicitHeight: Math.min(contentHeight, 300) Layout.fillWidth: true Layout.margins: 4 clip: true spacing: 4 ScrollBar.vertical: MScrollBar { id: sb } delegate: Loader { active: !objectsHideable || ((object.isDefault && GraphEditorSettings.showDefaultAttributes || !object.isDefault && GraphEditorSettings.showModifiedAttributes) && (object.isLinkNested && GraphEditorSettings.showLinkAttributes || !object.isLinkNested && GraphEditorSettings.showNotLinkAttributes)) visible: active height: implicitHeight sourceComponent: RowLayout { id: item property var childAttrib: object layoutDirection: Qt.RightToLeft width: lv.width - sb.width Component.onCompleted: { var cpt = Qt.createComponent("AttributeItemDelegate.qml") var obj = cpt.createObject(item, { 'attribute': Qt.binding(function() { return item.childAttrib }), 'readOnly': Qt.binding(function() { return !root.editable }) }) obj.Layout.fillWidth = true obj.label.text = index obj.label.horizontalAlignment = Text.AlignHCenter obj.label.verticalAlignment = Text.AlignVCenter obj.doubleClicked.connect(function(attr) { root.doubleClicked(attr) }) } ToolButton { enabled: root.editable text: MaterialIcons.remove_circle_outline font.family: MaterialIcons.fontFamily font.pointSize: 11 padding: 2 ToolTip.text: "Remove Element" ToolTip.visible: hovered onClicked: _reconstruction.removeAttribute(item.childAttrib) } } } } } } Component { id: groupAttributeComponent ColumnLayout { id: groupItem Component.onCompleted: { var cpt = Qt.createComponent("AttributeEditor.qml"); var obj = cpt.createObject(groupItem, { 'model': Qt.binding(function() { return attribute.value }), 'readOnly': Qt.binding(function() { return root.readOnly }), 'labelWidth': 100, // Reduce label width for children (space gain) 'objectsHideable': Qt.binding(function() { return root.objectsHideable }), 'filterText': Qt.binding(function() { return root.filterText }), }) obj.Layout.fillWidth = true; obj.attributeDoubleClicked.connect(function(attr) {root.doubleClicked(attr)}) } } } Component { id: colorHueComponent RowLayout { TextField { implicitWidth: 100 enabled: root.editable // Cast value to string to avoid intrusive scientific notations on numbers property string displayValue: String(slider.pressed ? slider.formattedValue : attribute.value) text: displayValue selectByMouse: true validator: DoubleValidator { locale: 'C' // Use '.' decimal separator disregarding the system locale } onEditingFinished: setTextFieldAttribute(text) onAccepted: setTextFieldAttribute(text) Component.onDestruction: { if (activeFocus) setTextFieldAttribute(text) } } Rectangle { height: slider.height width: height color: Qt.hsla(slider.pressed ? slider.formattedValue : attribute.value, 1, 0.5, 1) } Slider { id: slider Layout.fillWidth: true readonly property int stepDecimalCount: 2 readonly property real formattedValue: value.toFixed(stepDecimalCount) enabled: root.editable value: attribute.value from: 0 to: 1 stepSize: 0.01 snapMode: Slider.SnapAlways onPressedChanged: { if (!pressed) _reconstruction.setAttribute(attribute, formattedValue) } background: ShaderEffect { width: control.availableWidth height: control.availableHeight blending: false fragmentShader: "qrc:/shaders/AttributeItemDelegate.frag.qsb" } } } } } }