mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
return capture to vendors
This commit is contained in:
parent
6e2fdb880c
commit
f63dd1c048
27 changed files with 6007 additions and 0 deletions
833
pype/vendor/capture.py
vendored
Normal file
833
pype/vendor/capture.py
vendored
Normal 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
29
pype/vendor/capture_gui/__init__.py
vendored
Normal 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
624
pype/vendor/capture_gui/accordion.py
vendored
Normal 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
711
pype/vendor/capture_gui/app.py
vendored
Normal 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
55
pype/vendor/capture_gui/colorpicker.py
vendored
Normal 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
396
pype/vendor/capture_gui/lib.py
vendored
Normal 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
401
pype/vendor/capture_gui/plugin.py
vendored
Normal 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)
|
||||
141
pype/vendor/capture_gui/plugins/cameraplugin.py
vendored
Normal file
141
pype/vendor/capture_gui/plugins/cameraplugin.py
vendored
Normal 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)
|
||||
95
pype/vendor/capture_gui/plugins/codecplugin.py
vendored
Normal file
95
pype/vendor/capture_gui/plugins/codecplugin.py
vendored
Normal 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))
|
||||
47
pype/vendor/capture_gui/plugins/defaultoptionsplugin.py
vendored
Normal file
47
pype/vendor/capture_gui/plugins/defaultoptionsplugin.py
vendored
Normal 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
|
||||
179
pype/vendor/capture_gui/plugins/displayplugin.py
vendored
Normal file
179
pype/vendor/capture_gui/plugins/displayplugin.py
vendored
Normal 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}
|
||||
95
pype/vendor/capture_gui/plugins/genericplugin.py
vendored
Normal file
95
pype/vendor/capture_gui/plugins/genericplugin.py
vendored
Normal 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
|
||||
254
pype/vendor/capture_gui/plugins/ioplugin.py
vendored
Normal file
254
pype/vendor/capture_gui/plugins/ioplugin.py
vendored
Normal 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)
|
||||
48
pype/vendor/capture_gui/plugins/panzoomplugin.py
vendored
Normal file
48
pype/vendor/capture_gui/plugins/panzoomplugin.py
vendored
Normal 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()}
|
||||
104
pype/vendor/capture_gui/plugins/rendererplugin.py
vendored
Normal file
104
pype/vendor/capture_gui/plugins/rendererplugin.py
vendored
Normal 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)
|
||||
199
pype/vendor/capture_gui/plugins/resolutionplugin.py
vendored
Normal file
199
pype/vendor/capture_gui/plugins/resolutionplugin.py
vendored
Normal 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)
|
||||
292
pype/vendor/capture_gui/plugins/timeplugin.py
vendored
Normal file
292
pype/vendor/capture_gui/plugins/timeplugin.py
vendored
Normal 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))
|
||||
292
pype/vendor/capture_gui/plugins/viewportplugin.py
vendored
Normal file
292
pype/vendor/capture_gui/plugins/viewportplugin.py
vendored
Normal 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
105
pype/vendor/capture_gui/presets.py
vendored
Normal 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)
|
||||
BIN
pype/vendor/capture_gui/resources/config.png
vendored
Normal file
BIN
pype/vendor/capture_gui/resources/config.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 870 B |
BIN
pype/vendor/capture_gui/resources/import.png
vendored
Normal file
BIN
pype/vendor/capture_gui/resources/import.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 815 B |
BIN
pype/vendor/capture_gui/resources/reset.png
vendored
Normal file
BIN
pype/vendor/capture_gui/resources/reset.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 976 B |
BIN
pype/vendor/capture_gui/resources/save.png
vendored
Normal file
BIN
pype/vendor/capture_gui/resources/save.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 835 B |
68
pype/vendor/capture_gui/tokens.py
vendored
Normal file
68
pype/vendor/capture_gui/tokens.py
vendored
Normal 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
1030
pype/vendor/capture_gui/vendor/Qt.py
vendored
Normal file
File diff suppressed because it is too large
Load diff
0
pype/vendor/capture_gui/vendor/__init__.py
vendored
Normal file
0
pype/vendor/capture_gui/vendor/__init__.py
vendored
Normal file
9
pype/vendor/capture_gui/version.py
vendored
Normal file
9
pype/vendor/capture_gui/version.py
vendored
Normal 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__']
|
||||
Loading…
Add table
Add a link
Reference in a new issue