return capture to vendors

This commit is contained in:
Milan Kolar 2019-11-22 18:15:57 +01:00
parent 6e2fdb880c
commit f63dd1c048
27 changed files with 6007 additions and 0 deletions

833
pype/vendor/capture.py vendored Normal file
View file

@ -0,0 +1,833 @@
"""Maya Capture
Playblasting with independent viewport, camera and display options
"""
import re
import sys
import contextlib
from maya import cmds
from maya import mel
try:
from PySide2 import QtGui, QtWidgets
except ImportError:
from PySide import QtGui
QtWidgets = QtGui
version_info = (2, 3, 0)
__version__ = "%s.%s.%s" % version_info
__license__ = "MIT"
def capture(camera=None,
width=None,
height=None,
filename=None,
start_frame=None,
end_frame=None,
frame=None,
format='qt',
compression='H.264',
quality=100,
off_screen=False,
viewer=True,
show_ornaments=True,
sound=None,
isolate=None,
maintain_aspect_ratio=True,
overwrite=False,
frame_padding=4,
raw_frame_numbers=False,
camera_options=None,
display_options=None,
viewport_options=None,
viewport2_options=None,
complete_filename=None):
"""Playblast in an independent panel
Arguments:
camera (str, optional): Name of camera, defaults to "persp"
width (int, optional): Width of output in pixels
height (int, optional): Height of output in pixels
filename (str, optional): Name of output file. If
none is specified, no files are saved.
start_frame (float, optional): Defaults to current start frame.
end_frame (float, optional): Defaults to current end frame.
frame (float or tuple, optional): A single frame or list of frames.
Use this to capture a single frame or an arbitrary sequence of
frames.
format (str, optional): Name of format, defaults to "qt".
compression (str, optional): Name of compression, defaults to "H.264"
quality (int, optional): The quality of the output, defaults to 100
off_screen (bool, optional): Whether or not to playblast off screen
viewer (bool, optional): Display results in native player
show_ornaments (bool, optional): Whether or not model view ornaments
(e.g. axis icon, grid and HUD) should be displayed.
sound (str, optional): Specify the sound node to be used during
playblast. When None (default) no sound will be used.
isolate (list): List of nodes to isolate upon capturing
maintain_aspect_ratio (bool, optional): Modify height in order to
maintain aspect ratio.
overwrite (bool, optional): Whether or not to overwrite if file
already exists. If disabled and file exists and error will be
raised.
frame_padding (bool, optional): Number of zeros used to pad file name
for image sequences.
raw_frame_numbers (bool, optional): Whether or not to use the exact
frame numbers from the scene or capture to a sequence starting at
zero. Defaults to False. When set to True `viewer` can't be used
and will be forced to False.
camera_options (dict, optional): Supplied camera options,
using `CameraOptions`
display_options (dict, optional): Supplied display
options, using `DisplayOptions`
viewport_options (dict, optional): Supplied viewport
options, using `ViewportOptions`
viewport2_options (dict, optional): Supplied display
options, using `Viewport2Options`
complete_filename (str, optional): Exact name of output file. Use this
to override the output of `filename` so it excludes frame padding.
Example:
>>> # Launch default capture
>>> capture()
>>> # Launch capture with custom viewport settings
>>> capture('persp', 800, 600,
... viewport_options={
... "displayAppearance": "wireframe",
... "grid": False,
... "polymeshes": True,
... },
... camera_options={
... "displayResolution": True
... }
... )
"""
camera = camera or "persp"
# Ensure camera exists
if not cmds.objExists(camera):
raise RuntimeError("Camera does not exist: {0}".format(camera))
width = width or cmds.getAttr("defaultResolution.width")
height = height or cmds.getAttr("defaultResolution.height")
if maintain_aspect_ratio:
ratio = cmds.getAttr("defaultResolution.deviceAspectRatio")
height = round(width / ratio)
if start_frame is None:
start_frame = cmds.playbackOptions(minTime=True, query=True)
if end_frame is None:
end_frame = cmds.playbackOptions(maxTime=True, query=True)
# (#74) Bugfix: `maya.cmds.playblast` will raise an error when playblasting
# with `rawFrameNumbers` set to True but no explicit `frames` provided.
# Since we always know what frames will be included we can provide it
# explicitly
if raw_frame_numbers and frame is None:
frame = range(int(start_frame), int(end_frame) + 1)
# We need to wrap `completeFilename`, otherwise even when None is provided
# it will use filename as the exact name. Only when lacking as argument
# does it function correctly.
playblast_kwargs = dict()
if complete_filename:
playblast_kwargs['completeFilename'] = complete_filename
if frame is not None:
playblast_kwargs['frame'] = frame
if sound is not None:
playblast_kwargs['sound'] = sound
# We need to raise an error when the user gives a custom frame range with
# negative frames in combination with raw frame numbers. This will result
# in a minimal integer frame number : filename.-2147483648.png for any
# negative rendered frame
if frame and raw_frame_numbers:
check = frame if isinstance(frame, (list, tuple)) else [frame]
if any(f < 0 for f in check):
raise RuntimeError("Negative frames are not supported with "
"raw frame numbers and explicit frame numbers")
# (#21) Bugfix: `maya.cmds.playblast` suffers from undo bug where it
# always sets the currentTime to frame 1. By setting currentTime before
# the playblast call it'll undo correctly.
cmds.currentTime(cmds.currentTime(query=True))
padding = 10 # Extend panel to accommodate for OS window manager
with _independent_panel(width=width + padding,
height=height + padding,
off_screen=off_screen) as panel:
cmds.setFocus(panel)
with contextlib.nested(
_disabled_inview_messages(),
_maintain_camera(panel, camera),
_applied_viewport_options(viewport_options, panel),
_applied_camera_options(camera_options, panel),
_applied_display_options(display_options),
_applied_viewport2_options(viewport2_options),
_isolated_nodes(isolate, panel),
_maintained_time()):
output = cmds.playblast(
compression=compression,
format=format,
percent=100,
quality=quality,
viewer=viewer,
startTime=start_frame,
endTime=end_frame,
offScreen=off_screen,
showOrnaments=show_ornaments,
forceOverwrite=overwrite,
filename=filename,
widthHeight=[width, height],
rawFrameNumbers=raw_frame_numbers,
framePadding=frame_padding,
**playblast_kwargs)
return output
def snap(*args, **kwargs):
"""Single frame playblast in an independent panel.
The arguments of `capture` are all valid here as well, except for
`start_frame` and `end_frame`.
Arguments:
frame (float, optional): The frame to snap. If not provided current
frame is used.
clipboard (bool, optional): Whether to add the output image to the
global clipboard. This allows to easily paste the snapped image
into another application, eg. into Photoshop.
Keywords:
See `capture`.
"""
# capture single frame
frame = kwargs.pop('frame', cmds.currentTime(q=1))
kwargs['start_frame'] = frame
kwargs['end_frame'] = frame
kwargs['frame'] = frame
if not isinstance(frame, (int, float)):
raise TypeError("frame must be a single frame (integer or float). "
"Use `capture()` for sequences.")
# override capture defaults
format = kwargs.pop('format', "image")
compression = kwargs.pop('compression', "png")
viewer = kwargs.pop('viewer', False)
raw_frame_numbers = kwargs.pop('raw_frame_numbers', True)
kwargs['compression'] = compression
kwargs['format'] = format
kwargs['viewer'] = viewer
kwargs['raw_frame_numbers'] = raw_frame_numbers
# pop snap only keyword arguments
clipboard = kwargs.pop('clipboard', False)
# perform capture
output = capture(*args, **kwargs)
def replace(m):
"""Substitute # with frame number"""
return str(int(frame)).zfill(len(m.group()))
output = re.sub("#+", replace, output)
# add image to clipboard
if clipboard:
_image_to_clipboard(output)
return output
CameraOptions = {
"displayGateMask": False,
"displayResolution": False,
"displayFilmGate": False,
"displayFieldChart": False,
"displaySafeAction": False,
"displaySafeTitle": False,
"displayFilmPivot": False,
"displayFilmOrigin": False,
"overscan": 1.0,
"depthOfField": False,
}
DisplayOptions = {
"displayGradient": True,
"background": (0.631, 0.631, 0.631),
"backgroundTop": (0.535, 0.617, 0.702),
"backgroundBottom": (0.052, 0.052, 0.052),
}
# These display options require a different command to be queried and set
_DisplayOptionsRGB = set(["background", "backgroundTop", "backgroundBottom"])
ViewportOptions = {
# renderer
"rendererName": "vp2Renderer",
"fogging": False,
"fogMode": "linear",
"fogDensity": 1,
"fogStart": 1,
"fogEnd": 1,
"fogColor": (0, 0, 0, 0),
"shadows": False,
"displayTextures": True,
"displayLights": "default",
"useDefaultMaterial": False,
"wireframeOnShaded": False,
"displayAppearance": 'smoothShaded',
"selectionHiliteDisplay": False,
"headsUpDisplay": True,
# object display
"imagePlane": True,
"nurbsCurves": False,
"nurbsSurfaces": False,
"polymeshes": True,
"subdivSurfaces": False,
"planes": True,
"cameras": False,
"controlVertices": True,
"lights": False,
"grid": False,
"hulls": True,
"joints": False,
"ikHandles": False,
"deformers": False,
"dynamics": False,
"fluids": False,
"hairSystems": False,
"follicles": False,
"nCloths": False,
"nParticles": False,
"nRigids": False,
"dynamicConstraints": False,
"locators": False,
"manipulators": False,
"dimensions": False,
"handles": False,
"pivots": False,
"textures": False,
"strokes": False
}
Viewport2Options = {
"consolidateWorld": True,
"enableTextureMaxRes": False,
"bumpBakeResolution": 64,
"colorBakeResolution": 64,
"floatingPointRTEnable": True,
"floatingPointRTFormat": 1,
"gammaCorrectionEnable": False,
"gammaValue": 2.2,
"lineAAEnable": False,
"maxHardwareLights": 8,
"motionBlurEnable": False,
"motionBlurSampleCount": 8,
"motionBlurShutterOpenFraction": 0.2,
"motionBlurType": 0,
"multiSampleCount": 8,
"multiSampleEnable": False,
"singleSidedLighting": False,
"ssaoEnable": False,
"ssaoAmount": 1.0,
"ssaoFilterRadius": 16,
"ssaoRadius": 16,
"ssaoSamples": 16,
"textureMaxResolution": 4096,
"threadDGEvaluation": False,
"transparencyAlgorithm": 1,
"transparencyQuality": 0.33,
"useMaximumHardwareLights": True,
"vertexAnimationCache": 0
}
def apply_view(panel, **options):
"""Apply options to panel"""
camera = cmds.modelPanel(panel, camera=True, query=True)
# Display options
display_options = options.get("display_options", {})
for key, value in display_options.iteritems():
if key in _DisplayOptionsRGB:
cmds.displayRGBColor(key, *value)
else:
cmds.displayPref(**{key: value})
# Camera options
camera_options = options.get("camera_options", {})
for key, value in camera_options.iteritems():
cmds.setAttr("{0}.{1}".format(camera, key), value)
# Viewport options
viewport_options = options.get("viewport_options", {})
for key, value in viewport_options.iteritems():
cmds.modelEditor(panel, edit=True, **{key: value})
viewport2_options = options.get("viewport2_options", {})
for key, value in viewport2_options.iteritems():
attr = "hardwareRenderingGlobals.{0}".format(key)
cmds.setAttr(attr, value)
def parse_active_panel():
"""Parse the active modelPanel.
Raises
RuntimeError: When no active modelPanel an error is raised.
Returns:
str: Name of modelPanel
"""
panel = cmds.getPanel(withFocus=True)
# This happens when last focus was on panel
# that got deleted (e.g. `capture()` then `parse_active_view()`)
if not panel or "modelPanel" not in panel:
raise RuntimeError("No active model panel found")
return panel
def parse_active_view():
"""Parse the current settings from the active view"""
panel = parse_active_panel()
return parse_view(panel)
def parse_view(panel):
"""Parse the scene, panel and camera for their current settings
Example:
>>> parse_view("modelPanel1")
Arguments:
panel (str): Name of modelPanel
"""
camera = cmds.modelPanel(panel, query=True, camera=True)
# Display options
display_options = {}
for key in DisplayOptions:
if key in _DisplayOptionsRGB:
display_options[key] = cmds.displayRGBColor(key, query=True)
else:
display_options[key] = cmds.displayPref(query=True, **{key: True})
# Camera options
camera_options = {}
for key in CameraOptions:
camera_options[key] = cmds.getAttr("{0}.{1}".format(camera, key))
# Viewport options
viewport_options = {}
# capture plugin display filters first to ensure we never override
# built-in arguments if ever possible a plugin has similarly named
# plugin display filters (which it shouldn't!)
plugins = cmds.pluginDisplayFilter(query=True, listFilters=True)
for plugin in plugins:
plugin = str(plugin) # unicode->str for simplicity of the dict
state = cmds.modelEditor(panel, query=True, queryPluginObjects=plugin)
viewport_options[plugin] = state
for key in ViewportOptions:
viewport_options[key] = cmds.modelEditor(
panel, query=True, **{key: True})
viewport2_options = {}
for key in Viewport2Options.keys():
attr = "hardwareRenderingGlobals.{0}".format(key)
try:
viewport2_options[key] = cmds.getAttr(attr)
except ValueError:
continue
return {
"camera": camera,
"display_options": display_options,
"camera_options": camera_options,
"viewport_options": viewport_options,
"viewport2_options": viewport2_options
}
def parse_active_scene():
"""Parse active scene for arguments for capture()
*Resolution taken from render settings.
"""
time_control = mel.eval("$gPlayBackSlider = $gPlayBackSlider")
return {
"start_frame": cmds.playbackOptions(minTime=True, query=True),
"end_frame": cmds.playbackOptions(maxTime=True, query=True),
"width": cmds.getAttr("defaultResolution.width"),
"height": cmds.getAttr("defaultResolution.height"),
"compression": cmds.optionVar(query="playblastCompression"),
"filename": (cmds.optionVar(query="playblastFile")
if cmds.optionVar(query="playblastSaveToFile") else None),
"format": cmds.optionVar(query="playblastFormat"),
"off_screen": (True if cmds.optionVar(query="playblastOffscreen")
else False),
"show_ornaments": (True if cmds.optionVar(query="playblastShowOrnaments")
else False),
"quality": cmds.optionVar(query="playblastQuality"),
"sound": cmds.timeControl(time_control, q=True, sound=True) or None
}
def apply_scene(**options):
"""Apply options from scene
Example:
>>> apply_scene({"start_frame": 1009})
Arguments:
options (dict): Scene options
"""
if "start_frame" in options:
cmds.playbackOptions(minTime=options["start_frame"])
if "end_frame" in options:
cmds.playbackOptions(maxTime=options["end_frame"])
if "width" in options:
cmds.setAttr("defaultResolution.width", options["width"])
if "height" in options:
cmds.setAttr("defaultResolution.height", options["height"])
if "compression" in options:
cmds.optionVar(
stringValue=["playblastCompression", options["compression"]])
if "filename" in options:
cmds.optionVar(
stringValue=["playblastFile", options["filename"]])
if "format" in options:
cmds.optionVar(
stringValue=["playblastFormat", options["format"]])
if "off_screen" in options:
cmds.optionVar(
intValue=["playblastFormat", options["off_screen"]])
if "show_ornaments" in options:
cmds.optionVar(
intValue=["show_ornaments", options["show_ornaments"]])
if "quality" in options:
cmds.optionVar(
floatValue=["playblastQuality", options["quality"]])
@contextlib.contextmanager
def _applied_view(panel, **options):
"""Apply options to panel"""
original = parse_view(panel)
apply_view(panel, **options)
try:
yield
finally:
apply_view(panel, **original)
@contextlib.contextmanager
def _independent_panel(width, height, off_screen=False):
"""Create capture-window context without decorations
Arguments:
width (int): Width of panel
height (int): Height of panel
Example:
>>> with _independent_panel(800, 600):
... cmds.capture()
"""
# center panel on screen
screen_width, screen_height = _get_screen_size()
topLeft = [int((screen_height-height)/2.0),
int((screen_width-width)/2.0)]
window = cmds.window(width=width,
height=height,
topLeftCorner=topLeft,
menuBarVisible=False,
titleBar=False,
visible=not off_screen)
cmds.paneLayout()
panel = cmds.modelPanel(menuBarVisible=False,
label='CapturePanel')
# Hide icons under panel menus
bar_layout = cmds.modelPanel(panel, q=True, barLayout=True)
cmds.frameLayout(bar_layout, edit=True, collapse=True)
if not off_screen:
cmds.showWindow(window)
# Set the modelEditor of the modelPanel as the active view so it takes
# the playback focus. Does seem redundant with the `refresh` added in.
editor = cmds.modelPanel(panel, query=True, modelEditor=True)
cmds.modelEditor(editor, edit=True, activeView=True)
# Force a draw refresh of Maya so it keeps focus on the new panel
# This focus is required to force preview playback in the independent panel
cmds.refresh(force=True)
try:
yield panel
finally:
# Delete the panel to fix memory leak (about 5 mb per capture)
cmds.deleteUI(panel, panel=True)
cmds.deleteUI(window)
@contextlib.contextmanager
def _applied_camera_options(options, panel):
"""Context manager for applying `options` to `camera`"""
camera = cmds.modelPanel(panel, query=True, camera=True)
options = dict(CameraOptions, **(options or {}))
old_options = dict()
for opt in options.copy():
try:
old_options[opt] = cmds.getAttr(camera + "." + opt)
except:
sys.stderr.write("Could not get camera attribute "
"for capture: %s" % opt)
options.pop(opt)
for opt, value in options.iteritems():
cmds.setAttr(camera + "." + opt, value)
try:
yield
finally:
if old_options:
for opt, value in old_options.iteritems():
cmds.setAttr(camera + "." + opt, value)
@contextlib.contextmanager
def _applied_display_options(options):
"""Context manager for setting background color display options."""
options = dict(DisplayOptions, **(options or {}))
colors = ['background', 'backgroundTop', 'backgroundBottom']
preferences = ['displayGradient']
# Store current settings
original = {}
for color in colors:
original[color] = cmds.displayRGBColor(color, query=True) or []
for preference in preferences:
original[preference] = cmds.displayPref(
query=True, **{preference: True})
# Apply settings
for color in colors:
value = options[color]
cmds.displayRGBColor(color, *value)
for preference in preferences:
value = options[preference]
cmds.displayPref(**{preference: value})
try:
yield
finally:
# Restore original settings
for color in colors:
cmds.displayRGBColor(color, *original[color])
for preference in preferences:
cmds.displayPref(**{preference: original[preference]})
@contextlib.contextmanager
def _applied_viewport_options(options, panel):
"""Context manager for applying `options` to `panel`"""
options = dict(ViewportOptions, **(options or {}))
# separate the plugin display filter options since they need to
# be set differently (see #55)
plugins = cmds.pluginDisplayFilter(query=True, listFilters=True)
plugin_options = dict()
for plugin in plugins:
if plugin in options:
plugin_options[plugin] = options.pop(plugin)
# default options
cmds.modelEditor(panel, edit=True, **options)
# plugin display filter options
for plugin, state in plugin_options.items():
cmds.modelEditor(panel, edit=True, pluginObjects=(plugin, state))
yield
@contextlib.contextmanager
def _applied_viewport2_options(options):
"""Context manager for setting viewport 2.0 options.
These options are applied by setting attributes on the
"hardwareRenderingGlobals" node.
"""
options = dict(Viewport2Options, **(options or {}))
# Store current settings
original = {}
for opt in options.copy():
try:
original[opt] = cmds.getAttr("hardwareRenderingGlobals." + opt)
except ValueError:
options.pop(opt)
# Apply settings
for opt, value in options.iteritems():
cmds.setAttr("hardwareRenderingGlobals." + opt, value)
try:
yield
finally:
# Restore previous settings
for opt, value in original.iteritems():
cmds.setAttr("hardwareRenderingGlobals." + opt, value)
@contextlib.contextmanager
def _isolated_nodes(nodes, panel):
"""Context manager for isolating `nodes` in `panel`"""
if nodes is not None:
cmds.isolateSelect(panel, state=True)
for obj in nodes:
cmds.isolateSelect(panel, addDagObject=obj)
yield
@contextlib.contextmanager
def _maintained_time():
"""Context manager for preserving (resetting) the time after the context"""
current_time = cmds.currentTime(query=1)
try:
yield
finally:
cmds.currentTime(current_time)
@contextlib.contextmanager
def _maintain_camera(panel, camera):
state = {}
if not _in_standalone():
cmds.lookThru(panel, camera)
else:
state = dict((camera, cmds.getAttr(camera + ".rnd"))
for camera in cmds.ls(type="camera"))
cmds.setAttr(camera + ".rnd", True)
try:
yield
finally:
for camera, renderable in state.iteritems():
cmds.setAttr(camera + ".rnd", renderable)
@contextlib.contextmanager
def _disabled_inview_messages():
"""Disable in-view help messages during the context"""
original = cmds.optionVar(q="inViewMessageEnable")
cmds.optionVar(iv=("inViewMessageEnable", 0))
try:
yield
finally:
cmds.optionVar(iv=("inViewMessageEnable", original))
def _image_to_clipboard(path):
"""Copies the image at path to the system's global clipboard."""
if _in_standalone():
raise Exception("Cannot copy to clipboard from Maya Standalone")
image = QtGui.QImage(path)
clipboard = QtWidgets.QApplication.clipboard()
clipboard.setImage(image, mode=QtGui.QClipboard.Clipboard)
def _get_screen_size():
"""Return available screen size without space occupied by taskbar"""
if _in_standalone():
return [0, 0]
rect = QtWidgets.QDesktopWidget().screenGeometry(-1)
return [rect.width(), rect.height()]
def _in_standalone():
return not hasattr(cmds, "about") or cmds.about(batch=True)
# --------------------------------
#
# Apply version specific settings
#
# --------------------------------
version = mel.eval("getApplicationVersionAsFloat")
if version > 2015:
Viewport2Options.update({
"hwFogAlpha": 1.0,
"hwFogFalloff": 0,
"hwFogDensity": 0.1,
"hwFogEnable": False,
"holdOutDetailMode": 1,
"hwFogEnd": 100.0,
"holdOutMode": True,
"hwFogColorR": 0.5,
"hwFogColorG": 0.5,
"hwFogColorB": 0.5,
"hwFogStart": 0.0,
})
ViewportOptions.update({
"motionTrails": False
})

