From 9bc9e2c1295476d7851b8ccc80907ce69ffe7168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Wed, 12 Oct 2022 09:47:24 +0100 Subject: [PATCH 01/19] Add "Notes" tab with "comment"/"invalid comment" attributes Add two internal attributes, "Comment" and "Invalid comment", in a specific "Notes" tab, which will contain any further internal attribute. Internal attributes exist for all nodes. --- meshroom/core/desc.py | 18 ++++++ meshroom/core/graph.py | 17 +++++- meshroom/core/node.py | 33 +++++++++++ meshroom/ui/commands.py | 10 +++- .../qml/GraphEditor/AttributeItemDelegate.qml | 56 +++++++++++++++++++ meshroom/ui/qml/GraphEditor/NodeEditor.qml | 15 +++++ 6 files changed, 146 insertions(+), 3 deletions(-) diff --git a/meshroom/core/desc.py b/meshroom/core/desc.py index 1425b18217..a03cf33435 100644 --- a/meshroom/core/desc.py +++ b/meshroom/core/desc.py @@ -485,6 +485,24 @@ class Node(object): ram = Level.NORMAL packageName = '' packageVersion = '' + internalInputs = [ + StringParam( + name="comment", + label="Comments", + description="Comments on the node.", + value="", + semantic="multiline", + uid=[], + ), + StringParam( + name="invalid", + label="Invalid Comments", + description="Invalid comments on the node.", + value="", + semantic="multiline", + uid=[0], + ) + ] inputs = [] outputs = [] size = StaticNodeSize(1) diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index 8acd9295ce..078bcf2565 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -687,9 +687,24 @@ def attribute(self, fullName): # type: (str) -> Attribute """ Return the attribute identified by the unique name 'fullName'. + If it does not exist, return None. """ node, attribute = fullName.split('.', 1) - return self.node(node).attribute(attribute) + if self.node(node).hasAttribute(attribute): + return self.node(node).attribute(attribute) + return None + + @Slot(str, result=Attribute) + def internalAttribute(self, fullName): + # type: (str) -> Attribute + """ + Return the internal attribute identified by the unique name 'fullName'. + If it does not exist, return None. + """ + node, attribute = fullName.split('.', 1) + if self.node(node).hasInternalAttribute(attribute): + return self.node(node).internalAttribute(attribute) + return None @staticmethod def getNodeIndexFromName(name): diff --git a/meshroom/core/node.py b/meshroom/core/node.py index 08a0f1adaf..9f2fc08789 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -499,6 +499,7 @@ def __init__(self, nodeType, position=None, parent=None, **kwargs): self._size = 0 self._position = position or Position() self._attributes = DictModel(keyAttrName='name', parent=self) + self._internalAttributes = DictModel(keyAttrName='name', parent=self) self.attributesPerUid = defaultdict(set) self._alive = True # for QML side to know if the node can be used or is going to be deleted self._locked = False @@ -568,13 +569,26 @@ def attribute(self, name): att = self._attributes.get(name) return att + @Slot(str, result=Attribute) + def internalAttribute(self, name): + # No group or list attributes for internal attributes + # The internal attribute itself can be returned directly + return self._internalAttributes.get(name) + def getAttributes(self): return self._attributes + def getInternalAttributes(self): + return self._internalAttributes + @Slot(str, result=bool) def hasAttribute(self, name): return name in self._attributes.keys() + @Slot(str, result=bool) + def hasInternalAttribute(self, name): + return name in self._internalAttributes.keys() + def _applyExpr(self): for attr in self._attributes: attr._applyExpr() @@ -1074,6 +1088,7 @@ def canBeCanceled(self): x = Property(float, lambda self: self._position.x, notify=positionChanged) y = Property(float, lambda self: self._position.y, notify=positionChanged) attributes = Property(BaseObject, getAttributes, constant=True) + internalAttributes = Property(BaseObject, getInternalAttributes, constant=True) internalFolderChanged = Signal() internalFolder = Property(str, internalFolder.fget, notify=internalFolderChanged) depthChanged = Signal() @@ -1123,6 +1138,9 @@ def __init__(self, nodeType, position=None, parent=None, **kwargs): for attrDesc in self.nodeDesc.outputs: self._attributes.add(attributeFactory(attrDesc, None, True, self)) + for attrDesc in self.nodeDesc.internalInputs: + self._internalAttributes.add(attributeFactory(attrDesc, None, False, self)) + # List attributes per uid for attr in self._attributes: for uidIndex in attr.attributeDesc.uid: @@ -1150,8 +1168,15 @@ def upgradeAttributeValues(self, values): except ValueError: pass + def setInternalAttributeValues(self, values): + # initialize internal attribute values + for k, v in values.items(): + attr = self.internalAttribute(k) + attr.value = v + def toDict(self): inputs = {k: v.getExportValue() for k, v in self._attributes.objects.items() if v.isInput} + internalInputs = {k: v.getExportValue() for k, v in self._internalAttributes.objects.items()} outputs = ({k: v.getExportValue() for k, v in self._attributes.objects.items() if v.isOutput}) return { @@ -1165,6 +1190,7 @@ def toDict(self): 'uids': self._uids, 'internalFolder': self._internalFolder, 'inputs': {k: v for k, v in inputs.items() if v is not None}, # filter empty values + 'internalInputs': {k: v for k, v in internalInputs.items() if v is not None}, 'outputs': outputs, } @@ -1453,6 +1479,7 @@ def nodeFactory(nodeDict, name=None, template=False): # get node inputs/outputs inputs = nodeDict.get("inputs", {}) + internalInputs = nodeDict.get("internalInputs", {}) outputs = nodeDict.get("outputs", {}) version = nodeDict.get("version", None) internalFolder = nodeDict.get("internalFolder", None) @@ -1485,6 +1512,11 @@ def nodeFactory(nodeDict, name=None, template=False): if not CompatibilityNode.attributeDescFromName(nodeDesc.inputs, attrName, value): compatibilityIssue = CompatibilityIssue.DescriptionConflict break + # verify that all internal inputs match their description + for attrName, value in internalInputs.items(): + if not CompatibilityNode.attributeDescFromName(nodeDesc.internalInputs, attrName, value): + compatibilityIssue = CompatibilityIssue.DescriptionConflict + break # verify that all outputs match their descriptions for attrName, value in outputs.items(): if not CompatibilityNode.attributeDescFromName(nodeDesc.outputs, attrName, value): @@ -1493,6 +1525,7 @@ def nodeFactory(nodeDict, name=None, template=False): if compatibilityIssue is None: node = Node(nodeType, position, **inputs) + node.setInternalAttributeValues(internalInputs) else: logging.warning("Compatibility issue detected for node '{}': {}".format(name, compatibilityIssue.name)) node = CompatibilityNode(nodeType, nodeDict, position, compatibilityIssue) diff --git a/meshroom/ui/commands.py b/meshroom/ui/commands.py index bbfb30631c..f9f2b0f2f9 100755 --- a/meshroom/ui/commands.py +++ b/meshroom/ui/commands.py @@ -264,11 +264,17 @@ def __init__(self, graph, attribute, value, parent=None): def redoImpl(self): if self.value == self.oldValue: return False - self.graph.attribute(self.attrName).value = self.value + if self.graph.attribute(self.attrName) is not None: + self.graph.attribute(self.attrName).value = self.value + else: + self.graph.internalAttribute(self.attrName).value = self.value return True def undoImpl(self): - self.graph.attribute(self.attrName).value = self.oldValue + if self.graph.attribute(self.attrName) is not None: + self.graph.attribute(self.attrName).value = self.oldValue + else: + self.graph.internalAttribute(self.attrName).value = self.oldValue class AddEdgeCommand(GraphCommand): diff --git a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml index 9ee2692b43..dcb6f6d671 100644 --- a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml +++ b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml @@ -1,6 +1,7 @@ import QtQuick 2.9 import QtQuick.Layouts 1.3 import QtQuick.Controls 2.2 +import QtQuick.Dialogs 1.0 import MaterialIcons 2.2 import Utils 1.0 @@ -152,6 +153,10 @@ RowLayout { case "BoolParam": return checkbox_component case "ListAttribute": return listAttribute_component case "GroupAttribute": return groupAttribute_component + case "StringParam": + if (attribute.desc.semantic === 'multiline') + return textArea_component + return textField_component default: return textField_component } } @@ -184,6 +189,57 @@ RowLayout { } } + Component { + id: textArea_component + + Rectangle { + // Fixed background for the flickable object + color: palette.base + width: parent.width + height: 70 + + Flickable { + width: parent.width + height: parent.height + contentWidth: width + contentHeight: height + + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AlwaysOn + } + + TextArea.flickable: TextArea { + wrapMode: Text.WordWrap + padding: 0 + rightPadding: 5 + bottomPadding: 2 + topPadding: 2 + readOnly: !root.editable + onEditingFinished: setTextFieldAttribute(text) + text: attribute.value + selectByMouse: true + onPressed: { + root.forceActiveFocus() + } + Component.onDestruction: { + if (activeFocus) + setTextFieldAttribute(text) + } + DropArea { + enabled: root.editable + anchors.fill: parent + onDropped: { + if (drop.hasUrls) + setTextFieldAttribute(Filepath.urlToString(drop.urls[0])) + else if (drop.hasText && drop.text != '') + setTextFieldAttribute(drop.text) + } + } + } + } + } + } + Component { id: comboBox_component ComboBox { diff --git a/meshroom/ui/qml/GraphEditor/NodeEditor.qml b/meshroom/ui/qml/GraphEditor/NodeEditor.qml index 6a748cb94f..b9e7afdadb 100644 --- a/meshroom/ui/qml/GraphEditor/NodeEditor.qml +++ b/meshroom/ui/qml/GraphEditor/NodeEditor.qml @@ -247,6 +247,15 @@ Panel { Layout.fillWidth: true node: root.node } + + AttributeEditor { + Layout.fillHeight: true + Layout.fillWidth: true + model: root.node.internalAttributes + readOnly: root.readOnly || root.isCompatibilityNode + onAttributeDoubleClicked: root.attributeDoubleClicked(mouse, attribute) + onUpgradeRequest: root.upgradeRequest() + } } } } @@ -285,6 +294,12 @@ Panel { leftPadding: 8 rightPadding: leftPadding } + TabButton { + text: "Notes" + padding: 4 + leftPadding: 8 + rightPadding: leftPadding + } } } } From 330382ab0cf42aa354c683ee8a594d85cdca2ca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Tue, 2 Aug 2022 12:36:11 +0200 Subject: [PATCH 02/19] Add "label" as an internal attribute Setting the "label" internal attribute allows to give a custom label to replace the node's default label. --- meshroom/core/attribute.py | 10 ++++++++++ meshroom/core/desc.py | 7 +++++++ meshroom/core/node.py | 10 +++++++++- meshroom/ui/qml/GraphEditor/Node.qml | 5 +++++ 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/meshroom/core/attribute.py b/meshroom/core/attribute.py index 043804fba4..fc8cdffb0a 100644 --- a/meshroom/core/attribute.py +++ b/meshroom/core/attribute.py @@ -168,6 +168,10 @@ def _set_value(self, value): # TODO: only update the graph if this attribute participates to a UID if self.isInput: self.requestGraphUpdate() + # TODO: only call update of the node if the attribute is internal + # Internal attributes are set as inputs + self.requestNodeUpdate() + self.valueChanged.emit() def upgradeValue(self, exportedValue): @@ -181,6 +185,12 @@ def requestGraphUpdate(self): self.node.graph.markNodesDirty(self.node) self.node.graph.update() + def requestNodeUpdate(self): + # Update specific node information that do not affect the rest of the graph + # (like internal attributes) + if self.node: + self.node.updateInternalAttributes() + @property def isOutput(self): return self._isOutput diff --git a/meshroom/core/desc.py b/meshroom/core/desc.py index a03cf33435..092471d6d6 100644 --- a/meshroom/core/desc.py +++ b/meshroom/core/desc.py @@ -501,6 +501,13 @@ class Node(object): value="", semantic="multiline", uid=[0], + ), + StringParam( + name="label", + label="Label", + description="Custom label to replace the node's default label.", + value="", + uid=[], ) ] inputs = [] diff --git a/meshroom/core/node.py b/meshroom/core/node.py index 9f2fc08789..d2ed68e88c 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -524,8 +524,12 @@ def getName(self): def getLabel(self): """ Returns: - str: the high-level label of this node + str: the user-provided label if it exists, the high-level label of this node otherwise """ + if self.hasInternalAttribute("label"): + label = self.internalAttribute("label").value.strip() + if label: + return label return self.nameToLabel(self._name) @Slot(str, result=str) @@ -856,6 +860,9 @@ def updateInternals(self, cacheDir=None): if self.internalFolder != folder: self.internalFolderChanged.emit() + def updateInternalAttributes(self): + self.internalAttributesChanged.emit() + @property def internalFolder(self): return self._internalFolder.format(**self._cmdVars) @@ -1089,6 +1096,7 @@ def canBeCanceled(self): y = Property(float, lambda self: self._position.y, notify=positionChanged) attributes = Property(BaseObject, getAttributes, constant=True) internalAttributes = Property(BaseObject, getInternalAttributes, constant=True) + internalAttributesChanged = Signal() internalFolderChanged = Signal() internalFolder = Property(str, internalFolder.fget, notify=internalFolderChanged) depthChanged = Signal() diff --git a/meshroom/ui/qml/GraphEditor/Node.qml b/meshroom/ui/qml/GraphEditor/Node.qml index 8ec12f0792..5ddff3f49c 100755 --- a/meshroom/ui/qml/GraphEditor/Node.qml +++ b/meshroom/ui/qml/GraphEditor/Node.qml @@ -71,6 +71,10 @@ Item { root.x = root.node.x root.y = root.node.y } + + onInternalAttributesChanged: { + nodeLabel.text = node ? node.label : "" + } } // Whether an attribute can be displayed as an attribute pin on the node @@ -181,6 +185,7 @@ Item { // Node Name Label { + id: nodeLabel Layout.fillWidth: true text: node ? node.label : "" padding: 4 From 21d01acc9ae7ea24172d45f5786188bacc2f80d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Tue, 2 Aug 2022 14:16:11 +0200 Subject: [PATCH 03/19] Add "color" as an internal attribute Setting this attribute allows the user to change the color of a node, either by directly providing an SVG color name or an hexadecimal color code, or by picking a color with the selector. --- meshroom/core/desc.py | 20 ++++++ meshroom/core/node.py | 10 +++ .../qml/GraphEditor/AttributeItemDelegate.qml | 66 +++++++++++++++++++ meshroom/ui/qml/GraphEditor/Node.qml | 3 +- 4 files changed, 98 insertions(+), 1 deletion(-) diff --git a/meshroom/core/desc.py b/meshroom/core/desc.py index 092471d6d6..a3986ce87a 100644 --- a/meshroom/core/desc.py +++ b/meshroom/core/desc.py @@ -343,6 +343,19 @@ def checkValueTypes(self): return "" +class ColorParam(Param): + """ + """ + def __init__(self, name, label, description, value, uid, group='allParams', advanced=False, semantic='', enabled=True): + super(ColorParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, semantic=semantic, enabled=enabled) + + def validateValue(self, value): + if not isinstance(value, pyCompatibility.basestring) or len(value.split(" ")) > 1: + raise ValueError('ColorParam value should be a string containing either an SVG name or an hexadecimal ' + 'color code (param: {}, value: {}, type: {})'.format(self.name, value, type(value))) + return value + + class Level(Enum): NONE = 0 NORMAL = 1 @@ -508,6 +521,13 @@ class Node(object): description="Custom label to replace the node's default label.", value="", uid=[], + ), + ColorParam( + name="color", + label="Color", + description="Custom color for the node (SVG name or hexadecimal code).", + value="", + uid=[], ) ] inputs = [] diff --git a/meshroom/core/node.py b/meshroom/core/node.py index d2ed68e88c..210ab33ae0 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -532,6 +532,15 @@ def getLabel(self): return label return self.nameToLabel(self._name) + def getColor(self): + """ + Returns: + str: the user-provided custom color of the node if it exists, empty string otherwise + """ + if self.hasInternalAttribute("color"): + return self.internalAttribute("color").value.strip() + return "" + @Slot(str, result=str) def nameToLabel(self, name): """ @@ -1088,6 +1097,7 @@ def canBeCanceled(self): name = Property(str, getName, constant=True) label = Property(str, getLabel, constant=True) + color = Property(str, getColor, constant=True) nodeType = Property(str, nodeType.fget, constant=True) documentation = Property(str, getDocumentation, constant=True) positionChanged = Signal() diff --git a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml index dcb6f6d671..2d3ad5f980 100644 --- a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml +++ b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml @@ -157,6 +157,8 @@ RowLayout { if (attribute.desc.semantic === 'multiline') return textArea_component return textField_component + case "ColorParam": + return color_component default: return textField_component } } @@ -240,6 +242,70 @@ RowLayout { } } + Component { + id: color_component + RowLayout { + CheckBox { + id: color_checkbox + Layout.alignment: Qt.AlignLeft + checked: node.color === "" ? false : true + text: "Custom Color" + onClicked: { + if(checked) { + _reconstruction.setAttribute(attribute, "#0000FF") + } else { + _reconstruction.setAttribute(attribute, "") + } + } + } + TextField { + id: colorText + Layout.alignment: Qt.AlignLeft + implicitWidth: 100 + enabled: color_checkbox.checked + visible: enabled + text: enabled ? attribute.value : "" + selectByMouse: true + onEditingFinished: setTextFieldAttribute(text) + onAccepted: setTextFieldAttribute(text) + Component.onDestruction: { + if(activeFocus) + setTextFieldAttribute(text) + } + } + + Rectangle { + height: colorText.height + width: colorText.width / 2 + Layout.alignment: Qt.AlignLeft + visible: color_checkbox.checked + color: color_checkbox.checked ? attribute.value : "" + + MouseArea { + anchors.fill: parent + onClicked: colorDialog.open() + } + } + + ColorDialog { + id: colorDialog + title: "Please choose a color" + color: attribute.value + onAccepted: { + colorText.text = color + // Artificially trigger change of attribute value + colorText.editingFinished() + close() + } + onRejected: close() + } + Item { + // Dummy item to fill out the space if needed + Layout.fillWidth: true + } + } + } + Component { id: comboBox_component ComboBox { diff --git a/meshroom/ui/qml/GraphEditor/Node.qml b/meshroom/ui/qml/GraphEditor/Node.qml index 5ddff3f49c..6ddd30d170 100755 --- a/meshroom/ui/qml/GraphEditor/Node.qml +++ b/meshroom/ui/qml/GraphEditor/Node.qml @@ -74,6 +74,7 @@ Item { onInternalAttributesChanged: { nodeLabel.text = node ? node.label : "" + background.color = (node.color === "" ? Qt.lighter(activePalette.base, 1.4) : node.color) } } @@ -142,7 +143,7 @@ Item { Rectangle { id: background anchors.fill: nodeContent - color: Qt.lighter(activePalette.base, 1.4) + color: node.color === "" ? Qt.lighter(activePalette.base, 1.4) : node.color layer.enabled: true layer.effect: DropShadow { radius: 3; color: shadowColor } radius: 3 From 930af07966ba0188d0b293ee20be4e2d6cd34c8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Thu, 8 Sep 2022 12:13:48 +0200 Subject: [PATCH 04/19] [core] Correctly load internalAttributes in compatibility mode --- meshroom/core/node.py | 75 ++++++++++++++++++---- meshroom/ui/qml/GraphEditor/NodeEditor.qml | 2 + 2 files changed, 64 insertions(+), 13 deletions(-) diff --git a/meshroom/core/node.py b/meshroom/core/node.py index 210ab33ae0..71f43bbb83 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -588,6 +588,12 @@ def internalAttribute(self, name): # The internal attribute itself can be returned directly return self._internalAttributes.get(name) + def setInternalAttributeValues(self, values): + # initialize internal attribute values + for k, v in values.items(): + attr = self.internalAttribute(k) + attr.value = v + def getAttributes(self): return self._attributes @@ -1186,11 +1192,18 @@ def upgradeAttributeValues(self, values): except ValueError: pass - def setInternalAttributeValues(self, values): - # initialize internal attribute values + def upgradeInternalAttributeValues(self, values): + # initialize internal attibute values for k, v in values.items(): + if not self.hasInternalAttribute(k): + # skip missing atributes + continue attr = self.internalAttribute(k) - attr.value = v + if attr.isInput: + try: + attr.upgradeValue(v) + except ValueError: + pass def toDict(self): inputs = {k: v.getExportValue() for k, v in self._attributes.objects.items() if v.isInput} @@ -1264,6 +1277,7 @@ def __init__(self, nodeType, nodeDict, position=None, issue=CompatibilityIssue.U self.version = Version(self.nodeDict.get("version", None)) self._inputs = self.nodeDict.get("inputs", {}) + self._internalInputs = self.nodeDict.get("internalInputs", {}) self.outputs = self.nodeDict.get("outputs", {}) self._internalFolder = self.nodeDict.get("internalFolder", "") self._uids = self.nodeDict.get("uids", {}) @@ -1275,11 +1289,15 @@ def __init__(self, nodeType, nodeDict, position=None, issue=CompatibilityIssue.U # create input attributes for attrName, value in self._inputs.items(): - self._addAttribute(attrName, value, False) + self._addAttribute(attrName, value, isOutput=False) # create outputs attributes for attrName, value in self.outputs.items(): - self._addAttribute(attrName, value, True) + self._addAttribute(attrName, value, isOutput=True) + + # create internal attributes + for attrName, value in self._internalInputs.items(): + self._addAttribute(attrName, value, isOutput=False, internalAttr=True) # create NodeChunks matching serialized parallelization settings self._chunks.setObjectList([ @@ -1372,7 +1390,7 @@ def attributeDescFromName(refAttributes, name, value, strict=True): return None - def _addAttribute(self, name, val, isOutput): + def _addAttribute(self, name, val, isOutput, internalAttr=False): """ Add a new attribute on this node. @@ -1380,19 +1398,26 @@ def _addAttribute(self, name, val, isOutput): name (str): the name of the attribute val: the attribute's value isOutput: whether the attribute is an output + internalAttr: whether the attribute is internal Returns: bool: whether the attribute exists in the node description """ attrDesc = None if self.nodeDesc: - refAttrs = self.nodeDesc.outputs if isOutput else self.nodeDesc.inputs + if internalAttr: + refAttrs = self.nodeDesc.internalInputs + else: + refAttrs = self.nodeDesc.outputs if isOutput else self.nodeDesc.inputs attrDesc = CompatibilityNode.attributeDescFromName(refAttrs, name, val) matchDesc = attrDesc is not None - if not matchDesc: + if attrDesc is None: attrDesc = CompatibilityNode.attributeDescFromValue(name, val, isOutput) attribute = attributeFactory(attrDesc, val, isOutput, self) - self._attributes.add(attribute) + if internalAttr: + self._internalAttributes.add(attribute) + else: + self._attributes.add(attribute) return matchDesc @property @@ -1417,6 +1442,13 @@ def inputs(self): return self._inputs return {k: v.getExportValue() for k, v in self._attributes.objects.items() if v.isInput} + @property + def internalInputs(self): + """ Get current node's internal attributes """ + if not self.graph: + return self._internalInputs + return {k: v.getExportValue() for k, v in self._internalAttributes.objects.items()} + def toDict(self): """ Return the original serialized node that generated a compatibility issue. @@ -1450,9 +1482,16 @@ def upgrade(self): # store attributes that could be used during node upgrade commonInputs.append(attrName) + commonInternalAttributes = [] + for attrName, value in self._internalInputs.items(): + if self.attributeDescFromName(self.nodeDesc.internalInputs, attrName, value, strict=False): + # store internal attributes that could be used during node upgrade + commonInternalAttributes.append(attrName) + node = Node(self.nodeType, position=self.position) # convert attributes from a list of tuples into a dict attrValues = {key: value for (key, value) in self.inputs.items()} + intAttrValues = {key: value for (key, value) in self.internalInputs.items()} # Use upgrade method of the node description itself if available try: @@ -1465,9 +1504,15 @@ def upgrade(self): logging.error("Error in the upgrade implementation of the node: {}. The return type is incorrect.".format(self.name)) upgradedAttrValues = attrValues - upgradedAttrValuesTmp = {key: value for (key, value) in upgradedAttrValues.items() if key in commonInputs} - node.upgradeAttributeValues(upgradedAttrValues) + + try: + upgradedIntAttrValues = node.nodeDesc.upgradeAttributeValues(intAttrValues, self.version) + except Exception as e: + logging.error("Error in the upgrade implementation of the node: {}.\n{}".format(self.name, str(e))) + upgradedIntAttrValues = intAttrValues + + node.upgradeInternalAttributeValues(upgradedIntAttrValues) return node compatibilityIssue = Property(int, lambda self: self.issue.value, constant=True) @@ -1520,11 +1565,15 @@ def nodeFactory(nodeDict, name=None, template=False): compatibilityIssue = CompatibilityIssue.VersionConflict # in other cases, check attributes compatibility between serialized node and its description else: - # check that the node has the exact same set of inputs/outputs as its description, except if the node - # is described in a template file, in which only non-default parameters are saved + # check that the node has the exact same set of inputs/outputs as its description, except + # if the node is described in a template file, in which only non-default parameters are saved; + # do not perform that check for internal attributes because there is no point in + # raising compatibility issues if their number differs: in that case, it is only useful + # if some internal attributes do not exist or are invalid if not template and (sorted([attr.name for attr in nodeDesc.inputs]) != sorted(inputs.keys()) or \ sorted([attr.name for attr in nodeDesc.outputs]) != sorted(outputs.keys())): compatibilityIssue = CompatibilityIssue.DescriptionConflict + # verify that all inputs match their descriptions for attrName, value in inputs.items(): if not CompatibilityNode.attributeDescFromName(nodeDesc.inputs, attrName, value): diff --git a/meshroom/ui/qml/GraphEditor/NodeEditor.qml b/meshroom/ui/qml/GraphEditor/NodeEditor.qml index b9e7afdadb..782ade07f9 100644 --- a/meshroom/ui/qml/GraphEditor/NodeEditor.qml +++ b/meshroom/ui/qml/GraphEditor/NodeEditor.qml @@ -185,6 +185,7 @@ Panel { currentIndex: tabBar.currentIndex AttributeEditor { + id: inOutAttr Layout.fillHeight: true Layout.fillWidth: true model: root.node.attributes @@ -249,6 +250,7 @@ Panel { } AttributeEditor { + id: nodeInternalAttr Layout.fillHeight: true Layout.fillWidth: true model: root.node.internalAttributes From fe3a0764b089b4982a8de09b26fd8d8ede518c0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Wed, 12 Oct 2022 09:49:29 +0100 Subject: [PATCH 05/19] [core] Do not save default internal attributes in template mode --- meshroom/core/graph.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index 078bcf2565..240550e964 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -1236,14 +1236,14 @@ def save(self, filepath=None, setupProjectFile=True, template=False): def getNonDefaultInputAttributes(self): """ - Instead of getting all the inputs attribute keys, only get the keys of + Instead of getting all the inputs and internal attribute keys, only get the keys of the attributes whose value is not the default one. The output attributes, UIDs, parallelization parameters and internal folder are not relevant for templates, so they are explicitly removed from the returned dictionary. Returns: dict: self.toDict() with the output attributes, UIDs, parallelization parameters, internal folder - and input attributes with default values removed + and input/internal attributes with default values removed """ graph = self.toDict() for nodeName in graph.keys(): @@ -1251,12 +1251,24 @@ def getNonDefaultInputAttributes(self): inputKeys = list(graph[nodeName]["inputs"].keys()) + internalInputKeys = [] + + internalInputs = graph[nodeName].get("internalInputs", None) + if internalInputs: + internalInputKeys = list(internalInputs.keys()) + for attrName in inputKeys: attribute = node.attribute(attrName) # check that attribute is not a link for choice attributes if attribute.isDefault and not attribute.isLink: del graph[nodeName]["inputs"][attrName] + for attrName in internalInputKeys: + attribute = node.internalAttribute(attrName) + # check that internal attribute is not a link for choice attributes + if attribute.isDefault and not attribute.isLink: + del graph[nodeName]["internalInputs"][attrName] + del graph[nodeName]["outputs"] del graph[nodeName]["uids"] del graph[nodeName]["internalFolder"] From cc3c19ba15312a59bf150b0a0ea9375f9245f07e Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Mon, 10 Oct 2022 20:37:30 +0200 Subject: [PATCH 06/19] [core] internal attributes: update descriptions and declare "invalidation" as an advanced attribute --- meshroom/core/desc.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/meshroom/core/desc.py b/meshroom/core/desc.py index a3986ce87a..e5fc88fe68 100644 --- a/meshroom/core/desc.py +++ b/meshroom/core/desc.py @@ -502,23 +502,26 @@ class Node(object): StringParam( name="comment", label="Comments", - description="Comments on the node.", + description="User comments describing this specific node instance.", value="", semantic="multiline", uid=[], ), StringParam( - name="invalid", - label="Invalid Comments", - description="Invalid comments on the node.", + name="invalidation", + label="Invalidation Message", + description="A message that will invalidate the node's output folder.\n" + "This is useful for development, we can invalidate\n" + "the output of the node when we modify the code.", value="", semantic="multiline", uid=[0], + advanced=True, ), StringParam( name="label", - label="Label", - description="Custom label to replace the node's default label.", + label="Node's Label", + description="Customize the default label (to replace the technical name of the node instance).", value="", uid=[], ), From b645db99f76eeaf99b4a3f30027a86c4b5ae2d86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Tue, 11 Oct 2022 13:01:56 +0200 Subject: [PATCH 07/19] [core] Include internal attributes in the UIDs computation --- meshroom/core/node.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/meshroom/core/node.py b/meshroom/core/node.py index 71f43bbb83..8a1eac5c16 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -1170,6 +1170,11 @@ def __init__(self, nodeType, position=None, parent=None, **kwargs): for uidIndex in attr.attributeDesc.uid: self.attributesPerUid[uidIndex].add(attr) + # Add internal attributes with a UID to the list + for attr in self._internalAttributes: + for uidIndex in attr.attributeDesc.uid: + self.attributesPerUid[uidIndex].add(attr) + self.setAttributeValues(kwargs) def setAttributeValues(self, values): From 1015ea448a76efd213319a8f18e09cf1402888d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Thu, 13 Oct 2022 14:57:24 +0200 Subject: [PATCH 08/19] [ui] Add an icon and tooltip on a node's header if it has a comment If the "Comments" internal attribute is filled, add a corresponding icon in the node's header, as well as a tooltip that contains the comment. --- meshroom/core/node.py | 10 ++++++++++ meshroom/ui/qml/GraphEditor/Node.qml | 26 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/meshroom/core/node.py b/meshroom/core/node.py index 8a1eac5c16..3ff0c6ca7f 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -541,6 +541,15 @@ def getColor(self): return self.internalAttribute("color").value.strip() return "" + def getComment(self): + """ + Returns: + str: the comments on the node if they exist, empty string otherwise + """ + if self.hasInternalAttribute("comment"): + return self.internalAttribute("comment").value + return "" + @Slot(str, result=str) def nameToLabel(self, name): """ @@ -1104,6 +1113,7 @@ def canBeCanceled(self): name = Property(str, getName, constant=True) label = Property(str, getLabel, constant=True) color = Property(str, getColor, constant=True) + comment = Property(str, getComment, constant=True) nodeType = Property(str, nodeType.fget, constant=True) documentation = Property(str, getDocumentation, constant=True) positionChanged = Signal() diff --git a/meshroom/ui/qml/GraphEditor/Node.qml b/meshroom/ui/qml/GraphEditor/Node.qml index 6ddd30d170..85b9563fe9 100755 --- a/meshroom/ui/qml/GraphEditor/Node.qml +++ b/meshroom/ui/qml/GraphEditor/Node.qml @@ -75,6 +75,8 @@ Item { onInternalAttributesChanged: { nodeLabel.text = node ? node.label : "" background.color = (node.color === "" ? Qt.lighter(activePalette.base, 1.4) : node.color) + nodeCommentTooltip.text = node ? node.comment : "" + nodeComment.visible = node.comment != "" } } @@ -258,6 +260,30 @@ Item { palette.text: "red" ToolTip.text: "Locked" } + + MaterialLabel { + id: nodeComment + visible: node.comment != "" + text: MaterialIcons.comment + padding: 2 + font.pointSize: 7 + + ToolTip { + id: nodeCommentTooltip + parent: header + visible: nodeCommentMA.containsMouse && nodeComment.visible + text: node.comment + implicitWidth: 400 // Forces word-wrap for long comments but the tooltip will be bigger than needed for short comments + delay: 300 + } + + MouseArea { + // If the node header is hovered, comments may be displayed + id: nodeCommentMA + anchors.fill: parent + hoverEnabled: true + } + } } } } From 3689c12e9c4c91f5abe86fe5830c68783323e4b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Thu, 13 Oct 2022 16:04:54 +0200 Subject: [PATCH 09/19] [core] Check existence of group or list attributes correctly "hasAttribute" was previously never called before attempting to access an attribute. With the addition of internal attributes, we want to check the attribute's/internal attribute's before accessing it to avoid KeyError exceptions. "hasAttribute" (and the newly added "hasInternalAttribute") would not parse the attribute's name before checking for its existence, meaning that errors could be generated for list or group attributes as their checked name could contain other elements (e.g. "featuresFolder[0]" for a ListAttribute named "featuresFolder"). --- meshroom/core/node.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/meshroom/core/node.py b/meshroom/core/node.py index 3ff0c6ca7f..80842e08f0 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -611,10 +611,18 @@ def getInternalAttributes(self): @Slot(str, result=bool) def hasAttribute(self, name): + # Complex name indicating group or list attribute: parse it and get the + # first output element to check for the attribute's existence + if "[" in name or "." in name: + p = self.attributeRE.findall(name) + return p[0][0] in self._attributes.keys() or p[0][1] in self._attributes.keys() return name in self._attributes.keys() @Slot(str, result=bool) def hasInternalAttribute(self, name): + if "[" in name or "." in name: + p = self.attributeRE.findall(name) + return p[0][0] in self._internalAttributes.keys() or p[0][1] in self._internalAttributes.keys() return name in self._internalAttributes.keys() def _applyExpr(self): From 91db7657ac1803f7e751315f907ac1e2e0e08e24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Mon, 24 Oct 2022 10:30:46 +0200 Subject: [PATCH 10/19] [core] Don't write "internalInputs" entry in templates if there are only default values Non-default internal attributes need to be written in the templates, but the "internalInputs" entry in the dictionary should not be written at all if all the internal attributes are set to their default values. --- meshroom/core/graph.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index 240550e964..4ad153c1fd 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -1252,7 +1252,6 @@ def getNonDefaultInputAttributes(self): inputKeys = list(graph[nodeName]["inputs"].keys()) internalInputKeys = [] - internalInputs = graph[nodeName].get("internalInputs", None) if internalInputs: internalInputKeys = list(internalInputs.keys()) @@ -1269,6 +1268,10 @@ def getNonDefaultInputAttributes(self): if attribute.isDefault and not attribute.isLink: del graph[nodeName]["internalInputs"][attrName] + # If all the internal attributes are set to their default values, remove the entry + if len(graph[nodeName]["internalInputs"]) == 0: + del graph[nodeName]["internalInputs"] + del graph[nodeName]["outputs"] del graph[nodeName]["uids"] del graph[nodeName]["internalFolder"] From b47007866765ff2f52ac52b5688b7e20d7908bd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Mon, 24 Oct 2022 11:28:39 +0200 Subject: [PATCH 11/19] [tests] Add checks on internal attributes in the templatesVersion test If some internal attributes are saved in the templates, their description should be checked just like the input attributes to ensure there are no conflicts. --- tests/test_templatesVersion.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_templatesVersion.py b/tests/test_templatesVersion.py index 3d391ecf93..e27a41ce15 100644 --- a/tests/test_templatesVersion.py +++ b/tests/test_templatesVersion.py @@ -38,6 +38,7 @@ def test_templateVersions(): currentNodeVersion = meshroom.core.nodeVersion(nodeDesc) inputs = nodeData.get("inputs", {}) + internalInputs = nodeData.get("internalInputs", {}) version = nodesVersions.get(nodeType, None) compatibilityIssue = None @@ -49,5 +50,9 @@ def test_templateVersions(): if not CompatibilityNode.attributeDescFromName(nodeDesc.inputs, attrName, value): compatibilityIssue = CompatibilityIssue.DescriptionConflict break + for attrName, value in internalInputs.items(): + if not CompatibilityNode.attributeDescFromName(nodeDesc.internalInputs, attrName, value): + compatibilityIssue = CompatibilityIssue.DescriptionConflict + break assert compatibilityIssue is None From 7688b94ce5c1e86f55db788607a7edb3c66303bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Fri, 18 Nov 2022 18:01:48 +0100 Subject: [PATCH 12/19] [core] Raise compatibility issue if nodes miss invalidating internal attributes --- meshroom/core/node.py | 13 +++++++++++++ meshroom/ui/graph.py | 3 ++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/meshroom/core/node.py b/meshroom/core/node.py index 80842e08f0..fd4f8b9b65 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -1597,6 +1597,19 @@ def nodeFactory(nodeDict, name=None, template=False): sorted([attr.name for attr in nodeDesc.outputs]) != sorted(outputs.keys())): compatibilityIssue = CompatibilityIssue.DescriptionConflict + # check whether there are any internal attributes that are invalidating in the node description: if there + # are, then check that these internal attributes are part of nodeDict; if they are not, a compatibility + # issue must be raised to warn the user, as this will automatically change the node's UID + if not template: + invalidatingIntInputs = [] + for attr in nodeDesc.internalInputs: + if attr.uid == [0]: + invalidatingIntInputs.append(attr.name) + for attr in invalidatingIntInputs: + if attr not in internalInputs.keys(): + compatibilityIssue = CompatibilityIssue.DescriptionConflict + break + # verify that all inputs match their descriptions for attrName, value in inputs.items(): if not CompatibilityNode.attributeDescFromName(nodeDesc.inputs, attrName, value): diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index 3dfffd4587..988739d59f 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -710,7 +710,8 @@ def upgradeAllNodes(self): """ Upgrade all upgradable CompatibilityNode instances in the graph. """ with self.groupedGraphModification("Upgrade all Nodes"): nodes = [n for n in self._graph._compatibilityNodes.values() if n.canUpgrade] - for node in nodes: + sortedNodes = sorted(nodes, key=lambda x: x.name) + for node in sortedNodes: self.upgradeNode(node) @Slot() From 835e396d8dcb4ea5a4b50baf79b51da8aacc4d01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Wed, 7 Dec 2022 12:38:10 +0100 Subject: [PATCH 13/19] [core] Remove reference to pyCompatibility pyCompatibility has been removed at the same time as Python 2 support. --- meshroom/core/desc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/core/desc.py b/meshroom/core/desc.py index e5fc88fe68..2d73a8b629 100644 --- a/meshroom/core/desc.py +++ b/meshroom/core/desc.py @@ -350,7 +350,7 @@ def __init__(self, name, label, description, value, uid, group='allParams', adva super(ColorParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, semantic=semantic, enabled=enabled) def validateValue(self, value): - if not isinstance(value, pyCompatibility.basestring) or len(value.split(" ")) > 1: + if not isinstance(value, str) or len(value.split(" ")) > 1: raise ValueError('ColorParam value should be a string containing either an SVG name or an hexadecimal ' 'color code (param: {}, value: {}, type: {})'.format(self.name, value, type(value))) return value From 3bc944561591e9ead5e7b0685f3313db1380a30f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Thu, 15 Dec 2022 19:00:15 +0100 Subject: [PATCH 14/19] [core] Internal attributes: move "invalidation" before "comment" --- meshroom/core/desc.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/meshroom/core/desc.py b/meshroom/core/desc.py index 2d73a8b629..224a052434 100644 --- a/meshroom/core/desc.py +++ b/meshroom/core/desc.py @@ -499,25 +499,24 @@ class Node(object): packageName = '' packageVersion = '' internalInputs = [ - StringParam( - name="comment", - label="Comments", - description="User comments describing this specific node instance.", - value="", - semantic="multiline", - uid=[], - ), StringParam( name="invalidation", label="Invalidation Message", description="A message that will invalidate the node's output folder.\n" - "This is useful for development, we can invalidate\n" - "the output of the node when we modify the code.", + "This is useful for development, we can invalidate the output of the node when we modify the code.\n" value="", semantic="multiline", uid=[0], advanced=True, ), + StringParam( + name="comment", + label="Comments", + description="User comments describing this specific node instance.\n" + value="", + semantic="multiline", + uid=[], + ), StringParam( name="label", label="Node's Label", From 492e4d5dd060cc28910408e66e099eea8b2872ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Thu, 15 Dec 2022 19:01:41 +0100 Subject: [PATCH 15/19] [core] Add property for the invalidation message from internal attributes --- meshroom/core/node.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/meshroom/core/node.py b/meshroom/core/node.py index fd4f8b9b65..2b5fbafb1b 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -541,6 +541,15 @@ def getColor(self): return self.internalAttribute("color").value.strip() return "" + def getInvalidationMessage(self): + """ + Returns: + str: the invalidation message on the node if it exists, empty string otherwise + """ + if self.hasInternalAttribute("invalidation"): + return self.internalAttribute("invalidation").value + return "" + def getComment(self): """ Returns: @@ -1131,6 +1140,7 @@ def canBeCanceled(self): attributes = Property(BaseObject, getAttributes, constant=True) internalAttributes = Property(BaseObject, getInternalAttributes, constant=True) internalAttributesChanged = Signal() + invalidation = Property(str, getInvalidationMessage, notify=internalAttributesChanged) internalFolderChanged = Signal() internalFolder = Property(str, internalFolder.fget, notify=internalFolderChanged) depthChanged = Signal() From 4b7a548687004fae902f6c53a247aac111129dd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Thu, 15 Dec 2022 19:11:21 +0100 Subject: [PATCH 16/19] Notify changes in internal attributes' properties The "label", "color" and "comment" properties are not constant anymore, their changes in value are notified with the internalAttributesChanged() signal, like the "invalidation" property. This implies that the connection on "internalAttributesChanged" on the QML side is not needed anymore. --- meshroom/core/node.py | 6 +++--- meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml | 2 +- meshroom/ui/qml/GraphEditor/Node.qml | 7 ------- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/meshroom/core/node.py b/meshroom/core/node.py index 2b5fbafb1b..012de1d180 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -1128,9 +1128,6 @@ def canBeCanceled(self): name = Property(str, getName, constant=True) - label = Property(str, getLabel, constant=True) - color = Property(str, getColor, constant=True) - comment = Property(str, getComment, constant=True) nodeType = Property(str, nodeType.fget, constant=True) documentation = Property(str, getDocumentation, constant=True) positionChanged = Signal() @@ -1140,7 +1137,10 @@ def canBeCanceled(self): attributes = Property(BaseObject, getAttributes, constant=True) internalAttributes = Property(BaseObject, getInternalAttributes, constant=True) internalAttributesChanged = Signal() + label = Property(str, getLabel, notify=internalAttributesChanged) + color = Property(str, getColor, notify=internalAttributesChanged) invalidation = Property(str, getInvalidationMessage, notify=internalAttributesChanged) + comment = Property(str, getComment, notify=internalAttributesChanged) internalFolderChanged = Signal() internalFolder = Property(str, internalFolder.fget, notify=internalFolderChanged) depthChanged = Signal() diff --git a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml index 2d3ad5f980..e7152b6505 100644 --- a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml +++ b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml @@ -248,7 +248,7 @@ RowLayout { CheckBox { id: color_checkbox Layout.alignment: Qt.AlignLeft - checked: node.color === "" ? false : true + checked: node && node.color === "" ? false : true text: "Custom Color" onClicked: { if(checked) { diff --git a/meshroom/ui/qml/GraphEditor/Node.qml b/meshroom/ui/qml/GraphEditor/Node.qml index 85b9563fe9..cc79ef71e7 100755 --- a/meshroom/ui/qml/GraphEditor/Node.qml +++ b/meshroom/ui/qml/GraphEditor/Node.qml @@ -71,13 +71,6 @@ Item { root.x = root.node.x root.y = root.node.y } - - onInternalAttributesChanged: { - nodeLabel.text = node ? node.label : "" - background.color = (node.color === "" ? Qt.lighter(activePalette.base, 1.4) : node.color) - nodeCommentTooltip.text = node ? node.comment : "" - nodeComment.visible = node.comment != "" - } } // Whether an attribute can be displayed as an attribute pin on the node From 6381371e7dea272362e3a00ada04319369ea701b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Thu, 15 Dec 2022 19:17:54 +0100 Subject: [PATCH 17/19] Display the invalidation and comment messages in the internal attributes' tooltip The tooltip now displays both the invalidation message, followed by the comments. The invalidation message is displayed first in bold font, followed by an empty line and the comments in regular font. The tooltip now appears if at least one of the invalidation or comment messages exists. The invalidation and comment messages are formatted with HTML tags prior to their display. The descriptions of both attributes is also updated to indicate which one is displayed in bold or regular font. --- meshroom/core/desc.py | 2 ++ meshroom/ui/qml/GraphEditor/Node.qml | 32 ++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/meshroom/core/desc.py b/meshroom/core/desc.py index 224a052434..5e37f101a6 100644 --- a/meshroom/core/desc.py +++ b/meshroom/core/desc.py @@ -504,6 +504,7 @@ class Node(object): label="Invalidation Message", description="A message that will invalidate the node's output folder.\n" "This is useful for development, we can invalidate the output of the node when we modify the code.\n" + "It is displayed in bold font in the invalidation/comment messages tooltip.", value="", semantic="multiline", uid=[0], @@ -513,6 +514,7 @@ class Node(object): name="comment", label="Comments", description="User comments describing this specific node instance.\n" + "It is displayed in regular font in the invalidation/comment messages tooltip.", value="", semantic="multiline", uid=[], diff --git a/meshroom/ui/qml/GraphEditor/Node.qml b/meshroom/ui/qml/GraphEditor/Node.qml index cc79ef71e7..e0f7fc33c1 100755 --- a/meshroom/ui/qml/GraphEditor/Node.qml +++ b/meshroom/ui/qml/GraphEditor/Node.qml @@ -73,6 +73,29 @@ Item { } } + function formatInternalAttributesTooltip(invalidation, comment) { + /* + * Creates a string that contains the invalidation message (if it is not empty) in bold, + * followed by the comment message (if it exists) in regular font, separated by an empty + * line. + * Invalidation and comment messages have their tabs or line returns in plain text format replaced + * by their HTML equivalents. + */ + let str = "" + if (invalidation !== "") { + let replacedInvalidation = node.invalidation.replace(/\n/g, "
").replace(/\t/g, "    ") + str += "" + replacedInvalidation + "" + } + if (invalidation !== "" && comment !== "") { + str += "

