diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index 6cfc8a77..dd12c566 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -873,7 +873,7 @@ class Graph(BaseObject): flowEdges.append(link) return flowEdges - def nodesFromNode(self, startNode, filterTypes=None): + def nodesFromNode(self, startNode, filterTypes=None, reverse=True): """ Return the node chain from startNode to the graph leaves. @@ -881,6 +881,9 @@ class Graph(BaseObject): startNode (Node): the node to start the visit from. filterTypes (str list): (optional) only return the nodes of the given types (does not stop the visit, this is a post-process only) + reverse (bool): (optional) direction of visit. + True is for getting nodes depending on the startNode. + False is for getting nodes required for the startNode. Returns: The list of nodes and edges, from startNode to the graph leaves following edges. """ @@ -894,22 +897,15 @@ class Graph(BaseObject): visitor.discoverVertex = discoverVertex visitor.examineEdge = lambda edge, graph: edges.append(edge) - self.dfs(visitor=visitor, startNodes=[startNode], reverse=True) + self.dfs(visitor=visitor, startNodes=[startNode], reverse=reverse) return nodes, edges - @Slot(Node, result="QVariantList") - def onlyNodesFromNode(self, startNode, filterType=None): - nodes = [] - edges = [] - visitor = Visitor() + def nodesDependingOnNode(self, startNode, filterTypes=None): + nodes, edges = self.nodesFromNode(startNode, filterTypes=filterTypes, reverse=True) + return nodes - def discoverVertex(vertex, graph): - if not filterType or vertex.nodeType == filterType: - nodes.append(vertex) - - visitor.discoverVertex = discoverVertex - visitor.examineEdge = lambda edge, graph: edges.append(edge) - self.dfs(visitor=visitor, startNodes=[startNode], reverse=True) + def nodesRequiredForNode(self, startNode, filterTypes=None): + nodes, edges = self.nodesFromNode(startNode, filterTypes=filterTypes, reverse=False) return nodes @Slot(Node, result=int) diff --git a/meshroom/core/node.py b/meshroom/core/node.py index 033c4e13..1f351184 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -462,6 +462,9 @@ class BaseNode(BaseObject): self._position = position or Position() self._attributes = DictModel(keyAttrName='name', parent=self) self.attributesPerUid = defaultdict(set) + self._locked = False + + self.globalStatusChanged.connect(self.updateLocked) def __getattr__(self, k): try: @@ -813,6 +816,55 @@ class BaseNode(BaseObject): def __repr__(self): return self.name + def getLocked(self): + return self._locked + + def setLocked(self, lock): + if self._locked == lock: + return + self._locked = lock + self.lockedChanged.emit() + + def updateLocked(self): + currentStatus = self.getGlobalStatus() + + lockedStatus = (Status.RUNNING, Status.SUBMITTED) + + # Unlock required nodes if the current node changes to Error + if currentStatus == Status.ERROR: + requiredNodes = self.graph.nodesRequiredForNode(self) + for node in requiredNodes: + node.setLocked(False) + + # Avoid useless travel through nodes + # For instance: when loading a scene with successful nodes + if not self._locked and currentStatus == Status.SUCCESS: + return + + if currentStatus == Status.SUCCESS: + # At this moment, the node is necessarily locked because of previous if statement + requiredNodes = self.graph.nodesRequiredForNode(self) + dependentNodes = self.graph.nodesDependingOnNode(self) + stayLocked = None + + # Check if at least one dependentNode is submitted or currently running + for node in dependentNodes: + if node.getGlobalStatus() in lockedStatus and node._chunks.at(0).statusNodeName == node.name: + stayLocked = True + break + if not stayLocked: + # Unlock every required node + for node in requiredNodes: + node.setLocked(False) + return + elif currentStatus in lockedStatus: + requiredNodes = self.graph.nodesRequiredForNode(self) + for node in requiredNodes: + node.setLocked(True) + return + + self.setLocked(False) + name = Property(str, getName, constant=True) label = Property(str, getLabel, constant=True) nodeType = Property(str, nodeType.fget, constant=True) @@ -834,6 +886,8 @@ class BaseNode(BaseObject): globalStatusChanged = Signal() globalStatus = Property(str, lambda self: self.getGlobalStatus().name, notify=globalStatusChanged) isComputed = Property(bool, _isComputed, notify=globalStatusChanged) + lockedChanged = Signal() + locked = Property(bool, getLocked, setLocked, notify=lockedChanged) class Node(BaseNode): diff --git a/meshroom/ui/qml/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml index b5b3163d..2cf693ed 100755 --- a/meshroom/ui/qml/GraphEditor/GraphEditor.qml +++ b/meshroom/ui/qml/GraphEditor/GraphEditor.qml @@ -251,21 +251,7 @@ Item { point2x: dst.nodeItem.x + dstAnchor.x point2y: dst.nodeItem.y + dstAnchor.y onPressed: { - var canEdit = true - if(_reconstruction.computing) { - if(uigraph.taskManager.nodes.contains(edge.src.node)) { - canEdit = false; - } else { - if(object.globalStatus == "SUCCESS") { - var nodes = uigraph.graph.onlyNodesFromNode(edge.src.node); - for(var i = 0; i < nodes.length; i++) { - if(["SUBMITTED", "RUNNING"].includes(nodes[i].globalStatus) && nodes[i].chunks.at(0).statusNodeName == nodes[i].name) { - canEdit = false; - } - } - } - } - } + const canEdit = !edge.src.node.locked if(canEdit && event.button == Qt.RightButton) { @@ -325,27 +311,7 @@ Item { } MenuItem { text: "Remove Node" + (removeFollowingButton.hovered ? "s From Here" : "") - enabled: { - if(! _reconstruction.computing) { - return true; - } - - if(uigraph.taskManager.nodes.contains(uigraph.selectedNode)) { - return false; - } else { - if(uigraph.selectedNode.globalStatus == "SUCCESS") { - var nodes = uigraph.graph.onlyNodesFromNode(uigraph.selectedNode); - for(var i = 0; i < nodes.length; i++) { - if(["SUBMITTED", "RUNNING"].includes(nodes[i].globalStatus) && nodes[i].chunks.at(0).statusNodeName == nodes[i].name) { - return false; - } - } - } - } - - return true; - } - + enabled: nodeMenu.currentNode ? !nodeMenu.currentNode.locked : false onTriggered: uigraph.removeNode(nodeMenu.currentNode) MaterialToolButton { id: removeFollowingButton @@ -361,26 +327,7 @@ Item { MenuSeparator {} MenuItem { text: "Delete Data" + (deleteFollowingButton.hovered ? " From Here" : "" ) + "..." - enabled: { - if(! _reconstruction.computing) { - return true; - } - - if(uigraph.taskManager.nodes.contains(uigraph.selectedNode) || ["SUBMITTED", "RUNNING"].includes(_reconstruction.selectedNode.globalStatus)) { - return false; - } else { - if(uigraph.selectedNode.globalStatus == "SUCCESS") { - var nodes = uigraph.graph.onlyNodesFromNode(uigraph.selectedNode); - for(var i = 0; i < nodes.length; i++) { - if(["SUBMITTED", "RUNNING"].includes(nodes[i].globalStatus) && nodes[i].chunks.at(0).statusNodeName == nodes[i].name) { - return false; - } - } - } - } - - return true; - } + enabled: nodeMenu.currentNode ? !nodeMenu.currentNode.locked : false function showConfirmationDialog(deleteFollowing) { var obj = deleteDataDialog.createObject(root, @@ -443,26 +390,7 @@ Item { node: object width: uigraph.layout.nodeWidth - readOnly: { - if(! uigraph.computing) { - return false; - } - if(object.globalStatus == "SUCCESS") { - var nodes = uigraph.graph.onlyNodesFromNode(object); - for(var i = 0; i < nodes.length; i++) { - if(["SUBMITTED", "RUNNING"].includes(nodes[i].globalStatus) && nodes[i].chunks.at(0).statusNodeName == nodes[i].name) { - return true; - } - } - } else if(["SUBMITTED", "RUNNING"].includes(object.globalStatus)) { - return true; - } else { - return false; - } - - return false; - } selected: uigraph.selectedNode === node hovered: uigraph.hoveredNode === node onSelectedChanged: if(selected) forceActiveFocus() diff --git a/meshroom/ui/qml/GraphEditor/Node.qml b/meshroom/ui/qml/GraphEditor/Node.qml index 248d8ef8..bebbabf8 100755 --- a/meshroom/ui/qml/GraphEditor/Node.qml +++ b/meshroom/ui/qml/GraphEditor/Node.qml @@ -16,7 +16,7 @@ Item { /// The underlying Node object property variant node /// Whether the node can be modified - property bool readOnly: false + property bool readOnly: node.locked /// Whether the node is in compatibility mode readonly property bool isCompatibilityNode: node.hasOwnProperty("compatibilityIssue") /// Mouse related states diff --git a/meshroom/ui/qml/main.qml b/meshroom/ui/qml/main.qml index add5da37..ed26ce3b 100755 --- a/meshroom/ui/qml/main.qml +++ b/meshroom/ui/qml/main.qml @@ -167,13 +167,10 @@ ApplicationWindow { } else _reconstruction.execute(node); - - nodeEditor.updateNodeStatus() } function submit(node) { _reconstruction.submit(node); - nodeEditor.updateNodeStatus() } @@ -729,32 +726,8 @@ ApplicationWindow { node: _reconstruction.selectedNode property bool computing: _reconstruction.computing // Make NodeEditor readOnly when computing - readOnly: false - onNodeChanged: { updateNodeStatus() } - onComputingChanged: { updateNodeStatus() } + readOnly: node.locked - function updateNodeStatus() { - if(! _reconstruction.computing) { - readOnly = false; - return; - } - - if(node.globalStatus === "SUCCESS") { - var nodes = _reconstruction.graph.onlyNodesFromNode(node); - for(var i = 0; i < nodes.length; i++) { - if(["SUBMITTED", "RUNNING"].includes(nodes[i].globalStatus) && nodes[i].chunks.at(0).statusNodeName == nodes[i].name) { - readOnly = true; - return; - } - } - readOnly = false; - } else if(["SUBMITTED", "RUNNING"].includes(node.globalStatus)) { - readOnly = true; - } else { - readOnly = false; - } - - } onAttributeDoubleClicked: workspaceView.viewAttribute(attribute, mouse) onUpgradeRequest: { var n = _reconstruction.upgradeNode(node);