fix adding capture-gui

This commit is contained in:
Milan Kolar 2018-11-29 23:33:36 +01:00
parent f7cd93d723
commit 45eb1a858e
22 changed files with 3755 additions and 0 deletions

396
pype/vendor/capture_gui/lib.py vendored Normal file
View file

@ -0,0 +1,396 @@
# TODO: fetch Maya main window without shiboken that also doesn't crash
import sys
import logging
import json
import os
import glob
import subprocess
import contextlib
from collections import OrderedDict
import datetime
import maya.cmds as cmds
import maya.mel as mel
import maya.OpenMayaUI as omui
import capture
from .vendor.Qt import QtWidgets
try:
# PySide1
import shiboken
except ImportError:
# PySide2
import shiboken2 as shiboken
log = logging.getLogger(__name__)
# region Object types
OBJECT_TYPES = OrderedDict()
OBJECT_TYPES['NURBS Curves'] = 'nurbsCurves'
OBJECT_TYPES['NURBS Surfaces'] = 'nurbsSurfaces'
OBJECT_TYPES['NURBS CVs'] = 'controlVertices'
OBJECT_TYPES['NURBS Hulls'] = 'hulls'
OBJECT_TYPES['Polygons'] = 'polymeshes'
OBJECT_TYPES['Subdiv Surfaces'] = 'subdivSurfaces'
OBJECT_TYPES['Planes'] = 'planes'
OBJECT_TYPES['Lights'] = 'lights'
OBJECT_TYPES['Cameras'] = 'cameras'
OBJECT_TYPES['Image Planes'] = 'imagePlane'
OBJECT_TYPES['Joints'] = 'joints'
OBJECT_TYPES['IK Handles'] = 'ikHandles'
OBJECT_TYPES['Deformers'] = 'deformers'
OBJECT_TYPES['Dynamics'] = 'dynamics'
OBJECT_TYPES['Particle Instancers'] = 'particleInstancers'
OBJECT_TYPES['Fluids'] = 'fluids'
OBJECT_TYPES['Hair Systems'] = 'hairSystems'
OBJECT_TYPES['Follicles'] = 'follicles'
OBJECT_TYPES['nCloths'] = 'nCloths'
OBJECT_TYPES['nParticles'] = 'nParticles'
OBJECT_TYPES['nRigids'] = 'nRigids'
OBJECT_TYPES['Dynamic Constraints'] = 'dynamicConstraints'
OBJECT_TYPES['Locators'] = 'locators'
OBJECT_TYPES['Dimensions'] = 'dimensions'
OBJECT_TYPES['Pivots'] = 'pivots'
OBJECT_TYPES['Handles'] = 'handles'
OBJECT_TYPES['Textures Placements'] = 'textures'
OBJECT_TYPES['Strokes'] = 'strokes'
OBJECT_TYPES['Motion Trails'] = 'motionTrails'
OBJECT_TYPES['Plugin Shapes'] = 'pluginShapes'
OBJECT_TYPES['Clip Ghosts'] = 'clipGhosts'
OBJECT_TYPES['Grease Pencil'] = 'greasePencils'
OBJECT_TYPES['Manipulators'] = 'manipulators'
OBJECT_TYPES['Grid'] = 'grid'
OBJECT_TYPES['HUD'] = 'hud'
# endregion Object types
def get_show_object_types():
results = OrderedDict()
# Add the plug-in shapes
plugin_shapes = get_plugin_shapes()
results.update(plugin_shapes)
# We add default shapes last so plug-in shapes could
# never potentially overwrite any built-ins.
results.update(OBJECT_TYPES)
return results
def get_current_scenename():
path = cmds.file(query=True, sceneName=True)
if path:
return os.path.splitext(os.path.basename(path))[0]
return None
def get_current_camera():
"""Returns the currently active camera.
Searched in the order of:
1. Active Panel
2. Selected Camera Shape
3. Selected Camera Transform
Returns:
str: name of active camera transform
"""
# Get camera from active modelPanel (if any)
panel = cmds.getPanel(withFocus=True)
if cmds.getPanel(typeOf=panel) == "modelPanel":
cam = cmds.modelEditor(panel, query=True, camera=True)
# In some cases above returns the shape, but most often it returns the
# transform. Still we need to make sure we return the transform.
if cam:
if cmds.nodeType(cam) == "transform":
return cam
# camera shape is a shape type
elif cmds.objectType(cam, isAType="shape"):
parent = cmds.listRelatives(cam, parent=True, fullPath=True)
if parent:
return parent[0]
# Check if a camShape is selected (if so use that)
cam_shapes = cmds.ls(selection=True, type="camera")
if cam_shapes:
return cmds.listRelatives(cam_shapes,
parent=True,
fullPath=True)[0]
# Check if a transform of a camShape is selected
# (return cam transform if any)
transforms = cmds.ls(selection=True, type="transform")
if transforms:
cam_shapes = cmds.listRelatives(transforms, shapes=True, type="camera")
if cam_shapes:
return cmds.listRelatives(cam_shapes,
parent=True,
fullPath=True)[0]
def get_active_editor():
"""Return the active editor panel to playblast with"""
# fixes `cmds.playblast` undo bug
cmds.currentTime(cmds.currentTime(query=True))
panel = cmds.playblast(activeEditor=True)
return panel.split("|")[-1]
def get_current_frame():
return cmds.currentTime(query=True)
def get_time_slider_range(highlighted=True,
withinHighlighted=True,
highlightedOnly=False):
"""Return the time range from Maya's time slider.
Arguments:
highlighted (bool): When True if will return a selected frame range
(if there's any selection of more than one frame!) otherwise it
will return min and max playback time.
withinHighlighted (bool): By default Maya returns the highlighted range
end as a plus one value. When this is True this will be fixed by
removing one from the last number.
Returns:
list: List of two floats of start and end frame numbers.
"""
if highlighted is True:
gPlaybackSlider = mel.eval("global string $gPlayBackSlider; "
"$gPlayBackSlider = $gPlayBackSlider;")
if cmds.timeControl(gPlaybackSlider, query=True, rangeVisible=True):
highlightedRange = cmds.timeControl(gPlaybackSlider,
query=True,
rangeArray=True)
if withinHighlighted:
highlightedRange[-1] -= 1
return highlightedRange
if not highlightedOnly:
return [cmds.playbackOptions(query=True, minTime=True),
cmds.playbackOptions(query=True, maxTime=True)]
def get_current_renderlayer():
return cmds.editRenderLayerGlobals(query=True, currentRenderLayer=True)
def get_plugin_shapes():
"""Get all currently available plugin shapes
Returns:
dict: plugin shapes by their menu label and script name
"""
filters = cmds.pluginDisplayFilter(query=True, listFilters=True)
labels = [cmds.pluginDisplayFilter(f, query=True, label=True) for f in
filters]
return OrderedDict(zip(labels, filters))
def open_file(filepath):
"""Open file using OS default settings"""
if sys.platform.startswith('darwin'):
subprocess.call(('open', filepath))
elif os.name == 'nt':
os.startfile(filepath)
elif os.name == 'posix':
subprocess.call(('xdg-open', filepath))
else:
raise NotImplementedError("OS not supported: {0}".format(os.name))
def load_json(filepath):
"""open and read json, return read values"""
with open(filepath, "r") as f:
return json.load(f)
def _fix_playblast_output_path(filepath):
"""Workaround a bug in maya.cmds.playblast to return correct filepath.
When the `viewer` argument is set to False and maya.cmds.playblast does not
automatically open the playblasted file the returned filepath does not have
the file's extension added correctly.
To workaround this we just glob.glob() for any file extensions and assume
the latest modified file is the correct file and return it.
"""
# Catch cancelled playblast
if filepath is None:
log.warning("Playblast did not result in output path. "
"Playblast is probably interrupted.")
return
# Fix: playblast not returning correct filename (with extension)
# Lets assume the most recently modified file is the correct one.
if not os.path.exists(filepath):
directory = os.path.dirname(filepath)
filename = os.path.basename(filepath)
# check if the filepath is has frame based filename
# example : capture.####.png
parts = filename.split(".")
if len(parts) == 3:
query = os.path.join(directory, "{}.*.{}".format(parts[0],
parts[-1]))
files = glob.glob(query)
else:
files = glob.glob("{}.*".format(filepath))
if not files:
raise RuntimeError("Couldn't find playblast from: "
"{0}".format(filepath))
filepath = max(files, key=os.path.getmtime)
return filepath
def capture_scene(options):
"""Capture using scene settings.
Uses the view settings from "panel".
This ensures playblast is done as quicktime H.264 100% quality.
It forces showOrnaments to be off and does not render off screen.
Arguments:
options (dict): a collection of output options
Returns:
str: Full path to playblast file.
"""
filename = options.get("filename", "%TEMP%")
log.info("Capturing to: {0}".format(filename))
options = options.copy()
# Force viewer to False in call to capture because we have our own
# viewer opening call to allow a signal to trigger between playblast
# and viewer
options['viewer'] = False
# Remove panel key since it's internal value to capture_gui
options.pop("panel", None)
path = capture.capture(**options)
path = _fix_playblast_output_path(path)
return path
def browse(path=None):
"""Open a pop-up browser for the user"""
# Acquire path from user input if none defined
if path is None:
scene_path = cmds.file(query=True, sceneName=True)
# use scene file name as default name
default_filename = os.path.splitext(os.path.basename(scene_path))[0]
if not default_filename:
# Scene wasn't saved yet so found no valid name for playblast.
default_filename = "playblast"
# Default to images rule
default_root = os.path.normpath(get_project_rule("images"))
default_path = os.path.join(default_root, default_filename)
path = cmds.fileDialog2(fileMode=0,
dialogStyle=2,
startingDirectory=default_path)
if not path:
return
if isinstance(path, (tuple, list)):
path = path[0]
if path.endswith(".*"):
path = path[:-2]
# Bug-Fix/Workaround:
# Fix for playblasts that result in nesting of the
# extension (eg. '.mov.mov.mov') which happens if the format
# is defined in the filename used for saving.
extension = os.path.splitext(path)[-1]
if extension:
path = path[:-len(extension)]
return path
def default_output():
"""Return filename based on current scene name.
Returns:
str: A relative filename
"""
scene = get_current_scenename() or "playblast"
# get current datetime
timestamp = datetime.datetime.today()
str_timestamp = timestamp.strftime("%Y-%m-%d_%H-%M-%S")
filename = "{}_{}".format(scene, str_timestamp)
return filename
def get_project_rule(rule):
"""Get the full path of the rule of the project"""
workspace = cmds.workspace(query=True, rootDirectory=True)
folder = cmds.workspace(fileRuleEntry=rule)
if not folder:
log.warning("File Rule Entry '{}' has no value, please check if the "
"rule name is typed correctly".format(rule))
return os.path.join(workspace, folder)
def list_formats():
# Workaround for Maya playblast bug where undo would
# move the currentTime to frame one.
cmds.currentTime(cmds.currentTime(query=True))
return cmds.playblast(query=True, format=True)
def list_compressions(format='avi'):
# Workaround for Maya playblast bug where undo would
# move the currentTime to frame one.
cmds.currentTime(cmds.currentTime(query=True))
cmd = 'playblast -format "{0}" -query -compression'.format(format)
return mel.eval(cmd)
@contextlib.contextmanager
def no_undo():
"""Disable undo during the context"""
try:
cmds.undoInfo(stateWithoutFlush=False)
yield
finally:
cmds.undoInfo(stateWithoutFlush=True)
def get_maya_main_window():
"""Get the main Maya window as a QtGui.QMainWindow instance
Returns:
QtGui.QMainWindow: instance of the top level Maya windows
"""
ptr = omui.MQtUtil.mainWindow()
if ptr is not None:
return shiboken.wrapInstance(long(ptr), QtWidgets.QWidget)

