Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions meshroom/ui/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -1169,8 +1169,29 @@ def pasteNodes(self, serializedData: str, position: Optional[QPoint]=None) -> li
logging.warning("Content is not a valid graph data.")
return []
return result



@Slot(Node, result=bool)
def canComputeNode(self, node: Node) -> bool:
""" Check if the node can be computed """
if node.isCompatibilityNode or not node.isComputableType or node.getLocked():
return False
if node.isComputed:
return True
if self._graph.canComputeTopologically(node) and self._graph.canSubmitOrCompute(node) % 2 == 1:
return True
return False

@Slot(Node, result=bool)
def canSubmitNode(self, node: Node) -> bool:
""" Check if the node can be submitted """
if node.isCompatibilityNode or not node.isComputableType or node.getLocked():
return False
if node.isComputed:
return True
if self._graph.canComputeTopologically(node) and self._graph.canSubmitOrCompute(node)> 1:
return True
return False

undoStack = Property(QObject, lambda self: self._undoStack, constant=True)
graphChanged = Signal()
graph = Property(Graph, lambda self: self._graph, notify=graphChanged)
Expand Down
297 changes: 297 additions & 0 deletions meshroom/ui/qml/Controls/NodeActions.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

import MaterialIcons 2.2
import Utils 1.0

