From 45eb1a858e84bbf51a0a5fd984419c9eb653aef7 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 29 Nov 2018 23:33:36 +0100 Subject: [PATCH] fix adding capture-gui --- 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 + 22 files changed, 3755 insertions(+) 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_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__']