1+ import QtQuick
2+ import QtQuick.Controls
3+ import QtQuick.Layouts
4+
5+ import MaterialIcons 2.2
6+ import Utils 1.0
7+
8+ Item {
9+ id: root
10+
11+ // Settings
12+ readonly property real headerOffset: 10 // Distance above the node in screen pixels
13+ readonly property real _opacity: 0.9
14+
15+ // Objects passed from the graph editor
16+ property var uigraph: null
17+ property var draggable: null // The draggable container from GraphEditor
18+ property var nodeRepeater: null // Reference to nodeRepeater to find delegates
19+
20+ // Signals
21+ signal computeRequest (var node)
22+ signal stopComputeRequest (var node)
23+ signal deleteDataRequest (var node)
24+ signal submitRequest (var node)
25+
26+ SystemPalette { id: activePalette }
27+
28+ /**
29+ * Get the node delegate
30+ */
31+ function nodeDelegate (node ) {
32+ if (! nodeRepeater)
33+ return null
34+ for (var i = 0 ; i < nodeRepeater .count ; ++ i) {
35+ if (nodeRepeater .itemAt (i).node === node)
36+ return nodeRepeater .itemAt (i)
37+ }
38+ return null
39+ }
40+
41+ enum ButtonState {
42+ DISABLED = 0 ,
43+ LAUNCHABLE = 1 ,
44+ DELETABLE = 2 ,
45+ STOPPABLE = 3
46+ }
47+
48+ Rectangle {
49+ id: actionHeader
50+
51+ readonly property bool hasSelectedNode: uigraph && uigraph .nodeSelection .selectedIndexes .length === 1
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+ //
61+ // ===== Manage NodeActions position =====
62+ //
63+
64+ // Prevents losing focus on the node when we click on buttons of the actionItems
65+ MouseArea {
66+ anchors .fill : parent
67+ onPressed : function (mouse ) { mouse .accepted = true }
68+ onReleased : function (mouse ) { mouse .accepted = true }
69+ onClicked : function (mouse ) { mouse .accepted = true }
70+ onDoubleClicked : function (mouse ) { mouse .accepted = true }
71+ hoverEnabled: false
72+ }
73+
74+ // Update position
75+ function updatePosition () {
76+ if (! selectedNodeDelegate || ! draggable) return
77+ // Calculate node position in screen coordinates
78+ const nodeScreenX = selectedNodeDelegate .x * draggable .scale + draggable .x
79+ const nodeScreenY = selectedNodeDelegate .y * draggable .scale + draggable .y
80+ // Position header above the node (fixed offset in screen pixels)
81+ x = nodeScreenX + (selectedNodeDelegate .width * draggable .scale - width) / 2
82+ y = nodeScreenY - height - headerOffset
83+ }
84+
85+ onWidthChanged: {
86+ updatePosition ()
87+ }
88+
89+ // Update position when the user moves on the graph
90+ Connections {
91+ target: root .draggable
92+ function onXChanged () { actionHeader .updatePosition () }
93+ function onYChanged () { actionHeader .updatePosition () }
94+ function onScaleChanged () { actionHeader .updatePosition () }
95+ }
96+
97+ // Update position when nodes are moved
98+ Connections {
99+ target: actionHeader .selectedNodeDelegate
100+ function onXChanged () { actionHeader .updatePosition () }
101+ function onYChanged () { actionHeader .updatePosition () }
102+ ignoreUnknownSignals: true
103+ }
104+
105+ //
106+ // ===== Manage buttons =====
107+ //
108+
109+ property bool nodeIsLocked: false
110+ property bool canComputeNode: false
111+ property bool canStopNode: false
112+ property bool canRestartNode: false
113+ property bool canSubmitNode: false
114+ property bool nodeSubmitted: false
115+
116+ property int computeButtonState: NodeActions .ButtonState .LAUNCHABLE
117+ property string computeButtonIcon: {
118+ switch (computeButtonState) {
119+ case NodeActions .ButtonState .STOPPABLE : return MaterialIcons .cancel_schedule_send
120+ default : return MaterialIcons .send
121+ }
122+ }
123+ property int submitButtonState: NodeActions .ButtonState .LAUNCHABLE
124+
125+ function getComputeButtonState (node ) {
126+ if (actionHeader .canStopNode )
127+ return NodeActions .ButtonState .STOPPABLE
128+ if (! actionHeader .nodeIsLocked && node .globalStatus == " SUCCESS" )
129+ return NodeActions .ButtonState .DELETABLE
130+ if (actionHeader .canComputeNode )
131+ return NodeActions .ButtonState .LAUNCHABLE
132+ return NodeActions .ButtonState .DISABLED
133+ }
134+
135+ function getSubmitButtonState (node ) {
136+ if (actionHeader .nodeIsLocked || actionHeader .canStopNode )
137+ return NodeActions .ButtonState .DISABLED
138+ if (! actionHeader .nodeIsLocked && node .globalStatus == " SUCCESS" )
139+ return NodeActions .ButtonState .DISABLED
140+ if (actionHeader .canSubmitNode )
141+ return NodeActions .ButtonState .LAUNCHABLE
142+ return NodeActions .ButtonState .DISABLED
143+ }
144+
145+ function isSubmittedExternally (node ) {
146+ return node .globalExecMode == " EXTERN" && [" RUNNING" , " SUBMITTED" ].includes (node .globalStatus )
147+ }
148+
149+ function isNodeRestartable (node ) {
150+ return actionHeader .computeButtonState == NodeActions .ButtonState .LAUNCHABLE &&
151+ [" ERROR" , " STOPPED" , " KILLED" ].includes (node .globalStatus )
152+ }
153+
154+ function updateProperties (node ) {
155+ if (! node) return
156+ // Update properties values
157+ actionHeader .canComputeNode = uigraph .canComputeNode (node)
158+ actionHeader .canSubmitNode = uigraph .canSubmitNode (node)
159+ actionHeader .canStopNode = node .canBeStopped () || node .canBeCanceled ()
160+ actionHeader .nodeIsLocked = node .locked
161+ actionHeader .nodeSubmitted = isSubmittedExternally (node)
162+ // Update button states
163+ actionHeader .computeButtonState = getComputeButtonState (node)
164+ actionHeader .submitButtonState = getSubmitButtonState (node)
165+ actionHeader .canRestartNode = isNodeRestartable (node)
166+ }
167+
168+ // Set initial state & position
169+ onSelectedNodeDelegateChanged: {
170+ if (actionHeader .selectedNode ) {
171+ actionHeader .updateProperties (actionHeader .selectedNode )
172+ Qt .callLater (actionHeader .updatePosition )
173+ }
174+ }
175+
176+ // Listen to updates to status
177+ Connections {
178+ target: actionHeader .selectedNode
179+ function onGlobalStatusChanged () {
180+ actionHeader .updateProperties (target)
181+ }
182+ function onLockedChanged () {
183+ actionHeader .nodeIsLocked = target .locked
184+ }
185+ ignoreUnknownSignals: true
186+ }
187+
188+ // Listen to updates from nodes that are not selected
189+ Connections {
190+ target: root .uigraph
191+ function onComputingChanged () {
192+ actionHeader .updateProperties (actionHeader .selectedNode )
193+ }
194+ ignoreUnknownSignals: true
195+ }
196+
197+ Row {
198+ id: actionItemsRow
199+ anchors .centerIn : parent
200+ spacing: 2
201+
202+ // Compute button
203+ MaterialToolButton {
204+ id: computeButton
205+ font .pointSize : 16
206+ text: actionHeader .computeButtonIcon
207+ padding: 6
208+ ToolTip .text : " Start/Stop/Restart Compute"
209+ ToolTip .visible : hovered
210+ ToolTip .delay : 1000
211+ visible: actionHeader .computeButtonState != NodeActions .ButtonState .DELETABLE
212+ enabled: actionHeader .computeButtonState % 2 == 1 // Launchable & Stoppable
213+ background: Rectangle {
214+ color: {
215+ if (! computeButton .enabled ) {
216+ if (actionHeader .nodeSubmitted )
217+ return Qt .darker (Colors .statusColors [" SUBMITTED" ], 1.2 )
218+ return activePalette .button
219+ }
220+ if (actionHeader .computeButtonState == NodeActions .ButtonState .STOPPABLE )
221+ return computeButton .hovered ? Colors .orange : Qt .darker (Colors .orange , 1.3 )
222+ return computeButton .hovered ? activePalette .highlight : activePalette .button
223+ }
224+ opacity: computeButton .hovered ? 1 : root ._opacity
225+ border .color : computeButton .hovered ? activePalette .highlight : Qt .darker (activePalette .window , 1.3 )
226+ border .width : 1
227+ radius: 3
228+ }
229+ onClicked: {
230+ switch (actionHeader .computeButtonState ) {
231+ case NodeActions .ButtonState .STOPPABLE :
232+ root .stopComputeRequest (actionHeader .selectedNode )
233+ break
234+ case NodeActions .ButtonState .LAUNCHABLE :
235+ root .computeRequest (actionHeader .selectedNode )
236+ break
237+ }
238+ }
239+ }
240+
241+ // Clear node
242+ MaterialToolButton {
243+ id: deleteDataButton
244+ font .pointSize : 16
245+ text: MaterialIcons .delete_
246+ padding: 6
247+ ToolTip .text : " Delete data"
248+ ToolTip .visible : hovered
249+ ToolTip .delay : 1000
250+ visible: actionHeader .canRestartNode || actionHeader .computeButtonState == NodeActions .ButtonState .DELETABLE
251+ enabled: visible
252+ background: Rectangle {
253+ color: computeButton .hovered ? Colors .red : Qt .darker (Colors .red , 1.3 )
254+ opacity: computeButton .hovered ? 1 : root ._opacity
255+ border .color : computeButton .hovered ? activePalette .highlight : Qt .darker (activePalette .window , 1.3 )
256+ border .width : 1
257+ radius: 3
258+ }
259+ onClicked: {
260+ root .deleteDataRequest (actionHeader .selectedNode )
261+ }
262+ }
263+
264+ // Submit button
265+ MaterialToolButton {
266+ id: submitButton
267+ font .pointSize : 16
268+ text: MaterialIcons .rocket_launch
269+ padding: 6
270+ ToolTip .text : " Submit on Render Farm"
271+ ToolTip .visible : hovered
272+ ToolTip .delay : 1000
273+ visible: root .uigraph ? root .uigraph .canSubmit : false
274+ enabled: actionHeader .submitButtonState != NodeActions .ButtonState .DISABLED
275+ background: Rectangle {
276+ color: {
277+ if (! submitButton .enabled ) {
278+ if (actionHeader .nodeSubmitted )
279+ return Qt .darker (Colors .statusColors [" SUBMITTED" ], 1.2 )
280+ return activePalette .button
281+ }
282+ return submitButton .hovered ? activePalette .highlight : activePalette .button
283+ }
284+ opacity: submitButton .hovered ? 1 : root ._opacity
285+ border .color : submitButton .hovered ? activePalette .highlight : Qt .darker (activePalette .window , 1.3 )
286+ border .width : 1
287+ radius: 3
288+ }
289+ onClicked: {
290+ if (actionHeader .selectedNode ) {
291+ root .submitRequest (actionHeader .selectedNode )
292+ }
293+ }
294+ }
295+ }
296+ }
297+ }
0 commit comments