Merge pull request #1744 from alicevision/dev/internalAttributes

Add internal attributes in "Notes" tab
This commit is contained in:
Fabien Castan 2023-03-05 13:46:47 +01:00 committed by GitHub
commit ce2085faad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 471 additions and 25 deletions

View file

@ -143,6 +143,10 @@ class Attribute(BaseObject):
self._enabled = v self._enabled = v
self.enabledChanged.emit() 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): def _get_value(self):
if self.isLink: if self.isLink:
return self.getLinkParam().value return self.getLinkParam().value
@ -168,6 +172,10 @@ class Attribute(BaseObject):
# TODO: only update the graph if this attribute participates to a UID # TODO: only update the graph if this attribute participates to a UID
if self.isInput: if self.isInput:
self.requestGraphUpdate() 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() self.valueChanged.emit()
def upgradeValue(self, exportedValue): def upgradeValue(self, exportedValue):
@ -181,6 +189,12 @@ class Attribute(BaseObject):
self.node.graph.markNodesDirty(self.node) self.node.graph.markNodesDirty(self.node)
self.node.graph.update() 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 @property
def isOutput(self): def isOutput(self):
return self._isOutput return self._isOutput
@ -323,6 +337,7 @@ class Attribute(BaseObject):
node = Property(BaseObject, node.fget, constant=True) node = Property(BaseObject, node.fget, constant=True)
enabledChanged = Signal() enabledChanged = Signal()
enabled = Property(bool, getEnabled, setEnabled, notify=enabledChanged) enabled = Property(bool, getEnabled, setEnabled, notify=enabledChanged)
uidIgnoreValue = Property(Variant, getUidIgnoreValue, constant=True)
def raiseIfLink(func): def raiseIfLink(func):

View file

@ -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__() super(Attribute, self).__init__()
self._name = name self._name = name
self._label = label self._label = label
@ -25,6 +25,7 @@ class Attribute(BaseObject):
self._advanced = advanced self._advanced = advanced
self._enabled = enabled self._enabled = enabled
self._semantic = semantic self._semantic = semantic
self._uidIgnoreValue = uidIgnoreValue
name = Property(str, lambda self: self._name, constant=True) name = Property(str, lambda self: self._name, constant=True)
label = Property(str, lambda self: self._label, 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) advanced = Property(bool, lambda self: self._advanced, constant=True)
enabled = Property(Variant, lambda self: self._enabled, constant=True) enabled = Property(Variant, lambda self: self._enabled, constant=True)
semantic = Property(str, lambda self: self._semantic, 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) type = Property(str, lambda self: self.__class__.__name__, constant=True)
def validateValue(self, value): def validateValue(self, value):
@ -201,8 +203,9 @@ class GroupAttribute(Attribute):
class Param(Attribute): class Param(Attribute):
""" """
""" """
def __init__(self, name, label, description, value, uid, group, advanced, semantic, 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) 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): class File(Attribute):
@ -329,8 +332,9 @@ class ChoiceParam(Param):
class StringParam(Param): class StringParam(Param):
""" """
""" """
def __init__(self, name, label, description, value, uid, group='allParams', advanced=False, semantic='', enabled=True): 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) 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): def validateValue(self, value):
if not isinstance(value, str): if not isinstance(value, str):
@ -343,6 +347,19 @@ class StringParam(Param):
return "" 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, 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
class Level(Enum): class Level(Enum):
NONE = 0 NONE = 0
NORMAL = 1 NORMAL = 1
@ -485,6 +502,43 @@ class Node(object):
ram = Level.NORMAL ram = Level.NORMAL
packageName = '' packageName = ''
packageVersion = '' packageVersion = ''
internalInputs = [
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 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],
advanced=True,
uidIgnoreValue="", # If the invalidation string is empty, it does not participate to the node's UID
),
StringParam(
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=[],
),
StringParam(
name="label",
label="Node's Label",
description="Customize the default label (to replace the technical name of the node instance).",
value="",
uid=[],
),
ColorParam(
name="color",
label="Color",
description="Custom color for the node (SVG name or hexadecimal code).",
value="",
uid=[],
)
]
inputs = [] inputs = []
outputs = [] outputs = []
size = StaticNodeSize(1) size = StaticNodeSize(1)

