From 164163d1953d6b93255d395ea2d0892875934e78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Tue, 17 Sep 2024 17:44:39 +0200 Subject: [PATCH 01/21] [core] Attribute: Add notion of depth for attributes with parents For attributes in `GroupAttribute` and `ListAttribute`, the notion of parent attribute exists through the `root` property. As a parent can itself have a parent, the `depth` property describes how many levels there are between the attribute and the root level. A value of 0 means that the attribute is at the root level, and it increases as it gets deeper. --- meshroom/core/attribute.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/meshroom/core/attribute.py b/meshroom/core/attribute.py index dd69db1251..65364653b7 100644 --- a/meshroom/core/attribute.py +++ b/meshroom/core/attribute.py @@ -64,6 +64,13 @@ def __init__(self, node, attributeDesc, isOutput, root=None, parent=None): self._description = attributeDesc.description self._invalidate = False if self._isOutput else attributeDesc.invalidate + self._depth = 0 + if root is not None: + current = self + while current.root is not None: + self._depth += 1 + current = current.root + # invalidation value for output attributes self._invalidationValue = "" @@ -79,6 +86,9 @@ def node(self): def root(self): return self._root() if self._root else None + def getDepth(self): + return self._depth + def getName(self): """ Attribute name """ return self._name @@ -438,6 +448,7 @@ def updateInternals(self): validValueChanged = Signal() validValue = Property(bool, getValidValue, setValidValue, notify=validValueChanged) root = Property(BaseObject, root.fget, constant=True) + depth = Property(int, getDepth, constant=True) def raiseIfLink(func): From 4ab9347c025f5477024ba81228606db7288115f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Thu, 19 Sep 2024 12:00:39 +0200 Subject: [PATCH 02/21] [core] Attribute: Add a `flattenedChildren` property `flattenedChildren` returns the list of all the attributes that refer to this attribute as their parent (either direct or indirect) through the `root` property. The search for the children attributes is recursive and alllows to retrieve at once all the nested attributes, independently from their level. At the moment, only `ListAttribute` and `GroupAttribute` will return a non-empty list of flattened children attributes. --- meshroom/core/attribute.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/meshroom/core/attribute.py b/meshroom/core/attribute.py index 65364653b7..7e65ffca44 100644 --- a/meshroom/core/attribute.py +++ b/meshroom/core/attribute.py @@ -407,6 +407,33 @@ def updateInternals(self): # Emit if the enable status has changed self.setEnabled(self.getEnabled()) + def getFlattenedChildren(self): + """ Return a list of all the attributes that refer to this instance as their parent through + the 'root' property. If no such attribute exist, return an empty list. The depth difference is not + taken into account in the list, which is thus always flat. """ + attributes = ListModel(parent=self) + if isinstance(self._value, str): + # String/File attributes are iterable but cannot have children: immediately rule that case out + return attributes + + try: + # If self._value is not iterable, then it is not an attribute that can have children + iter(self._value) + except TypeError: + return attributes + + for attribute in self._value: + if not isinstance(attribute, Attribute): + # Handle ChoiceParam values, which contained in a list hence iterable, but are string + continue + attributes.add(attribute) + if isinstance(attribute, ListAttribute) or isinstance(attribute, GroupAttribute): + # Handle nested ListAttributes and GroupAttributes + flattened = attribute.getFlattenedChildren() + for i in flattened: + attributes.add(i) + return attributes + name = Property(str, getName, constant=True) fullName = Property(str, getFullName, constant=True) fullNameToNode = Property(str, getFullNameToNode, constant=True) @@ -449,6 +476,7 @@ def updateInternals(self): validValue = Property(bool, getValidValue, setValidValue, notify=validValueChanged) root = Property(BaseObject, root.fget, constant=True) depth = Property(int, getDepth, constant=True) + flattenedChildren = Property(BaseObject, getFlattenedChildren, constant=True) def raiseIfLink(func): From 7c7cae051dbb3cb72953e6e7ace7bbc55a22c4fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Mon, 23 Sep 2024 18:54:14 +0200 Subject: [PATCH 03/21] [core] Correctly match descriptions for connected attributes within groups --- meshroom/core/attribute.py | 2 +- meshroom/core/node.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/meshroom/core/attribute.py b/meshroom/core/attribute.py index 7e65ffca44..856d2aa91b 100644 --- a/meshroom/core/attribute.py +++ b/meshroom/core/attribute.py @@ -340,7 +340,7 @@ def _applyExpr(self): elif self.isInput and Attribute.isLinkExpression(v): # value is a link to another attribute link = v[1:-1] - linkNode, linkAttr = link.split('.') + linkNode, linkAttr = link.split('.', 1) try: g.addEdge(g.node(linkNode).attribute(linkAttr), self) except KeyError as err: diff --git a/meshroom/core/node.py b/meshroom/core/node.py index 1b8806e2e4..bbe54f1000 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -1711,9 +1711,18 @@ def attributeDescFromName(refAttributes, name, value, strict=True): attrDesc = next((d for d in refAttributes if d.name == name), None) if attrDesc is None: return None + # We have found a description, and we still need to # check if the value matches the attribute description. - # + + # If it is a GroupAttribute, all attributes within the group should be matched individually + # so that links that can be correctly evaluated + if isinstance(attrDesc, desc.GroupAttribute): + for k, v in value.items(): + if CompatibilityNode.attributeDescFromName(attrDesc.groupDesc, k, v, strict=True) is None: + return None + return attrDesc + # If it is a serialized link expression (no proper value to set/evaluate) if Attribute.isLinkExpression(value): return attrDesc From b9dcc7f3bb6435afa1ed1b3bf4709f701fc3ab33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Fri, 20 Sep 2024 15:04:07 +0200 Subject: [PATCH 04/21] [core] Attribute: Parent's `exposed` property takes precedence over description's The `exposed` property, which determines whether the attribute is displayed on the upper part of the node in the Graph Editor, is set for each attribute individually in their node's description. If an attribute has a parent (meaning it depends on a `GroupAttribute` or a `ListAttribute`) whose `exposed` property value differs, it does not make sense to display it separately from it. The attribute's `exposed` should align with its parent's. --- meshroom/core/attribute.py | 7 +++++++ meshroom/ui/qml/GraphEditor/Node.qml | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/meshroom/core/attribute.py b/meshroom/core/attribute.py index 856d2aa91b..09bf10cd10 100644 --- a/meshroom/core/attribute.py +++ b/meshroom/core/attribute.py @@ -64,11 +64,14 @@ def __init__(self, node, attributeDesc, isOutput, root=None, parent=None): self._description = attributeDesc.description self._invalidate = False if self._isOutput else attributeDesc.invalidate + self._exposed = attributeDesc.exposed self._depth = 0 if root is not None: current = self while current.root is not None: self._depth += 1 + if current.root.exposed != self._exposed: + self._exposed = current.root.exposed current = current.root # invalidation value for output attributes @@ -89,6 +92,9 @@ def root(self): def getDepth(self): return self._depth + def getExposed(self): + return self._exposed + def getName(self): """ Attribute name """ return self._name @@ -446,6 +452,7 @@ def getFlattenedChildren(self): type = Property(str, getType, constant=True) baseType = Property(str, getType, constant=True) isReadOnly = Property(bool, _isReadOnly, constant=True) + exposed = Property(bool, getExposed, constant=True) # Description of the attribute descriptionChanged = Signal() diff --git a/meshroom/ui/qml/GraphEditor/Node.qml b/meshroom/ui/qml/GraphEditor/Node.qml index 65b95cd827..865ee2f2f9 100755 --- a/meshroom/ui/qml/GraphEditor/Node.qml +++ b/meshroom/ui/qml/GraphEditor/Node.qml @@ -438,7 +438,7 @@ Item { delegate: Loader { id: inputLoader - active: !object.isOutput && object.desc.exposed && object.desc.visible + active: !object.isOutput && object.exposed && object.desc.visible visible: Boolean(object.enabled) width: inputs.width @@ -498,7 +498,7 @@ Item { model: node ? node.attributes : undefined delegate: Loader { id: paramLoader - active: !object.isOutput && !object.desc.exposed && object.desc.visible + active: !object.isOutput && !object.exposed && object.desc.visible visible: Boolean(object.enabled || object.isLinkNested || object.hasOutputConnections) property bool isFullyActive: Boolean(m.displayParams || object.isLinkNested || object.hasOutputConnections) width: parent.width From a97778f2c225c03543ba7aee44fac19e45bff6d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Mon, 23 Sep 2024 12:21:58 +0200 Subject: [PATCH 05/21] [GraphEditor] Allow to display and connect attributes within `GroupAttribute` --- meshroom/ui/qml/GraphEditor/AttributePin.qml | 41 ++++- meshroom/ui/qml/GraphEditor/Node.qml | 160 +++++++++++++++++-- 2 files changed, 180 insertions(+), 21 deletions(-) diff --git a/meshroom/ui/qml/GraphEditor/AttributePin.qml b/meshroom/ui/qml/GraphEditor/AttributePin.qml index d435601bd5..2b8fc55fa1 100755 --- a/meshroom/ui/qml/GraphEditor/AttributePin.qml +++ b/meshroom/ui/qml/GraphEditor/AttributePin.qml @@ -13,6 +13,7 @@ RowLayout { property var nodeItem property var attribute + property bool expanded: false property bool readOnly: false /// Whether to display an output pin for input attribute property bool displayOutputPinForInput: true @@ -25,6 +26,8 @@ RowLayout { outputAnchor.y + outputAnchor.height / 2) readonly property bool isList: attribute && attribute.type === "ListAttribute" + readonly property bool isGroup: attribute && attribute.type === "GroupAttribute" + readonly property bool isChild: attribute && attribute.root signal childPinCreated(var childAttribute, var pin) signal childPinDeleted(var childAttribute, var pin) @@ -32,6 +35,8 @@ RowLayout { signal pressed(var mouse) signal edgeAboutToBeRemoved(var input) + signal clicked() + objectName: attribute ? attribute.name + "." : "" layoutDirection: Qt.LeftToRight spacing: 3 @@ -52,6 +57,24 @@ RowLayout { } } + function updateLabel() { + var label = "" + var expandedGroup = expanded ? "-" : "+" + if (attribute && attribute.label !== undefined) { + label = attribute.label + if (isGroup && attribute.isOutput) { + label = label + " " + expandedGroup + } else if (isGroup && !attribute.isOutput) { + label = expandedGroup + " " + label + } + } + return label + } + + onExpandedChanged: { + nameLabel.text = updateLabel() + } + // Instantiate empty Items for each child attribute Repeater { id: childrenRepeater @@ -171,7 +194,8 @@ RowLayout { onReleased: { inputDragTarget.Drag.drop() } - hoverEnabled: true + onClicked: root.clicked() + hoverEnabled: root.visible } Edge { @@ -197,18 +221,22 @@ RowLayout { id: nameLabel enabled: !root.readOnly + visible: true property bool hovered: (inputConnectMA.containsMouse || inputConnectMA.drag.active || inputDropArea.containsDrag || outputConnectMA.containsMouse || outputConnectMA.drag.active || outputDropArea.containsDrag) - text: (attribute && attribute.label) !== undefined ? attribute.label : "" + text: root.updateLabel() elide: hovered ? Text.ElideNone : Text.ElideMiddle width: hovered ? contentWidth : parent.width font.pointSize: 7 + font.italic: isChild ? true : false horizontalAlignment: attribute && attribute.isOutput ? Text.AlignRight : Text.AlignLeft anchors.right: attribute && attribute.isOutput ? parent.right : undefined rightPadding: 0 color: { if ((object.hasOutputConnections || object.isLink) && !object.enabled) return Colors.lightgrey - return hovered ? palette.highlight : palette.text + else if (hovered) + return palette.highlight + return palette.text } } } @@ -234,8 +262,8 @@ RowLayout { anchors.fill: parent anchors.margins: 2 color: { - if (object.enabled && (outputConnectMA.containsMouse || outputConnectMA.drag.active || - (outputDropArea.containsDrag && outputDropArea.acceptableDrop))) + if (modelData.enabled && (outputConnectMA.containsMouse || outputConnectMA.drag.active || + (outputDropArea.containsDrag && outputDropArea.acceptableDrop))) return Colors.sysPalette.highlight return Colors.sysPalette.text } @@ -314,8 +342,9 @@ RowLayout { onPressed: function(mouse) { root.pressed(mouse) } onReleased: outputDragTarget.Drag.drop() + onClicked: root.clicked() - hoverEnabled: true + hoverEnabled: root.visible } Edge { diff --git a/meshroom/ui/qml/GraphEditor/Node.qml b/meshroom/ui/qml/GraphEditor/Node.qml index 865ee2f2f9..1fc0e0c97f 100755 --- a/meshroom/ui/qml/GraphEditor/Node.qml +++ b/meshroom/ui/qml/GraphEditor/Node.qml @@ -117,6 +117,54 @@ Item { return str } + function updateChildPin(attribute, parentPins, pin) { + /* + * Update the pin of a child attribute: if the attribute is enabled and its parent is a GroupAttribute, + * the visibility is determined based on the parent pin's "expanded" state, using the "parentPins" map to + * access the status. + * If the current pin is also a GroupAttribute and is expanded while its newly "visible" state is false, + * it is reset. + */ + if (Boolean(attribute.enabled)) { + // If the parent's a GroupAttribute, use status of the parent's pin to determine visibility + if (attribute.root && attribute.root.type === "GroupAttribute") { + var visible = Boolean(parentPins.get(attribute.root.name)) + if (!visible && parentPins.has(attribute.name) && parentPins.get(attribute.name) === true) { + parentPins.set(attribute.name, false) + pin.expanded = false + } + return visible + } + return true + } + return false + } + + function generateAttributesModel(isOutput, parentPins) { + if (!node) + return undefined + + const attributes = [] + for (let i = 0; i < node.attributes.count; ++i) { + let attr = node.attributes.at(i) + if (attr.isOutput == isOutput) { + attributes.push(attr) + if (attr.type === "GroupAttribute") { + parentPins.set(attr.name, false) + } + + for (let j = 0; j < attr.flattenedChildren.count; ++j) { + attributes.push(attr.flattenedChildren.at(j)) + if (attr.flattenedChildren.at(j).type === "GroupAttribute") { + parentPins.set(attr.flattenedChildren.at(j).name, false) + } + } + } + } + + return attributes + } + // Main Layout MouseArea { id: mouseArea @@ -398,26 +446,53 @@ Item { width: parent.width spacing: 3 + property var parentPins: new Map() + signal parentPinsUpdated() + Repeater { - model: node ? node.attributes : undefined + model: generateAttributesModel(true, outputs.parentPins) // isOutput = true delegate: Loader { id: outputLoader - active: Boolean(object.isOutput && object.desc.visible) - visible: Boolean(object.enabled || object.hasOutputConnections) + active: Boolean(modelData.isOutput && modelData.desc.visible) + + visible: { + if (Boolean(modelData.enabled || modelData.hasOutputConnections)) { + if (modelData.root && modelData.root.type === "GroupAttribute") { + return Boolean(outputs.parentPins.get(modelData.root.name)) + } + return true + } + return false + } anchors.right: parent.right width: outputs.width + Connections { + target: outputs + + function onParentPinsUpdated() { + visible = updateChildPin(modelData, outputs.parentPins, outputLoader.item) + } + } + sourceComponent: AttributePin { id: outPin nodeItem: root - attribute: object + attribute: modelData property real globalX: root.x + nodeAttributes.x + outputs.x + outputLoader.x + outPin.x property real globalY: root.y + nodeAttributes.y + outputs.y + outputLoader.y + outPin.y onPressed: function(mouse) { root.pressed(mouse) } onEdgeAboutToBeRemoved: function(input) { root.edgeAboutToBeRemoved(input) } + onClicked: { + expanded = !expanded + if (outputs.parentPins.has(modelData.name)) { + outputs.parentPins.set(modelData.name, expanded) + outputs.parentPinsUpdated() + } + } Component.onCompleted: attributePinCreated(attribute, outPin) onChildPinCreated: attributePinCreated(childAttribute, outPin) @@ -433,19 +508,38 @@ Item { width: parent.width spacing: 3 + property var parentPins: new Map() + signal parentPinsUpdated() + Repeater { - model: node ? node.attributes : undefined + model: generateAttributesModel(false, inputs.parentPins) // isOutput = false delegate: Loader { id: inputLoader - active: !object.isOutput && object.exposed && object.desc.visible - visible: Boolean(object.enabled) + active: !modelData.isOutput && modelData.exposed && modelData.desc.visible + visible: { + if (Boolean(modelData.enabled)) { + if (modelData.root && modelData.root.type === "GroupAttribute") { + return Boolean(inputs.parentPins.get(modelData.root.name)) + } + return true + } + return false + } width: inputs.width + Connections { + target: inputs + + function onParentPinsUpdated() { + visible = updateChildPin(modelData, inputs.parentPins, inputLoader.item) + } + } + sourceComponent: AttributePin { id: inPin nodeItem: root - attribute: object + attribute: modelData property real globalX: root.x + nodeAttributes.x + inputs.x + inputLoader.x + inPin.x property real globalY: root.y + nodeAttributes.y + inputs.y + inputLoader.y + inPin.y @@ -454,6 +548,13 @@ Item { Component.onCompleted: attributePinCreated(attribute, inPin) Component.onDestruction: attributePinDeleted(attribute, inPin) onPressed: function(mouse) { root.pressed(mouse) } + onClicked: { + expanded = !expanded + if (inputs.parentPins.has(modelData.name)) { + inputs.parentPins.set(modelData.name, expanded) + inputs.parentPinsUpdated() + } + } onEdgeAboutToBeRemoved: function(input) { root.edgeAboutToBeRemoved(input) } onChildPinCreated: function(childAttribute, inPin) { attributePinCreated(childAttribute, inPin) } onChildPinDeleted: function(childAttribute, inPin) { attributePinDeleted(childAttribute, inPin) } @@ -493,30 +594,59 @@ Item { id: inputParams width: parent.width spacing: 3 + + property var parentPins: new Map() + signal parentPinsUpdated() + Repeater { - id: inputParamsRepeater - model: node ? node.attributes : undefined + model: generateAttributesModel(false, inputParams.parentPins) // isOutput = false + delegate: Loader { id: paramLoader - active: !object.isOutput && !object.exposed && object.desc.visible - visible: Boolean(object.enabled || object.isLinkNested || object.hasOutputConnections) - property bool isFullyActive: Boolean(m.displayParams || object.isLinkNested || object.hasOutputConnections) + active: !modelData.isOutput && !modelData.exposed && modelData.desc.visible + visible: { + if (Boolean(modelData.enabled || modelData.isLinkNested || modelData.hasOutputConnections)) { + if (modelData.root && modelData.root.type === "GroupAttribute") { + return Boolean(inputParams.parentPins.get(modelData.root.name)) + } + return true + } + return false + } + property bool isFullyActive: Boolean(m.displayParams || modelData.isLinkNested || modelData.hasOutputConnections) width: parent.width + Connections { + target: inputParams + + function onParentPinsUpdated() { + visible = updateChildPin(modelData, inputParams.parentPins, paramLoader.item) + } + } + sourceComponent: AttributePin { id: inParamsPin nodeItem: root + attribute: modelData + property real globalX: root.x + nodeAttributes.x + inputParamsRect.x + paramLoader.x + inParamsPin.x property real globalY: root.y + nodeAttributes.y + inputParamsRect.y + paramLoader.y + inParamsPin.y height: isFullyActive ? childrenRect.height : 0 Behavior on height { PropertyAnimation {easing.type: Easing.Linear} } visible: (height == childrenRect.height) - attribute: object - readOnly: Boolean(root.readOnly || object.isReadOnly) + + readOnly: Boolean(root.readOnly || modelData.isReadOnly) Component.onCompleted: attributePinCreated(attribute, inParamsPin) Component.onDestruction: attributePinDeleted(attribute, inParamsPin) onPressed: function(mouse) { root.pressed(mouse) } + onClicked: { + expanded = !expanded + if (inputParams.parentPins.has(modelData.name)) { + inputParams.parentPins.set(modelData.name, expanded) + inputParams.parentPinsUpdated() + } + } onEdgeAboutToBeRemoved: function(input) { root.edgeAboutToBeRemoved(input) } onChildPinCreated: function(childAttribute, inParamsPin) { attributePinCreated(childAttribute, inParamsPin) } onChildPinDeleted: function(childAttribute, inParamsPin) { attributePinDeleted(childAttribute, inParamsPin) } From 5c7f75fd6f61a54efacbda3b59a1001ed30fc2a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Mon, 23 Sep 2024 19:02:34 +0200 Subject: [PATCH 06/21] [ui] Graph: `canExpandForLoop`: Ensure all checks are performed on a `ListAttribute` --- meshroom/ui/graph.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index 0cde6b2ddd..ac3f6dd4ae 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -760,7 +760,8 @@ def duplicateNodesFrom(self, nodes: list[Node]) -> list[Node]: def canExpandForLoop(self, currentEdge): """ Check if the list attribute can be expanded by looking at all the edges connected to it. """ listAttribute = currentEdge.src.root - if not listAttribute: + # Check that the parent is indeed a ListAttribute (it could be a GroupAttribute, for instance) + if not listAttribute or not isinstance(listAttribute, ListAttribute): return False srcIndex = listAttribute.index(currentEdge.src) allSrc = [e.src for e in self._graph.edges.values()] From 55ae221724330732c9f74fc6a5efb226b6267dab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Mon, 23 Sep 2024 19:19:36 +0200 Subject: [PATCH 07/21] [GraphEditor] Refer to an attribute's parent in its tooltip If an attribute belongs to a `GroupAttribute` or a `ListAttribute`, it has a parent, and its full name is "parentName.attributeName". Instead of displaying only "attributeName" in the tooltip, this commit now displays "parentName.attributeName" to ensure that the link is obvious. --- meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml | 5 +++-- meshroom/ui/qml/GraphEditor/AttributePin.qml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml index f433a3042a..105f82c654 100644 --- a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml +++ b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml @@ -35,8 +35,9 @@ RowLayout { 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) + tooltip += " " + attribute.fullName + ": " + attribute.type + "
" + Format.plainToHtml(attribute.desc.description) + console.warn(attribute.fullName) parameterTooltip.text = tooltip } } @@ -85,7 +86,7 @@ RowLayout { 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) + tooltip += "" + object.fullName + ": " + attribute.type + "
" + Format.plainToHtml(object.description) return tooltip } visible: parameterMA.containsMouse diff --git a/meshroom/ui/qml/GraphEditor/AttributePin.qml b/meshroom/ui/qml/GraphEditor/AttributePin.qml index 2b8fc55fa1..afe1b0e8ad 100755 --- a/meshroom/ui/qml/GraphEditor/AttributePin.qml +++ b/meshroom/ui/qml/GraphEditor/AttributePin.qml @@ -42,7 +42,7 @@ RowLayout { spacing: 3 ToolTip { - text: attribute.name + ": " + attribute.type + text: attribute.fullName + ": " + attribute.type visible: nameLabel.hovered y: nameLabel.y + nameLabel.height From 521d7b7816a237e273d3ce6fec351199c0f9bf0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Thu, 12 Dec 2024 15:47:42 +0100 Subject: [PATCH 08/21] [core] Add dedicated `flatStaticChildren` property for GroupAttributes Rename `getFlattenedChildren` into `getFlatStaticChildren` and have a default version for all the Attributes, as well as a dedicated one for GroupAttributes. Children of ListAttributes are currently not taken into account when gathering the list of children. --- meshroom/core/attribute.py | 43 ++++++++++++---------------- meshroom/ui/qml/GraphEditor/Node.qml | 11 +++---- 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/meshroom/core/attribute.py b/meshroom/core/attribute.py index 09bf10cd10..92f2bf277b 100644 --- a/meshroom/core/attribute.py +++ b/meshroom/core/attribute.py @@ -413,32 +413,12 @@ def updateInternals(self): # Emit if the enable status has changed self.setEnabled(self.getEnabled()) - def getFlattenedChildren(self): + def getFlatStaticChildren(self): """ Return a list of all the attributes that refer to this instance as their parent through the 'root' property. If no such attribute exist, return an empty list. The depth difference is not taken into account in the list, which is thus always flat. """ - attributes = ListModel(parent=self) - if isinstance(self._value, str): - # String/File attributes are iterable but cannot have children: immediately rule that case out - return attributes - - try: - # If self._value is not iterable, then it is not an attribute that can have children - iter(self._value) - except TypeError: - return attributes - - for attribute in self._value: - if not isinstance(attribute, Attribute): - # Handle ChoiceParam values, which contained in a list hence iterable, but are string - continue - attributes.add(attribute) - if isinstance(attribute, ListAttribute) or isinstance(attribute, GroupAttribute): - # Handle nested ListAttributes and GroupAttributes - flattened = attribute.getFlattenedChildren() - for i in flattened: - attributes.add(i) - return attributes + # For all attributes but GroupAttributes, there cannot be any child + return [] name = Property(str, getName, constant=True) fullName = Property(str, getFullName, constant=True) @@ -483,7 +463,7 @@ def getFlattenedChildren(self): validValue = Property(bool, getValidValue, setValidValue, notify=validValueChanged) root = Property(BaseObject, root.fget, constant=True) depth = Property(int, getDepth, constant=True) - flattenedChildren = Property(BaseObject, getFlattenedChildren, constant=True) + flatStaticChildren = Property(Variant, getFlatStaticChildren, constant=True) def raiseIfLink(func): @@ -836,6 +816,20 @@ def updateInternals(self): for attr in self._value: attr.updateInternals() + def getFlatStaticChildren(self): + """ Return a list of all the attributes that refer to this instance of GroupAttribute as their parent + through the 'root' property. In the case of GroupAttributes, any attribute within said group will be + a child. The depth difference is not taken into account when generating the list, which is thus always + flat. """ + attributes = [] + + # Iterate over the values and add the flat children of every child (if they exist) + for attribute in self._value: + attributes.append(attribute) + attributes = attributes + attribute.getFlatStaticChildren() + + return attributes + @Slot(str, result=bool) def matchText(self, text): return super().matchText(text) or any(c.matchText(text) for c in self._value) @@ -843,3 +837,4 @@ def matchText(self, text): # Override value property value = Property(Variant, Attribute._get_value, _set_value, notify=Attribute.valueChanged) isDefault = Property(bool, _isDefault, notify=Attribute.valueChanged) + flatStaticChildren = Property(Variant, getFlatStaticChildren, constant=True) diff --git a/meshroom/ui/qml/GraphEditor/Node.qml b/meshroom/ui/qml/GraphEditor/Node.qml index 1fc0e0c97f..60ec434581 100755 --- a/meshroom/ui/qml/GraphEditor/Node.qml +++ b/meshroom/ui/qml/GraphEditor/Node.qml @@ -153,12 +153,13 @@ Item { parentPins.set(attr.name, false) } - for (let j = 0; j < attr.flattenedChildren.count; ++j) { - attributes.push(attr.flattenedChildren.at(j)) - if (attr.flattenedChildren.at(j).type === "GroupAttribute") { - parentPins.set(attr.flattenedChildren.at(j).name, false) + // Check and add any child this attribute might have + attr.flatStaticChildren.forEach((child) => { + attributes.push(child) + if (child.type === "GroupAttribute") { + parentPins.set(child.name, false) } - } + }) } } From 50c4347db0c416cdb0e09b8b5f3c27a32ad21d8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Mon, 21 Oct 2024 17:47:43 +0200 Subject: [PATCH 09/21] [tests] Add test node with `GroupAttributes` and unit tests The test ensures that if the attributes within `GroupAttributes` are connected to each other, the graph can be saved and reloaded without triggering compatibility issues for these nodes. --- tests/nodes/test/GroupAttributes.py | 152 ++++++++++++++++++++++++++++ tests/test_groupAttributes.py | 103 +++++++++++++++++++ 2 files changed, 255 insertions(+) create mode 100644 tests/nodes/test/GroupAttributes.py create mode 100644 tests/test_groupAttributes.py diff --git a/tests/nodes/test/GroupAttributes.py b/tests/nodes/test/GroupAttributes.py new file mode 100644 index 0000000000..028fcf7117 --- /dev/null +++ b/tests/nodes/test/GroupAttributes.py @@ -0,0 +1,152 @@ +from meshroom.core import desc + +class GroupAttributes(desc.Node): + documentation = """ Test node to connect GroupAttributes to other GroupAttributes. """ + category = 'Test' + + # Inputs to the node + inputs = [ + desc.GroupAttribute( + name="firstGroup", + label="First Group", + description="Group at the root level.", + group=None, + exposed=True, + groupDesc=[ + desc.IntParam( + name="firstGroupIntA", + label="Integer A", + description="First integer in group.", + value=1024, + range=(-1, 2000, 10), + exposed=True, + ), + desc.BoolParam( + name="firstGroupBool", + label="Boolean", + description="Boolean in group.", + value=True, + advanced=True, + exposed=True, + ), + desc.ChoiceParam( + name="firstGroupExclusiveChoiceParam", + label="Exclusive Choice Param", + description="Exclusive choice parameter.", + value="one", + values=["one", "two", "three", "four"], + exclusive=True, + exposed=True, + ), + desc.ChoiceParam( + name="firstGroupChoiceParam", + label="ChoiceParam", + description="Non-exclusive choice parameter.", + value=["one", "two"], + values=["one", "two", "three", "four"], + exclusive=False, + exposed=True + ), + desc.GroupAttribute( + name="nestedGroup", + label="Nested Group", + description="A group within a group.", + group=None, + exposed=True, + groupDesc=[ + desc.FloatParam( + name="nestedGroupFloat", + label="Floating Number", + description="Floating number in group.", + value=1.0, + range=(0.0, 100.0, 0.01), + exposed=True + ), + ], + ), + desc.ListAttribute( + name="groupedList", + label="Grouped List", + description="List of groups within a group.", + advanced=True, + exposed=True, + elementDesc=desc.GroupAttribute( + name="listedGroup", + label="Listed Group", + description="Group in a list within a group.", + joinChar=":", + group=None, + groupDesc=[ + desc.IntParam( + name="listedGroupInt", + label="Integer 1", + description="Integer in a group in a list within a group.", + value=12, + range=(3, 24, 1), + exposed=True, + ), + ], + ), + ), + desc.ListAttribute( + name="singleGroupedList", + label="Grouped List With Single Element", + description="List of integers within a group.", + advanced=True, + exposed=True, + elementDesc=desc.IntParam( + name="listedInt", + label="Integer In List", + description="Integer in a list within a group.", + value=40, + ), + ), + ], + ), + desc.IntParam( + name="exposedInt", + label="Exposed Integer", + description="Integer at the rool level, exposed.", + value=1000, + exposed=True, + ), + desc.BoolParam( + name="unexposedBool", + label="Unexposed Boolean", + description="Boolean at the root level, unexposed.", + value=True, + ), + desc.GroupAttribute( + name="inputGroup", + label="Input Group", + description="A group set as an input.", + group=None, + groupDesc=[ + desc.BoolParam( + name="inputBool", + label="Input Bool", + description="", + value=False, + ), + ], + ), + ] + + outputs = [ + desc.GroupAttribute( + name="outputGroup", + label="Output Group", + description="A group set as an output.", + group=None, + exposed=True, + groupDesc=[ + desc.BoolParam( + name="outputBool", + label="Output Bool", + description="", + value=False, + exposed=True, + ), + ], + ), + ] \ No newline at end of file diff --git a/tests/test_groupAttributes.py b/tests/test_groupAttributes.py new file mode 100644 index 0000000000..89d9887ed0 --- /dev/null +++ b/tests/test_groupAttributes.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python +# coding:utf-8 + +import os +import tempfile + +from meshroom.core.graph import Graph, loadGraph +from meshroom.core.node import CompatibilityNode +from meshroom.core.attribute import GroupAttribute + +GROUPATTRIBUTES_FIRSTGROUP_NB_CHILDREN = 8 # 1 int, 1 exclusive choice param, 1 choice param, 1 bool, 1 group, 1 float nested in the group, 2 lists +GROUPATTRIBUTES_FIRSTGROUP_NESTED_NB_CHILDREN = 1 # 1 float +GROUPATTRIBUTES_OUTPUTGROUP_NB_CHILDREN = 1 # 1 bool +GROUPATTRIBUTES_FIRSTGROUP_DEPTHS = [1, 1, 1, 1, 1, 2, 1, 1] + +def test_saveLoadGroupConnections(): + """ + Ensure that connecting attributes that are part of GroupAttributes does not cause + their nodes to have CompatibilityIssues when re-opening them. + """ + graph = Graph("Connections between GroupAttributes") + + # Create two "GroupAttributes" nodes with their default parameters + nodeA = graph.addNewNode("GroupAttributes") + nodeB = graph.addNewNode("GroupAttributes") + + # Connect attributes within groups at different depth levels + graph.addEdges( + (nodeA.firstGroup.firstGroupIntA, nodeB.firstGroup.firstGroupIntA), + (nodeA.firstGroup.nestedGroup.nestedGroupFloat, nodeB.firstGroup.nestedGroup.nestedGroupFloat) + ) + + # Save the graph in a file + graphFile = os.path.join(tempfile.mkdtemp(), "test_io_group_connections.mg") + graph.save(graphFile) + + # Reload the graph + graph = loadGraph(graphFile) + + # Ensure the nodes are not CompatibilityNodes + for node in graph.nodes: + assert not isinstance(node, CompatibilityNode) + + +def test_groupAttributesFlatChildren(): + """ + Check that the list of static flat children is correct, even with list elements. + """ + graph = Graph("Children of GroupAttributes") + + # Create two "GroupAttributes" nodes with their default parameters + node = graph.addNewNode("GroupAttributes") + + intAttr = node.attribute("exposedInt") + assert not isinstance(intAttr, GroupAttribute) + assert len(intAttr.flatStaticChildren) == 0 # Not a Group, cannot have any child + + inputGroup = node.attribute("firstGroup") + assert isinstance(inputGroup, GroupAttribute) + assert len(inputGroup.flatStaticChildren) == GROUPATTRIBUTES_FIRSTGROUP_NB_CHILDREN + + # Add an element to a list within the group and check the number of children hasn't changed + groupedList = node.attribute("firstGroup.singleGroupedList") + groupedList.insert(0, 30) + assert len(groupedList.flatStaticChildren) == 0 # Not a Group, elements are not counted as children + assert len(inputGroup.flatStaticChildren) == GROUPATTRIBUTES_FIRSTGROUP_NB_CHILDREN + + nestedGroup = node.attribute("firstGroup.nestedGroup") + assert isinstance(nestedGroup, GroupAttribute) + assert len(nestedGroup.flatStaticChildren) == GROUPATTRIBUTES_FIRSTGROUP_NESTED_NB_CHILDREN + + outputGroup = node.attribute("outputGroup") + assert isinstance(outputGroup, GroupAttribute) + assert len(outputGroup.flatStaticChildren) == GROUPATTRIBUTES_OUTPUTGROUP_NB_CHILDREN + + +def test_groupAttributesDepthLevels(): + """ + Check that the depth level of children attributes is correctly set. + """ + graph = Graph("Children of GroupAttributes") + + # Create two "GroupAttributes" nodes with their default parameters + node = graph.addNewNode("GroupAttributes") + inputGroup = node.attribute("firstGroup") + assert isinstance(inputGroup, GroupAttribute) + assert inputGroup.depth == 0 # Root level + + cnt = 0 + for child in inputGroup.flatStaticChildren: + assert child.depth == GROUPATTRIBUTES_FIRSTGROUP_DEPTHS[cnt] + cnt = cnt + 1 + + outputGroup = node.attribute("outputGroup") + assert isinstance(outputGroup, GroupAttribute) + assert outputGroup.depth == 0 + for child in outputGroup.flatStaticChildren: # Single element in the group + assert child.depth == 1 + + + intAttr = node.attribute("exposedInt") + assert not isinstance(intAttr, GroupAttribute) + assert intAttr.depth == 0 \ No newline at end of file From 06f9d157c8a7d5e39e4aa2b77565a6fcb7da9b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Thu, 12 Dec 2024 17:30:35 +0100 Subject: [PATCH 10/21] [MaterialIcons] MaterialToolLabel: Add new accessors to the item's elements --- meshroom/ui/qml/GraphEditor/Edge.qml | 2 +- meshroom/ui/qml/MaterialIcons/MaterialToolLabel.qml | 6 +++++- meshroom/ui/qml/Viewer3D/Inspector3D.qml | 6 +++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/meshroom/ui/qml/GraphEditor/Edge.qml b/meshroom/ui/qml/GraphEditor/Edge.qml index ba90a688a8..ef22386bf3 100644 --- a/meshroom/ui/qml/GraphEditor/Edge.qml +++ b/meshroom/ui/qml/GraphEditor/Edge.qml @@ -118,7 +118,7 @@ Item { anchors.centerIn: parent iconText: MaterialIcons.loop - label: (root.iteration + 1) + "/" + root.loopSize + " " + label.text: (root.iteration + 1) + "/" + root.loopSize + " " labelIconColor: palette.base ToolTip.text: "Foreach Loop" diff --git a/meshroom/ui/qml/MaterialIcons/MaterialToolLabel.qml b/meshroom/ui/qml/MaterialIcons/MaterialToolLabel.qml index 92b7be1c7a..ac92736367 100644 --- a/meshroom/ui/qml/MaterialIcons/MaterialToolLabel.qml +++ b/meshroom/ui/qml/MaterialIcons/MaterialToolLabel.qml @@ -9,15 +9,19 @@ import QtQuick.Layouts Item { id: control + property alias icon: iconItem property alias iconText: iconItem.text property alias iconSize: iconItem.font.pointSize - property alias label: labelItem.text + property alias label: labelItem + property alias labelIconRow: contentRow property var labelIconColor: palette.text + property alias labelIconMouseArea: mouseArea implicitWidth: childrenRect.width implicitHeight: childrenRect.height anchors.rightMargin: 5 RowLayout { + id: contentRow Label { id: iconItem font.family: MaterialIcons.fontFamily diff --git a/meshroom/ui/qml/Viewer3D/Inspector3D.qml b/meshroom/ui/qml/Viewer3D/Inspector3D.qml index 43a08095c1..f35e0377a4 100644 --- a/meshroom/ui/qml/Viewer3D/Inspector3D.qml +++ b/meshroom/ui/qml/Viewer3D/Inspector3D.qml @@ -241,7 +241,7 @@ FloatingPane { MaterialToolLabel { iconText: MaterialIcons.stop - label: { + label.text: { var id = undefined // Ensure there are entries in resectionGroups and a valid resectionId before accessing anything if (Viewer3DSettings.resectionId !== undefined && Viewer3DSettings.resectionGroups && @@ -259,7 +259,7 @@ FloatingPane { MaterialToolLabel { iconText: MaterialIcons.auto_awesome_motion - label: { + label.text: { let currentCameras = 0 if (Viewer3DSettings.resectionGroups) { for (let i = 0; i <= Viewer3DSettings.resectionIdCount; i++) { @@ -277,7 +277,7 @@ FloatingPane { MaterialToolLabel { iconText: MaterialIcons.videocam - label: { + label.text: { let totalCameras = 0 if (Viewer3DSettings.resectionGroups) { for (let i = 0; i <= Viewer3DSettings.resectionIdCount; i++) { From 201c961f4f91a867906cd701fb041e3c72ddf681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Thu, 12 Dec 2024 17:31:22 +0100 Subject: [PATCH 11/21] [GraphEditor] AttributePin: Add a tree view for children attributes --- meshroom/ui/qml/GraphEditor/AttributePin.qml | 70 +++++++++++++++----- 1 file changed, 52 insertions(+), 18 deletions(-) diff --git a/meshroom/ui/qml/GraphEditor/AttributePin.qml b/meshroom/ui/qml/GraphEditor/AttributePin.qml index afe1b0e8ad..8b41ad8ed0 100755 --- a/meshroom/ui/qml/GraphEditor/AttributePin.qml +++ b/meshroom/ui/qml/GraphEditor/AttributePin.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import Utils 1.0 +import MaterialIcons 2.2 /** * The representation of an Attribute on a Node. @@ -71,10 +72,6 @@ RowLayout { return label } - onExpandedChanged: { - nameLabel.text = updateLabel() - } - // Instantiate empty Items for each child attribute Repeater { id: childrenRepeater @@ -215,29 +212,66 @@ RowLayout { id: nameContainer implicitHeight: childrenRect.height Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter + Layout.alignment: { + if (attribute.isOutput) { + return Qt.AlignRight | Qt.AlignVCenter + } + return Qt.AlignLeft | Qt.AlignVCenter + } - Label { + MaterialToolLabel { id: nameLabel + anchors.rightMargin: 0 + anchors.right: attribute && attribute.isOutput ? parent.right : undefined + labelIconRow.layoutDirection: attribute.isOutput ? Qt.RightToLeft : Qt.LeftToRight + labelIconRow.spacing: 0 + enabled: !root.readOnly visible: true - property bool hovered: (inputConnectMA.containsMouse || inputConnectMA.drag.active || inputDropArea.containsDrag || outputConnectMA.containsMouse || outputConnectMA.drag.active || outputDropArea.containsDrag) - text: root.updateLabel() - elide: hovered ? Text.ElideNone : Text.ElideMiddle - width: hovered ? contentWidth : parent.width - font.pointSize: 7 - font.italic: isChild ? true : false - horizontalAlignment: attribute && attribute.isOutput ? Text.AlignRight : Text.AlignLeft - anchors.right: attribute && attribute.isOutput ? parent.right : undefined - rightPadding: 0 - color: { - if ((object.hasOutputConnections || object.isLink) && !object.enabled) + property bool hovered: (inputConnectMA.containsMouse || inputConnectMA.drag.active || + inputDropArea.containsDrag || outputConnectMA.containsMouse || + outputConnectMA.drag.active || outputDropArea.containsDrag) + + labelIconColor: { + if ((object.hasOutputConnections || object.isLink) && !object.enabled) { return Colors.lightgrey - else if (hovered) + } else if (hovered) { return palette.highlight + } return palette.text } + labelIconMouseArea.enabled: false // Prevent mixing mouse interactions between the label and the pin context + + // Text + label.text: attribute.label + label.font.pointSize: 7 + label.elide: hovered ? Text.ElideNone : Text.ElideMiddle + label.horizontalAlignment: attribute && attribute.isOutput ? Text.AlignRight : Text.AlignLeft + + // Icon + iconText: { + if (isGroup) { + return expanded ? MaterialIcons.expand_more : MaterialIcons.chevron_right + } + return "" + } + iconSize: 7 + icon.horizontalAlignment: attribute && attribute.isOutput ? Text.AlignRight : Text.AlignLeft + + // Handle tree view for nested attributes + icon.leftPadding: { + if (attribute.depth != 0 && !attribute.isOutput) { + return attribute.depth * 10 + } + return 0 + } + icon.rightPadding: { + if (attribute.depth != 0 && attribute.isOutput) { + return attribute.depth * 10 + } + return 0 + } } } From 4280295e90344f0beb6dc3b39fbaddbd355e74b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Thu, 12 Dec 2024 18:11:01 +0100 Subject: [PATCH 12/21] [GraphEditor] Node: Display connected children when the parent is collapsed Just like any other connected attribute, any child attribute within a Group that is either the source or destination of an edge needs to remain on display, independently from the status of its parent (collapsed or expanded). --- meshroom/ui/qml/GraphEditor/AttributePin.qml | 2 +- meshroom/ui/qml/GraphEditor/Node.qml | 50 +++++++++++++++----- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/meshroom/ui/qml/GraphEditor/AttributePin.qml b/meshroom/ui/qml/GraphEditor/AttributePin.qml index 8b41ad8ed0..b8130c5895 100755 --- a/meshroom/ui/qml/GraphEditor/AttributePin.qml +++ b/meshroom/ui/qml/GraphEditor/AttributePin.qml @@ -29,13 +29,13 @@ RowLayout { readonly property bool isList: attribute && attribute.type === "ListAttribute" readonly property bool isGroup: attribute && attribute.type === "GroupAttribute" readonly property bool isChild: attribute && attribute.root + readonly property bool isConnected: attribute.isLinkNested || attribute.hasOutputConnections signal childPinCreated(var childAttribute, var pin) signal childPinDeleted(var childAttribute, var pin) signal pressed(var mouse) signal edgeAboutToBeRemoved(var input) - signal clicked() objectName: attribute ? attribute.name + "." : "" diff --git a/meshroom/ui/qml/GraphEditor/Node.qml b/meshroom/ui/qml/GraphEditor/Node.qml index 60ec434581..99a57885c7 100755 --- a/meshroom/ui/qml/GraphEditor/Node.qml +++ b/meshroom/ui/qml/GraphEditor/Node.qml @@ -126,9 +126,10 @@ Item { * it is reset. */ if (Boolean(attribute.enabled)) { - // If the parent's a GroupAttribute, use status of the parent's pin to determine visibility + // If the parent's a GroupAttribute, use status of the parent's pin to determine visibility UNLESS the + // child attribute is already connected if (attribute.root && attribute.root.type === "GroupAttribute") { - var visible = Boolean(parentPins.get(attribute.root.name)) + var visible = Boolean(parentPins.get(attribute.root.name) || attribute.hasOutputConnections || attribute.isLinkNested) if (!visible && parentPins.has(attribute.name) && parentPins.get(attribute.name) === true) { parentPins.set(attribute.name, false) pin.expanded = false @@ -148,8 +149,10 @@ Item { for (let i = 0; i < node.attributes.count; ++i) { let attr = node.attributes.at(i) if (attr.isOutput == isOutput) { + // Add the attribute to the model attributes.push(attr) if (attr.type === "GroupAttribute") { + // If it is a GroupAttribute, initialize its pin status parentPins.set(attr.name, false) } @@ -451,16 +454,17 @@ Item { signal parentPinsUpdated() Repeater { - model: generateAttributesModel(true, outputs.parentPins) // isOutput = true + model: root.generateAttributesModel(true, outputs.parentPins) // isOutput = true delegate: Loader { id: outputLoader active: Boolean(modelData.isOutput && modelData.desc.visible) visible: { - if (Boolean(modelData.enabled || modelData.hasOutputConnections)) { + if (Boolean(modelData.enabled || modelData.hasOutputConnections || modelData.isLinkNested)) { if (modelData.root && modelData.root.type === "GroupAttribute") { - return Boolean(outputs.parentPins.get(modelData.root.name)) + return Boolean(outputs.parentPins.get(modelData.root.name) || + modelData.hasOutputConnections || modelData.isLinkNested) } return true } @@ -485,8 +489,11 @@ Item { property real globalX: root.x + nodeAttributes.x + outputs.x + outputLoader.x + outPin.x property real globalY: root.y + nodeAttributes.y + outputs.y + outputLoader.y + outPin.y + onIsConnectedChanged: function() { + outputs.parentPinsUpdated() + } + onPressed: function(mouse) { root.pressed(mouse) } - onEdgeAboutToBeRemoved: function(input) { root.edgeAboutToBeRemoved(input) } onClicked: { expanded = !expanded if (outputs.parentPins.has(modelData.name)) { @@ -494,6 +501,9 @@ Item { outputs.parentPinsUpdated() } } + onEdgeAboutToBeRemoved: function(input) { + root.edgeAboutToBeRemoved(input) + } Component.onCompleted: attributePinCreated(attribute, outPin) onChildPinCreated: attributePinCreated(childAttribute, outPin) @@ -513,7 +523,7 @@ Item { signal parentPinsUpdated() Repeater { - model: generateAttributesModel(false, inputs.parentPins) // isOutput = false + model: root.generateAttributesModel(false, inputs.parentPins) // isOutput = false delegate: Loader { id: inputLoader @@ -521,7 +531,8 @@ Item { visible: { if (Boolean(modelData.enabled)) { if (modelData.root && modelData.root.type === "GroupAttribute") { - return Boolean(inputs.parentPins.get(modelData.root.name)) + return Boolean(inputs.parentPins.get(modelData.root.name) || + modelData.hasOutputConnections || modelData.isLinkNested) } return true } @@ -545,6 +556,10 @@ Item { property real globalX: root.x + nodeAttributes.x + inputs.x + inputLoader.x + inPin.x property real globalY: root.y + nodeAttributes.y + inputs.y + inputLoader.y + inPin.y + onIsConnectedChanged: function() { + inputs.parentPinsUpdated() + } + readOnly: Boolean(root.readOnly || object.isReadOnly) Component.onCompleted: attributePinCreated(attribute, inPin) Component.onDestruction: attributePinDeleted(attribute, inPin) @@ -556,7 +571,10 @@ Item { inputs.parentPinsUpdated() } } - onEdgeAboutToBeRemoved: function(input) { root.edgeAboutToBeRemoved(input) } + onEdgeAboutToBeRemoved: function(input) { + root.edgeAboutToBeRemoved(input) + } + onChildPinCreated: function(childAttribute, inPin) { attributePinCreated(childAttribute, inPin) } onChildPinDeleted: function(childAttribute, inPin) { attributePinDeleted(childAttribute, inPin) } } @@ -600,7 +618,7 @@ Item { signal parentPinsUpdated() Repeater { - model: generateAttributesModel(false, inputParams.parentPins) // isOutput = false + model: root.generateAttributesModel(false, inputParams.parentPins) // isOutput = false delegate: Loader { id: paramLoader @@ -608,7 +626,8 @@ Item { visible: { if (Boolean(modelData.enabled || modelData.isLinkNested || modelData.hasOutputConnections)) { if (modelData.root && modelData.root.type === "GroupAttribute") { - return Boolean(inputParams.parentPins.get(modelData.root.name)) + return Boolean(inputParams.parentPins.get(modelData.root.name) || + modelData.hasOutputConnections || modelData.isLinkNested) } return true } @@ -633,6 +652,10 @@ Item { property real globalX: root.x + nodeAttributes.x + inputParamsRect.x + paramLoader.x + inParamsPin.x property real globalY: root.y + nodeAttributes.y + inputParamsRect.y + paramLoader.y + inParamsPin.y + onIsConnectedChanged: function() { + inputParams.parentPinsUpdated() + } + height: isFullyActive ? childrenRect.height : 0 Behavior on height { PropertyAnimation {easing.type: Easing.Linear} } visible: (height == childrenRect.height) @@ -648,7 +671,10 @@ Item { inputParams.parentPinsUpdated() } } - onEdgeAboutToBeRemoved: function(input) { root.edgeAboutToBeRemoved(input) } + onEdgeAboutToBeRemoved: function(input) { + root.edgeAboutToBeRemoved(input) + } + onChildPinCreated: function(childAttribute, inParamsPin) { attributePinCreated(childAttribute, inParamsPin) } onChildPinDeleted: function(childAttribute, inParamsPin) { attributePinDeleted(childAttribute, inParamsPin) } } From f521bfa663273ceb150495fe5661c75a37ecae3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Fri, 13 Dec 2024 17:05:30 +0100 Subject: [PATCH 13/21] [GraphEditor] Remove enabling/disabling pins depending on edge's visibility When edges could exist while being invisible in the graph, pins needed to be enabled/disabled manually according to the visibility of the edge. This was especially important when closing the application and removing all the (hidden) edges as the pins needed to be properly destryed as well. Edges are not hidden anymore, and any attribute that should be hidden but is in fact connected remains visible as long as the connection exists. --- meshroom/ui/qml/GraphEditor/AttributePin.qml | 8 -------- meshroom/ui/qml/GraphEditor/GraphEditor.qml | 12 ------------ 2 files changed, 20 deletions(-) diff --git a/meshroom/ui/qml/GraphEditor/AttributePin.qml b/meshroom/ui/qml/GraphEditor/AttributePin.qml index b8130c5895..7d17b6bc4e 100755 --- a/meshroom/ui/qml/GraphEditor/AttributePin.qml +++ b/meshroom/ui/qml/GraphEditor/AttributePin.qml @@ -50,14 +50,6 @@ RowLayout { x: nameLabel.x } - function updatePin(isSrc, isVisible) { - if (isSrc) { - innerOutputAnchor.linkEnabled = isVisible - } else { - innerInputAnchor.linkEnabled = isVisible - } - } - function updateLabel() { var label = "" var expandedGroup = expanded ? "-" : "+" diff --git a/meshroom/ui/qml/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml index 0f33730828..7a17f4adef 100755 --- a/meshroom/ui/qml/GraphEditor/GraphEditor.qml +++ b/meshroom/ui/qml/GraphEditor/GraphEditor.qml @@ -511,18 +511,6 @@ Item { } } } - - Component.onDestruction: { - // Handles the case where the edge is destroyed while hidden because it is replaced: the pins should be re-enabled - if (!_window.isClosing) { - // When the window is closing, the QML context becomes invalid, which causes function calls to result in errors - if (src && src !== undefined) - src.updatePin(true, true) // isSrc = true, isVisible = true - if (dst && dst !== undefined) - dst.updatePin(false, true) // isSrc = false, isVisible = true - } - } - } } From 279582b975a258b94bca4d6a5e3feec6b9889426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Thu, 19 Dec 2024 16:26:00 +0100 Subject: [PATCH 14/21] [core] Attribute: Use `root`'s values for `depth` and `exposed` If an attribute has a parent (`root is not None`), directly use the parent's values for the `depth` and `exposed` properties instead of setting them from scratch everytime from a `while` loop. If the attribute has no parent, then `depth = 0` and `exposed` is set directly from the description. --- meshroom/core/attribute.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/meshroom/core/attribute.py b/meshroom/core/attribute.py index 92f2bf277b..00e46f0da9 100644 --- a/meshroom/core/attribute.py +++ b/meshroom/core/attribute.py @@ -64,15 +64,8 @@ def __init__(self, node, attributeDesc, isOutput, root=None, parent=None): self._description = attributeDesc.description self._invalidate = False if self._isOutput else attributeDesc.invalidate - self._exposed = attributeDesc.exposed - self._depth = 0 - if root is not None: - current = self - while current.root is not None: - self._depth += 1 - if current.root.exposed != self._exposed: - self._exposed = current.root.exposed - current = current.root + self._exposed = root.exposed if root is not None else attributeDesc.exposed + self._depth = root.depth + 1 if root is not None else 0 # invalidation value for output attributes self._invalidationValue = "" From f6c9da1d1ab66649b1bac79a4c94367934df452d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Fri, 20 Dec 2024 16:12:19 +0100 Subject: [PATCH 15/21] [GraphEditor] AttributePin: Add a delay before displaying the tooltip --- meshroom/ui/qml/GraphEditor/AttributePin.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/meshroom/ui/qml/GraphEditor/AttributePin.qml b/meshroom/ui/qml/GraphEditor/AttributePin.qml index 7d17b6bc4e..f2b6271779 100755 --- a/meshroom/ui/qml/GraphEditor/AttributePin.qml +++ b/meshroom/ui/qml/GraphEditor/AttributePin.qml @@ -45,6 +45,7 @@ RowLayout { ToolTip { text: attribute.fullName + ": " + attribute.type visible: nameLabel.hovered + delay: 500 y: nameLabel.y + nameLabel.height x: nameLabel.x From 3a1ab9b9df40330a363f4e6d0ec6b2aa6d13a7aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Mon, 30 Dec 2024 16:26:31 +0100 Subject: [PATCH 16/21] [GraphEditor] AttributePin: Use `root` accessor for QML properties --- meshroom/ui/qml/GraphEditor/AttributePin.qml | 48 ++++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/meshroom/ui/qml/GraphEditor/AttributePin.qml b/meshroom/ui/qml/GraphEditor/AttributePin.qml index f2b6271779..792fa2b335 100755 --- a/meshroom/ui/qml/GraphEditor/AttributePin.qml +++ b/meshroom/ui/qml/GraphEditor/AttributePin.qml @@ -68,7 +68,7 @@ RowLayout { // Instantiate empty Items for each child attribute Repeater { id: childrenRepeater - model: isList && !attribute.isLink ? attribute.value : 0 + model: root.isList && !root.attribute.isLink ? root.attribute.value : 0 onItemAdded: function(index, item) { childPinCreated(item.childAttribute, root) } onItemRemoved: function(index, item) { childPinDeleted(item.childAttribute, root) } delegate: Item { @@ -78,12 +78,12 @@ RowLayout { } Rectangle { - visible: !attribute.isOutput + visible: !root.attribute.isOutput id: inputAnchor width: 8 height: width - radius: isList ? 0 : width / 2 + radius: root.isList ? 0 : width / 2 Layout.alignment: Qt.AlignVCenter border.color: Colors.sysPalette.mid @@ -92,8 +92,8 @@ RowLayout { Rectangle { id: innerInputAnchor property bool linkEnabled: true - visible: inputConnectMA.containsMouse || childrenRepeater.count > 0 || (attribute && attribute.isLink && linkEnabled) || inputConnectMA.drag.active || inputDropArea.containsDrag - radius: isList ? 0 : 2 + visible: inputConnectMA.containsMouse || childrenRepeater.count > 0 || (root.attribute && root.attribute.isLink && linkEnabled) || inputConnectMA.drag.active || inputDropArea.containsDrag + radius: root.isList ? 0 : 2 anchors.fill: parent anchors.margins: 2 color: { @@ -168,7 +168,7 @@ RowLayout { MouseArea { id: inputConnectMA - drag.target: attribute.isReadOnly ? undefined : inputDragTarget + drag.target: root.attribute.isReadOnly ? undefined : inputDragTarget drag.threshold: 0 // Move the edge's tip straight to the the current mouse position instead of waiting after the drag operation has started drag.smoothed: false @@ -206,7 +206,7 @@ RowLayout { implicitHeight: childrenRect.height Layout.fillWidth: true Layout.alignment: { - if (attribute.isOutput) { + if (root.attribute.isOutput) { return Qt.AlignRight | Qt.AlignVCenter } return Qt.AlignLeft | Qt.AlignVCenter @@ -216,8 +216,8 @@ RowLayout { id: nameLabel anchors.rightMargin: 0 - anchors.right: attribute && attribute.isOutput ? parent.right : undefined - labelIconRow.layoutDirection: attribute.isOutput ? Qt.RightToLeft : Qt.LeftToRight + anchors.right: root.attribute && root.attribute.isOutput ? parent.right : undefined + labelIconRow.layoutDirection: root.attribute.isOutput ? Qt.RightToLeft : Qt.LeftToRight labelIconRow.spacing: 0 enabled: !root.readOnly @@ -227,7 +227,7 @@ RowLayout { outputConnectMA.drag.active || outputDropArea.containsDrag) labelIconColor: { - if ((object.hasOutputConnections || object.isLink) && !object.enabled) { + if ((root.attribute.hasOutputConnections || root.attribute.isLink) && !root.attribute.enabled) { return Colors.lightgrey } else if (hovered) { return palette.highlight @@ -237,31 +237,31 @@ RowLayout { labelIconMouseArea.enabled: false // Prevent mixing mouse interactions between the label and the pin context // Text - label.text: attribute.label + label.text: root.attribute.label label.font.pointSize: 7 label.elide: hovered ? Text.ElideNone : Text.ElideMiddle - label.horizontalAlignment: attribute && attribute.isOutput ? Text.AlignRight : Text.AlignLeft + label.horizontalAlignment: root.attribute && root.attribute.isOutput ? Text.AlignRight : Text.AlignLeft // Icon iconText: { - if (isGroup) { - return expanded ? MaterialIcons.expand_more : MaterialIcons.chevron_right + if (root.isGroup) { + return root.expanded ? MaterialIcons.expand_more : MaterialIcons.chevron_right } return "" } iconSize: 7 - icon.horizontalAlignment: attribute && attribute.isOutput ? Text.AlignRight : Text.AlignLeft + icon.horizontalAlignment: root.attribute && root.attribute.isOutput ? Text.AlignRight : Text.AlignLeft // Handle tree view for nested attributes icon.leftPadding: { - if (attribute.depth != 0 && !attribute.isOutput) { - return attribute.depth * 10 + if (root.attribute.depth != 0 && !root.attribute.isOutput) { + return root.attribute.depth * 10 } return 0 } icon.rightPadding: { - if (attribute.depth != 0 && attribute.isOutput) { - return attribute.depth * 10 + if (root.attribute.depth != 0 && root.attribute.isOutput) { + return root.attribute.depth * 10 } return 0 } @@ -271,10 +271,10 @@ RowLayout { Rectangle { id: outputAnchor - visible: displayOutputPinForInput || attribute.isOutput + visible: root.displayOutputPinForInput || root.attribute.isOutput width: 8 height: width - radius: isList ? 0 : width / 2 + radius: root.isList ? 0 : width / 2 Layout.alignment: Qt.AlignVCenter @@ -284,8 +284,8 @@ RowLayout { Rectangle { id: innerOutputAnchor property bool linkEnabled: true - visible: (attribute.hasOutputConnections && linkEnabled) || outputConnectMA.containsMouse || outputConnectMA.drag.active || outputDropArea.containsDrag - radius: isList ? 0 : 2 + visible: (root.attribute.hasOutputConnections && linkEnabled) || outputConnectMA.containsMouse || outputConnectMA.drag.active || outputDropArea.containsDrag + radius: root.isList ? 0 : 2 anchors.fill: parent anchors.margins: 2 color: { @@ -343,7 +343,7 @@ RowLayout { readonly property alias nodeItem: root.nodeItem readonly property bool isOutput: Boolean(attribute.isOutput) readonly property alias isList: root.isList - readonly property string baseType: attribute.baseType !== undefined ? attribute.baseType : "" + readonly property string baseType: root.attribute.baseType !== undefined ? attribute.baseType : "" property bool dropAccepted: false anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter From 431c5288d313d9ea869b973d3cd74981072b9b6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Mon, 30 Dec 2024 16:39:15 +0100 Subject: [PATCH 17/21] [GraphEditor] AttributePin: Disable connections between `GroupAttributes` As connections between groups are currently not supported, add connecting any `GroupAttribute` to the list of connections to refuse. --- meshroom/ui/qml/GraphEditor/AttributePin.qml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/meshroom/ui/qml/GraphEditor/AttributePin.qml b/meshroom/ui/qml/GraphEditor/AttributePin.qml index 792fa2b335..5b83979c7c 100755 --- a/meshroom/ui/qml/GraphEditor/AttributePin.qml +++ b/meshroom/ui/qml/GraphEditor/AttributePin.qml @@ -123,6 +123,7 @@ RowLayout { || drag.source.nodeItem === inputDragTarget.nodeItem // Connection between attributes of the same node || (drag.source.isList && childrenRepeater.count) // Source/target are lists but target already has children || drag.source.connectorType === "input" // Refuse to connect an "input pin" on another one (input attr can be connected to input attr, but not the graphical pin) + || (drag.source.isGroup || inputDragTarget.isGroup) // Refuse connection between Groups, which is unsupported ) { // Refuse attributes connection drag.accepted = false @@ -155,6 +156,7 @@ RowLayout { readonly property bool isOutput: Boolean(attribute.isOutput) readonly property string baseType: attribute.baseType !== undefined ? attribute.baseType : "" readonly property alias isList: root.isList + readonly property alias isGroup: root.isGroup property bool dragAccepted: false anchors.verticalCenter: parent.verticalCenter anchors.horizontalCenter: parent.horizontalCenter @@ -316,6 +318,7 @@ RowLayout { || (!drag.source.isList && outputDragTarget.isList) // Connection between a list and a simple attribute || (drag.source.isList && childrenRepeater.count) // Source/target are lists but target already has children || drag.source.connectorType === "output" // Refuse to connect an output pin on another one + || (drag.source.isGroup || outputDragTarget.isGroup) // Refuse connection between Groups, which is unsupported ) { // Refuse attributes connection drag.accepted = false @@ -343,6 +346,7 @@ RowLayout { readonly property alias nodeItem: root.nodeItem readonly property bool isOutput: Boolean(attribute.isOutput) readonly property alias isList: root.isList + readonly property alias isGroup: root.isGroup readonly property string baseType: root.attribute.baseType !== undefined ? attribute.baseType : "" property bool dropAccepted: false anchors.horizontalCenter: parent.horizontalCenter From d5a7e2572a3506abc3439bb77029dafc99fb78a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Mon, 30 Dec 2024 18:34:36 +0100 Subject: [PATCH 18/21] [GraphEditor] AttributePin: Do not start dragging edge on `pressed` Prior to this commit, the edge that was being dragged to be connected appeared as soon as the `pressed` signal was emitted by the mouse. Now that the user can actually click (press & release) without dragging the mouse to expand or collapse groups, we want to be able to distinguish cases where the user presses the mouse to click (to expand/collapse groups) from those where the user presses the mouse to drag it (to connect edges). Instead of triggering the drag as soon as the `pressed` signal is emitted, we wait to see if the mouse moves significantly enough to indicate the will to drag; an arbitrary limit of 10 pixels along the X- or Y-axis is used to determine that the user means to drag the mouse. --- meshroom/ui/qml/GraphEditor/AttributePin.qml | 47 ++++++++++++++++++-- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/meshroom/ui/qml/GraphEditor/AttributePin.qml b/meshroom/ui/qml/GraphEditor/AttributePin.qml index 5b83979c7c..f4cb3d8a59 100755 --- a/meshroom/ui/qml/GraphEditor/AttributePin.qml +++ b/meshroom/ui/qml/GraphEditor/AttributePin.qml @@ -180,13 +180,31 @@ RowLayout { anchors.margins: inputDropArea.anchors.margins anchors.leftMargin: inputDropArea.anchors.leftMargin anchors.rightMargin: inputDropArea.anchors.rightMargin + + property bool dragTriggered: false // An edge is being dragged from the output connector + property bool isPressed: false // The mouse has been pressed but not yet released + property double initialX: 0.0 + property double initialY: 0.0 + onPressed: function(mouse) { root.pressed(mouse) + isPressed = true + initialX = mouse.x + initialY = mouse.y } - onReleased: { + onReleased: function(mouse) { inputDragTarget.Drag.drop() + isPressed = false + dragTriggered = false } onClicked: root.clicked() + onPositionChanged: function(mouse) { + // If there's been a significant (10px along the X- or Y- axis) while the mouse is being pressed, + // then we can consider being in the dragging state + if (isPressed && (Math.abs(mouse.x - initialX) >= 5.0 || Math.abs(mouse.y - initialY) >= 5.0)) { + dragTriggered = true + } + } hoverEnabled: root.visible } @@ -371,9 +389,30 @@ RowLayout { anchors.leftMargin: outputDropArea.anchors.leftMargin anchors.rightMargin: outputDropArea.anchors.rightMargin - onPressed: function(mouse) { root.pressed(mouse) } - onReleased: outputDragTarget.Drag.drop() + property bool dragTriggered: false // An edge is being dragged from the output connector + property bool isPressed: false // The mouse has been pressed but not yet released + property double initialX: 0.0 + property double initialY: 0.0 + + onPressed: function(mouse) { + root.pressed(mouse) + isPressed = true + initialX = mouse.x + initialY = mouse.y + } + onReleased: function(mouse) { + outputDragTarget.Drag.drop() + isPressed = false + dragTriggered = false + } onClicked: root.clicked() + onPositionChanged: function(mouse) { + // If there's been a significant (10px along the X- or Y- axis) while the mouse is being pressed, + // then we can consider being in the dragging state + if (isPressed && (Math.abs(mouse.x - initialX) >= 5.0 || Math.abs(mouse.y - initialY) >= 5.0)) { + dragTriggered = true + } + } hoverEnabled: root.visible } @@ -390,7 +429,7 @@ RowLayout { } } - state: (inputConnectMA.pressed) ? "DraggingInput" : outputConnectMA.pressed ? "DraggingOutput" : "" + state: inputConnectMA.dragTriggered ? "DraggingInput" : outputConnectMA.dragTriggered ? "DraggingOutput" : "" states: [ State { From 11f25761806c433ceb849b875d79037fcda2d0fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Tue, 31 Dec 2024 13:22:10 +0100 Subject: [PATCH 19/21] [MaterialIcons] MaterialToolLabel: Handle label's size correctly --- meshroom/ui/qml/MaterialIcons/MaterialToolLabel.qml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/meshroom/ui/qml/MaterialIcons/MaterialToolLabel.qml b/meshroom/ui/qml/MaterialIcons/MaterialToolLabel.qml index ac92736367..aac08d5df8 100644 --- a/meshroom/ui/qml/MaterialIcons/MaterialToolLabel.qml +++ b/meshroom/ui/qml/MaterialIcons/MaterialToolLabel.qml @@ -16,10 +16,15 @@ Item { property alias labelIconRow: contentRow property var labelIconColor: palette.text property alias labelIconMouseArea: mouseArea + property var labelWidth: undefined implicitWidth: childrenRect.width implicitHeight: childrenRect.height anchors.rightMargin: 5 + onLabelWidthChanged: { + labelItem.width = labelWidth + } + RowLayout { id: contentRow Label { @@ -34,6 +39,12 @@ Item { id: labelItem text: "" color: labelIconColor + width: labelWidth + + onWidthChanged: { + if (labelWidth != undefined && width != labelWidth) + width = labelWidth + } } } From 3234bb939208b5ebaa51b7bd543b8552401c34ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Tue, 31 Dec 2024 13:24:40 +0100 Subject: [PATCH 20/21] [MaterialIcons] MaterialToolLabel: Use `control` accessor for properties --- meshroom/ui/qml/MaterialIcons/MaterialToolLabel.qml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/meshroom/ui/qml/MaterialIcons/MaterialToolLabel.qml b/meshroom/ui/qml/MaterialIcons/MaterialToolLabel.qml index aac08d5df8..53aac3a78d 100644 --- a/meshroom/ui/qml/MaterialIcons/MaterialToolLabel.qml +++ b/meshroom/ui/qml/MaterialIcons/MaterialToolLabel.qml @@ -33,17 +33,17 @@ Item { font.pointSize: 13 padding: 0 text: "" - color: labelIconColor + color: control.labelIconColor } Label { id: labelItem text: "" - color: labelIconColor - width: labelWidth + color: control.labelIconColor + width: control.labelWidth onWidthChanged: { - if (labelWidth != undefined && width != labelWidth) - width = labelWidth + if (control.labelWidth != undefined && width != control.labelWidth) + width = control.labelWidth } } } From 0716f0046fca607e1331a383500742576592c829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Fri, 10 Jan 2025 15:38:28 +0000 Subject: [PATCH 21/21] [GraphEditor] AttributePin: Handle width and elide for attributes' name --- meshroom/ui/qml/GraphEditor/AttributePin.qml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/meshroom/ui/qml/GraphEditor/AttributePin.qml b/meshroom/ui/qml/GraphEditor/AttributePin.qml index f4cb3d8a59..e4718fc9e5 100755 --- a/meshroom/ui/qml/GraphEditor/AttributePin.qml +++ b/meshroom/ui/qml/GraphEditor/AttributePin.qml @@ -236,13 +236,26 @@ RowLayout { id: nameLabel anchors.rightMargin: 0 - anchors.right: root.attribute && root.attribute.isOutput ? parent.right : undefined labelIconRow.layoutDirection: root.attribute.isOutput ? Qt.RightToLeft : Qt.LeftToRight + anchors.right: root.attribute && root.attribute.isOutput ? parent.right : undefined labelIconRow.spacing: 0 + width: { + if (hovered) { + return icon.width + label.contentWidth + } else { + if (nameContainer.width > 0 && icon.width + label.contentWidth < nameContainer.width) + return icon.width + label.contentWidth + return nameContainer.width + } + } enabled: !root.readOnly visible: true - property bool hovered: (inputConnectMA.containsMouse || inputConnectMA.drag.active || + property bool parentNotReady: nameContainer.width == 0 // Allows to trigger a change of state once the parent is ready, + // ensuring the correct width of the elements upon their first + // display without waiting for a mouse interaction + property bool hovered: parentNotReady || + (inputConnectMA.containsMouse || inputConnectMA.drag.active || inputDropArea.containsDrag || outputConnectMA.containsMouse || outputConnectMA.drag.active || outputDropArea.containsDrag) @@ -259,6 +272,7 @@ RowLayout { // Text label.text: root.attribute.label label.font.pointSize: 7 + labelWidth: hovered ? label.contentWidth : nameLabel.width - icon.width label.elide: hovered ? Text.ElideNone : Text.ElideMiddle label.horizontalAlignment: root.attribute && root.attribute.isOutput ? Text.AlignRight : Text.AlignLeft