diff --git a/cuegui/cuegui/Constants.py b/cuegui/cuegui/Constants.py index 319b7e147..1c1a85937 100644 --- a/cuegui/cuegui/Constants.py +++ b/cuegui/cuegui/Constants.py @@ -201,11 +201,12 @@ def __get_version_from_cmd(command): RESOURCE_LIMITS = __config.get('resources') -OUTPUT_VIEWER_ACTION_TEXT = __config.get('output_viewer', {}).get('action_text') -OUTPUT_VIEWER_EXTRACT_ARGS_REGEX = __config.get('output_viewer', {}).get('extract_args_regex') -OUTPUT_VIEWER_CMD_PATTERN = __config.get('output_viewer', {}).get('cmd_pattern') -OUTPUT_VIEWER_DIRECT_CMD_CALL = __config.get('output_viewer', {}).get('direct_cmd_call') -OUTPUT_VIEWER_STEREO_MODIFIERS = __config.get('output_viewer', {}).get('stereo_modifiers') +OUTPUT_VIEWERS = [] +for viewer in __config.get('output_viewers', {}): + OUTPUT_VIEWERS.append(viewer) + +OUTPUT_VIEWER_DIRECT_CMD_CALL = __config.get('output_viewer_direct_cmd_call') + FINISHED_JOBS_READONLY_FRAME = __config.get('finished_jobs_readonly.frame', False) FINISHED_JOBS_READONLY_LAYER = __config.get('finished_jobs_readonly.layer', False) diff --git a/cuegui/cuegui/FrameMonitorTree.py b/cuegui/cuegui/FrameMonitorTree.py index 9f6baf2f8..9e29aa0b2 100644 --- a/cuegui/cuegui/FrameMonitorTree.py +++ b/cuegui/cuegui/FrameMonitorTree.py @@ -24,6 +24,7 @@ from builtins import map from builtins import object import datetime +import functools import glob import os import re @@ -912,8 +913,19 @@ def __init__(self, widget, filterSelectedLayersCallback, readonly=False): if bool(int(self.app.settings.value("AllowDeeding", 0))): self.__menuActions.frames().addAction(self, "useLocalCores") - if cuegui.Constants.OUTPUT_VIEWER_CMD_PATTERN: - self.__menuActions.frames().addAction(self, "viewOutput") + if cuegui.Constants.OUTPUT_VIEWERS: + job = widget.getJob() + outputPaths = [] + for frame in widget.selectedObjects(): + layer = job.getLayer(frame.layer()) + outputPaths.extend(cuegui.Utils.getOutputFromFrame(layer, frame)) + if outputPaths: + for viewer in cuegui.Constants.OUTPUT_VIEWERS: + self.addAction(viewer['action_text'], + functools.partial(cuegui.Utils.viewFramesOutput, + job, + widget.selectedObjects(), + viewer['action_text'])) if self.app.applicationName() == "CueCommander": self.__menuActions.frames().addAction(self, "viewHost") diff --git a/cuegui/cuegui/JobMonitorTree.py b/cuegui/cuegui/JobMonitorTree.py index 5b8897bba..901ae62e9 100644 --- a/cuegui/cuegui/JobMonitorTree.py +++ b/cuegui/cuegui/JobMonitorTree.py @@ -22,6 +22,7 @@ from future.utils import iteritems from builtins import map +import functools import time import pickle @@ -425,10 +426,16 @@ def contextMenuEvent(self, e): if bool(int(self.app.settings.value("AllowDeeding", 0))): self.__menuActions.jobs().addAction(menu, "useLocalCores") - if cuegui.Constants.OUTPUT_VIEWER_CMD_PATTERN: - viewer_action = self.__menuActions.jobs().addAction(menu, "viewOutput") - viewer_action.setDisabled(__count == 0) - viewer_action.setToolTip("Open Viewer for the selected items") + if cuegui.Constants.OUTPUT_VIEWERS: + job = __selectedObjects[0] + for viewer in cuegui.Constants.OUTPUT_VIEWERS: + viewer_menu = QtWidgets.QMenu(viewer['action_text'], self) + for layer in job.getLayers(): + viewer_menu.addAction(layer.name(), + functools.partial(cuegui.Utils.viewOutput, + [layer], + viewer['action_text'])) + menu.addMenu(viewer_menu) depend_menu = QtWidgets.QMenu("&Dependencies",self) self.__menuActions.jobs().addAction(depend_menu, "viewDepends") diff --git a/cuegui/cuegui/LayerMonitorTree.py b/cuegui/cuegui/LayerMonitorTree.py index 0f110f874..83b615f68 100644 --- a/cuegui/cuegui/LayerMonitorTree.py +++ b/cuegui/cuegui/LayerMonitorTree.py @@ -20,6 +20,7 @@ from __future__ import print_function from __future__ import division +import functools from qtpy import QtCore from qtpy import QtWidgets @@ -239,8 +240,15 @@ def contextMenuEvent(self, e): menu = QtWidgets.QMenu() self.__menuActions.layers().addAction(menu, "view") - if cuegui.Constants.OUTPUT_VIEWER_CMD_PATTERN: - self.__menuActions.layers().addAction(menu, "viewOutput") + + if (len(cuegui.Constants.OUTPUT_VIEWERS) > 0 + and sum(len(layer.getOutputPaths()) for layer in __selectedObjects) > 0): + for viewer in cuegui.Constants.OUTPUT_VIEWERS: + menu.addAction(viewer['action_text'], + functools.partial(cuegui.Utils.viewOutput, + __selectedObjects, + viewer['action_text'])) + depend_menu = QtWidgets.QMenu("&Dependencies", self) self.__menuActions.layers().addAction(depend_menu, "viewDepends") self.__menuActions.layers().addAction(depend_menu, "dependWizard") diff --git a/cuegui/cuegui/MenuActions.py b/cuegui/cuegui/MenuActions.py index ee2e15532..d19637ee3 100644 --- a/cuegui/cuegui/MenuActions.py +++ b/cuegui/cuegui/MenuActions.py @@ -234,12 +234,6 @@ def view(self, rpcObjects=None): for job in self._getOnlyJobObjects(rpcObjects): self.app.view_object.emit(job) - viewOutput_info = [cuegui.Constants.OUTPUT_VIEWER_ACTION_TEXT, None, "view"] - def viewOutput(self, rpcObjects=None): - jobs = self._getOnlyJobObjects(rpcObjects) - if jobs and cuegui.Constants.OUTPUT_VIEWER_ACTION_TEXT: - cuegui.Utils.viewOutput(jobs) - viewDepends_info = ["&View Dependencies...", None, "log"] def viewDepends(self, rpcObjects=None): @@ -938,12 +932,6 @@ def dependWizard(self, rpcObjects=None): if layers: cuegui.DependWizard.DependWizard(self._caller, [self._getSource()], layers=layers) - viewOutput_info = [cuegui.Constants.OUTPUT_VIEWER_ACTION_TEXT, None, "view"] - def viewOutput(self, rpcObjects=None): - layers = self._getOnlyLayerObjects(rpcObjects) - if layers and cuegui.Constants.OUTPUT_VIEWER_ACTION_TEXT: - cuegui.Utils.viewOutput(layers) - reorder_info = ["Reorder Frames...", None, "configure"] def reorder(self, rpcObjects=None): @@ -1134,12 +1122,6 @@ def viewDepends(self, rpcObjects=None): frames = self._getOnlyFrameObjects(rpcObjects) cuegui.DependDialog.DependDialog(frames[0], self._caller).show() - viewOutput_info = [cuegui.Constants.OUTPUT_VIEWER_ACTION_TEXT, None, "view"] - def viewOutput(self, rpcObjects=None): - frames = self._getOnlyFrameObjects(rpcObjects) - if frames and cuegui.Constants.OUTPUT_VIEWER_ACTION_TEXT: - cuegui.Utils.viewFramesOutput(self._getSource(), frames) - getWhatDependsOnThis_info = ["print getWhatDependsOnThis", None, "log"] def getWhatDependsOnThis(self, rpcObjects=None): diff --git a/cuegui/cuegui/Utils.py b/cuegui/cuegui/Utils.py index 80163da8f..b59789d4b 100644 --- a/cuegui/cuegui/Utils.py +++ b/cuegui/cuegui/Utils.py @@ -38,6 +38,8 @@ from qtpy import QtWidgets import six +import FileSequence + import opencue import opencue.wrappers.group @@ -543,11 +545,13 @@ def popupFrameXdiff(job, frame1, frame2, frame3 = None): # View output in viewer ################################################################################ -def viewOutput(items): +def viewOutput(items, actionText): """Views the output of a list of jobs or list of layers in viewer - @type items: list or list - @param items: List of jobs or list of layers to view the entire job's outputs""" + @type items: list or list + @param items: List of jobs or list of layers to view the entire job's outputs + @type actionText: String + @param actionText: String to identity which viewer to use""" if items and len(items) >= 1: paths = [] @@ -563,27 +567,40 @@ def viewOutput(items): raise Exception("The function expects a list of jobs or a list of layers") # Launch viewer using paths if paths exists and are valid - launchViewerUsingPaths(paths) + launchViewerUsingPaths(paths, actionText) -def viewFramesOutput(job, frames): +def viewFramesOutput(job, frames, actionText): """Views the output of a list of frames in viewer using the job's layer associated with the frames @type job: Job or None @param job: The job with the output to view. @type frames: list - @param frames: List of frames to view the entire job's outputs""" + @param frames: List of frames to view the entire job's outputs + @type actionText: String + @param actionText: String to identity which viewer to use""" + if frames and len(frames) >= 1: paths = [] - all_layers = { layer.data.name: layer for layer in job.getLayers() } + all_layers = { layer.name(): layer for layer in job.getLayers() } for frame in frames: - paths.extend(__getOutputFromFrame(all_layers[frame.data.layer_name], frame)) - launchViewerUsingPaths(paths) + paths.extend(getOutputFromFrame(all_layers[frame.layer()], frame)) + launchViewerUsingPaths(paths, actionText) + +def getViewer(actionText): + """Retrieves the viewer from cuegui.Constants.OUTPUT_VIEWERS using the actionText + + @type actionText: String + @param actionText: String to identity which viewer to use""" + for viewer in cuegui.Constants.OUTPUT_VIEWERS: + if viewer['action_text'] == actionText: + return viewer + return None -def launchViewerUsingPaths(paths, test_mode=False): +def launchViewerUsingPaths(paths, actionText, test_mode=False): """Launch viewer using paths if paths exists and are valid This function relies on the following constants that should be configured on the output_viewer section of the config file: @@ -591,7 +608,10 @@ def launchViewerUsingPaths(paths, test_mode=False): - OUTPUT_VIEWER_EXTRACT_ARGS_REGEX - OUTPUT_VIEWER_CMD_PATTERN @type paths: list - @param paths: List of paths""" + @param paths: List of paths + @type actionText: String + @param actionText: String to identity which viewer to use""" + viewer = getViewer(actionText) if not paths: if not test_mode: showErrorMessageBox( @@ -603,8 +623,8 @@ def launchViewerUsingPaths(paths, test_mode=False): # Stereo ouputs are usually differentiated by a modifier like _lf_ and _rt_, # the viewer should only be called with one of them if OUTPUT_VIEWER_STEREO_MODIFIERS # is set. - if cuegui.Constants.OUTPUT_VIEWER_STEREO_MODIFIERS: - stereo_modifiers = cuegui.Constants.OUTPUT_VIEWER_STEREO_MODIFIERS.split(",") + if 'stereo_modifiers' in viewer: + stereo_modifiers = viewer['stereo_modifiers'].split(",") if len(paths) == 2 and len(stereo_modifiers) == 2: unified_paths = [path.replace(stereo_modifiers[0].strip(), stereo_modifiers[1].strip()) @@ -617,8 +637,8 @@ def launchViewerUsingPaths(paths, test_mode=False): # should be the same as the quantity expected by cmd_pattern. # If no regex is provided, cmd_pattern is executed as it is sample_path = paths[0] - regexp = cuegui.Constants.OUTPUT_VIEWER_EXTRACT_ARGS_REGEX - cmd_pattern = cuegui.Constants.OUTPUT_VIEWER_CMD_PATTERN + regexp = viewer.get('extract_args_regex') + cmd_pattern = viewer.get('cmd_pattern') joined_paths = " ".join(paths) # Default to the cmd + paths @@ -697,7 +717,7 @@ def __getOutputFromLayers(layers): return paths -def __getOutputFromFrame(layer, frame): +def getOutputFromFrame(layer, frame): """Returns the output paths from a single frame @type layer: Layer @@ -710,9 +730,8 @@ def __getOutputFromFrame(layer, frame): outputs = layer.getOutputPaths() if not outputs: return [] - main_output = __findMainOutputPath(outputs) - main_output = main_output.replace("#", "%04d" % frame.data.number) - return [main_output] + seq = FileSequence.FileSequence(__findMainOutputPath(outputs)) + return seq.getFileList(frameSet=FileSequence.FrameSet(str(frame.number()))) except IndexError: return [] diff --git a/cuegui/cuegui/config/cuegui.yaml b/cuegui/cuegui/config/cuegui.yaml index 381eedc94..529cb9e00 100644 --- a/cuegui/cuegui/config/cuegui.yaml +++ b/cuegui/cuegui/config/cuegui.yaml @@ -131,27 +131,28 @@ startup_notice.msg: '' # Memory usage above this level will be displayed in a different color. memory_warning_level: 5242880 -# Output Viewer config. +# Output Viewers config. # # ------------------------------------------------------------------------------------------------------ # Frame, Layer and Job objects have right click menu option for opening an output viewer # (eg. OpenRV) -# output_viewer: -# # Text to be displayed at the menu action button -# action_text: "View Output in Itview" +#output_viewers: +# # Text to be displayed at the menu action button +# - action_text: "View in OpenRV" # # extract_args_regex: Regex to extract arguments from the output path produced by a job/layer/frame # # cmd_pattern: Command pattern to be matched with the regex defined at extract_args_regex # # if extract_args_regex is not provided, cmd_pattern is called directly with paths as arguments # extract_args_regex: '/shots/(?P\w+)/(?Pshot\w+)/.*' # cmd_pattern: "env SHOW={show} SHOT={shot} COLOR_IO=/{show}/home/colorspaces.xml OCIO=/{show}/home/config.ocio openrv {paths}" -# # Pattern to call viewer cmd directly without extracting environment variables. Used for previewing frames -# direct_cmd_call: "openrv {paths}" # # if provided, paths containing any of the two values are considered the same output and only one # # of them will be passed to the viewer # stereo_modifiers: "_rt_,_lf_" # # ------------------------------------------------------------------------------------------------------ +# Pattern to call viewer cmd directly without extracting environment variables. Used for previewing frames +# output_viewer_direct_cmd_call: "openrv {paths}" + # These flags determine whether or not layers/frames will be readonly when job is finished. # If flags are set as true, layers/frames cannot be retried, eaten, edited dependency on, etc. # In order to toggle the same protection on cuebot's side, set flags in opencue.properties diff --git a/cuegui/tests/Utils_tests.py b/cuegui/tests/Utils_tests.py index 325f66732..b4a1d9715 100644 --- a/cuegui/tests/Utils_tests.py +++ b/cuegui/tests/Utils_tests.py @@ -85,55 +85,63 @@ def test_shouldReturnResourceLimitsFromYaml(self): class UtilsViewerTests(unittest.TestCase): def test_shouldLaunchViewerUsingEmptyPaths(self): # Test launching without empty paths - self.assertIsNone(cuegui.Utils.launchViewerUsingPaths([], test_mode=True)) + self.assertIsNone(cuegui.Utils.launchViewerUsingPaths([], "test", test_mode=True)) def test_shouldLaunchViewerUsingSimplePath(self): # Test launching without regexp - cuegui.Constants.OUTPUT_VIEWER_EXTRACT_ARGS_REGEX = None - cuegui.Constants.OUTPUT_VIEWER_CMD_PATTERN = 'echo' - + cuegui.Constants.OUTPUT_VIEWERS = [{"action_text": "test", + "extract_args_regex": None, + "cmd_pattern": 'echo'}] out = cuegui.Utils.launchViewerUsingPaths(["/shots/test_show/test_shot/something/else"], + "test", test_mode=True) self.assertEqual('echo /shots/test_show/test_shot/something/else', out) def test_shouldNotLaunchViewerUsingInvalidCombination(self): # Test launching with invalig regex and pattern combination - cuegui.Constants.OUTPUT_VIEWER_EXTRACT_ARGS_REGEX = \ - r'/shots/(?P\w+)/(?Pshot\w+)/.*' - cuegui.Constants.OUTPUT_VIEWER_CMD_PATTERN = \ - 'echo show={not_a_show}, shot={shot}' + cuegui.Constants.OUTPUT_VIEWERS = [ + {"action_text": "test", + "extract_args_regex": r'/shots/(?P\w+)/(?Pshot\w+)/.*', + "cmd_pattern": 'echo show={not_a_show}, shot={shot}'}] out = cuegui.Utils.launchViewerUsingPaths(["/shots/test_show/test_shot/something/else"], + "test", test_mode=True) self.assertIsNone(out) def test_shouldLaunchViewerUsingRegextAndPattern(self): # Test launching with valid regex and pattern - cuegui.Constants.OUTPUT_VIEWER_EXTRACT_ARGS_REGEX = \ - r'/shots/(?P\w+)/(?P\w+)/.*' - cuegui.Constants.OUTPUT_VIEWER_CMD_PATTERN = 'echo show={show}, shot={shot}' + cuegui.Constants.OUTPUT_VIEWERS = [ + {"action_text": "test", + "extract_args_regex": r'/shots/(?P\w+)/(?P\w+)/.*', + "cmd_pattern": 'echo show={show}, shot={shot}'}] out = cuegui.Utils.launchViewerUsingPaths(["/shots/test_show/test_shot/something/else"], + "test", test_mode=True) self.assertEqual('echo show=test_show, shot=test_shot', out) def test_shouldLaunchViewerUsingStereoPaths(self): # Test launching with stereo output - cuegui.Constants.OUTPUT_VIEWER_EXTRACT_ARGS_REGEX = None - cuegui.Constants.OUTPUT_VIEWER_CMD_PATTERN = 'echo' - cuegui.Constants.OUTPUT_VIEWER_STEREO_MODIFIERS = '_lf_,_rt_' + cuegui.Constants.OUTPUT_VIEWERS = [{"action_text": "test", + "extract_args_regex": None, + "cmd_pattern": 'echo', + "stereo_modifiers": '_lf_,_rt_'}] out = cuegui.Utils.launchViewerUsingPaths(["/test/something_lf_something", "/test/something_rt_something"], + "test", test_mode=True) self.assertEqual('echo /test/something_lf_something', out) def test_shouldLaunchViewerUsingMultiplePaths(self): # Test launching multiple outputs - cuegui.Constants.OUTPUT_VIEWER_EXTRACT_ARGS_REGEX = None - cuegui.Constants.OUTPUT_VIEWER_CMD_PATTERN = 'echo' + cuegui.Constants.OUTPUT_VIEWERS = [{"action_text": "test", + "extract_args_regex": None, + "cmd_pattern": 'echo'}] out = cuegui.Utils.launchViewerUsingPaths(["/test/something_1", "/test/something_2"], + "test", test_mode=True) self.assertEqual('echo /test/something_1 /test/something_2', out) diff --git a/pycue/FileSequence/FileSequence.py b/pycue/FileSequence/FileSequence.py index 61d86a55e..784df0502 100644 --- a/pycue/FileSequence/FileSequence.py +++ b/pycue/FileSequence/FileSequence.py @@ -90,8 +90,14 @@ def getFileList(self, frameSet=None): """ Returns the file list of the sequence """ filelist = [] paddingString = "%%0%dd" % self.getPadSize() - for frame in self.frameSet.getAll(): - if frameSet is None or (isinstance(frameSet, FrameSet) and frame in frameSet.getAll()): + if self.frameSet: + for frame in self.frameSet.getAll(): + if (frameSet is None or + (isinstance(frameSet, FrameSet) and frame in frameSet.getAll())): + framepath = self.getPrefix() + paddingString % frame + self.getSuffix() + filelist.append(framepath) + else: + for frame in frameSet.getAll(): framepath = self.getPrefix() + paddingString % frame + self.getSuffix() filelist.append(framepath) return filelist diff --git a/pycue/opencue/wrappers/job.py b/pycue/opencue/wrappers/job.py index 65572090c..e582a91bf 100644 --- a/pycue/opencue/wrappers/job.py +++ b/pycue/opencue/wrappers/job.py @@ -23,6 +23,7 @@ from opencue import Cuebot from opencue.compiled_proto import comment_pb2 from opencue.compiled_proto import job_pb2 +import opencue.api import opencue.search import opencue.wrappers.comment import opencue.wrappers.depend @@ -188,6 +189,14 @@ def getLayers(self): layerSeq = response.layers return [opencue.wrappers.layer.Layer(lyr) for lyr in layerSeq.layers] + def getLayer(self, layerName): + """ Returns the layer with the specified name + :type: layername: str + :rtype: opencue.wrappers.layer.Layer + :return: specific layer in the job + """ + return opencue.api.findLayer(self.name(), layerName) + def getFrames(self, **options): """Returns the list of up to 1000 frames from within the job.