View file

@ -695,9 +695,24 @@ class Graph(BaseObject):
# type: (str) -> Attribute # type: (str) -> Attribute
""" """
Return the attribute identified by the unique name 'fullName'. Return the attribute identified by the unique name 'fullName'.
If it does not exist, return None.
""" """
node, attribute = fullName.split('.', 1) 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 @staticmethod
def getNodeIndexFromName(name): def getNodeIndexFromName(name):
@ -1229,14 +1244,14 @@ class Graph(BaseObject):
def getNonDefaultInputAttributes(self): 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 attributes whose value is not the default one.
The output attributes, UIDs, parallelization parameters and internal folder are The output attributes, UIDs, parallelization parameters and internal folder are
not relevant for templates, so they are explicitly removed from the returned dictionary. not relevant for templates, so they are explicitly removed from the returned dictionary.
Returns: Returns:
dict: self.toDict() with the output attributes, UIDs, parallelization parameters, internal folder 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() graph = self.toDict()
for nodeName in graph.keys(): for nodeName in graph.keys():
@ -1244,12 +1259,27 @@ class Graph(BaseObject):
inputKeys = list(graph[nodeName]["inputs"].keys()) inputKeys = list(graph[nodeName]["inputs"].keys())
internalInputKeys = []
internalInputs = graph[nodeName].get("internalInputs", None)
if internalInputs:
internalInputKeys = list(internalInputs.keys())
for attrName in inputKeys: for attrName in inputKeys:
attribute = node.attribute(attrName) attribute = node.attribute(attrName)
# check that attribute is not a link for choice attributes # check that attribute is not a link for choice attributes
if attribute.isDefault and not attribute.isLink: if attribute.isDefault and not attribute.isLink:
del graph[nodeName]["inputs"][attrName] 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]
# 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]["outputs"]
del graph[nodeName]["uids"] del graph[nodeName]["uids"]
del graph[nodeName]["internalFolder"] del graph[nodeName]["internalFolder"]

View file

