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