add maya-capture and maya-capture-gui to vendor

This commit is contained in:
Milan Kolar 2018-11-29 23:32:35 +01:00
parent c3e5d70b4d
commit f7cd93d723
5 changed files with 2252 additions and 0 deletions

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

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

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

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

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

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

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

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

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

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