@ -499,6 +499,7 @@ class BaseNode(BaseObject):
self._size = 0 self._size = 0
self._position = position or Position() self._position = position or Position()
self._attributes = DictModel(keyAttrName='name', parent=self) self._attributes = DictModel(keyAttrName='name', parent=self)
self._internalAttributes = DictModel(keyAttrName='name', parent=self)
self.attributesPerUid = defaultdict(set) 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._alive = True # for QML side to know if the node can be used or is going to be deleted
self._locked = False self._locked = False
@ -523,10 +524,41 @@ class BaseNode(BaseObject):
def getLabel(self): def getLabel(self):
""" """
Returns: 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) 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 ""
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:
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) @Slot(str, result=str)
def nameToLabel(self, name): def nameToLabel(self, name):
""" """
@ -568,13 +600,37 @@ class BaseNode(BaseObject):
att = self._attributes.get(name) att = self._attributes.get(name)
return att 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 setInternalAttributeValues(self, values):
# initialize internal attribute values
for k, v in values.items():
attr = self.internalAttribute(k)
attr.value = v
def getAttributes(self): def getAttributes(self):
return self._attributes return self._attributes
def getInternalAttributes(self):
return self._internalAttributes
@Slot(str, result=bool) @Slot(str, result=bool)
def hasAttribute(self, name): 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() return name in self._attributes.keys()
@Slot(str, result=bool)
def hasInternalAttribute(self, name):
return name in self._internalAttributes.keys()
def _applyExpr(self): def _applyExpr(self):
for attr in self._attributes: for attr in self._attributes:
attr._applyExpr() attr._applyExpr()
@ -632,7 +688,7 @@ class BaseNode(BaseObject):
""" Compute node uids by combining associated attributes' uids. """ """ Compute node uids by combining associated attributes' uids. """
for uidIndex, associatedAttributes in self.attributesPerUid.items(): 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 # 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() uidAttributes.sort()
self._uids[uidIndex] = hashValue(uidAttributes) self._uids[uidIndex] = hashValue(uidAttributes)
@ -842,6 +898,9 @@ class BaseNode(BaseObject):
if self.internalFolder != folder: if self.internalFolder != folder:
self.internalFolderChanged.emit() self.internalFolderChanged.emit()
def updateInternalAttributes(self):
self.internalAttributesChanged.emit()
@property @property
def internalFolder(self): def internalFolder(self):
return self._internalFolder.format(**self._cmdVars) return self._internalFolder.format(**self._cmdVars)
@ -1066,7 +1125,6 @@ class BaseNode(BaseObject):
name = Property(str, getName, constant=True) name = Property(str, getName, constant=True)
label = Property(str, getLabel, constant=True)
nodeType = Property(str, nodeType.fget, constant=True) nodeType = Property(str, nodeType.fget, constant=True)
documentation = Property(str, getDocumentation, constant=True) documentation = Property(str, getDocumentation, constant=True)
positionChanged = Signal() positionChanged = Signal()
@ -1074,6 +1132,12 @@ class BaseNode(BaseObject):
x = Property(float, lambda self: self._position.x, notify=positionChanged) x = Property(float, lambda self: self._position.x, notify=positionChanged)
y = Property(float, lambda self: self._position.y, notify=positionChanged) y = Property(float, lambda self: self._position.y, notify=positionChanged)
attributes = Property(BaseObject, getAttributes, constant=True) 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() internalFolderChanged = Signal()
internalFolder = Property(str, internalFolder.fget, notify=internalFolderChanged) internalFolder = Property(str, internalFolder.fget, notify=internalFolderChanged)
depthChanged = Signal() depthChanged = Signal()
@ -1123,11 +1187,19 @@ class Node(BaseNode):
for attrDesc in self.nodeDesc.outputs: for attrDesc in self.nodeDesc.outputs:
self._attributes.add(attributeFactory(attrDesc, None, True, self)) 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 # List attributes per uid
for attr in self._attributes: for attr in self._attributes:
for uidIndex in attr.attributeDesc.uid: for uidIndex in attr.attributeDesc.uid:
self.attributesPerUid[uidIndex].add(attr) 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) self.setAttributeValues(kwargs)
def setAttributeValues(self, values): def setAttributeValues(self, values):
@ -1150,8 +1222,22 @@ class Node(BaseNode):
except ValueError: except ValueError:
pass pass
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)
if attr.isInput:
try:
attr.upgradeValue(v)
except ValueError:
pass
def toDict(self): def toDict(self):
inputs = {k: v.getExportValue() for k, v in self._attributes.objects.items() if v.isInput} 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}) outputs = ({k: v.getExportValue() for k, v in self._attributes.objects.items() if v.isOutput})
return { return {
@ -1165,6 +1251,7 @@ class Node(BaseNode):
'uids': self._uids, 'uids': self._uids,
'internalFolder': self._internalFolder, 'internalFolder': self._internalFolder,
'inputs': {k: v for k, v in inputs.items() if v is not None}, # filter empty values '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, 'outputs': outputs,
} }
@ -1220,6 +1307,7 @@ class CompatibilityNode(BaseNode):
self.version = Version(self.nodeDict.get("version", None)) self.version = Version(self.nodeDict.get("version", None))
self._inputs = self.nodeDict.get("inputs", {}) self._inputs = self.nodeDict.get("inputs", {})
self._internalInputs = self.nodeDict.get("internalInputs", {})
self.outputs = self.nodeDict.get("outputs", {}) self.outputs = self.nodeDict.get("outputs", {})
self._internalFolder = self.nodeDict.get("internalFolder", "") self._internalFolder = self.nodeDict.get("internalFolder", "")
self._uids = self.nodeDict.get("uids", {}) self._uids = self.nodeDict.get("uids", {})
@ -1231,11 +1319,15 @@ class CompatibilityNode(BaseNode):
# create input attributes # create input attributes
for attrName, value in self._inputs.items(): for attrName, value in self._inputs.items():
self._addAttribute(attrName, value, False) self._addAttribute(attrName, value, isOutput=False)
# create outputs attributes # create outputs attributes
for attrName, value in self.outputs.items(): 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 # create NodeChunks matching serialized parallelization settings
self._chunks.setObjectList([ self._chunks.setObjectList([
@ -1328,7 +1420,7 @@ class CompatibilityNode(BaseNode):
return None return None
def _addAttribute(self, name, val, isOutput): def _addAttribute(self, name, val, isOutput, internalAttr=False):
""" """
Add a new attribute on this node. Add a new attribute on this node.
@ -1336,19 +1428,26 @@ class CompatibilityNode(BaseNode):
name (str): the name of the attribute name (str): the name of the attribute
val: the attribute's value val: the attribute's value
isOutput: whether the attribute is an output isOutput: whether the attribute is an output
internalAttr: whether the attribute is internal
Returns: Returns:
bool: whether the attribute exists in the node description bool: whether the attribute exists in the node description
""" """
attrDesc = None attrDesc = None
if self.nodeDesc: 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) attrDesc = CompatibilityNode.attributeDescFromName(refAttrs, name, val)
matchDesc = attrDesc is not None matchDesc = attrDesc is not None
if not matchDesc: if attrDesc is None:
attrDesc = CompatibilityNode.attributeDescFromValue(name, val, isOutput) attrDesc = CompatibilityNode.attributeDescFromValue(name, val, isOutput)
attribute = attributeFactory(attrDesc, val, isOutput, self) attribute = attributeFactory(attrDesc, val, isOutput, self)
self._attributes.add(attribute) if internalAttr:
self._internalAttributes.add(attribute)
else:
self._attributes.add(attribute)
return matchDesc return matchDesc
@property @property
@ -1373,6 +1472,13 @@ class CompatibilityNode(BaseNode):
return self._inputs return self._inputs
return {k: v.getExportValue() for k, v in self._attributes.objects.items() if v.isInput} 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): def toDict(self):
""" """
Return the original serialized node that generated a compatibility issue. Return the original serialized node that generated a compatibility issue.
@ -1406,9 +1512,16 @@ class CompatibilityNode(BaseNode):
# store attributes that could be used during node upgrade # store attributes that could be used during node upgrade
commonInputs.append(attrName) 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) node = Node(self.nodeType, position=self.position)
# convert attributes from a list of tuples into a dict # convert attributes from a list of tuples into a dict
attrValues = {key: value for (key, value) in self.inputs.items()} 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 # Use upgrade method of the node description itself if available
try: try:
@ -1421,9 +1534,15 @@ class CompatibilityNode(BaseNode):
logging.error("Error in the upgrade implementation of the node: {}. The return type is incorrect.".format(self.name)) logging.error("Error in the upgrade implementation of the node: {}. The return type is incorrect.".format(self.name))
upgradedAttrValues = attrValues upgradedAttrValues = attrValues
upgradedAttrValuesTmp = {key: value for (key, value) in upgradedAttrValues.items() if key in commonInputs}
node.upgradeAttributeValues(upgradedAttrValues) 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 return node
compatibilityIssue = Property(int, lambda self: self.issue.value, constant=True) compatibilityIssue = Property(int, lambda self: self.issue.value, constant=True)
@ -1453,6 +1572,7 @@ def nodeFactory(nodeDict, name=None, template=False):
# get node inputs/outputs # get node inputs/outputs
inputs = nodeDict.get("inputs", {}) inputs = nodeDict.get("inputs", {})
internalInputs = nodeDict.get("internalInputs", {})
outputs = nodeDict.get("outputs", {}) outputs = nodeDict.get("outputs", {})
version = nodeDict.get("version", None) version = nodeDict.get("version", None)
internalFolder = nodeDict.get("internalFolder", None) internalFolder = nodeDict.get("internalFolder", None)
@ -1475,16 +1595,38 @@ def nodeFactory(nodeDict, name=None, template=False):
compatibilityIssue = CompatibilityIssue.VersionConflict compatibilityIssue = CompatibilityIssue.VersionConflict
# in other cases, check attributes compatibility between serialized node and its description # in other cases, check attributes compatibility between serialized node and its description
else: else:
# check that the node has the exact same set of inputs/outputs as its description, except if the node # check that the node has the exact same set of inputs/outputs as its description, except
# is described in a template file, in which only non-default parameters are saved # 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 \ 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())): sorted([attr.name for attr in nodeDesc.outputs]) != sorted(outputs.keys())):
compatibilityIssue = CompatibilityIssue.DescriptionConflict 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 # verify that all inputs match their descriptions
for attrName, value in inputs.items(): for attrName, value in inputs.items():
if not CompatibilityNode.attributeDescFromName(nodeDesc.inputs, attrName, value): if not CompatibilityNode.attributeDescFromName(nodeDesc.inputs, attrName, value):
compatibilityIssue = CompatibilityIssue.DescriptionConflict compatibilityIssue = CompatibilityIssue.DescriptionConflict
break 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 # verify that all outputs match their descriptions
for attrName, value in outputs.items(): for attrName, value in outputs.items():
if not CompatibilityNode.attributeDescFromName(nodeDesc.outputs, attrName, value): if not CompatibilityNode.attributeDescFromName(nodeDesc.outputs, attrName, value):
@ -1493,6 +1635,7 @@ def nodeFactory(nodeDict, name=None, template=False):
if compatibilityIssue is None: if compatibilityIssue is None:
node = Node(nodeType, position, **inputs) node = Node(nodeType, position, **inputs)
node.setInternalAttributeValues(internalInputs)
else: else:
logging.warning("Compatibility issue detected for node '{}': {}".format(name, compatibilityIssue.name)) logging.warning("Compatibility issue detected for node '{}': {}".format(name, compatibilityIssue.name))
node = CompatibilityNode(nodeType, nodeDict, position, compatibilityIssue) node = CompatibilityNode(nodeType, nodeDict, position, compatibilityIssue)

View file

@ -264,11 +264,17 @@ class SetAttributeCommand(GraphCommand):
def redoImpl(self): def redoImpl(self):
if self.value == self.oldValue: if self.value == self.oldValue:
return False 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 return True
def undoImpl(self): 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): class AddEdgeCommand(GraphCommand):

