mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 05:14:40 +01:00
add maya-capture and maya-capture-gui to vendor
This commit is contained in:
parent
c3e5d70b4d
commit
f7cd93d723
5 changed files with 2252 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()]
|
||||
Loading…
Add table
Add a link
Reference in a new issue