Item {
id: root

// Settings
readonly property real headerOffset: 10 // Distance above the node in screen pixels
readonly property real _opacity: 0.9

// Objects passed from the graph editor
property var uigraph: null
property var draggable: null // The draggable container from GraphEditor
property var nodeRepeater: null // Reference to nodeRepeater to find delegates

// Signals
signal computeRequest(var node)
signal stopComputeRequest(var node)
signal deleteDataRequest(var node)
signal submitRequest(var node)

SystemPalette { id: activePalette }

/**
* Get the node delegate
*/
function nodeDelegate(node) {
if (!nodeRepeater)
return null
for (var i = 0; i < nodeRepeater.count; ++i) {
if (nodeRepeater.itemAt(i).node === node)
return nodeRepeater.itemAt(i)
}
return null
}

enum ButtonState {
DISABLED = 0,
LAUNCHABLE = 1,
DELETABLE = 2,
STOPPABLE = 3
}

Rectangle {
id: actionHeader

readonly property bool hasSelectedNode: uigraph && uigraph.nodeSelection.selectedIndexes.length === 1
readonly property var selectedNode: hasSelectedNode ? uigraph.selectedNode : null
readonly property var selectedNodeDelegate: selectedNode ? root.nodeDelegate(selectedNode) : null

visible: selectedNodeDelegate !== null
color: "transparent"
width: actionItemsRow.width
height: actionItemsRow.height

//
// ===== Manage NodeActions position =====
//

// Prevents losing focus on the node when we click on buttons of the actionItems
MouseArea {
anchors.fill: parent
onPressed: function(mouse) { mouse.accepted = true }
onReleased: function(mouse) { mouse.accepted = true }
onClicked: function(mouse) { mouse.accepted = true }
onDoubleClicked: function(mouse) { mouse.accepted = true }
hoverEnabled: false
}

// Update position
function updatePosition() {
if (!selectedNodeDelegate || !draggable) return
// Calculate node position in screen coordinates
const nodeScreenX = selectedNodeDelegate.x * draggable.scale + draggable.x
const nodeScreenY = selectedNodeDelegate.y * draggable.scale + draggable.y
// Position header above the node (fixed offset in screen pixels)
x = nodeScreenX + (selectedNodeDelegate.width * draggable.scale - width) / 2
y = nodeScreenY - height - headerOffset
}

onWidthChanged: {
updatePosition()
}

// Update position when the user moves on the graph
Connections {
target: root.draggable
function onXChanged() { actionHeader.updatePosition() }
function onYChanged() { actionHeader.updatePosition() }
function onScaleChanged() { actionHeader.updatePosition() }
}

// Update position when nodes are moved
Connections {
target: actionHeader.selectedNodeDelegate
function onXChanged() { actionHeader.updatePosition() }
function onYChanged() { actionHeader.updatePosition() }
ignoreUnknownSignals: true
}

//
// ===== Manage buttons =====
//

property bool nodeIsLocked: false
property bool canComputeNode: false
property bool canStopNode: false
property bool canRestartNode: false
property bool canSubmitNode: false
property bool nodeSubmitted: false

property int computeButtonState: NodeActions.ButtonState.LAUNCHABLE
property string computeButtonIcon: {
switch (computeButtonState) {
case NodeActions.ButtonState.STOPPABLE: return MaterialIcons.cancel_schedule_send
default: return MaterialIcons.send
}
}
property int submitButtonState: NodeActions.ButtonState.LAUNCHABLE

function getComputeButtonState(node) {
if (actionHeader.canStopNode)
return NodeActions.ButtonState.STOPPABLE
if (!actionHeader.nodeIsLocked && node.globalStatus == "SUCCESS")
return NodeActions.ButtonState.DELETABLE
if (actionHeader.canComputeNode)
return NodeActions.ButtonState.LAUNCHABLE
return NodeActions.ButtonState.DISABLED
}

function getSubmitButtonState(node) {
if (actionHeader.nodeIsLocked || actionHeader.canStopNode)
return NodeActions.ButtonState.DISABLED
if (!actionHeader.nodeIsLocked && node.globalStatus == "SUCCESS")
return NodeActions.ButtonState.DISABLED
if (actionHeader.canSubmitNode)
return NodeActions.ButtonState.LAUNCHABLE
return NodeActions.ButtonState.DISABLED
}

function isSubmittedExternally(node) {
return node.globalExecMode == "EXTERN" && ["RUNNING", "SUBMITTED"].includes(node.globalStatus)
}

function isNodeRestartable(node) {
return actionHeader.computeButtonState == NodeActions.ButtonState.LAUNCHABLE &&
["ERROR", "STOPPED", "KILLED"].includes(node.globalStatus)
}

function updateProperties(node) {
if (!node) return
// Update properties values
actionHeader.canComputeNode = uigraph.canComputeNode(node)
actionHeader.canSubmitNode = uigraph.canSubmitNode(node)
actionHeader.canStopNode = node.canBeStopped() || node.canBeCanceled()
actionHeader.nodeIsLocked = node.locked
actionHeader.nodeSubmitted = isSubmittedExternally(node)
// Update button states
actionHeader.computeButtonState = getComputeButtonState(node)
actionHeader.submitButtonState = getSubmitButtonState(node)
actionHeader.canRestartNode = isNodeRestartable(node)
}

// Set initial state & position
onSelectedNodeDelegateChanged: {
if (actionHeader.selectedNode) {
actionHeader.updateProperties(actionHeader.selectedNode)
Qt.callLater(actionHeader.updatePosition)
}
}

// Listen to updates to status
Connections {
target: actionHeader.selectedNode
function onGlobalStatusChanged() {
actionHeader.updateProperties(target)
}
function onLockedChanged() {
actionHeader.nodeIsLocked = target.locked
}
ignoreUnknownSignals: true
}

// Listen to updates from nodes that are not selected
Connections {
target: root.uigraph
function onComputingChanged() {
actionHeader.updateProperties(actionHeader.selectedNode)
}
ignoreUnknownSignals: true
}

Row {
id: actionItemsRow
anchors.centerIn: parent
spacing: 2

// Compute button
MaterialToolButton {
id: computeButton
font.pointSize: 16
text: actionHeader.computeButtonIcon
padding: 6
ToolTip.text: "Start/Stop/Restart Compute"
ToolTip.visible: hovered
ToolTip.delay: 1000
visible: actionHeader.computeButtonState != NodeActions.ButtonState.DELETABLE
enabled: actionHeader.computeButtonState % 2 == 1 // Launchable & Stoppable
background: Rectangle {
color: {
if (!computeButton.enabled) {
if (actionHeader.nodeSubmitted)
return Qt.darker(Colors.statusColors["SUBMITTED"], 1.2)
return activePalette.button
}
if (actionHeader.computeButtonState == NodeActions.ButtonState.STOPPABLE)
return computeButton.hovered ? Colors.orange : Qt.darker(Colors.orange, 1.3)
return computeButton.hovered ? activePalette.highlight : activePalette.button
}
opacity: computeButton.hovered ? 1 : root._opacity
border.color: computeButton.hovered ? activePalette.highlight : Qt.darker(activePalette.window, 1.3)
border.width: 1
radius: 3
}
onClicked: {
switch (actionHeader.computeButtonState) {
case NodeActions.ButtonState.STOPPABLE:
root.stopComputeRequest(actionHeader.selectedNode)
break
case NodeActions.ButtonState.LAUNCHABLE:
root.computeRequest(actionHeader.selectedNode)
break
}
}
}

// Clear node
MaterialToolButton {
id: deleteDataButton
font.pointSize: 16
text: MaterialIcons.delete_
padding: 6
ToolTip.text: "Delete data"
ToolTip.visible: hovered
ToolTip.delay: 1000
visible: actionHeader.canRestartNode || actionHeader.computeButtonState == NodeActions.ButtonState.DELETABLE
enabled: visible
background: Rectangle {
color: computeButton.hovered ? Colors.red : Qt.darker(Colors.red, 1.3)
opacity: computeButton.hovered ? 1 : root._opacity
border.color: computeButton.hovered ? activePalette.highlight : Qt.darker(activePalette.window, 1.3)
border.width: 1
radius: 3
}
onClicked: {
root.deleteDataRequest(actionHeader.selectedNode)
}
}

// Submit button
MaterialToolButton {
id: submitButton
font.pointSize: 16
text: MaterialIcons.rocket_launch
padding: 6
ToolTip.text: "Submit on Render Farm"
ToolTip.visible: hovered
ToolTip.delay: 1000
visible: root.uigraph ? root.uigraph.canSubmit : false
enabled: actionHeader.submitButtonState != NodeActions.ButtonState.DISABLED
background: Rectangle {
color: {
if (!submitButton.enabled) {
if (actionHeader.nodeSubmitted)
return Qt.darker(Colors.statusColors["SUBMITTED"], 1.2)
return activePalette.button
}
return submitButton.hovered ? activePalette.highlight : activePalette.button
}
opacity: submitButton.hovered ? 1 : root._opacity
border.color: submitButton.hovered ? activePalette.highlight : Qt.darker(activePalette.window, 1.3)
border.width: 1
radius: 3
}
onClicked: {
if (actionHeader.selectedNode) {
root.submitRequest(actionHeader.selectedNode)
}
}
}
}
}
}
1 change: 1 addition & 0 deletions meshroom/ui/qml/Controls/qmldir
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ SelectionLine 1.0 SelectionLine.qml
DelegateSelectionBox 1.0 DelegateSelectionBox.qml
DelegateSelectionLine 1.0 DelegateSelectionLine.qml
StatusBar 1.0 StatusBar.qml
NodeActions 1.0 NodeActions.qml
28 changes: 28 additions & 0 deletions meshroom/ui/qml/GraphEditor/GraphEditor.qml
Original file line number Diff line number Diff line change
Expand Up @@ -1075,6 +1075,34 @@ Item {
}
}

NodeActions {
id: nodeActions
uigraph: root.uigraph
draggable: draggable
nodeRepeater: nodeRepeater
anchors.fill: parent

onComputeRequest: function(node) {
root.computeRequest([node])
}

onStopComputeRequest: function(node) {
if (node.canBeStopped()) {
uigraph.stopNodeComputation(node)
} else if (node.canBeCanceled()) {
uigraph.cancelNodeComputation(node)
}
}

onDeleteDataRequest: function(node) {
uigraph.clearSelectedNodesData();
}

onSubmitRequest: function(node) {
root.submitRequest([node])
}
}

MessageDialog {
id: errorDialog

Expand Down