mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-04-29 02:08:08 +02:00
Merge pull request #1744 from alicevision/dev/internalAttributes
Add internal attributes in "Notes" tab
This commit is contained in:
commit
ce2085faad
10 changed files with 471 additions and 25 deletions
|
@ -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
|
||||
|
@ -168,6 +172,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 +189,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
|
||||
|
@ -323,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):
|
||||
|
|
|
@ -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):
|
||||
|
@ -343,6 +347,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, 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):
|
||||
NONE = 0
|
||||
NORMAL = 1
|
||||
|
@ -485,6 +502,43 @@ class Node(object):
|
|||
ram = Level.NORMAL
|
||||
packageName = ''
|
||||
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 = []
|
||||
outputs = []
|
||||
size = StaticNodeSize(1)
|
||||
|
|
|
@ -695,9 +695,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):
|
||||
|
@ -1229,14 +1244,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():
|
||||
|
@ -1244,12 +1259,27 @@ 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]
|
||||
|
||||
# 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"]
|
||||
|
|
|
@ -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
|
||||
|
@ -523,10 +524,41 @@ 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)
|
||||
|
||||
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)
|
||||
def nameToLabel(self, name):
|
||||
"""
|
||||
|
@ -568,13 +600,37 @@ 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 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
|
||||
|
||||
def getInternalAttributes(self):
|
||||
return self._internalAttributes
|
||||
|
||||
@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):
|
||||
return name in self._internalAttributes.keys()
|
||||
|
||||
def _applyExpr(self):
|
||||
for attr in self._attributes:
|
||||
attr._applyExpr()
|
||||
|
@ -632,7 +688,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)
|
||||
|
||||
|
@ -842,6 +898,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)
|
||||
|
@ -1066,7 +1125,6 @@ class BaseNode(BaseObject):
|
|||
|
||||
|
||||
name = Property(str, getName, constant=True)
|
||||
label = Property(str, getLabel, constant=True)
|
||||
nodeType = Property(str, nodeType.fget, constant=True)
|
||||
documentation = Property(str, getDocumentation, constant=True)
|
||||
positionChanged = Signal()
|
||||
|
@ -1074,6 +1132,12 @@ 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)
|
||||
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()
|
||||
|
@ -1123,11 +1187,19 @@ 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:
|
||||
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):
|
||||
|
@ -1150,8 +1222,22 @@ class Node(BaseNode):
|
|||
except ValueError:
|
||||
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):
|
||||
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 +1251,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,
|
||||
}
|
||||
|
||||
|
@ -1220,6 +1307,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", {})
|
||||
|
@ -1231,11 +1319,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([
|
||||
|
@ -1328,7 +1420,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.
|
||||
|
||||
|
@ -1336,19 +1428,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
|
||||
|
@ -1373,6 +1472,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.
|
||||
|
@ -1406,9 +1512,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:
|
||||
|
@ -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))
|
||||
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)
|
||||
|
@ -1453,6 +1572,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)
|
||||
|
@ -1475,16 +1595,38 @@ 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
|
||||
|
||||
# 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):
|
||||
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 +1635,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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,12 @@ 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
|
||||
case "ColorParam":
|
||||
return color_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 {
|
||||
id: comboBox_component
|
||||
ComboBox {
|
||||
|
|
|
@ -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, " ")
|
||||
str += "<b>" + replacedInvalidation + "</b>"
|
||||
}
|
||||
if (invalidation !== "" && comment !== "") {
|
||||
str += "<br/><br/>"
|
||||
}
|
||||
if (comment !== "") {
|
||||
let replacedComment = node.comment.replace(/\n/g, "<br/>").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
|
||||
|
@ -138,7 +161,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
|
||||
|
@ -181,6 +204,7 @@ Item {
|
|||
|
||||
// Node Name
|
||||
Label {
|
||||
id: nodeLabel
|
||||
Layout.fillWidth: true
|
||||
text: node ? node.label : ""
|
||||
padding: 4
|
||||
|
@ -252,6 +276,35 @@ Item {
|
|||
palette.text: "red"
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -185,6 +185,7 @@ Panel {
|
|||
currentIndex: tabBar.currentIndex
|
||||
|
||||
AttributeEditor {
|
||||
id: inOutAttr
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
model: root.node.attributes
|
||||
|
@ -247,6 +248,16 @@ Panel {
|
|||
Layout.fillWidth: true
|
||||
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
|
||||
rightPadding: leftPadding
|
||||
}
|
||||
TabButton {
|
||||
text: "Notes"
|
||||
padding: 4
|
||||
leftPadding: 8
|
||||
rightPadding: leftPadding
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue