mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-01 16:34:53 +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