diff --git a/meshroom/core/desc.py b/meshroom/core/desc.py index 1a7f4893c6..a7ceb3355c 100644 --- a/meshroom/core/desc.py +++ b/meshroom/core/desc.py @@ -702,6 +702,22 @@ class Node(object): value="", invalidate=False, ), + FloatParam( + name="nodeWidth", + label="Node Width", + description="The Node's Width.", + value=160, + range=None, + enabled=False # Hidden always + ), + FloatParam( + name="nodeHeight", + label="Node Height", + description="The Node's Height.", + value=120, + range=None, + enabled=False # Hidden always + ), ColorParam( name="color", label="Color", @@ -779,6 +795,68 @@ def __init__(self): def processChunk(self, chunk): pass + def stopProcess(self, chunk): + pass + + +class Backdrop(InputNode): + """ A Backdrop for other nodes. + """ + + # The internal inputs' of Backdrop Node needs a Integer Field to determine the font size for the comment + 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", + 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", + invalidate=False, + ), + IntParam( + name="fontSize", + label="Font Size", + description="The Font size for the User Comment on the Backdrop.", + value=12, + range=(6, 100, 1), + ), + FloatParam( + name="nodeWidth", + label="Node Width", + description="The Backdrop Node's Width.", + value=600, + range=None, + enabled=False # Hidden always + ), + FloatParam( + name="nodeHeight", + label="Node Height", + description="The Backdrop Node's Height.", + value=400, + range=None, + enabled=False # Hidden always + ), + ColorParam( + name="color", + label="Color", + description="Custom color for the node (SVG name or hexadecimal code).", + value="", + invalidate=False, + ) + ] + class CommandLineNode(Node): """ diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index e278ae33e9..003833387d 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -16,7 +16,7 @@ from meshroom.core import Version from meshroom.core.attribute import Attribute, ListAttribute, GroupAttribute from meshroom.core.exception import StopGraphVisit, StopBranchVisit -from meshroom.core.node import nodeFactory, Status, Node, CompatibilityNode +from meshroom.core.node import nodeFactory, Status, Node, CompatibilityNode, Position # Replace default encoder to support Enums @@ -56,6 +56,130 @@ def GraphModification(graph): graph.updateEnabled = enabled +class Region: + """ Defines the boundaries for a Region on the 2d Plane (Graph in our Context). + """ + + class Range: + """ Defines a Range between two points in the Graph or 2d Plane, inclusive of the 2nd Point. + """ + + def __init__(self, x, y): + """ Constructor. + + Args: + x (int | float): An integer or float start. + y (int | float): An integer or float end. + """ + # Internal Coords + self._x = x + self._y = y + + def __repr__(self): + """ Represents the instance. + """ + return f"Range::{self._x}, {self._y}" + + def __contains__(self, _i): + """ Returns True if the provided integer or float falls between the start and the end point of the range. + """ + return self._x < _i <= self._y + + + def __init__(self, x=0, y=0, right=0, bottom=0): + """ Constructor. + + Args: + x (int | float): The x coordinate of the top-left point on the Region. + y (int | float): The y coordinate of the top-left point on the Region. + right (int | float): The x coordinate of the bottom-right point on the Region. + bottom (int | float): The y coordinate of the bottom-right point on the Region. + """ + # The coords of the Region can be represented as + # (x, y) + # .------------------------. + # | | + # | | + # | | + # | | + # '------------------------' + # (right, bottom) + self._x = x + self._y = y + self._right = right + self._bottom = bottom + + # Properties + x = property(lambda self: self._x) + y = property(lambda self: self._y) + right = property(lambda self: self._right) + bottom = property(lambda self: self._bottom) + + def __contains__(self, point): + """ Returns True if the provided Point is present in the Region. + """ + return self.contains(point) + + def __repr__(self): + """ Represents the instance. + """ + return f"Region::{self.points()}" + + # Public + def xrange(self): + """ Defines the Range between the left most and right most x-coordinate. + + Returns: + Region.Range. Range of the left most and right most x-coordinate. + """ + return Region.Range(self._x, self._right) + + def yrange(self): + """ Defines the Range between the top most and bottom most y-coordinate. + + Returns: + Region.Range. Range of the top most and bottom most y-coordinate. + """ + return Region.Range(self._y, self._bottom) + + def contains(self, point): + """ Returns True if the provided point is present inside the Region. + + Args: + point (Position) A 2d Point position. + + Returns: + bool. True if the point is in the Region else False. + """ + return point.x in self.xrange() and point.y in self.yrange() + + def points(self): + """ A Region can be represented by basic 2 points defining its top-left and bottom right position. + + Returns: + list. Array of Positions for the Region. + """ + return [Position(self._x, self._y), Position(self._right, self._bottom)] + + def containsRegion(self, region): + """ Returns True if the provided region belongs to the current Region. + + Args: + region (Region): The region to check for. + + Returns: + bool. True if the provided region belongs to the current Region. + """ + # Check if both top-left and bottom-right points of the region fall in the current region + for point in region.points(): + # If any of the point is outside of the -> The region can be safely marked as not in current region + if point not in self: + return False + + # Else it belongs + return True + + class Edge(BaseObject): def __init__(self, src, dst, parent=None): @@ -165,6 +289,9 @@ class Graph(BaseObject): """ _cacheDir = "" + # Graph's Region Of Interest + ROI = Region + class IO(object): """ Centralize Graph file keys and IO version. """ __version__ = "2.0" @@ -679,6 +806,26 @@ def nodeOutEdges(self, node): """ Return the list of edges starting from this node """ return [edge for edge in self.edges if edge.src.node == node] + def nodesInRegion(self, region): + """ Returns the Nodes present in this region. + + Args: + region (Graph.ROI): Region to look for nodes. + + Returns: + list. Array of Nodes present in the Region. + """ + # A node with 40 pixels inside the backdrop in terms of height could be considered a candidate ? + nodes = [] + for node in self._nodes.values(): + # Node's Region + noder = Graph.ROI(node.x, node.y, node.x + node.nodeWidth, node.y + 40) + # If the provided region contains the node region -> add the node to the array of nodes + if region.containsRegion(noder): + nodes.append(node) + + return nodes + @changeTopology def removeNode(self, nodeName): """ diff --git a/meshroom/core/node.py b/meshroom/core/node.py index b78f3725e6..6eb1e73db1 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -52,6 +52,8 @@ class Status(Enum): SUCCESS = 6 INPUT = 7 # Special status for input nodes + BACKDROP = 8 # Backdrops are just well.. backdrops + class ExecMode(Enum): NONE = 0 @@ -570,6 +572,60 @@ def getComment(self): return self.internalAttribute("comment").value return "" + def getFontSize(self): + """ Gets the Font Size of the node comment. + + Returns: + int: The font size from the node if it exists, else 12 as default. + """ + if self.hasInternalAttribute("fontSize"): + return self.internalAttribute("fontSize").value + + # Default to 12 + return 12 + + def getNodeWidth(self): + """ Gets the Width of the node from the internal attribute. + + Returns: + int: The Width from the node if the attribute exists, else 160 as default. + """ + if self.hasInternalAttribute("nodeWidth"): + return self.internalAttribute("nodeWidth").value + + # Default to 160 + return 160 + + def getNodeHeight(self): + """ Gets the Height of the node from the internal attribute. + + Returns: + int: The Height from the node if the attribute exists, else 120 as default. + """ + if self.hasInternalAttribute("nodeHeight"): + return self.internalAttribute("nodeHeight").value + + # Default to 120 + return 120 + + def setNodeWidth(self, width): + """ Gets the Width of the node from the internal attribute. + + Returns: + int: The Width from the node if the attribute exists, else 160 as default. + """ + if self.hasInternalAttribute("nodeWidth"): + self.internalAttribute("nodeWidth").value = width + + def setNodeHeight(self, height): + """ Gets the Height of the node from the internal attribute. + + Returns: + int: The Height from the node if the attribute exists, else 120 as default. + """ + if self.hasInternalAttribute("nodeHeight"): + self.internalAttribute("nodeHeight").value = height + @Slot(str, result=str) def nameToLabel(self, name): """ @@ -817,7 +873,7 @@ def nbParallelizationBlocks(self): def hasStatus(self, status): if not self._chunks: - return (status == Status.INPUT) + return (status in (Status.BACKDROP, Status.INPUT)) for chunk in self._chunks: if chunk.status.status != status: return False @@ -829,7 +885,13 @@ def _isComputed(self): return self.hasStatus(Status.SUCCESS) def _isComputable(self): - return self.getGlobalStatus() != Status.INPUT + # A node is not considered computable if it is a Backdrop or an inputNode + return self.getGlobalStatus() not in (Status.BACKDROP, Status.INPUT) + + def _isBackdrop(self): + """ If this is a backdrop node? + """ + return self.getGlobalStatus() == Status.BACKDROP def clearData(self): """ Delete this Node internal folder. @@ -1126,8 +1188,13 @@ def getGlobalStatus(self): Returns: Status: the node global status """ + # A Backdrop Node instance + if isinstance(self.nodeDesc, desc.Backdrop): + return Status.BACKDROP + if isinstance(self.nodeDesc, desc.InputNode): return Status.INPUT + chunksStatus = [chunk.status.status for chunk in self._chunks] anyOf = (Status.ERROR, Status.STOPPED, Status.KILLED, @@ -1323,7 +1390,7 @@ def hasSequenceOutputAttribute(self): False otherwise. """ for attr in self._attributes: - if attr.enabled and attr.isOutput and (attr.desc.semantic == "sequence" or + if attr.enabled and attr.isOutput and (attr.desc.semantic == "sequence" or attr.desc.semantic == "imageList"): return True return False @@ -1356,6 +1423,12 @@ def has3DOutputAttribute(self): color = Property(str, getColor, notify=internalAttributesChanged) invalidation = Property(str, getInvalidationMessage, notify=internalAttributesChanged) comment = Property(str, getComment, notify=internalAttributesChanged) + fontSize = Property(int, getFontSize, notify=internalAttributesChanged) + + # Node Dimensions + nodeWidth = Property(float, getNodeWidth, notify=internalAttributesChanged) + nodeHeight = Property(float, getNodeHeight, notify=internalAttributesChanged) + internalFolderChanged = Signal() internalFolder = Property(str, internalFolder.fget, notify=internalFolderChanged) valuesFile = Property(str, valuesFile.fget, notify=internalFolderChanged) @@ -1381,6 +1454,7 @@ def has3DOutputAttribute(self): isExternal = Property(bool, isExtern, notify=globalExecModeChanged) isComputed = Property(bool, _isComputed, notify=globalStatusChanged) isComputable = Property(bool, _isComputable, notify=globalStatusChanged) + isBackdrop = Property(bool, _isBackdrop, notify=globalStatusChanged) aliveChanged = Signal() alive = Property(bool, alive.fget, alive.fset, notify=aliveChanged) lockedChanged = Signal() @@ -1526,8 +1600,10 @@ def toDict(self): def _updateChunks(self): """ Update Node's computation task splitting into NodeChunks based on its description """ - if isinstance(self.nodeDesc, desc.InputNode): + # No chunks for Input and Backdrop nodes as they don't need any processing + if isinstance(self.nodeDesc, (desc.InputNode, desc.Backdrop)): return + self.setSize(self.nodeDesc.size.computeSize(self)) if self.isParallelized: try: @@ -1876,7 +1952,7 @@ def nodeFactory(nodeDict, name=None, template=False, uidConflict=False): # 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 + if not template and (sorted([attr.name for attr in nodeDesc.inputs if not isinstance(attr, desc.PushButtonParam)]) != sorted(inputs.keys()) or sorted([attr.name for attr in nodeDesc.outputs if not attr.isDynamicValue]) != sorted(outputs.keys())): diff --git a/meshroom/core/taskManager.py b/meshroom/core/taskManager.py index b1d225a6c1..e97c422fc6 100644 --- a/meshroom/core/taskManager.py +++ b/meshroom/core/taskManager.py @@ -336,10 +336,14 @@ def checkNodesDependencies(self, graph, toNodes, context): """ ready = [] computed = [] - inputNodes = [] + + # The Node which does not have processing functionality + incomputable = [] + for node in toNodes: + # Input or Backdrop nodes if not node.isComputable: - inputNodes.append(node) + incomputable.append(node) elif context == "COMPUTATION": if graph.canComputeTopologically(node) and graph.canSubmitOrCompute(node) % 2 == 1: ready.append(node) @@ -353,7 +357,7 @@ def checkNodesDependencies(self, graph, toNodes, context): else: raise ValueError("Argument 'context' must be: 'COMPUTATION' or 'SUBMITTING'") - if len(ready) + len(computed) + len(inputNodes) != len(toNodes): + if len(ready) + len(computed) + len(incomputable) != len(toNodes): toNodes.clear() toNodes.extend(ready) return False diff --git a/meshroom/nodes/aliceVision/BasicBackdrop.py b/meshroom/nodes/aliceVision/BasicBackdrop.py new file mode 100644 index 0000000000..5c1235fd25 --- /dev/null +++ b/meshroom/nodes/aliceVision/BasicBackdrop.py @@ -0,0 +1,13 @@ +""" A Backdrop node. +""" +__version__ = "1.0" + +from meshroom.core import desc + + +class Backdrop(desc.Backdrop): + """ A Basic Backdrop node. + """ + + # Is a Utility Node + category = "Utils" diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index 448f25484a..d8308d55b9 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -642,6 +642,20 @@ def filterNodes(self, nodes): nodes = [nodes] return [ n for n in nodes if n in self._graph.nodes.values() ] + @Slot(Node, float, float) + def resizeNode(self, node, width, height): + """ Resizes the Node. + + Args: + node (Node): the node to move + width (float): Node width. + height (float): Node height. + """ + # Update the node size + with self.groupedGraphModification("Resize Node Dimensions"): + node.setNodeWidth(width) + node.setNodeHeight(height) + @Slot(Node, QPoint, QObject) def moveNode(self, node, position, nodes=None): """ @@ -748,7 +762,7 @@ def duplicateNodesFrom(self, nodes): uniqueNodesToDuplicate = list(dict.fromkeys(nodesToDuplicate)) duplicates = self.duplicateNodes(uniqueNodesToDuplicate) return duplicates - + @Slot(Edge, result=bool) def canExpandForLoop(self, currentEdge): """ Check if the list attribute can be expanded by looking at all the edges connected to it. """ @@ -846,7 +860,7 @@ def replaceEdge(self, edge, newSrc, newDst): self.removeEdge(edge) self.addEdge(newSrc, newDst) return self._graph.edge(newDst) - + @Slot(Attribute, result=Edge) def getEdge(self, dst): return self._graph.edge(dst) @@ -940,6 +954,21 @@ def appendSelection(self, node): if not self._selectedNodes.contains(node): self._selectedNodes.append(node) + @Slot(Node) + def appendBackdropSelection(self, node): + """ Appends 'children' of the provided backdrop node if they're not already part of the selection. + + Args: + node (Node): Backdrop Node instance. + """ + # Only applicable for backdrop nodes + if not node.isBackdrop: + return + + # Iterate and select the individual nodes which belong to the backdrop as per the graph + for child in self.getBackdropNodes(node): + self.appendSelection(child) + @Slot("QVariantList") def selectNodes(self, nodes): """ Append 'nodes' to the selection. """ @@ -947,6 +976,19 @@ def selectNodes(self, nodes): self.appendSelection(node) self.selectedNodesChanged.emit() + @Slot(Node, result="QVariantList") + def getBackdropNodes(self, node): + """ Returns the Nodes from the graph which are present in a region. + """ + # A region of intereset for the Backdrop + region = self._graph.ROI(node.x, node.y, node.x + node.nodeWidth, node.y + node.nodeHeight) + + # Fetch the nodes from the graph which belong to the region of the Backdrop + nodes = self._graph.nodesInRegion(region) + + # Ignore the current node from the region nodes + return [n for n in nodes if n != node] + @Slot(Node) def selectFollowing(self, node): """ Select all the nodes the depend on 'node'. """ diff --git a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml index d0942e2c6e..ebec39b37f 100644 --- a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml +++ b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml @@ -405,7 +405,7 @@ RowLayout { text: "Custom Color" onClicked: { if (checked) { - _reconstruction.setAttribute(attribute, "#0000FF") + _reconstruction.setAttribute(attribute, node.isBackdrop ? "#fffb85" : "#0000FF") } else { _reconstruction.setAttribute(attribute, "") } diff --git a/meshroom/ui/qml/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml index c55ca2e22f..739bc2440f 100755 --- a/meshroom/ui/qml/GraphEditor/GraphEditor.qml +++ b/meshroom/ui/qml/GraphEditor/GraphEditor.qml @@ -65,6 +65,10 @@ Item { uigraph.selectedNode = node if (node !== null) { uigraph.appendSelection(node) + + // If the node is a backdrop -> Select it's children + if (node.isBackdrop) uigraph.appendBackdropSelection(node) + uigraph.selectedNodesChanged() } } @@ -135,7 +139,7 @@ Item { Keys.onPressed: { if (event.key === Qt.Key_F) { fit() - } else if (event.key === Qt.Key_Delete) { + } else if (event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace) { // Backspace supports both Windows and MacOS Keyboards if (event.modifiers === Qt.AltModifier) { uigraph.removeNodesFrom(uigraph.selectedNodes) } else { @@ -858,6 +862,9 @@ Item { onMoved: uigraph.moveNode(node, position, uigraph.selectedNodes) + // Update the Node size + onResized: uigraph.resizeNode(node, width, height) + onEntered: uigraph.hoveredNode = node onExited: uigraph.hoveredNode = null diff --git a/meshroom/ui/qml/GraphEditor/Node.qml b/meshroom/ui/qml/GraphEditor/Node.qml index 9a8bb05a17..ea26adc189 100755 --- a/meshroom/ui/qml/GraphEditor/Node.qml +++ b/meshroom/ui/qml/GraphEditor/Node.qml @@ -19,23 +19,25 @@ Item { property bool readOnly: node.locked /// Whether the node is in compatibility mode readonly property bool isCompatibilityNode: node ? node.hasOwnProperty("compatibilityIssue") : false + /// Whether the node is a backdrop node + readonly property bool isBackdrop: node ? node.isBackdrop : false /// Mouse related states property bool mainSelected: false property bool selected: false property bool hovered: false - property bool dragging: mouseArea.drag.active + property bool dragging: mouseArea.drag.active || leftDragger.drag.active /// Combined x and y property point position: Qt.point(x, y) /// Styling property color shadowColor: "#cc000000" - readonly property color defaultColor: isCompatibilityNode ? "#444" : !node.isComputable ? "#BA3D69" : activePalette.base + readonly property color defaultColor: _defaultColor() property color baseColor: defaultColor property point mousePosition: Qt.point(mouseArea.mouseX, mouseArea.mouseY) Item { id: m - property bool displayParams: false + property bool displayParams: isBackdrop ? true : false } // Mouse interaction related signals @@ -45,6 +47,9 @@ Item { signal entered() signal exited() + // Size signal + signal resized(var width, var height) + // Already connected attribute with another edge in DropArea signal edgeAboutToBeRemoved(var input) @@ -60,6 +65,9 @@ Item { x: root.node ? root.node.x : undefined y: root.node ? root.node.y : undefined + // The backdrop node always needs to be at the back + z: isBackdrop ? -1 : 1 + implicitHeight: childrenRect.height SystemPalette { id: activePalette } @@ -73,6 +81,21 @@ Item { } } + function _defaultColor() { + /* + * Returns the default color for the Node. + * If the node is in Compatibility Mode - Grey + * If the node is an InputNode - Pinkish + * If the node is a Backdrop - Yellow or the Node Color + */ + if (isCompatibilityNode) return "#444" + else if (isBackdrop) return node.color === "" ? "#fffb85" : node.color + else if (!node.isComputable) return "#BA3D69" + + // The default color for the node + return activePalette.base + } + function formatInternalAttributesTooltip(invalidation, comment) { /* * Creates a string that contains the invalidation message (if it is not empty) in bold, @@ -118,7 +141,7 @@ Item { // Main Layout MouseArea { id: mouseArea - width: parent.width + width: isBackdrop ? node.nodeWidth : parent.width height: body.height drag.target: root // small drag threshold to avoid moving the node by mistake @@ -137,6 +160,138 @@ Item { cursorShape: drag.active ? Qt.ClosedHandCursor : Qt.ArrowCursor + /// Backdrop Resize Controls ??? + /// + /// Resize Right Side + /// + Rectangle { + width: 4 + height: nodeContent.height + + color: baseColor + opacity: 0 + + // Only Visible for a backdrop node + visible: isBackdrop + + anchors.horizontalCenter: parent.right + + // This mouse area serves as the dragging rectangle + MouseArea { + id: rightDragger + + cursorShape: Qt.SizeHorCursor + anchors.fill: parent + + drag{ target: parent; axis: Drag.XAxis } + + onMouseXChanged: { + if (drag.active) { + // Width of the Area + let w = 0 + + // Update the area width + w = mouseArea.width + mouseX + + // Ensure we have a minimum width always + if (w < 300) { + w = 300 + } + + // emit the width and height + root.resized(w, nodeContent.height) + } + } + } + } + + /// + /// Resize Left Side + /// + Rectangle { + width: 4 + height: nodeContent.height + + color: baseColor + opacity: 0 + + // Only Visible for a backdrop node + visible: isBackdrop + + anchors.horizontalCenter: parent.left + + // This mouse area serves as the dragging rectangle + MouseArea { + id: leftDragger + + cursorShape: Qt.SizeHorCursor + anchors.fill: parent + + drag{ target: parent; axis: Drag.XAxis } + + onMouseXChanged: { + if (drag.active) { + // Width of the Area + let w = 0 + + // Update the area width + w = mouseArea.width - mouseX + + // Ensure we have a minimum width always + if (w > 300) { + // Update the node's x position + root.x = root.x + mouseX + // Emit the updated width and height + root.resized(w, nodeContent.height) + } + } + } + } + } + + /// + /// Resize Bottom + /// + Rectangle { + width: mouseArea.width + height: 4 + + color: baseColor + opacity: 0 + + // Only Visible for a backdrop node + visible: isBackdrop + + anchors.verticalCenter: nodeContent.bottom + + MouseArea { + id: bottomDragger + + cursorShape: Qt.SizeVerCursor + anchors.fill: parent + + drag{ target: parent; axis: Drag.YAxis } + + onMouseYChanged: { + if (drag.active) { + // Height of the node + let h = 0 + + // Update the height + h = nodeContent.height + mouseY + + // Ensure a minimum height + if (h < 300) { + h = 300 + } + + // emit the width and height for it to be updated + root.resized(mouseArea.width, h) + } + } + } + } + // Selection border Rectangle { anchors.fill: nodeContent @@ -164,7 +319,7 @@ Item { Rectangle { id: background anchors.fill: nodeContent - color: node.color === "" ? Qt.lighter(activePalette.base, 1.4) : node.color + color: isBackdrop ? baseColor : node.color === "" ? Qt.lighter(activePalette.base, 1.4) : node.color layer.enabled: true layer.effect: DropShadow { radius: 3; color: shadowColor } radius: 3 @@ -174,7 +329,7 @@ Item { Rectangle { id: nodeContent width: parent.width - height: childrenRect.height + height: isBackdrop ? node.nodeHeight : childrenRect.height color: "transparent" // Data Layout @@ -211,7 +366,7 @@ Item { Layout.fillWidth: true text: node ? node.label : "" padding: 4 - color: root.mainSelected ? "white" : activePalette.text + color: isBackdrop ? "#2b2b2b" : root.mainSelected ? "white" : activePalette.text elide: Text.ElideMiddle font.pointSize: 8 } @@ -240,7 +395,7 @@ Item { MaterialToolButton { property string baseText: "Shares internal folder (data) with other node(s). Hold click for details." property string toolTipText: visible ? baseText : "" - visible: node.hasDuplicates + visible: !isBackdrop && node.hasDuplicates text: MaterialIcons.layers font.pointSize: 7 padding: 2 @@ -290,7 +445,7 @@ Item { MaterialLabel { id: nodeComment - visible: node.comment !== "" || node.invalidation !== "" + visible: !isBackdrop && (node.comment !== "" || node.invalidation !== "") text: MaterialIcons.comment padding: 2 font.pointSize: 7 @@ -376,6 +531,20 @@ Item { // Vertical Spacer Item { width: parent.width; height: 2 } + // Node Text + Rectangle { + y: header.height + + // Show only when the node is backdrop node and if we have a comment + visible: isBackdrop && node.comment + + Text { + text: node.comment + padding: 4 + font.pointSize: node.fontSize + } + } + // Input/Output Attributes Item { id: nodeAttributes @@ -531,6 +700,7 @@ Item { spacing: 0 anchors.margins: 0 font.pointSize: 10 + visible: !isBackdrop onClicked: { m.displayParams = ! m.displayParams }