29
pype/vendor/capture_gui/__init__.py vendored Normal file
View file

@ -0,0 +1,29 @@
from .vendor.Qt import QtWidgets
from . import app
from . import lib
def main(show=True):
"""Convenience method to run the Application inside Maya.
Args:
show (bool): Whether to directly show the instantiated application.
Defaults to True. Set this to False if you want to manage the
application (like callbacks) prior to showing the interface.
Returns:
capture_gui.app.App: The pyblish gui application instance.
"""
# get main maya window to parent widget to
parent = lib.get_maya_main_window()
instance = parent.findChild(QtWidgets.QWidget, app.App.object_name)
if instance:
instance.close()
# launch app
window = app.App(title="Capture GUI", parent=parent)
if show:
window.show()
return window

624
pype/vendor/capture_gui/accordion.py vendored Normal file
View file

@ -0,0 +1,624 @@
from .vendor.Qt import QtCore, QtWidgets, QtGui
class AccordionItem(QtWidgets.QGroupBox):
trigger = QtCore.Signal(bool)
def __init__(self, accordion, title, widget):
QtWidgets.QGroupBox.__init__(self, parent=accordion)
# create the layout
layout = QtWidgets.QVBoxLayout()
layout.setContentsMargins(6, 12, 6, 6)
layout.setSpacing(0)
layout.addWidget(widget)
self._accordianWidget = accordion
self._rolloutStyle = 2
self._dragDropMode = 0
self.setAcceptDrops(True)
self.setLayout(layout)
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.showMenu)
# create custom properties
self._widget = widget
self._collapsed = False
self._collapsible = True
self._clicked = False
self._customData = {}
# set common properties
self.setTitle(title)
def accordionWidget(self):
"""
\remarks grabs the parent item for the accordian widget
\return <blurdev.gui.widgets.accordianwidget.AccordianWidget>
"""
return self._accordianWidget
def customData(self, key, default=None):
"""
\remarks return a custom pointer to information stored with this item
\param key <str>
\param default <variant> default value to return if the key was not found
\return <variant> data
"""
return self._customData.get(str(key), default)
def dragEnterEvent(self, event):
if not self._dragDropMode:
return
source = event.source()
if source != self and source.parent() == self.parent() and isinstance(
source, AccordionItem):
event.acceptProposedAction()
def dragDropRect(self):
return QtCore.QRect(25, 7, 10, 6)
def dragDropMode(self):
return self._dragDropMode
def dragMoveEvent(self, event):
if not self._dragDropMode:
return
source = event.source()
if source != self and source.parent() == self.parent() and isinstance(
source, AccordionItem):
event.acceptProposedAction()
def dropEvent(self, event):
widget = event.source()
layout = self.parent().layout()
layout.insertWidget(layout.indexOf(self), widget)
self._accordianWidget.emitItemsReordered()
def expandCollapseRect(self):
return QtCore.QRect(0, 0, self.width(), 20)
def enterEvent(self, event):
self.accordionWidget().leaveEvent(event)
event.accept()
def leaveEvent(self, event):
self.accordionWidget().enterEvent(event)
event.accept()
def mouseReleaseEvent(self, event):
if self._clicked and self.expandCollapseRect().contains(event.pos()):
self.toggleCollapsed()
event.accept()
else:
event.ignore()
self._clicked = False
def mouseMoveEvent(self, event):
event.ignore()
def mousePressEvent(self, event):
# handle an internal move
# start a drag event
if event.button() == QtCore.Qt.LeftButton and self.dragDropRect().contains(
event.pos()):
# create the pixmap
pixmap = QtGui.QPixmap.grabWidget(self, self.rect())
# create the mimedata
mimeData = QtCore.QMimeData()
mimeData.setText('ItemTitle::%s' % (self.title()))
# create the drag
drag = QtGui.QDrag(self)
drag.setMimeData(mimeData)
drag.setPixmap(pixmap)
drag.setHotSpot(event.pos())
if not drag.exec_():
self._accordianWidget.emitItemDragFailed(self)
event.accept()
# determine if the expand/collapse should occur
elif event.button() == QtCore.Qt.LeftButton and self.expandCollapseRect().contains(
event.pos()):
self._clicked = True
event.accept()
else:
event.ignore()
def isCollapsed(self):
return self._collapsed
def isCollapsible(self):
return self._collapsible
def __drawTriangle(self, painter, x, y):
brush = QtGui.QBrush(QtGui.QColor(255, 255, 255, 160),
QtCore.Qt.SolidPattern)
if not self.isCollapsed():
tl, tr, tp = QtCore.QPoint(x + 9, y + 8), QtCore.QPoint(x + 19,
y + 8), QtCore.QPoint(
x + 14, y + 13.0)
points = [tl, tr, tp]
triangle = QtGui.QPolygon(points)
else:
tl, tr, tp = QtCore.QPoint(x + 11, y + 6), QtCore.QPoint(x + 16,
y + 11), QtCore.QPoint(
x + 11, y + 16.0)
points = [tl, tr, tp]
triangle = QtGui.QPolygon(points)
currentBrush = painter.brush()
painter.setBrush(brush)
painter.drawPolygon(triangle)
painter.setBrush(currentBrush)
def paintEvent(self, event):
painter = QtGui.QPainter()
painter.begin(self)
painter.setRenderHint(painter.Antialiasing)
font = painter.font()
font.setBold(True)
painter.setFont(font)
x = self.rect().x()
y = self.rect().y()
w = self.rect().width() - 1
h = self.rect().height() - 1
r = 8
# draw a rounded style
if self._rolloutStyle == 2:
# draw the text
painter.drawText(x + 33, y + 3, w, 16,
QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop,
self.title())
# draw the triangle
self.__drawTriangle(painter, x, y)
# draw the borders
pen = QtGui.QPen(self.palette().color(QtGui.QPalette.Light))
pen.setWidthF(0.6)
painter.setPen(pen)
painter.drawRoundedRect(x + 1, y + 1, w - 1, h - 1, r, r)
pen.setColor(self.palette().color(QtGui.QPalette.Shadow))
painter.setPen(pen)
painter.drawRoundedRect(x, y, w - 1, h - 1, r, r)
# draw a square style
if self._rolloutStyle == 3:
# draw the text
painter.drawText(x + 33, y + 3, w, 16,
QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop,
self.title())
self.__drawTriangle(painter, x, y)
# draw the borders
pen = QtGui.QPen(self.palette().color(QtGui.QPalette.Light))
pen.setWidthF(0.6)
painter.setPen(pen)
painter.drawRect(x + 1, y + 1, w - 1, h - 1)
pen.setColor(self.palette().color(QtGui.QPalette.Shadow))
painter.setPen(pen)
painter.drawRect(x, y, w - 1, h - 1)
# draw a Maya style
if self._rolloutStyle == 4:
# draw the text
painter.drawText(x + 33, y + 3, w, 16,
QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop,
self.title())
painter.setRenderHint(QtGui.QPainter.Antialiasing, False)
self.__drawTriangle(painter, x, y)
# draw the borders - top
headerHeight = 20
headerRect = QtCore.QRect(x + 1, y + 1, w - 1, headerHeight)
headerRectShadow = QtCore.QRect(x - 1, y - 1, w + 1,
headerHeight + 2)
# Highlight
pen = QtGui.QPen(self.palette().color(QtGui.QPalette.Light))
pen.setWidthF(0.4)
painter.setPen(pen)
painter.drawRect(headerRect)
painter.fillRect(headerRect, QtGui.QColor(255, 255, 255, 18))
# Shadow
pen.setColor(self.palette().color(QtGui.QPalette.Dark))
painter.setPen(pen)
painter.drawRect(headerRectShadow)
if not self.isCollapsed():
# draw the lover border
pen = QtGui.QPen(self.palette().color(QtGui.QPalette.Dark))
pen.setWidthF(0.8)
painter.setPen(pen)
offSet = headerHeight + 3
bodyRect = QtCore.QRect(x, y + offSet, w, h - offSet)
bodyRectShadow = QtCore.QRect(x + 1, y + offSet, w + 1,
h - offSet + 1)
painter.drawRect(bodyRect)
pen.setColor(self.palette().color(QtGui.QPalette.Light))
pen.setWidthF(0.4)
painter.setPen(pen)
painter.drawRect(bodyRectShadow)
# draw a boxed style
elif self._rolloutStyle == 1:
if self.isCollapsed():
arect = QtCore.QRect(x + 1, y + 9, w - 1, 4)
brect = QtCore.QRect(x, y + 8, w - 1, 4)
text = '+'
else:
arect = QtCore.QRect(x + 1, y + 9, w - 1, h - 9)
brect = QtCore.QRect(x, y + 8, w - 1, h - 9)
text = '-'
# draw the borders
pen = QtGui.QPen(self.palette().color(QtGui.QPalette.Light))
pen.setWidthF(0.6)
painter.setPen(pen)
painter.drawRect(arect)
pen.setColor(self.palette().color(QtGui.QPalette.Shadow))
painter.setPen(pen)
painter.drawRect(brect)
painter.setRenderHint(painter.Antialiasing, False)
painter.setBrush(
self.palette().color(QtGui.QPalette.Window).darker(120))
painter.drawRect(x + 10, y + 1, w - 20, 16)
painter.drawText(x + 16, y + 1,
w - 32, 16,
QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter,
text)
painter.drawText(x + 10, y + 1,
w - 20, 16,
QtCore.Qt.AlignCenter,
self.title())
if self.dragDropMode():
rect = self.dragDropRect()
# draw the lines
l = rect.left()
r = rect.right()
cy = rect.center().y()
for y in (cy - 3, cy, cy + 3):
painter.drawLine(l, y, r, y)
painter.end()
def setCollapsed(self, state=True):
if self.isCollapsible():
accord = self.accordionWidget()
accord.setUpdatesEnabled(False)
self._collapsed = state
if state:
self.setMinimumHeight(22)
self.setMaximumHeight(22)
self.widget().setVisible(False)
else:
self.setMinimumHeight(0)
self.setMaximumHeight(1000000)
self.widget().setVisible(True)
self._accordianWidget.emitItemCollapsed(self)
accord.setUpdatesEnabled(True)
def setCollapsible(self, state=True):
self._collapsible = state
def setCustomData(self, key, value):
"""
\remarks set a custom pointer to information stored on this item
\param key <str>
\param value <variant>
"""
self._customData[str(key)] = value
def setDragDropMode(self, mode):
self._dragDropMode = mode
def setRolloutStyle(self, style):
self._rolloutStyle = style
def showMenu(self):
if QtCore.QRect(0, 0, self.width(), 20).contains(
self.mapFromGlobal(QtGui.QCursor.pos())):
self._accordianWidget.emitItemMenuRequested(self)
def rolloutStyle(self):
return self._rolloutStyle
def toggleCollapsed(self):
# enable signaling here
collapse_state = not self.isCollapsed()
self.setCollapsed(collapse_state)
return collapse_state
def widget(self):
return self._widget
class AccordionWidget(QtWidgets.QScrollArea):
"""Accordion style widget.
A collapsible accordion widget like Maya's attribute editor.
This is a modified version bsed on Blur's Accordion Widget to
include a Maya style.
"""
itemCollapsed = QtCore.Signal(AccordionItem)
itemMenuRequested = QtCore.Signal(AccordionItem)
itemDragFailed = QtCore.Signal(AccordionItem)
itemsReordered = QtCore.Signal()
Boxed = 1
Rounded = 2
Square = 3
Maya = 4
NoDragDrop = 0
InternalMove = 1
def __init__(self, parent):
QtWidgets.QScrollArea.__init__(self, parent)
self.setFrameShape(QtWidgets.QScrollArea.NoFrame)
self.setAutoFillBackground(False)
self.setWidgetResizable(True)
self.setMouseTracking(True)
self.verticalScrollBar().setMaximumWidth(10)
widget = QtWidgets.QWidget(self)
# define custom properties
self._rolloutStyle = AccordionWidget.Rounded
self._dragDropMode = AccordionWidget.NoDragDrop
self._scrolling = False
self._scrollInitY = 0
self._scrollInitVal = 0
self._itemClass = AccordionItem
layout = QtWidgets.QVBoxLayout()
layout.setContentsMargins(2, 2, 2, 6)
layout.setSpacing(2)
layout.addStretch(1)
widget.setLayout(layout)
self.setWidget(widget)
def setSpacing(self, spaceInt):
self.widget().layout().setSpacing(spaceInt)
def addItem(self, title, widget, collapsed=False):
self.setUpdatesEnabled(False)
item = self._itemClass(self, title, widget)
item.setRolloutStyle(self.rolloutStyle())
item.setDragDropMode(self.dragDropMode())
layout = self.widget().layout()
layout.insertWidget(layout.count() - 1, item)
layout.setStretchFactor(item, 0)
if collapsed:
item.setCollapsed(collapsed)
self.setUpdatesEnabled(True)
return item
def clear(self):
self.setUpdatesEnabled(False)
layout = self.widget().layout()
while layout.count() > 1:
item = layout.itemAt(0)
# remove the item from the layout
w = item.widget()
layout.removeItem(item)
# close the widget and delete it
w.close()
w.deleteLater()
self.setUpdatesEnabled(True)
def eventFilter(self, object, event):
if event.type() == QtCore.QEvent.MouseButtonPress:
self.mousePressEvent(event)
return True
elif event.type() == QtCore.QEvent.MouseMove:
self.mouseMoveEvent(event)
return True
elif event.type() == QtCore.QEvent.MouseButtonRelease:
self.mouseReleaseEvent(event)
return True
return False
def canScroll(self):
return self.verticalScrollBar().maximum() > 0
def count(self):
return self.widget().layout().count() - 1
def dragDropMode(self):
return self._dragDropMode
def indexOf(self, widget):
"""
\remarks Searches for widget(not including child layouts).
Returns the index of widget, or -1 if widget is not found
\return <int>
"""
layout = self.widget().layout()
for index in range(layout.count()):
if layout.itemAt(index).widget().widget() == widget:
return index
return -1
def isBoxedMode(self):
return self._rolloutStyle == AccordionWidget.Maya
def itemClass(self):
return self._itemClass
def itemAt(self, index):
layout = self.widget().layout()
if 0 <= index and index < layout.count() - 1:
return layout.itemAt(index).widget()
return None
def emitItemCollapsed(self, item):
if not self.signalsBlocked():
self.itemCollapsed.emit(item)
def emitItemDragFailed(self, item):
if not self.signalsBlocked():
self.itemDragFailed.emit(item)
def emitItemMenuRequested(self, item):
if not self.signalsBlocked():
self.itemMenuRequested.emit(item)
def emitItemsReordered(self):
if not self.signalsBlocked():
self.itemsReordered.emit()
def enterEvent(self, event):
if self.canScroll():
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.OpenHandCursor)
def leaveEvent(self, event):
if self.canScroll():
QtWidgets.QApplication.restoreOverrideCursor()
def mouseMoveEvent(self, event):
if self._scrolling:
sbar = self.verticalScrollBar()
smax = sbar.maximum()
# calculate the distance moved for the moust point
dy = event.globalY() - self._scrollInitY
# calculate the percentage that is of the scroll bar
dval = smax * (dy / float(sbar.height()))
# calculate the new value
sbar.setValue(self._scrollInitVal - dval)
event.accept()
def mousePressEvent(self, event):
# handle a scroll event
if event.button() == QtCore.Qt.LeftButton and self.canScroll():
self._scrolling = True
self._scrollInitY = event.globalY()
self._scrollInitVal = self.verticalScrollBar().value()
QtWidgets.QApplication.setOverrideCursor(
QtCore.Qt.ClosedHandCursor)
event.accept()
def mouseReleaseEvent(self, event):
if self._scrolling:
QtWidgets.QApplication.restoreOverrideCursor()
self._scrolling = False
self._scrollInitY = 0
self._scrollInitVal = 0
event.accept()
def moveItemDown(self, index):
layout = self.widget().layout()
if (layout.count() - 1) > (index + 1):
widget = layout.takeAt(index).widget()
layout.insertWidget(index + 1, widget)
def moveItemUp(self, index):
if index > 0:
layout = self.widget().layout()
widget = layout.takeAt(index).widget()
layout.insertWidget(index - 1, widget)
def setBoxedMode(self, state):
if state:
self._rolloutStyle = AccordionWidget.Boxed
else:
self._rolloutStyle = AccordionWidget.Rounded
def setDragDropMode(self, dragDropMode):
self._dragDropMode = dragDropMode
for item in self.findChildren(AccordionItem):
item.setDragDropMode(self._dragDropMode)
def setItemClass(self, itemClass):
self._itemClass = itemClass
def setRolloutStyle(self, rolloutStyle):
self._rolloutStyle = rolloutStyle
for item in self.findChildren(AccordionItem):
item.setRolloutStyle(self._rolloutStyle)
def rolloutStyle(self):
return self._rolloutStyle
def takeAt(self, index):
self.setUpdatesEnabled(False)
layout = self.widget().layout()
widget = None
if 0 <= index and index < layout.count() - 1:
item = layout.itemAt(index)
widget = item.widget()
layout.removeItem(item)
widget.close()
self.setUpdatesEnabled(True)
return widget
def widgetAt(self, index):
item = self.itemAt(index)
if item:
return item.widget()
return None
pyBoxedMode = QtCore.Property('bool', isBoxedMode, setBoxedMode)

