Skip to content

Commit 8a02963

Browse files
authored
[cuegui] feat: Add job node graph plugin v2 (#1400)
**Link the Issue(s) this Pull Request is related to.** #888 (original PR) **Summarize your change.** This is an adapted PR to support QtPy and PySide6 It depends a fork of NodeGraphQt that has been adapted to use QtPy instead of Qt directly.
1 parent 0fa5d6d commit 8a02963

25 files changed

+829
-4
lines changed

cuegui/cuegui/AbstractGraphWidget.py

+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Copyright Contributors to the OpenCue Project
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
"""Base class for CueGUI graph widgets."""
17+
18+
from qtpy import QtCore
19+
from qtpy import QtWidgets
20+
21+
from NodeGraphQtPy import NodeGraph
22+
from NodeGraphQtPy.errors import NodeRegistrationError
23+
from cuegui.nodegraph import CueLayerNode
24+
from cuegui import app
25+
26+
27+
class AbstractGraphWidget(QtWidgets.QWidget):
28+
"""Base class for CueGUI graph widgets"""
29+
30+
def __init__(self, parent=None):
31+
super(AbstractGraphWidget, self).__init__(parent=parent)
32+
self.graph = NodeGraph()
33+
self.setupUI()
34+
35+
self.timer = QtCore.QTimer(self)
36+
# pylint: disable=no-member
37+
self.timer.timeout.connect(self.update)
38+
self.timer.setInterval(1000 * 20)
39+
40+
self.graph.node_selection_changed.connect(self.onNodeSelectionChanged)
41+
app().quit.connect(self.timer.stop)
42+
43+
def setupUI(self):
44+
"""Setup the UI."""
45+
try:
46+
self.graph.register_node(CueLayerNode)
47+
except NodeRegistrationError:
48+
pass
49+
self.graph.viewer().installEventFilter(self)
50+
51+
layout = QtWidgets.QVBoxLayout(self)
52+
layout.addWidget(self.graph.viewer())
53+
54+
def onNodeSelectionChanged(self):
55+
"""Slot run when a node is selected.
56+
57+
Updates the nodes to ensure they're visualising current data.
58+
Can be used to notify other widgets of object selection.
59+
"""
60+
self.update()
61+
62+
def handleSelectObjects(self, rpcObjects):
63+
"""Select incoming objects in graph.
64+
"""
65+
received = [o.name() for o in rpcObjects]
66+
current = [rpcObject.name() for rpcObject in self.selectedObjects()]
67+
if received == current:
68+
# prevent recursing
69+
return
70+
71+
for node in self.graph.all_nodes():
72+
node.set_selected(False)
73+
for rpcObject in rpcObjects:
74+
node = self.graph.get_node_by_name(rpcObject.name())
75+
node.set_selected(True)
76+
77+
def selectedObjects(self):
78+
"""Return the selected nodes rpcObjects in the graph.
79+
:rtype: [opencue.wrappers.layer.Layer]
80+
:return: List of selected layers
81+
"""
82+
rpcObjects = [n.rpcObject for n in self.graph.selected_nodes()]
83+
return rpcObjects
84+
85+
def eventFilter(self, target, event):
86+
"""Override eventFilter
87+
88+
Centre nodes in graph viewer on 'F' key press.
89+
90+
@param target: widget event occurred on
91+
@type target: QtWidgets.QWidget
92+
@param event: Qt event
93+
@type event: QtCore.QEvent
94+
"""
95+
if hasattr(self, "graph"):
96+
viewer = self.graph.viewer()
97+
if target == viewer:
98+
if event.type() == QtCore.QEvent.KeyPress:
99+
if event.key() == QtCore.Qt.Key_F:
100+
self.graph.center_on()
101+
if event.key() == QtCore.Qt.Key_L:
102+
self.graph.auto_layout_nodes()
103+
104+
return super(AbstractGraphWidget, self).eventFilter(target, event)
105+
106+
def clearGraph(self):
107+
"""Clear all nodes from the graph
108+
"""
109+
for node in self.graph.all_nodes():
110+
for port in node.output_ports():
111+
port.unlock()
112+
for port in node.input_ports():
113+
port.unlock()
114+
self.graph.clear_session()
115+
116+
def createGraph(self):
117+
"""Create the graph to visualise OpenCue objects
118+
"""
119+
raise NotImplementedError()
120+
121+
def update(self):
122+
"""Update nodes with latest data
123+
124+
This is run every 20 seconds by the timer.
125+
"""
126+
raise NotImplementedError()

cuegui/cuegui/App.py

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class CueGuiApplication(QtWidgets.QApplication):
4040
request_update = QtCore.Signal()
4141
status = QtCore.Signal()
4242
quit = QtCore.Signal()
43+
select_layers = QtCore.Signal(list)
4344

4445
# Thread pool
4546
threadpool = None

cuegui/cuegui/JobMonitorGraph.py

+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# Copyright Contributors to the OpenCue Project
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
"""Node graph to display Layers of a Job"""
17+
18+
19+
from qtpy import QtWidgets
20+
21+
import cuegui.Utils
22+
import cuegui.MenuActions
23+
from cuegui.nodegraph import CueLayerNode
24+
from cuegui.AbstractGraphWidget import AbstractGraphWidget
25+
26+
27+
class JobMonitorGraph(AbstractGraphWidget):
28+
"""Graph widget to display connections of layers in a job"""
29+
30+
def __init__(self, parent=None):
31+
super(JobMonitorGraph, self).__init__(parent=parent)
32+
self.job = None
33+
self.setupContextMenu()
34+
35+
# wire signals
36+
cuegui.app().select_layers.connect(self.handleSelectObjects)
37+
38+
def onNodeSelectionChanged(self):
39+
"""Notify other widgets of Layer selection.
40+
41+
Emit signal to notify other widgets of Layer selection, this keeps
42+
all widgets with selectable Layers in sync with each other.
43+
44+
Also force updates the nodes, as the timed updates are infrequent.
45+
"""
46+
self.update()
47+
layers = self.selectedObjects()
48+
cuegui.app().select_layers.emit(layers)
49+
50+
def setupContextMenu(self):
51+
"""Setup context menu for nodes in node graph"""
52+
self.__menuActions = cuegui.MenuActions.MenuActions(
53+
self, self.update, self.selectedObjects, self.getJob
54+
)
55+
56+
menu = self.graph.context_menu().qmenu
57+
58+
dependMenu = QtWidgets.QMenu("&Dependencies", self)
59+
self.__menuActions.layers().addAction(dependMenu, "viewDepends")
60+
self.__menuActions.layers().addAction(dependMenu, "dependWizard")
61+
dependMenu.addSeparator()
62+
self.__menuActions.layers().addAction(dependMenu, "markdone")
63+
menu.addMenu(dependMenu)
64+
menu.addSeparator()
65+
self.__menuActions.layers().addAction(menu, "useLocalCores")
66+
self.__menuActions.layers().addAction(menu, "reorder")
67+
self.__menuActions.layers().addAction(menu, "stagger")
68+
menu.addSeparator()
69+
self.__menuActions.layers().addAction(menu, "setProperties")
70+
menu.addSeparator()
71+
self.__menuActions.layers().addAction(menu, "kill")
72+
self.__menuActions.layers().addAction(menu, "eat")
73+
self.__menuActions.layers().addAction(menu, "retry")
74+
menu.addSeparator()
75+
self.__menuActions.layers().addAction(menu, "retryDead")
76+
77+
def setJob(self, job):
78+
"""Set Job to be displayed
79+
@param job: Job to display as node graph
80+
@type job: opencue.wrappers.job.Job
81+
"""
82+
self.timer.stop()
83+
self.clearGraph()
84+
85+
if job is None:
86+
self.job = None
87+
return
88+
89+
job = cuegui.Utils.findJob(job)
90+
self.job = job
91+
self.createGraph()
92+
self.timer.start()
93+
94+
def getJob(self):
95+
"""Return the currently set job
96+
:rtype: opencue.wrappers.job.Job
97+
:return: Currently set job
98+
"""
99+
return self.job
100+
101+
def selectedObjects(self):
102+
"""Return the selected Layer rpcObjects in the graph.
103+
:rtype: [opencue.wrappers.layer.Layer]
104+
:return: List of selected layers
105+
"""
106+
layers = [n.rpcObject for n in self.graph.selected_nodes() if isinstance(n, CueLayerNode)]
107+
return layers
108+
109+
def createGraph(self):
110+
"""Create the graph to visualise the grid job submission
111+
"""
112+
if not self.job:
113+
return
114+
115+
layers = self.job.getLayers()
116+
117+
# add job layers to tree
118+
for layer in layers:
119+
node = CueLayerNode(layer)
120+
self.graph.add_node(node)
121+
node.set_name(layer.name())
122+
123+
# setup connections
124+
self.setupNodeConnections()
125+
126+
self.graph.auto_layout_nodes()
127+
self.graph.center_on()
128+
129+
def setupNodeConnections(self):
130+
"""Setup connections between nodes based on their dependencies"""
131+
for node in self.graph.all_nodes():
132+
for depend in node.rpcObject.getWhatDependsOnThis():
133+
child_node = self.graph.get_node_by_name(depend.dependErLayer())
134+
if child_node:
135+
# todo check if connection exists
136+
child_node.set_input(0, node.output(0))
137+
138+
for node in self.graph.all_nodes():
139+
for port in node.output_ports():
140+
port.lock()
141+
for port in node.input_ports():
142+
port.lock()
143+
144+
def update(self):
145+
"""Update nodes with latest Layer data
146+
147+
This is run every 20 seconds by the timer.
148+
"""
149+
if self.job is not None:
150+
layers = self.job.getLayers()
151+
for layer in layers:
152+
node = self.graph.get_node_by_name(layer.name())
153+
node.setRpcObject(layer)

cuegui/cuegui/LayerMonitorTree.py

+46-4
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,9 @@ def __init__(self, parent):
150150
tip="Timeout for a frames\' LLU, Hours:Minutes")
151151
cuegui.AbstractTreeWidget.AbstractTreeWidget.__init__(self, parent)
152152

