Skip to content

Commit

Permalink
[cuegui] Add support for multiple viewers (AcademySoftwareFoundation#…
Browse files Browse the repository at this point in the history
…1513)

**Summarize your change.**
This adds support for multiple output viewers.
The `cuegui.yaml` option `output_viewer_direct_cmd_call` is still kept
as a single viewer command.

It also changes the way that single frames are resolved. Instead of a
hardcoded padding of 4, it will use the FileSequence and FrameSet
classes to resolve the frame path.

Examples:

![image](https://github.com/user-attachments/assets/d6b2641e-5bc6-4f3f-813f-24cafcd3ce78)

![image](https://github.com/user-attachments/assets/10f2b801-e2da-411b-a214-f6c0a7f1edea)
  • Loading branch information
lithorus authored Oct 1, 2024
1 parent 8f7f23c commit 380dbfe
Show file tree
Hide file tree
Showing 10 changed files with 127 additions and 74 deletions.
11 changes: 6 additions & 5 deletions cuegui/cuegui/Constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
16 changes: 14 additions & 2 deletions cuegui/cuegui/FrameMonitorTree.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from builtins import map
from builtins import object
import datetime
import functools
import glob
import os
import re
Expand Down Expand Up @@ -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")
Expand Down
15 changes: 11 additions & 4 deletions cuegui/cuegui/JobMonitorTree.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

from future.utils import iteritems
from builtins import map
import functools
import time
import pickle

Expand Down Expand Up @@ -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")
Expand Down
12 changes: 10 additions & 2 deletions cuegui/cuegui/LayerMonitorTree.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from __future__ import print_function
from __future__ import division

import functools

from qtpy import QtCore
from qtpy import QtWidgets
Expand Down Expand Up @@ -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")
Expand Down
18 changes: 0 additions & 18 deletions cuegui/cuegui/MenuActions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
57 changes: 38 additions & 19 deletions cuegui/cuegui/Utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
from qtpy import QtWidgets
import six

import FileSequence

import opencue
import opencue.wrappers.group

Expand Down Expand Up @@ -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<Job> or list<Layer>
@param items: List of jobs or list of layers to view the entire job's outputs"""
@type items: list<Job> or list<Layer>
@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 = []

Expand All @@ -563,35 +567,51 @@ 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<Frame>
@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:
- OUTPUT_VIEWER_STEREO_MODIFIERS
- OUTPUT_VIEWER_EXTRACT_ARGS_REGEX
- OUTPUT_VIEWER_CMD_PATTERN
@type paths: list<String>
@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(
Expand All @@ -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())
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 []

Expand Down
13 changes: 7 additions & 6 deletions cuegui/cuegui/config/cuegui.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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<show>\w+)/(?P<shot>shot\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
Expand Down
40 changes: 24 additions & 16 deletions cuegui/tests/Utils_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<show>\w+)/(?P<name>shot\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<show>\w+)/(?P<name>shot\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<show>\w+)/(?P<shot>\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<show>\w+)/(?P<shot>\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)

Expand Down
Loading

0 comments on commit 380dbfe

Please sign in to comment.