711
pype/vendor/capture_gui/app.py vendored Normal file
View file

@ -0,0 +1,711 @@
import json
import logging
import os
import tempfile
import capture
import maya.cmds as cmds
from .vendor.Qt import QtCore, QtWidgets, QtGui
from . import lib
from . import plugin
from . import presets
from . import version
from . import tokens
from .accordion import AccordionWidget
log = logging.getLogger("Capture Gui")
class ClickLabel(QtWidgets.QLabel):
"""A QLabel that emits a clicked signal when clicked upon."""
clicked = QtCore.Signal()
def mouseReleaseEvent(self, event):
self.clicked.emit()
return super(ClickLabel, self).mouseReleaseEvent(event)
class PreviewWidget(QtWidgets.QWidget):
"""The playblast image preview widget.
Upon refresh it will retrieve the options through the function set as
`options_getter` and make a call to `capture.capture()` for a single
frame (playblasted) snapshot. The result is displayed as image.
"""
preview_width = 320
preview_height = 180
def __init__(self, options_getter, validator, parent=None):
QtWidgets.QWidget.__init__(self, parent=parent)
# Add attributes
self.options_getter = options_getter
self.validator = validator
self.preview = ClickLabel()
self.preview.setFixedWidth(self.preview_width)
self.preview.setFixedHeight(self.preview_height)
tip = "Click to force a refresh"
self.preview.setToolTip(tip)
self.preview.setStatusTip(tip)
# region Build
self.layout = QtWidgets.QVBoxLayout()
self.layout.setAlignment(QtCore.Qt.AlignHCenter)
self.layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(self.layout)
self.layout.addWidget(self.preview)
# endregion Build
# Connect widgets to functions
self.preview.clicked.connect(self.refresh)
def refresh(self):
"""Refresh the playblast preview"""
frame = cmds.currentTime(query=True)
# When playblasting outside of an undo queue it seems that undoing
# actually triggers a reset to frame 0. As such we sneak in the current
# time into the undo queue to enforce correct undoing.
cmds.currentTime(frame, update=True)
# check if plugin outputs are correct
valid = self.validator()
if not valid:
return
with lib.no_undo():
options = self.options_getter()
tempdir = tempfile.mkdtemp()
# override settings that are constants for the preview
options = options.copy()
options['filename'] = None
options['complete_filename'] = os.path.join(tempdir, "temp.jpg")
options['width'] = self.preview_width
options['height'] = self.preview_height
options['viewer'] = False
options['frame'] = frame
options['off_screen'] = True
options['format'] = "image"
options['compression'] = "jpg"
options['sound'] = None
fname = capture.capture(**options)
if not fname:
log.warning("Preview failed")
return
image = QtGui.QPixmap(fname)
self.preview.setPixmap(image)
os.remove(fname)
def showEvent(self, event):
"""Initialize when shown"""
self.refresh()
event.accept()
class PresetWidget(QtWidgets.QWidget):
"""Preset Widget
Allows the user to set preferences and create presets to load before
capturing.
"""
preset_loaded = QtCore.Signal(dict)
config_opened = QtCore.Signal()
id = "Presets"
label = "Presets"
def __init__(self, inputs_getter, parent=None):
QtWidgets.QWidget.__init__(self, parent=parent)
self.inputs_getter = inputs_getter
layout = QtWidgets.QHBoxLayout(self)
layout.setAlignment(QtCore.Qt.AlignCenter)
layout.setContentsMargins(0, 0, 0, 0)
presets = QtWidgets.QComboBox()
presets.setFixedWidth(220)
presets.addItem("*")
# Icons
icon_path = os.path.join(os.path.dirname(__file__), "resources")
save_icon = os.path.join(icon_path, "save.png")
load_icon = os.path.join(icon_path, "import.png")
config_icon = os.path.join(icon_path, "config.png")
# Create buttons
save = QtWidgets.QPushButton()
save.setIcon(QtGui.QIcon(save_icon))
save.setFixedWidth(30)
save.setToolTip("Save Preset")
save.setStatusTip("Save Preset")
load = QtWidgets.QPushButton()
load.setIcon(QtGui.QIcon(load_icon))
load.setFixedWidth(30)
load.setToolTip("Load Preset")
load.setStatusTip("Load Preset")
config = QtWidgets.QPushButton()
config.setIcon(QtGui.QIcon(config_icon))
config.setFixedWidth(30)
config.setToolTip("Preset configuration")
config.setStatusTip("Preset configuration")
layout.addWidget(presets)
layout.addWidget(save)
layout.addWidget(load)
layout.addWidget(config)
# Make available for all methods
self.presets = presets
self.config = config
self.load = load
self.save = save
# Signals
self.save.clicked.connect(self.on_save_preset)
self.load.clicked.connect(self.import_preset)
self.config.clicked.connect(self.config_opened)
self.presets.currentIndexChanged.connect(self.load_active_preset)
self._process_presets()
def _process_presets(self):
"""Adds all preset files from preset paths to the Preset widget.
Returns:
None
"""
for presetfile in presets.discover():
self.add_preset(presetfile)
def import_preset(self):
"""Load preset files to override output values"""
path = self._default_browse_path()
filters = "Text file (*.json)"
dialog = QtWidgets.QFileDialog
filename, _ = dialog.getOpenFileName(self, "Open preference file",
path, filters)
if not filename:
return
# create new entry in combobox
self.add_preset(filename)
# read file
return self.load_active_preset()
def load_active_preset(self):
"""Load the active preset.
Returns:
dict: The preset inputs.
"""
current_index = self.presets.currentIndex()
filename = self.presets.itemData(current_index)
if not filename:
return {}
preset = lib.load_json(filename)
# Emit preset load signal
log.debug("Emitting preset_loaded: {0}".format(filename))
self.preset_loaded.emit(preset)
# Ensure we preserve the index after loading the changes
# for all the plugin widgets
self.presets.blockSignals(True)
self.presets.setCurrentIndex(current_index)
self.presets.blockSignals(False)
return preset
def add_preset(self, filename):
"""Add the filename to the preset list.
This also sets the index to the filename.
Returns:
None
"""
filename = os.path.normpath(filename)
if not os.path.exists(filename):
log.warning("Preset file does not exist: {0}".format(filename))
return
label = os.path.splitext(os.path.basename(filename))[0]
item_count = self.presets.count()
paths = [self.presets.itemData(i) for i in range(item_count)]
if filename in paths:
log.info("Preset is already in the "
"presets list: {0}".format(filename))
item_index = paths.index(filename)
else:
self.presets.addItem(label, userData=filename)
item_index = item_count
self.presets.blockSignals(True)
self.presets.setCurrentIndex(item_index)
self.presets.blockSignals(False)
return item_index
def _default_browse_path(self):
"""Return the current browse path for save/load preset.
If a preset is currently loaded it will use that specific path
otherwise it will go to the last registered preset path.
Returns:
str: Path to use as default browse location.
"""
current_index = self.presets.currentIndex()
path = self.presets.itemData(current_index)
if not path:
# Fallback to last registered preset path
paths = presets.preset_paths()
if paths:
path = paths[-1]
return path
def save_preset(self, inputs):
"""Save inputs to a file"""
path = self._default_browse_path()
filters = "Text file (*.json)"
filename, _ = QtWidgets.QFileDialog.getSaveFileName(self,
"Save preferences",
path,
filters)
if not filename:
return
with open(filename, "w") as f:
json.dump(inputs, f, sort_keys=True,
indent=4, separators=(',', ': '))
self.add_preset(filename)
return filename
def get_presets(self):
"""Return all currently listed presets"""
configurations = [self.presets.itemText(i) for
i in range(self.presets.count())]
return configurations
def on_save_preset(self):
"""Save the inputs of all the plugins in a preset."""
inputs = self.inputs_getter(as_preset=True)
self.save_preset(inputs)
def apply_inputs(self, settings):
path = settings.get("selected", None)
index = self.presets.findData(path)
if index == -1:
# If the last loaded preset still exists but wasn't on the
# "discovered preset paths" then add it.
if os.path.exists(path):
log.info("Adding previously selected preset explicitly: %s",
path)
self.add_preset(path)
return
else:
log.warning("Previously selected preset is not available: %s",
path)
index = 0
self.presets.setCurrentIndex(index)
def get_inputs(self, as_preset=False):
if as_preset:
# Don't save the current preset into the preset because
# that would just be recursive and make no sense
return {}
else:
current_index = self.presets.currentIndex()
selected = self.presets.itemData(current_index)
return {"selected": selected}
class App(QtWidgets.QWidget):
"""The main application in which the widgets are placed"""
# Signals
options_changed = QtCore.Signal(dict)
playblast_start = QtCore.Signal(dict)
playblast_finished = QtCore.Signal(dict)
viewer_start = QtCore.Signal(dict)
# Attributes
object_name = "CaptureGUI"
application_sections = ["config", "app"]
def __init__(self, title, parent=None):
QtWidgets.QWidget.__init__(self, parent=parent)
# Settings
# Remove pointer for memory when closed
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.settingfile = self._ensure_config_exist()
self.plugins = {"app": list(),
"config": list()}
self._config_dialog = None
self._build_configuration_dialog()
# region Set Attributes
title_version = "{} v{}".format(title, version.version)
self.setObjectName(self.object_name)
self.setWindowTitle(title_version)
self.setMinimumWidth(380)
# Set dialog window flags so the widget can be correctly parented
# to Maya main window
self.setWindowFlags(self.windowFlags() | QtCore.Qt.Dialog)
self.setProperty("saveWindowPref", True)
# endregion Set Attributes
self.layout = QtWidgets.QVBoxLayout()
self.layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(self.layout)
# Add accordion widget (Maya attribute editor style)
self.widgetlibrary = AccordionWidget(self)
self.widgetlibrary.setRolloutStyle(AccordionWidget.Maya)
# Add separate widgets
self.widgetlibrary.addItem("Preview",
PreviewWidget(self.get_outputs,
self.validate,
parent=self),
collapsed=True)
self.presetwidget = PresetWidget(inputs_getter=self.get_inputs,
parent=self)
self.widgetlibrary.addItem("Presets", self.presetwidget)
# add plug-in widgets
for widget in plugin.discover():
self.add_plugin(widget)
self.layout.addWidget(self.widgetlibrary)
# add standard buttons
self.apply_button = QtWidgets.QPushButton("Capture")
self.layout.addWidget(self.apply_button)
# default actions
self.apply_button.clicked.connect(self.apply)
# signals and slots
self.presetwidget.config_opened.connect(self.show_config)
self.presetwidget.preset_loaded.connect(self.apply_inputs)
self.apply_inputs(self._read_widget_configuration())
def apply(self):
"""Run capture action with current settings"""
valid = self.validate()
if not valid:
return
options = self.get_outputs()
filename = options.get("filename", None)
self.playblast_start.emit(options)
# The filename can be `None` when the
# playblast will *not* be saved.
if filename is not None:
# Format the tokens in the filename
filename = tokens.format_tokens(filename, options)
# expand environment variables
filename = os.path.expandvars(filename)
# Make relative paths absolute to the "images" file rule by default
if not os.path.isabs(filename):
root = lib.get_project_rule("images")
filename = os.path.join(root, filename)
# normalize (to remove double slashes and alike)
filename = os.path.normpath(filename)
options["filename"] = filename
# Perform capture and store returned filename with extension
options["filename"] = lib.capture_scene(options)
self.playblast_finished.emit(options)
filename = options["filename"] # get filename after callbacks
# Show viewer
viewer = options.get("viewer", False)
if viewer:
if filename and os.path.exists(filename):
self.viewer_start.emit(options)
lib.open_file(filename)
else:
raise RuntimeError("Can't open playblast because file "
"doesn't exist: {0}".format(filename))
return filename
def apply_inputs(self, inputs):
"""Apply all the settings of the widgets.
Arguments:
inputs (dict): input values per plug-in widget
Returns:
None
"""
if not inputs:
return
widgets = self._get_plugin_widgets()
widgets.append(self.presetwidget)
for widget in widgets:
widget_inputs = inputs.get(widget.id, None)
if not widget_inputs:
continue
widget.apply_inputs(widget_inputs)
def show_config(self):
"""Show the advanced configuration"""
# calculate center of main widget
geometry = self.geometry()
self._config_dialog.move(QtCore.QPoint(geometry.x()+30,
geometry.y()))
self._config_dialog.show()
def add_plugin(self, plugin):
"""Add an options widget plug-in to the UI"""
if plugin.section not in self.application_sections:
log.warning("{}'s section is invalid: "
"{}".format(plugin.label, plugin.section))
return
widget = plugin(parent=self)
widget.initialize()
widget.options_changed.connect(self.on_widget_settings_changed)
self.playblast_finished.connect(widget.on_playblast_finished)
# Add to plug-ins in its section
self.plugins[widget.section].append(widget)
# Implement additional settings depending on section
if widget.section == "app":
if not widget.hidden:
item = self.widgetlibrary.addItem(widget.label, widget)
# connect label change behaviour
widget.label_changed.connect(item.setTitle)
# Add the plugin in a QGroupBox to the configuration dialog
if widget.section == "config":
layout = self._config_dialog.layout()
# create group box
group_widget = QtWidgets.QGroupBox(widget.label)
group_layout = QtWidgets.QVBoxLayout(group_widget)
group_layout.addWidget(widget)
layout.addWidget(group_widget)
def validate(self):
"""Validate whether the outputs of the widgets are good.
Returns:
bool: Whether it's valid to capture the current settings.
"""
errors = list()
for widget in self._get_plugin_widgets():
widget_errors = widget.validate()
if widget_errors:
errors.extend(widget_errors)
if errors:
message_title = "%s Validation Error(s)" % len(errors)
message = "\n".join(errors)
QtWidgets.QMessageBox.critical(self,
message_title,
message,
QtWidgets.QMessageBox.Ok)
return False
return True
def get_outputs(self):
"""Return settings for a capture as currently set in the Application.
Returns:
dict: Current output settings
"""
# Get settings from widgets
outputs = dict()
for widget in self._get_plugin_widgets():
widget_outputs = widget.get_outputs()
if not widget_outputs:
continue
for key, value in widget_outputs.items():
# We merge dictionaries by updating them so we have
# the "mixed" values of both settings
if isinstance(value, dict) and key in outputs:
outputs[key].update(value)
else:
outputs[key] = value
return outputs
def get_inputs(self, as_preset=False):
"""Return the inputs per plug-in widgets by `plugin.id`.
Returns:
dict: The inputs per widget
"""
inputs = dict()
# Here we collect all the widgets from which we want to store the
# current inputs. This will be restored in the next session
# The preset widget is added to make sure the user starts with the
# previously selected preset configuration
config_widgets = self._get_plugin_widgets()
config_widgets.append(self.presetwidget)
for widget in config_widgets:
widget_inputs = widget.get_inputs(as_preset=as_preset)
if not isinstance(widget_inputs, dict):
log.debug("Widget inputs are not a dictionary "
"'{}': {}".format(widget.id, widget_inputs))
return
if not widget_inputs:
continue
inputs[widget.id] = widget_inputs
return inputs
def on_widget_settings_changed(self):
"""Set current preset to '*' on settings change"""
self.options_changed.emit(self.get_outputs)
self.presetwidget.presets.setCurrentIndex(0)
def _build_configuration_dialog(self):
"""Build a configuration to store configuration widgets in"""
dialog = QtWidgets.QDialog(self)
dialog.setWindowTitle("Capture - Preset Configuration")
QtWidgets.QVBoxLayout(dialog)
self._config_dialog = dialog
def _ensure_config_exist(self):
"""Create the configuration file if it does not exist yet.
Returns:
unicode: filepath of the configuration file
"""
userdir = os.path.expanduser("~")
capturegui_dir = os.path.join(userdir, "CaptureGUI")
capturegui_inputs = os.path.join(capturegui_dir, "capturegui.json")
if not os.path.exists(capturegui_dir):
os.makedirs(capturegui_dir)
if not os.path.isfile(capturegui_inputs):
config = open(capturegui_inputs, "w")
config.close()
return capturegui_inputs
def _store_widget_configuration(self):
"""Store all used widget settings in the local json file"""
inputs = self.get_inputs(as_preset=False)
path = self.settingfile
with open(path, "w") as f:
log.debug("Writing JSON file: {0}".format(path))
json.dump(inputs, f, sort_keys=True,
indent=4, separators=(',', ': '))
def _read_widget_configuration(self):
"""Read the stored widget inputs"""
inputs = {}
path = self.settingfile
if not os.path.isfile(path) or os.stat(path).st_size == 0:
return inputs
with open(path, "r") as f:
log.debug("Reading JSON file: {0}".format(path))
try:
inputs = json.load(f)
except ValueError as error:
log.error(str(error))
return inputs
def _get_plugin_widgets(self):
"""List all plug-in widgets.
Returns:
list: The plug-in widgets in *all* sections
"""
widgets = list()
for section in self.plugins.values():
widgets.extend(section)
return widgets
# override close event to ensure the input are stored
def closeEvent(self, event):
"""Store current configuration upon closing the application."""
self._store_widget_configuration()
for section_widgets in self.plugins.values():
for widget in section_widgets:
widget.uninitialize()
event.accept()