401
pype/vendor/capture_gui/plugin.py vendored Normal file
View file

@ -0,0 +1,401 @@
"""Plug-in system
Works similar to how OSs look for executables; i.e. a number of
absolute paths are searched for a given match. The predicate for
executables is whether or not an extension matches a number of
options, such as ".exe" or ".bat".
In this system, the predicate is whether or not a fname ends with ".py"
"""
# Standard library
import os
import sys
import types
import logging
import inspect
from .vendor.Qt import QtCore, QtWidgets
log = logging.getLogger(__name__)
_registered_paths = list()
_registered_plugins = dict()
class classproperty(object):
def __init__(self, getter):
self.getter = getter
def __get__(self, instance, owner):
return self.getter(owner)
class Plugin(QtWidgets.QWidget):
"""Base class for Option plug-in Widgets.
This is a regular Qt widget that can be added to the capture interface
as an additional component, like a plugin.
The plug-ins are sorted in the interface by their `order` attribute and
will be displayed in the main interface when `section` is set to "app"
and displayed in the additional settings pop-up when set to "config".
When `hidden` is set to True the widget will not be shown in the interface.
This could be useful as a plug-in that supplies solely default values to
the capture GUI command.
"""
label = ""
section = "app" # "config" or "app"
hidden = False
options_changed = QtCore.Signal()
label_changed = QtCore.Signal(str)
order = 0
highlight = "border: 1px solid red;"
validate_state = True
def on_playblast_finished(self, options):
pass
def validate(self):
"""
Ensure outputs of the widget are possible, when errors are raised it
will return a message with what has caused the error
:return:
"""
errors = []
return errors
def get_outputs(self):
"""Return the options as set in this plug-in widget.
This is used to identify the settings to be used for the playblast.
As such the values should be returned in a way that a call to
`capture.capture()` would understand as arguments.
Args:
panel (str): The active modelPanel of the user. This is passed so
values could potentially be parsed from the active panel
Returns:
dict: The options for this plug-in. (formatted `capture` style)
"""
return dict()
def get_inputs(self, as_preset):
"""Return widget's child settings.
This should provide a dictionary of input settings of the plug-in
that results in a dictionary that can be supplied to `apply_input()`
This is used to save the settings of the preset to a widget.
:param as_preset:
:param as_presets: Toggle to mute certain input values of the widget
:type as_presets: bool
Returns:
dict: The currently set inputs of this widget.
"""
return dict()
def apply_inputs(self, settings):
"""Apply a dictionary of settings to the widget.
This should update the widget's inputs to the settings provided in
the dictionary. This is used to apply settings from a preset.
Returns:
None
"""
pass
def initialize(self):
"""
This method is used to register any callbacks
:return:
"""
pass
def uninitialize(self):
"""
Unregister any callback created when deleting the widget
A general explation:
The deletion method is an attribute that lives inside the object to be
deleted, and that is the problem:
Destruction seems not to care about the order of destruction,
and the __dict__ that also holds the onDestroy bound method
gets destructed before it is called.
Another solution is to use a weakref
:return: None
"""
pass
def __str__(self):
return self.label or type(self).__name__
def __repr__(self):
return u"%s.%s(%r)" % (__name__, type(self).__name__, self.__str__())
id = classproperty(lambda cls: cls.__name__)
def register_plugin_path(path):
"""Plug-ins are looked up at run-time from directories registered here
To register a new directory, run this command along with the absolute
path to where you"re plug-ins are located.
Example:
>>> import os
>>> my_plugins = "/server/plugins"
>>> register_plugin_path(my_plugins)
'/server/plugins'
Returns:
Actual path added, including any post-processing
"""
if path in _registered_paths:
return log.warning("Path already registered: {0}".format(path))
_registered_paths.append(path)
return path
def deregister_plugin_path(path):
"""Remove a _registered_paths path
Raises:
KeyError if `path` isn't registered
"""
_registered_paths.remove(path)
def deregister_all_plugin_paths():
"""Mainly used in tests"""
_registered_paths[:] = []
def registered_plugin_paths():
"""Return paths added via registration
..note:: This returns a copy of the registered paths
and can therefore not be modified directly.
"""
return list(_registered_paths)
def registered_plugins():
"""Return plug-ins added via :func:`register_plugin`
.. note:: This returns a copy of the registered plug-ins
and can therefore not be modified directly
"""
return _registered_plugins.values()
def register_plugin(plugin):
"""Register a new plug-in
Arguments:
plugin (Plugin): Plug-in to register
Raises:
TypeError if `plugin` is not callable
"""
if not hasattr(plugin, "__call__"):
raise TypeError("Plug-in must be callable "
"returning an instance of a class")
if not plugin_is_valid(plugin):
raise TypeError("Plug-in invalid: %s", plugin)
_registered_plugins[plugin.__name__] = plugin
def plugin_paths():
"""Collect paths from all sources.
This function looks at the three potential sources of paths
and returns a list with all of them together.
The sources are:
- Registered paths using :func:`register_plugin_path`
Returns:
list of paths in which plugins may be locat
"""
paths = list()
for path in registered_plugin_paths():
if path in paths:
continue
paths.append(path)
return paths
def discover(paths=None):
"""Find and return available plug-ins
This function looks for files within paths registered via
:func:`register_plugin_path`.
Arguments:
paths (list, optional): Paths to discover plug-ins from.
If no paths are provided, all paths are searched.
"""
plugins = dict()
# Include plug-ins from registered paths
for path in paths or plugin_paths():
path = os.path.normpath(path)
if not os.path.isdir(path):
continue
for fname in os.listdir(path):
if fname.startswith("_"):
continue
abspath = os.path.join(path, fname)
if not os.path.isfile(abspath):
continue
mod_name, mod_ext = os.path.splitext(fname)
if not mod_ext == ".py":
continue
module = types.ModuleType(mod_name)
module.__file__ = abspath
try:
execfile(abspath, module.__dict__)
# Store reference to original module, to avoid
# garbage collection from collecting it's global
# imports, such as `import os`.
sys.modules[mod_name] = module
except Exception as err:
log.debug("Skipped: \"%s\" (%s)", mod_name, err)
continue
for plugin in plugins_from_module(module):
if plugin.id in plugins:
log.debug("Duplicate plug-in found: %s", plugin)
continue
plugins[plugin.id] = plugin
# Include plug-ins from registration.
# Directly registered plug-ins take precedence.
for name, plugin in _registered_plugins.items():
if name in plugins:
log.debug("Duplicate plug-in found: %s", plugin)
continue
plugins[name] = plugin
plugins = list(plugins.values())
sort(plugins) # In-place
return plugins
def plugins_from_module(module):
"""Return plug-ins from module
Arguments:
module (types.ModuleType): Imported module from which to
parse valid plug-ins.
Returns:
List of plug-ins, or empty list if none is found.
"""
plugins = list()
for name in dir(module):
if name.startswith("_"):
continue
# It could be anything at this point
obj = getattr(module, name)
if not inspect.isclass(obj):
continue
if not issubclass(obj, Plugin):
continue
if not plugin_is_valid(obj):
log.debug("Plug-in invalid: %s", obj)
continue
plugins.append(obj)
return plugins
def plugin_is_valid(plugin):
"""Determine whether or not plug-in `plugin` is valid
Arguments:
plugin (Plugin): Plug-in to assess
"""
if not plugin:
return False
return True
def sort(plugins):
"""Sort `plugins` in-place
Their order is determined by their `order` attribute.
Arguments:
plugins (list): Plug-ins to sort
"""
if not isinstance(plugins, list):
raise TypeError("plugins must be of type list")
plugins.sort(key=lambda p: p.order)
return plugins
# Register default paths
default_plugins_path = os.path.join(os.path.dirname(__file__), "plugins")
register_plugin_path(default_plugins_path)