View file

@ -710,7 +710,8 @@ class UIGraph(QObject):
""" Upgrade all upgradable CompatibilityNode instances in the graph. """ """ Upgrade all upgradable CompatibilityNode instances in the graph. """
with self.groupedGraphModification("Upgrade all Nodes"): with self.groupedGraphModification("Upgrade all Nodes"):
nodes = [n for n in self._graph._compatibilityNodes.values() if n.canUpgrade] 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) self.upgradeNode(node)
@Slot() @Slot()

View file

@ -1,6 +1,7 @@
import QtQuick 2.9 import QtQuick 2.9
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.0
import MaterialIcons 2.2 import MaterialIcons 2.2
import Utils 1.0 import Utils 1.0
@ -152,6 +153,12 @@ RowLayout {
case "BoolParam": return checkbox_component case "BoolParam": return checkbox_component
case "ListAttribute": return listAttribute_component case "ListAttribute": return listAttribute_component
case "GroupAttribute": return groupAttribute_component case "GroupAttribute": return groupAttribute_component
case "StringParam":
if (attribute.desc.semantic === 'multiline')
return textArea_component
return textField_component
case "ColorParam":
return color_component
default: return textField_component default: return textField_component
} }
} }
@ -184,6 +191,121 @@ 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: color_component
RowLayout {
CheckBox {
id: color_checkbox
Layout.alignment: Qt.AlignLeft
checked: node && 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 { Component {
id: comboBox_component id: comboBox_component
ComboBox { ComboBox {

View file

@ -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, "<br/>").replace(/\t/g, "&nbsp;&nbsp;&nbsp;&nbsp;")
str += "<b>" + replacedInvalidation + "</b>"
}
if (invalidation !== "" && comment !== "") {
str += "<br/><br/>"
}
if (comment !== "") {
let replacedComment = node.comment.replace(/\n/g, "<br/>").replace(/\t/g, "&nbsp;&nbsp;&nbsp;&nbsp;")
str += replacedComment
}
return str
}
// Whether an attribute can be displayed as an attribute pin on the node // Whether an attribute can be displayed as an attribute pin on the node
function isFileAttributeBaseType(attribute) { function isFileAttributeBaseType(attribute) {
// ATM, only File attributes are meant to be connected // ATM, only File attributes are meant to be connected
@ -138,7 +161,7 @@ Item {
Rectangle { Rectangle {
id: background id: background
anchors.fill: nodeContent 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.enabled: true
layer.effect: DropShadow { radius: 3; color: shadowColor } layer.effect: DropShadow { radius: 3; color: shadowColor }
radius: 3 radius: 3
@ -181,6 +204,7 @@ Item {
// Node Name // Node Name
Label { Label {
id: nodeLabel
Layout.fillWidth: true Layout.fillWidth: true
text: node ? node.label : "" text: node ? node.label : ""
padding: 4 padding: 4
@ -252,6 +276,35 @@ Item {
palette.text: "red" palette.text: "red"
ToolTip.text: "Locked" ToolTip.text: "Locked"
} }
MaterialLabel {
id: nodeComment
visible: node.comment !== "" || node.invalidation !== ""
text: MaterialIcons.comment
padding: 2
font.pointSize: 7
ToolTip {
id: nodeCommentTooltip
parent: header
visible: nodeCommentMA.containsMouse && nodeComment.visible
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 {
// If the node header is hovered, comments may be displayed
id: nodeCommentMA
anchors.fill: parent
hoverEnabled: true
}
}
} }
} }
} }

View file

@ -185,6 +185,7 @@ Panel {
currentIndex: tabBar.currentIndex currentIndex: tabBar.currentIndex
AttributeEditor { AttributeEditor {
id: inOutAttr
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
model: root.node.attributes model: root.node.attributes
@ -247,6 +248,16 @@ Panel {
Layout.fillWidth: true Layout.fillWidth: true
node: root.node node: root.node
} }
AttributeEditor {
id: nodeInternalAttr
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 +296,12 @@ Panel {
leftPadding: 8 leftPadding: 8
rightPadding: leftPadding rightPadding: leftPadding
} }
TabButton {
text: "Notes"
padding: 4
leftPadding: 8
rightPadding: leftPadding
}
} }
} }
} }

View file

@ -38,6 +38,7 @@ def test_templateVersions():
currentNodeVersion = meshroom.core.nodeVersion(nodeDesc) currentNodeVersion = meshroom.core.nodeVersion(nodeDesc)
inputs = nodeData.get("inputs", {}) inputs = nodeData.get("inputs", {})
internalInputs = nodeData.get("internalInputs", {})
version = nodesVersions.get(nodeType, None) version = nodesVersions.get(nodeType, None)
compatibilityIssue = None compatibilityIssue = None
@ -49,5 +50,9 @@ def test_templateVersions():
if not CompatibilityNode.attributeDescFromName(nodeDesc.inputs, attrName, value): if not CompatibilityNode.attributeDescFromName(nodeDesc.inputs, attrName, value):
compatibilityIssue = CompatibilityIssue.DescriptionConflict compatibilityIssue = CompatibilityIssue.DescriptionConflict
break break
for attrName, value in internalInputs.items():
if not CompatibilityNode.attributeDescFromName(nodeDesc.internalInputs, attrName, value):
compatibilityIssue = CompatibilityIssue.DescriptionConflict
break
assert compatibilityIssue is None assert compatibilityIssue is None