diff --git a/cuegui/cuegui/AbstractGraphWidget.py b/cuegui/cuegui/AbstractGraphWidget.py new file mode 100644 index 000000000..8b4880644 --- /dev/null +++ b/cuegui/cuegui/AbstractGraphWidget.py @@ -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() diff --git a/cuegui/cuegui/App.py b/cuegui/cuegui/App.py index 8e5409520..eaf48c500 100644 --- a/cuegui/cuegui/App.py +++ b/cuegui/cuegui/App.py @@ -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 diff --git a/cuegui/cuegui/JobMonitorGraph.py b/cuegui/cuegui/JobMonitorGraph.py new file mode 100644 index 000000000..4e0bbe882 --- /dev/null +++ b/cuegui/cuegui/JobMonitorGraph.py @@ -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) diff --git a/cuegui/cuegui/LayerMonitorTree.py b/cuegui/cuegui/LayerMonitorTree.py index 83b615f68..70059391b 100644 --- a/cuegui/cuegui/LayerMonitorTree.py +++ b/cuegui/cuegui/LayerMonitorTree.py @@ -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( @@ -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): diff --git a/cuegui/cuegui/images/apps/blender.png b/cuegui/cuegui/images/apps/blender.png new file mode 100644 index 000000000..c32f78a08 Binary files /dev/null and b/cuegui/cuegui/images/apps/blender.png differ diff --git a/cuegui/cuegui/images/apps/ffmpeg.png b/cuegui/cuegui/images/apps/ffmpeg.png new file mode 100644 index 000000000..1c1644247 Binary files /dev/null and b/cuegui/cuegui/images/apps/ffmpeg.png differ diff --git a/cuegui/cuegui/images/apps/gaffer.png b/cuegui/cuegui/images/apps/gaffer.png new file mode 100644 index 000000000..293bf7028 Binary files /dev/null and b/cuegui/cuegui/images/apps/gaffer.png differ diff --git a/cuegui/cuegui/images/apps/krita.png b/cuegui/cuegui/images/apps/krita.png new file mode 100644 index 000000000..7515f0b70 Binary files /dev/null and b/cuegui/cuegui/images/apps/krita.png differ diff --git a/cuegui/cuegui/images/apps/natron.png b/cuegui/cuegui/images/apps/natron.png new file mode 100644 index 000000000..6d8b56ba8 Binary files /dev/null and b/cuegui/cuegui/images/apps/natron.png differ diff --git a/cuegui/cuegui/images/apps/oiio.png b/cuegui/cuegui/images/apps/oiio.png new file mode 100644 index 000000000..7396c24c4 Binary files /dev/null and b/cuegui/cuegui/images/apps/oiio.png differ diff --git a/cuegui/cuegui/images/apps/placeholder.png b/cuegui/cuegui/images/apps/placeholder.png new file mode 100644 index 000000000..dad292e86 Binary files /dev/null and b/cuegui/cuegui/images/apps/placeholder.png differ diff --git a/cuegui/cuegui/images/apps/postprocess.png b/cuegui/cuegui/images/apps/postprocess.png new file mode 100644 index 000000000..74978c2f6 Binary files /dev/null and b/cuegui/cuegui/images/apps/postprocess.png differ diff --git a/cuegui/cuegui/images/apps/rm.png b/cuegui/cuegui/images/apps/rm.png new file mode 100644 index 000000000..689f725fd Binary files /dev/null and b/cuegui/cuegui/images/apps/rm.png differ diff --git a/cuegui/cuegui/images/apps/shell.png b/cuegui/cuegui/images/apps/shell.png new file mode 100644 index 000000000..f9a53c4ce Binary files /dev/null and b/cuegui/cuegui/images/apps/shell.png differ diff --git a/cuegui/cuegui/images/apps/terminal.png b/cuegui/cuegui/images/apps/terminal.png new file mode 100644 index 000000000..f9a53c4ce Binary files /dev/null and b/cuegui/cuegui/images/apps/terminal.png differ diff --git a/cuegui/cuegui/nodegraph/__init__.py b/cuegui/cuegui/nodegraph/__init__.py new file mode 100644 index 000000000..625cfd879 --- /dev/null +++ b/cuegui/cuegui/nodegraph/__init__.py @@ -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 diff --git a/cuegui/cuegui/nodegraph/nodes/__init__.py b/cuegui/cuegui/nodegraph/nodes/__init__.py new file mode 100644 index 000000000..bd04f566e --- /dev/null +++ b/cuegui/cuegui/nodegraph/nodes/__init__.py @@ -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 diff --git a/cuegui/cuegui/nodegraph/nodes/base.py b/cuegui/cuegui/nodegraph/nodes/base.py new file mode 100644 index 000000000..6665c6faf --- /dev/null +++ b/cuegui/cuegui/nodegraph/nodes/base.py @@ -0,0 +1,79 @@ +# 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 any cue nodes to work with NodeGraphQtPy""" + + +from builtins import str +from NodeGraphQtPy import BaseNode +from cuegui.nodegraph.widgets.nodeWidgets import NodeProgressBar + + +class CueBaseNode(BaseNode): + """Base class for any cue nodes to work with NodeGraphQtPy""" + + __identifier__ = "aswf.opencue" + + NODE_NAME = "Base" + + def __init__(self, rpcObject=None): + super(CueBaseNode, self).__init__() + self.add_input(name="parent", multi_input=True, display_name=False) + self.add_output(name="children", multi_output=True, display_name=False) + + self.rpcObject = rpcObject + + def setRpcObject(self, rpcObject): + """Set the nodes rpc object + @param rpc object to set on node + @type opencue.wrappers.layer.Layer + """ + self.rpcObject = rpcObject + + def addProgressBar( + self, + name="", + label="", + value=0, + max_value=100, + display_format="%p%", + tab=None + ): + """Add progress bar property to node + @param name: name of the custom property + @type name: str + @param label: label to be displayed + @type label: str + @param value: value to set progress bar to + @type value: int + @param max_value: max_value value progress bar can go up to + @type max_value: int + @param display_format: string format to display value on progress bar with + @type display_format: str + @param tab:name of the widget tab to display in. + @type tab: str + """ + self.create_property( + name, str(value), tab=tab + ) + widget = NodeProgressBar( + self.view,name, + label, + value, + max_value=max_value, + display_format=display_format + ) + widget.value_changed.connect(self.set_property) + self.view.add_widget(widget) diff --git a/cuegui/cuegui/nodegraph/nodes/layer.py b/cuegui/cuegui/nodegraph/nodes/layer.py new file mode 100644 index 000000000..7c9ec2f14 --- /dev/null +++ b/cuegui/cuegui/nodegraph/nodes/layer.py @@ -0,0 +1,97 @@ +# 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. + + +"""Implementation of a Cue Layer node that works with NodeGraphQtPy""" + + +from __future__ import division +import os +from qtpy import QtGui +import opencue +import NodeGraphQtPy.qgraphics.node_base +import cuegui.images +from cuegui.Constants import RGB_FRAME_STATE +from cuegui.nodegraph.nodes.base import CueBaseNode + + +class CueLayerNode(CueBaseNode): + """Implementation of a Cue Layer node that works with NodeGraphQtPy""" + + __identifier__ = "aswf.opencue" + + NODE_NAME = "Layer" + + def __init__(self, layerRpcObject=None): + super(CueLayerNode, self).__init__(rpcObject=layerRpcObject) + + self.set_name(layerRpcObject.name()) + + NodeGraphQtPy.qgraphics.node_base.NODE_ICON_SIZE = 30 + services = layerRpcObject.services() + if services: + app = services[0].name() + imagesPath = cuegui.images.__path__[0] + iconPath = os.path.join(imagesPath, "apps", app + ".png") + if os.path.exists(iconPath): + self.set_icon(iconPath) + + self.addProgressBar( + name="succeededFrames", + label="", + value=layerRpcObject.succeededFrames(), + max_value=layerRpcObject.totalFrames(), + display_format="%v / %m" + ) + + font = self.view.text_item.font() + font.setPointSize(16) + self.view.text_item.setFont(font) + # Lock the node text so it can't be edited + self.view.text_item.set_locked(True) + + self.setRpcObject(layerRpcObject) + + def updateNodeColour(self): + """Update the colour of the node to reflect the status of the layer""" + # default colour + r, g, b = self.color() + color = QtGui.QColor(r, g, b) + + # state specific colours + if self.rpcObject.totalFrames() == self.rpcObject.succeededFrames(): + color = RGB_FRAME_STATE[opencue.api.job_pb2.SUCCEEDED] + if self.rpcObject.waitingFrames() > 0: + color = RGB_FRAME_STATE[opencue.api.job_pb2.WAITING] + if self.rpcObject.dependFrames() > 0: + color = RGB_FRAME_STATE[opencue.api.job_pb2.DEPEND] + if self.rpcObject.runningFrames() > 0: + color = RGB_FRAME_STATE[opencue.api.job_pb2.RUNNING] + if self.rpcObject.deadFrames() > 0: + color = RGB_FRAME_STATE[opencue.api.job_pb2.DEAD] + + self.set_color( + color.red() // 2, + color.green() // 2, + color.blue() // 2 + ) + + def setRpcObject(self, rpcObject): + """Set the nodes layer rpc object + @param rpc object to set on node + @type opencue.wrappers.layer.Layer + """ + super(CueLayerNode, self).setRpcObject(rpcObject) + self.set_property("succeededFrames", rpcObject.succeededFrames()) + self.updateNodeColour() diff --git a/cuegui/cuegui/nodegraph/widgets/__init__.py b/cuegui/cuegui/nodegraph/widgets/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cuegui/cuegui/nodegraph/widgets/nodeWidgets.py b/cuegui/cuegui/nodegraph/widgets/nodeWidgets.py new file mode 100644 index 000000000..e881f0a27 --- /dev/null +++ b/cuegui/cuegui/nodegraph/widgets/nodeWidgets.py @@ -0,0 +1,112 @@ +# 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 defining custom widgets that appear on nodes in the nodegraph. + +The classes defined here inherit from NodeGraphQtPy base classes, therefore any +snake_case methods defined here are overriding the base class and must remain +snake_case to work properly. +""" + + +from qtpy import QtWidgets +from qtpy import QtCore +from NodeGraphQtPy.widgets.node_widgets import NodeBaseWidget + + +class NodeProgressBar(NodeBaseWidget): + """ + ProgressBar Node Widget. + """ + + def __init__( + self, + parent=None, + name="", + label="", + value=0, + max_value=100, + display_format="%p%" + ): + super(NodeProgressBar, self).__init__(parent, name, label) + self._progressbar = QtWidgets.QProgressBar() + self._progressbar.setAlignment(QtCore.Qt.AlignCenter) + self._progressbar.setFormat(display_format) + self._progressbar.setMaximum(max_value) + self._progressbar.setValue(value) + progress_style = """ +QProgressBar { + background-color: rgba(40, 40, 40, 255); + border: 1px solid grey; + border-radius: 1px; + margin: 0px; +} +QProgressBar::chunk { + background-color: rgba(100, 120, 250, 150); +} + """ + self._progressbar.setStyleSheet(progress_style) + self.set_custom_widget(self._progressbar) + self.text = str(value) + + @property + def type_(self): + """ + @return: Name of widget type + @rtype: str + """ + return "ProgressBarNodeWidget" + + def get_value(self): + """Get value from progress bar on node + @return: progress bar value + @rtype: int + """ + return self._progressbar.value() + + def set_value(self, text=0): + """Set value on progress bar + @param text: Text value to set on progress bar + @type text: int + """ + if int(float(text)) != self.get_value(): + self._progressbar.setValue(int(float(text))) + self.on_value_changed() + + @property + def value(self): + """Get value from progress bar on node + XXX: This property shouldn't be required as it's been superseded by get_value, + however the progress bar doesn't update without it. Believe it may be + a bug in NodeGraphQtPy's `NodeObject.set_property`. We should remove this + once it's been resolved. + @return: progress bar value + @rtype: int + """ + return self._progressbar.value() + + @value.setter + def value(self, value=0): + """Set value on progress bar + XXX: This property shouldn't be required as it's been superseded by set_value, + however the progress bar doesn't update without it. Believe it may be + a bug in NodeGraphQtPy's `NodeObject.set_property`. We should remove this + once it's been resolved. + @param value: Value to set on progress bar + @type value: int + """ + if int(float(value)) != self.value: + self._progressbar.setValue(int(float(value))) + self.on_value_changed() diff --git a/cuegui/cuegui/plugins/MonitorJobGraphPlugin.py b/cuegui/cuegui/plugins/MonitorJobGraphPlugin.py new file mode 100644 index 000000000..9d93e27f0 --- /dev/null +++ b/cuegui/cuegui/plugins/MonitorJobGraphPlugin.py @@ -0,0 +1,103 @@ +# 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. + + + +"""Plugin for displaying node graph representation of layer in the selected job. + +Job selection is triggered by other plugins using the application's view_object signal.""" + + +from __future__ import print_function +from __future__ import division +from __future__ import absolute_import + +import opencue + +import cuegui.AbstractDockWidget +import cuegui.Logger +import cuegui.Utils + +import cuegui.JobMonitorGraph + + +logger = cuegui.Logger.getLogger(__file__) + +PLUGIN_NAME = "Job Graph" +PLUGIN_CATEGORY = "Cuetopia" +PLUGIN_DESCRIPTION = "Visualise a job's layers in a node graph" +PLUGIN_PROVIDES = "MonitorGraphDockWidget" + + +class MonitorGraphDockWidget(cuegui.AbstractDockWidget.AbstractDockWidget): + """Plugin for displaying node graph representation of layer in the selected job.""" + + def __init__(self, parent): + """Creates the dock widget and docks it to the parent. + @param parent: The main window to dock to + @type parent: QMainWindow""" + cuegui.AbstractDockWidget.AbstractDockWidget.__init__(self, parent, PLUGIN_NAME) + + self.__job = None + + self.__monitorGraph = cuegui.JobMonitorGraph.JobMonitorGraph(self) + + self.setAcceptDrops(True) + + self.layout().addWidget(self.__monitorGraph) + + cuegui.app().view_object.connect(self.__setJob) + cuegui.app().unmonitor.connect(self.__unmonitor) + cuegui.app().facility_changed.connect(self.__setJob) + + # pylint: disable=missing-function-docstring + def dragEnterEvent(self, event): + cuegui.Utils.dragEnterEvent(event) + + # pylint: disable=missing-function-docstring + def dragMoveEvent(self, event): + cuegui.Utils.dragMoveEvent(event) + + # pylint: disable=missing-function-docstring + def dropEvent(self, event): + for jobName in cuegui.Utils.dropEvent(event): + self.__setJob(jobName) + + def __setJob(self, job = None): + """Set the job to be displayed + @param job: Selected job + @type job: opencue.wrappers.job.Job + """ + if cuegui.Utils.isJob(job) and self.__job and opencue.id(job) == opencue.id(self.__job): + return + + newJob = cuegui.Utils.findJob(job) + if newJob: + self.__job = newJob + self.setWindowTitle("%s" % newJob.name()) + self.raise_() + + self.__monitorGraph.setJob(newJob) + elif not job and self.__job: + self.__unmonitor(self.__job) + + def __unmonitor(self, proxy): + """Unmonitors the current job if it matches the supplied proxy. + @param proxy: A job proxy + @type proxy: proxy""" + if self.__job and self.__job == proxy: + self.__job = None + self.setWindowTitle("Monitor Job Graph") + + self.__monitorGraph.setJob(None) diff --git a/cuegui/tests/plugins/JobGraphPlugin_tests.py b/cuegui/tests/plugins/JobGraphPlugin_tests.py new file mode 100644 index 000000000..4912825fe --- /dev/null +++ b/cuegui/tests/plugins/JobGraphPlugin_tests.py @@ -0,0 +1,64 @@ +# 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. + + +"""Tests for cuegui.plugins.MonitorJobGraphPlugin.""" + + +import unittest + +import mock + +import opencue.compiled_proto.job_pb2 +import opencue.compiled_proto.depend_pb2 +import opencue.wrappers.job +import opencue.wrappers.layer +import opencue.wrappers.depend + +import qtpy.QtCore +import qtpy.QtGui +import qtpy.QtTest +import qtpy.QtWidgets + +import cuegui.Main +import cuegui.plugins.MonitorJobGraphPlugin +import cuegui.JobMonitorGraph +import cuegui.Style +from .. import test_utils + + +@mock.patch('opencue.cuebot.Cuebot.getStub', new=mock.Mock()) +class MonitorJobGraphPluginTests(unittest.TestCase): + + @mock.patch('opencue.cuebot.Cuebot.getStub', new=mock.Mock()) + @mock.patch('opencue.api.getJob') + def setUp(self, getJobMock): + app = test_utils.createApplication() + app.settings = qtpy.QtCore.QSettings() + cuegui.Style.init() + self.main_window = qtpy.QtWidgets.QMainWindow() + + self.job = opencue.wrappers.job.Job(opencue.compiled_proto.job_pb2.Job(id='foo')) + layer = opencue.wrappers.layer.Layer(opencue.compiled_proto.job_pb2.Layer(name='layer1')) + depend = opencue.wrappers.depend.Depend(opencue.compiled_proto.depend_pb2.Depend()) + layer.getWhatDependsOnThis = lambda: [depend] + self.job.getLayers = lambda: [layer] + self.jobGraph = cuegui.JobMonitorGraph.JobMonitorGraph(self.main_window) + self.jobGraph.setJob(self.job) + + def test_setup(self): + pass + + def test_job(self): + self.assertNotEqual(None, self.jobGraph.getJob()) diff --git a/pycue/opencue/wrappers/layer.py b/pycue/opencue/wrappers/layer.py index 264ac8bf3..778580fc8 100644 --- a/pycue/opencue/wrappers/layer.py +++ b/pycue/opencue/wrappers/layer.py @@ -591,3 +591,10 @@ def parent(self): :return: the layer's parent job """ return opencue.api.getJob(self.data.parent_id) + + def services(self): + """Returns list of services applied to this layer + :rtype: opencue.wrappers.service.Service + :return: the layer's services + """ + return [opencue.api.getService(service) for service in self.data.services] diff --git a/requirements_gui.txt b/requirements_gui.txt index 1f5b19637..b93a77357 100644 --- a/requirements_gui.txt +++ b/requirements_gui.txt @@ -3,3 +3,4 @@ PySide6==6.5.3;python_version=="3.11" PySide2==5.15.2.1;python_version<="3.10" QtPy==1.11.3;python_version<"3.7" QtPy==2.4.1;python_version>="3.7" +NodeGraphQtPy==0.6.38.6