View file

@ -0,0 +1,141 @@
import maya.cmds as cmds
from capture_gui.vendor.Qt import QtCore, QtWidgets
import capture_gui.lib as lib
import capture_gui.plugin
class CameraPlugin(capture_gui.plugin.Plugin):
"""Camera widget.
Allows to select a camera.
"""
id = "Camera"
section = "app"
order = 10
def __init__(self, parent=None):
super(CameraPlugin, self).__init__(parent=parent)
self._layout = QtWidgets.QHBoxLayout()
self._layout.setContentsMargins(5, 0, 5, 0)
self.setLayout(self._layout)
self.cameras = QtWidgets.QComboBox()
self.cameras.setMinimumWidth(200)
self.get_active = QtWidgets.QPushButton("Get active")
self.get_active.setToolTip("Set camera from currently active view")
self.refresh = QtWidgets.QPushButton("Refresh")
self.refresh.setToolTip("Refresh the list of cameras")
self._layout.addWidget(self.cameras)
self._layout.addWidget(self.get_active)
self._layout.addWidget(self.refresh)
# Signals
self.connections()
# Force update of the label
self.set_active_cam()
self.on_update_label()
def connections(self):
self.get_active.clicked.connect(self.set_active_cam)
self.refresh.clicked.connect(self.on_refresh)
self.cameras.currentIndexChanged.connect(self.on_update_label)
self.cameras.currentIndexChanged.connect(self.validate)
def set_active_cam(self):
cam = lib.get_current_camera()
self.on_refresh(camera=cam)
def select_camera(self, cam):
if cam:
# Ensure long name
cameras = cmds.ls(cam, long=True)
if not cameras:
return
cam = cameras[0]
# Find the index in the list
for i in range(self.cameras.count()):
value = str(self.cameras.itemText(i))
if value == cam:
self.cameras.setCurrentIndex(i)
return
def validate(self):
errors = []
camera = self.cameras.currentText()
if not cmds.objExists(camera):
errors.append("{} : Selected camera '{}' "
"does not exist!".format(self.id, camera))
self.cameras.setStyleSheet(self.highlight)
else:
self.cameras.setStyleSheet("")
return errors
def get_outputs(self):
"""Return currently selected camera from combobox."""
idx = self.cameras.currentIndex()
camera = str(self.cameras.itemText(idx)) if idx != -1 else None
return {"camera": camera}
def on_refresh(self, camera=None):
"""Refresh the camera list with all current cameras in scene.
A currentIndexChanged signal is only emitted for the cameras combobox
when the camera is different at the end of the refresh.
Args:
camera (str): When name of camera is passed it will try to select
the camera with this name after the refresh.
Returns:
None
"""
cam = self.get_outputs()['camera']
# Get original selection
if camera is None:
index = self.cameras.currentIndex()
if index != -1:
camera = self.cameras.currentText()
self.cameras.blockSignals(True)
# Update the list with available cameras
self.cameras.clear()
cam_shapes = cmds.ls(type="camera")
cam_transforms = cmds.listRelatives(cam_shapes,
parent=True,
fullPath=True)
self.cameras.addItems(cam_transforms)
# If original selection, try to reselect
self.select_camera(camera)
self.cameras.blockSignals(False)
# If camera changed emit signal
if cam != self.get_outputs()['camera']:
idx = self.cameras.currentIndex()
self.cameras.currentIndexChanged.emit(idx)
def on_update_label(self):
cam = self.cameras.currentText()
cam = cam.rsplit("|", 1)[-1] # ensure short name
self.label = "Camera ({0})".format(cam)
self.label_changed.emit(self.label)

View file

@ -0,0 +1,95 @@
from capture_gui.vendor.Qt import QtCore, QtWidgets
import capture_gui.lib as lib
import capture_gui.plugin
class CodecPlugin(capture_gui.plugin.Plugin):
"""Codec widget.
Allows to set format, compression and quality.
"""
id = "Codec"
label = "Codec"
section = "config"
order = 50
def __init__(self, parent=None):
super(CodecPlugin, self).__init__(parent=parent)
self._layout = QtWidgets.QHBoxLayout()
self._layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(self._layout)
self.format = QtWidgets.QComboBox()
self.compression = QtWidgets.QComboBox()
self.quality = QtWidgets.QSpinBox()
self.quality.setMinimum(0)
self.quality.setMaximum(100)
self.quality.setValue(100)
self.quality.setToolTip("Compression quality percentage")
self._layout.addWidget(self.format)
self._layout.addWidget(self.compression)
self._layout.addWidget(self.quality)
self.format.currentIndexChanged.connect(self.on_format_changed)
self.refresh()
# Default to format 'qt'
index = self.format.findText("qt")
if index != -1:
self.format.setCurrentIndex(index)
# Default to compression 'H.264'
index = self.compression.findText("H.264")
if index != -1:
self.compression.setCurrentIndex(index)
self.connections()
def connections(self):
self.compression.currentIndexChanged.connect(self.options_changed)
self.format.currentIndexChanged.connect(self.options_changed)
self.quality.valueChanged.connect(self.options_changed)
def refresh(self):
formats = sorted(lib.list_formats())
self.format.clear()
self.format.addItems(formats)
def on_format_changed(self):
"""Refresh the available compressions."""
format = self.format.currentText()
compressions = lib.list_compressions(format)
self.compression.clear()
self.compression.addItems(compressions)
def get_outputs(self):
"""Get the plugin outputs that matches `capture.capture` arguments
Returns:
dict: Plugin outputs
"""
return {"format": self.format.currentText(),
"compression": self.compression.currentText(),
"quality": self.quality.value()}
def get_inputs(self, as_preset):
# a bit redundant but it will work when iterating over widgets
# so we don't have to write an exception
return self.get_outputs()
def apply_inputs(self, settings):
codec_format = settings.get("format", 0)
compr = settings.get("compression", 4)
quality = settings.get("quality", 100)
self.format.setCurrentIndex(self.format.findText(codec_format))
self.compression.setCurrentIndex(self.compression.findText(compr))
self.quality.setValue(int(quality))

View file

@ -0,0 +1,47 @@
import capture
import capture_gui.plugin
class DefaultOptionsPlugin(capture_gui.plugin.Plugin):
"""Invisible Plugin that supplies some default values to the gui.
This enures:
- no HUD is present in playblasts
- no overscan (`overscan` set to 1.0)
- no title safe, action safe, gate mask, etc.
- active sound is included in video playblasts
"""
order = -1
hidden = True
def get_outputs(self):
"""Get the plugin outputs that matches `capture.capture` arguments
Returns:
dict: Plugin outputs
"""
outputs = dict()
# use active sound track
scene = capture.parse_active_scene()
outputs['sound'] = scene['sound']
# override default settings
outputs['show_ornaments'] = True # never show HUD or overlays
# override camera options
outputs['camera_options'] = dict()
outputs['camera_options']['overscan'] = 1.0
outputs['camera_options']['displayFieldChart'] = False
outputs['camera_options']['displayFilmGate'] = False
outputs['camera_options']['displayFilmOrigin'] = False
outputs['camera_options']['displayFilmPivot'] = False
outputs['camera_options']['displayGateMask'] = False
outputs['camera_options']['displayResolution'] = False
outputs['camera_options']['displaySafeAction'] = False
outputs['camera_options']['displaySafeTitle'] = False
return outputs

