diff --git a/meshroom/core/attribute.py b/meshroom/core/attribute.py
index a80a3fb667..4256cd19b4 100644
--- a/meshroom/core/attribute.py
+++ b/meshroom/core/attribute.py
@@ -77,6 +77,9 @@ def __init__(self, node, attributeDesc: desc.Attribute, isOutput: bool, root=Non
self._description: str = attributeDesc.description
self._invalidate = False if self._isOutput else attributeDesc.invalidate
+ 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 = ""
@@ -92,6 +95,12 @@ def node(self):
def root(self):
return self._root() if self._root else None
+ def getDepth(self):
+ return self._depth
+
+ def getExposed(self) -> bool:
+ return self._exposed
+
def getName(self) -> str:
""" Attribute name """
return self._name
@@ -379,7 +388,7 @@ def _applyExpr(self):
elif self.isInput and Attribute.isLinkExpression(v):
# value is a link to another attribute
link = v[1:-1]
- linkNodeName, linkAttrName = link.split('.')
+ linkNodeName, linkAttrName = link.split('.', 1)
try:
node = g.node(linkNodeName)
if not node:
@@ -460,6 +469,13 @@ def updateInternals(self):
# Emit if the enable status has changed
self.setEnabled(self.getEnabled())
+ 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. """
+ # For all attributes but GroupAttributes, there cannot be any child
+ return []
+
def _is3D(self) -> bool:
""" Return True if the current attribute is considered as a 3d file """
@@ -493,6 +509,7 @@ def _is2D(self) -> bool:
type = Property(str, getType, constant=True)
baseType = Property(str, getType, constant=True)
isReadOnly = Property(bool, _isReadOnly, constant=True)
+ exposed = Property(bool, getExposed, constant=True)
is3D = Property(bool, _is3D, constant=True)
is2D = Property(bool, _is2D, constant=True)
@@ -531,6 +548,8 @@ def _is2D(self) -> bool:
validValueChanged = Signal()
validValue = Property(bool, getValidValue, setValidValue, notify=validValueChanged)
root = Property(BaseObject, root.fget, constant=True)
+ depth = Property(int, getDepth, constant=True)
+ flatStaticChildren = Property(Variant, getFlatStaticChildren, constant=True)
def raiseIfLink(func):
@@ -945,6 +964,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: str) -> bool:
return super().matchText(text) or any(c.matchText(text) for c in self._value)
@@ -952,3 +985,4 @@ def matchText(self, text: str) -> bool:
# 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/core/node.py b/meshroom/core/node.py
index 50ee7ba617..bc10c5253d 100644
--- a/meshroom/core/node.py
+++ b/meshroom/core/node.py
@@ -1947,9 +1947,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
diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py
index 699d3a8a83..5a66084ca7 100644
--- a/meshroom/ui/graph.py
+++ b/meshroom/ui/graph.py
@@ -780,7 +780,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()]
diff --git a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml
index 7a830d6e3f..7d28bea13a 100644
--- a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml
+++ b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml
@@ -39,8 +39,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
}
}
@@ -113,7 +114,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 d435601bd5..5f16490c6c 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.
@@ -13,6 +14,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,37 +27,48 @@ 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
+ 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 + "." : ""
layoutDirection: Qt.LeftToRight
spacing: 3
ToolTip {
- text: attribute.name + ": " + attribute.type
+ text: attribute.fullName + ": " + attribute.type
visible: nameLabel.hovered
+ delay: 500
- y: nameLabel.y + nameLabel.height
x: nameLabel.x
+ y: nameLabel.y + nameLabel.height
}
- function updatePin(isSrc, isVisible) {
- if (isSrc) {
- innerOutputAnchor.linkEnabled = isVisible
- } else {
- innerInputAnchor.linkEnabled = isVisible
+ 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
}
// 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 {
@@ -64,125 +77,153 @@ RowLayout {
}
}
- Rectangle {
- visible: !attribute.isOutput
- id: inputAnchor
-
- width: 8
- height: width
- radius: isList ? 0 : width / 2
+ Item {
+ width: childrenRect.width
Layout.alignment: Qt.AlignVCenter
-
- border.color: Colors.sysPalette.mid
- color: Colors.sysPalette.base
+ Layout.fillWidth: false
+ Layout.fillHeight: true
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
- anchors.fill: parent
- anchors.margins: 2
- color: {
- if (inputConnectMA.containsMouse || inputConnectMA.drag.active || (inputDropArea.containsDrag && inputDropArea.acceptableDrop))
- return Colors.sysPalette.highlight
- return Colors.sysPalette.text
- }
- }
+ visible: !root.attribute.isOutput
+ id: inputAnchor
- DropArea {
- id: inputDropArea
+ width: 8
+ height: width
+ radius: root.isList ? 0 : width / 2
+ anchors.verticalCenter: parent.verticalCenter
- property bool acceptableDrop: false
+ border.color: Colors.sysPalette.mid
+ color: Colors.sysPalette.base
+
+ Rectangle {
+ id: innerInputAnchor
+ property bool linkEnabled: true
+ 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: {
+ if (inputConnectMA.containsMouse || inputConnectMA.drag.active || (inputDropArea.containsDrag && inputDropArea.acceptableDrop))
+ return Colors.sysPalette.highlight
+ return Colors.sysPalette.text
+ }
+ }
- // Add negative margins for DropArea to make the connection zone easier to reach
- anchors.fill: parent
- anchors.margins: -2
- // Add horizontal negative margins according to the current layout
- anchors.rightMargin: -root.width * 0.3
+ DropArea {
+ id: inputDropArea
+
+ property bool acceptableDrop: false
+
+ // Add negative margins for DropArea to make the connection zone easier to reach
+ anchors.fill: parent
+ anchors.margins: -2
+ // Add horizontal negative margins according to the current layout
+ anchors.rightMargin: -root.width * 0.3
+
+ keys: [inputDragTarget.objectName]
+ onEntered: function(drag) {
+ // Check if attributes are compatible to create a valid connection
+ if (root.readOnly // Cannot connect on a read-only attribute
+ || drag.source.objectName != inputDragTarget.objectName // Not an edge connector
+ || drag.source.baseType !== inputDragTarget.baseType // Not the same base type
+ || 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
+ } else if (inputDragTarget.attribute.isLink) { // Already connected attribute
+ root.edgeAboutToBeRemoved(inputDragTarget.attribute)
+ }
+ inputDropArea.acceptableDrop = drag.accepted
+ }
- keys: [inputDragTarget.objectName]
- onEntered: function(drag) {
- // Check if attributes are compatible to create a valid connection
- if (root.readOnly // Cannot connect on a read-only attribute
- || drag.source.objectName != inputDragTarget.objectName // Not an edge connector
- || drag.source.baseType !== inputDragTarget.baseType // Not the same base type
- || 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)
- ) {
- // Refuse attributes connection
- drag.accepted = false
- } else if (inputDragTarget.attribute.isLink) { // Already connected attribute
- root.edgeAboutToBeRemoved(inputDragTarget.attribute)
+ onExited: {
+ if (inputDragTarget.attribute.isLink) { // Already connected attribute
+ root.edgeAboutToBeRemoved(undefined)
+ }
+ acceptableDrop = false
+ drag.source.dropAccepted = false
}
- inputDropArea.acceptableDrop = drag.accepted
- }
- onExited: {
- if (inputDragTarget.attribute.isLink) { // Already connected attribute
+ onDropped: function(drop) {
root.edgeAboutToBeRemoved(undefined)
+ _reconstruction.addEdge(drag.source.attribute, inputDragTarget.attribute)
}
- acceptableDrop = false
- drag.source.dropAccepted = false
}
- onDropped: function(drop) {
- root.edgeAboutToBeRemoved(undefined)
- _reconstruction.addEdge(drag.source.attribute, inputDragTarget.attribute)
+ Item {
+ id: inputDragTarget
+ objectName: "edgeConnector"
+ readonly property string connectorType: "input"
+ readonly property alias attribute: root.attribute
+ readonly property alias nodeItem: root.nodeItem
+ 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
+ width: parent.width
+ height: parent.height
+ Drag.keys: [inputDragTarget.objectName]
+ Drag.active: inputConnectMA.drag.active
+ Drag.hotSpot.x: width * 0.5
+ Drag.hotSpot.y: height * 0.5
}
- }
- Item {
- id: inputDragTarget
- objectName: "edgeConnector"
- readonly property string connectorType: "input"
- readonly property alias attribute: root.attribute
- readonly property alias nodeItem: root.nodeItem
- readonly property bool isOutput: Boolean(attribute.isOutput)
- readonly property string baseType: attribute.baseType !== undefined ? attribute.baseType : ""
- readonly property alias isList: root.isList
- property bool dragAccepted: false
- anchors.verticalCenter: parent.verticalCenter
- anchors.horizontalCenter: parent.horizontalCenter
- width: parent.width
- height: parent.height
- Drag.keys: [inputDragTarget.objectName]
- Drag.active: inputConnectMA.drag.active
- Drag.hotSpot.x: width * 0.5
- Drag.hotSpot.y: height * 0.5
- }
-
- MouseArea {
- id: inputConnectMA
- drag.target: 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
- enabled: !root.readOnly
- anchors.fill: parent
- // Use the same negative margins as DropArea to ease pin selection
- anchors.margins: inputDropArea.anchors.margins
- anchors.leftMargin: inputDropArea.anchors.leftMargin
- anchors.rightMargin: inputDropArea.anchors.rightMargin
- onPressed: function(mouse) {
- root.pressed(mouse)
- }
- onReleased: {
- inputDragTarget.Drag.drop()
+ MouseArea {
+ id: inputConnectMA
+ 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
+ enabled: !root.readOnly
+ anchors.fill: parent
+ // Use the same negative margins as DropArea to ease pin selection
+ 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: 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
}
- hoverEnabled: true
- }
- Edge {
- id: inputConnectEdge
- visible: false
- point1x: inputDragTarget.x + inputDragTarget.width / 2
- point1y: inputDragTarget.y + inputDragTarget.height / 2
- point2x: parent.width / 2
- point2y: parent.width / 2
- color: palette.highlight
- thickness: outputDragTarget.dropAccepted ? 2 : 1
+ Edge {
+ id: inputConnectEdge
+ visible: false
+ point1x: inputDragTarget.x + inputDragTarget.width / 2
+ point1y: inputDragTarget.y + inputDragTarget.height / 2
+ point2x: parent.width / 2
+ point2y: parent.width / 2
+ color: palette.highlight
+ thickness: outputDragTarget.dropAccepted ? 2 : 1
+ }
}
}
@@ -191,35 +232,70 @@ RowLayout {
id: nameContainer
implicitHeight: childrenRect.height
Layout.fillWidth: true
+ Layout.fillHeight: true
Layout.alignment: Qt.AlignVCenter
- Label {
+ MaterialToolLabel {
id: nameLabel
+ anchors.fill: parent
+ anchors.verticalCenter: parent.verticalCenter
+
+ anchors.margins: 0
+ labelIconRow.layoutDirection: root.attribute.isOutput ? Qt.RightToLeft : Qt.LeftToRight
+ labelIconRow.spacing: 0
+
enabled: !root.readOnly
- property bool hovered: (inputConnectMA.containsMouse || inputConnectMA.drag.active || inputDropArea.containsDrag || outputConnectMA.containsMouse || outputConnectMA.drag.active || outputDropArea.containsDrag)
- text: (attribute && attribute.label) !== undefined ? attribute.label : ""
- elide: hovered ? Text.ElideNone : Text.ElideMiddle
- width: hovered ? contentWidth : parent.width
- font.pointSize: 7
- 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)
+ visible: true
+ 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)
+
+ labelIconColor: {
+ if ((root.attribute.hasOutputConnections || root.attribute.isLink) && !root.attribute.enabled) {
return Colors.lightgrey
- return hovered ? palette.highlight : palette.text
+ } 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: root.attribute.label
+ label.font.pointSize: 7
+ label.elide: Text.ElideRight
+ label.horizontalAlignment: root.attribute && root.attribute.isOutput ? Text.AlignRight : Text.AlignLeft
+ label.verticalAlignment: Text.AlignVCenter
+
+ // Icon
+ iconText: {
+ if (root.isGroup) {
+ return root.expanded ? MaterialIcons.expand_more : MaterialIcons.chevron_right
+ }
+ return ""
}
+ iconSize: 7
+ icon.horizontalAlignment: root.attribute && root.attribute.isOutput ? Text.AlignRight : Text.AlignLeft
+
+ // Handle tree view for nested attributes
+ property int groupPaddingWidth: root.attribute.depth * 10
+ icon.leftPadding: root.attribute.isOutput ? 0 : groupPaddingWidth
+ icon.rightPadding: root.attribute.isOutput ? groupPaddingWidth : 0
}
}
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
@@ -229,13 +305,13 @@ 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: {
- 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
}
@@ -261,6 +337,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
@@ -288,7 +365,8 @@ 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 alias isGroup: root.isGroup
+ readonly property string baseType: root.attribute.baseType !== undefined ? attribute.baseType : ""
property bool dropAccepted: false
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
@@ -312,10 +390,32 @@ 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: true
+ hoverEnabled: root.visible
}
Edge {
@@ -330,7 +430,7 @@ RowLayout {
}
}
- state: (inputConnectMA.pressed) ? "DraggingInput" : outputConnectMA.pressed ? "DraggingOutput" : ""
+ state: inputConnectMA.dragTriggered ? "DraggingInput" : outputConnectMA.dragTriggered ? "DraggingOutput" : ""
states: [
State {
diff --git a/meshroom/ui/qml/GraphEditor/Edge.qml b/meshroom/ui/qml/GraphEditor/Edge.qml
index 36d4a909c8..c4b80bb50c 100644
--- a/meshroom/ui/qml/GraphEditor/Edge.qml
+++ b/meshroom/ui/qml/GraphEditor/Edge.qml
@@ -128,7 +128,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/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml
index 54300a3a12..4edf8cdd04 100755
--- a/meshroom/ui/qml/GraphEditor/GraphEditor.qml
+++ b/meshroom/ui/qml/GraphEditor/GraphEditor.qml
@@ -519,18 +519,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
- }
- }
-
}
}
diff --git a/meshroom/ui/qml/GraphEditor/Node.qml b/meshroom/ui/qml/GraphEditor/Node.qml
index 28969f68b3..b430a63165 100755
--- a/meshroom/ui/qml/GraphEditor/Node.qml
+++ b/meshroom/ui/qml/GraphEditor/Node.qml
@@ -233,6 +233,58 @@ 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 UNLESS the
+ // child attribute is already connected
+ if (attribute.root && attribute.root.type === "GroupAttribute") {
+ 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
+ }
+ 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) {
+ // 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)
+ }
+
+ // 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)
+ }
+ })
+ }
+ }
+
+ return attributes
+ }
+
// Main Layout
MouseArea {
id: mouseArea
@@ -524,26 +576,60 @@ Item {
width: parent.width
spacing: 3
+ property var parentPins: new Map()
+ signal parentPinsUpdated()
+
Repeater {
- model: node ? node.attributes : undefined
+ model: root.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 || modelData.isLinkNested)) {
+ if (modelData.root && modelData.root.type === "GroupAttribute") {
+ return Boolean(outputs.parentPins.get(modelData.root.name) ||
+ modelData.hasOutputConnections || modelData.isLinkNested)
+ }
+ 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
+ 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)) {
+ outputs.parentPins.set(modelData.name, expanded)
+ outputs.parentPinsUpdated()
+ }
+ }
+ onEdgeAboutToBeRemoved: function(input) {
+ root.edgeAboutToBeRemoved(input)
+ }
Component.onCompleted: attributePinCreated(attribute, outPin)
onChildPinCreated: attributePinCreated(childAttribute, outPin)
@@ -559,28 +645,62 @@ Item {
width: parent.width
spacing: 3
+ property var parentPins: new Map()
+ signal parentPinsUpdated()
+
Repeater {
- model: node ? node.attributes : undefined
+ model: root.generateAttributesModel(false, inputs.parentPins) // isOutput = false
delegate: Loader {
id: inputLoader
- active: !object.isOutput && object.desc.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) ||
+ modelData.hasOutputConnections || modelData.isLinkNested)
+ }
+ 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
+ onIsConnectedChanged: function() {
+ inputs.parentPinsUpdated()
+ }
+
readOnly: Boolean(root.readOnly || object.isReadOnly)
Component.onCompleted: attributePinCreated(attribute, inPin)
Component.onDestruction: attributePinDeleted(attribute, inPin)
onPressed: function(mouse) { root.pressed(mouse) }
- onEdgeAboutToBeRemoved: function(input) { root.edgeAboutToBeRemoved(input) }
+ 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) }
}
@@ -619,31 +739,68 @@ Item {
id: inputParams
width: parent.width
spacing: 3
+
+ property var parentPins: new Map()
+ signal parentPinsUpdated()
+
Repeater {
- id: inputParamsRepeater
- model: node ? node.attributes : undefined
+ model: root.generateAttributesModel(false, inputParams.parentPins) // isOutput = false
+
delegate: Loader {
id: paramLoader
- active: !object.isOutput && !object.desc.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) ||
+ modelData.hasOutputConnections || modelData.isLinkNested)
+ }
+ 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
+ onIsConnectedChanged: function() {
+ inputParams.parentPinsUpdated()
+ }
+
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) }
- onEdgeAboutToBeRemoved: function(input) { root.edgeAboutToBeRemoved(input) }
+ 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) }
}
diff --git a/meshroom/ui/qml/MaterialIcons/MaterialToolLabel.qml b/meshroom/ui/qml/MaterialIcons/MaterialToolLabel.qml
index 92b7be1c7a..6b4fc847ec 100644
--- a/meshroom/ui/qml/MaterialIcons/MaterialToolLabel.qml
+++ b/meshroom/ui/qml/MaterialIcons/MaterialToolLabel.qml
@@ -9,27 +9,37 @@ 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
+ // If we are fitting to a top container, we need to propagate the "anchors.fill: parent"
+ // Otherwise, the component defines its own size based on its children.
+ anchors.fill: control.anchors.fill ? parent : undefined
Label {
id: iconItem
font.family: MaterialIcons.fontFamily
font.pointSize: 13
padding: 0
text: ""
- color: labelIconColor
+ color: control.labelIconColor
+ Layout.fillWidth: false
+ Layout.fillHeight: true
}
Label {
id: labelItem
text: ""
- color: labelIconColor
+ color: control.labelIconColor
+ Layout.fillWidth: true
+ Layout.fillHeight: true
}
}
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++) {
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