Skip to content

Commit

Permalink
[cuegui] feat: Add job node graph plugin v2 (#1400)
Browse files Browse the repository at this point in the history
**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.
  • Loading branch information
lithorus authored Dec 13, 2024
1 parent 0fa5d6d commit 8a02963
Show file tree
Hide file tree
Showing 25 changed files with 829 additions and 4 deletions.
126 changes: 126 additions & 0 deletions cuegui/cuegui/AbstractGraphWidget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Copyright Contributors to the OpenCue Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


"""Base class for CueGUI graph widgets."""

from qtpy import QtCore
from qtpy import QtWidgets

from NodeGraphQtPy import NodeGraph
from NodeGraphQtPy.errors import NodeRegistrationError
from cuegui.nodegraph import CueLayerNode
from cuegui import app


class AbstractGraphWidget(QtWidgets.QWidget):
"""Base class for CueGUI graph widgets"""

def __init__(self, parent=None):
super(AbstractGraphWidget, self).__init__(parent=parent)
self.graph = NodeGraph()
self.setupUI()

self.timer = QtCore.QTimer(self)
# pylint: disable=no-member
self.timer.timeout.connect(self.update)
self.timer.setInterval(1000 * 20)

self.graph.node_selection_changed.connect(self.onNodeSelectionChanged)
app().quit.connect(self.timer.stop)

def setupUI(self):
"""Setup the UI."""
try:
self.graph.register_node(CueLayerNode)
except NodeRegistrationError:
pass
self.graph.viewer().installEventFilter(self)

layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.graph.viewer())

def onNodeSelectionChanged(self):
"""Slot run when a node is selected.
Updates the nodes to ensure they're visualising current data.
Can be used to notify other widgets of object selection.
"""
self.update()

def handleSelectObjects(self, rpcObjects):
"""Select incoming objects in graph.
"""
received = [o.name() for o in rpcObjects]
current = [rpcObject.name() for rpcObject in self.selectedObjects()]
if received == current:
# prevent recursing
return

for node in self.graph.all_nodes():
node.set_selected(False)
for rpcObject in rpcObjects:
node = self.graph.get_node_by_name(rpcObject.name())
node.set_selected(True)

def selectedObjects(self):
"""Return the selected nodes rpcObjects in the graph.
:rtype: [opencue.wrappers.layer.Layer]
:return: List of selected layers
"""
rpcObjects = [n.rpcObject for n in self.graph.selected_nodes()]
return rpcObjects

def eventFilter(self, target, event):
"""Override eventFilter
Centre nodes in graph viewer on 'F' key press.
@param target: widget event occurred on
@type target: QtWidgets.QWidget
@param event: Qt event
@type event: QtCore.QEvent
"""
if hasattr(self, "graph"):
viewer = self.graph.viewer()
if target == viewer:
if event.type() == QtCore.QEvent.KeyPress:
if event.key() == QtCore.Qt.Key_F:
self.graph.center_on()
if event.key() == QtCore.Qt.Key_L:
self.graph.auto_layout_nodes()

return super(AbstractGraphWidget, self).eventFilter(target, event)

def clearGraph(self):
"""Clear all nodes from the graph
"""
for node in self.graph.all_nodes():
for port in node.output_ports():
port.unlock()
for port in node.input_ports():
port.unlock()
self.graph.clear_session()

def createGraph(self):
"""Create the graph to visualise OpenCue objects
"""
raise NotImplementedError()

def update(self):
"""Update nodes with latest data
This is run every 20 seconds by the timer.
"""
raise NotImplementedError()
1 change: 1 addition & 0 deletions cuegui/cuegui/App.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class CueGuiApplication(QtWidgets.QApplication):
request_update = QtCore.Signal()
status = QtCore.Signal()
quit = QtCore.Signal()
select_layers = QtCore.Signal(list)

# Thread pool
threadpool = None
Expand Down
153 changes: 153 additions & 0 deletions cuegui/cuegui/JobMonitorGraph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# Copyright Contributors to the OpenCue Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


"""Node graph to display Layers of a Job"""


from qtpy import QtWidgets

import cuegui.Utils
import cuegui.MenuActions
from cuegui.nodegraph import CueLayerNode
from cuegui.AbstractGraphWidget import AbstractGraphWidget


class JobMonitorGraph(AbstractGraphWidget):
"""Graph widget to display connections of layers in a job"""

def __init__(self, parent=None):
super(JobMonitorGraph, self).__init__(parent=parent)
self.job = None
self.setupContextMenu()

# wire signals
cuegui.app().select_layers.connect(self.handleSelectObjects)

def onNodeSelectionChanged(self):
"""Notify other widgets of Layer selection.
Emit signal to notify other widgets of Layer selection, this keeps
all widgets with selectable Layers in sync with each other.
Also force updates the nodes, as the timed updates are infrequent.
"""
self.update()
layers = self.selectedObjects()
cuegui.app().select_layers.emit(layers)

def setupContextMenu(self):
"""Setup context menu for nodes in node graph"""
self.__menuActions = cuegui.MenuActions.MenuActions(
self, self.update, self.selectedObjects, self.getJob
)

menu = self.graph.context_menu().qmenu

