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.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):

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__()
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)

View file

@ -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"]

View file

@ -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)

View file

@ -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):

View file

@ -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()

View file

@ -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 {

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
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
}
}
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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