View file

@ -0,0 +1,179 @@
import maya.cmds as cmds
from capture_gui.vendor.Qt import QtCore, QtWidgets
import capture_gui.plugin
import capture_gui.colorpicker as colorpicker
# region GLOBALS
BACKGROUND_DEFAULT = [0.6309999823570251,
0.6309999823570251,
0.6309999823570251]
TOP_DEFAULT = [0.5350000262260437,
0.6169999837875366,
0.7020000219345093]
BOTTOM_DEFAULT = [0.052000001072883606,
0.052000001072883606,
0.052000001072883606]
COLORS = {"background": BACKGROUND_DEFAULT,
"backgroundTop": TOP_DEFAULT,
"backgroundBottom": BOTTOM_DEFAULT}
LABELS = {"background": "Background",
"backgroundTop": "Top",
"backgroundBottom": "Bottom"}
# endregion GLOBALS
class DisplayPlugin(capture_gui.plugin.Plugin):
"""Plugin to apply viewport visibilities and settings"""
id = "Display Options"
label = "Display Options"
section = "config"
order = 70
def __init__(self, parent=None):
super(DisplayPlugin, self).__init__(parent=parent)
self._colors = dict()
self._layout = QtWidgets.QVBoxLayout()
self._layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(self._layout)
self.override = QtWidgets.QCheckBox("Override Display Options")
self.display_type = QtWidgets.QComboBox()
self.display_type.addItems(["Solid", "Gradient"])
# create color columns
self._color_layout = QtWidgets.QHBoxLayout()
for label, default in COLORS.items():
self.add_color_picker(self._color_layout, label, default)
# populate layout
self._layout.addWidget(self.override)
self._layout.addWidget(self.display_type)
self._layout.addLayout(self._color_layout)
# ensure widgets are in the correct enable state
self.on_toggle_override()
self.connections()
def connections(self):
self.override.toggled.connect(self.on_toggle_override)
self.override.toggled.connect(self.options_changed)
self.display_type.currentIndexChanged.connect(self.options_changed)
def add_color_picker(self, layout, label, default):
"""Create a column with a label and a button to select a color
Arguments:
layout (QtWidgets.QLayout): Layout to add color picker to
label (str): system name for the color type, e.g. : backgroundTop
default (list): The default color values to start with
Returns:
colorpicker.ColorPicker: a color picker instance
"""
column = QtWidgets.QVBoxLayout()
label_widget = QtWidgets.QLabel(LABELS[label])
color_picker = colorpicker.ColorPicker()
color_picker.color = default
column.addWidget(label_widget)
column.addWidget(color_picker)
column.setAlignment(label_widget, QtCore.Qt.AlignCenter)
layout.addLayout(column)
# connect signal
color_picker.valueChanged.connect(self.options_changed)
# store widget
self._colors[label] = color_picker
return color_picker
def on_toggle_override(self):
"""Callback when override is toggled.
Enable or disable the color pickers and background type widgets bases
on the current state of the override checkbox
Returns:
None
"""
state = self.override.isChecked()
self.display_type.setEnabled(state)
for widget in self._colors.values():
widget.setEnabled(state)
def display_gradient(self):
"""Return whether the background should be displayed as gradient.
When True the colors will use the top and bottom color to define the
gradient otherwise the background color will be used as solid color.
Returns:
bool: Whether background is gradient
"""
return self.display_type.currentText() == "Gradient"
def apply_inputs(self, settings):
"""Apply the saved inputs from the inputs configuration
Arguments:
settings (dict): The input settings to apply.
"""
for label, widget in self._colors.items():
default = COLORS.get(label, [0, 0, 0]) # fallback default to black
value = settings.get(label, default)
widget.color = value
override = settings.get("override_display", False)
self.override.setChecked(override)
def get_inputs(self, as_preset):
inputs = {"override_display": self.override.isChecked()}
for label, widget in self._colors.items():
inputs[label] = widget.color
return inputs
def get_outputs(self):
"""Get the plugin outputs that matches `capture.capture` arguments
Returns:
dict: Plugin outputs
"""
outputs = {}
if self.override.isChecked():
outputs["displayGradient"] = self.display_gradient()
for label, widget in self._colors.items():
outputs[label] = widget.color
else:
# Parse active color settings
outputs["displayGradient"] = cmds.displayPref(query=True,
displayGradient=True)
for key in COLORS.keys():
color = cmds.displayRGBColor(key, query=True)
outputs[key] = color
return {"display_options": outputs}

View file

@ -0,0 +1,95 @@
import maya.cmds as mc
from capture_gui.vendor.Qt import QtCore, QtWidgets
import capture_gui.plugin
import capture_gui.lib
class GenericPlugin(capture_gui.plugin.Plugin):
"""Widget for generic options"""
id = "Generic"
label = "Generic"
section = "config"
order = 100
def __init__(self, parent=None):
super(GenericPlugin, self).__init__(parent=parent)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(layout)
isolate_view = QtWidgets.QCheckBox(
"Use isolate view from active panel")
off_screen = QtWidgets.QCheckBox("Render offscreen")
layout.addWidget(isolate_view)
layout.addWidget(off_screen)
isolate_view.stateChanged.connect(self.options_changed)
off_screen.stateChanged.connect(self.options_changed)
self.widgets = {
"off_screen": off_screen,
"isolate_view": isolate_view
}
self.apply_inputs(self.get_defaults())
def get_defaults(self):
return {
"off_screen": True,
"isolate_view": False
}
def get_inputs(self, as_preset):
"""Return the widget options
Returns:
dict: The input settings of the widgets.
"""
inputs = dict()
for key, widget in self.widgets.items():
state = widget.isChecked()
inputs[key] = state
return inputs
def apply_inputs(self, inputs):
"""Apply the saved inputs from the inputs configuration
Arguments:
inputs (dict): The input settings to apply.
"""
for key, widget in self.widgets.items():
state = inputs.get(key, None)
if state is not None:
widget.setChecked(state)
return inputs
def get_outputs(self):
"""Returns all the options from the widget
Returns: dictionary with the settings
"""
inputs = self.get_inputs(as_preset=False)
outputs = dict()
outputs['off_screen'] = inputs['off_screen']
import capture_gui.lib
# Get isolate view members of the active panel
if inputs['isolate_view']:
panel = capture_gui.lib.get_active_editor()
filter_set = mc.modelEditor(panel, query=True, viewObjects=True)
isolate = mc.sets(filter_set, query=True) if filter_set else None
outputs['isolate'] = isolate
return outputs

View file