153-
self.itemDoubleClicked.connect(self.__itemDoubleClickedFilterLayer)
153+
# pylint: disable=no-member
154+
self.itemSelectionChanged.connect(self.__itemSelectionChangedFilterLayer)
155+
cuegui.app().select_layers.connect(self.__handle_select_layers)
154156

155157
# Used to build right click context menus
156158
self.__menuActions = cuegui.MenuActions.MenuActions(
@@ -277,9 +279,49 @@ def contextMenuEvent(self, e):
277279

278280
menu.exec_(e.globalPos())
279281

280-
def __itemDoubleClickedFilterLayer(self, item, col):
281-
del col
282-
self.handle_filter_layers_byLayer.emit([item.rpcObject.data.name])
282+
def __itemSelectionChangedFilterLayer(self):
283+
"""Filter FrameMonitor to selected Layers.
284+
Emits signal to filter FrameMonitor to selected Layers.
285+
Also emits signal for other widgets to select Layers.
286+
"""
287+
layers = self.selectedObjects()
288+
layer_names = [layer.data.name for layer in layers]
289+
290+
# emit signal to filter Frame Monitor
291+
self.handle_filter_layers_byLayer.emit(layer_names)
292+
293+
# emit signal to select Layers in other widgets
294+
cuegui.app().select_layers.emit(layers)
295+
296+
def __handle_select_layers(self, layerRpcObjects):
297+
"""Select incoming Layers in tree.
298+
Slot connected to QtGui.qApp.select_layers inorder to handle
299+
selecting Layers in Tree.
300+
Also emits signal to filter FrameMonitor
301+
"""
302+
received_layers = [l.data.name for l in layerRpcObjects]
303+
current_layers = [l.data.name for l in self.selectedObjects()]
304+
if received_layers == current_layers:
305+
# prevent recursion
306+
return
307+
308+
# prevent unnecessary calls to __itemSelectionChangedFilterLayer
309+
self.blockSignals(True)
310+
try:
311+
for item in self._items.values():
312+
item.setSelected(False)
313+
for layer in layerRpcObjects:
314+
objectKey = cuegui.Utils.getObjectKey(layer)
315+
if objectKey not in self._items:
316+
self.addObject(layer)
317+
item = self._items[objectKey]
318+
item.setSelected(True)
319+
finally:
320+
# make sure signals are re-enabled
321+
self.blockSignals(False)
322+
323+
# emit signal to filter Frame Monitor
324+
self.handle_filter_layers_byLayer.emit(received_layers)
283325

284326

285327
class LayerWidgetItem(cuegui.AbstractWidgetItem.AbstractWidgetItem):

cuegui/cuegui/images/apps/blender.png

5.53 KB
Loading

cuegui/cuegui/images/apps/ffmpeg.png

6.63 KB
Loading

cuegui/cuegui/images/apps/gaffer.png

3.32 KB
Loading

cuegui/cuegui/images/apps/krita.png

9.19 KB
Loading

cuegui/cuegui/images/apps/natron.png

4.18 KB
Loading

cuegui/cuegui/images/apps/oiio.png

2.62 KB
Loading
695 Bytes
Loading
64.3 KB
Loading

cuegui/cuegui/images/apps/rm.png

1.46 KB
Loading

cuegui/cuegui/images/apps/shell.png

5.29 KB
Loading
5.29 KB
Loading

cuegui/cuegui/nodegraph/__init__.py

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright Contributors to the OpenCue Project
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
"""nodegraph is an OpenCue specific extension of NodeGraphQtPy
17+
18+
The docs for NodeGraphQtPy can be found at:
19+
http://chantasticvfx.com/nodeGraphQt/html/nodes.html
20+
"""
21+
from .nodes import CueLayerNode
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright Contributors to the OpenCue Project
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
"""Module housing node implementations that work with NodeGraphQtPy"""
17+
18+
19+
from .layer import CueLayerNode

0 commit comments

Comments
 (0)