" + } + if (comment !== "") { + let replacedComment = node.comment.replace(/\n/g, "
").replace(/\t/g, "    ") + str += replacedComment + } + return str + } + // Whether an attribute can be displayed as an attribute pin on the node function isFileAttributeBaseType(attribute) { // ATM, only File attributes are meant to be connected @@ -256,7 +279,7 @@ Item { MaterialLabel { id: nodeComment - visible: node.comment != "" + visible: node.comment !== "" || node.invalidation !== "" text: MaterialIcons.comment padding: 2 font.pointSize: 7 @@ -265,9 +288,14 @@ Item { id: nodeCommentTooltip parent: header visible: nodeCommentMA.containsMouse && nodeComment.visible - text: node.comment + text: formatInternalAttributesTooltip(node.invalidation, node.comment) implicitWidth: 400 // Forces word-wrap for long comments but the tooltip will be bigger than needed for short comments delay: 300 + + // Relative position for the tooltip to ensure we won't get stuck in a case where it starts appearing over the mouse's + // position because it's a bit long and cutting off the hovering of the mouse area (which leads to the tooltip beginning + // to appear and immediately disappearing, over and over again) + x: implicitWidth / 2.5 } MouseArea { From 311ab9cb4030bd83ad61d51ebab191585850fe23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Candice=20Bent=C3=A9jac?= Date: Fri, 17 Feb 2023 15:34:39 +0100 Subject: [PATCH 18/19] [core] Add a property to ignore an attribute during the UID computation By default, an attribute that belongs to the UID group 0 is taken into the node's UID computation independently from its value, as long as it is enabled. When such an attribute is added to a node's list of attributes, it automatically invalidates all computations made for this node prior to its addition. This commits adds a new attribute property, "uidIgnoreValue". This property determines whether the attribute must be taken into consideration during the node's UID computation: if the value of the attribute is the same as "uidIgnoreValue", then it should be ignored; otherwise, it should be taken into account. By default, "uidIgnoreValue" is set to "None", meaning that any attribute that may be considered during the UID computation will be taken into account. In the context of the internal attributes, "uidIgnoreValue" is set to empty string, so the "invalidation" attribute will not automatically invalidate 100% of the nodes from existing graphs until its value is set to a non-empty string. --- meshroom/core/attribute.py | 5 +++++ meshroom/core/desc.py | 15 ++++++++++----- meshroom/core/node.py | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/meshroom/core/attribute.py b/meshroom/core/attribute.py index fc8cdffb0a..779974e19a 100644 --- a/meshroom/core/attribute.py +++ b/meshroom/core/attribute.py @@ -143,6 +143,10 @@ def setEnabled(self, v): self._enabled = v self.enabledChanged.emit() + def getUidIgnoreValue(self): + """ Value for which the attribute should be ignored during the UID computation. """ + return self.attributeDesc.uidIgnoreValue + def _get_value(self): if self.isLink: return self.getLinkParam().value @@ -333,6 +337,7 @@ def updateInternals(self): node = Property(BaseObject, node.fget, constant=True) enabledChanged = Signal() enabled = Property(bool, getEnabled, setEnabled, notify=enabledChanged) + uidIgnoreValue = Property(Variant, getUidIgnoreValue, constant=True) def raiseIfLink(func): diff --git a/meshroom/core/desc.py b/meshroom/core/desc.py index 5e37f101a6..f2df5988a5 100644 --- a/meshroom/core/desc.py +++ b/meshroom/core/desc.py @@ -14,7 +14,7 @@ class Attribute(BaseObject): """ """ - def __init__(self, name, label, description, value, advanced, semantic, uid, group, enabled): + def __init__(self, name, label, description, value, advanced, semantic, uid, group, enabled, uidIgnoreValue=None): super(Attribute, self).__init__() self._name = name self._label = label @@ -25,6 +25,7 @@ def __init__(self, name, label, description, value, advanced, semantic, uid, gro self._advanced = advanced self._enabled = enabled self._semantic = semantic + self._uidIgnoreValue = uidIgnoreValue name = Property(str, lambda self: self._name, constant=True) label = Property(str, lambda self: self._label, constant=True) @@ -35,6 +36,7 @@ def __init__(self, name, label, description, value, advanced, semantic, uid, gro advanced = Property(bool, lambda self: self._advanced, constant=True) enabled = Property(Variant, lambda self: self._enabled, constant=True) semantic = Property(str, lambda self: self._semantic, constant=True) + uidIgnoreValue = Property(Variant, lambda self: self._uidIgnoreValue, constant=True) type = Property(str, lambda self: self.__class__.__name__, constant=True) def validateValue(self, value): @@ -201,8 +203,9 @@ def retrieveChildrenUids(self): class Param(Attribute): """ """ - def __init__(self, name, label, description, value, uid, group, advanced, semantic, enabled): - super(Param, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, semantic=semantic, enabled=enabled) + def __init__(self, name, label, description, value, uid, group, advanced, semantic, enabled, uidIgnoreValue=None): + super(Param, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, semantic=semantic, enabled=enabled, + uidIgnoreValue=uidIgnoreValue) class File(Attribute): @@ -329,8 +332,9 @@ def checkValueTypes(self): class StringParam(Param): """ """ - def __init__(self, name, label, description, value, uid, group='allParams', advanced=False, semantic='', enabled=True): - super(StringParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, semantic=semantic, enabled=enabled) + def __init__(self, name, label, description, value, uid, group='allParams', advanced=False, semantic='', enabled=True, uidIgnoreValue=None): + super(StringParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, semantic=semantic, enabled=enabled, + uidIgnoreValue=uidIgnoreValue) def validateValue(self, value): if not isinstance(value, str): @@ -509,6 +513,7 @@ class Node(object): semantic="multiline", uid=[0], advanced=True, + uidIgnoreValue="", # If the invalidation string is empty, it does not participate to the node's UID ), StringParam( name="comment", diff --git a/meshroom/core/node.py b/meshroom/core/node.py index 012de1d180..e9c06cdf4e 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -691,7 +691,7 @@ def _computeUids(self): """ Compute node uids by combining associated attributes' uids. """ for uidIndex, associatedAttributes in self.attributesPerUid.items(): # uid is computed by hashing the sorted list of tuple (name, value) of all attributes impacting this uid - uidAttributes = [(a.getName(), a.uid(uidIndex)) for a in associatedAttributes if a.enabled] + uidAttributes = [(a.getName(), a.uid(uidIndex)) for a in associatedAttributes if a.enabled and a.value != a.uidIgnoreValue] uidAttributes.sort() self._uids[uidIndex] = hashValue(uidAttributes) From 25c12bbc516a26f81c216cb3b4afaf5df10fa224 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Wed, 22 Feb 2023 12:04:00 +0100 Subject: [PATCH 19/19] [core] Node: hasInternalAttribute does not support groups and lists hasInternalAttribute() should not support more cases than internalAttribute() --- meshroom/core/node.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/meshroom/core/node.py b/meshroom/core/node.py index e9c06cdf4e..7e5999f606 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -629,9 +629,6 @@ def hasAttribute(self, name): @Slot(str, result=bool) def hasInternalAttribute(self, name): - if "[" in name or "." in name: - p = self.attributeRE.findall(name) - return p[0][0] in self._internalAttributes.keys() or p[0][1] in self._internalAttributes.keys() return name in self._internalAttributes.keys() def _applyExpr(self):