@ -0,0 +1,254 @@
import os
import logging
from functools import partial
from capture_gui.vendor.Qt import QtCore, QtWidgets
from capture_gui import plugin, lib
from capture_gui import tokens
log = logging.getLogger("IO")
class IoAction(QtWidgets.QAction):
def __init__(self, parent, filepath):
super(IoAction, self).__init__(parent)
action_label = os.path.basename(filepath)
self.setText(action_label)
self.setData(filepath)
# check if file exists and disable when false
self.setEnabled(os.path.isfile(filepath))
# get icon from file
info = QtCore.QFileInfo(filepath)
icon_provider = QtWidgets.QFileIconProvider()
self.setIcon(icon_provider.icon(info))
self.triggered.connect(self.open_object_data)
def open_object_data(self):
lib.open_file(self.data())
class IoPlugin(plugin.Plugin):
"""Codec widget.
Allows to set format, compression and quality.
"""
id = "IO"
label = "Save"
section = "app"
order = 40
max_recent_playblasts = 5
def __init__(self, parent=None):
super(IoPlugin, self).__init__(parent=parent)
self.recent_playblasts = list()
self._layout = QtWidgets.QVBoxLayout()
self._layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(self._layout)
# region Checkboxes
self.save_file = QtWidgets.QCheckBox(text="Save")
self.open_viewer = QtWidgets.QCheckBox(text="View when finished")
self.raw_frame_numbers = QtWidgets.QCheckBox(text="Raw frame numbers")
checkbox_hlayout = QtWidgets.QHBoxLayout()
checkbox_hlayout.setContentsMargins(5, 0, 5, 0)
checkbox_hlayout.addWidget(self.save_file)
checkbox_hlayout.addWidget(self.open_viewer)
checkbox_hlayout.addWidget(self.raw_frame_numbers)
checkbox_hlayout.addStretch(True)
# endregion Checkboxes
# region Path
self.path_widget = QtWidgets.QWidget()
self.browse = QtWidgets.QPushButton("Browse")
self.file_path = QtWidgets.QLineEdit()
self.file_path.setPlaceholderText("(not set; using scene name)")
tip = "Right click in the text field to insert tokens"
self.file_path.setToolTip(tip)
self.file_path.setStatusTip(tip)
self.file_path.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.file_path.customContextMenuRequested.connect(self.show_token_menu)
path_hlayout = QtWidgets.QHBoxLayout()
path_hlayout.setContentsMargins(0, 0, 0, 0)
path_label = QtWidgets.QLabel("Path:")
path_label.setFixedWidth(30)
path_hlayout.addWidget(path_label)
path_hlayout.addWidget(self.file_path)
path_hlayout.addWidget(self.browse)
self.path_widget.setLayout(path_hlayout)
# endregion Path
# region Recent Playblast
self.play_recent = QtWidgets.QPushButton("Play recent playblast")
self.recent_menu = QtWidgets.QMenu()
self.play_recent.setMenu(self.recent_menu)
# endregion Recent Playblast
self._layout.addLayout(checkbox_hlayout)
self._layout.addWidget(self.path_widget)
self._layout.addWidget(self.play_recent)
# Signals / connections
self.browse.clicked.connect(self.show_browse_dialog)
self.file_path.textChanged.connect(self.options_changed)
self.save_file.stateChanged.connect(self.options_changed)
self.raw_frame_numbers.stateChanged.connect(self.options_changed)
self.save_file.stateChanged.connect(self.on_save_changed)
# Ensure state is up-to-date with current settings
self.on_save_changed()
def on_save_changed(self):
"""Update the visibility of the path field"""
state = self.save_file.isChecked()
if state:
self.path_widget.show()
else:
self.path_widget.hide()
def show_browse_dialog(self):
"""Set the filepath using a browser dialog.
:return: None
"""
path = lib.browse()
if not path:
return
# Maya's browser return Linux based file paths to ensure Windows is
# supported we use normpath
path = os.path.normpath(path)
self.file_path.setText(path)
def add_playblast(self, item):
"""
Add an item to the previous playblast menu
:param item: full path to a playblast file
:type item: str
:return: None
"""
# If item already in the recent playblasts remove it so we are
# sure to add it as the new first most-recent
try:
self.recent_playblasts.remove(item)
except ValueError:
pass
# Add as first in the recent playblasts
self.recent_playblasts.insert(0, item)
# Ensure the playblast list is never longer than maximum amount
# by removing the older entries that are at the end of the list
if len(self.recent_playblasts) > self.max_recent_playblasts:
del self.recent_playblasts[self.max_recent_playblasts:]
# Rebuild the actions menu
self.recent_menu.clear()
for playblast in self.recent_playblasts:
action = IoAction(parent=self, filepath=playblast)
self.recent_menu.addAction(action)
def on_playblast_finished(self, options):
"""Take action after the play blast is done"""
playblast_file = options['filename']
if not playblast_file:
return
self.add_playblast(playblast_file)
def get_outputs(self):
"""Get the plugin outputs that matches `capture.capture` arguments
Returns:
dict: Plugin outputs
"""
output = {"filename": None,
"raw_frame_numbers": self.raw_frame_numbers.isChecked(),
"viewer": self.open_viewer.isChecked()}
save = self.save_file.isChecked()
if not save:
return output
# get path, if nothing is set fall back to default
# project/images/playblast
path = self.file_path.text()
if not path:
path = lib.default_output()
output["filename"] = path
return output
def get_inputs(self, as_preset):
inputs = {"name": self.file_path.text(),
"save_file": self.save_file.isChecked(),
"open_finished": self.open_viewer.isChecked(),
"recent_playblasts": self.recent_playblasts,
"raw_frame_numbers": self.raw_frame_numbers.isChecked()}
if as_preset:
inputs["recent_playblasts"] = []
return inputs
def apply_inputs(self, settings):
directory = settings.get("name", None)
save_file = settings.get("save_file", True)
open_finished = settings.get("open_finished", True)
raw_frame_numbers = settings.get("raw_frame_numbers", False)
previous_playblasts = settings.get("recent_playblasts", [])
self.save_file.setChecked(save_file)
self.open_viewer.setChecked(open_finished)
self.raw_frame_numbers.setChecked(raw_frame_numbers)
for playblast in reversed(previous_playblasts):
self.add_playblast(playblast)
self.file_path.setText(directory)
def token_menu(self):
"""
Build the token menu based on the registered tokens
:returns: Menu
:rtype: QtWidgets.QMenu
"""
menu = QtWidgets.QMenu(self)
registered_tokens = tokens.list_tokens()
for token, value in registered_tokens.items():
label = "{} \t{}".format(token, value['label'])
action = QtWidgets.QAction(label, menu)
fn = partial(self.file_path.insert, token)
action.triggered.connect(fn)
menu.addAction(action)
return menu
def show_token_menu(self, pos):
"""Show custom manu on position of widget"""
menu = self.token_menu()
globalpos = QtCore.QPoint(self.file_path.mapToGlobal(pos))
menu.exec_(globalpos)

View file

@ -0,0 +1,48 @@
from capture_gui.vendor.Qt import QtCore, QtWidgets
import capture_gui.plugin
class PanZoomPlugin(capture_gui.plugin.Plugin):
"""Pan/Zoom widget.
Allows to toggle whether you want to playblast with the camera's pan/zoom
state or disable it during the playblast. When "Use pan/zoom from camera"
is *not* checked it will force disable pan/zoom.
"""
id = "PanZoom"
label = "Pan/Zoom"
section = "config"
order = 110
def __init__(self, parent=None):
super(PanZoomPlugin, self).__init__(parent=parent)
self._layout = QtWidgets.QHBoxLayout()
self._layout.setContentsMargins(5, 0, 5, 0)
self.setLayout(self._layout)
self.pan_zoom = QtWidgets.QCheckBox("Use pan/zoom from camera")
self.pan_zoom.setChecked(True)
self._layout.addWidget(self.pan_zoom)
self.pan_zoom.stateChanged.connect(self.options_changed)
def get_outputs(self):
if not self.pan_zoom.isChecked():
return {"camera_options": {
"panZoomEnabled": 1,
"horizontalPan": 0.0,
"verticalPan": 0.0,
"zoom": 1.0}
}
else:
return {}
def apply_inputs(self, settings):
self.pan_zoom.setChecked(settings.get("pan_zoom", True))
def get_inputs(self, as_preset):
return {"pan_zoom": self.pan_zoom.isChecked()}

View file

@ -0,0 +1,104 @@
import maya.cmds as cmds
from capture_gui.vendor.Qt import QtCore, QtWidgets
import capture_gui.lib as lib
import capture_gui.plugin
class RendererPlugin(capture_gui.plugin.Plugin):
"""Renderer plugin to control the used playblast renderer for viewport"""
id = "Renderer"
label = "Renderer"
section = "config"
order = 60
def __init__(self, parent=None):
super(RendererPlugin, self).__init__(parent=parent)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(layout)
# Get active renderers for viewport
self._renderers = self.get_renderers()
# Create list of renderers
self.renderers = QtWidgets.QComboBox()
self.renderers.addItems(self._renderers.keys())
layout.addWidget(self.renderers)
self.apply_inputs(self.get_defaults())
# Signals
self.renderers.currentIndexChanged.connect(self.options_changed)
def get_current_renderer(self):
"""Get current renderer by internal name (non-UI)
Returns:
str: Name of renderer.
"""
renderer_ui = self.renderers.currentText()
renderer = self._renderers.get(renderer_ui, None)
if renderer is None:
raise RuntimeError("No valid renderer: {0}".format(renderer_ui))
return renderer
def get_renderers(self):
"""Collect all available renderers for playblast"""
active_editor = lib.get_active_editor()
renderers_ui = cmds.modelEditor(active_editor,
query=True,
rendererListUI=True)
renderers_id = cmds.modelEditor(active_editor,
query=True,
rendererList=True)
renderers = dict(zip(renderers_ui, renderers_id))
renderers.pop("Stub Renderer")
return renderers
def get_defaults(self):
return {"rendererName": "vp2Renderer"}
def get_inputs(self, as_preset):
return {"rendererName": self.get_current_renderer()}
def get_outputs(self):
"""Get the plugin outputs that matches `capture.capture` arguments
Returns:
dict: Plugin outputs
"""
return {
"viewport_options": {
"rendererName": self.get_current_renderer()
}
}
def apply_inputs(self, inputs):
"""Apply previous settings or settings from a preset
Args:
inputs (dict): Plugin input settings
Returns:
None
"""
reverse_lookup = {value: key for key, value in self._renderers.items()}
renderer = inputs.get("rendererName", "vp2Renderer")
renderer_ui = reverse_lookup.get(renderer)
if renderer_ui:
index = self.renderers.findText(renderer_ui)
self.renderers.setCurrentIndex(index)
else:
self.renderers.setCurrentIndex(1)

