Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ef57737
[ui] AttributeEditor: Add navigation buttons for linked attributes
nicolas-lambert-tc May 2, 2025
a26870c
[ui] AttributeEditor: Add missing singal connections to allow attribu…
nicolas-lambert-tc May 2, 2025
3a9565f
[ui] AttributeEditor: The attribute nav buttons are now aligned with …
nicolas-lambert-tc May 2, 2025
97196d9
[ui] Attribute: Add tests to check retrieving the linked input/output…
nicolas-lambert-tc May 2, 2025
2f39d41
[ui] Attribute: Refacto the test to avoid the coverage complaining ab…
nicolas-lambert-tc May 2, 2025
ea1adcd
[ui] AttributeEditor: Remove console.log
nicolas-lambert-tc May 2, 2025
a3056c8
[ui] AttributeEditor: Factorize the visibility conditions in nav buttons
nicolas-lambert-tc May 2, 2025
96a09fe
[ui,core] Attribute: Refacto the linked attributes method's name for …
nicolas-lambert-tc May 2, 2025
1de3834
[ui] AttributeDelegate: Use correct alignment strategy to avoid warnings
nicolas-lambert-tc May 2, 2025
1943474
[ui] AttributeEditor: Now, middle click on nav button fit the view to…
nicolas-lambert-tc May 2, 2025
cc4e1a2
[ui, core] AttributeEditor: Fix the ListAttribute navigation buttons …
nicolas-lambert-tc May 2, 2025
3de4471
[ui] AttributeItemDelegate: Add navButton ids for clarity and future …
nicolas-lambert-tc May 2, 2025
812cc65
[ui] NodeEditor: add the listing of connected attributes for rightCli…
nicolas-lambert-tc May 2, 2025
cc2d485
[ui] NodeEditor: Factorize attributeNavButton click behavior
nicolas-lambert-tc May 2, 2025
81f6924
[core] attributes: Add tests for coverage
nicolas-lambert-tc May 2, 2025
03c745a
[ui] AttributeEditor: Fix navButton left click behavior. (Wasn't sele…
nicolas-lambert-tc May 5, 2025
3da2918
[ui] AttributeEditor: navButton context show truncated text if the la…
nicolas-lambert-tc May 5, 2025
5d2c85b
[core] Attribute: Fix ListAttributes input / output connections retri…
nicolas-lambert-tc May 5, 2025
d1b434d
[ui] Attribute: Use bool type for shouldBeVisible property
nicolas-lambert-tc May 6, 2025
7881fa1
[ui] Attribute: Align non linked attributes label (in/out) with the l…
nicolas-lambert-tc May 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 64 additions & 4 deletions meshroom/core/attribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
from meshroom.common import BaseObject, Property, Variant, Signal, ListModel, DictModel, Slot
from meshroom.core import desc, hashValue

from typing import TYPE_CHECKING

if TYPE_CHECKING:

Check warning on line 17 in meshroom/core/attribute.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/attribute.py#L17

Added line #L17 was not covered by tests
from meshroom.core.graph import Edge


def attributeFactory(description, value, isOutput, node, root=None, parent=None):
"""
Expand Down Expand Up @@ -320,12 +325,35 @@
# safety check to avoid evaluation errors
if not self.node.graph or not self.node.graph.edges:
return False
# if the attribute is a ListAttribute, we need to check if any of its elements has output connections
if isinstance(self, ListAttribute):
return next((edge for edge in self.node.graph.edges.values() if edge.src == self), None) is not None or \
any(attr.hasOutputConnections for attr in self._value if hasattr(attr, 'hasOutputConnections'))

Comment thread
fabiencastan marked this conversation as resolved.
return next((edge for edge in self.node.graph.edges.values() if edge.src == self), None) is not None

def getInputConnections(self) -> list["Edge"]:
""" Retrieve the upstreams connected edges """

if not self.node.graph or not self.node.graph.edges:
return []

return [edge for edge in self.node.graph.edges.values() if edge.dst == self]

def getOutputConnections(self) -> list["Edge"]:
""" Retrieve all the edges connected to this attribute """

if not self.node.graph or not self.node.graph.edges:
return []

return [edge for edge in self.node.graph.edges.values() if edge.src == self]

def getLinkedInAttributes(self) -> list["Attribute"]:
""" Return the upstreams connected attributes """

return [edge.src for edge in self.getInputConnections()]

def getLinkedOutAttributes(self) -> list["Attribute"]:
""" Return the downstreams connected attributes """

return [edge.dst for edge in self.getOutputConnections()]

def _applyExpr(self):
"""
For string parameters with an expression (when loaded from file),
Expand Down Expand Up @@ -449,6 +477,8 @@
isLinkNested = isLink
hasOutputConnectionsChanged = Signal()
hasOutputConnections = Property(bool, hasOutputConnections.fget, notify=hasOutputConnectionsChanged)
linkedInAttributes = Property(Variant, getLinkedInAttributes)
Comment thread
nicolas-lambert-tc marked this conversation as resolved.
linkedOutAttributes = Property(Variant, getLinkedOutAttributes)
isDefault = Property(bool, _isDefault, notify=valueChanged)
linkParam = Property(BaseObject, getLinkParam, notify=isLinkChanged)
rootLinkParam = Property(BaseObject, lambda self: self.getLinkParam(recursive=True), notify=isLinkChanged)
Expand Down Expand Up @@ -700,12 +730,42 @@
return self.isLink \
or self.node.graph and self.isInput and self.node.graph._edges \
and any(v in self.node.graph._edges.keys() for v in self._value)

# override
@property
def hasOutputConnections(self):
""" Whether the attribute has output connections, i.e is the source of at least one edge. """

# safety check to avoid evaluation errors
if not self.node.graph or not self.node.graph.edges:

Check warning on line 740 in meshroom/core/attribute.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/attribute.py#L739-L740

Added lines #L739 - L740 were not covered by tests
return False

Check warning on line 742 in meshroom/core/attribute.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/attribute.py#L742

Added line #L742 was not covered by tests
return next((edge for edge in self.node.graph.edges.values() if edge.src in self._value), None) is not None or \
any(attr.hasOutputConnections for attr in self._value if hasattr(attr, 'hasOutputConnections'))

# override
def getInputConnections(self) -> list["Edge"]:

if not self.node.graph or not self.node.graph.edges:

Check warning on line 749 in meshroom/core/attribute.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/attribute.py#L748-L749

Added lines #L748 - L749 were not covered by tests
return []

Check warning on line 751 in meshroom/core/attribute.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/attribute.py#L751

Added line #L751 was not covered by tests
return [edge for edge in self.node.graph.edges.values() if edge.dst == self or edge.dst in self._value]

# override
def getOutputConnections(self) -> list["Edge"]:

if not self.node.graph or not self.node.graph.edges:

Check warning on line 757 in meshroom/core/attribute.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/attribute.py#L756-L757

Added lines #L756 - L757 were not covered by tests
return []

Check warning on line 759 in meshroom/core/attribute.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/attribute.py#L759

Added line #L759 was not covered by tests
return [edge for edge in self.node.graph.edges.values() if edge.src == self or edge.src in self._value]

# Override value property setter
value = Property(Variant, Attribute._get_value, _set_value, notify=Attribute.valueChanged)
isDefault = Property(bool, _isDefault, notify=Attribute.valueChanged)
baseType = Property(str, getBaseType, constant=True)
isLinkNested = Property(bool, isLinkNested.fget)
hasOutputConnections = Property(bool, hasOutputConnections.fget, notify=Attribute.hasOutputConnectionsChanged)



class GroupAttribute(Attribute):
Expand Down
76 changes: 75 additions & 1 deletion meshroom/ui/qml/Application.qml
Original file line number Diff line number Diff line change
Expand Up @@ -1335,14 +1335,88 @@ Page {
SplitView.minimumWidth: 80

node: _reconstruction ? _reconstruction.selectedNode : null
property bool computing: _reconstruction ? _reconstruction.computing : false
property bool computing: _reconstruction ? _reconstruction.computing : false
property var currentAttributes: []

// Make NodeEditor readOnly when computing
readOnly: node ? node.locked : false

onUpgradeRequest: {
var n = _reconstruction.upgradeNode(node)
_reconstruction.selectedNode = n
}

onInAttributeClicked: function(srcItem, mouse, inAttributes) {
_handleNavButtonClick(srcItem, mouse, inAttributes)
}

onOutAttributeClicked: function(srcItem, mouse, outAttributes) {
_handleNavButtonClick(srcItem, mouse, outAttributes)
}

// NavButtonContextMenu
Menu {
id: navButtonContextMenu

Repeater {
model: nodeEditor.currentAttributes

delegate: MenuItem {

contentItem: Text {
text: `${modelData.node.label}.${modelData.label}`
elide: Text.ElideLeft
color: Colors.sysPalette.text
}

onTriggered: {
nodeEditor._selectNodesFromAttributes([nodeEditor.currentAttributes[index]])
}
}
}

}

function _selectNodesFromAttributes(attributes) {
/*
Retrieve the nodes from given attributes, and select its
*/

if ( !attributes || attributes.length == 0) { return }

graphEditor.uigraph.clearNodeSelection()

const nodes = attributes.map( attr => attr.node)

if (attributes.length == 1) {
_reconstruction.selectedNode = attributes[0].node
}
graphEditor.uigraph.selectNodes(nodes)
}

function _openLinkAttributesContextMenu(srcItem, mouse, attributes) {
nodeEditor.currentAttributes = attributes
const srcGlobal = srcItem.mapToGlobal(0, 0)
const nodeEditorGlobal = nodeEditor.mapToGlobal(0, 0)
navButtonContextMenu.x = srcGlobal.x - nodeEditorGlobal.x
navButtonContextMenu.y = srcGlobal.y - nodeEditorGlobal.y - 14 // TODO: Couldn't found a way to avoid padding in position. 14 = navButtonOut.paddingTop * 2
navButtonContextMenu.open()
}

function _handleNavButtonClick(srcItem, mouse, attributes) {

if (mouse.button === Qt.RightButton) {
nodeEditor._openLinkAttributesContextMenu(srcItem, mouse, attributes)
return
}

nodeEditor._selectNodesFromAttributes(attributes)

if (mouse.button === Qt.MiddleButton) {
graphEditor.fit()
}
}

}
}
}
Expand Down
10 changes: 10 additions & 0 deletions meshroom/ui/qml/GraphEditor/AttributeEditor.qml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ ListView {

signal upgradeRequest()
signal attributeDoubleClicked(var mouse, var attribute)
signal inAttributeClicked(var srcItem, var mouse, var inAttributes)
signal outAttributeClicked(var srcItem, var mouse, var outAttributes)

implicitHeight: contentHeight

Expand All @@ -40,9 +42,17 @@ ListView {
filterText: root.filterText
objectsHideable: root.objectsHideable
attribute: object

onDoubleClicked: function(mouse, attr) {
root.attributeDoubleClicked(mouse, attr)
}
onInAttributeClicked: function(srcItem, mouse, inAttributes) {
root.inAttributeClicked(srcItem, mouse, inAttributes)
}
onOutAttributeClicked: function(srcItem, mouse, outAttributes) {
root.outAttributeClicked(srcItem, mouse, outAttributes)
}

}

onActiveChanged: height = active ? item.implicitHeight : -spacing
Expand Down
51 changes: 51 additions & 0 deletions meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ RowLayout {
readonly property bool editable: !attribute.isOutput && !attribute.isLink && !readOnly

signal doubleClicked(var mouse, var attr)
signal inAttributeClicked(var srcItem, var mouse, var inAttributes)
signal outAttributeClicked(var srcItem, var mouse, var outAttributes)

spacing: 2

Expand Down Expand Up @@ -56,6 +58,29 @@ RowLayout {
width: parent.width
height: parent.height

// In connection
MaterialToolButton {
id: navButtonIn

property bool shouldBeVisible: (object != undefined && object.isLinkNested)

text: shouldBeVisible ? MaterialIcons.login : " "
enabled: shouldBeVisible
font.pointSize: 8
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
topPadding: 7

MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton

onClicked: function(mouse) {
root.inAttributeClicked(navButtonIn, mouse, object.linkedInAttributes)
}
}

}

Label {
id: parameterLabel

Expand Down Expand Up @@ -167,6 +192,30 @@ RowLayout {
}
}
}

MaterialToolButton {
id: navButtonOut

property bool shouldBeVisible: (attribute != undefined && attribute.hasOutputConnections)

text: shouldBeVisible ? MaterialIcons.logout : " "
font.pointSize: 8
enabled: shouldBeVisible
Layout.alignment: Qt.AlignTop | Qt.AlignRight
topPadding: 7

MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton

onClicked: function(mouse) {
root.outAttributeClicked(navButtonOut, mouse, attribute.linkedOutAttributes)
}
}


}