dependMenu = QtWidgets.QMenu("&Dependencies", self)
self.__menuActions.layers().addAction(dependMenu, "viewDepends")
self.__menuActions.layers().addAction(dependMenu, "dependWizard")
dependMenu.addSeparator()
self.__menuActions.layers().addAction(dependMenu, "markdone")
menu.addMenu(dependMenu)
menu.addSeparator()
self.__menuActions.layers().addAction(menu, "useLocalCores")
self.__menuActions.layers().addAction(menu, "reorder")
self.__menuActions.layers().addAction(menu, "stagger")
menu.addSeparator()
self.__menuActions.layers().addAction(menu, "setProperties")
menu.addSeparator()
self.__menuActions.layers().addAction(menu, "kill")
self.__menuActions.layers().addAction(menu, "eat")
self.__menuActions.layers().addAction(menu, "retry")
menu.addSeparator()
self.__menuActions.layers().addAction(menu, "retryDead")

def setJob(self, job):
"""Set Job to be displayed
@param job: Job to display as node graph
@type job: opencue.wrappers.job.Job
"""
self.timer.stop()
self.clearGraph()

if job is None:
self.job = None
return

job = cuegui.Utils.findJob(job)
self.job = job
self.createGraph()
self.timer.start()

def getJob(self):
"""Return the currently set job
:rtype: opencue.wrappers.job.Job
:return: Currently set job
"""
return self.job

def selectedObjects(self):
"""Return the selected Layer rpcObjects in the graph.
:rtype: [opencue.wrappers.layer.Layer]
:return: List of selected layers
"""
layers = [n.rpcObject for n in self.graph.selected_nodes() if isinstance(n, CueLayerNode)]
return layers

def createGraph(self):
"""Create the graph to visualise the grid job submission
"""
if not self.job:
return

layers = self.job.getLayers()

# add job layers to tree
for layer in layers:
node = CueLayerNode(layer)
self.graph.add_node(node)
node.set_name(layer.name())

# setup connections
self.setupNodeConnections()

self.graph.auto_layout_nodes()
self.graph.center_on()

def setupNodeConnections(self):
"""Setup connections between nodes based on their dependencies"""
for node in self.graph.all_nodes():
for depend in node.rpcObject.getWhatDependsOnThis():
child_node = self.graph.get_node_by_name(depend.dependErLayer())
if child_node:
# todo check if connection exists
child_node.set_input(0, node.output(0))

for node in self.graph.all_nodes():
for port in node.output_ports():
port.lock()
for port in node.input_ports():
port.lock()

def update(self):
"""Update nodes with latest Layer data
This is run every 20 seconds by the timer.
"""
if self.job is not None:
layers = self.job.getLayers()
for layer in layers:
node = self.graph.get_node_by_name(layer.name())
node.setRpcObject(layer)
50 changes: 46 additions & 4 deletions cuegui/cuegui/LayerMonitorTree.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,9 @@ def __init__(self, parent):
tip="Timeout for a frames\' LLU, Hours:Minutes")
cuegui.AbstractTreeWidget.AbstractTreeWidget.__init__(self, parent)

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

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

menu.exec_(e.globalPos())

def __itemDoubleClickedFilterLayer(self, item, col):
del col
self.handle_filter_layers_byLayer.emit([item.rpcObject.data.name])
def __itemSelectionChangedFilterLayer(self):
"""Filter FrameMonitor to selected Layers.
Emits signal to filter FrameMonitor to selected Layers.
Also emits signal for other widgets to select Layers.
"""
layers = self.selectedObjects()
layer_names = [layer.data.name for layer in layers]

# emit signal to filter Frame Monitor
self.handle_filter_layers_byLayer.emit(layer_names)

# emit signal to select Layers in other widgets
cuegui.app().select_layers.emit(layers)

def __handle_select_layers(self, layerRpcObjects):
"""Select incoming Layers in tree.
Slot connected to QtGui.qApp.select_layers inorder to handle
selecting Layers in Tree.
Also emits signal to filter FrameMonitor
"""
received_layers = [l.data.name for l in layerRpcObjects]
current_layers = [l.data.name for l in self.selectedObjects()]
if received_layers == current_layers:
# prevent recursion
return

# prevent unnecessary calls to __itemSelectionChangedFilterLayer
self.blockSignals(True)
try:
for item in self._items.values():
item.setSelected(False)
for layer in layerRpcObjects:
objectKey = cuegui.Utils.getObjectKey(layer)
if objectKey not in self._items:
self.addObject(layer)
item = self._items[objectKey]
item.setSelected(True)
finally:
# make sure signals are re-enabled
self.blockSignals(False)

# emit signal to filter Frame Monitor
self.handle_filter_layers_byLayer.emit(received_layers)


class LayerWidgetItem(cuegui.AbstractWidgetItem.AbstractWidgetItem):
Expand Down
Binary file added cuegui/cuegui/images/apps/blender.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added cuegui/cuegui/images/apps/ffmpeg.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added cuegui/cuegui/images/apps/gaffer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added cuegui/cuegui/images/apps/krita.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added cuegui/cuegui/images/apps/natron.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added cuegui/cuegui/images/apps/oiio.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added cuegui/cuegui/images/apps/placeholder.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added cuegui/cuegui/images/apps/postprocess.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added cuegui/cuegui/images/apps/rm.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added cuegui/cuegui/images/apps/shell.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added cuegui/cuegui/images/apps/terminal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions cuegui/cuegui/nodegraph/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copyright Contributors to the OpenCue Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


"""nodegraph is an OpenCue specific extension of NodeGraphQtPy
The docs for NodeGraphQtPy can be found at:
http://chantasticvfx.com/nodeGraphQt/html/nodes.html
"""
from .nodes import CueLayerNode
19 changes: 19 additions & 0 deletions cuegui/cuegui/nodegraph/nodes/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright Contributors to the OpenCue Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


"""Module housing node implementations that work with NodeGraphQtPy"""


from .layer import CueLayerNode
Loading

0 comments on commit 8a02963

Please sign in to comment.