55
pype/vendor/capture_gui/colorpicker.py vendored Normal file
View file

@ -0,0 +1,55 @@
from capture_gui.vendor.Qt import QtCore, QtWidgets, QtGui
class ColorPicker(QtWidgets.QPushButton):
"""Custom color pick button to store and retrieve color values"""
valueChanged = QtCore.Signal()
def __init__(self):
QtWidgets.QPushButton.__init__(self)
self.clicked.connect(self.show_color_dialog)
self._color = None
self.color = [1, 1, 1]
# region properties
@property
def color(self):
return self._color
@color.setter
def color(self, values):
"""Set the color value and update the stylesheet
Arguments:
values (list): the color values; red, green, blue
Returns:
None
"""
self._color = values
self.valueChanged.emit()
values = [int(x*255) for x in values]
self.setStyleSheet("background: rgb({},{},{})".format(*values))
# endregion properties
def show_color_dialog(self):
"""Display a color picker to change color.
When a color has been chosen this updates the color of the button
and its current value
:return: the red, green and blue values
:rtype: list
"""
current = QtGui.QColor()
current.setRgbF(*self._color)
colors = QtWidgets.QColorDialog.getColor(current)
if not colors:
return
self.color = [colors.redF(), colors.greenF(), colors.blueF()]

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__']