View file

@ -0,0 +1,199 @@
import math
from functools import partial
import maya.cmds as cmds
from capture_gui.vendor.Qt import QtCore, QtWidgets
import capture_gui.lib as lib
import capture_gui.plugin
class ResolutionPlugin(capture_gui.plugin.Plugin):
"""Resolution widget.
Allows to set scale based on set of options.
"""
id = "Resolution"
section = "app"
order = 20
resolution_changed = QtCore.Signal()
ScaleWindow = "From Window"
ScaleRenderSettings = "From Render Settings"
ScaleCustom = "Custom"
def __init__(self, parent=None):
super(ResolutionPlugin, self).__init__(parent=parent)
self._layout = QtWidgets.QVBoxLayout()
self._layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(self._layout)
# Scale
self.mode = QtWidgets.QComboBox()
self.mode.addItems([self.ScaleWindow,
self.ScaleRenderSettings,
self.ScaleCustom])
self.mode.setCurrentIndex(1) # Default: From render settings
# Custom width/height
self.resolution = QtWidgets.QWidget()
self.resolution.setContentsMargins(0, 0, 0, 0)
resolution_layout = QtWidgets.QHBoxLayout()
resolution_layout.setContentsMargins(0, 0, 0, 0)
resolution_layout.setSpacing(6)
self.resolution.setLayout(resolution_layout)
width_label = QtWidgets.QLabel("Width")
width_label.setFixedWidth(40)
self.width = QtWidgets.QSpinBox()
self.width.setMinimum(0)
self.width.setMaximum(99999)
self.width.setValue(1920)
heigth_label = QtWidgets.QLabel("Height")
heigth_label.setFixedWidth(40)
self.height = QtWidgets.QSpinBox()
self.height.setMinimum(0)
self.height.setMaximum(99999)
self.height.setValue(1080)
resolution_layout.addWidget(width_label)
resolution_layout.addWidget(self.width)
resolution_layout.addWidget(heigth_label)
resolution_layout.addWidget(self.height)
self.scale_result = QtWidgets.QLineEdit()
self.scale_result.setReadOnly(True)
# Percentage
self.percent_label = QtWidgets.QLabel("Scale")
self.percent = QtWidgets.QDoubleSpinBox()
self.percent.setMinimum(0.01)
self.percent.setValue(1.0) # default value
self.percent.setSingleStep(0.05)
self.percent_presets = QtWidgets.QHBoxLayout()
self.percent_presets.setSpacing(4)
for value in [0.25, 0.5, 0.75, 1.0, 2.0]:
btn = QtWidgets.QPushButton(str(value))
self.percent_presets.addWidget(btn)
btn.setFixedWidth(35)
btn.clicked.connect(partial(self.percent.setValue, value))
self.percent_layout = QtWidgets.QHBoxLayout()
self.percent_layout.addWidget(self.percent_label)
self.percent_layout.addWidget(self.percent)
self.percent_layout.addLayout(self.percent_presets)
# Resulting scale display
self._layout.addWidget(self.mode)
self._layout.addWidget(self.resolution)
self._layout.addLayout(self.percent_layout)
self._layout.addWidget(self.scale_result)
# refresh states
self.on_mode_changed()
self.on_resolution_changed()
# connect signals
self.mode.currentIndexChanged.connect(self.on_mode_changed)
self.mode.currentIndexChanged.connect(self.on_resolution_changed)
self.percent.valueChanged.connect(self.on_resolution_changed)
self.width.valueChanged.connect(self.on_resolution_changed)
self.height.valueChanged.connect(self.on_resolution_changed)
# Connect options changed
self.mode.currentIndexChanged.connect(self.options_changed)
self.percent.valueChanged.connect(self.options_changed)
self.width.valueChanged.connect(self.options_changed)
self.height.valueChanged.connect(self.options_changed)
def on_mode_changed(self):
"""Update the width/height enabled state when mode changes"""
if self.mode.currentText() != self.ScaleCustom:
self.width.setEnabled(False)
self.height.setEnabled(False)
self.resolution.hide()
else:
self.width.setEnabled(True)
self.height.setEnabled(True)
self.resolution.show()
def _get_output_resolution(self):
options = self.get_outputs()
return int(options["width"]), int(options["height"])
def on_resolution_changed(self):
"""Update the resulting resolution label"""
width, height = self._get_output_resolution()
label = "Result: {0}x{1}".format(width, height)
self.scale_result.setText(label)
# Update label
self.label = "Resolution ({0}x{1})".format(width, height)
self.label_changed.emit(self.label)
def get_outputs(self):
"""Return width x height defined by the combination of settings
Returns:
dict: width and height key values
"""
mode = self.mode.currentText()
panel = lib.get_active_editor()
if mode == self.ScaleCustom:
width = self.width.value()
height = self.height.value()
elif mode == self.ScaleRenderSettings:
# width height from render resolution
width = cmds.getAttr("defaultResolution.width")
height = cmds.getAttr("defaultResolution.height")
elif mode == self.ScaleWindow:
# width height from active view panel size
if not panel:
# No panel would be passed when updating in the UI as such
# the resulting resolution can't be previewed. But this should
# never happen when starting the capture.
width = 0
height = 0
else:
width = cmds.control(panel, query=True, width=True)
height = cmds.control(panel, query=True, height=True)
else:
raise NotImplementedError("Unsupported scale mode: "
"{0}".format(mode))
scale = [width, height]
percentage = self.percent.value()
scale = [math.floor(x * percentage) for x in scale]
return {"width": scale[0], "height": scale[1]}
def get_inputs(self, as_preset):
return {"mode": self.mode.currentText(),
"width": self.width.value(),
"height": self.height.value(),
"percent": self.percent.value()}
def apply_inputs(self, settings):
# get value else fall back to default values
mode = settings.get("mode", self.ScaleRenderSettings)
width = int(settings.get("width", 1920))
height = int(settings.get("height", 1080))
percent = float(settings.get("percent", 1.0))
# set values
self.mode.setCurrentIndex(self.mode.findText(mode))
self.width.setValue(width)
self.height.setValue(height)
self.percent.setValue(percent)

View file

