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 1425b182..a03cf334 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 8acd9295..078bcf25 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -687,9 +687,24 @@ class Graph(BaseObject): # 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 08a0f1ad..9f2fc087 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -499,6 +499,7 @@ class BaseNode(BaseObject): 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 @@ class BaseNode(BaseObject): 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 @@ class BaseNode(BaseObject): 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 @@ class Node(BaseNode): 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 @@ class Node(BaseNode): 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 @@ class Node(BaseNode): '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 bbfb3063..f9f2b0f2 100755 --- a/meshroom/ui/commands.py +++ b/meshroom/ui/commands.py @@ -264,11 +264,17 @@ class SetAttributeCommand(GraphCommand): 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 9ee2692b..dcb6f6d6 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 6a748cb9..b9e7afda 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 043804fb..fc8cdffb 100644 --- a/meshroom/core/attribute.py +++ b/meshroom/core/attribute.py @@ -168,6 +168,10 @@ class Attribute(BaseObject): # 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 @@ class Attribute(BaseObject): 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 a03cf334..092471d6 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 9f2fc087..d2ed68e8 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -524,8 +524,12 @@ class BaseNode(BaseObject): 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 @@ class BaseNode(BaseObject): 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 @@ class BaseNode(BaseObject): 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 8ec12f07..5ddff3f4 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 092471d6..a3986ce8 100644 --- a/meshroom/core/desc.py +++ b/meshroom/core/desc.py @@ -343,6 +343,19 @@ class StringParam(Param): 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 d2ed68e8..210ab33a 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -532,6 +532,15 @@ class BaseNode(BaseObject): 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 @@ class BaseNode(BaseObject): 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 dcb6f6d6..2d3ad5f9 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 5ddff3f4..6ddd30d1 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 210ab33a..71f43bbb 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -588,6 +588,12 @@ class BaseNode(BaseObject): # 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 @@ class Node(BaseNode): 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 @@ class CompatibilityNode(BaseNode): 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 @@ class CompatibilityNode(BaseNode): # 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 @@ class CompatibilityNode(BaseNode): 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 @@ class CompatibilityNode(BaseNode): 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 @@ class CompatibilityNode(BaseNode): 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 @@ class CompatibilityNode(BaseNode): # 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 @@ class CompatibilityNode(BaseNode): 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 b9e7afda..782ade07 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 078bcf25..240550e9 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -1236,14 +1236,14 @@ class Graph(BaseObject): 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 @@ class Graph(BaseObject): 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 a3986ce8..e5fc88fe 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 71f43bbb..8a1eac5c 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -1170,6 +1170,11 @@ class Node(BaseNode): 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 8a1eac5c..3ff0c6ca 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -541,6 +541,15 @@ class BaseNode(BaseObject): 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 @@ class BaseNode(BaseObject): 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 6ddd30d1..85b9563f 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 3ff0c6ca..80842e08 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -611,10 +611,18 @@ class BaseNode(BaseObject): @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 240550e9..4ad153c1 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -1252,7 +1252,6 @@ class Graph(BaseObject): inputKeys = list(graph[nodeName]["inputs"].keys()) internalInputKeys = [] - internalInputs = graph[nodeName].get("internalInputs", None) if internalInputs: internalInputKeys = list(internalInputs.keys()) @@ -1269,6 +1268,10 @@ class Graph(BaseObject): 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 3d391ecf..e27a41ce 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 80842e08..fd4f8b9b 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 3dfffd45..988739d5 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -710,7 +710,8 @@ class UIGraph(QObject): """ 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 e5fc88fe..2d73a8b6 100644 --- a/meshroom/core/desc.py +++ b/meshroom/core/desc.py @@ -350,7 +350,7 @@ class ColorParam(Param): 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 2d73a8b6..224a0524 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 fd4f8b9b..2b5fbafb 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -541,6 +541,15 @@ class BaseNode(BaseObject): 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 @@ class BaseNode(BaseObject): 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 2b5fbafb..012de1d1 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -1128,9 +1128,6 @@ class BaseNode(BaseObject): 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 @@ class BaseNode(BaseObject): 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 2d3ad5f9..e7152b65 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 85b9563f..cc79ef71 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 224a0524..5e37f101 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 cc79ef71..e0f7fc33 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 fc8cdffb..779974e1 100644 --- a/meshroom/core/attribute.py +++ b/meshroom/core/attribute.py @@ -143,6 +143,10 @@ class Attribute(BaseObject): 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 @@ class Attribute(BaseObject): 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 5e37f101..f2df5988 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 @@ class Attribute(BaseObject): 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 @@ class Attribute(BaseObject): 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 @@ class GroupAttribute(Attribute): 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 @@ class ChoiceParam(Param): 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 012de1d1..e9c06cdf 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -691,7 +691,7 @@ class BaseNode(BaseObject): """ 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 e9c06cdf..7e5999f6 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -629,9 +629,6 @@ class BaseNode(BaseObject): @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):