Skip to content

Commit ef95a3e

Browse files
committed
[ui] Add NodeActions qml element to re-launch computation
1 parent c2f3115 commit ef95a3e

File tree

3 files changed

+310
-0
lines changed

3 files changed

+310
-0
lines changed
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
import QtQuick
2+
import QtQuick.Controls
3+
import QtQuick.Layouts
4+
5+
import MaterialIcons 2.2
6+
import Utils 1.0
7+
8+
9+
Item {
10+
id: root
11+
12+
// Settings
13+
property real headerOffset: 10 // Distance above the node in screen pixels
14+
property real _opacity: 0.9
15+
16+
// Objects passed from the graph editor
17+
property var uigraph: null
18+
property var draggable: null // The draggable container from GraphEditor
19+
property var nodeRepeater: null // Reference to nodeRepeater to find delegates
20+
21+
// Signals
22+
signal computeRequest(var node)
23+
signal stopComputeRequest(var node)
24+
signal reComputeRequest(var node)
25+
signal submitRequest(var node)
26+
signal reSubmitRequest(var node)
27+
28+
SystemPalette { id: activePalette }
29+
30+
/**
31+
* Get the node delegate
32+
*/
33+
function nodeDelegate(node) {
34+
if (!nodeRepeater)
35+
return null
36+
37+
for(var i = 0; i < nodeRepeater.count; ++i) {
38+
if (nodeRepeater.itemAt(i).node === node)
39+
return nodeRepeater.itemAt(i)
40+
}
41+
42+
return null
43+
}
44+
45+
Rectangle {
46+
id: actionHeader
47+
48+
function hasSelectedNode() {
49+
return uigraph && uigraph.nodeSelection.selectedIndexes.length===1
50+
}
51+
52+
readonly property var selectedNode: hasSelectedNode() ? uigraph.selectedNode : null
53+
readonly property var selectedNodeDelegate: selectedNode ? root.nodeDelegate(selectedNode) : null
54+
55+
visible: selectedNodeDelegate !== null
56+
color: "transparent"
57+
width: actionItemsRow.width
58+
height: actionItemsRow.height
59+
60+
// Properties depending on selectedNode
61+
readonly property string currentExecMode: selectedNode ? selectedNode.globalExecMode : "NONE"
62+
readonly property string currentStatus: selectedNode ? selectedNode.globalStatus : "NONE"
63+
readonly property bool nodeCanBeStopped: selectedNode ? selectedNode.canBeStopped() : false
64+
readonly property bool nodeIsExternal: selectedNode ? selectedNode.isExternal : false
65+
readonly property bool nodeLocked: selectedNode ? selectedNode.locked : false
66+
67+
Connections {
68+
target: actionHeader.selectedNode
69+
function onGlobalStatusChanged() {
70+
actionHeader.currentStatusChanged()
71+
}
72+
function onGlobalExecModeChanged() {
73+
actionHeader.currentExecModeChanged()
74+
}
75+
ignoreUnknownSignals: true
76+
}
77+
78+
// Prevents losing focus on the node when we click on buttons of the actionItems
79+
MouseArea {
80+
anchors.fill: parent
81+
// Consume all mouse events to prevent propagation to GraphEditor
82+
onPressed: function(mouse) { mouse.accepted = true }
83+
onReleased: function(mouse) { mouse.accepted = true }
84+
onClicked: function(mouse) { mouse.accepted = true }
85+
onDoubleClicked: function(mouse) { mouse.accepted = true }
86+
// Allow the buttons to receive hover events
87+
hoverEnabled: false
88+
}
89+
90+
// Update position
91+
function updatePosition() {
92+
if (!selectedNodeDelegate || !draggable) return
93+
94+
// Calculate node position in screen coordinates
95+
const nodeScreenX = selectedNodeDelegate.x * draggable.scale + draggable.x
96+
const nodeScreenY = selectedNodeDelegate.y * draggable.scale + draggable.y
97+
98+
// Position header above the node (fixed offset in screen pixels)
99+
x = nodeScreenX + (selectedNodeDelegate.width * draggable.scale - width) / 2
100+
y = nodeScreenY - height - headerOffset
101+
}
102+
103+
// Update position when the user moves on the graph
104+
Connections {
105+
target: root.draggable
106+
function onXChanged() { actionHeader.updatePosition() }
107+
function onYChanged() { actionHeader.updatePosition() }
108+
function onScaleChanged() { actionHeader.updatePosition() }
109+
}
110+
111+
// Update position when nodes are moved
112+
Connections {
113+
target: actionHeader.selectedNodeDelegate
114+
function onXChanged() { actionHeader.updatePosition() }
115+
function onYChanged() { actionHeader.updatePosition() }
116+
ignoreUnknownSignals: true
117+
}
118+
119+
// Initial position update
120+
onSelectedNodeDelegateChanged: updatePosition()
121+
122+
function isRunningExternally() {
123+
return actionHeader.currentExecMode === "EXTERN" && ["SUBMITTED", "RUNNING"].includes(actionHeader.currentStatus)
124+
}
125+
126+
function isRunningLocally() {
127+
if (!actionHeader.selectedNode) return false
128+
if (actionHeader.nodeIsExternal) return false
129+
return actionHeader.currentStatus === "RUNNING"
130+
}
131+
132+
function canBeStopped() {
133+
if (!actionHeader.selectedNode) return false
134+
if (actionHeader.currentStatus !== "RUNNING") return false
135+
return actionHeader.nodeCanBeStopped
136+
}
137+
138+
function canBeLaunched() {
139+
if (!actionHeader.selectedNode) return false
140+
if (actionHeader.nodeCanBeStopped) return true
141+
return ["NONE", "STOPPED", "KILLED", "ERROR"].includes(actionHeader.currentStatus)
142+
}
143+
144+
Row {
145+
id: actionItemsRow
146+
anchors.centerIn: parent
147+
spacing: 2
148+
149+
// Compute button
150+
MaterialToolButton {
151+
id: computeButton
152+
font.pointSize: 16
153+
text: actionHeader.canBeStopped() ? MaterialIcons.cancel_schedule_send : MaterialIcons.send
154+
padding: 6
155+
ToolTip.text: "Start/Stop Compute"
156+
ToolTip.visible: hovered
157+
ToolTip.delay: 1000
158+
enabled: actionHeader.selectedNode && actionHeader.canBeLaunched()
159+
background: Rectangle {
160+
color: {
161+
if (!computeButton.enabled) return activePalette.button
162+
if (actionHeader.currentStatus === "RUNNING") {
163+
if (computeButton.hovered) return Colors.statusColors["STOPPED"]
164+
return Qt.darker(Colors.statusColors["STOPPED"], 1.3)
165+
} else {
166+
if (computeButton.hovered) return activePalette.highlight
167+
return activePalette.button
168+
}
169+
}
170+
opacity: computeButton.hovered ? 1 : root._opacity
171+
border.color: computeButton.hovered ? activePalette.highlight : Qt.darker(activePalette.window, 1.3)
172+
border.width: 1
173+
radius: 3
174+
}
175+
onClicked: {
176+
if (actionHeader.isRunningLocally()) {
177+
root.stopComputeRequest(actionHeader.selectedNode)
178+
} else {
179+
root.computeRequest(actionHeader.selectedNode)
180+
}
181+
}
182+
}
183+
184+
// Re-compute button : stop local process and relaunch locally
185+
MaterialToolButton {
186+
id: reComputeButton
187+
font.pointSize: 16
188+
text: MaterialIcons.autorenew
189+
padding: 6
190+
ToolTip.text: "Re-compute"
191+
ToolTip.visible: hovered
192+
ToolTip.delay: 1000
193+
enabled: actionHeader.selectedNode && !actionHeader.isRunningExternally()
194+
background: Rectangle {
195+
color: {
196+
if (!reComputeButton.enabled) return activePalette.button
197+
if (reComputeButton.hovered) return activePalette.highlight;
198+
return activePalette.button;
199+
}
200+
opacity: reComputeButton.hovered ? 1 : root._opacity
201+
border.color: reComputeButton.hovered ? activePalette.highlight : Qt.darker(activePalette.window, 1.3)
202+
border.width: 1
203+
radius: 3
204+
}
205+
onClicked: {
206+
if (actionHeader.selectedNode) {
207+
root.reComputeRequest(actionHeader.selectedNode)
208+
}
209+
}
210+
}
211+
212+
// Submit button
213+
MaterialToolButton {
214+
id: submitButton
215+
font.pointSize: 16
216+
text: MaterialIcons.rocket_launch
217+
padding: 6
218+
ToolTip.text: "Submit on Render Farm"
219+
ToolTip.visible: hovered
220+
ToolTip.delay: 1000
221+
visible: root.uigraph ? root.uigraph.canSubmit : false
222+
enabled: actionHeader.selectedNode ? !actionHeader.selectedNode.locked : false
223+
224+
background: Rectangle {
225+
color: {
226+
if (!submitButton.enabled) return activePalette.button
227+
if (actionHeader.isRunningExternally()) return Colors.statusColors["SUBMITTED"];
228+
if (submitButton.hovered) return activePalette.highlight;
229+
return activePalette.button;
230+
}
231+
opacity: submitButton.hovered ? 1 : root._opacity
232+
border.color: submitButton.hovered ? activePalette.highlight : Qt.darker(activePalette.window, 1.3)
233+
border.width: 1
234+
radius: 3
235+
}
236+
onClicked: {
237+
if (actionHeader.selectedNode) {
238+
root.submitRequest(actionHeader.selectedNode)
239+
}
240+
}
241+
}
242+
243+
// Re-submit button : stop everything and relaunch on farm
244+
// TODO : disabled for now because we can't stop jobs submitted on farm
245+
MaterialToolButton {
246+
id: reSubmitButton
247+
font.pointSize: 16
248+
text: MaterialIcons.double_arrow
249+
padding: 6
250+
ToolTip.text: "Re-submit on Render Farm"
251+
ToolTip.visible: hovered
252+
ToolTip.delay: 1000
253+
visible: false // root.uigraph ? root.uigraph.canSubmit : false
254+
enabled: actionHeader.selectedNode !== null
255+
background: Rectangle {
256+
color: {
257+
if (!actionHeader.selectedNode) return activePalette.button
258+
if (reSubmitButton.hovered) return activePalette.highlight;
259+
return activePalette.button;
260+
}
261+
opacity: reSubmitButton.hovered ? 1 : root._opacity
262+
border.color: reSubmitButton.hovered ? activePalette.highlight : Qt.darker(activePalette.window, 1.3)
263+
border.width: 1
264+
radius: 3
265+
}
266+
onClicked: {
267+
if (actionHeader.selectedNode) {
268+
root.reSubmitRequest(actionHeader.selectedNode)
269+
}
270+
}
271+
}
272+
}
273+
}
274+
}