@ -0,0 +1,292 @@
import sys
import logging
import re
import maya.OpenMaya as om
from capture_gui.vendor.Qt import QtCore, QtWidgets
import capture_gui.lib
import capture_gui.plugin
log = logging.getLogger("Time Range")
def parse_frames(string):
"""Parse the resulting frames list from a frame list string.
Examples
>>> parse_frames("0-3;30")
[0, 1, 2, 3, 30]
>>> parse_frames("0,2,4,-10")
[0, 2, 4, -10]
>>> parse_frames("-10--5,-2")
[-10, -9, -8, -7, -6, -5, -2]
Args:
string (str): The string to parse for frames.
Returns:
list: A list of frames
"""
result = list()
if not string.strip():
raise ValueError("Can't parse an empty frame string.")
if not re.match("^[-0-9,; ]*$", string):
raise ValueError("Invalid symbols in frame string: {}".format(string))
for raw in re.split(";|,", string):
# Skip empty elements
value = raw.strip().replace(" ", "")
if not value:
continue
# Check for sequences (1-20) including negatives (-10--8)
sequence = re.search("(-?[0-9]+)-(-?[0-9]+)", value)
# Sequence
if sequence:
start, end = sequence.groups()
frames = range(int(start), int(end) + 1)
result.extend(frames)
# Single frame
else:
try:
frame = int(value)
except ValueError:
raise ValueError("Invalid frame description: "
"'{0}'".format(value))
result.append(frame)
if not result:
# This happens when only spaces are entered with a separator like `,` or `;`
raise ValueError("Unable to parse any frames from string: {}".format(string))
return result
class TimePlugin(capture_gui.plugin.Plugin):
"""Widget for time based options"""
id = "Time Range"
section = "app"
order = 30
RangeTimeSlider = "Time Slider"
RangeStartEnd = "Start/End"
CurrentFrame = "Current Frame"
CustomFrames = "Custom Frames"
def __init__(self, parent=None):
super(TimePlugin, self).__init__(parent=parent)
self._event_callbacks = list()
self._layout = QtWidgets.QHBoxLayout()
self._layout.setContentsMargins(5, 0, 5, 0)
self.setLayout(self._layout)
self.mode = QtWidgets.QComboBox()
self.mode.addItems([self.RangeTimeSlider,
self.RangeStartEnd,
self.CurrentFrame,
self.CustomFrames])
frame_input_height = 20
self.start = QtWidgets.QSpinBox()
self.start.setRange(-sys.maxint, sys.maxint)
self.start.setFixedHeight(frame_input_height)
self.end = QtWidgets.QSpinBox()
self.end.setRange(-sys.maxint, sys.maxint)
self.end.setFixedHeight(frame_input_height)
# unique frames field
self.custom_frames = QtWidgets.QLineEdit()
self.custom_frames.setFixedHeight(frame_input_height)
self.custom_frames.setPlaceholderText("Example: 1-20,25;50;75,100-150")
self.custom_frames.setVisible(False)
self._layout.addWidget(self.mode)
self._layout.addWidget(self.start)
self._layout.addWidget(self.end)
self._layout.addWidget(self.custom_frames)
# Connect callbacks to ensure start is never higher then end
# and the end is never lower than start
self.end.valueChanged.connect(self._ensure_start)
self.start.valueChanged.connect(self._ensure_end)
self.on_mode_changed() # force enabled state refresh
self.mode.currentIndexChanged.connect(self.on_mode_changed)
self.start.valueChanged.connect(self.on_mode_changed)
self.end.valueChanged.connect(self.on_mode_changed)
self.custom_frames.textChanged.connect(self.on_mode_changed)
def _ensure_start(self, value):
self.start.setValue(min(self.start.value(), value))
def _ensure_end(self, value):
self.end.setValue(max(self.end.value(), value))
def on_mode_changed(self, emit=True):
"""Update the GUI when the user updated the time range or settings.
Arguments:
emit (bool): Whether to emit the options changed signal
Returns:
None
"""
mode = self.mode.currentText()
if mode == self.RangeTimeSlider:
start, end = capture_gui.lib.get_time_slider_range()
self.start.setEnabled(False)
self.end.setEnabled(False)
self.start.setVisible(True)
self.end.setVisible(True)
self.custom_frames.setVisible(False)
mode_values = int(start), int(end)
elif mode == self.RangeStartEnd:
self.start.setEnabled(True)
self.end.setEnabled(True)
self.start.setVisible(True)
self.end.setVisible(True)
self.custom_frames.setVisible(False)
mode_values = self.start.value(), self.end.value()
elif mode == self.CustomFrames:
self.start.setVisible(False)
self.end.setVisible(False)
self.custom_frames.setVisible(True)
mode_values = "({})".format(self.custom_frames.text())
# ensure validation state for custom frames
self.validate()
else:
self.start.setEnabled(False)
self.end.setEnabled(False)
self.start.setVisible(True)
self.end.setVisible(True)
self.custom_frames.setVisible(False)
currentframe = int(capture_gui.lib.get_current_frame())
mode_values = "({})".format(currentframe)
# Update label
self.label = "Time Range {}".format(mode_values)
self.label_changed.emit(self.label)
if emit:
self.options_changed.emit()
def validate(self):
errors = []
if self.mode.currentText() == self.CustomFrames:
# Reset
self.custom_frames.setStyleSheet("")
try:
parse_frames(self.custom_frames.text())
except ValueError as exc:
errors.append("{} : Invalid frame description: "
"{}".format(self.id, exc))
self.custom_frames.setStyleSheet(self.highlight)
return errors
def get_outputs(self, panel=""):
"""Get the plugin outputs that matches `capture.capture` arguments
Returns:
dict: Plugin outputs
"""
mode = self.mode.currentText()
frames = None
if mode == self.RangeTimeSlider:
start, end = capture_gui.lib.get_time_slider_range()
elif mode == self.RangeStartEnd:
start = self.start.value()
end = self.end.value()
elif mode == self.CurrentFrame:
frame = capture_gui.lib.get_current_frame()
start = frame
end = frame
elif mode == self.CustomFrames:
frames = parse_frames(self.custom_frames.text())
start = None
end = None
else:
raise NotImplementedError("Unsupported time range mode: "
"{0}".format(mode))
return {"start_frame": start,
"end_frame": end,
"frame": frames}
def get_inputs(self, as_preset):
return {"time": self.mode.currentText(),
"start_frame": self.start.value(),
"end_frame": self.end.value(),
"frame": self.custom_frames.text()}
def apply_inputs(self, settings):
# get values
mode = self.mode.findText(settings.get("time", self.RangeTimeSlider))
startframe = settings.get("start_frame", 1)
endframe = settings.get("end_frame", 120)
custom_frames = settings.get("frame", None)
# set values
self.mode.setCurrentIndex(mode)
self.start.setValue(int(startframe))
self.end.setValue(int(endframe))
if custom_frames is not None:
self.custom_frames.setText(custom_frames)
def initialize(self):
self._register_callbacks()
def uninitialize(self):
self._remove_callbacks()
def _register_callbacks(self):
"""Register maya time and playback range change callbacks.
Register callbacks to ensure Capture GUI reacts to changes in
the Maya GUI in regards to time slider and current frame
"""
callback = lambda x: self.on_mode_changed(emit=False)
# this avoid overriding the ids on re-run
currentframe = om.MEventMessage.addEventCallback("timeChanged",
callback)
timerange = om.MEventMessage.addEventCallback("playbackRangeChanged",
callback)
self._event_callbacks.append(currentframe)
self._event_callbacks.append(timerange)
def _remove_callbacks(self):
"""Remove callbacks when closing widget"""
for callback in self._event_callbacks:
try:
om.MEventMessage.removeCallback(callback)
except RuntimeError, error:
log.error("Encounter error : {}".format(error))

View file

