From f63dd1c0484b7bad71148329f7be990340a31991 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 22 Nov 2019 18:15:57 +0100 Subject: [PATCH] return capture to vendors --- pype/vendor/capture.py | 833 +++++++++++++ pype/vendor/capture_gui/__init__.py | 29 + pype/vendor/capture_gui/accordion.py | 624 ++++++++++ pype/vendor/capture_gui/app.py | 711 ++++++++++++ pype/vendor/capture_gui/colorpicker.py | 55 + pype/vendor/capture_gui/lib.py | 396 +++++++ pype/vendor/capture_gui/plugin.py | 401 +++++++ .../capture_gui/plugins/cameraplugin.py | 141 +++ .../vendor/capture_gui/plugins/codecplugin.py | 95 ++ .../plugins/defaultoptionsplugin.py | 47 + .../capture_gui/plugins/displayplugin.py | 179 +++ .../capture_gui/plugins/genericplugin.py | 95 ++ pype/vendor/capture_gui/plugins/ioplugin.py | 254 ++++ .../capture_gui/plugins/panzoomplugin.py | 48 + .../capture_gui/plugins/rendererplugin.py | 104 ++ .../capture_gui/plugins/resolutionplugin.py | 199 ++++ pype/vendor/capture_gui/plugins/timeplugin.py | 292 +++++ .../capture_gui/plugins/viewportplugin.py | 292 +++++ pype/vendor/capture_gui/presets.py | 105 ++ pype/vendor/capture_gui/resources/config.png | Bin 0 -> 870 bytes pype/vendor/capture_gui/resources/import.png | Bin 0 -> 815 bytes pype/vendor/capture_gui/resources/reset.png | Bin 0 -> 976 bytes pype/vendor/capture_gui/resources/save.png | Bin 0 -> 835 bytes pype/vendor/capture_gui/tokens.py | 68 ++ pype/vendor/capture_gui/vendor/Qt.py | 1030 +++++++++++++++++ pype/vendor/capture_gui/vendor/__init__.py | 0 pype/vendor/capture_gui/version.py | 9 + 27 files changed, 6007 insertions(+) create mode 100644 pype/vendor/capture.py create mode 100644 pype/vendor/capture_gui/__init__.py create mode 100644 pype/vendor/capture_gui/accordion.py create mode 100644 pype/vendor/capture_gui/app.py create mode 100644 pype/vendor/capture_gui/colorpicker.py create mode 100644 pype/vendor/capture_gui/lib.py create mode 100644 pype/vendor/capture_gui/plugin.py create mode 100644 pype/vendor/capture_gui/plugins/cameraplugin.py create mode 100644 pype/vendor/capture_gui/plugins/codecplugin.py create mode 100644 pype/vendor/capture_gui/plugins/defaultoptionsplugin.py create mode 100644 pype/vendor/capture_gui/plugins/displayplugin.py create mode 100644 pype/vendor/capture_gui/plugins/genericplugin.py create mode 100644 pype/vendor/capture_gui/plugins/ioplugin.py create mode 100644 pype/vendor/capture_gui/plugins/panzoomplugin.py create mode 100644 pype/vendor/capture_gui/plugins/rendererplugin.py create mode 100644 pype/vendor/capture_gui/plugins/resolutionplugin.py create mode 100644 pype/vendor/capture_gui/plugins/timeplugin.py create mode 100644 pype/vendor/capture_gui/plugins/viewportplugin.py create mode 100644 pype/vendor/capture_gui/presets.py create mode 100644 pype/vendor/capture_gui/resources/config.png create mode 100644 pype/vendor/capture_gui/resources/import.png create mode 100644 pype/vendor/capture_gui/resources/reset.png create mode 100644 pype/vendor/capture_gui/resources/save.png create mode 100644 pype/vendor/capture_gui/tokens.py create mode 100644 pype/vendor/capture_gui/vendor/Qt.py create mode 100644 pype/vendor/capture_gui/vendor/__init__.py create mode 100644 pype/vendor/capture_gui/version.py diff --git a/pype/vendor/capture.py b/pype/vendor/capture.py new file mode 100644 index 0000000000..83816ec92a --- /dev/null +++ b/pype/vendor/capture.py @@ -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 + }) diff --git a/pype/vendor/capture_gui/__init__.py b/pype/vendor/capture_gui/__init__.py new file mode 100644 index 0000000000..6c6a813636 --- /dev/null +++ b/pype/vendor/capture_gui/__init__.py @@ -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 diff --git a/pype/vendor/capture_gui/accordion.py b/pype/vendor/capture_gui/accordion.py new file mode 100644 index 0000000000..f721837c57 --- /dev/null +++ b/pype/vendor/capture_gui/accordion.py @@ -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 + """ + return self._accordianWidget + + def customData(self, key, default=None): + """ + \remarks return a custom pointer to information stored with this item + \param key + \param default default value to return if the key was not found + \return 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 + \param value + """ + 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 + """ + 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) diff --git a/pype/vendor/capture_gui/app.py b/pype/vendor/capture_gui/app.py new file mode 100644 index 0000000000..1860b084ba --- /dev/null +++ b/pype/vendor/capture_gui/app.py @@ -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() diff --git a/pype/vendor/capture_gui/colorpicker.py b/pype/vendor/capture_gui/colorpicker.py new file mode 100644 index 0000000000..aa00a7386d --- /dev/null +++ b/pype/vendor/capture_gui/colorpicker.py @@ -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()] diff --git a/pype/vendor/capture_gui/lib.py b/pype/vendor/capture_gui/lib.py new file mode 100644 index 0000000000..823ca8f7c8 --- /dev/null +++ b/pype/vendor/capture_gui/lib.py @@ -0,0 +1,396 @@ +# TODO: fetch Maya main window without shiboken that also doesn't crash + +import sys +import logging +import json +import os +import glob +import subprocess +import contextlib +from collections import OrderedDict + +import datetime +import maya.cmds as cmds +import maya.mel as mel +import maya.OpenMayaUI as omui +import capture + +from .vendor.Qt import QtWidgets +try: + # PySide1 + import shiboken +except ImportError: + # PySide2 + import shiboken2 as shiboken + +log = logging.getLogger(__name__) + +# region Object types +OBJECT_TYPES = OrderedDict() +OBJECT_TYPES['NURBS Curves'] = 'nurbsCurves' +OBJECT_TYPES['NURBS Surfaces'] = 'nurbsSurfaces' +OBJECT_TYPES['NURBS CVs'] = 'controlVertices' +OBJECT_TYPES['NURBS Hulls'] = 'hulls' +OBJECT_TYPES['Polygons'] = 'polymeshes' +OBJECT_TYPES['Subdiv Surfaces'] = 'subdivSurfaces' +OBJECT_TYPES['Planes'] = 'planes' +OBJECT_TYPES['Lights'] = 'lights' +OBJECT_TYPES['Cameras'] = 'cameras' +OBJECT_TYPES['Image Planes'] = 'imagePlane' +OBJECT_TYPES['Joints'] = 'joints' +OBJECT_TYPES['IK Handles'] = 'ikHandles' +OBJECT_TYPES['Deformers'] = 'deformers' +OBJECT_TYPES['Dynamics'] = 'dynamics' +OBJECT_TYPES['Particle Instancers'] = 'particleInstancers' +OBJECT_TYPES['Fluids'] = 'fluids' +OBJECT_TYPES['Hair Systems'] = 'hairSystems' +OBJECT_TYPES['Follicles'] = 'follicles' +OBJECT_TYPES['nCloths'] = 'nCloths' +OBJECT_TYPES['nParticles'] = 'nParticles' +OBJECT_TYPES['nRigids'] = 'nRigids' +OBJECT_TYPES['Dynamic Constraints'] = 'dynamicConstraints' +OBJECT_TYPES['Locators'] = 'locators' +OBJECT_TYPES['Dimensions'] = 'dimensions' +OBJECT_TYPES['Pivots'] = 'pivots' +OBJECT_TYPES['Handles'] = 'handles' +OBJECT_TYPES['Textures Placements'] = 'textures' +OBJECT_TYPES['Strokes'] = 'strokes' +OBJECT_TYPES['Motion Trails'] = 'motionTrails' +OBJECT_TYPES['Plugin Shapes'] = 'pluginShapes' +OBJECT_TYPES['Clip Ghosts'] = 'clipGhosts' +OBJECT_TYPES['Grease Pencil'] = 'greasePencils' +OBJECT_TYPES['Manipulators'] = 'manipulators' +OBJECT_TYPES['Grid'] = 'grid' +OBJECT_TYPES['HUD'] = 'hud' +# endregion Object types + + +def get_show_object_types(): + + results = OrderedDict() + + # Add the plug-in shapes + plugin_shapes = get_plugin_shapes() + results.update(plugin_shapes) + + # We add default shapes last so plug-in shapes could + # never potentially overwrite any built-ins. + results.update(OBJECT_TYPES) + + return results + + +def get_current_scenename(): + path = cmds.file(query=True, sceneName=True) + if path: + return os.path.splitext(os.path.basename(path))[0] + return None + + +def get_current_camera(): + """Returns the currently active camera. + + Searched in the order of: + 1. Active Panel + 2. Selected Camera Shape + 3. Selected Camera Transform + + Returns: + str: name of active camera transform + + """ + + # Get camera from active modelPanel (if any) + panel = cmds.getPanel(withFocus=True) + if cmds.getPanel(typeOf=panel) == "modelPanel": + cam = cmds.modelEditor(panel, query=True, camera=True) + # In some cases above returns the shape, but most often it returns the + # transform. Still we need to make sure we return the transform. + if cam: + if cmds.nodeType(cam) == "transform": + return cam + # camera shape is a shape type + elif cmds.objectType(cam, isAType="shape"): + parent = cmds.listRelatives(cam, parent=True, fullPath=True) + if parent: + return parent[0] + + # Check if a camShape is selected (if so use that) + cam_shapes = cmds.ls(selection=True, type="camera") + if cam_shapes: + return cmds.listRelatives(cam_shapes, + parent=True, + fullPath=True)[0] + + # Check if a transform of a camShape is selected + # (return cam transform if any) + transforms = cmds.ls(selection=True, type="transform") + if transforms: + cam_shapes = cmds.listRelatives(transforms, shapes=True, type="camera") + if cam_shapes: + return cmds.listRelatives(cam_shapes, + parent=True, + fullPath=True)[0] + + +def get_active_editor(): + """Return the active editor panel to playblast with""" + # fixes `cmds.playblast` undo bug + cmds.currentTime(cmds.currentTime(query=True)) + panel = cmds.playblast(activeEditor=True) + return panel.split("|")[-1] + + +def get_current_frame(): + return cmds.currentTime(query=True) + + +def get_time_slider_range(highlighted=True, + withinHighlighted=True, + highlightedOnly=False): + """Return the time range from Maya's time slider. + + Arguments: + highlighted (bool): When True if will return a selected frame range + (if there's any selection of more than one frame!) otherwise it + will return min and max playback time. + withinHighlighted (bool): By default Maya returns the highlighted range + end as a plus one value. When this is True this will be fixed by + removing one from the last number. + + Returns: + list: List of two floats of start and end frame numbers. + + """ + if highlighted is True: + gPlaybackSlider = mel.eval("global string $gPlayBackSlider; " + "$gPlayBackSlider = $gPlayBackSlider;") + if cmds.timeControl(gPlaybackSlider, query=True, rangeVisible=True): + highlightedRange = cmds.timeControl(gPlaybackSlider, + query=True, + rangeArray=True) + if withinHighlighted: + highlightedRange[-1] -= 1 + return highlightedRange + if not highlightedOnly: + return [cmds.playbackOptions(query=True, minTime=True), + cmds.playbackOptions(query=True, maxTime=True)] + + +def get_current_renderlayer(): + return cmds.editRenderLayerGlobals(query=True, currentRenderLayer=True) + + +def get_plugin_shapes(): + """Get all currently available plugin shapes + + Returns: + dict: plugin shapes by their menu label and script name + + """ + filters = cmds.pluginDisplayFilter(query=True, listFilters=True) + labels = [cmds.pluginDisplayFilter(f, query=True, label=True) for f in + filters] + return OrderedDict(zip(labels, filters)) + + +def open_file(filepath): + """Open file using OS default settings""" + if sys.platform.startswith('darwin'): + subprocess.call(('open', filepath)) + elif os.name == 'nt': + os.startfile(filepath) + elif os.name == 'posix': + subprocess.call(('xdg-open', filepath)) + else: + raise NotImplementedError("OS not supported: {0}".format(os.name)) + + +def load_json(filepath): + """open and read json, return read values""" + with open(filepath, "r") as f: + return json.load(f) + + +def _fix_playblast_output_path(filepath): + """Workaround a bug in maya.cmds.playblast to return correct filepath. + + When the `viewer` argument is set to False and maya.cmds.playblast does not + automatically open the playblasted file the returned filepath does not have + the file's extension added correctly. + + To workaround this we just glob.glob() for any file extensions and assume + the latest modified file is the correct file and return it. + + """ + # Catch cancelled playblast + if filepath is None: + log.warning("Playblast did not result in output path. " + "Playblast is probably interrupted.") + return + + # Fix: playblast not returning correct filename (with extension) + # Lets assume the most recently modified file is the correct one. + if not os.path.exists(filepath): + directory = os.path.dirname(filepath) + filename = os.path.basename(filepath) + # check if the filepath is has frame based filename + # example : capture.####.png + parts = filename.split(".") + if len(parts) == 3: + query = os.path.join(directory, "{}.*.{}".format(parts[0], + parts[-1])) + files = glob.glob(query) + else: + files = glob.glob("{}.*".format(filepath)) + + if not files: + raise RuntimeError("Couldn't find playblast from: " + "{0}".format(filepath)) + filepath = max(files, key=os.path.getmtime) + + return filepath + + +def capture_scene(options): + """Capture using scene settings. + + Uses the view settings from "panel". + + This ensures playblast is done as quicktime H.264 100% quality. + It forces showOrnaments to be off and does not render off screen. + + Arguments: + options (dict): a collection of output options + + Returns: + str: Full path to playblast file. + + """ + + filename = options.get("filename", "%TEMP%") + log.info("Capturing to: {0}".format(filename)) + + options = options.copy() + + # Force viewer to False in call to capture because we have our own + # viewer opening call to allow a signal to trigger between playblast + # and viewer + options['viewer'] = False + + # Remove panel key since it's internal value to capture_gui + options.pop("panel", None) + + path = capture.capture(**options) + path = _fix_playblast_output_path(path) + + return path + + +def browse(path=None): + """Open a pop-up browser for the user""" + + # Acquire path from user input if none defined + if path is None: + + scene_path = cmds.file(query=True, sceneName=True) + + # use scene file name as default name + default_filename = os.path.splitext(os.path.basename(scene_path))[0] + if not default_filename: + # Scene wasn't saved yet so found no valid name for playblast. + default_filename = "playblast" + + # Default to images rule + default_root = os.path.normpath(get_project_rule("images")) + default_path = os.path.join(default_root, default_filename) + path = cmds.fileDialog2(fileMode=0, + dialogStyle=2, + startingDirectory=default_path) + + if not path: + return + + if isinstance(path, (tuple, list)): + path = path[0] + + if path.endswith(".*"): + path = path[:-2] + + # Bug-Fix/Workaround: + # Fix for playblasts that result in nesting of the + # extension (eg. '.mov.mov.mov') which happens if the format + # is defined in the filename used for saving. + extension = os.path.splitext(path)[-1] + if extension: + path = path[:-len(extension)] + + return path + + +def default_output(): + """Return filename based on current scene name. + + Returns: + str: A relative filename + + """ + + scene = get_current_scenename() or "playblast" + + # get current datetime + timestamp = datetime.datetime.today() + str_timestamp = timestamp.strftime("%Y-%m-%d_%H-%M-%S") + filename = "{}_{}".format(scene, str_timestamp) + + return filename + + +def get_project_rule(rule): + """Get the full path of the rule of the project""" + + workspace = cmds.workspace(query=True, rootDirectory=True) + folder = cmds.workspace(fileRuleEntry=rule) + if not folder: + log.warning("File Rule Entry '{}' has no value, please check if the " + "rule name is typed correctly".format(rule)) + + return os.path.join(workspace, folder) + + +def list_formats(): + # Workaround for Maya playblast bug where undo would + # move the currentTime to frame one. + cmds.currentTime(cmds.currentTime(query=True)) + return cmds.playblast(query=True, format=True) + + +def list_compressions(format='avi'): + # Workaround for Maya playblast bug where undo would + # move the currentTime to frame one. + cmds.currentTime(cmds.currentTime(query=True)) + + cmd = 'playblast -format "{0}" -query -compression'.format(format) + return mel.eval(cmd) + + +@contextlib.contextmanager +def no_undo(): + """Disable undo during the context""" + try: + cmds.undoInfo(stateWithoutFlush=False) + yield + finally: + cmds.undoInfo(stateWithoutFlush=True) + + +def get_maya_main_window(): + """Get the main Maya window as a QtGui.QMainWindow instance + + Returns: + QtGui.QMainWindow: instance of the top level Maya windows + + """ + ptr = omui.MQtUtil.mainWindow() + if ptr is not None: + return shiboken.wrapInstance(long(ptr), QtWidgets.QWidget) diff --git a/pype/vendor/capture_gui/plugin.py b/pype/vendor/capture_gui/plugin.py new file mode 100644 index 0000000000..7d087936d7 --- /dev/null +++ b/pype/vendor/capture_gui/plugin.py @@ -0,0 +1,401 @@ +"""Plug-in system + +Works similar to how OSs look for executables; i.e. a number of +absolute paths are searched for a given match. The predicate for +executables is whether or not an extension matches a number of +options, such as ".exe" or ".bat". + +In this system, the predicate is whether or not a fname ends with ".py" + +""" + +# Standard library +import os +import sys +import types +import logging +import inspect + +from .vendor.Qt import QtCore, QtWidgets + +log = logging.getLogger(__name__) + +_registered_paths = list() +_registered_plugins = dict() + + +class classproperty(object): + def __init__(self, getter): + self.getter = getter + + def __get__(self, instance, owner): + return self.getter(owner) + + +class Plugin(QtWidgets.QWidget): + """Base class for Option plug-in Widgets. + + This is a regular Qt widget that can be added to the capture interface + as an additional component, like a plugin. + + The plug-ins are sorted in the interface by their `order` attribute and + will be displayed in the main interface when `section` is set to "app" + and displayed in the additional settings pop-up when set to "config". + + When `hidden` is set to True the widget will not be shown in the interface. + This could be useful as a plug-in that supplies solely default values to + the capture GUI command. + + """ + + label = "" + section = "app" # "config" or "app" + hidden = False + options_changed = QtCore.Signal() + label_changed = QtCore.Signal(str) + order = 0 + highlight = "border: 1px solid red;" + validate_state = True + + def on_playblast_finished(self, options): + pass + + def validate(self): + """ + Ensure outputs of the widget are possible, when errors are raised it + will return a message with what has caused the error + :return: + """ + errors = [] + return errors + + def get_outputs(self): + """Return the options as set in this plug-in widget. + + This is used to identify the settings to be used for the playblast. + As such the values should be returned in a way that a call to + `capture.capture()` would understand as arguments. + + Args: + panel (str): The active modelPanel of the user. This is passed so + values could potentially be parsed from the active panel + + Returns: + dict: The options for this plug-in. (formatted `capture` style) + + """ + return dict() + + def get_inputs(self, as_preset): + """Return widget's child settings. + + This should provide a dictionary of input settings of the plug-in + that results in a dictionary that can be supplied to `apply_input()` + This is used to save the settings of the preset to a widget. + + :param as_preset: + :param as_presets: Toggle to mute certain input values of the widget + :type as_presets: bool + + Returns: + dict: The currently set inputs of this widget. + + """ + return dict() + + def apply_inputs(self, settings): + """Apply a dictionary of settings to the widget. + + This should update the widget's inputs to the settings provided in + the dictionary. This is used to apply settings from a preset. + + Returns: + None + + """ + pass + + def initialize(self): + """ + This method is used to register any callbacks + :return: + """ + pass + + def uninitialize(self): + """ + Unregister any callback created when deleting the widget + + A general explation: + + The deletion method is an attribute that lives inside the object to be + deleted, and that is the problem: + Destruction seems not to care about the order of destruction, + and the __dict__ that also holds the onDestroy bound method + gets destructed before it is called. + + Another solution is to use a weakref + + :return: None + """ + pass + + def __str__(self): + return self.label or type(self).__name__ + + def __repr__(self): + return u"%s.%s(%r)" % (__name__, type(self).__name__, self.__str__()) + + id = classproperty(lambda cls: cls.__name__) + + +def register_plugin_path(path): + """Plug-ins are looked up at run-time from directories registered here + + To register a new directory, run this command along with the absolute + path to where you"re plug-ins are located. + + Example: + >>> import os + >>> my_plugins = "/server/plugins" + >>> register_plugin_path(my_plugins) + '/server/plugins' + + Returns: + Actual path added, including any post-processing + + """ + + if path in _registered_paths: + return log.warning("Path already registered: {0}".format(path)) + + _registered_paths.append(path) + + return path + + +def deregister_plugin_path(path): + """Remove a _registered_paths path + + Raises: + KeyError if `path` isn't registered + + """ + + _registered_paths.remove(path) + + +def deregister_all_plugin_paths(): + """Mainly used in tests""" + _registered_paths[:] = [] + + +def registered_plugin_paths(): + """Return paths added via registration + + ..note:: This returns a copy of the registered paths + and can therefore not be modified directly. + + """ + + return list(_registered_paths) + + +def registered_plugins(): + """Return plug-ins added via :func:`register_plugin` + + .. note:: This returns a copy of the registered plug-ins + and can therefore not be modified directly + + """ + + return _registered_plugins.values() + + +def register_plugin(plugin): + """Register a new plug-in + + Arguments: + plugin (Plugin): Plug-in to register + + Raises: + TypeError if `plugin` is not callable + + """ + + if not hasattr(plugin, "__call__"): + raise TypeError("Plug-in must be callable " + "returning an instance of a class") + + if not plugin_is_valid(plugin): + raise TypeError("Plug-in invalid: %s", plugin) + + _registered_plugins[plugin.__name__] = plugin + + +def plugin_paths(): + """Collect paths from all sources. + + This function looks at the three potential sources of paths + and returns a list with all of them together. + + The sources are: + + - Registered paths using :func:`register_plugin_path` + + Returns: + list of paths in which plugins may be locat + + """ + + paths = list() + + for path in registered_plugin_paths(): + if path in paths: + continue + paths.append(path) + + return paths + + +def discover(paths=None): + """Find and return available plug-ins + + This function looks for files within paths registered via + :func:`register_plugin_path`. + + Arguments: + paths (list, optional): Paths to discover plug-ins from. + If no paths are provided, all paths are searched. + + """ + + plugins = dict() + + # Include plug-ins from registered paths + for path in paths or plugin_paths(): + path = os.path.normpath(path) + + if not os.path.isdir(path): + continue + + for fname in os.listdir(path): + if fname.startswith("_"): + continue + + abspath = os.path.join(path, fname) + + if not os.path.isfile(abspath): + continue + + mod_name, mod_ext = os.path.splitext(fname) + + if not mod_ext == ".py": + continue + + module = types.ModuleType(mod_name) + module.__file__ = abspath + + try: + execfile(abspath, module.__dict__) + + # Store reference to original module, to avoid + # garbage collection from collecting it's global + # imports, such as `import os`. + sys.modules[mod_name] = module + + except Exception as err: + log.debug("Skipped: \"%s\" (%s)", mod_name, err) + continue + + for plugin in plugins_from_module(module): + if plugin.id in plugins: + log.debug("Duplicate plug-in found: %s", plugin) + continue + + plugins[plugin.id] = plugin + + # Include plug-ins from registration. + # Directly registered plug-ins take precedence. + for name, plugin in _registered_plugins.items(): + if name in plugins: + log.debug("Duplicate plug-in found: %s", plugin) + continue + plugins[name] = plugin + + plugins = list(plugins.values()) + sort(plugins) # In-place + + return plugins + + +def plugins_from_module(module): + """Return plug-ins from module + + Arguments: + module (types.ModuleType): Imported module from which to + parse valid plug-ins. + + Returns: + List of plug-ins, or empty list if none is found. + + """ + + plugins = list() + + for name in dir(module): + if name.startswith("_"): + continue + + # It could be anything at this point + obj = getattr(module, name) + + if not inspect.isclass(obj): + continue + + if not issubclass(obj, Plugin): + continue + + if not plugin_is_valid(obj): + log.debug("Plug-in invalid: %s", obj) + continue + + plugins.append(obj) + + return plugins + + +def plugin_is_valid(plugin): + """Determine whether or not plug-in `plugin` is valid + + Arguments: + plugin (Plugin): Plug-in to assess + + """ + + if not plugin: + return False + + return True + + +def sort(plugins): + """Sort `plugins` in-place + + Their order is determined by their `order` attribute. + + Arguments: + plugins (list): Plug-ins to sort + + """ + + if not isinstance(plugins, list): + raise TypeError("plugins must be of type list") + + plugins.sort(key=lambda p: p.order) + return plugins + + +# Register default paths +default_plugins_path = os.path.join(os.path.dirname(__file__), "plugins") +register_plugin_path(default_plugins_path) diff --git a/pype/vendor/capture_gui/plugins/cameraplugin.py b/pype/vendor/capture_gui/plugins/cameraplugin.py new file mode 100644 index 0000000000..1902330622 --- /dev/null +++ b/pype/vendor/capture_gui/plugins/cameraplugin.py @@ -0,0 +1,141 @@ +import maya.cmds as cmds +from capture_gui.vendor.Qt import QtCore, QtWidgets + +import capture_gui.lib as lib +import capture_gui.plugin + + +class CameraPlugin(capture_gui.plugin.Plugin): + """Camera widget. + + Allows to select a camera. + + """ + id = "Camera" + section = "app" + order = 10 + + def __init__(self, parent=None): + super(CameraPlugin, self).__init__(parent=parent) + + self._layout = QtWidgets.QHBoxLayout() + self._layout.setContentsMargins(5, 0, 5, 0) + self.setLayout(self._layout) + + self.cameras = QtWidgets.QComboBox() + self.cameras.setMinimumWidth(200) + + self.get_active = QtWidgets.QPushButton("Get active") + self.get_active.setToolTip("Set camera from currently active view") + self.refresh = QtWidgets.QPushButton("Refresh") + self.refresh.setToolTip("Refresh the list of cameras") + + self._layout.addWidget(self.cameras) + self._layout.addWidget(self.get_active) + self._layout.addWidget(self.refresh) + + # Signals + self.connections() + + # Force update of the label + self.set_active_cam() + self.on_update_label() + + def connections(self): + self.get_active.clicked.connect(self.set_active_cam) + self.refresh.clicked.connect(self.on_refresh) + + self.cameras.currentIndexChanged.connect(self.on_update_label) + self.cameras.currentIndexChanged.connect(self.validate) + + def set_active_cam(self): + cam = lib.get_current_camera() + self.on_refresh(camera=cam) + + def select_camera(self, cam): + if cam: + # Ensure long name + cameras = cmds.ls(cam, long=True) + if not cameras: + return + cam = cameras[0] + + # Find the index in the list + for i in range(self.cameras.count()): + value = str(self.cameras.itemText(i)) + if value == cam: + self.cameras.setCurrentIndex(i) + return + + def validate(self): + + errors = [] + camera = self.cameras.currentText() + if not cmds.objExists(camera): + errors.append("{} : Selected camera '{}' " + "does not exist!".format(self.id, camera)) + self.cameras.setStyleSheet(self.highlight) + else: + self.cameras.setStyleSheet("") + + return errors + + def get_outputs(self): + """Return currently selected camera from combobox.""" + + idx = self.cameras.currentIndex() + camera = str(self.cameras.itemText(idx)) if idx != -1 else None + + return {"camera": camera} + + def on_refresh(self, camera=None): + """Refresh the camera list with all current cameras in scene. + + A currentIndexChanged signal is only emitted for the cameras combobox + when the camera is different at the end of the refresh. + + Args: + camera (str): When name of camera is passed it will try to select + the camera with this name after the refresh. + + Returns: + None + + """ + + cam = self.get_outputs()['camera'] + + # Get original selection + if camera is None: + index = self.cameras.currentIndex() + if index != -1: + camera = self.cameras.currentText() + + self.cameras.blockSignals(True) + + # Update the list with available cameras + self.cameras.clear() + + cam_shapes = cmds.ls(type="camera") + cam_transforms = cmds.listRelatives(cam_shapes, + parent=True, + fullPath=True) + self.cameras.addItems(cam_transforms) + + # If original selection, try to reselect + self.select_camera(camera) + + self.cameras.blockSignals(False) + + # If camera changed emit signal + if cam != self.get_outputs()['camera']: + idx = self.cameras.currentIndex() + self.cameras.currentIndexChanged.emit(idx) + + def on_update_label(self): + + cam = self.cameras.currentText() + cam = cam.rsplit("|", 1)[-1] # ensure short name + self.label = "Camera ({0})".format(cam) + + self.label_changed.emit(self.label) diff --git a/pype/vendor/capture_gui/plugins/codecplugin.py b/pype/vendor/capture_gui/plugins/codecplugin.py new file mode 100644 index 0000000000..694194aafe --- /dev/null +++ b/pype/vendor/capture_gui/plugins/codecplugin.py @@ -0,0 +1,95 @@ +from capture_gui.vendor.Qt import QtCore, QtWidgets + +import capture_gui.lib as lib +import capture_gui.plugin + + +class CodecPlugin(capture_gui.plugin.Plugin): + """Codec widget. + + Allows to set format, compression and quality. + + """ + id = "Codec" + label = "Codec" + section = "config" + order = 50 + + def __init__(self, parent=None): + super(CodecPlugin, self).__init__(parent=parent) + + self._layout = QtWidgets.QHBoxLayout() + self._layout.setContentsMargins(0, 0, 0, 0) + self.setLayout(self._layout) + + self.format = QtWidgets.QComboBox() + self.compression = QtWidgets.QComboBox() + self.quality = QtWidgets.QSpinBox() + self.quality.setMinimum(0) + self.quality.setMaximum(100) + self.quality.setValue(100) + self.quality.setToolTip("Compression quality percentage") + + self._layout.addWidget(self.format) + self._layout.addWidget(self.compression) + self._layout.addWidget(self.quality) + + self.format.currentIndexChanged.connect(self.on_format_changed) + + self.refresh() + + # Default to format 'qt' + index = self.format.findText("qt") + if index != -1: + self.format.setCurrentIndex(index) + + # Default to compression 'H.264' + index = self.compression.findText("H.264") + if index != -1: + self.compression.setCurrentIndex(index) + + self.connections() + + def connections(self): + self.compression.currentIndexChanged.connect(self.options_changed) + self.format.currentIndexChanged.connect(self.options_changed) + self.quality.valueChanged.connect(self.options_changed) + + def refresh(self): + formats = sorted(lib.list_formats()) + self.format.clear() + self.format.addItems(formats) + + def on_format_changed(self): + """Refresh the available compressions.""" + + format = self.format.currentText() + compressions = lib.list_compressions(format) + self.compression.clear() + self.compression.addItems(compressions) + + def get_outputs(self): + """Get the plugin outputs that matches `capture.capture` arguments + + Returns: + dict: Plugin outputs + + """ + + return {"format": self.format.currentText(), + "compression": self.compression.currentText(), + "quality": self.quality.value()} + + def get_inputs(self, as_preset): + # a bit redundant but it will work when iterating over widgets + # so we don't have to write an exception + return self.get_outputs() + + def apply_inputs(self, settings): + codec_format = settings.get("format", 0) + compr = settings.get("compression", 4) + quality = settings.get("quality", 100) + + self.format.setCurrentIndex(self.format.findText(codec_format)) + self.compression.setCurrentIndex(self.compression.findText(compr)) + self.quality.setValue(int(quality)) diff --git a/pype/vendor/capture_gui/plugins/defaultoptionsplugin.py b/pype/vendor/capture_gui/plugins/defaultoptionsplugin.py new file mode 100644 index 0000000000..f56897e562 --- /dev/null +++ b/pype/vendor/capture_gui/plugins/defaultoptionsplugin.py @@ -0,0 +1,47 @@ +import capture +import capture_gui.plugin + + +class DefaultOptionsPlugin(capture_gui.plugin.Plugin): + """Invisible Plugin that supplies some default values to the gui. + + This enures: + - no HUD is present in playblasts + - no overscan (`overscan` set to 1.0) + - no title safe, action safe, gate mask, etc. + - active sound is included in video playblasts + + """ + order = -1 + hidden = True + + def get_outputs(self): + """Get the plugin outputs that matches `capture.capture` arguments + + Returns: + dict: Plugin outputs + + """ + + outputs = dict() + + # use active sound track + scene = capture.parse_active_scene() + outputs['sound'] = scene['sound'] + + # override default settings + outputs['show_ornaments'] = True # never show HUD or overlays + + # override camera options + outputs['camera_options'] = dict() + outputs['camera_options']['overscan'] = 1.0 + outputs['camera_options']['displayFieldChart'] = False + outputs['camera_options']['displayFilmGate'] = False + outputs['camera_options']['displayFilmOrigin'] = False + outputs['camera_options']['displayFilmPivot'] = False + outputs['camera_options']['displayGateMask'] = False + outputs['camera_options']['displayResolution'] = False + outputs['camera_options']['displaySafeAction'] = False + outputs['camera_options']['displaySafeTitle'] = False + + return outputs diff --git a/pype/vendor/capture_gui/plugins/displayplugin.py b/pype/vendor/capture_gui/plugins/displayplugin.py new file mode 100644 index 0000000000..3dffb98654 --- /dev/null +++ b/pype/vendor/capture_gui/plugins/displayplugin.py @@ -0,0 +1,179 @@ +import maya.cmds as cmds + +from capture_gui.vendor.Qt import QtCore, QtWidgets +import capture_gui.plugin +import capture_gui.colorpicker as colorpicker + + +# region GLOBALS + +BACKGROUND_DEFAULT = [0.6309999823570251, + 0.6309999823570251, + 0.6309999823570251] + +TOP_DEFAULT = [0.5350000262260437, + 0.6169999837875366, + 0.7020000219345093] + +BOTTOM_DEFAULT = [0.052000001072883606, + 0.052000001072883606, + 0.052000001072883606] + +COLORS = {"background": BACKGROUND_DEFAULT, + "backgroundTop": TOP_DEFAULT, + "backgroundBottom": BOTTOM_DEFAULT} + +LABELS = {"background": "Background", + "backgroundTop": "Top", + "backgroundBottom": "Bottom"} +# endregion GLOBALS + + +class DisplayPlugin(capture_gui.plugin.Plugin): + """Plugin to apply viewport visibilities and settings""" + + id = "Display Options" + label = "Display Options" + section = "config" + order = 70 + + def __init__(self, parent=None): + super(DisplayPlugin, self).__init__(parent=parent) + + self._colors = dict() + + self._layout = QtWidgets.QVBoxLayout() + self._layout.setContentsMargins(0, 0, 0, 0) + self.setLayout(self._layout) + + self.override = QtWidgets.QCheckBox("Override Display Options") + + self.display_type = QtWidgets.QComboBox() + self.display_type.addItems(["Solid", "Gradient"]) + + # create color columns + self._color_layout = QtWidgets.QHBoxLayout() + for label, default in COLORS.items(): + self.add_color_picker(self._color_layout, label, default) + + # populate layout + self._layout.addWidget(self.override) + self._layout.addWidget(self.display_type) + self._layout.addLayout(self._color_layout) + + # ensure widgets are in the correct enable state + self.on_toggle_override() + + self.connections() + + def connections(self): + self.override.toggled.connect(self.on_toggle_override) + self.override.toggled.connect(self.options_changed) + self.display_type.currentIndexChanged.connect(self.options_changed) + + def add_color_picker(self, layout, label, default): + """Create a column with a label and a button to select a color + + Arguments: + layout (QtWidgets.QLayout): Layout to add color picker to + label (str): system name for the color type, e.g. : backgroundTop + default (list): The default color values to start with + + Returns: + colorpicker.ColorPicker: a color picker instance + + """ + + column = QtWidgets.QVBoxLayout() + label_widget = QtWidgets.QLabel(LABELS[label]) + + color_picker = colorpicker.ColorPicker() + color_picker.color = default + + column.addWidget(label_widget) + column.addWidget(color_picker) + + column.setAlignment(label_widget, QtCore.Qt.AlignCenter) + + layout.addLayout(column) + + # connect signal + color_picker.valueChanged.connect(self.options_changed) + + # store widget + self._colors[label] = color_picker + + return color_picker + + def on_toggle_override(self): + """Callback when override is toggled. + + Enable or disable the color pickers and background type widgets bases + on the current state of the override checkbox + + Returns: + None + + """ + state = self.override.isChecked() + self.display_type.setEnabled(state) + for widget in self._colors.values(): + widget.setEnabled(state) + + def display_gradient(self): + """Return whether the background should be displayed as gradient. + + When True the colors will use the top and bottom color to define the + gradient otherwise the background color will be used as solid color. + + Returns: + bool: Whether background is gradient + + """ + return self.display_type.currentText() == "Gradient" + + def apply_inputs(self, settings): + """Apply the saved inputs from the inputs configuration + + Arguments: + settings (dict): The input settings to apply. + + """ + + for label, widget in self._colors.items(): + default = COLORS.get(label, [0, 0, 0]) # fallback default to black + value = settings.get(label, default) + widget.color = value + + override = settings.get("override_display", False) + self.override.setChecked(override) + + def get_inputs(self, as_preset): + inputs = {"override_display": self.override.isChecked()} + for label, widget in self._colors.items(): + inputs[label] = widget.color + + return inputs + + def get_outputs(self): + """Get the plugin outputs that matches `capture.capture` arguments + + Returns: + dict: Plugin outputs + + """ + + outputs = {} + if self.override.isChecked(): + outputs["displayGradient"] = self.display_gradient() + for label, widget in self._colors.items(): + outputs[label] = widget.color + else: + # Parse active color settings + outputs["displayGradient"] = cmds.displayPref(query=True, + displayGradient=True) + for key in COLORS.keys(): + color = cmds.displayRGBColor(key, query=True) + outputs[key] = color + + return {"display_options": outputs} diff --git a/pype/vendor/capture_gui/plugins/genericplugin.py b/pype/vendor/capture_gui/plugins/genericplugin.py new file mode 100644 index 0000000000..a43d43f3cc --- /dev/null +++ b/pype/vendor/capture_gui/plugins/genericplugin.py @@ -0,0 +1,95 @@ +import maya.cmds as mc +from capture_gui.vendor.Qt import QtCore, QtWidgets + +import capture_gui.plugin +import capture_gui.lib + + +class GenericPlugin(capture_gui.plugin.Plugin): + """Widget for generic options""" + id = "Generic" + label = "Generic" + section = "config" + order = 100 + + def __init__(self, parent=None): + super(GenericPlugin, self).__init__(parent=parent) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + self.setLayout(layout) + + isolate_view = QtWidgets.QCheckBox( + "Use isolate view from active panel") + off_screen = QtWidgets.QCheckBox("Render offscreen") + + layout.addWidget(isolate_view) + layout.addWidget(off_screen) + + isolate_view.stateChanged.connect(self.options_changed) + off_screen.stateChanged.connect(self.options_changed) + + self.widgets = { + "off_screen": off_screen, + "isolate_view": isolate_view + } + + self.apply_inputs(self.get_defaults()) + + def get_defaults(self): + return { + "off_screen": True, + "isolate_view": False + } + + def get_inputs(self, as_preset): + """Return the widget options + + Returns: + dict: The input settings of the widgets. + + """ + + inputs = dict() + for key, widget in self.widgets.items(): + state = widget.isChecked() + inputs[key] = state + + return inputs + + def apply_inputs(self, inputs): + """Apply the saved inputs from the inputs configuration + + Arguments: + inputs (dict): The input settings to apply. + + """ + + for key, widget in self.widgets.items(): + state = inputs.get(key, None) + if state is not None: + widget.setChecked(state) + + return inputs + + def get_outputs(self): + """Returns all the options from the widget + + Returns: dictionary with the settings + + """ + + inputs = self.get_inputs(as_preset=False) + outputs = dict() + outputs['off_screen'] = inputs['off_screen'] + + import capture_gui.lib + + # Get isolate view members of the active panel + if inputs['isolate_view']: + panel = capture_gui.lib.get_active_editor() + filter_set = mc.modelEditor(panel, query=True, viewObjects=True) + isolate = mc.sets(filter_set, query=True) if filter_set else None + outputs['isolate'] = isolate + + return outputs diff --git a/pype/vendor/capture_gui/plugins/ioplugin.py b/pype/vendor/capture_gui/plugins/ioplugin.py new file mode 100644 index 0000000000..defdc190df --- /dev/null +++ b/pype/vendor/capture_gui/plugins/ioplugin.py @@ -0,0 +1,254 @@ +import os +import logging +from functools import partial + +from capture_gui.vendor.Qt import QtCore, QtWidgets +from capture_gui import plugin, lib +from capture_gui import tokens + +log = logging.getLogger("IO") + + +class IoAction(QtWidgets.QAction): + + def __init__(self, parent, filepath): + super(IoAction, self).__init__(parent) + + action_label = os.path.basename(filepath) + + self.setText(action_label) + self.setData(filepath) + + # check if file exists and disable when false + self.setEnabled(os.path.isfile(filepath)) + + # get icon from file + info = QtCore.QFileInfo(filepath) + icon_provider = QtWidgets.QFileIconProvider() + self.setIcon(icon_provider.icon(info)) + + self.triggered.connect(self.open_object_data) + + def open_object_data(self): + lib.open_file(self.data()) + + +class IoPlugin(plugin.Plugin): + """Codec widget. + + Allows to set format, compression and quality. + + """ + id = "IO" + label = "Save" + section = "app" + order = 40 + max_recent_playblasts = 5 + + def __init__(self, parent=None): + super(IoPlugin, self).__init__(parent=parent) + + self.recent_playblasts = list() + + self._layout = QtWidgets.QVBoxLayout() + self._layout.setContentsMargins(0, 0, 0, 0) + self.setLayout(self._layout) + + # region Checkboxes + self.save_file = QtWidgets.QCheckBox(text="Save") + self.open_viewer = QtWidgets.QCheckBox(text="View when finished") + self.raw_frame_numbers = QtWidgets.QCheckBox(text="Raw frame numbers") + + checkbox_hlayout = QtWidgets.QHBoxLayout() + checkbox_hlayout.setContentsMargins(5, 0, 5, 0) + checkbox_hlayout.addWidget(self.save_file) + checkbox_hlayout.addWidget(self.open_viewer) + checkbox_hlayout.addWidget(self.raw_frame_numbers) + checkbox_hlayout.addStretch(True) + # endregion Checkboxes + + # region Path + self.path_widget = QtWidgets.QWidget() + + self.browse = QtWidgets.QPushButton("Browse") + self.file_path = QtWidgets.QLineEdit() + self.file_path.setPlaceholderText("(not set; using scene name)") + tip = "Right click in the text field to insert tokens" + self.file_path.setToolTip(tip) + self.file_path.setStatusTip(tip) + self.file_path.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.file_path.customContextMenuRequested.connect(self.show_token_menu) + + path_hlayout = QtWidgets.QHBoxLayout() + path_hlayout.setContentsMargins(0, 0, 0, 0) + path_label = QtWidgets.QLabel("Path:") + path_label.setFixedWidth(30) + + path_hlayout.addWidget(path_label) + path_hlayout.addWidget(self.file_path) + path_hlayout.addWidget(self.browse) + self.path_widget.setLayout(path_hlayout) + # endregion Path + + # region Recent Playblast + self.play_recent = QtWidgets.QPushButton("Play recent playblast") + self.recent_menu = QtWidgets.QMenu() + self.play_recent.setMenu(self.recent_menu) + # endregion Recent Playblast + + self._layout.addLayout(checkbox_hlayout) + self._layout.addWidget(self.path_widget) + self._layout.addWidget(self.play_recent) + + # Signals / connections + self.browse.clicked.connect(self.show_browse_dialog) + self.file_path.textChanged.connect(self.options_changed) + self.save_file.stateChanged.connect(self.options_changed) + self.raw_frame_numbers.stateChanged.connect(self.options_changed) + self.save_file.stateChanged.connect(self.on_save_changed) + + # Ensure state is up-to-date with current settings + self.on_save_changed() + + def on_save_changed(self): + """Update the visibility of the path field""" + + state = self.save_file.isChecked() + if state: + self.path_widget.show() + else: + self.path_widget.hide() + + def show_browse_dialog(self): + """Set the filepath using a browser dialog. + + :return: None + """ + + path = lib.browse() + if not path: + return + + # Maya's browser return Linux based file paths to ensure Windows is + # supported we use normpath + path = os.path.normpath(path) + + self.file_path.setText(path) + + def add_playblast(self, item): + """ + Add an item to the previous playblast menu + + :param item: full path to a playblast file + :type item: str + + :return: None + """ + + # If item already in the recent playblasts remove it so we are + # sure to add it as the new first most-recent + try: + self.recent_playblasts.remove(item) + except ValueError: + pass + + # Add as first in the recent playblasts + self.recent_playblasts.insert(0, item) + + # Ensure the playblast list is never longer than maximum amount + # by removing the older entries that are at the end of the list + if len(self.recent_playblasts) > self.max_recent_playblasts: + del self.recent_playblasts[self.max_recent_playblasts:] + + # Rebuild the actions menu + self.recent_menu.clear() + for playblast in self.recent_playblasts: + action = IoAction(parent=self, filepath=playblast) + self.recent_menu.addAction(action) + + def on_playblast_finished(self, options): + """Take action after the play blast is done""" + playblast_file = options['filename'] + if not playblast_file: + return + self.add_playblast(playblast_file) + + def get_outputs(self): + """Get the plugin outputs that matches `capture.capture` arguments + + Returns: + dict: Plugin outputs + + """ + + output = {"filename": None, + "raw_frame_numbers": self.raw_frame_numbers.isChecked(), + "viewer": self.open_viewer.isChecked()} + + save = self.save_file.isChecked() + if not save: + return output + + # get path, if nothing is set fall back to default + # project/images/playblast + path = self.file_path.text() + if not path: + path = lib.default_output() + + output["filename"] = path + + return output + + def get_inputs(self, as_preset): + inputs = {"name": self.file_path.text(), + "save_file": self.save_file.isChecked(), + "open_finished": self.open_viewer.isChecked(), + "recent_playblasts": self.recent_playblasts, + "raw_frame_numbers": self.raw_frame_numbers.isChecked()} + + if as_preset: + inputs["recent_playblasts"] = [] + + return inputs + + def apply_inputs(self, settings): + + directory = settings.get("name", None) + save_file = settings.get("save_file", True) + open_finished = settings.get("open_finished", True) + raw_frame_numbers = settings.get("raw_frame_numbers", False) + previous_playblasts = settings.get("recent_playblasts", []) + + self.save_file.setChecked(save_file) + self.open_viewer.setChecked(open_finished) + self.raw_frame_numbers.setChecked(raw_frame_numbers) + + for playblast in reversed(previous_playblasts): + self.add_playblast(playblast) + + self.file_path.setText(directory) + + def token_menu(self): + """ + Build the token menu based on the registered tokens + + :returns: Menu + :rtype: QtWidgets.QMenu + """ + menu = QtWidgets.QMenu(self) + registered_tokens = tokens.list_tokens() + + for token, value in registered_tokens.items(): + label = "{} \t{}".format(token, value['label']) + action = QtWidgets.QAction(label, menu) + fn = partial(self.file_path.insert, token) + action.triggered.connect(fn) + menu.addAction(action) + + return menu + + def show_token_menu(self, pos): + """Show custom manu on position of widget""" + menu = self.token_menu() + globalpos = QtCore.QPoint(self.file_path.mapToGlobal(pos)) + menu.exec_(globalpos) diff --git a/pype/vendor/capture_gui/plugins/panzoomplugin.py b/pype/vendor/capture_gui/plugins/panzoomplugin.py new file mode 100644 index 0000000000..5bf818ff2d --- /dev/null +++ b/pype/vendor/capture_gui/plugins/panzoomplugin.py @@ -0,0 +1,48 @@ +from capture_gui.vendor.Qt import QtCore, QtWidgets +import capture_gui.plugin + + +class PanZoomPlugin(capture_gui.plugin.Plugin): + """Pan/Zoom widget. + + Allows to toggle whether you want to playblast with the camera's pan/zoom + state or disable it during the playblast. When "Use pan/zoom from camera" + is *not* checked it will force disable pan/zoom. + + """ + id = "PanZoom" + label = "Pan/Zoom" + section = "config" + order = 110 + + def __init__(self, parent=None): + super(PanZoomPlugin, self).__init__(parent=parent) + + self._layout = QtWidgets.QHBoxLayout() + self._layout.setContentsMargins(5, 0, 5, 0) + self.setLayout(self._layout) + + self.pan_zoom = QtWidgets.QCheckBox("Use pan/zoom from camera") + self.pan_zoom.setChecked(True) + + self._layout.addWidget(self.pan_zoom) + + self.pan_zoom.stateChanged.connect(self.options_changed) + + def get_outputs(self): + + if not self.pan_zoom.isChecked(): + return {"camera_options": { + "panZoomEnabled": 1, + "horizontalPan": 0.0, + "verticalPan": 0.0, + "zoom": 1.0} + } + else: + return {} + + def apply_inputs(self, settings): + self.pan_zoom.setChecked(settings.get("pan_zoom", True)) + + def get_inputs(self, as_preset): + return {"pan_zoom": self.pan_zoom.isChecked()} diff --git a/pype/vendor/capture_gui/plugins/rendererplugin.py b/pype/vendor/capture_gui/plugins/rendererplugin.py new file mode 100644 index 0000000000..17932d69d9 --- /dev/null +++ b/pype/vendor/capture_gui/plugins/rendererplugin.py @@ -0,0 +1,104 @@ +import maya.cmds as cmds + +from capture_gui.vendor.Qt import QtCore, QtWidgets +import capture_gui.lib as lib +import capture_gui.plugin + + +class RendererPlugin(capture_gui.plugin.Plugin): + """Renderer plugin to control the used playblast renderer for viewport""" + + id = "Renderer" + label = "Renderer" + section = "config" + order = 60 + + def __init__(self, parent=None): + super(RendererPlugin, self).__init__(parent=parent) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + self.setLayout(layout) + + # Get active renderers for viewport + self._renderers = self.get_renderers() + + # Create list of renderers + self.renderers = QtWidgets.QComboBox() + self.renderers.addItems(self._renderers.keys()) + + layout.addWidget(self.renderers) + + self.apply_inputs(self.get_defaults()) + + # Signals + self.renderers.currentIndexChanged.connect(self.options_changed) + + def get_current_renderer(self): + """Get current renderer by internal name (non-UI) + + Returns: + str: Name of renderer. + + """ + renderer_ui = self.renderers.currentText() + renderer = self._renderers.get(renderer_ui, None) + if renderer is None: + raise RuntimeError("No valid renderer: {0}".format(renderer_ui)) + + return renderer + + def get_renderers(self): + """Collect all available renderers for playblast""" + active_editor = lib.get_active_editor() + renderers_ui = cmds.modelEditor(active_editor, + query=True, + rendererListUI=True) + renderers_id = cmds.modelEditor(active_editor, + query=True, + rendererList=True) + + renderers = dict(zip(renderers_ui, renderers_id)) + renderers.pop("Stub Renderer") + + return renderers + + def get_defaults(self): + return {"rendererName": "vp2Renderer"} + + def get_inputs(self, as_preset): + return {"rendererName": self.get_current_renderer()} + + def get_outputs(self): + """Get the plugin outputs that matches `capture.capture` arguments + + Returns: + dict: Plugin outputs + + """ + return { + "viewport_options": { + "rendererName": self.get_current_renderer() + } + } + + def apply_inputs(self, inputs): + """Apply previous settings or settings from a preset + + Args: + inputs (dict): Plugin input settings + + Returns: + None + + """ + + reverse_lookup = {value: key for key, value in self._renderers.items()} + renderer = inputs.get("rendererName", "vp2Renderer") + renderer_ui = reverse_lookup.get(renderer) + + if renderer_ui: + index = self.renderers.findText(renderer_ui) + self.renderers.setCurrentIndex(index) + else: + self.renderers.setCurrentIndex(1) diff --git a/pype/vendor/capture_gui/plugins/resolutionplugin.py b/pype/vendor/capture_gui/plugins/resolutionplugin.py new file mode 100644 index 0000000000..193a95b8ba --- /dev/null +++ b/pype/vendor/capture_gui/plugins/resolutionplugin.py @@ -0,0 +1,199 @@ +import math +from functools import partial + +import maya.cmds as cmds +from capture_gui.vendor.Qt import QtCore, QtWidgets + +import capture_gui.lib as lib +import capture_gui.plugin + + +class ResolutionPlugin(capture_gui.plugin.Plugin): + """Resolution widget. + + Allows to set scale based on set of options. + + """ + id = "Resolution" + section = "app" + order = 20 + + resolution_changed = QtCore.Signal() + + ScaleWindow = "From Window" + ScaleRenderSettings = "From Render Settings" + ScaleCustom = "Custom" + + def __init__(self, parent=None): + super(ResolutionPlugin, self).__init__(parent=parent) + + self._layout = QtWidgets.QVBoxLayout() + self._layout.setContentsMargins(0, 0, 0, 0) + self.setLayout(self._layout) + + # Scale + self.mode = QtWidgets.QComboBox() + self.mode.addItems([self.ScaleWindow, + self.ScaleRenderSettings, + self.ScaleCustom]) + self.mode.setCurrentIndex(1) # Default: From render settings + + # Custom width/height + self.resolution = QtWidgets.QWidget() + self.resolution.setContentsMargins(0, 0, 0, 0) + resolution_layout = QtWidgets.QHBoxLayout() + resolution_layout.setContentsMargins(0, 0, 0, 0) + resolution_layout.setSpacing(6) + + self.resolution.setLayout(resolution_layout) + width_label = QtWidgets.QLabel("Width") + width_label.setFixedWidth(40) + self.width = QtWidgets.QSpinBox() + self.width.setMinimum(0) + self.width.setMaximum(99999) + self.width.setValue(1920) + heigth_label = QtWidgets.QLabel("Height") + heigth_label.setFixedWidth(40) + self.height = QtWidgets.QSpinBox() + self.height.setMinimum(0) + self.height.setMaximum(99999) + self.height.setValue(1080) + + resolution_layout.addWidget(width_label) + resolution_layout.addWidget(self.width) + resolution_layout.addWidget(heigth_label) + resolution_layout.addWidget(self.height) + + self.scale_result = QtWidgets.QLineEdit() + self.scale_result.setReadOnly(True) + + # Percentage + self.percent_label = QtWidgets.QLabel("Scale") + self.percent = QtWidgets.QDoubleSpinBox() + self.percent.setMinimum(0.01) + self.percent.setValue(1.0) # default value + self.percent.setSingleStep(0.05) + + self.percent_presets = QtWidgets.QHBoxLayout() + self.percent_presets.setSpacing(4) + for value in [0.25, 0.5, 0.75, 1.0, 2.0]: + btn = QtWidgets.QPushButton(str(value)) + self.percent_presets.addWidget(btn) + btn.setFixedWidth(35) + btn.clicked.connect(partial(self.percent.setValue, value)) + + self.percent_layout = QtWidgets.QHBoxLayout() + self.percent_layout.addWidget(self.percent_label) + self.percent_layout.addWidget(self.percent) + self.percent_layout.addLayout(self.percent_presets) + + # Resulting scale display + self._layout.addWidget(self.mode) + self._layout.addWidget(self.resolution) + self._layout.addLayout(self.percent_layout) + self._layout.addWidget(self.scale_result) + + # refresh states + self.on_mode_changed() + self.on_resolution_changed() + + # connect signals + self.mode.currentIndexChanged.connect(self.on_mode_changed) + self.mode.currentIndexChanged.connect(self.on_resolution_changed) + self.percent.valueChanged.connect(self.on_resolution_changed) + self.width.valueChanged.connect(self.on_resolution_changed) + self.height.valueChanged.connect(self.on_resolution_changed) + + # Connect options changed + self.mode.currentIndexChanged.connect(self.options_changed) + self.percent.valueChanged.connect(self.options_changed) + self.width.valueChanged.connect(self.options_changed) + self.height.valueChanged.connect(self.options_changed) + + def on_mode_changed(self): + """Update the width/height enabled state when mode changes""" + + if self.mode.currentText() != self.ScaleCustom: + self.width.setEnabled(False) + self.height.setEnabled(False) + self.resolution.hide() + else: + self.width.setEnabled(True) + self.height.setEnabled(True) + self.resolution.show() + + def _get_output_resolution(self): + + options = self.get_outputs() + return int(options["width"]), int(options["height"]) + + def on_resolution_changed(self): + """Update the resulting resolution label""" + + width, height = self._get_output_resolution() + label = "Result: {0}x{1}".format(width, height) + + self.scale_result.setText(label) + + # Update label + self.label = "Resolution ({0}x{1})".format(width, height) + self.label_changed.emit(self.label) + + def get_outputs(self): + """Return width x height defined by the combination of settings + + Returns: + dict: width and height key values + + """ + mode = self.mode.currentText() + panel = lib.get_active_editor() + + if mode == self.ScaleCustom: + width = self.width.value() + height = self.height.value() + + elif mode == self.ScaleRenderSettings: + # width height from render resolution + width = cmds.getAttr("defaultResolution.width") + height = cmds.getAttr("defaultResolution.height") + + elif mode == self.ScaleWindow: + # width height from active view panel size + if not panel: + # No panel would be passed when updating in the UI as such + # the resulting resolution can't be previewed. But this should + # never happen when starting the capture. + width = 0 + height = 0 + else: + width = cmds.control(panel, query=True, width=True) + height = cmds.control(panel, query=True, height=True) + else: + raise NotImplementedError("Unsupported scale mode: " + "{0}".format(mode)) + + scale = [width, height] + percentage = self.percent.value() + scale = [math.floor(x * percentage) for x in scale] + + return {"width": scale[0], "height": scale[1]} + + def get_inputs(self, as_preset): + return {"mode": self.mode.currentText(), + "width": self.width.value(), + "height": self.height.value(), + "percent": self.percent.value()} + + def apply_inputs(self, settings): + # get value else fall back to default values + mode = settings.get("mode", self.ScaleRenderSettings) + width = int(settings.get("width", 1920)) + height = int(settings.get("height", 1080)) + percent = float(settings.get("percent", 1.0)) + + # set values + self.mode.setCurrentIndex(self.mode.findText(mode)) + self.width.setValue(width) + self.height.setValue(height) + self.percent.setValue(percent) diff --git a/pype/vendor/capture_gui/plugins/timeplugin.py b/pype/vendor/capture_gui/plugins/timeplugin.py new file mode 100644 index 0000000000..b4901f9cb4 --- /dev/null +++ b/pype/vendor/capture_gui/plugins/timeplugin.py @@ -0,0 +1,292 @@ +import sys +import logging +import re + +import maya.OpenMaya as om +from capture_gui.vendor.Qt import QtCore, QtWidgets + +import capture_gui.lib +import capture_gui.plugin + +log = logging.getLogger("Time Range") + + +def parse_frames(string): + """Parse the resulting frames list from a frame list string. + + Examples + >>> parse_frames("0-3;30") + [0, 1, 2, 3, 30] + >>> parse_frames("0,2,4,-10") + [0, 2, 4, -10] + >>> parse_frames("-10--5,-2") + [-10, -9, -8, -7, -6, -5, -2] + + Args: + string (str): The string to parse for frames. + + Returns: + list: A list of frames + + """ + + result = list() + if not string.strip(): + raise ValueError("Can't parse an empty frame string.") + + if not re.match("^[-0-9,; ]*$", string): + raise ValueError("Invalid symbols in frame string: {}".format(string)) + + for raw in re.split(";|,", string): + + # Skip empty elements + value = raw.strip().replace(" ", "") + if not value: + continue + + # Check for sequences (1-20) including negatives (-10--8) + sequence = re.search("(-?[0-9]+)-(-?[0-9]+)", value) + + # Sequence + if sequence: + start, end = sequence.groups() + frames = range(int(start), int(end) + 1) + result.extend(frames) + + # Single frame + else: + try: + frame = int(value) + except ValueError: + raise ValueError("Invalid frame description: " + "'{0}'".format(value)) + + result.append(frame) + + if not result: + # This happens when only spaces are entered with a separator like `,` or `;` + raise ValueError("Unable to parse any frames from string: {}".format(string)) + + return result + + +class TimePlugin(capture_gui.plugin.Plugin): + """Widget for time based options""" + + id = "Time Range" + section = "app" + order = 30 + + RangeTimeSlider = "Time Slider" + RangeStartEnd = "Start/End" + CurrentFrame = "Current Frame" + CustomFrames = "Custom Frames" + + def __init__(self, parent=None): + super(TimePlugin, self).__init__(parent=parent) + + self._event_callbacks = list() + + self._layout = QtWidgets.QHBoxLayout() + self._layout.setContentsMargins(5, 0, 5, 0) + self.setLayout(self._layout) + + self.mode = QtWidgets.QComboBox() + self.mode.addItems([self.RangeTimeSlider, + self.RangeStartEnd, + self.CurrentFrame, + self.CustomFrames]) + + frame_input_height = 20 + self.start = QtWidgets.QSpinBox() + self.start.setRange(-sys.maxint, sys.maxint) + self.start.setFixedHeight(frame_input_height) + self.end = QtWidgets.QSpinBox() + self.end.setRange(-sys.maxint, sys.maxint) + self.end.setFixedHeight(frame_input_height) + + # unique frames field + self.custom_frames = QtWidgets.QLineEdit() + self.custom_frames.setFixedHeight(frame_input_height) + self.custom_frames.setPlaceholderText("Example: 1-20,25;50;75,100-150") + self.custom_frames.setVisible(False) + + self._layout.addWidget(self.mode) + self._layout.addWidget(self.start) + self._layout.addWidget(self.end) + self._layout.addWidget(self.custom_frames) + + # Connect callbacks to ensure start is never higher then end + # and the end is never lower than start + self.end.valueChanged.connect(self._ensure_start) + self.start.valueChanged.connect(self._ensure_end) + + self.on_mode_changed() # force enabled state refresh + + self.mode.currentIndexChanged.connect(self.on_mode_changed) + self.start.valueChanged.connect(self.on_mode_changed) + self.end.valueChanged.connect(self.on_mode_changed) + self.custom_frames.textChanged.connect(self.on_mode_changed) + + def _ensure_start(self, value): + self.start.setValue(min(self.start.value(), value)) + + def _ensure_end(self, value): + self.end.setValue(max(self.end.value(), value)) + + def on_mode_changed(self, emit=True): + """Update the GUI when the user updated the time range or settings. + + Arguments: + emit (bool): Whether to emit the options changed signal + + Returns: + None + + """ + + mode = self.mode.currentText() + if mode == self.RangeTimeSlider: + start, end = capture_gui.lib.get_time_slider_range() + self.start.setEnabled(False) + self.end.setEnabled(False) + self.start.setVisible(True) + self.end.setVisible(True) + self.custom_frames.setVisible(False) + mode_values = int(start), int(end) + elif mode == self.RangeStartEnd: + self.start.setEnabled(True) + self.end.setEnabled(True) + self.start.setVisible(True) + self.end.setVisible(True) + self.custom_frames.setVisible(False) + mode_values = self.start.value(), self.end.value() + elif mode == self.CustomFrames: + self.start.setVisible(False) + self.end.setVisible(False) + self.custom_frames.setVisible(True) + mode_values = "({})".format(self.custom_frames.text()) + + # ensure validation state for custom frames + self.validate() + + else: + self.start.setEnabled(False) + self.end.setEnabled(False) + self.start.setVisible(True) + self.end.setVisible(True) + self.custom_frames.setVisible(False) + currentframe = int(capture_gui.lib.get_current_frame()) + mode_values = "({})".format(currentframe) + + # Update label + self.label = "Time Range {}".format(mode_values) + self.label_changed.emit(self.label) + + if emit: + self.options_changed.emit() + + def validate(self): + errors = [] + + if self.mode.currentText() == self.CustomFrames: + + # Reset + self.custom_frames.setStyleSheet("") + + try: + parse_frames(self.custom_frames.text()) + except ValueError as exc: + errors.append("{} : Invalid frame description: " + "{}".format(self.id, exc)) + self.custom_frames.setStyleSheet(self.highlight) + + return errors + + def get_outputs(self, panel=""): + """Get the plugin outputs that matches `capture.capture` arguments + + Returns: + dict: Plugin outputs + + """ + + mode = self.mode.currentText() + frames = None + + if mode == self.RangeTimeSlider: + start, end = capture_gui.lib.get_time_slider_range() + + elif mode == self.RangeStartEnd: + start = self.start.value() + end = self.end.value() + + elif mode == self.CurrentFrame: + frame = capture_gui.lib.get_current_frame() + start = frame + end = frame + + elif mode == self.CustomFrames: + frames = parse_frames(self.custom_frames.text()) + start = None + end = None + else: + raise NotImplementedError("Unsupported time range mode: " + "{0}".format(mode)) + + return {"start_frame": start, + "end_frame": end, + "frame": frames} + + def get_inputs(self, as_preset): + return {"time": self.mode.currentText(), + "start_frame": self.start.value(), + "end_frame": self.end.value(), + "frame": self.custom_frames.text()} + + def apply_inputs(self, settings): + # get values + mode = self.mode.findText(settings.get("time", self.RangeTimeSlider)) + startframe = settings.get("start_frame", 1) + endframe = settings.get("end_frame", 120) + custom_frames = settings.get("frame", None) + + # set values + self.mode.setCurrentIndex(mode) + self.start.setValue(int(startframe)) + self.end.setValue(int(endframe)) + if custom_frames is not None: + self.custom_frames.setText(custom_frames) + + def initialize(self): + self._register_callbacks() + + def uninitialize(self): + self._remove_callbacks() + + def _register_callbacks(self): + """Register maya time and playback range change callbacks. + + Register callbacks to ensure Capture GUI reacts to changes in + the Maya GUI in regards to time slider and current frame + + """ + + callback = lambda x: self.on_mode_changed(emit=False) + + # this avoid overriding the ids on re-run + currentframe = om.MEventMessage.addEventCallback("timeChanged", + callback) + timerange = om.MEventMessage.addEventCallback("playbackRangeChanged", + callback) + + self._event_callbacks.append(currentframe) + self._event_callbacks.append(timerange) + + def _remove_callbacks(self): + """Remove callbacks when closing widget""" + for callback in self._event_callbacks: + try: + om.MEventMessage.removeCallback(callback) + except RuntimeError, error: + log.error("Encounter error : {}".format(error)) diff --git a/pype/vendor/capture_gui/plugins/viewportplugin.py b/pype/vendor/capture_gui/plugins/viewportplugin.py new file mode 100644 index 0000000000..96f311fdcf --- /dev/null +++ b/pype/vendor/capture_gui/plugins/viewportplugin.py @@ -0,0 +1,292 @@ +from capture_gui.vendor.Qt import QtCore, QtWidgets +import capture_gui.plugin +import capture_gui.lib as lib +import capture + + +class ViewportPlugin(capture_gui.plugin.Plugin): + """Plugin to apply viewport visibilities and settings""" + + id = "Viewport Options" + label = "Viewport Options" + section = "config" + order = 70 + + def __init__(self, parent=None): + super(ViewportPlugin, self).__init__(parent=parent) + + # set inherited attributes + self.setObjectName(self.label) + + # custom atttributes + self.show_type_actions = list() + + # get information + self.show_types = lib.get_show_object_types() + + # set main layout for widget + self._layout = QtWidgets.QVBoxLayout() + self._layout.setContentsMargins(0, 0, 0, 0) + self.setLayout(self._layout) + + # build + # region Menus + menus_vlayout = QtWidgets.QHBoxLayout() + + # Display Lights + self.display_light_menu = self._build_light_menu() + self.display_light_menu.setFixedHeight(20) + + # Show + self.show_types_button = QtWidgets.QPushButton("Show") + self.show_types_button.setFixedHeight(20) + self.show_types_menu = self._build_show_menu() + self.show_types_button.setMenu(self.show_types_menu) + + # fill layout + menus_vlayout.addWidget(self.display_light_menu) + menus_vlayout.addWidget(self.show_types_button) + + # endregion Menus + + # region Checkboxes + checkbox_layout = QtWidgets.QGridLayout() + self.high_quality = QtWidgets.QCheckBox() + self.high_quality.setText("Force Viewport 2.0 + AA") + self.override_viewport = QtWidgets.QCheckBox("Override viewport " + "settings") + self.override_viewport.setChecked(True) + + # two sided lighting + self.two_sided_ligthing = QtWidgets.QCheckBox("Two Sided Ligthing") + self.two_sided_ligthing.setChecked(False) + + # show + self.shadows = QtWidgets.QCheckBox("Shadows") + self.shadows.setChecked(False) + + checkbox_layout.addWidget(self.override_viewport, 0, 0) + checkbox_layout.addWidget(self.high_quality, 0, 1) + checkbox_layout.addWidget(self.two_sided_ligthing, 1, 0) + checkbox_layout.addWidget(self.shadows, 1, 1) + # endregion Checkboxes + + self._layout.addLayout(checkbox_layout) + self._layout.addLayout(menus_vlayout) + + self.connections() + + def connections(self): + + self.high_quality.stateChanged.connect(self.options_changed) + self.override_viewport.stateChanged.connect(self.options_changed) + self.override_viewport.stateChanged.connect(self.on_toggle_override) + + self.two_sided_ligthing.stateChanged.connect(self.options_changed) + self.shadows.stateChanged.connect(self.options_changed) + + self.display_light_menu.currentIndexChanged.connect( + self.options_changed + ) + + def _build_show_menu(self): + """Build the menu to select which object types are shown in the output. + + Returns: + QtGui.QMenu: The visibilities "show" menu. + + """ + + menu = QtWidgets.QMenu(self) + menu.setObjectName("ShowShapesMenu") + menu.setWindowTitle("Show") + menu.setFixedWidth(180) + menu.setTearOffEnabled(True) + + # Show all check + toggle_all = QtWidgets.QAction(menu, text="All") + toggle_none = QtWidgets.QAction(menu, text="None") + menu.addAction(toggle_all) + menu.addAction(toggle_none) + menu.addSeparator() + + # add plugin shapes if any + for shape in self.show_types: + action = QtWidgets.QAction(menu, text=shape) + action.setCheckable(True) + # emit signal when the state is changed of the checkbox + action.toggled.connect(self.options_changed) + menu.addAction(action) + self.show_type_actions.append(action) + + # connect signals + toggle_all.triggered.connect(self.toggle_all_visbile) + toggle_none.triggered.connect(self.toggle_all_hide) + + return menu + + def _build_light_menu(self): + """Build lighting menu. + + Create the menu items for the different types of lighting for + in the viewport + + Returns: + None + + """ + + menu = QtWidgets.QComboBox(self) + + # names cane be found in + display_lights = (("Use Default Lighting", "default"), + ("Use All Lights", "all"), + ("Use Selected Lights", "active"), + ("Use Flat Lighting", "flat"), + ("Use No Lights", "none")) + + for label, name in display_lights: + menu.addItem(label, userData=name) + + return menu + + def on_toggle_override(self): + """Enable or disable show menu when override is checked""" + state = self.override_viewport.isChecked() + self.show_types_button.setEnabled(state) + self.high_quality.setEnabled(state) + self.display_light_menu.setEnabled(state) + self.shadows.setEnabled(state) + self.two_sided_ligthing.setEnabled(state) + + def toggle_all_visbile(self): + """Set all object types off or on depending on the state""" + for action in self.show_type_actions: + action.setChecked(True) + + def toggle_all_hide(self): + """Set all object types off or on depending on the state""" + for action in self.show_type_actions: + action.setChecked(False) + + def get_show_inputs(self): + """Return checked state of show menu items + + Returns: + dict: The checked show states in the widget. + + """ + + show_inputs = {} + # get all checked objects + for action in self.show_type_actions: + label = action.text() + name = self.show_types.get(label, None) + if name is None: + continue + show_inputs[name] = action.isChecked() + + return show_inputs + + def get_displaylights(self): + """Get and parse the currently selected displayLights options. + + Returns: + dict: The display light options + + """ + indx = self.display_light_menu.currentIndex() + return {"displayLights": self.display_light_menu.itemData(indx), + "shadows": self.shadows.isChecked(), + "twoSidedLighting": self.two_sided_ligthing.isChecked()} + + def get_inputs(self, as_preset): + """Return the widget options + + Returns: + dict: The input settings of the widgets. + + """ + inputs = {"high_quality": self.high_quality.isChecked(), + "override_viewport_options": self.override_viewport.isChecked(), + "displayLights": self.display_light_menu.currentIndex(), + "shadows": self.shadows.isChecked(), + "twoSidedLighting": self.two_sided_ligthing.isChecked()} + + inputs.update(self.get_show_inputs()) + + return inputs + + def apply_inputs(self, inputs): + """Apply the saved inputs from the inputs configuration + + Arguments: + settings (dict): The input settings to apply. + + """ + + # get input values directly from input given + override_viewport = inputs.get("override_viewport_options", True) + high_quality = inputs.get("high_quality", True) + displaylight = inputs.get("displayLights", 0) # default lighting + two_sided_ligthing = inputs.get("twoSidedLighting", False) + shadows = inputs.get("shadows", False) + + self.high_quality.setChecked(high_quality) + self.override_viewport.setChecked(override_viewport) + self.show_types_button.setEnabled(override_viewport) + + # display light menu + self.display_light_menu.setCurrentIndex(displaylight) + self.shadows.setChecked(shadows) + self.two_sided_ligthing.setChecked(two_sided_ligthing) + + for action in self.show_type_actions: + system_name = self.show_types[action.text()] + state = inputs.get(system_name, True) + action.setChecked(state) + + def get_outputs(self): + """Get the plugin outputs that matches `capture.capture` arguments + + Returns: + dict: Plugin outputs + + """ + outputs = dict() + + high_quality = self.high_quality.isChecked() + override_viewport_options = self.override_viewport.isChecked() + + if override_viewport_options: + outputs['viewport2_options'] = dict() + outputs['viewport_options'] = dict() + + if high_quality: + # force viewport 2.0 and AA + outputs['viewport_options']['rendererName'] = 'vp2Renderer' + outputs['viewport2_options']['multiSampleEnable'] = True + outputs['viewport2_options']['multiSampleCount'] = 8 + + show_per_type = self.get_show_inputs() + display_lights = self.get_displaylights() + outputs['viewport_options'].update(show_per_type) + outputs['viewport_options'].update(display_lights) + else: + # TODO: When this fails we should give the user a warning + # Use settings from the active viewport + outputs = capture.parse_active_view() + + # Remove the display options and camera attributes + outputs.pop("display_options", None) + outputs.pop("camera", None) + + # Remove the current renderer because there's already + # renderer plug-in handling that + outputs["viewport_options"].pop("rendererName", None) + + # Remove all camera options except depth of field + dof = outputs["camera_options"]["depthOfField"] + outputs["camera_options"] = {"depthOfField": dof} + + return outputs diff --git a/pype/vendor/capture_gui/presets.py b/pype/vendor/capture_gui/presets.py new file mode 100644 index 0000000000..634e8264ec --- /dev/null +++ b/pype/vendor/capture_gui/presets.py @@ -0,0 +1,105 @@ +import glob +import os +import logging + +_registered_paths = [] +log = logging.getLogger("Presets") + + +def discover(paths=None): + """Get the full list of files found in the registered folders + + Args: + paths (list, Optional): directories which host preset files or None. + When None (default) it will list from the registered preset paths. + + Returns: + list: valid .json preset file paths. + + """ + + presets = [] + for path in paths or preset_paths(): + path = os.path.normpath(path) + if not os.path.isdir(path): + continue + + # check for json files + glob_query = os.path.abspath(os.path.join(path, "*.json")) + filenames = glob.glob(glob_query) + for filename in filenames: + # skip private files + if filename.startswith("_"): + continue + + # check for file size + if not check_file_size(filename): + log.warning("Filesize is smaller than 1 byte for file '%s'", + filename) + continue + + if filename not in presets: + presets.append(filename) + + return presets + + +def check_file_size(filepath): + """Check if filesize of the given file is bigger than 1.0 byte + + Args: + filepath (str): full filepath of the file to check + + Returns: + bool: Whether bigger than 1 byte. + + """ + + file_stats = os.stat(filepath) + if file_stats.st_size < 1: + return False + return True + + +def preset_paths(): + """Return existing registered preset paths + + Returns: + list: List of full paths. + + """ + + paths = list() + for path in _registered_paths: + # filter duplicates + if path in paths: + continue + + if not os.path.exists(path): + continue + + paths.append(path) + + return paths + + +def register_preset_path(path): + """Add filepath to registered presets + + :param path: the directory of the preset file(s) + :type path: str + + :return: + """ + if path in _registered_paths: + return log.warning("Path already registered: %s", path) + + _registered_paths.append(path) + + return path + + +# Register default user folder +user_folder = os.path.expanduser("~") +capture_gui_presets = os.path.join(user_folder, "CaptureGUI", "presets") +register_preset_path(capture_gui_presets) diff --git a/pype/vendor/capture_gui/resources/config.png b/pype/vendor/capture_gui/resources/config.png new file mode 100644 index 0000000000000000000000000000000000000000..634a1da65aafd0eb2a82064de127553c6b1f353f GIT binary patch literal 870 zcmeAS@N?(olHy`uVBq!ia0vp^DImXv&W`k z6GI9=^OS0~G`&dO9+6A&#?|#t|Eh4Qs9Nhy3i1XyGcIOMW;r3o+T+&~s356YyZz|un|@PH z*e>~ejc0u-gTTg0MtC?qQDinbB`Tbh+Im4FeC4@N1(Q>6b*5@NWK6NXu|2JMOWfbM zebt3(9^A@jXP>VBFUsn)IHp!|QJ~+OjY(D$oaJDVlt@%Had{UbLP>-HA8UfXLM`E%20`}8eoxkrjS#94c$xh2GhakKKUyOmr& zDaR$@*OM{fmK(nt`!S9Qx?B=-B^EmvarG8CDX2R=oHS9?smwo3;gRK&Cu?`Ena1dF zZzKXQIv${6p-UX z6-`cEP5*YK{FNokzr}a{85zq0bv!n$*8=Kz%UdDkk=?v3@LiiJFHpFw)Q9!vzExja zzu9Pg%J5zsGx2ZOZ~sow&WqN9aYui2JQvXClIYVEWPB94??6Gq62(9TcZ~!K!$po+ z8VMx{Jq$M&HC*g8?OA npD0sD1ly5A3!PBX^K<*n?5|SArY!#s%s&jCu6{1-oD!M+A79AZ{`ie6$E3p_ zD_m~4@x^?P*>Xw=ruXEwsijtP4<#6MY+hfrH_j&Y_RpHW<@>beU2)!jzkjp%RR6b< zHn&cFQYyV`ux0Ag8JM=AJ7K#cU%T^;yLp9a?WYxPzcuS|Tl|UFI6LqTt7Fg0lBjw6 zV)ULZ+QAjnAGlHMsPBZ39xu_OOyZ7mM^~Il+g#CgJG#&6kesMcg7++29(`R5AWdG7xY6>N-{vfgx7lNE=P&h*n3*=*s~ z;V2B9l#-kJaC!LbFCU*;$vmC(`o_H+M-~3Ho8T!4J#JYS3LT1%{$a!T UrZ++Yn8+DCUHx3vIVCg!0KU#vB>(^b literal 0 HcmV?d00001 diff --git a/pype/vendor/capture_gui/resources/reset.png b/pype/vendor/capture_gui/resources/reset.png new file mode 100644 index 0000000000000000000000000000000000000000..629822cd44b6f84bd1819a3abaa8797eb2b838fd GIT binary patch literal 976 zcmeAS@N?(olHy`uVBq!ia0vp^DImA~zI`)TdXm)6yF+tdHw zsSRJI8^RR6Fm3V484gXgoGeNc5;$3A_gT3p92DX(lP&FVXi^o(v3e!Y$l|SVqvVPr z6X!IC4KG(Xvk2)lCR`3|=ST@=DnIk4edga=Ca(@yyT21isF~*=*V7~)m);<#C}Q*F4p53!;zlYzkeBp^QK^Sl zB1a%G=SV^m$Kl3m*6m<87yFN)ALFe=w)1e&&i@D|0v}$+8}A_rA9^kMY>uro5;- zOHEI3XjJfg$y5CjEjuuJ&Bm$-j!@WoAmHaH?vt zJ)14}L`2o-n1O;Z*S^}b_o|h2!krr@e9De1>rh=?nRTRzNwMvi5%Y>7kGCFLGb4#g|8CNZt_>^BHgXbH&n v;##6+dnlq&z-g_f=7d>aIFj)x{>PgA=5Feji7o-aoXX(o>gTe~DWM4fKM9-w literal 0 HcmV?d00001 diff --git a/pype/vendor/capture_gui/resources/save.png b/pype/vendor/capture_gui/resources/save.png new file mode 100644 index 0000000000000000000000000000000000000000..817af19e9f156cebdb646fffeabb87a3e26a2559 GIT binary patch literal 835 zcmeAS@N?(olHy`uVBq!ia0vp^DImub7fF=dkjx|1p+sjV^`r9ov3SX8RO+sXgkb)%O0^-K(1n%OX=n&)nX|>2T%(FqkV@ zqvDHB^gp=G)IMV)BZtE$t_`Vsuf152zyJBf-`f&S8zpa+RtOe0ylnmZ42OcW;OwgR z3gt@pQtnPKGC*jI*b*3N*a2Te&&a^X&b)^wYNvzWuHjrrtimd6$mPskqIp zKwv&H}9kS}-6kzAlPh#M3_@}+$ z`Tq@@rx^1^u8LXy*^Kp*-=*b#!q(UFX76je9+fJ)I%8WQ^AR3i2VxQ+Jb9jJ_`|l0 XH?!nJaoS^G=3wx2^>bP0l+XkK7V2>$ literal 0 HcmV?d00001 diff --git a/pype/vendor/capture_gui/tokens.py b/pype/vendor/capture_gui/tokens.py new file mode 100644 index 0000000000..d34167b53d --- /dev/null +++ b/pype/vendor/capture_gui/tokens.py @@ -0,0 +1,68 @@ +"""Token system + +The capture gui application will format tokens in the filename. +The tokens can be registered using `register_token` + +""" +from . import lib + +_registered_tokens = dict() + + +def format_tokens(string, options): + """Replace the tokens with the correlated strings + + Arguments: + string (str): filename of the playblast with tokens. + options (dict): The parsed capture options. + + Returns: + str: The formatted filename with all tokens resolved + + """ + + if not string: + return string + + for token, value in _registered_tokens.items(): + if token in string: + func = value['func'] + string = string.replace(token, func(options)) + + return string + + +def register_token(token, func, label=""): + assert token.startswith("<") and token.endswith(">") + assert callable(func) + _registered_tokens[token] = {"func": func, "label": label} + + +def list_tokens(): + return _registered_tokens.copy() + + +# register default tokens +# scene based tokens +def _camera_token(options): + """Return short name of camera from capture options""" + camera = options['camera'] + camera = camera.rsplit("|", 1)[-1] # use short name + camera = camera.replace(":", "_") # namespace `:` to `_` + return camera + + +register_token("", _camera_token, + label="Insert camera name") +register_token("", lambda options: lib.get_current_scenename() or "playblast", + label="Insert current scene name") +register_token("", lambda options: lib.get_current_renderlayer(), + label="Insert active render layer name") + +# project based tokens +register_token("", + lambda options: lib.get_project_rule("images"), + label="Insert image directory of set project") +register_token("", + lambda options: lib.get_project_rule("movie"), + label="Insert movies directory of set project") diff --git a/pype/vendor/capture_gui/vendor/Qt.py b/pype/vendor/capture_gui/vendor/Qt.py new file mode 100644 index 0000000000..3a97da872d --- /dev/null +++ b/pype/vendor/capture_gui/vendor/Qt.py @@ -0,0 +1,1030 @@ +"""The MIT License (MIT) + +Copyright (c) 2016-2017 Marcus Ottosson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Documentation + + Map all bindings to PySide2 + + Project goals: + Qt.py was born in the film and visual effects industry to address + the growing need for the development of software capable of running + with more than one flavour of the Qt bindings for Python - PySide, + PySide2, PyQt4 and PyQt5. + + 1. Build for one, run with all + 2. Explicit is better than implicit + 3. Support co-existence + + Default resolution order: + - PySide2 + - PyQt5 + - PySide + - PyQt4 + + Usage: + >> import sys + >> from Qt import QtWidgets + >> app = QtWidgets.QApplication(sys.argv) + >> button = QtWidgets.QPushButton("Hello World") + >> button.show() + >> app.exec_() + + All members of PySide2 are mapped from other bindings, should they exist. + If no equivalent member exist, it is excluded from Qt.py and inaccessible. + The idea is to highlight members that exist across all supported binding, + and guarantee that code that runs on one binding runs on all others. + + For more details, visit https://github.com/mottosso/Qt.py + +""" + +import os +import sys +import types +import shutil +import importlib + +__version__ = "1.0.0.b3" + +# Enable support for `from Qt import *` +__all__ = [] + +# Flags from environment variables +QT_VERBOSE = bool(os.getenv("QT_VERBOSE")) +QT_PREFERRED_BINDING = os.getenv("QT_PREFERRED_BINDING", "") +QT_SIP_API_HINT = os.getenv("QT_SIP_API_HINT") + +# Reference to Qt.py +Qt = sys.modules[__name__] +Qt.QtCompat = types.ModuleType("QtCompat") + +"""Common members of all bindings + +This is where each member of Qt.py is explicitly defined. +It is based on a "lowest commond denominator" of all bindings; +including members found in each of the 4 bindings. + +Find or add excluded members in build_membership.py + +""" + +_common_members = { + "QtGui": [ + "QAbstractTextDocumentLayout", + "QActionEvent", + "QBitmap", + "QBrush", + "QClipboard", + "QCloseEvent", + "QColor", + "QConicalGradient", + "QContextMenuEvent", + "QCursor", + "QDoubleValidator", + "QDrag", + "QDragEnterEvent", + "QDragLeaveEvent", + "QDragMoveEvent", + "QDropEvent", + "QFileOpenEvent", + "QFocusEvent", + "QFont", + "QFontDatabase", + "QFontInfo", + "QFontMetrics", + "QFontMetricsF", + "QGradient", + "QHelpEvent", + "QHideEvent", + "QHoverEvent", + "QIcon", + "QIconDragEvent", + "QIconEngine", + "QImage", + "QImageIOHandler", + "QImageReader", + "QImageWriter", + "QInputEvent", + "QInputMethodEvent", + "QIntValidator", + "QKeyEvent", + "QKeySequence", + "QLinearGradient", + "QMatrix2x2", + "QMatrix2x3", + "QMatrix2x4", + "QMatrix3x2", + "QMatrix3x3", + "QMatrix3x4", + "QMatrix4x2", + "QMatrix4x3", + "QMatrix4x4", + "QMouseEvent", + "QMoveEvent", + "QMovie", + "QPaintDevice", + "QPaintEngine", + "QPaintEngineState", + "QPaintEvent", + "QPainter", + "QPainterPath", + "QPainterPathStroker", + "QPalette", + "QPen", + "QPicture", + "QPictureIO", + "QPixmap", + "QPixmapCache", + "QPolygon", + "QPolygonF", + "QQuaternion", + "QRadialGradient", + "QRegExpValidator", + "QRegion", + "QResizeEvent", + "QSessionManager", + "QShortcutEvent", + "QShowEvent", + "QStandardItem", + "QStandardItemModel", + "QStatusTipEvent", + "QSyntaxHighlighter", + "QTabletEvent", + "QTextBlock", + "QTextBlockFormat", + "QTextBlockGroup", + "QTextBlockUserData", + "QTextCharFormat", + "QTextCursor", + "QTextDocument", + "QTextDocumentFragment", + "QTextFormat", + "QTextFragment", + "QTextFrame", + "QTextFrameFormat", + "QTextImageFormat", + "QTextInlineObject", + "QTextItem", + "QTextLayout", + "QTextLength", + "QTextLine", + "QTextList", + "QTextListFormat", + "QTextObject", + "QTextObjectInterface", + "QTextOption", + "QTextTable", + "QTextTableCell", + "QTextTableCellFormat", + "QTextTableFormat", + "QTransform", + "QValidator", + "QVector2D", + "QVector3D", + "QVector4D", + "QWhatsThisClickedEvent", + "QWheelEvent", + "QWindowStateChangeEvent", + "qAlpha", + "qBlue", + "qGray", + "qGreen", + "qIsGray", + "qRed", + "qRgb", + "qRgb", + ], + "QtWidgets": [ + "QAbstractButton", + "QAbstractGraphicsShapeItem", + "QAbstractItemDelegate", + "QAbstractItemView", + "QAbstractScrollArea", + "QAbstractSlider", + "QAbstractSpinBox", + "QAction", + "QActionGroup", + "QApplication", + "QBoxLayout", + "QButtonGroup", + "QCalendarWidget", + "QCheckBox", + "QColorDialog", + "QColumnView", + "QComboBox", + "QCommandLinkButton", + "QCommonStyle", + "QCompleter", + "QDataWidgetMapper", + "QDateEdit", + "QDateTimeEdit", + "QDesktopWidget", + "QDial", + "QDialog", + "QDialogButtonBox", + "QDirModel", + "QDockWidget", + "QDoubleSpinBox", + "QErrorMessage", + "QFileDialog", + "QFileIconProvider", + "QFileSystemModel", + "QFocusFrame", + "QFontComboBox", + "QFontDialog", + "QFormLayout", + "QFrame", + "QGesture", + "QGestureEvent", + "QGestureRecognizer", + "QGraphicsAnchor", + "QGraphicsAnchorLayout", + "QGraphicsBlurEffect", + "QGraphicsColorizeEffect", + "QGraphicsDropShadowEffect", + "QGraphicsEffect", + "QGraphicsEllipseItem", + "QGraphicsGridLayout", + "QGraphicsItem", + "QGraphicsItemGroup", + "QGraphicsLayout", + "QGraphicsLayoutItem", + "QGraphicsLineItem", + "QGraphicsLinearLayout", + "QGraphicsObject", + "QGraphicsOpacityEffect", + "QGraphicsPathItem", + "QGraphicsPixmapItem", + "QGraphicsPolygonItem", + "QGraphicsProxyWidget", + "QGraphicsRectItem", + "QGraphicsRotation", + "QGraphicsScale", + "QGraphicsScene", + "QGraphicsSceneContextMenuEvent", + "QGraphicsSceneDragDropEvent", + "QGraphicsSceneEvent", + "QGraphicsSceneHelpEvent", + "QGraphicsSceneHoverEvent", + "QGraphicsSceneMouseEvent", + "QGraphicsSceneMoveEvent", + "QGraphicsSceneResizeEvent", + "QGraphicsSceneWheelEvent", + "QGraphicsSimpleTextItem", + "QGraphicsTextItem", + "QGraphicsTransform", + "QGraphicsView", + "QGraphicsWidget", + "QGridLayout", + "QGroupBox", + "QHBoxLayout", + "QHeaderView", + "QInputDialog", + "QItemDelegate", + "QItemEditorCreatorBase", + "QItemEditorFactory", + "QKeyEventTransition", + "QLCDNumber", + "QLabel", + "QLayout", + "QLayoutItem", + "QLineEdit", + "QListView", + "QListWidget", + "QListWidgetItem", + "QMainWindow", + "QMdiArea", + "QMdiSubWindow", + "QMenu", + "QMenuBar", + "QMessageBox", + "QMouseEventTransition", + "QPanGesture", + "QPinchGesture", + "QPlainTextDocumentLayout", + "QPlainTextEdit", + "QProgressBar", + "QProgressDialog", + "QPushButton", + "QRadioButton", + "QRubberBand", + "QScrollArea", + "QScrollBar", + "QShortcut", + "QSizeGrip", + "QSizePolicy", + "QSlider", + "QSpacerItem", + "QSpinBox", + "QSplashScreen", + "QSplitter", + "QSplitterHandle", + "QStackedLayout", + "QStackedWidget", + "QStatusBar", + "QStyle", + "QStyleFactory", + "QStyleHintReturn", + "QStyleHintReturnMask", + "QStyleHintReturnVariant", + "QStyleOption", + "QStyleOptionButton", + "QStyleOptionComboBox", + "QStyleOptionComplex", + "QStyleOptionDockWidget", + "QStyleOptionFocusRect", + "QStyleOptionFrame", + "QStyleOptionGraphicsItem", + "QStyleOptionGroupBox", + "QStyleOptionHeader", + "QStyleOptionMenuItem", + "QStyleOptionProgressBar", + "QStyleOptionRubberBand", + "QStyleOptionSizeGrip", + "QStyleOptionSlider", + "QStyleOptionSpinBox", + "QStyleOptionTab", + "QStyleOptionTabBarBase", + "QStyleOptionTabWidgetFrame", + "QStyleOptionTitleBar", + "QStyleOptionToolBar", + "QStyleOptionToolBox", + "QStyleOptionToolButton", + "QStyleOptionViewItem", + "QStylePainter", + "QStyledItemDelegate", + "QSwipeGesture", + "QSystemTrayIcon", + "QTabBar", + "QTabWidget", + "QTableView", + "QTableWidget", + "QTableWidgetItem", + "QTableWidgetSelectionRange", + "QTapAndHoldGesture", + "QTapGesture", + "QTextBrowser", + "QTextEdit", + "QTimeEdit", + "QToolBar", + "QToolBox", + "QToolButton", + "QToolTip", + "QTreeView", + "QTreeWidget", + "QTreeWidgetItem", + "QTreeWidgetItemIterator", + "QUndoCommand", + "QUndoGroup", + "QUndoStack", + "QUndoView", + "QVBoxLayout", + "QWhatsThis", + "QWidget", + "QWidgetAction", + "QWidgetItem", + "QWizard", + "QWizardPage", + ], + "QtCore": [ + "QAbstractAnimation", + "QAbstractEventDispatcher", + "QAbstractItemModel", + "QAbstractListModel", + "QAbstractState", + "QAbstractTableModel", + "QAbstractTransition", + "QAnimationGroup", + "QBasicTimer", + "QBitArray", + "QBuffer", + "QByteArray", + "QByteArrayMatcher", + "QChildEvent", + "QCoreApplication", + "QCryptographicHash", + "QDataStream", + "QDate", + "QDateTime", + "QDir", + "QDirIterator", + "QDynamicPropertyChangeEvent", + "QEasingCurve", + "QElapsedTimer", + "QEvent", + "QEventLoop", + "QEventTransition", + "QFile", + "QFileInfo", + "QFileSystemWatcher", + "QFinalState", + "QGenericArgument", + "QGenericReturnArgument", + "QHistoryState", + "QIODevice", + "QLibraryInfo", + "QLine", + "QLineF", + "QLocale", + "QMargins", + "QMetaClassInfo", + "QMetaEnum", + "QMetaMethod", + "QMetaObject", + "QMetaProperty", + "QMimeData", + "QModelIndex", + "QMutex", + "QMutexLocker", + "QObject", + "QParallelAnimationGroup", + "QPauseAnimation", + "QPersistentModelIndex", + "QPluginLoader", + "QPoint", + "QPointF", + "QProcess", + "QProcessEnvironment", + "QPropertyAnimation", + "QReadLocker", + "QReadWriteLock", + "QRect", + "QRectF", + "QRegExp", + "QResource", + "QRunnable", + "QSemaphore", + "QSequentialAnimationGroup", + "QSettings", + "QSignalMapper", + "QSignalTransition", + "QSize", + "QSizeF", + "QSocketNotifier", + "QState", + "QStateMachine", + "QSysInfo", + "QSystemSemaphore", + "QTemporaryFile", + "QTextBoundaryFinder", + "QTextCodec", + "QTextDecoder", + "QTextEncoder", + "QTextStream", + "QTextStreamManipulator", + "QThread", + "QThreadPool", + "QTime", + "QTimeLine", + "QTimer", + "QTimerEvent", + "QTranslator", + "QUrl", + "QVariantAnimation", + "QWaitCondition", + "QWriteLocker", + "QXmlStreamAttribute", + "QXmlStreamAttributes", + "QXmlStreamEntityDeclaration", + "QXmlStreamEntityResolver", + "QXmlStreamNamespaceDeclaration", + "QXmlStreamNotationDeclaration", + "QXmlStreamReader", + "QXmlStreamWriter", + "Qt", + "QtCriticalMsg", + "QtDebugMsg", + "QtFatalMsg", + "QtMsgType", + "QtSystemMsg", + "QtWarningMsg", + "qAbs", + "qAddPostRoutine", + "qChecksum", + "qCritical", + "qDebug", + "qFatal", + "qFuzzyCompare", + "qIsFinite", + "qIsInf", + "qIsNaN", + "qIsNull", + "qRegisterResourceData", + "qUnregisterResourceData", + "qVersion", + "qWarning", + "qrand", + "qsrand", + ], + "QtXml": [ + "QDomAttr", + "QDomCDATASection", + "QDomCharacterData", + "QDomComment", + "QDomDocument", + "QDomDocumentFragment", + "QDomDocumentType", + "QDomElement", + "QDomEntity", + "QDomEntityReference", + "QDomImplementation", + "QDomNamedNodeMap", + "QDomNode", + "QDomNodeList", + "QDomNotation", + "QDomProcessingInstruction", + "QDomText", + "QXmlAttributes", + "QXmlContentHandler", + "QXmlDTDHandler", + "QXmlDeclHandler", + "QXmlDefaultHandler", + "QXmlEntityResolver", + "QXmlErrorHandler", + "QXmlInputSource", + "QXmlLexicalHandler", + "QXmlLocator", + "QXmlNamespaceSupport", + "QXmlParseException", + "QXmlReader", + "QXmlSimpleReader" + ], + "QtHelp": [ + "QHelpContentItem", + "QHelpContentModel", + "QHelpContentWidget", + "QHelpEngine", + "QHelpEngineCore", + "QHelpIndexModel", + "QHelpIndexWidget", + "QHelpSearchEngine", + "QHelpSearchQuery", + "QHelpSearchQueryWidget", + "QHelpSearchResultWidget" + ], + "QtNetwork": [ + "QAbstractNetworkCache", + "QAbstractSocket", + "QAuthenticator", + "QHostAddress", + "QHostInfo", + "QLocalServer", + "QLocalSocket", + "QNetworkAccessManager", + "QNetworkAddressEntry", + "QNetworkCacheMetaData", + "QNetworkConfiguration", + "QNetworkConfigurationManager", + "QNetworkCookie", + "QNetworkCookieJar", + "QNetworkDiskCache", + "QNetworkInterface", + "QNetworkProxy", + "QNetworkProxyFactory", + "QNetworkProxyQuery", + "QNetworkReply", + "QNetworkRequest", + "QNetworkSession", + "QSsl", + "QTcpServer", + "QTcpSocket", + "QUdpSocket" + ], + "QtOpenGL": [ + "QGL", + "QGLContext", + "QGLFormat", + "QGLWidget" + ] +} + + +def _new_module(name): + return types.ModuleType(__name__ + "." + name) + + +def _setup(module, extras): + """Install common submodules""" + + Qt.__binding__ = module.__name__ + + for name in list(_common_members) + extras: + try: + # print("Trying %s" % name) + submodule = importlib.import_module( + module.__name__ + "." + name) + except ImportError: + # print("Failed %s" % name) + continue + + setattr(Qt, "_" + name, submodule) + + if name not in extras: + # Store reference to original binding, + # but don't store speciality modules + # such as uic or QtUiTools + setattr(Qt, name, _new_module(name)) + + +def _pyside2(): + """Initialise PySide2 + + These functions serve to test the existence of a binding + along with set it up in such a way that it aligns with + the final step; adding members from the original binding + to Qt.py + + """ + + import PySide2 as module + _setup(module, ["QtUiTools"]) + + Qt.__binding_version__ = module.__version__ + + if hasattr(Qt, "_QtUiTools"): + Qt.QtCompat.loadUi = lambda fname: \ + Qt._QtUiTools.QUiLoader().load(fname) + + if hasattr(Qt, "_QtGui") and hasattr(Qt, "_QtCore"): + Qt.QtCore.QStringListModel = Qt._QtGui.QStringListModel + + if hasattr(Qt, "_QtWidgets"): + Qt.QtCompat.setSectionResizeMode = \ + Qt._QtWidgets.QHeaderView.setSectionResizeMode + + if hasattr(Qt, "_QtCore"): + Qt.__qt_version__ = Qt._QtCore.qVersion() + Qt.QtCompat.translate = Qt._QtCore.QCoreApplication.translate + + Qt.QtCore.Property = Qt._QtCore.Property + Qt.QtCore.Signal = Qt._QtCore.Signal + Qt.QtCore.Slot = Qt._QtCore.Slot + + Qt.QtCore.QAbstractProxyModel = Qt._QtCore.QAbstractProxyModel + Qt.QtCore.QSortFilterProxyModel = Qt._QtCore.QSortFilterProxyModel + Qt.QtCore.QItemSelection = Qt._QtCore.QItemSelection + Qt.QtCore.QItemSelectionRange = Qt._QtCore.QItemSelectionRange + Qt.QtCore.QItemSelectionModel = Qt._QtCore.QItemSelectionModel + + +def _pyside(): + """Initialise PySide""" + + import PySide as module + _setup(module, ["QtUiTools"]) + + Qt.__binding_version__ = module.__version__ + + if hasattr(Qt, "_QtUiTools"): + Qt.QtCompat.loadUi = lambda fname: \ + Qt._QtUiTools.QUiLoader().load(fname) + + if hasattr(Qt, "_QtGui"): + setattr(Qt, "QtWidgets", _new_module("QtWidgets")) + setattr(Qt, "_QtWidgets", Qt._QtGui) + + Qt.QtCompat.setSectionResizeMode = Qt._QtGui.QHeaderView.setResizeMode + + if hasattr(Qt, "_QtCore"): + Qt.QtCore.QAbstractProxyModel = Qt._QtGui.QAbstractProxyModel + Qt.QtCore.QSortFilterProxyModel = Qt._QtGui.QSortFilterProxyModel + Qt.QtCore.QStringListModel = Qt._QtGui.QStringListModel + Qt.QtCore.QItemSelection = Qt._QtGui.QItemSelection + Qt.QtCore.QItemSelectionRange = Qt._QtGui.QItemSelectionRange + Qt.QtCore.QItemSelectionModel = Qt._QtGui.QItemSelectionModel + + if hasattr(Qt, "_QtCore"): + Qt.__qt_version__ = Qt._QtCore.qVersion() + + Qt.QtCore.Property = Qt._QtCore.Property + Qt.QtCore.Signal = Qt._QtCore.Signal + Qt.QtCore.Slot = Qt._QtCore.Slot + + QCoreApplication = Qt._QtCore.QCoreApplication + Qt.QtCompat.translate = ( + lambda context, sourceText, disambiguation, n: + QCoreApplication.translate( + context, + sourceText, + disambiguation, + QCoreApplication.CodecForTr, + n + ) + ) + + +def _pyqt5(): + """Initialise PyQt5""" + + import PyQt5 as module + _setup(module, ["uic"]) + + if hasattr(Qt, "_uic"): + Qt.QtCompat.loadUi = lambda fname: Qt._uic.loadUi(fname) + + if hasattr(Qt, "_QtWidgets"): + Qt.QtCompat.setSectionResizeMode = \ + Qt._QtWidgets.QHeaderView.setSectionResizeMode + + if hasattr(Qt, "_QtCore"): + Qt.QtCompat.translate = Qt._QtCore.QCoreApplication.translate + + Qt.QtCore.Property = Qt._QtCore.pyqtProperty + Qt.QtCore.Signal = Qt._QtCore.pyqtSignal + Qt.QtCore.Slot = Qt._QtCore.pyqtSlot + + Qt.QtCore.QAbstractProxyModel = Qt._QtCore.QAbstractProxyModel + Qt.QtCore.QSortFilterProxyModel = Qt._QtCore.QSortFilterProxyModel + Qt.QtCore.QStringListModel = Qt._QtCore.QStringListModel + Qt.QtCore.QItemSelection = Qt._QtCore.QItemSelection + Qt.QtCore.QItemSelectionModel = Qt._QtCore.QItemSelectionModel + Qt.QtCore.QItemSelectionRange = Qt._QtCore.QItemSelectionRange + + Qt.__qt_version__ = Qt._QtCore.QT_VERSION_STR + Qt.__binding_version__ = Qt._QtCore.PYQT_VERSION_STR + + +def _pyqt4(): + """Initialise PyQt4""" + + import sip + + # Validation of envivornment variable. Prevents an error if + # the variable is invalid since it's just a hint. + try: + hint = int(QT_SIP_API_HINT) + except TypeError: + hint = None # Variable was None, i.e. not set. + except ValueError: + raise ImportError("QT_SIP_API_HINT=%s must be a 1 or 2") + + for api in ("QString", + "QVariant", + "QDate", + "QDateTime", + "QTextStream", + "QTime", + "QUrl"): + try: + sip.setapi(api, hint or 2) + except AttributeError: + raise ImportError("PyQt4 < 4.6 isn't supported by Qt.py") + except ValueError: + actual = sip.getapi(api) + if not hint: + raise ImportError("API version already set to %d" % actual) + else: + # Having provided a hint indicates a soft constraint, one + # that doesn't throw an exception. + sys.stderr.write( + "Warning: API '%s' has already been set to %d.\n" + % (api, actual) + ) + + import PyQt4 as module + _setup(module, ["uic"]) + + if hasattr(Qt, "_uic"): + Qt.QtCompat.loadUi = lambda fname: Qt._uic.loadUi(fname) + + if hasattr(Qt, "_QtGui"): + setattr(Qt, "QtWidgets", _new_module("QtWidgets")) + setattr(Qt, "_QtWidgets", Qt._QtGui) + + Qt.QtCompat.setSectionResizeMode = \ + Qt._QtGui.QHeaderView.setResizeMode + + if hasattr(Qt, "_QtCore"): + Qt.QtCore.QAbstractProxyModel = Qt._QtGui.QAbstractProxyModel + Qt.QtCore.QSortFilterProxyModel = Qt._QtGui.QSortFilterProxyModel + Qt.QtCore.QItemSelection = Qt._QtGui.QItemSelection + Qt.QtCore.QStringListModel = Qt._QtGui.QStringListModel + Qt.QtCore.QItemSelectionModel = Qt._QtGui.QItemSelectionModel + Qt.QtCore.QItemSelectionRange = Qt._QtGui.QItemSelectionRange + + if hasattr(Qt, "_QtCore"): + Qt.__qt_version__ = Qt._QtCore.QT_VERSION_STR + Qt.__binding_version__ = Qt._QtCore.PYQT_VERSION_STR + + Qt.QtCore.Property = Qt._QtCore.pyqtProperty + Qt.QtCore.Signal = Qt._QtCore.pyqtSignal + Qt.QtCore.Slot = Qt._QtCore.pyqtSlot + + QCoreApplication = Qt._QtCore.QCoreApplication + Qt.QtCompat.translate = ( + lambda context, sourceText, disambiguation, n: + QCoreApplication.translate( + context, + sourceText, + disambiguation, + QCoreApplication.CodecForTr, + n) + ) + + +def _none(): + """Internal option (used in installer)""" + + Mock = type("Mock", (), {"__getattr__": lambda Qt, attr: None}) + + Qt.__binding__ = "None" + Qt.__qt_version__ = "0.0.0" + Qt.__binding_version__ = "0.0.0" + Qt.QtCompat.loadUi = lambda fname: None + Qt.QtCompat.setSectionResizeMode = lambda *args, **kwargs: None + + for submodule in _common_members.keys(): + setattr(Qt, submodule, Mock()) + setattr(Qt, "_" + submodule, Mock()) + + +def _log(text): + if QT_VERBOSE: + sys.stdout.write(text + "\n") + + +def _convert(lines): + """Convert compiled .ui file from PySide2 to Qt.py + + Arguments: + lines (list): Each line of of .ui file + + Usage: + >> with open("myui.py") as f: + .. lines = _convert(f.readlines()) + + """ + + def parse(line): + line = line.replace("from PySide2 import", "from Qt import") + line = line.replace("QtWidgets.QApplication.translate", + "Qt.QtCompat.translate") + return line + + parsed = list() + for line in lines: + line = parse(line) + parsed.append(line) + + return parsed + + +def _cli(args): + """Qt.py command-line interface""" + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument("--convert", + help="Path to compiled Python module, e.g. my_ui.py") + parser.add_argument("--compile", + help="Accept raw .ui file and compile with native " + "PySide2 compiler.") + parser.add_argument("--stdout", + help="Write to stdout instead of file", + action="store_true") + parser.add_argument("--stdin", + help="Read from stdin instead of file", + action="store_true") + + args = parser.parse_args(args) + + if args.stdout: + raise NotImplementedError("--stdout") + + if args.stdin: + raise NotImplementedError("--stdin") + + if args.compile: + raise NotImplementedError("--compile") + + if args.convert: + sys.stdout.write("#\n" + "# WARNING: --convert is an ALPHA feature.\n#\n" + "# See https://github.com/mottosso/Qt.py/pull/132\n" + "# for details.\n" + "#\n") + + # + # ------> Read + # + with open(args.convert) as f: + lines = _convert(f.readlines()) + + backup = "%s_backup%s" % os.path.splitext(args.convert) + sys.stdout.write("Creating \"%s\"..\n" % backup) + shutil.copy(args.convert, backup) + + # + # <------ Write + # + with open(args.convert, "w") as f: + f.write("".join(lines)) + + sys.stdout.write("Successfully converted \"%s\"\n" % args.convert) + + +def _install(): + # Default order (customise order and content via QT_PREFERRED_BINDING) + default_order = ("PySide2", "PyQt5", "PySide", "PyQt4") + preferred_order = list( + b for b in QT_PREFERRED_BINDING.split(os.pathsep) if b + ) + + order = preferred_order or default_order + + available = { + "PySide2": _pyside2, + "PyQt5": _pyqt5, + "PySide": _pyside, + "PyQt4": _pyqt4, + "None": _none + } + + _log("Order: '%s'" % "', '".join(order)) + + found_binding = False + for name in order: + _log("Trying %s" % name) + + try: + available[name]() + found_binding = True + break + + except ImportError as e: + _log("ImportError: %s" % e) + + except KeyError: + _log("ImportError: Preferred binding '%s' not found." % name) + + if not found_binding: + # If not binding were found, throw this error + raise ImportError("No Qt binding were found.") + + # Install individual members + for name, members in _common_members.items(): + try: + their_submodule = getattr(Qt, "_%s" % name) + except AttributeError: + continue + + our_submodule = getattr(Qt, name) + + # Enable import * + __all__.append(name) + + # Enable direct import of submodule, + # e.g. import Qt.QtCore + sys.modules[__name__ + "." + name] = our_submodule + + for member in members: + # Accept that a submodule may miss certain members. + try: + their_member = getattr(their_submodule, member) + except AttributeError: + _log("'%s.%s' was missing." % (name, member)) + continue + + setattr(our_submodule, member, their_member) + + # Backwards compatibility + Qt.QtCompat.load_ui = Qt.QtCompat.loadUi + + +_install() + + +"""Augment QtCompat + +QtCompat contains wrappers and added functionality +to the original bindings, such as the CLI interface +and otherwise incompatible members between bindings, +such as `QHeaderView.setSectionResizeMode`. + +""" + +Qt.QtCompat._cli = _cli +Qt.QtCompat._convert = _convert + +# Enable command-line interface +if __name__ == "__main__": + _cli(sys.argv[1:]) diff --git a/pype/vendor/capture_gui/vendor/__init__.py b/pype/vendor/capture_gui/vendor/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pype/vendor/capture_gui/version.py b/pype/vendor/capture_gui/version.py new file mode 100644 index 0000000000..badefb1659 --- /dev/null +++ b/pype/vendor/capture_gui/version.py @@ -0,0 +1,9 @@ +VERSION_MAJOR = 1 +VERSION_MINOR = 5 +VERSION_PATCH = 0 + + +version = '{}.{}.{}'.format(VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH) +__version__ = version + +__all__ = ['version', 'version_info', '__version__']