meshroom/ui/qml/Controls/qmldir

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ SelectionBox 1.0 SelectionBox.qml
2121
SelectionLine 1.0 SelectionLine.qml
2222
DelegateSelectionBox 1.0 DelegateSelectionBox.qml
2323
DelegateSelectionLine 1.0 DelegateSelectionLine.qml
24+
NodeActions 1.0 NodeActions.qml

meshroom/ui/qml/GraphEditor/GraphEditor.qml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1075,6 +1075,41 @@ Item {
10751075
}
10761076
}
10771077

1078+
NodeActions {
1079+
id: nodeActions
1080+
uigraph: root.uigraph
1081+
draggable: draggable
1082+
nodeRepeater: nodeRepeater
1083+
anchors.fill: parent
1084+
1085+
onComputeRequest: function(node) {
1086+
root.computeRequest([node])
1087+
}
1088+
1089+
onStopComputeRequest: function(node) {
1090+
uigraph.stopNodeComputation(node)
1091+
}
1092+
1093+
onReComputeRequest: function(node) {
1094+
// Only triggered if the node is already computed
1095+
// so we don't have to check if we should erase the data
1096+
if (node.canBeStopped) uigraph.stopNodeComputation(node)
1097+
uigraph.clearSelectedNodesData();
1098+
root.computeRequest([node])
1099+
}
1100+
1101+
onSubmitRequest: function(node) {
1102+
if (node.isComputed) uigraph.clearSelectedNodesData();
1103+
root.submitRequest([node])
1104+
}
1105+
1106+
// TODO : If we want this, we should add the possibility to stop job on farm first
1107+
// onReSubmitRequest: function(node) {
1108+
// uigraph.clearSelectedNodesData();
1109+
// root.submitRequest([node])
1110+
// }
1111+
}
1112+
10781113
MessageDialog {
10791114
id: errorDialog
10801115

0 commit comments

Comments
 (0)