@ -0,0 +1,292 @@
from capture_gui.vendor.Qt import QtCore, QtWidgets
import capture_gui.plugin
import capture_gui.lib as lib
import capture
class ViewportPlugin(capture_gui.plugin.Plugin):
"""Plugin to apply viewport visibilities and settings"""
id = "Viewport Options"
label = "Viewport Options"
section = "config"
order = 70
def __init__(self, parent=None):
super(ViewportPlugin, self).__init__(parent=parent)
# set inherited attributes
self.setObjectName(self.label)
# custom atttributes
self.show_type_actions = list()
# get information
self.show_types = lib.get_show_object_types()
# set main layout for widget
self._layout = QtWidgets.QVBoxLayout()
self._layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(self._layout)
# build
# region Menus
menus_vlayout = QtWidgets.QHBoxLayout()
# Display Lights
self.display_light_menu = self._build_light_menu()
self.display_light_menu.setFixedHeight(20)
# Show
self.show_types_button = QtWidgets.QPushButton("Show")
self.show_types_button.setFixedHeight(20)
self.show_types_menu = self._build_show_menu()
self.show_types_button.setMenu(self.show_types_menu)
# fill layout
menus_vlayout.addWidget(self.display_light_menu)
menus_vlayout.addWidget(self.show_types_button)
# endregion Menus
# region Checkboxes
checkbox_layout = QtWidgets.QGridLayout()
self.high_quality = QtWidgets.QCheckBox()
self.high_quality.setText("Force Viewport 2.0 + AA")
self.override_viewport = QtWidgets.QCheckBox("Override viewport "
"settings")
self.override_viewport.setChecked(True)
# two sided lighting
self.two_sided_ligthing = QtWidgets.QCheckBox("Two Sided Ligthing")
self.two_sided_ligthing.setChecked(False)
# show
self.shadows = QtWidgets.QCheckBox("Shadows")
self.shadows.setChecked(False)
checkbox_layout.addWidget(self.override_viewport, 0, 0)
checkbox_layout.addWidget(self.high_quality, 0, 1)
checkbox_layout.addWidget(self.two_sided_ligthing, 1, 0)
checkbox_layout.addWidget(self.shadows, 1, 1)
# endregion Checkboxes
self._layout.addLayout(checkbox_layout)
self._layout.addLayout(menus_vlayout)
self.connections()
def connections(self):
self.high_quality.stateChanged.connect(self.options_changed)
self.override_viewport.stateChanged.connect(self.options_changed)
self.override_viewport.stateChanged.connect(self.on_toggle_override)
self.two_sided_ligthing.stateChanged.connect(self.options_changed)
self.shadows.stateChanged.connect(self.options_changed)
self.display_light_menu.currentIndexChanged.connect(
self.options_changed
)
def _build_show_menu(self):
"""Build the menu to select which object types are shown in the output.
Returns:
QtGui.QMenu: The visibilities "show" menu.
"""
menu = QtWidgets.QMenu(self)
menu.setObjectName("ShowShapesMenu")
menu.setWindowTitle("Show")
menu.setFixedWidth(180)
menu.setTearOffEnabled(True)
# Show all check
toggle_all = QtWidgets.QAction(menu, text="All")
toggle_none = QtWidgets.QAction(menu, text="None")
menu.addAction(toggle_all)
menu.addAction(toggle_none)
menu.addSeparator()
# add plugin shapes if any
for shape in self.show_types:
action = QtWidgets.QAction(menu, text=shape)
action.setCheckable(True)
# emit signal when the state is changed of the checkbox
action.toggled.connect(self.options_changed)
menu.addAction(action)
self.show_type_actions.append(action)
# connect signals
toggle_all.triggered.connect(self.toggle_all_visbile)
toggle_none.triggered.connect(self.toggle_all_hide)
return menu
def _build_light_menu(self):
"""Build lighting menu.
Create the menu items for the different types of lighting for
in the viewport
Returns:
None
"""
menu = QtWidgets.QComboBox(self)
# names cane be found in
display_lights = (("Use Default Lighting", "default"),
("Use All Lights", "all"),
("Use Selected Lights", "active"),
("Use Flat Lighting", "flat"),
("Use No Lights", "none"))
for label, name in display_lights:
menu.addItem(label, userData=name)
return menu
def on_toggle_override(self):
"""Enable or disable show menu when override is checked"""
state = self.override_viewport.isChecked()
self.show_types_button.setEnabled(state)
self.high_quality.setEnabled(state)
self.display_light_menu.setEnabled(state)
self.shadows.setEnabled(state)
self.two_sided_ligthing.setEnabled(state)
def toggle_all_visbile(self):
"""Set all object types off or on depending on the state"""
for action in self.show_type_actions:
action.setChecked(True)
def toggle_all_hide(self):
"""Set all object types off or on depending on the state"""
for action in self.show_type_actions:
action.setChecked(False)
def get_show_inputs(self):
"""Return checked state of show menu items
Returns:
dict: The checked show states in the widget.
"""
show_inputs = {}
# get all checked objects
for action in self.show_type_actions:
label = action.text()
name = self.show_types.get(label, None)
if name is None:
continue
show_inputs[name] = action.isChecked()
return show_inputs
def get_displaylights(self):
"""Get and parse the currently selected displayLights options.
Returns:
dict: The display light options
"""
indx = self.display_light_menu.currentIndex()
return {"displayLights": self.display_light_menu.itemData(indx),
"shadows": self.shadows.isChecked(),
"twoSidedLighting": self.two_sided_ligthing.isChecked()}
def get_inputs(self, as_preset):
"""Return the widget options
Returns:
dict: The input settings of the widgets.
"""
inputs = {"high_quality": self.high_quality.isChecked(),
"override_viewport_options": self.override_viewport.isChecked(),
"displayLights": self.display_light_menu.currentIndex(),
"shadows": self.shadows.isChecked(),
"twoSidedLighting": self.two_sided_ligthing.isChecked()}
inputs.update(self.get_show_inputs())
return inputs
def apply_inputs(self, inputs):
"""Apply the saved inputs from the inputs configuration
Arguments:
settings (dict): The input settings to apply.
"""
# get input values directly from input given
override_viewport = inputs.get("override_viewport_options", True)
high_quality = inputs.get("high_quality", True)
displaylight = inputs.get("displayLights", 0) # default lighting
two_sided_ligthing = inputs.get("twoSidedLighting", False)
shadows = inputs.get("shadows", False)
self.high_quality.setChecked(high_quality)
self.override_viewport.setChecked(override_viewport)
self.show_types_button.setEnabled(override_viewport)
# display light menu
self.display_light_menu.setCurrentIndex(displaylight)
self.shadows.setChecked(shadows)
self.two_sided_ligthing.setChecked(two_sided_ligthing)
for action in self.show_type_actions:
system_name = self.show_types[action.text()]
state = inputs.get(system_name, True)
action.setChecked(state)
def get_outputs(self):
"""Get the plugin outputs that matches `capture.capture` arguments
Returns:
dict: Plugin outputs
"""
outputs = dict()
high_quality = self.high_quality.isChecked()
override_viewport_options = self.override_viewport.isChecked()
if override_viewport_options:
outputs['viewport2_options'] = dict()
outputs['viewport_options'] = dict()
if high_quality:
# force viewport 2.0 and AA
outputs['viewport_options']['rendererName'] = 'vp2Renderer'
outputs['viewport2_options']['multiSampleEnable'] = True
outputs['viewport2_options']['multiSampleCount'] = 8
show_per_type = self.get_show_inputs()
display_lights = self.get_displaylights()
outputs['viewport_options'].update(show_per_type)
outputs['viewport_options'].update(display_lights)
else:
# TODO: When this fails we should give the user a warning
# Use settings from the active viewport
outputs = capture.parse_active_view()
# Remove the display options and camera attributes
outputs.pop("display_options", None)
outputs.pop("camera", None)
# Remove the current renderer because there's already
# renderer plug-in handling that
outputs["viewport_options"].pop("rendererName", None)
# Remove all camera options except depth of field
dof = outputs["camera_options"]["depthOfField"]
outputs["camera_options"] = {"depthOfField": dof}
return outputs

105
pype/vendor/capture_gui/presets.py vendored Normal file
View file

@ -0,0 +1,105 @@
import glob
import os
import logging
_registered_paths = []
log = logging.getLogger("Presets")
def discover(paths=None):
"""Get the full list of files found in the registered folders
Args:
paths (list, Optional): directories which host preset files or None.
When None (default) it will list from the registered preset paths.
Returns:
list: valid .json preset file paths.
"""
presets = []
for path in paths or preset_paths():
path = os.path.normpath(path)
if not os.path.isdir(path):
continue
# check for json files
glob_query = os.path.abspath(os.path.join(path, "*.json"))
filenames = glob.glob(glob_query)
for filename in filenames:
# skip private files
if filename.startswith("_"):
continue
# check for file size
if not check_file_size(filename):
log.warning("Filesize is smaller than 1 byte for file '%s'",
filename)
continue
if filename not in presets:
presets.append(filename)
return presets
def check_file_size(filepath):
"""Check if filesize of the given file is bigger than 1.0 byte
Args:
filepath (str): full filepath of the file to check
Returns:
bool: Whether bigger than 1 byte.
"""
file_stats = os.stat(filepath)
if file_stats.st_size < 1:
return False
return True
def preset_paths():
"""Return existing registered preset paths
Returns:
list: List of full paths.
"""
paths = list()
for path in _registered_paths:
# filter duplicates
if path in paths:
continue
if not os.path.exists(path):
continue
paths.append(path)
return paths
def register_preset_path(path):
"""Add filepath to registered presets
:param path: the directory of the preset file(s)
:type path: str
:return:
"""
if path in _registered_paths:
return log.warning("Path already registered: %s", path)
_registered_paths.append(path)
return path
# Register default user folder
user_folder = os.path.expanduser("~")
capture_gui_presets = os.path.join(user_folder, "CaptureGUI", "presets")
register_preset_path(capture_gui_presets)

Binary file not shown.

After

Width:  |  Height:  |  Size: 870 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 815 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 976 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 B

68
pype/vendor/capture_gui/tokens.py vendored Normal file
View file

@ -0,0 +1,68 @@
"""Token system
The capture gui application will format tokens in the filename.
The tokens can be registered using `register_token`
"""
from . import lib
_registered_tokens = dict()
def format_tokens(string, options):
"""Replace the tokens with the correlated strings
Arguments:
string (str): filename of the playblast with tokens.
options (dict): The parsed capture options.
Returns:
str: The formatted filename with all tokens resolved
"""
if not string:
return string
for token, value in _registered_tokens.items():
if token in string:
func = value['func']
string = string.replace(token, func(options))
return string
def register_token(token, func, label=""):
assert token.startswith("<") and token.endswith(">")
assert callable(func)
_registered_tokens[token] = {"func": func, "label": label}
def list_tokens():
return _registered_tokens.copy()
# register default tokens
# scene based tokens
def _camera_token(options):
"""Return short name of camera from capture options"""
camera = options['camera']
camera = camera.rsplit("|", 1)[-1] # use short name
camera = camera.replace(":", "_") # namespace `:` to `_`
return camera
register_token("<Camera>", _camera_token,
label="Insert camera name")
register_token("<Scene>", lambda options: lib.get_current_scenename() or "playblast",
label="Insert current scene name")
register_token("<RenderLayer>", lambda options: lib.get_current_renderlayer(),
label="Insert active render layer name")
# project based tokens
register_token("<Images>",
lambda options: lib.get_project_rule("images"),
label="Insert image directory of set project")
register_token("<Movies>",
lambda options: lib.get_project_rule("movie"),
label="Insert movies directory of set project")

1030
pype/vendor/capture_gui/vendor/Qt.py vendored Normal file

File diff suppressed because it is too large Load diff

View file

9
pype/vendor/capture_gui/version.py vendored Normal file
View file

@ -0,0 +1,9 @@
VERSION_MAJOR = 1
VERSION_MINOR = 5
VERSION_PATCH = 0
version = '{}.{}.{}'.format(VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH)
__version__ = version
__all__ = ['version', 'version_info', '__version__']