MaterialLabel {
visible: attribute.desc.advanced
text: MaterialIcons.build
Expand Down Expand Up @@ -666,6 +715,8 @@ RowLayout {
obj.label.horizontalAlignment = Text.AlignHCenter
obj.label.verticalAlignment = Text.AlignVCenter
obj.doubleClicked.connect(function(attr) { root.doubleClicked(attr) })
obj.inAttributeClicked.connect(function(srcItem, mouse, inAttributes) { root.inAttributeClicked(srcItem, mouse, inAttributes) })
obj.outAttributeClicked.connect(function(srcItem, mouse, outAttributes) { root.outAttributeClicked(srcItem, mouse, outAttributes) })
}
ToolButton {
enabled: root.editable
Expand Down
9 changes: 9 additions & 0 deletions meshroom/ui/qml/GraphEditor/NodeEditor.qml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Panel {
property string nodeStartDateTime: ""

signal attributeDoubleClicked(var mouse, var attribute)
signal inAttributeClicked(var srcItem, var mouse, var inAttributes)
signal outAttributeClicked(var srcItem, var mouse, var outAttributes)
signal upgradeRequest()

title: "Node" + (node !== null ? " - <b>" + node.label + "</b>" + (node.label !== node.defaultLabel ? " (" + node.defaultLabel + ")" : "") : "")
Expand Down Expand Up @@ -301,6 +303,13 @@ Panel {
onAttributeDoubleClicked: function(mouse, attribute) { root.attributeDoubleClicked(mouse, attribute) }
onUpgradeRequest: root.upgradeRequest()
filterText: searchBar.text

onInAttributeClicked: function(srcItem, mouse, inAttributes) {
root.inAttributeClicked(srcItem, mouse, inAttributes)
}
onOutAttributeClicked: function(srcItem, mouse, outAttributes) {
root.outAttributeClicked(srcItem, mouse, outAttributes)
}
}

Loader {
Expand Down
39 changes: 39 additions & 0 deletions tests/test_attributes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from meshroom.core.graph import Graph


def test_attribute_retrieve_linked_input_and_output_attributes():
"""
Check that an attribute can retrieve the linked input and output attributes
"""

# n0 -- n1 -- n2
# \ \
# ---------- n3

g = Graph('')
n0 = g.addNewNode('Ls', input='')
n1 = g.addNewNode('Ls', input=n0.output)
n2 = g.addNewNode('Ls', input=n1.output)
n3 = g.addNewNode('AppendFiles', input=n1.output, input2=n2.output)

# check that the attribute can retrieve its linked input attributes

assert n0.output.hasOutputConnections
assert not n3.output.hasOutputConnections

assert len(n0.input.getLinkedInAttributes()) == 0
assert len(n1.input.getLinkedInAttributes()) == 1
assert n1.input.getLinkedInAttributes()[0] == n0.output

assert len(n1.output.getLinkedOutAttributes()) == 2

assert n1.output.getLinkedOutAttributes()[0] == n2.input
assert n1.output.getLinkedOutAttributes()[1] == n3.input

n0.graph = None

# Bounding cases
assert not n0.output.hasOutputConnections
assert len(n0.input.getLinkedInAttributes()) == 0
assert len(n0.output.getLinkedOutAttributes()) == 0