diff --git a/pype/hooks/resolve/prelaunch.py b/pype/hooks/resolve/prelaunch.py new file mode 100644 index 0000000000..bddeccf4a3 --- /dev/null +++ b/pype/hooks/resolve/prelaunch.py @@ -0,0 +1,67 @@ +import os +import traceback +import importlib +from pype.lib import PypeHook +from pypeapp import Logger +from pype.hosts.resolve import utils + + +class ResolvePrelaunch(PypeHook): + """ + This hook will check if current workfile path has Resolve + project inside. IF not, it initialize it and finally it pass + path to the project by environment variable to Premiere launcher + shell script. + """ + + def __init__(self, logger=None): + if not logger: + self.log = Logger().get_logger(self.__class__.__name__) + else: + self.log = logger + + self.signature = "( {} )".format(self.__class__.__name__) + + def execute(self, *args, env: dict = None) -> bool: + + if not env: + env = os.environ + + # making sure pyton 3.6 is installed at provided path + py36_dir = os.path.normpath(env.get("PYTHON36_RESOLVE", "")) + assert os.path.isdir(py36_dir), ( + "Python 3.6 is not installed at the provided folder path. Either " + "make sure the `environments\resolve.json` is having correctly " + "set `PYTHON36_RESOLVE` or make sure Python 3.6 is installed " + f"in given path. \nPYTHON36_RESOLVE: `{py36_dir}`" + ) + self.log.info(f"Path to Resolve Python folder: `{py36_dir}`...") + env["PYTHON36_RESOLVE"] = py36_dir + + # setting utility scripts dir for scripts syncing + us_dir = os.path.normpath(env.get("RESOLVE_UTILITY_SCRIPTS_DIR", "")) + assert os.path.isdir(us_dir), ( + "Resolve utility script dir does not exists. Either make sure " + "the `environments\resolve.json` is having correctly set " + "`RESOLVE_UTILITY_SCRIPTS_DIR` or reinstall DaVinci Resolve. \n" + f"RESOLVE_UTILITY_SCRIPTS_DIR: `{us_dir}`" + ) + + # correctly format path for pre python script + pre_py_sc = os.path.normpath(env.get("PRE_PYTHON_SCRIPT", "")) + env["PRE_PYTHON_SCRIPT"] = pre_py_sc + + try: + __import__("pype.resolve") + __import__("pyblish") + + except ImportError as e: + print(traceback.format_exc()) + print("pyblish: Could not load integration: %s " % e) + + else: + # Resolve Setup integration + importlib.reload(utils) + utils.setup(env) + + return True diff --git a/pype/hosts/photoshop/__init__.py b/pype/hosts/photoshop/__init__.py new file mode 100644 index 0000000000..01ed757a8d --- /dev/null +++ b/pype/hosts/photoshop/__init__.py @@ -0,0 +1,33 @@ +import os + +from avalon import api +import pyblish.api + + +def install(): + print("Installing Pype config...") + + plugins_directory = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(__file__))), + "plugins", + "photoshop" + ) + + pyblish.api.register_plugin_path( + os.path.join(plugins_directory, "publish") + ) + api.register_plugin_path( + api.Loader, os.path.join(plugins_directory, "load") + ) + api.register_plugin_path( + api.Creator, os.path.join(plugins_directory, "create") + ) + + pyblish.api.register_callback( + "instanceToggled", on_pyblish_instance_toggled + ) + + +def on_pyblish_instance_toggled(instance, old_value, new_value): + """Toggle layer visibility on instance toggles.""" + instance[0].Visible = new_value diff --git a/pype/hosts/resolve/README.markdown b/pype/hosts/resolve/README.markdown new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/pype/hosts/resolve/README.markdown @@ -0,0 +1 @@ + diff --git a/pype/hosts/resolve/RESOLVE_API_README.txt b/pype/hosts/resolve/RESOLVE_API_README.txt new file mode 100644 index 0000000000..139b66bc24 --- /dev/null +++ b/pype/hosts/resolve/RESOLVE_API_README.txt @@ -0,0 +1,189 @@ +Updated as of 08 March 2019 + +-------------------------- +In this package, you will find a brief introduction to the Scripting API for DaVinci Resolve Studio. Apart from this README.txt file, this package contains folders containing the basic import modules for scripting access (DaVinciResolve.py) and some representative examples. + +Overview +-------- + +As with Blackmagic Design Fusion scripts, user scripts written in Lua and Python programming languages are supported. By default, scripts can be invoked from the Console window in the Fusion page, or via command line. This permission can be changed in Resolve Preferences, to be only from Console, or to be invoked from the local network. Please be aware of the security implications when allowing scripting access from outside of the Resolve application. + + +Using a script +-------------- +DaVinci Resolve needs to be running for a script to be invoked. + +For a Resolve script to be executed from an external folder, the script needs to know of the API location. +You may need to set the these environment variables to allow for your Python installation to pick up the appropriate dependencies as shown below: + + Mac OS X: + RESOLVE_SCRIPT_API="/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting/" + RESOLVE_SCRIPT_LIB="/Applications/DaVinci Resolve/DaVinci Resolve.app/Contents/Libraries/Fusion/fusionscript.so" + PYTHONPATH="$PYTHONPATH:$RESOLVE_SCRIPT_API/Modules/" + + Windows: + RESOLVE_SCRIPT_API="%PROGRAMDATA%\\Blackmagic Design\\DaVinci Resolve\\Support\\Developer\\Scripting\\" + RESOLVE_SCRIPT_LIB="C:\\Program Files\\Blackmagic Design\\DaVinci Resolve\\fusionscript.dll" + PYTHONPATH="%PYTHONPATH%;%RESOLVE_SCRIPT_API%\\Modules\\" + + Linux: + RESOLVE_SCRIPT_API="/opt/resolve/Developer/Scripting/" + RESOLVE_SCRIPT_LIB="/opt/resolve/libs/Fusion/fusionscript.so" + PYTHONPATH="$PYTHONPATH:$RESOLVE_SCRIPT_API/Modules/" + (Note: For standard ISO Linux installations, the path above may need to be modified to refer to /home/resolve instead of /opt/resolve) + +As with Fusion scripts, Resolve scripts can also be invoked via the menu and the Console. + +On startup, DaVinci Resolve scans the Utility Scripts directory and enumerates the scripts found in the Script application menu. Placing your script in this folder and invoking it from this menu is the easiest way to use scripts. The Utility Scripts folder is located in: + Mac OS X: /Library/Application Support/Blackmagic Design/DaVinci Resolve/Fusion/Scripts/Comp/ + Windows: %APPDATA%\Blackmagic Design\DaVinci Resolve\Fusion\Scripts\Comp\ + Linux: /opt/resolve/Fusion/Scripts/Comp/ (or /home/resolve/Fusion/Scripts/Comp/ depending on installation) + +The interactive Console window allows for an easy way to execute simple scripting commands, to query or modify properties, and to test scripts. The console accepts commands in Python 2.7, Python 3.6 and Lua and evaluates and executes them immediately. For more information on how to use the Console, please refer to the DaVinci Resolve User Manual. + +This example Python script creates a simple project: + #!/usr/bin/env python + import DaVinciResolveScript as dvr_script + resolve = dvr_script.scriptapp("Resolve") + fusion = resolve.Fusion() + projectManager = resolve.GetProjectManager() + projectManager.CreateProject("Hello World") + +The resolve object is the fundamental starting point for scripting via Resolve. As a native object, it can be inspected for further scriptable properties - using table iteration and `getmetatable` in Lua and dir, help etc in Python (among other methods). A notable scriptable object above is fusion - it allows access to all existing Fusion scripting functionality. + +Running DaVinci Resolve in headless mode +---------------------------------------- + +DaVinci Resolve can be launched in a headless mode without the user interface using the -nogui command line option. When DaVinci Resolve is launched using this option, the user interface is disabled. However, the various scripting APIs will continue to work as expected. + +Basic Resolve API +----------------- + +Some commonly used API functions are described below (*). As with the resolve object, each object is inspectable for properties and functions. + + +Resolve + Fusion() --> Fusion # Returns the Fusion object. Starting point for Fusion scripts. + GetMediaStorage() --> MediaStorage # Returns media storage object to query and act on media locations. + GetProjectManager() --> ProjectManager # Returns project manager object for currently open database. + OpenPage(pageName) --> None # Switches to indicated page in DaVinci Resolve. Input can be one of ("media", "edit", "fusion", "color", "fairlight", "deliver"). +ProjectManager + CreateProject(projectName) --> Project # Creates and returns a project if projectName (text) is unique, and None if it is not. + LoadProject(projectName) --> Project # Loads and returns the project with name = projectName (text) if there is a match found, and None if there is no matching Project. + GetCurrentProject() --> Project # Returns the currently loaded Resolve project. + SaveProject() --> Bool # Saves the currently loaded project with its own name. Returns True if successful. + CreateFolder(folderName) --> Bool # Creates a folder if folderName (text) is unique. + GetProjectsInCurrentFolder() --> [project names...] # Returns an array of project names in current folder. + GetFoldersInCurrentFolder() --> [folder names...] # Returns an array of folder names in current folder. + GotoRootFolder() --> Bool # Opens root folder in database. + GotoParentFolder() --> Bool # Opens parent folder of current folder in database if current folder has parent. + OpenFolder(folderName) --> Bool # Opens folder under given name. + ImportProject(filePath) --> Bool # Imports a project under given file path. Returns true in case of success. + ExportProject(projectName, filePath) --> Bool # Exports a project based on given name into provided file path. Returns true in case of success. + RestoreProject(filePath) --> Bool # Restores a project under given backup file path. Returns true in case of success. +Project + GetMediaPool() --> MediaPool # Returns the Media Pool object. + GetTimelineCount() --> int # Returns the number of timelines currently present in the project. + GetTimelineByIndex(idx) --> Timeline # Returns timeline at the given index, 1 <= idx <= project.GetTimelineCount() + GetCurrentTimeline() --> Timeline # Returns the currently loaded timeline. + SetCurrentTimeline(timeline) --> Bool # Sets given timeline as current timeline for the project. Returns True if successful. + GetName() --> string # Returns project name. + SetName(projectName) --> Bool # Sets project name if given projectname (text) is unique. + GetPresets() --> [presets...] # Returns a table of presets and their information. + SetPreset(presetName) --> Bool # Sets preset by given presetName (string) into project. + GetRenderJobs() --> [render jobs...] # Returns a table of render jobs and their information. + GetRenderPresets() --> [presets...] # Returns a table of render presets and their information. + StartRendering(index1, index2, ...) --> Bool # Starts rendering for given render jobs based on their indices. If no parameter is given rendering would start for all render jobs. + StartRendering([idxs...]) --> Bool # Starts rendering for given render jobs based on their indices. If no parameter is given rendering would start for all render jobs. + StopRendering() --> None # Stops rendering for all render jobs. + IsRenderingInProgress() --> Bool # Returns true is rendering is in progress. + AddRenderJob() --> Bool # Adds render job to render queue. + DeleteRenderJobByIndex(idx) --> Bool # Deletes render job based on given job index (int). + DeleteAllRenderJobs() --> Bool # Deletes all render jobs. + LoadRenderPreset(presetName) --> Bool # Sets a preset as current preset for rendering if presetName (text) exists. + SaveAsNewRenderPreset(presetName) --> Bool # Creates a new render preset by given name if presetName(text) is unique. + SetRenderSettings([settings map]) --> Bool # Sets given settings for rendering. Settings map is a map, keys of map are: "SelectAllFrames", "MarkIn", "MarkOut", "TargetDir", "CustomName". + GetRenderJobStatus(idx) --> [status info] # Returns job status and completion rendering percentage of the job by given job index (int). + GetSetting(settingName) --> string # Returns setting value by given settingName (string) if the setting exist. With empty settingName the function returns a full list of settings. + SetSetting(settingName, settingValue) --> Bool # Sets project setting base on given name (string) and value (string). + GetRenderFormats() --> [render formats...]# Returns a list of available render formats. + GetRenderCodecs(renderFormat) --> [render codecs...] # Returns a list of available codecs for given render format (string). + GetCurrentRenderFormatAndCodec() --> [format, codec] # Returns currently selected render format and render codec. + SetCurrentRenderFormatAndCodec(format, codec) --> Bool # Sets given render format (string) and render codec (string) as options for rendering. +MediaStorage + GetMountedVolumes() --> [paths...] # Returns an array of folder paths corresponding to mounted volumes displayed in Resolve’s Media Storage. + GetSubFolders(folderPath) --> [paths...] # Returns an array of folder paths in the given absolute folder path. + GetFiles(folderPath) --> [paths...] # Returns an array of media and file listings in the given absolute folder path. Note that media listings may be logically consolidated entries. + RevealInStorage(path) --> None # Expands and displays a given file/folder path in Resolve’s Media Storage. + AddItemsToMediaPool(item1, item2, ...) --> [clips...] # Adds specified file/folder paths from Media Store into current Media Pool folder. Input is one or more file/folder paths. + AddItemsToMediaPool([items...]) --> [clips...] # Adds specified file/folder paths from Media Store into current Media Pool folder. Input is an array of file/folder paths. +MediaPool + GetRootFolder() --> Folder # Returns the root Folder of Media Pool + AddSubFolder(folder, name) --> Folder # Adds a new subfolder under specified Folder object with the given name. + CreateEmptyTimeline(name) --> Timeline # Adds a new timeline with given name. + AppendToTimeline(clip1, clip2...) --> Bool # Appends specified MediaPoolItem objects in the current timeline. Returns True if successful. + AppendToTimeline([clips]) --> Bool # Appends specified MediaPoolItem objects in the current timeline. Returns True if successful. + CreateTimelineFromClips(name, clip1, clip2, ...)--> Timeline # Creates a new timeline with specified name, and appends the specified MediaPoolItem objects. + CreateTimelineFromClips(name, [clips]) --> Timeline # Creates a new timeline with specified name, and appends the specified MediaPoolItem objects. + ImportTimelineFromFile(filePath) --> Timeline # Creates timeline based on parameters within given file. + GetCurrentFolder() --> Folder # Returns currently selected Folder. + SetCurrentFolder(Folder) --> Bool # Sets current folder by given Folder. +Folder + GetClips() --> [clips...] # Returns a list of clips (items) within the folder. + GetName() --> string # Returns user-defined name of the folder. + GetSubFolders() --> [folders...] # Returns a list of subfolders in the folder. +MediaPoolItem + GetMetadata(metadataType) --> [[types],[values]] # Returns a value of metadataType. If parameter is not specified returns all set metadata parameters. + SetMetadata(metadataType, metadataValue) --> Bool # Sets metadata by given type and value. Returns True if successful. + GetMediaId() --> string # Returns a unique ID name related to MediaPoolItem. + AddMarker(frameId, color, name, note, duration) --> Bool # Creates a new marker at given frameId position and with given marker information. + GetMarkers() --> [markers...] # Returns a list of all markers and their information. + AddFlag(color) --> Bool # Adds a flag with given color (text). + GetFlags() --> [colors...] # Returns a list of flag colors assigned to the item. + GetClipColor() --> string # Returns an item color as a string. + GetClipProperty(propertyName) --> [[types],[values]] # Returns property value related to the item based on given propertyName (string). if propertyName is empty then it returns a full list of properties. + SetClipProperty(propertyName, propertyValue) --> Bool # Sets into given propertyName (string) propertyValue (string). +Timeline + GetName() --> string # Returns user-defined name of the timeline. + SetName(timelineName) --> Bool # Sets timeline name is timelineName (text) is unique. + GetStartFrame() --> int # Returns frame number at the start of timeline. + GetEndFrame() --> int # Returns frame number at the end of timeline. + GetTrackCount(trackType) --> int # Returns a number of track based on specified track type ("audio", "video" or "subtitle"). + GetItemsInTrack(trackType, index) --> [items...] # Returns an array of Timeline items on the video or audio track (based on trackType) at specified index. 1 <= index <= GetTrackCount(trackType). + AddMarker(frameId, color, name, note, duration) --> Bool # Creates a new marker at given frameId position and with given marker information. + GetMarkers() --> [markers...] # Returns a list of all markers and their information. + ApplyGradeFromDRX(path, gradeMode, item1, item2, ...)--> Bool # Loads a still from given file path (string) and applies grade to Timeline Items with gradeMode (int): 0 - "No keyframes", 1 - "Source Timecode aligned", 2 - "Start Frames aligned". + ApplyGradeFromDRX(path, gradeMode, [items]) --> Bool # Loads a still from given file path (string) and applies grade to Timeline Items with gradeMode (int): 0 - "No keyframes", 1 - "Source Timecode aligned", 2 - "Start Frames aligned". + GetCurrentTimecode() --> string # Returns a string representing a timecode for current position of the timeline, while on Cut, Edit, Color and Deliver page. + GetCurrentVideoItem() --> item # Returns current video timeline item. + GetCurrentClipThumbnailImage() --> [width, height, format, data] # Returns raw thumbnail image data (This image data is encoded in base 64 format and the image format is RGB 8 bit) for the current media in the Color Page in the format of dictionary (in Python) and table (in Lua). Information return are "width", "height", "format" and "data". Example is provided in 6_get_current_media_thumbnail.py in Example folder. +TimelineItem + GetName() --> string # Returns a name of the item. + GetDuration() --> int # Returns a duration of item. + GetEnd() --> int # Returns a position of end frame. + GetFusionCompCount() --> int # Returns the number of Fusion compositions associated with the timeline item. + GetFusionCompByIndex(compIndex) --> fusionComp # Returns Fusion composition object based on given index. 1 <= compIndex <= timelineItem.GetFusionCompCount() + GetFusionCompNames() --> [names...] # Returns a list of Fusion composition names associated with the timeline item. + GetFusionCompByName(compName) --> fusionComp # Returns Fusion composition object based on given name. + GetLeftOffset() --> int # Returns a maximum extension by frame for clip from left side. + GetRightOffset() --> int # Returns a maximum extension by frame for clip from right side. + GetStart() --> int # Returns a position of first frame. + AddMarker(frameId, color, name, note, duration) --> Bool # Creates a new marker at given frameId position and with given marker information. + GetMarkers() --> [markers...] # Returns a list of all markers and their information. + GetFlags() --> [colors...] # Returns a list of flag colors assigned to the item. + GetClipColor() --> string # Returns an item color as a string. + AddFusionComp() --> fusionComp # Adds a new Fusion composition associated with the timeline item. + ImportFusionComp(path) --> fusionComp # Imports Fusion composition from given file path by creating and adding a new composition for the item. + ExportFusionComp(path, compIndex) --> Bool # Exports Fusion composition based on given index into provided file name path. + DeleteFusionCompByName(compName) --> Bool # Deletes Fusion composition by provided name. + LoadFusionCompByName(compName) --> fusionComp # Loads Fusion composition by provided name and sets it as active composition. + RenameFusionCompByName(oldName, newName) --> Bool # Renames Fusion composition by provided name with new given name. + AddVersion(versionName, versionType) --> Bool # Adds a new Version associated with the timeline item. versionType: 0 - local, 1 - remote. + DeleteVersionByName(versionName, versionType) --> Bool # Deletes Version by provided name. versionType: 0 - local, 1 - remote. + LoadVersionByName(versionName, versionType) --> Bool # Loads Version by provided name and sets it as active Version. versionType: 0 - local, 1 - remote. + RenameVersionByName(oldName, newName, versionType)--> Bool # Renames Version by provided name with new given name. versionType: 0 - local, 1 - remote. + GetMediaPoolItem() --> MediaPoolItem # Returns a corresponding to the timeline item media pool item if it exists. + GetVersionNames(versionType) --> [strings...] # Returns a list of version names by provided versionType: 0 - local, 1 - remote. + GetStereoConvergenceValues() --> [offset, value] # Returns a table of keyframe offsets and respective convergence values + GetStereoLeftFloatingWindowParams() --> [offset, value] # For the LEFT eye -> returns a table of keyframe offsets and respective floating window params. Value at particular offset includes the left, right, top and bottom floating window values + GetStereoRightFloatingWindowParams() --> [offset, value] # For the RIGHT eye -> returns a table of keyframe offsets and respective floating window params. Value at particular offset includes the left, right, top and bottom floating window values diff --git a/pype/hosts/resolve/__init__.py b/pype/hosts/resolve/__init__.py new file mode 100644 index 0000000000..72d6314b5e --- /dev/null +++ b/pype/hosts/resolve/__init__.py @@ -0,0 +1,59 @@ +from .pipeline import ( + install, + uninstall, + ls, + containerise, + publish, + launch_workfiles_app +) + +from .utils import ( + setup, + get_resolve_module +) + +from .workio import ( + open_file, + save_file, + current_file, + has_unsaved_changes, + file_extensions, + work_root +) + +from .lib import ( + get_project_manager, + set_project_manager_to_folder_name +) + +from .menu import launch_pype_menu + +__all__ = [ + # pipeline + "install", + "uninstall", + "ls", + "containerise", + "reload_pipeline", + "publish", + "launch_workfiles_app", + + # utils + "setup", + "get_resolve_module", + + # lib + "get_project_manager", + "set_project_manager_to_folder_name", + + # menu + "launch_pype_menu", + + # workio + "open_file", + "save_file", + "current_file", + "has_unsaved_changes", + "file_extensions", + "work_root" +] diff --git a/pype/hosts/resolve/action.py b/pype/hosts/resolve/action.py new file mode 100644 index 0000000000..31830937c1 --- /dev/null +++ b/pype/hosts/resolve/action.py @@ -0,0 +1,54 @@ +# absolute_import is needed to counter the `module has no cmds error` in Maya +from __future__ import absolute_import + +import pyblish.api + + +from ...action import get_errored_instances_from_context + + +class SelectInvalidAction(pyblish.api.Action): + """Select invalid clips in Resolve timeline when plug-in failed. + + To retrieve the invalid nodes this assumes a static `get_invalid()` + method is available on the plugin. + + """ + label = "Select invalid" + on = "failed" # This action is only available on a failed plug-in + icon = "search" # Icon from Awesome Icon + + def process(self, context, plugin): + + try: + from pype.hosts.resolve.utils import get_resolve_module + resolve = get_resolve_module() + self.log.debug(resolve) + except ImportError: + raise ImportError("Current host is not Resolve") + + errored_instances = get_errored_instances_from_context(context) + + # Apply pyblish.logic to get the instances for the plug-in + instances = pyblish.api.instances_by_plugin(errored_instances, plugin) + + # Get the invalid nodes for the plug-ins + self.log.info("Finding invalid clips..") + invalid = list() + for instance in instances: + invalid_nodes = plugin.get_invalid(instance) + if invalid_nodes: + if isinstance(invalid_nodes, (list, tuple)): + invalid.extend(invalid_nodes) + else: + self.log.warning("Plug-in returned to be invalid, " + "but has no selectable nodes.") + + # Ensure unique (process each node only once) + invalid = list(set(invalid)) + + if invalid: + self.log.info("Selecting invalid nodes: %s" % ", ".join(invalid)) + # TODO: select resolve timeline track items in current timeline + else: + self.log.info("No invalid nodes found.") diff --git a/pype/hosts/resolve/lib.py b/pype/hosts/resolve/lib.py new file mode 100644 index 0000000000..2576136df5 --- /dev/null +++ b/pype/hosts/resolve/lib.py @@ -0,0 +1,78 @@ +import sys +from .utils import get_resolve_module +from pypeapp import Logger + +log = Logger().get_logger(__name__, "resolve") + +self = sys.modules[__name__] +self.pm = None + + +def get_project_manager(): + if not self.pm: + resolve = get_resolve_module() + self.pm = resolve.GetProjectManager() + return self.pm + + +def set_project_manager_to_folder_name(folder_name): + """ + Sets context of Project manager to given folder by name. + + Searching for folder by given name from root folder to nested. + If no existing folder by name it will create one in root folder. + + Args: + folder_name (str): name of searched folder + + Returns: + bool: True if success + + Raises: + Exception: Cannot create folder in root + + """ + # initialize project manager + get_project_manager() + + set_folder = False + + # go back to root folder + if self.pm.GotoRootFolder(): + log.info(f"Testing existing folder: {folder_name}") + folders = convert_resolve_list_type( + self.pm.GetFoldersInCurrentFolder()) + log.info(f"Testing existing folders: {folders}") + # get me first available folder object + # with the same name as in `folder_name` else return False + if next((f for f in folders if f in folder_name), False): + log.info(f"Found existing folder: {folder_name}") + set_folder = self.pm.OpenFolder(folder_name) + + if set_folder: + return True + + # if folder by name is not existent then create one + # go back to root folder + log.info(f"Folder `{folder_name}` not found and will be created") + if self.pm.GotoRootFolder(): + try: + # create folder by given name + self.pm.CreateFolder(folder_name) + self.pm.OpenFolder(folder_name) + return True + except NameError as e: + log.error((f"Folder with name `{folder_name}` cannot be created!" + f"Error: {e}")) + return False + + +def convert_resolve_list_type(resolve_list): + """ Resolve is using indexed dictionary as list type. + `{1.0: 'vaule'}` + This will convert it to normal list class + """ + assert isinstance(resolve_list, dict), ( + "Input argument should be dict() type") + + return [resolve_list[i] for i in sorted(resolve_list.keys())] diff --git a/pype/hosts/resolve/menu.py b/pype/hosts/resolve/menu.py new file mode 100644 index 0000000000..73ea937513 --- /dev/null +++ b/pype/hosts/resolve/menu.py @@ -0,0 +1,154 @@ +import os +import sys + +from Qt import QtWidgets, QtCore + +from .pipeline import ( + publish, + launch_workfiles_app +) + +from avalon.tools import ( + creator, + loader, + sceneinventory, + libraryloader +) + + +def load_stylesheet(): + path = os.path.join(os.path.dirname(__file__), "menu_style.qss") + if not os.path.exists(path): + print("Unable to load stylesheet, file not found in resources") + return "" + + with open(path, "r") as file_stream: + stylesheet = file_stream.read() + return stylesheet + + +class Spacer(QtWidgets.QWidget): + def __init__(self, height, *args, **kwargs): + super(self.__class__, self).__init__(*args, **kwargs) + + self.setFixedHeight(height) + + real_spacer = QtWidgets.QWidget(self) + real_spacer.setObjectName("Spacer") + real_spacer.setFixedHeight(height) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(real_spacer) + + self.setLayout(layout) + + +class PypeMenu(QtWidgets.QWidget): + def __init__(self, *args, **kwargs): + super(self.__class__, self).__init__(*args, **kwargs) + + self.setObjectName("PypeMenu") + + self.setWindowFlags( + QtCore.Qt.Window + | QtCore.Qt.CustomizeWindowHint + | QtCore.Qt.WindowTitleHint + | QtCore.Qt.WindowCloseButtonHint + | QtCore.Qt.WindowStaysOnTopHint + ) + + self.setWindowTitle("Pype") + workfiles_btn = QtWidgets.QPushButton("Workfiles", self) + create_btn = QtWidgets.QPushButton("Create", self) + publish_btn = QtWidgets.QPushButton("Publish", self) + load_btn = QtWidgets.QPushButton("Load", self) + inventory_btn = QtWidgets.QPushButton("Inventory", self) + libload_btn = QtWidgets.QPushButton("Library", self) + rename_btn = QtWidgets.QPushButton("Rename", self) + set_colorspace_btn = QtWidgets.QPushButton( + "Set colorspace from presets", self + ) + reset_resolution_btn = QtWidgets.QPushButton( + "Reset Resolution from peresets", self + ) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(10, 20, 10, 20) + + layout.addWidget(workfiles_btn) + layout.addWidget(create_btn) + layout.addWidget(publish_btn) + layout.addWidget(load_btn) + layout.addWidget(inventory_btn) + + layout.addWidget(Spacer(15, self)) + + layout.addWidget(libload_btn) + + layout.addWidget(Spacer(15, self)) + + layout.addWidget(rename_btn) + + layout.addWidget(Spacer(15, self)) + + layout.addWidget(set_colorspace_btn) + layout.addWidget(reset_resolution_btn) + + self.setLayout(layout) + + workfiles_btn.clicked.connect(self.on_workfile_clicked) + create_btn.clicked.connect(self.on_create_clicked) + publish_btn.clicked.connect(self.on_publish_clicked) + load_btn.clicked.connect(self.on_load_clicked) + inventory_btn.clicked.connect(self.on_inventory_clicked) + libload_btn.clicked.connect(self.on_libload_clicked) + rename_btn.clicked.connect(self.on_rename_clicked) + set_colorspace_btn.clicked.connect(self.on_set_colorspace_clicked) + reset_resolution_btn.clicked.connect(self.on_reset_resolution_clicked) + + def on_workfile_clicked(self): + print("Clicked Workfile") + launch_workfiles_app() + + def on_create_clicked(self): + print("Clicked Create") + creator.show() + + def on_publish_clicked(self): + print("Clicked Publish") + publish(None) + + def on_load_clicked(self): + print("Clicked Load") + loader.show(use_context=True) + + def on_inventory_clicked(self): + print("Clicked Inventory") + sceneinventory.show() + + def on_libload_clicked(self): + print("Clicked Library") + libraryloader.show() + + def on_rename_clicked(self): + print("Clicked Rename") + + def on_set_colorspace_clicked(self): + print("Clicked Set Colorspace") + + def on_reset_resolution_clicked(self): + print("Clicked Reset Resolution") + + +def launch_pype_menu(): + app = QtWidgets.QApplication(sys.argv) + + pype_menu = PypeMenu() + + stylesheet = load_stylesheet() + pype_menu.setStyleSheet(stylesheet) + + pype_menu.show() + + sys.exit(app.exec_()) diff --git a/pype/hosts/resolve/menu_style.qss b/pype/hosts/resolve/menu_style.qss new file mode 100644 index 0000000000..df4fd7e949 --- /dev/null +++ b/pype/hosts/resolve/menu_style.qss @@ -0,0 +1,29 @@ +QWidget { + background-color: #282828; + border-radius: 3; +} + +QPushButton { + border: 1px solid #090909; + background-color: #201f1f; + color: #ffffff; + padding: 5; +} + +QPushButton:focus { + background-color: "#171717"; + color: #d0d0d0; +} + +QPushButton:hover { + background-color: "#171717"; + color: #e64b3d; +} + +#PypeMenu { + border: 1px solid #fef9ef; +} + +#Spacer { + background-color: #282828; +} diff --git a/pype/hosts/resolve/pipeline.py b/pype/hosts/resolve/pipeline.py new file mode 100644 index 0000000000..967aed1436 --- /dev/null +++ b/pype/hosts/resolve/pipeline.py @@ -0,0 +1,142 @@ +""" +Basic avalon integration +""" +import os +# import sys +from avalon.tools import workfiles +from avalon import api as avalon +from pyblish import api as pyblish +from pypeapp import Logger + +log = Logger().get_logger(__name__, "resolve") + +# self = sys.modules[__name__] + +AVALON_CONFIG = os.environ["AVALON_CONFIG"] +PARENT_DIR = os.path.dirname(__file__) +PACKAGE_DIR = os.path.dirname(PARENT_DIR) +PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") + +LOAD_PATH = os.path.join(PLUGINS_DIR, "resolve", "load") +CREATE_PATH = os.path.join(PLUGINS_DIR, "resolve", "create") +INVENTORY_PATH = os.path.join(PLUGINS_DIR, "resolve", "inventory") + +PUBLISH_PATH = os.path.join( + PLUGINS_DIR, "resolve", "publish" +).replace("\\", "/") + +AVALON_CONTAINERS = ":AVALON_CONTAINERS" +# IS_HEADLESS = not hasattr(cmds, "about") or cmds.about(batch=True) + + +def install(): + """Install resolve-specific functionality of avalon-core. + + This is where you install menus and register families, data + and loaders into resolve. + + It is called automatically when installing via `api.install(resolve)`. + + See the Maya equivalent for inspiration on how to implement this. + + """ + + # Disable all families except for the ones we explicitly want to see + family_states = [ + "imagesequence", + "mov" + ] + avalon.data["familiesStateDefault"] = False + avalon.data["familiesStateToggled"] = family_states + + log.info("pype.hosts.resolve installed") + + pyblish.register_host("resolve") + pyblish.register_plugin_path(PUBLISH_PATH) + log.info("Registering DaVinci Resovle plug-ins..") + + avalon.register_plugin_path(avalon.Loader, LOAD_PATH) + avalon.register_plugin_path(avalon.Creator, CREATE_PATH) + avalon.register_plugin_path(avalon.InventoryAction, INVENTORY_PATH) + + +def uninstall(): + """Uninstall all tha was installed + + This is where you undo everything that was done in `install()`. + That means, removing menus, deregistering families and data + and everything. It should be as though `install()` was never run, + because odds are calling this function means the user is interested + in re-installing shortly afterwards. If, for example, he has been + modifying the menu or registered families. + + """ + pyblish.deregister_host("resolve") + pyblish.deregister_plugin_path(PUBLISH_PATH) + log.info("Deregistering DaVinci Resovle plug-ins..") + + avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH) + avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH) + avalon.deregister_plugin_path(avalon.InventoryAction, INVENTORY_PATH) + + +def containerise(obj, + name, + namespace, + context, + loader=None, + data=None): + """Bundle Resolve's object into an assembly and imprint it with metadata + + Containerisation enables a tracking of version, author and origin + for loaded assets. + + Arguments: + obj (obj): Resolve's object to imprint as container + name (str): Name of resulting assembly + namespace (str): Namespace under which to host container + context (dict): Asset information + loader (str, optional): Name of node used to produce this container. + + Returns: + obj (obj): containerised object + + """ + pass + + +def ls(): + """List available containers. + + This function is used by the Container Manager in Nuke. You'll + need to implement a for-loop that then *yields* one Container at + a time. + + See the `container.json` schema for details on how it should look, + and the Maya equivalent, which is in `avalon.maya.pipeline` + """ + pass + + +def parse_container(container): + """Return the container node's full container data. + + Args: + container (str): A container node name. + + Returns: + dict: The container schema data for this container node. + + """ + pass + + +def launch_workfiles_app(*args): + workdir = os.environ["AVALON_WORKDIR"] + workfiles.show(workdir) + + +def publish(parent): + """Shorthand to publish from within host""" + from avalon.tools import publish + return publish.show(parent) diff --git a/pype/hosts/resolve/plugin.py b/pype/hosts/resolve/plugin.py new file mode 100644 index 0000000000..628d4bdb26 --- /dev/null +++ b/pype/hosts/resolve/plugin.py @@ -0,0 +1,75 @@ +from avalon import api +# from pype.hosts.resolve import lib as drlib +from avalon.vendor import qargparse + + +def get_reference_node_parents(ref): + """Return all parent reference nodes of reference node + + Args: + ref (str): reference node. + + Returns: + list: The upstream parent reference nodes. + + """ + parents = [] + return parents + + +class SequenceLoader(api.Loader): + """A basic SequenceLoader for Resolve + + This will implement the basic behavior for a loader to inherit from that + will containerize the reference and will implement the `remove` and + `update` logic. + + """ + + options = [ + qargparse.Toggle( + "handles", + label="Include handles", + default=0, + help="Load with handles or without?" + ), + qargparse.Choice( + "load_to", + label="Where to load clips", + items=[ + "Current timeline", + "New timeline" + ], + default=0, + help="Where do you want clips to be loaded?" + ), + qargparse.Choice( + "load_how", + label="How to load clips", + items=[ + "original timing", + "sequential in order" + ], + default=0, + help="Would you like to place it at orignal timing?" + ) + ] + + def load( + self, + context, + name=None, + namespace=None, + options=None + ): + pass + + def update(self, container, representation): + """Update an existing `container` + """ + pass + + def remove(self, container): + """Remove an existing `container` + """ + pass diff --git a/pype/hosts/resolve/preload_console.py b/pype/hosts/resolve/preload_console.py new file mode 100644 index 0000000000..ea1bd4f180 --- /dev/null +++ b/pype/hosts/resolve/preload_console.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +import time +from pype.hosts.resolve.utils import get_resolve_module +from pypeapp import Logger + +log = Logger().get_logger(__name__, "resolve") + +wait_delay = 2.5 +wait = 0.00 +ready = None +while True: + try: + # Create project and set parameters: + resolve = get_resolve_module() + pm = resolve.GetProjectManager() + if pm: + ready = None + else: + ready = True + except AttributeError: + pass + + if ready is None: + time.sleep(wait_delay) + log.info(f"Waiting {wait}s for Resolve to have opened Project Manager") + wait += wait_delay + else: + print(f"Preloaded variables: \n\n\tResolve module: " + f"`resolve` > {type(resolve)} \n\tProject manager: " + f"`pm` > {type(pm)}") + break diff --git a/pype/hosts/resolve/utility_scripts/Pype_menu.py b/pype/hosts/resolve/utility_scripts/Pype_menu.py new file mode 100644 index 0000000000..1f5cd36277 --- /dev/null +++ b/pype/hosts/resolve/utility_scripts/Pype_menu.py @@ -0,0 +1,26 @@ +import os +import sys +import avalon.api as avalon +import pype + +from pypeapp import Logger + +log = Logger().get_logger(__name__) + + +def main(env): + import pype.hosts.resolve as bmdvr + # Registers pype's Global pyblish plugins + pype.install() + + # activate resolve from pype + avalon.install(bmdvr) + + log.info(f"Avalon registred hosts: {avalon.registered_host()}") + + bmdvr.launch_pype_menu() + + +if __name__ == "__main__": + result = main(os.environ) + sys.exit(not bool(result)) diff --git a/pype/hosts/resolve/utility_scripts/README.markdown b/pype/hosts/resolve/utility_scripts/README.markdown new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/pype/hosts/resolve/utility_scripts/README.markdown @@ -0,0 +1 @@ + diff --git a/pype/hosts/resolve/utility_scripts/__dev_compound_clip.py b/pype/hosts/resolve/utility_scripts/__dev_compound_clip.py new file mode 100644 index 0000000000..fe47008c70 --- /dev/null +++ b/pype/hosts/resolve/utility_scripts/__dev_compound_clip.py @@ -0,0 +1,65 @@ +#! python3 +# -*- coding: utf-8 -*- + + +# convert clip def +def convert_clip(timeline=None): + """Convert timeline item (clip) into compound clip pype container + + Args: + timeline (MediaPool.Timeline): Object of timeline + + Returns: + bool: `True` if success + + Raises: + Exception: description + + """ + pass + + +# decorator function create_current_timeline_media_bin() +def create_current_timeline_media_bin(timeline=None): + """Convert timeline item (clip) into compound clip pype container + + Args: + timeline (MediaPool.Timeline): Object of timeline + + Returns: + bool: `True` if success + + Raises: + Exception: description + + """ + pass + + +# decorator function get_selected_track_items() +def get_selected_track_items(): + """Convert timeline item (clip) into compound clip pype container + + Args: + timeline (MediaPool.Timeline): Object of timeline + + Returns: + bool: `True` if success + + Raises: + Exception: description + + """ + print("testText") + + +# PypeCompoundClip() class +class PypeCompoundClip(object): + """docstring for .""" + + def __init__(self, arg): + super(self).__init__() + self.arg = arg + + def create_compound_clip(self): + pass diff --git a/pype/hosts/resolve/utility_scripts/__test_pyblish.py b/pype/hosts/resolve/utility_scripts/__test_pyblish.py new file mode 100644 index 0000000000..a6fe991025 --- /dev/null +++ b/pype/hosts/resolve/utility_scripts/__test_pyblish.py @@ -0,0 +1,57 @@ +import os +import sys +import pype +import importlib +import pyblish.api +import pyblish.util +import avalon.api +from avalon.tools import publish +from pypeapp import Logger + +log = Logger().get_logger(__name__) + + +def main(env): + # Registers pype's Global pyblish plugins + pype.install() + + # Register Host (and it's pyblish plugins) + host_name = env["AVALON_APP"] + # TODO not sure if use "pype." or "avalon." for host import + host_import_str = f"pype.{host_name}" + + try: + host_module = importlib.import_module(host_import_str) + except ModuleNotFoundError: + log.error(( + f"Host \"{host_name}\" can't be imported." + f" Import string \"{host_import_str}\" failed." + )) + return False + + avalon.api.install(host_module) + + # Register additional paths + addition_paths_str = env.get("PUBLISH_PATHS") or "" + addition_paths = addition_paths_str.split(os.pathsep) + for path in addition_paths: + path = os.path.normpath(path) + if not os.path.exists(path): + continue + + pyblish.api.register_plugin_path(path) + + # Register project specific plugins + project_name = os.environ["AVALON_PROJECT"] + project_plugins_paths = env.get("PYPE_PROJECT_PLUGINS") or "" + for path in project_plugins_paths.split(os.pathsep): + plugin_path = os.path.join(path, project_name, "plugins") + if os.path.exists(plugin_path): + pyblish.api.register_plugin_path(plugin_path) + + return publish.show() + + +if __name__ == "__main__": + result = main(os.environ) + sys.exit(not bool(result)) diff --git a/pype/hosts/resolve/utility_scripts/__test_subprocess.py b/pype/hosts/resolve/utility_scripts/__test_subprocess.py new file mode 100644 index 0000000000..bdc57bbf00 --- /dev/null +++ b/pype/hosts/resolve/utility_scripts/__test_subprocess.py @@ -0,0 +1,35 @@ +#! python3 +# -*- coding: utf-8 -*- +import os +from pypeapp import execute, Logger +from pype.hosts.resolve.utils import get_resolve_module + +log = Logger().get_logger("Resolve") + +CURRENT_DIR = os.getenv("RESOLVE_UTILITY_SCRIPTS_DIR", "") +python_dir = os.getenv("PYTHON36_RESOLVE") +python_exe = os.path.normpath( + os.path.join(python_dir, "python.exe") +) + +resolve = get_resolve_module() +PM = resolve.GetProjectManager() +P = PM.GetCurrentProject() + +log.info(P.GetName()) + + +# ______________________________________________________ +# testing subprocessing Scripts +testing_py = os.path.join(CURRENT_DIR, "ResolvePageSwitcher.py") +testing_py = os.path.normpath(testing_py) +log.info(f"Testing path to script: `{testing_py}`") + +returncode = execute( + [python_exe, os.path.normpath(testing_py)], + env=dict(os.environ) +) + +# Check if output file exists +if returncode != 0: + log.error("Executing failed!") diff --git a/pype/hosts/resolve/utils.py b/pype/hosts/resolve/utils.py new file mode 100644 index 0000000000..f5add53a6b --- /dev/null +++ b/pype/hosts/resolve/utils.py @@ -0,0 +1,136 @@ +#! python3 + +""" +Resolve's tools for setting environment +""" + +import sys +import os +import shutil + +from pypeapp import Logger + +log = Logger().get_logger(__name__, "resolve") + +self = sys.modules[__name__] +self.bmd = None + + +def get_resolve_module(): + # dont run if already loaded + if self.bmd: + return self.bmd + + try: + """ + The PYTHONPATH needs to be set correctly for this import + statement to work. An alternative is to import the + DaVinciResolveScript by specifying absolute path + (see ExceptionHandler logic) + """ + import DaVinciResolveScript as bmd + except ImportError: + if sys.platform.startswith("darwin"): + expected_path = ("/Library/Application Support/Blackmagic Design" + "/DaVinci Resolve/Developer/Scripting/Modules") + elif sys.platform.startswith("win") \ + or sys.platform.startswith("cygwin"): + expected_path = os.path.normpath( + os.getenv('PROGRAMDATA') + ( + "/Blackmagic Design/DaVinci Resolve/Support/Developer" + "/Scripting/Modules" + ) + ) + elif sys.platform.startswith("linux"): + expected_path = "/opt/resolve/libs/Fusion/Modules" + + # check if the default path has it... + print(("Unable to find module DaVinciResolveScript from " + "$PYTHONPATH - trying default locations")) + + module_path = os.path.normpath( + os.path.join( + expected_path, + "DaVinciResolveScript.py" + ) + ) + + try: + import imp + bmd = imp.load_source('DaVinciResolveScript', module_path) + except ImportError: + # No fallbacks ... report error: + log.error( + ("Unable to find module DaVinciResolveScript - please " + "ensure that the module DaVinciResolveScript is " + "discoverable by python") + ) + log.error( + ("For a default DaVinci Resolve installation, the " + f"module is expected to be located in: {expected_path}") + ) + sys.exit() + # assign global var and return + self.bmd = bmd.scriptapp("Resolve") + return self.bmd + + +def _sync_utility_scripts(env=None): + """ Synchronizing basic utlility scripts for resolve. + + To be able to run scripts from inside `Resolve/Workspace/Scripts` menu + all scripts has to be accessible from defined folder. + """ + if not env: + env = os.environ + + # initiate inputs + scripts = {} + us_env = env.get("RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR") + us_dir = env.get("RESOLVE_UTILITY_SCRIPTS_DIR", "") + us_paths = [os.path.join( + os.path.dirname(__file__), + "utility_scripts" + )] + + # collect script dirs + if us_env: + log.info(f"Utility Scripts Env: `{us_env}`") + us_paths = us_env.split( + os.pathsep) + us_paths + + # collect scripts from dirs + for path in us_paths: + scripts.update({path: os.listdir(path)}) + + log.info(f"Utility Scripts Dir: `{us_paths}`") + log.info(f"Utility Scripts: `{scripts}`") + + # make sure no script file is in folder + if next((s for s in os.listdir(us_dir)), None): + for s in os.listdir(us_dir): + path = os.path.join(us_dir, s) + log.info(f"Removing `{path}`...") + os.remove(path) + + # copy scripts into Resolve's utility scripts dir + for d, sl in scripts.items(): + # directory and scripts list + for s in sl: + # script in script list + src = os.path.join(d, s) + dst = os.path.join(us_dir, s) + log.info(f"Copying `{src}` to `{dst}`...") + shutil.copy2(src, dst) + + +def setup(env=None): + """ Wrapper installer started from pype.hooks.resolve.ResolvePrelaunch() + """ + if not env: + env = os.environ + + # synchronize resolve utility scripts + _sync_utility_scripts(env) + + log.info("Resolve Pype wrapper has been installed") diff --git a/pype/hosts/resolve/workio.py b/pype/hosts/resolve/workio.py new file mode 100644 index 0000000000..e1e30a8734 --- /dev/null +++ b/pype/hosts/resolve/workio.py @@ -0,0 +1,92 @@ +"""Host API required Work Files tool""" + +import os +from pypeapp import Logger +from .lib import ( + get_project_manager, + set_project_manager_to_folder_name +) + + +log = Logger().get_logger(__name__, "resolve") + +exported_projet_ext = ".drp" + + +def file_extensions(): + return [exported_projet_ext] + + +def has_unsaved_changes(): + get_project_manager().SaveProject() + return False + + +def save_file(filepath): + pm = get_project_manager() + file = os.path.basename(filepath) + fname, _ = os.path.splitext(file) + project = pm.GetCurrentProject() + name = project.GetName() + + if "Untitled Project" not in name: + log.info("Saving project: `{}` as '{}'".format(name, file)) + pm.ExportProject(name, filepath) + else: + log.info("Creating new project...") + pm.CreateProject(fname) + pm.ExportProject(name, filepath) + + +def open_file(filepath): + """ + Loading project + """ + pm = get_project_manager() + file = os.path.basename(filepath) + fname, _ = os.path.splitext(file) + dname, _ = fname.split("_v") + + # deal with current project + project = pm.GetCurrentProject() + log.info(f"Test `pm`: {pm}") + pm.SaveProject() + + try: + log.info(f"Test `dname`: {dname}") + if not set_project_manager_to_folder_name(dname): + raise + # load project from input path + project = pm.LoadProject(fname) + log.info(f"Project {project.GetName()} opened...") + return True + except AttributeError: + log.warning((f"Project with name `{fname}` does not exist! It will " + f"be imported from {filepath} and then loaded...")) + if pm.ImportProject(filepath): + # load project from input path + project = pm.LoadProject(fname) + log.info(f"Project imported/loaded {project.GetName()}...") + return True + else: + return False + + +def current_file(): + pm = get_project_manager() + current_dir = os.getenv("AVALON_WORKDIR") + project = pm.GetCurrentProject() + name = project.GetName() + fname = name + exported_projet_ext + current_file = os.path.join(current_dir, fname) + normalised = os.path.normpath(current_file) + + # Unsaved current file + if normalised == "": + return None + + return normalised + + +def work_root(session): + return os.path.normpath(session["AVALON_WORKDIR"]).replace("\\", "/") diff --git a/pype/modules/clockify/clockify.py b/pype/modules/clockify/clockify.py index 6d5dc9213c..2ab22702c1 100644 --- a/pype/modules/clockify/clockify.py +++ b/pype/modules/clockify/clockify.py @@ -1,7 +1,7 @@ import os import threading from pype.api import Logger -from pypeapp import style +from avalon import style from Qt import QtWidgets from . import ClockifySettings, ClockifyAPI, MessageWidget diff --git a/pype/modules/clockify/widget_message.py b/pype/modules/clockify/widget_message.py index 349875b9e5..f919c3f819 100644 --- a/pype/modules/clockify/widget_message.py +++ b/pype/modules/clockify/widget_message.py @@ -1,5 +1,5 @@ from Qt import QtCore, QtGui, QtWidgets -from pypeapp import style +from avalon import style class MessageWidget(QtWidgets.QWidget): diff --git a/pype/modules/clockify/widget_settings.py b/pype/modules/clockify/widget_settings.py index 027268834c..956bdb1916 100644 --- a/pype/modules/clockify/widget_settings.py +++ b/pype/modules/clockify/widget_settings.py @@ -1,6 +1,6 @@ import os from Qt import QtCore, QtGui, QtWidgets -from pypeapp import style +from avalon import style class ClockifySettings(QtWidgets.QWidget): diff --git a/pype/modules/ftrack/actions/action_application_loader.py b/pype/modules/ftrack/actions/action_application_loader.py index 11aac28615..ec7fc53fb6 100644 --- a/pype/modules/ftrack/actions/action_application_loader.py +++ b/pype/modules/ftrack/actions/action_application_loader.py @@ -1,7 +1,7 @@ import os import toml import time -from pype.modules.ftrack import AppAction +from pype.modules.ftrack.lib import AppAction from avalon import lib from pype.api import Logger from pype.lib import get_all_avalon_projects @@ -72,7 +72,7 @@ def register(session, plugins_presets={}): for app in apps: try: registerApp(app, session, plugins_presets) - if app_counter%5 == 0: + if app_counter % 5 == 0: time.sleep(0.1) app_counter += 1 except Exception as exc: diff --git a/pype/modules/ftrack/actions/action_attributes_remapper.py b/pype/modules/ftrack/actions/action_attributes_remapper.py deleted file mode 100644 index b5fad7dc45..0000000000 --- a/pype/modules/ftrack/actions/action_attributes_remapper.py +++ /dev/null @@ -1,284 +0,0 @@ -import os - -import ftrack_api -from pype.modules.ftrack import BaseAction -from pype.modules.ftrack.lib.io_nonsingleton import DbConnector - - -class AttributesRemapper(BaseAction): - '''Edit meta data action.''' - - ignore_me = True - #: Action identifier. - identifier = 'attributes.remapper' - #: Action label. - label = "Pype Doctor" - variant = '- Attributes Remapper' - #: Action description. - description = 'Remaps attributes in avalon DB' - - #: roles that are allowed to register this action - role_list = ["Pypeclub", "Administrator"] - icon = '{}/ftrack/action_icons/PypeDoctor.svg'.format( - os.environ.get('PYPE_STATICS_SERVER', '') - ) - - db_con = DbConnector() - keys_to_change = { - "fstart": "frameStart", - "startFrame": "frameStart", - "edit_in": "frameStart", - - "fend": "frameEnd", - "endFrame": "frameEnd", - "edit_out": "frameEnd", - - "handle_start": "handleStart", - "handle_end": "handleEnd", - "handles": ["handleEnd", "handleStart"], - - "frameRate": "fps", - "framerate": "fps", - "resolution_width": "resolutionWidth", - "resolution_height": "resolutionHeight", - "pixel_aspect": "pixelAspect" - } - - def discover(self, session, entities, event): - ''' Validation ''' - - return True - - def interface(self, session, entities, event): - if event['data'].get('values', {}): - return - - title = 'Select Projects where attributes should be remapped' - - items = [] - - selection_enum = { - 'label': 'Process type', - 'type': 'enumerator', - 'name': 'process_type', - 'data': [ - { - 'label': 'Selection', - 'value': 'selection' - }, { - 'label': 'Inverted selection', - 'value': 'except' - } - ], - 'value': 'selection' - } - selection_label = { - 'type': 'label', - 'value': ( - 'Selection based variants:
' - '- `Selection` - ' - 'NOTHING is processed when nothing is selected
' - '- `Inverted selection` - ' - 'ALL Projects are processed when nothing is selected' - ) - } - - items.append(selection_enum) - items.append(selection_label) - - item_splitter = {'type': 'label', 'value': '---'} - - all_projects = session.query('Project').all() - for project in all_projects: - item_label = { - 'type': 'label', - 'value': '{} ({})'.format( - project['full_name'], project['name'] - ) - } - item = { - 'name': project['id'], - 'type': 'boolean', - 'value': False - } - if len(items) > 0: - items.append(item_splitter) - items.append(item_label) - items.append(item) - - if len(items) == 0: - return { - 'success': False, - 'message': 'Didn\'t found any projects' - } - else: - return { - 'items': items, - 'title': title - } - - def launch(self, session, entities, event): - if 'values' not in event['data']: - return - - values = event['data']['values'] - process_type = values.pop('process_type') - - selection = True - if process_type == 'except': - selection = False - - interface_messages = {} - - projects_to_update = [] - for project_id, update_bool in values.items(): - if not update_bool and selection: - continue - - if update_bool and not selection: - continue - - project = session.query( - 'Project where id is "{}"'.format(project_id) - ).one() - projects_to_update.append(project) - - if not projects_to_update: - self.log.debug('Nothing to update') - return { - 'success': True, - 'message': 'Nothing to update' - } - - - self.db_con.install() - - relevant_types = ["project", "asset", "version"] - - for ft_project in projects_to_update: - self.log.debug( - "Processing project \"{}\"".format(ft_project["full_name"]) - ) - - self.db_con.Session["AVALON_PROJECT"] = ft_project["full_name"] - project = self.db_con.find_one({'type': 'project'}) - if not project: - key = "Projects not synchronized to db" - if key not in interface_messages: - interface_messages[key] = [] - interface_messages[key].append(ft_project["full_name"]) - continue - - # Get all entities in project collection from MongoDB - _entities = self.db_con.find({}) - for _entity in _entities: - ent_t = _entity.get("type", "*unknown type") - name = _entity.get("name", "*unknown name") - - self.log.debug( - "- {} ({})".format(name, ent_t) - ) - - # Skip types that do not store keys to change - if ent_t.lower() not in relevant_types: - self.log.debug("-- skipping - type is not relevant") - continue - - # Get data which will change - updating_data = {} - source_data = _entity["data"] - - for key_from, key_to in self.keys_to_change.items(): - # continue if final key already exists - if type(key_to) == list: - for key in key_to: - # continue if final key was set in update_data - if key in updating_data: - continue - - # continue if source key not exist or value is None - value = source_data.get(key_from) - if value is None: - continue - - self.log.debug( - "-- changing key {} to {}".format( - key_from, - key - ) - ) - - updating_data[key] = value - else: - if key_to in source_data: - continue - - # continue if final key was set in update_data - if key_to in updating_data: - continue - - # continue if source key not exist or value is None - value = source_data.get(key_from) - if value is None: - continue - - self.log.debug( - "-- changing key {} to {}".format(key_from, key_to) - ) - updating_data[key_to] = value - - # Pop out old keys from entity - is_obsolete = False - for key in self.keys_to_change: - if key not in source_data: - continue - is_obsolete = True - source_data.pop(key) - - # continue if there is nothing to change - if not is_obsolete and not updating_data: - self.log.debug("-- nothing to change") - continue - - source_data.update(updating_data) - - self.db_con.update_many( - {"_id": _entity["_id"]}, - {"$set": {"data": source_data}} - ) - - self.db_con.uninstall() - - if interface_messages: - self.show_interface_from_dict( - messages=interface_messages, - title="Errors during remapping attributes", - event=event - ) - - return True - - def show_interface_from_dict(self, event, messages, title=""): - items = [] - - for key, value in messages.items(): - if not value: - continue - subtitle = {'type': 'label', 'value': '# {}'.format(key)} - items.append(subtitle) - if isinstance(value, list): - for item in value: - message = { - 'type': 'label', 'value': '

{}

'.format(item) - } - items.append(message) - else: - message = {'type': 'label', 'value': '

{}

'.format(value)} - items.append(message) - - self.show_interface(items=items, title=title, event=event) - -def register(session, plugins_presets={}): - '''Register plugin. Called when used as an plugin.''' - - AttributesRemapper(session, plugins_presets).register() diff --git a/pype/modules/ftrack/actions/action_clean_hierarchical_attributes.py b/pype/modules/ftrack/actions/action_clean_hierarchical_attributes.py index 0319528319..86503ff5bc 100644 --- a/pype/modules/ftrack/actions/action_clean_hierarchical_attributes.py +++ b/pype/modules/ftrack/actions/action_clean_hierarchical_attributes.py @@ -1,7 +1,6 @@ -import os import collections import ftrack_api -from pype.modules.ftrack import BaseAction +from pype.modules.ftrack.lib import BaseAction, statics_icon from pype.modules.ftrack.lib.avalon_sync import get_avalon_attr @@ -11,9 +10,7 @@ class CleanHierarchicalAttrsAction(BaseAction): variant = "- Clean hierarchical custom attributes" description = "Unset empty hierarchical attribute values." role_list = ["Pypeclub", "Administrator", "Project Manager"] - icon = "{}/ftrack/action_icons/PypeAdmin.svg".format( - os.environ.get("PYPE_STATICS_SERVER", "") - ) + icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg") all_project_entities_query = ( "select id, name, parent_id, link" diff --git a/pype/modules/ftrack/actions/action_client_review_sort.py b/pype/modules/ftrack/actions/action_client_review_sort.py index 1909c31759..72387fe695 100644 --- a/pype/modules/ftrack/actions/action_client_review_sort.py +++ b/pype/modules/ftrack/actions/action_client_review_sort.py @@ -1,4 +1,4 @@ -from pype.modules.ftrack import BaseAction +from pype.modules.ftrack.lib import BaseAction try: from functools import cmp_to_key except Exception: diff --git a/pype/modules/ftrack/actions/action_component_open.py b/pype/modules/ftrack/actions/action_component_open.py index aebd543769..5fe8fe831b 100644 --- a/pype/modules/ftrack/actions/action_component_open.py +++ b/pype/modules/ftrack/actions/action_component_open.py @@ -1,10 +1,7 @@ import os import sys -import argparse -import logging import subprocess -import ftrack_api -from pype.modules.ftrack import BaseAction +from pype.modules.ftrack.lib import BaseAction, statics_icon class ComponentOpen(BaseAction): @@ -15,9 +12,7 @@ class ComponentOpen(BaseAction): # Action label label = 'Open File' # Action icon - icon = '{}/ftrack/action_icons/ComponentOpen.svg'.format( - os.environ.get('PYPE_STATICS_SERVER', '') - ) + icon = statics_icon("ftrack", "action_icons", "ComponentOpen.svg") def discover(self, session, entities, event): ''' Validation ''' @@ -69,42 +64,3 @@ def register(session, plugins_presets={}): '''Register action. Called when used as an event plugin.''' ComponentOpen(session, plugins_presets).register() - - -def main(arguments=None): - '''Set up logging and register action.''' - if arguments is None: - arguments = [] - - parser = argparse.ArgumentParser() - # Allow setting of logging level from arguments. - loggingLevels = {} - for level in ( - logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING, - logging.ERROR, logging.CRITICAL - ): - loggingLevels[logging.getLevelName(level).lower()] = level - - parser.add_argument( - '-v', '--verbosity', - help='Set the logging output verbosity.', - choices=loggingLevels.keys(), - default='info' - ) - namespace = parser.parse_args(arguments) - - # Set up basic logging - logging.basicConfig(level=loggingLevels[namespace.verbosity]) - - session = ftrack_api.Session() - register(session) - - # Wait for events - logging.info( - 'Registered actions and listening for events. Use Ctrl-C to abort.' - ) - session.event_hub.wait() - - -if __name__ == '__main__': - raise SystemExit(main(sys.argv[1:])) diff --git a/pype/modules/ftrack/actions/action_create_cust_attrs.py b/pype/modules/ftrack/actions/action_create_cust_attrs.py index 6e9827a231..9845cc8876 100644 --- a/pype/modules/ftrack/actions/action_create_cust_attrs.py +++ b/pype/modules/ftrack/actions/action_create_cust_attrs.py @@ -1,9 +1,8 @@ -import os import collections import json import arrow import ftrack_api -from pype.modules.ftrack import BaseAction +from pype.modules.ftrack.lib import BaseAction, statics_icon from pype.modules.ftrack.lib.avalon_sync import CustAttrIdKey from pype.api import config @@ -114,9 +113,7 @@ class CustomAttributes(BaseAction): description = 'Creates Avalon/Mongo ID for double check' #: roles that are allowed to register this action role_list = ['Pypeclub', 'Administrator'] - icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format( - os.environ.get('PYPE_STATICS_SERVER', '') - ) + icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg") required_keys = ['key', 'label', 'type'] type_posibilities = [ diff --git a/pype/modules/ftrack/actions/action_create_folders.py b/pype/modules/ftrack/actions/action_create_folders.py index 9146c54fad..e689e0260c 100644 --- a/pype/modules/ftrack/actions/action_create_folders.py +++ b/pype/modules/ftrack/actions/action_create_folders.py @@ -1,5 +1,5 @@ import os -from pype.modules.ftrack import BaseAction +from pype.modules.ftrack.lib import BaseAction, statics_icon from avalon import lib as avalonlib from pype.api import config, Anatomy @@ -7,9 +7,7 @@ from pype.api import config, Anatomy class CreateFolders(BaseAction): identifier = "create.folders" label = "Create Folders" - icon = "{}/ftrack/action_icons/CreateFolders.svg".format( - os.environ.get("PYPE_STATICS_SERVER", "") - ) + icon = statics_icon("ftrack", "action_icons", "CreateFolders.svg") def discover(self, session, entities, event): if len(entities) != 1: diff --git a/pype/modules/ftrack/actions/action_create_project_structure.py b/pype/modules/ftrack/actions/action_create_project_structure.py index 526cf172bf..22190c16db 100644 --- a/pype/modules/ftrack/actions/action_create_project_structure.py +++ b/pype/modules/ftrack/actions/action_create_project_structure.py @@ -1,7 +1,7 @@ import os import re -from pype.modules.ftrack import BaseAction +from pype.modules.ftrack.lib import BaseAction, statics_icon from pype.api import config, Anatomy @@ -52,9 +52,7 @@ class CreateProjectFolders(BaseAction): label = "Create Project Structure" description = "Creates folder structure" role_list = ["Pypeclub", "Administrator", "Project Manager"] - icon = "{}/ftrack/action_icons/CreateProjectFolders.svg".format( - os.environ.get("PYPE_STATICS_SERVER", "") - ) + icon = statics_icon("ftrack", "action_icons", "CreateProjectFolders.svg") pattern_array = re.compile(r"\[.*\]") pattern_ftrack = re.compile(r".*\[[.]*ftrack[.]*") diff --git a/pype/modules/ftrack/actions/action_cust_attr_doctor.py b/pype/modules/ftrack/actions/action_cust_attr_doctor.py deleted file mode 100644 index e67ce4e5bf..0000000000 --- a/pype/modules/ftrack/actions/action_cust_attr_doctor.py +++ /dev/null @@ -1,336 +0,0 @@ -import os -import sys -import json -import argparse -import logging - -import ftrack_api -from pype.modules.ftrack import BaseAction - - -class CustomAttributeDoctor(BaseAction): - - ignore_me = True - #: Action identifier. - identifier = 'custom.attributes.doctor' - #: Action label. - label = "Pype Doctor" - variant = '- Custom Attributes Doctor' - #: Action description. - description = ( - 'Fix hierarchical custom attributes mainly handles, fstart' - ' and fend' - ) - - icon = '{}/ftrack/action_icons/PypeDoctor.svg'.format( - os.environ.get('PYPE_STATICS_SERVER', '') - ) - hierarchical_ca = ['handleStart', 'handleEnd', 'frameStart', 'frameEnd'] - hierarchical_alternatives = { - 'handleStart': 'handles', - 'handleEnd': 'handles', - "frameStart": "fstart", - "frameEnd": "fend" - } - - # Roles for new custom attributes - read_roles = ['ALL',] - write_roles = ['ALL',] - - data_ca = { - 'handleStart': { - 'label': 'Frame handles start', - 'type': 'number', - 'config': json.dumps({'isdecimal': False}) - }, - 'handleEnd': { - 'label': 'Frame handles end', - 'type': 'number', - 'config': json.dumps({'isdecimal': False}) - }, - 'frameStart': { - 'label': 'Frame start', - 'type': 'number', - 'config': json.dumps({'isdecimal': False}) - }, - 'frameEnd': { - 'label': 'Frame end', - 'type': 'number', - 'config': json.dumps({'isdecimal': False}) - } - } - - def discover(self, session, entities, event): - ''' Validation ''' - - return True - - def interface(self, session, entities, event): - if event['data'].get('values', {}): - return - - title = 'Select Project to fix Custom attributes' - - items = [] - item_splitter = {'type': 'label', 'value': '---'} - - all_projects = session.query('Project').all() - for project in all_projects: - item_label = { - 'type': 'label', - 'value': '{} ({})'.format( - project['full_name'], project['name'] - ) - } - item = { - 'name': project['id'], - 'type': 'boolean', - 'value': False - } - if len(items) > 0: - items.append(item_splitter) - items.append(item_label) - items.append(item) - - if len(items) == 0: - return { - 'success': False, - 'message': 'Didn\'t found any projects' - } - else: - return { - 'items': items, - 'title': title - } - - def launch(self, session, entities, event): - if 'values' not in event['data']: - return - - values = event['data']['values'] - projects_to_update = [] - for project_id, update_bool in values.items(): - if not update_bool: - continue - - project = session.query( - 'Project where id is "{}"'.format(project_id) - ).one() - projects_to_update.append(project) - - if not projects_to_update: - self.log.debug('Nothing to update') - return { - 'success': True, - 'message': 'Nothing to update' - } - - self.security_roles = {} - self.to_process = {} - # self.curent_default_values = {} - existing_attrs = session.query('CustomAttributeConfiguration').all() - self.prepare_custom_attributes(existing_attrs) - - self.projects_data = {} - for project in projects_to_update: - self.process_data(project) - - return True - - def process_data(self, entity): - cust_attrs = entity.get('custom_attributes') - if not cust_attrs: - return - for dst_key, src_key in self.to_process.items(): - if src_key in cust_attrs: - value = cust_attrs[src_key] - entity['custom_attributes'][dst_key] = value - self.session.commit() - - for child in entity.get('children', []): - self.process_data(child) - - def prepare_custom_attributes(self, existing_attrs): - to_process = {} - to_create = [] - all_keys = {attr['key']: attr for attr in existing_attrs} - for key in self.hierarchical_ca: - if key not in all_keys: - self.log.debug( - 'Custom attribute "{}" does not exist at all'.format(key) - ) - to_create.append(key) - if key in self.hierarchical_alternatives: - alt_key = self.hierarchical_alternatives[key] - if alt_key in all_keys: - self.log.debug(( - 'Custom attribute "{}" will use values from "{}"' - ).format(key, alt_key)) - - to_process[key] = alt_key - - obj = all_keys[alt_key] - # if alt_key not in self.curent_default_values: - # self.curent_default_values[alt_key] = obj['default'] - obj['default'] = None - self.session.commit() - - else: - obj = all_keys[key] - new_key = key + '_old' - - if obj['is_hierarchical']: - if new_key not in all_keys: - self.log.info(( - 'Custom attribute "{}" is already hierarchical' - ' and can\'t find old one' - ).format(key) - ) - continue - - to_process[key] = new_key - continue - - # default_value = obj['default'] - # if new_key not in self.curent_default_values: - # self.curent_default_values[new_key] = default_value - - obj['key'] = new_key - obj['label'] = obj['label'] + '(old)' - obj['default'] = None - - self.session.commit() - - to_create.append(key) - to_process[key] = new_key - - self.to_process = to_process - for key in to_create: - data = { - 'key': key, - 'entity_type': 'show', - 'is_hierarchical': True, - 'default': None - } - for _key, _value in self.data_ca.get(key, {}).items(): - if _key == 'type': - _value = self.session.query(( - 'CustomAttributeType where name is "{}"' - ).format(_value)).first() - - data[_key] = _value - - avalon_group = self.session.query( - 'CustomAttributeGroup where name is "avalon"' - ).first() - if avalon_group: - data['group'] = avalon_group - - read_roles = self.get_security_role(self.read_roles) - write_roles = self.get_security_role(self.write_roles) - data['read_security_roles'] = read_roles - data['write_security_roles'] = write_roles - - self.session.create('CustomAttributeConfiguration', data) - self.session.commit() - - # def return_back_defaults(self): - # existing_attrs = self.session.query( - # 'CustomAttributeConfiguration' - # ).all() - # - # for attr_key, default in self.curent_default_values.items(): - # for attr in existing_attrs: - # if attr['key'] != attr_key: - # continue - # attr['default'] = default - # self.session.commit() - # break - - def get_security_role(self, security_roles): - roles = [] - if len(security_roles) == 0 or security_roles[0] == 'ALL': - roles = self.get_role_ALL() - elif security_roles[0] == 'except': - excepts = security_roles[1:] - all = self.get_role_ALL() - for role in all: - if role['name'] not in excepts: - roles.append(role) - if role['name'] not in self.security_roles: - self.security_roles[role['name']] = role - else: - for role_name in security_roles: - if role_name in self.security_roles: - roles.append(self.security_roles[role_name]) - continue - - try: - query = 'SecurityRole where name is "{}"'.format(role_name) - role = self.session.query(query).one() - self.security_roles[role_name] = role - roles.append(role) - except Exception: - self.log.warning( - 'Securit role "{}" does not exist'.format(role_name) - ) - continue - - return roles - - def get_role_ALL(self): - role_name = 'ALL' - if role_name in self.security_roles: - all_roles = self.security_roles[role_name] - else: - all_roles = self.session.query('SecurityRole').all() - self.security_roles[role_name] = all_roles - for role in all_roles: - if role['name'] not in self.security_roles: - self.security_roles[role['name']] = role - return all_roles - - -def register(session, plugins_presets={}): - '''Register plugin. Called when used as an plugin.''' - - CustomAttributeDoctor(session, plugins_presets).register() - - -def main(arguments=None): - '''Set up logging and register action.''' - if arguments is None: - arguments = [] - - parser = argparse.ArgumentParser() - # Allow setting of logging level from arguments. - loggingLevels = {} - for level in ( - logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING, - logging.ERROR, logging.CRITICAL - ): - loggingLevels[logging.getLevelName(level).lower()] = level - - parser.add_argument( - '-v', '--verbosity', - help='Set the logging output verbosity.', - choices=loggingLevels.keys(), - default='info' - ) - namespace = parser.parse_args(arguments) - - # Set up basic logging - logging.basicConfig(level=loggingLevels[namespace.verbosity]) - - session = ftrack_api.Session() - register(session) - - # Wait for events - logging.info( - 'Registered actions and listening for events. Use Ctrl-C to abort.' - ) - session.event_hub.wait() - - -if __name__ == '__main__': - raise SystemExit(main(sys.argv[1:])) diff --git a/pype/modules/ftrack/actions/action_delete_asset.py b/pype/modules/ftrack/actions/action_delete_asset.py index d4f6eb6594..1074efee3b 100644 --- a/pype/modules/ftrack/actions/action_delete_asset.py +++ b/pype/modules/ftrack/actions/action_delete_asset.py @@ -1,11 +1,10 @@ -import os import collections import uuid from datetime import datetime from queue import Queue from bson.objectid import ObjectId -from pype.modules.ftrack import BaseAction +from pype.modules.ftrack.lib import BaseAction, statics_icon from pype.modules.ftrack.lib.io_nonsingleton import DbConnector @@ -18,9 +17,7 @@ class DeleteAssetSubset(BaseAction): label = "Delete Asset/Subsets" #: Action description. description = "Removes from Avalon with all childs and asset from Ftrack" - icon = "{}/ftrack/action_icons/DeleteAsset.svg".format( - os.environ.get("PYPE_STATICS_SERVER", "") - ) + icon = statics_icon("ftrack", "action_icons", "DeleteAsset.svg") #: roles that are allowed to register this action role_list = ["Pypeclub", "Administrator", "Project Manager"] #: Db connection diff --git a/pype/modules/ftrack/actions/action_delete_old_versions.py b/pype/modules/ftrack/actions/action_delete_old_versions.py index 00432d5c3f..46652b136a 100644 --- a/pype/modules/ftrack/actions/action_delete_old_versions.py +++ b/pype/modules/ftrack/actions/action_delete_old_versions.py @@ -5,7 +5,7 @@ import uuid import clique from pymongo import UpdateOne -from pype.modules.ftrack import BaseAction +from pype.modules.ftrack.lib import BaseAction, statics_icon from pype.modules.ftrack.lib.io_nonsingleton import DbConnector from pype.api import Anatomy @@ -22,9 +22,7 @@ class DeleteOldVersions(BaseAction): " archived with only lates versions." ) role_list = ["Pypeclub", "Project Manager", "Administrator"] - icon = "{}/ftrack/action_icons/PypeAdmin.svg".format( - os.environ.get("PYPE_STATICS_SERVER", "") - ) + icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg") dbcon = DbConnector() diff --git a/pype/modules/ftrack/actions/action_delivery.py b/pype/modules/ftrack/actions/action_delivery.py index 1dfd3dd5d5..a2048222e5 100644 --- a/pype/modules/ftrack/actions/action_delivery.py +++ b/pype/modules/ftrack/actions/action_delivery.py @@ -8,11 +8,11 @@ from bson.objectid import ObjectId from avalon import pipeline from avalon.vendor import filelink -from avalon.tools.libraryloader.io_nonsingleton import DbConnector from pype.api import Anatomy -from pype.modules.ftrack import BaseAction +from pype.modules.ftrack.lib import BaseAction, statics_icon from pype.modules.ftrack.lib.avalon_sync import CustAttrIdKey +from pype.modules.ftrack.lib.io_nonsingleton import DbConnector class Delivery(BaseAction): @@ -21,9 +21,7 @@ class Delivery(BaseAction): label = "Delivery" description = "Deliver data to client" role_list = ["Pypeclub", "Administrator", "Project manager"] - icon = "{}/ftrack/action_icons/Delivery.svg".format( - os.environ.get("PYPE_STATICS_SERVER", "") - ) + icon = statics_icon("ftrack", "action_icons", "Delivery.svg") db_con = DbConnector() @@ -508,6 +506,7 @@ class Delivery(BaseAction): "message": "Delivery Finished" } + def register(session, plugins_presets={}): '''Register plugin. Called when used as an plugin.''' diff --git a/pype/modules/ftrack/actions/action_djvview.py b/pype/modules/ftrack/actions/action_djvview.py index 32c5824e0f..9708503ad1 100644 --- a/pype/modules/ftrack/actions/action_djvview.py +++ b/pype/modules/ftrack/actions/action_djvview.py @@ -1,11 +1,10 @@ import os import sys -import json import logging import subprocess from operator import itemgetter import ftrack_api -from pype.modules.ftrack import BaseAction +from pype.modules.ftrack.lib import BaseAction, statics_icon from pype.api import Logger, config log = Logger().get_logger(__name__) @@ -16,9 +15,8 @@ class DJVViewAction(BaseAction): identifier = "djvview-launch-action" label = "DJV View" description = "DJV View Launcher" - icon = '{}/app_icons/djvView.png'.format( - os.environ.get('PYPE_STATICS_SERVER', '') - ) + icon = statics_icon("app_icons", "djvView.png") + type = 'Application' def __init__(self, session, plugins_presets): diff --git a/pype/modules/ftrack/actions/action_job_killer.py b/pype/modules/ftrack/actions/action_job_killer.py index 064d7ed209..ff23da2a54 100644 --- a/pype/modules/ftrack/actions/action_job_killer.py +++ b/pype/modules/ftrack/actions/action_job_killer.py @@ -1,11 +1,5 @@ -import os -import sys -import argparse -import logging import json - -import ftrack_api -from pype.modules.ftrack import BaseAction +from pype.modules.ftrack.lib import BaseAction, statics_icon class JobKiller(BaseAction): @@ -20,9 +14,7 @@ class JobKiller(BaseAction): description = 'Killing selected running jobs' #: roles that are allowed to register this action role_list = ['Pypeclub', 'Administrator'] - icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format( - os.environ.get('PYPE_STATICS_SERVER', '') - ) + icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg") def discover(self, session, entities, event): ''' Validation ''' @@ -124,43 +116,3 @@ def register(session, plugins_presets={}): '''Register plugin. Called when used as an plugin.''' JobKiller(session, plugins_presets).register() - - -def main(arguments=None): - '''Set up logging and register action.''' - if arguments is None: - arguments = [] - - parser = argparse.ArgumentParser() - # Allow setting of logging level from arguments. - loggingLevels = {} - for level in ( - logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING, - logging.ERROR, logging.CRITICAL - ): - loggingLevels[logging.getLevelName(level).lower()] = level - - parser.add_argument( - '-v', '--verbosity', - help='Set the logging output verbosity.', - choices=loggingLevels.keys(), - default='info' - ) - namespace = parser.parse_args(arguments) - - # Set up basic logging - logging.basicConfig(level=loggingLevels[namespace.verbosity]) - - session = ftrack_api.Session() - - register(session) - - # Wait for events - logging.info( - 'Registered actions and listening for events. Use Ctrl-C to abort.' - ) - session.event_hub.wait() - - -if __name__ == '__main__': - raise SystemExit(main(sys.argv[1:])) diff --git a/pype/modules/ftrack/actions/action_multiple_notes.py b/pype/modules/ftrack/actions/action_multiple_notes.py index dcdb4c5dc9..c1a5cc6ce0 100644 --- a/pype/modules/ftrack/actions/action_multiple_notes.py +++ b/pype/modules/ftrack/actions/action_multiple_notes.py @@ -1,10 +1,4 @@ -import os -import sys -import argparse -import logging -import ftrack_api - -from pype.modules.ftrack import BaseAction +from pype.modules.ftrack.lib import BaseAction, statics_icon class MultipleNotes(BaseAction): @@ -16,9 +10,7 @@ class MultipleNotes(BaseAction): label = 'Multiple Notes' #: Action description. description = 'Add same note to multiple Asset Versions' - icon = '{}/ftrack/action_icons/MultipleNotes.svg'.format( - os.environ.get('PYPE_STATICS_SERVER', '') - ) + icon = statics_icon("ftrack", "action_icons", "MultipleNotes.svg") def discover(self, session, entities, event): ''' Validation ''' @@ -116,42 +108,3 @@ def register(session, plugins_presets={}): '''Register plugin. Called when used as an plugin.''' MultipleNotes(session, plugins_presets).register() - - -def main(arguments=None): - '''Set up logging and register action.''' - if arguments is None: - arguments = [] - - parser = argparse.ArgumentParser() - # Allow setting of logging level from arguments. - loggingLevels = {} - for level in ( - logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING, - logging.ERROR, logging.CRITICAL - ): - loggingLevels[logging.getLevelName(level).lower()] = level - - parser.add_argument( - '-v', '--verbosity', - help='Set the logging output verbosity.', - choices=loggingLevels.keys(), - default='info' - ) - namespace = parser.parse_args(arguments) - - # Set up basic logging - logging.basicConfig(level=loggingLevels[namespace.verbosity]) - - session = ftrack_api.Session() - register(session) - - # Wait for events - logging.info( - 'Registered actions and listening for events. Use Ctrl-C to abort.' - ) - session.event_hub.wait() - - -if __name__ == '__main__': - raise SystemExit(main(sys.argv[1:])) diff --git a/pype/modules/ftrack/actions/action_prepare_project.py b/pype/modules/ftrack/actions/action_prepare_project.py index 31cc802109..f51a9eb9a6 100644 --- a/pype/modules/ftrack/actions/action_prepare_project.py +++ b/pype/modules/ftrack/actions/action_prepare_project.py @@ -1,7 +1,7 @@ import os import json -from pype.modules.ftrack import BaseAction +from pype.modules.ftrack.lib import BaseAction, statics_icon from pype.api import config, Anatomy, project_overrides_dir_path from pype.modules.ftrack.lib.avalon_sync import get_avalon_attr @@ -17,9 +17,7 @@ class PrepareProject(BaseAction): description = 'Set basic attributes on the project' #: roles that are allowed to register this action role_list = ["Pypeclub", "Administrator", "Project manager"] - icon = '{}/ftrack/action_icons/PrepareProject.svg'.format( - os.environ.get('PYPE_STATICS_SERVER', '') - ) + icon = statics_icon("ftrack", "action_icons", "PrepareProject.svg") # Key to store info about trigerring create folder structure create_project_structure_key = "create_folder_structure" diff --git a/pype/modules/ftrack/actions/action_rv.py b/pype/modules/ftrack/actions/action_rv.py index ce2371b5b8..528eeeee07 100644 --- a/pype/modules/ftrack/actions/action_rv.py +++ b/pype/modules/ftrack/actions/action_rv.py @@ -1,17 +1,13 @@ import os -import sys import subprocess -import logging import traceback import json -from pype.api import Logger, config -from pype.modules.ftrack import BaseAction +from pype.api import config +from pype.modules.ftrack.lib import BaseAction, statics_icon import ftrack_api from avalon import io, api -log = Logger().get_logger(__name__) - class RVAction(BaseAction): """ Launch RV action """ @@ -19,9 +15,8 @@ class RVAction(BaseAction): identifier = "rv.launch.action" label = "rv" description = "rv Launcher" - icon = '{}/ftrack/action_icons/RV.png'.format( - os.environ.get('PYPE_STATICS_SERVER', '') - ) + icon = statics_icon("ftrack", "action_icons", "RV.png") + type = 'Application' def __init__(self, session, plugins_presets): @@ -144,7 +139,7 @@ class RVAction(BaseAction): try: items = self.get_interface_items(session, entities) except Exception: - log.error(traceback.format_exc()) + self.log.error(traceback.format_exc()) job["status"] = "failed" else: job["status"] = "done" @@ -238,7 +233,7 @@ class RVAction(BaseAction): try: paths = self.get_file_paths(session, event) except Exception: - log.error(traceback.format_exc()) + self.log.error(traceback.format_exc()) job["status"] = "failed" else: job["status"] = "done" @@ -254,7 +249,7 @@ class RVAction(BaseAction): args.extend(paths) - log.info("Running rv: {}".format(args)) + self.log.info("Running rv: {}".format(args)) subprocess.Popen(args) @@ -332,43 +327,3 @@ def register(session, plugins_presets={}): """Register hooks.""" RVAction(session, plugins_presets).register() - - -def main(arguments=None): - '''Set up logging and register action.''' - if arguments is None: - arguments = [] - - import argparse - parser = argparse.ArgumentParser() - # Allow setting of logging level from arguments. - loggingLevels = {} - for level in ( - logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING, - logging.ERROR, logging.CRITICAL - ): - loggingLevels[logging.getLevelName(level).lower()] = level - - parser.add_argument( - '-v', '--verbosity', - help='Set the logging output verbosity.', - choices=loggingLevels.keys(), - default='info' - ) - namespace = parser.parse_args(arguments) - - # Set up basic logging - logging.basicConfig(level=loggingLevels[namespace.verbosity]) - - session = ftrack_api.Session() - register(session) - - # Wait for events - logging.info( - 'Registered actions and listening for events. Use Ctrl-C to abort.' - ) - session.event_hub.wait() - - -if __name__ == '__main__': - raise SystemExit(main(sys.argv[1:])) diff --git a/pype/modules/ftrack/actions/action_seed.py b/pype/modules/ftrack/actions/action_seed.py index 72625c13e4..d6288a03aa 100644 --- a/pype/modules/ftrack/actions/action_seed.py +++ b/pype/modules/ftrack/actions/action_seed.py @@ -1,6 +1,6 @@ import os from operator import itemgetter -from pype.modules.ftrack import BaseAction +from pype.modules.ftrack.lib import BaseAction, statics_icon class SeedDebugProject(BaseAction): @@ -16,9 +16,7 @@ class SeedDebugProject(BaseAction): priority = 100 #: roles that are allowed to register this action role_list = ["Pypeclub"] - icon = "{}/ftrack/action_icons/SeedProject.svg".format( - os.environ.get("PYPE_STATICS_SERVER", "") - ) + icon = statics_icon("ftrack", "action_icons", "SeedProject.svg") # Asset names which will be created in `Assets` entity assets = [ @@ -429,6 +427,7 @@ class SeedDebugProject(BaseAction): self.session.commit() return True + def register(session, plugins_presets={}): '''Register plugin. Called when used as an plugin.''' diff --git a/pype/modules/ftrack/actions/action_start_timer.py b/pype/modules/ftrack/actions/action_start_timer.py index 3e5cf0d4c1..6e8fcb29e2 100644 --- a/pype/modules/ftrack/actions/action_start_timer.py +++ b/pype/modules/ftrack/actions/action_start_timer.py @@ -1,5 +1,4 @@ -import ftrack_api -from pype.modules.ftrack import BaseAction +from pype.modules.ftrack.lib import BaseAction class StartTimer(BaseAction): diff --git a/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py b/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py index 180d6ae03c..b399dab7ce 100644 --- a/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py +++ b/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py @@ -4,7 +4,7 @@ import errno import json from bson.objectid import ObjectId -from pype.modules.ftrack import BaseAction +from pype.modules.ftrack.lib import BaseAction, statics_icon from pype.api import Anatomy from pype.modules.ftrack.lib.io_nonsingleton import DbConnector @@ -22,10 +22,7 @@ class StoreThumbnailsToAvalon(BaseAction): description = 'Test action' # roles that are allowed to register this action role_list = ["Pypeclub", "Administrator", "Project Manager"] - - icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format( - os.environ.get('PYPE_STATICS_SERVER', '') - ) + icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg") thumbnail_key = "AVALON_THUMBNAIL_ROOT" db_con = DbConnector() diff --git a/pype/modules/ftrack/actions/action_sync_to_avalon.py b/pype/modules/ftrack/actions/action_sync_to_avalon.py index 8c6519e4dc..dfe1f2c464 100644 --- a/pype/modules/ftrack/actions/action_sync_to_avalon.py +++ b/pype/modules/ftrack/actions/action_sync_to_avalon.py @@ -1,8 +1,7 @@ -import os import time import traceback -from pype.modules.ftrack import BaseAction +from pype.modules.ftrack.lib import BaseAction, statics_icon from pype.modules.ftrack.lib.avalon_sync import SyncEntitiesFactory @@ -43,9 +42,7 @@ class SyncToAvalonLocal(BaseAction): priority = 200 #: roles that are allowed to register this action role_list = ["Pypeclub"] - icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format( - os.environ.get('PYPE_STATICS_SERVER', '') - ) + icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg") def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/pype/modules/ftrack/actions/action_test.py b/pype/modules/ftrack/actions/action_test.py index 784bf80944..e4936274b3 100644 --- a/pype/modules/ftrack/actions/action_test.py +++ b/pype/modules/ftrack/actions/action_test.py @@ -1,37 +1,19 @@ -import os -import sys -import argparse -import logging -import collections -import json -import re - -import ftrack_api -from avalon import io, inventory, schema -from pype.modules.ftrack import BaseAction +from pype.modules.ftrack.lib import BaseAction, statics_icon class TestAction(BaseAction): - '''Edit meta data action.''' + """Action for testing purpose or as base for new actions.""" ignore_me = True - #: Action identifier. + identifier = 'test.action' - #: Action label. label = 'Test action' - #: Action description. description = 'Test action' - #: priority priority = 10000 - #: roles that are allowed to register this action role_list = ['Pypeclub'] - icon = '{}/ftrack/action_icons/TestAction.svg'.format( - os.environ.get('PYPE_STATICS_SERVER', '') - ) + icon = statics_icon("ftrack", "action_icons", "TestAction.svg") def discover(self, session, entities, event): - ''' Validation ''' - return True def launch(self, session, entities, event): @@ -41,45 +23,4 @@ class TestAction(BaseAction): def register(session, plugins_presets={}): - '''Register plugin. Called when used as an plugin.''' - TestAction(session, plugins_presets).register() - - -def main(arguments=None): - '''Set up logging and register action.''' - if arguments is None: - arguments = [] - - parser = argparse.ArgumentParser() - # Allow setting of logging level from arguments. - loggingLevels = {} - for level in ( - logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING, - logging.ERROR, logging.CRITICAL - ): - loggingLevels[logging.getLevelName(level).lower()] = level - - parser.add_argument( - '-v', '--verbosity', - help='Set the logging output verbosity.', - choices=loggingLevels.keys(), - default='info' - ) - namespace = parser.parse_args(arguments) - - # Set up basic logging - logging.basicConfig(level=loggingLevels[namespace.verbosity]) - - session = ftrack_api.Session() - register(session) - - # Wait for events - logging.info( - 'Registered actions and listening for events. Use Ctrl-C to abort.' - ) - session.event_hub.wait() - - -if __name__ == '__main__': - raise SystemExit(main(sys.argv[1:])) diff --git a/pype/modules/ftrack/actions/action_thumbnail_to_childern.py b/pype/modules/ftrack/actions/action_thumbnail_to_childern.py index e194134694..3c6af10b43 100644 --- a/pype/modules/ftrack/actions/action_thumbnail_to_childern.py +++ b/pype/modules/ftrack/actions/action_thumbnail_to_childern.py @@ -1,11 +1,5 @@ -import os -import sys -import argparse -import logging import json - -import ftrack_api -from pype.modules.ftrack import BaseAction +from pype.modules.ftrack.lib import BaseAction, statics_icon class ThumbToChildren(BaseAction): @@ -18,9 +12,7 @@ class ThumbToChildren(BaseAction): # Action variant variant = " to Children" # Action icon - icon = '{}/ftrack/action_icons/Thumbnail.svg'.format( - os.environ.get('PYPE_STATICS_SERVER', '') - ) + icon = statics_icon("ftrack", "action_icons", "Thumbnail.svg") def discover(self, session, entities, event): ''' Validation ''' @@ -71,42 +63,3 @@ def register(session, plugins_presets={}): '''Register action. Called when used as an event plugin.''' ThumbToChildren(session, plugins_presets).register() - - -def main(arguments=None): - '''Set up logging and register action.''' - if arguments is None: - arguments = [] - - parser = argparse.ArgumentParser() - # Allow setting of logging level from arguments. - loggingLevels = {} - for level in ( - logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING, - logging.ERROR, logging.CRITICAL - ): - loggingLevels[logging.getLevelName(level).lower()] = level - - parser.add_argument( - '-v', '--verbosity', - help='Set the logging output verbosity.', - choices=loggingLevels.keys(), - default='info' - ) - namespace = parser.parse_args(arguments) - - # Set up basic logging - logging.basicConfig(level=loggingLevels[namespace.verbosity]) - - session = ftrack_api.Session() - register(session) - - # Wait for events - logging.info( - 'Registered actions and listening for events. Use Ctrl-C to abort.' - ) - session.event_hub.wait() - - -if __name__ == '__main__': - raise SystemExit(main(sys.argv[1:])) diff --git a/pype/modules/ftrack/actions/action_thumbnail_to_parent.py b/pype/modules/ftrack/actions/action_thumbnail_to_parent.py index 86ada64e5a..8710fa9dcf 100644 --- a/pype/modules/ftrack/actions/action_thumbnail_to_parent.py +++ b/pype/modules/ftrack/actions/action_thumbnail_to_parent.py @@ -1,10 +1,5 @@ -import os -import sys -import argparse -import logging import json -import ftrack_api -from pype.modules.ftrack import BaseAction +from pype.modules.ftrack.lib import BaseAction, statics_icon class ThumbToParent(BaseAction): @@ -17,9 +12,7 @@ class ThumbToParent(BaseAction): # Action variant variant = " to Parent" # Action icon - icon = '{}/ftrack/action_icons/Thumbnail.svg'.format( - os.environ.get('PYPE_STATICS_SERVER', '') - ) + icon = statics_icon("ftrack", "action_icons", "Thumbnail.svg") def discover(self, session, entities, event): '''Return action config if triggered on asset versions.''' @@ -93,42 +86,3 @@ def register(session, plugins_presets={}): '''Register action. Called when used as an event plugin.''' ThumbToParent(session, plugins_presets).register() - - -def main(arguments=None): - '''Set up logging and register action.''' - if arguments is None: - arguments = [] - - parser = argparse.ArgumentParser() - # Allow setting of logging level from arguments. - loggingLevels = {} - for level in ( - logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING, - logging.ERROR, logging.CRITICAL - ): - loggingLevels[logging.getLevelName(level).lower()] = level - - parser.add_argument( - '-v', '--verbosity', - help='Set the logging output verbosity.', - choices=loggingLevels.keys(), - default='info' - ) - namespace = parser.parse_args(arguments) - - # Set up basic logging - logging.basicConfig(level=loggingLevels[namespace.verbosity]) - - session = ftrack_api.Session() - register(session) - - # Wait for events - logging.info( - 'Registered actions and listening for events. Use Ctrl-C to abort.' - ) - session.event_hub.wait() - - -if __name__ == '__main__': - raise SystemExit(main(sys.argv[1:])) diff --git a/pype/modules/ftrack/actions/action_update_from_v2-2-0.py b/pype/modules/ftrack/actions/action_update_from_v2-2-0.py deleted file mode 100644 index 805072ce5d..0000000000 --- a/pype/modules/ftrack/actions/action_update_from_v2-2-0.py +++ /dev/null @@ -1,189 +0,0 @@ -import os - -from pype.modules.ftrack import BaseAction -from pype.modules.ftrack.lib.io_nonsingleton import DbConnector - - -class PypeUpdateFromV2_2_0(BaseAction): - """This action is to remove silo field from database and changes asset - schema to newer version - - WARNING: it is NOT for situations when you want to switch from avalon-core - to Pype's avalon-core!!! - - """ - #: Action identifier. - identifier = "silos.doctor" - #: Action label. - label = "Pype Update" - variant = "- v2.2.0 to v2.3.0 or higher" - #: Action description. - description = "Use when Pype was updated from v2.2.0 to v2.3.0 or higher" - - #: roles that are allowed to register this action - role_list = ["Pypeclub", "Administrator"] - icon = "{}/ftrack/action_icons/PypeUpdate.svg".format( - os.environ.get("PYPE_STATICS_SERVER", "") - ) - # connector to MongoDB (Avalon mongo) - db_con = DbConnector() - - def discover(self, session, entities, event): - """ Validation """ - if len(entities) != 1: - return False - - if entities[0].entity_type.lower() != "project": - return False - - return True - - def interface(self, session, entities, event): - if event['data'].get('values', {}): - return - - items = [] - item_splitter = {'type': 'label', 'value': '---'} - title = "Updated Pype from v 2.2.0 to v2.3.0 or higher" - - items.append({ - "type": "label", - "value": ( - "NOTE: This doctor action should be used ONLY when Pype" - " was updated from v2.2.0 to v2.3.0 or higher.


" - ) - }) - - items.append({ - "type": "label", - "value": ( - "Select if want to process all synchronized projects" - " or selection." - ) - }) - - items.append({ - "type": "enumerator", - "name": "__process_all__", - "data": [{ - "label": "All synchronized projects", - "value": True - }, { - "label": "Selection", - "value": False - }], - "value": False - }) - - items.append({ - "type": "label", - "value": ( - "

Synchronized projects:

" - "(ignore if \"ALL projects\" selected)" - ) - }) - - self.log.debug("Getting all Ftrack projects") - # Get all Ftrack projects - all_ftrack_projects = [ - project["full_name"] for project in session.query("Project").all() - ] - - self.log.debug("Getting Avalon projects that are also in the Ftrack") - # Get Avalon projects that are in Ftrack - self.db_con.install() - possible_projects = [ - project["name"] for project in self.db_con.projects() - if project["name"] in all_ftrack_projects - ] - - for project in possible_projects: - item_label = { - "type": "label", - "value": project - } - item = { - "label": "- process", - "name": project, - "type": 'boolean', - "value": False - } - items.append(item_splitter) - items.append(item_label) - items.append(item) - - if len(possible_projects) == 0: - return { - "success": False, - "message": ( - "Nothing to process." - " There are not projects synchronized to avalon." - ) - } - else: - return { - "items": items, - "title": title - } - - def launch(self, session, entities, event): - if 'values' not in event['data']: - return - - projects_selection = { - True: [], - False: [] - } - process_all = None - - values = event['data']['values'] - for key, value in values.items(): - if key == "__process_all__": - process_all = value - continue - - projects_selection[value].append(key) - - # Skip if process_all value is not boolean - # - may happen when user delete string line in combobox - if not isinstance(process_all, bool): - self.log.warning( - "Nothing was processed. User didn't select if want to process" - " selection or all projects!" - ) - return { - "success": False, - "message": ( - "Nothing was processed. You must select if want to process" - " \"selection\" or \"all projects\"!" - ) - } - - projects_to_process = projects_selection[True] - if process_all: - projects_to_process.extend(projects_selection[False]) - - self.db_con.install() - for project in projects_to_process: - self.log.debug("Processing project \"{}\"".format(project)) - self.db_con.Session["AVALON_PROJECT"] = project - - self.log.debug("- Unsetting silos on assets") - self.db_con.update_many( - {"type": "asset"}, - {"$unset": {"silo": ""}} - ) - - self.log.debug("- setting schema of assets to v.3") - self.db_con.update_many( - {"type": "asset"}, - {"$set": {"schema": "avalon-core:asset-3.0"}} - ) - - return True - - -def register(session, plugins_presets={}): - """Register plugin. Called when used as an plugin.""" - - PypeUpdateFromV2_2_0(session, plugins_presets).register() diff --git a/pype/modules/ftrack/actions/action_where_run_ask.py b/pype/modules/ftrack/actions/action_where_run_ask.py index d7bc8b11f9..42640fb506 100644 --- a/pype/modules/ftrack/actions/action_where_run_ask.py +++ b/pype/modules/ftrack/actions/action_where_run_ask.py @@ -1,23 +1,15 @@ -import os -from pype.modules.ftrack import BaseAction +from pype.modules.ftrack.lib import BaseAction, statics_icon class ActionAskWhereIRun(BaseAction): """ Sometimes user forget where pipeline with his credentials is running. - this action triggers `ActionShowWhereIRun` """ - # Action is ignored by default ignore_me = True - #: Action identifier. identifier = 'ask.where.i.run' - #: Action label. label = 'Ask where I run' - #: Action description. description = 'Triggers PC info where user have running Pype' - #: Action icon - icon = '{}/ftrack/action_icons/ActionAskWhereIRun.svg'.format( - os.environ.get('PYPE_STATICS_SERVER', '') - ) + icon = statics_icon("ftrack", "action_icons", "ActionAskWhereIRun.svg") def discover(self, session, entities, event): """ Hide by default - Should be enabled only if you want to run. diff --git a/pype/modules/ftrack/actions/action_where_run_show.py b/pype/modules/ftrack/actions/action_where_run_show.py index c7abccb8c9..a084547a45 100644 --- a/pype/modules/ftrack/actions/action_where_run_show.py +++ b/pype/modules/ftrack/actions/action_where_run_show.py @@ -1,8 +1,7 @@ import platform import socket import getpass -import ftrack_api -from pype.modules.ftrack import BaseAction +from pype.modules.ftrack.lib import BaseAction class ActionShowWhereIRun(BaseAction): diff --git a/pype/modules/ftrack/lib/__init__.py b/pype/modules/ftrack/lib/__init__.py index e58804440a..df546ab725 100644 --- a/pype/modules/ftrack/lib/__init__.py +++ b/pype/modules/ftrack/lib/__init__.py @@ -2,7 +2,7 @@ from . import avalon_sync from . import credentials from .ftrack_base_handler import BaseHandler from .ftrack_event_handler import BaseEvent -from .ftrack_action_handler import BaseAction +from .ftrack_action_handler import BaseAction, statics_icon from .ftrack_app_handler import AppAction __all__ = [ @@ -11,5 +11,6 @@ __all__ = [ "BaseHandler", "BaseEvent", "BaseAction", + "statics_icon", "AppAction" ] diff --git a/pype/modules/ftrack/lib/custom_db_connector.py b/pype/modules/ftrack/lib/custom_db_connector.py index 8e8eab8740..b307117127 100644 --- a/pype/modules/ftrack/lib/custom_db_connector.py +++ b/pype/modules/ftrack/lib/custom_db_connector.py @@ -5,27 +5,20 @@ Copy of io module in avalon-core. - In this case not working as singleton with api.Session! """ -import os import time -import errno -import shutil import logging -import tempfile import functools -import contextlib import atexit -import requests - # Third-party dependencies import pymongo -from pymongo.client_session import ClientSession + class NotActiveTable(Exception): def __init__(self, *args, **kwargs): msg = "Active table is not set. (This is bug)" if not (args or kwargs): - args = (default_message,) + args = [msg] super().__init__(*args, **kwargs) @@ -120,7 +113,7 @@ class DbConnector: else: raise IOError( "ERROR: Couldn't connect to %s in " - "less than %.3f ms" % (self._mongo_url, timeout) + "less than %.3f ms" % (self._mongo_url, self.timeout) ) self.log.info("Connected to %s, delay %.3f s" % ( diff --git a/pype/modules/ftrack/lib/ftrack_action_handler.py b/pype/modules/ftrack/lib/ftrack_action_handler.py index 66d321316f..76c8e41411 100644 --- a/pype/modules/ftrack/lib/ftrack_action_handler.py +++ b/pype/modules/ftrack/lib/ftrack_action_handler.py @@ -1,6 +1,14 @@ +import os from .ftrack_base_handler import BaseHandler +def statics_icon(*icon_statics_file_parts): + statics_server = os.environ.get("PYPE_STATICS_SERVER") + if not statics_server: + return None + return "/".join((statics_server, *icon_statics_file_parts)) + + class BaseAction(BaseHandler): '''Custom Action base class @@ -177,7 +185,9 @@ class BaseAction(BaseHandler): else: for key in ('success', 'message'): if key not in result: - raise KeyError('Missing required key: {0}.'.format(key)) + raise KeyError( + "Missing required key: {0}.".format(key) + ) return result self.log.warning(( diff --git a/pype/modules/ftrack/tray/login_dialog.py b/pype/modules/ftrack/tray/login_dialog.py index 0bfa1cc481..3b8a366209 100644 --- a/pype/modules/ftrack/tray/login_dialog.py +++ b/pype/modules/ftrack/tray/login_dialog.py @@ -1,6 +1,6 @@ import os import requests -from pypeapp import style +from avalon import style from pype.modules.ftrack import credentials from . import login_tools from Qt import QtCore, QtGui, QtWidgets diff --git a/pype/modules/logging/gui/app.py b/pype/modules/logging/gui/app.py index 9767077f80..99b0b230a9 100644 --- a/pype/modules/logging/gui/app.py +++ b/pype/modules/logging/gui/app.py @@ -1,6 +1,6 @@ from Qt import QtWidgets, QtCore -from .widgets import LogsWidget, LogDetailWidget -from pypeapp import style +from .widgets import LogsWidget, OutputWidget +from avalon import style class LogsWindow(QtWidgets.QWidget): @@ -10,7 +10,7 @@ class LogsWindow(QtWidgets.QWidget): self.setStyleSheet(style.load_stylesheet()) self.resize(1200, 800) logs_widget = LogsWidget(parent=self) - log_detail = LogDetailWidget(parent=self) + log_detail = OutputWidget(parent=self) main_layout = QtWidgets.QHBoxLayout() @@ -33,7 +33,5 @@ class LogsWindow(QtWidgets.QWidget): def on_selection_changed(self): index = self.logs_widget.selected_log() - if not index or not index.isValid(): - return node = index.data(self.logs_widget.model.NodeRole) self.log_detail.set_detail(node) diff --git a/pype/modules/logging/gui/models.py b/pype/modules/logging/gui/models.py index 2ef79554fe..484fd6dc69 100644 --- a/pype/modules/logging/gui/models.py +++ b/pype/modules/logging/gui/models.py @@ -1,4 +1,5 @@ import os +import collections from Qt import QtCore from pype.api import Logger from pypeapp.lib.log import _bootstrap_mongo_log @@ -8,31 +9,32 @@ log = Logger().get_logger("LogModel", "LoggingModule") class LogModel(QtCore.QAbstractItemModel): COLUMNS = [ - "user", - "host", - "lineNumber", - "method", - "module", - "fileName", - "loggerName", - "message", - "level", - "timestamp", + "process_name", + "hostname", + "hostip", + "username", + "system_name", + "started" ] colums_mapping = { - "user": "User", - "host": "Host", - "lineNumber": "Line n.", - "method": "Method", - "module": "Module", - "fileName": "File name", - "loggerName": "Logger name", - "message": "Message", - "level": "Level", - "timestamp": "Timestamp", + "process_name": "Process Name", + "process_id": "Process Id", + "hostname": "Hostname", + "hostip": "Host IP", + "username": "Username", + "system_name": "System name", + "started": "Started at" } - + process_keys = [ + "process_id", "hostname", "hostip", + "username", "system_name", "process_name" + ] + log_keys = [ + "timestamp", "level", "thread", "threadName", "message", "loggerName", + "fileName", "module", "method", "lineNumber" + ] + default_value = "- Not set -" NodeRole = QtCore.Qt.UserRole + 1 def __init__(self, parent=None): @@ -50,14 +52,47 @@ class LogModel(QtCore.QAbstractItemModel): self._root_node.add_child(node) def refresh(self): + self.log_by_process = collections.defaultdict(list) + self.process_info = {} + self.clear() self.beginResetModel() if self.dbcon: result = self.dbcon.find({}) for item in result: - self.add_log(item) - self.endResetModel() + process_id = item.get("process_id") + # backwards (in)compatibility + if not process_id: + continue + if process_id not in self.process_info: + proc_dict = {} + for key in self.process_keys: + proc_dict[key] = ( + item.get(key) or self.default_value + ) + self.process_info[process_id] = proc_dict + + if "_logs" not in self.process_info[process_id]: + self.process_info[process_id]["_logs"] = [] + + log_item = {} + for key in self.log_keys: + log_item[key] = item.get(key) or self.default_value + + if "exception" in item: + log_item["exception"] = item["exception"] + + self.process_info[process_id]["_logs"].append(log_item) + + for item in self.process_info.values(): + item["_logs"] = sorted( + item["_logs"], key=lambda item: item["timestamp"] + ) + item["started"] = item["_logs"][0]["timestamp"] + self.add_log(item) + + self.endResetModel() def data(self, index, role): if not index.isValid(): @@ -68,7 +103,7 @@ class LogModel(QtCore.QAbstractItemModel): column = index.column() key = self.COLUMNS[column] - if key == "timestamp": + if key == "started": return str(node.get(key, None)) return node.get(key, None) @@ -86,8 +121,7 @@ class LogModel(QtCore.QAbstractItemModel): child_item = parent_node.child(row) if child_item: return self.createIndex(row, column, child_item) - else: - return QtCore.QModelIndex() + return QtCore.QModelIndex() def rowCount(self, parent): node = self._root_node diff --git a/pype/modules/logging/gui/widgets.py b/pype/modules/logging/gui/widgets.py index 1daaa28326..cf20066397 100644 --- a/pype/modules/logging/gui/widgets.py +++ b/pype/modules/logging/gui/widgets.py @@ -1,5 +1,5 @@ -import getpass from Qt import QtCore, QtWidgets, QtGui +from PyQt5.QtCore import QVariant from .models import LogModel @@ -97,7 +97,6 @@ class SelectableMenu(QtWidgets.QMenu): class CustomCombo(QtWidgets.QWidget): selection_changed = QtCore.Signal() - checked_changed = QtCore.Signal(bool) def __init__(self, title, parent=None): super(CustomCombo, self).__init__(parent) @@ -126,27 +125,12 @@ class CustomCombo(QtWidgets.QWidget): self.toolmenu.clear() self.addItems(items) - def select_items(self, items, ignore_input=False): - if not isinstance(items, list): - items = [items] - - for action in self.toolmenu.actions(): - check = True - if ( - action.text() in items and ignore_input or - action.text() not in items and not ignore_input - ): - check = False - - action.setChecked(check) - def addItems(self, items): for item in items: action = self.toolmenu.addAction(item) action.setCheckable(True) - self.toolmenu.addAction(action) action.setChecked(True) - action.triggered.connect(self.checked_changed) + self.toolmenu.addAction(action) def items(self): for action in self.toolmenu.actions(): @@ -200,42 +184,15 @@ class CheckableComboBox(QtWidgets.QComboBox): for text, checked in items: text_item = QtGui.QStandardItem(text) checked_item = QtGui.QStandardItem() - checked_item.setData( - QtCore.QVariant(checked), QtCore.Qt.CheckStateRole - ) + checked_item.setData(QVariant(checked), QtCore.Qt.CheckStateRole) self.model.appendRow([text_item, checked_item]) -class FilterLogModel(QtCore.QSortFilterProxyModel): - sub_dict = ["$gt", "$lt", "$not"] - def __init__(self, key_values, parent=None): - super(FilterLogModel, self).__init__(parent) - self.allowed_key_values = key_values - - def filterAcceptsRow(self, row, parent): - """ - Reimplemented from base class. - """ - model = self.sourceModel() - for key, values in self.allowed_key_values.items(): - col_indx = model.COLUMNS.index(key) - value = model.index(row, col_indx, parent).data( - QtCore.Qt.DisplayRole - ) - if value not in values: - return False - return True - - class LogsWidget(QtWidgets.QWidget): """A widget that lists the published subsets for an asset""" active_changed = QtCore.Signal() - _level_order = [ - "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL" - ] - def __init__(self, parent=None): super(LogsWidget, self).__init__(parent=parent) @@ -243,41 +200,47 @@ class LogsWidget(QtWidgets.QWidget): filter_layout = QtWidgets.QHBoxLayout() + # user_filter = SearchComboBox(self, "Users") user_filter = CustomCombo("Users", self) users = model.dbcon.distinct("user") user_filter.populate(users) - user_filter.checked_changed.connect(self.user_changed) - user_filter.select_items(getpass.getuser()) + user_filter.selection_changed.connect(self.user_changed) level_filter = CustomCombo("Levels", self) + # levels = [(level, True) for level in model.dbcon.distinct("level")] levels = model.dbcon.distinct("level") - _levels = [] - for level in self._level_order: - if level in levels: - _levels.append(level) - level_filter.populate(_levels) - level_filter.checked_changed.connect(self.level_changed) + level_filter.addItems(levels) - # date_from_label = QtWidgets.QLabel("From:") - # date_filter_from = QtWidgets.QDateTimeEdit() - # - # date_from_layout = QtWidgets.QVBoxLayout() - # date_from_layout.addWidget(date_from_label) - # date_from_layout.addWidget(date_filter_from) - # - # date_to_label = QtWidgets.QLabel("To:") - # date_filter_to = QtWidgets.QDateTimeEdit() - # - # date_to_layout = QtWidgets.QVBoxLayout() - # date_to_layout.addWidget(date_to_label) - # date_to_layout.addWidget(date_filter_to) + date_from_label = QtWidgets.QLabel("From:") + date_filter_from = QtWidgets.QDateTimeEdit() + + date_from_layout = QtWidgets.QVBoxLayout() + date_from_layout.addWidget(date_from_label) + date_from_layout.addWidget(date_filter_from) + + # now = datetime.datetime.now() + # QtCore.QDateTime( + # now.year, + # now.month, + # now.day, + # now.hour, + # now.minute, + # second=0, + # msec=0, + # timeSpec=0 + # ) + date_to_label = QtWidgets.QLabel("To:") + date_filter_to = QtWidgets.QDateTimeEdit() + + date_to_layout = QtWidgets.QVBoxLayout() + date_to_layout.addWidget(date_to_label) + date_to_layout.addWidget(date_filter_to) filter_layout.addWidget(user_filter) filter_layout.addWidget(level_filter) - filter_layout.setAlignment(QtCore.Qt.AlignLeft) - # filter_layout.addLayout(date_from_layout) - # filter_layout.addLayout(date_to_layout) + filter_layout.addLayout(date_from_layout) + filter_layout.addLayout(date_to_layout) view = QtWidgets.QTreeView(self) view.setAllColumnsShowFocus(True) @@ -290,58 +253,28 @@ class LogsWidget(QtWidgets.QWidget): view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) view.setSortingEnabled(True) view.sortByColumn( - model.COLUMNS.index("timestamp"), + model.COLUMNS.index("started"), QtCore.Qt.AscendingOrder ) - key_val = { - "user": users, - "level": levels - } - proxy_model = FilterLogModel(key_val, view) - proxy_model.setSourceModel(model) - view.setModel(proxy_model) - - view.customContextMenuRequested.connect(self.on_context_menu) - view.selectionModel().selectionChanged.connect(self.active_changed) - - # WARNING this is cool but slows down widget a lot - # header = view.header() - # # Enforce the columns to fit the data (purely cosmetic) - # if Qt.__binding__ in ("PySide2", "PyQt5"): - # header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents) - # else: - # header.setResizeMode(QtWidgets.QHeaderView.ResizeToContents) - + view.setModel(model) + view.pressed.connect(self._on_activated) # prepare model.refresh() # Store to memory self.model = model - self.proxy_model = proxy_model self.view = view self.user_filter = user_filter self.level_filter = level_filter + def _on_activated(self, *args, **kwargs): + self.active_changed.emit() + def user_changed(self): - valid_actions = [] for action in self.user_filter.items(): - if action.isChecked(): - valid_actions.append(action.text()) - - self.proxy_model.allowed_key_values["user"] = valid_actions - self.proxy_model.invalidate() - - def level_changed(self): - valid_actions = [] - for action in self.level_filter.items(): - if action.isChecked(): - valid_actions.append(action.text()) - - self.proxy_model.allowed_key_values["level"] = valid_actions - self.proxy_model.invalidate() - + print(action) def on_context_menu(self, point): # TODO will be any actions? it's ready @@ -360,10 +293,74 @@ class LogsWidget(QtWidgets.QWidget): rows = selection.selectedRows(column=0) if len(rows) == 1: return rows[0] - return None +class OutputWidget(QtWidgets.QWidget): + def __init__(self, parent=None): + super(OutputWidget, self).__init__(parent=parent) + layout = QtWidgets.QVBoxLayout(self) + output_text = QtWidgets.QTextEdit() + output_text.setReadOnly(True) + # output_text.setLineWrapMode(QtWidgets.QTextEdit.FixedPixelWidth) + + layout.addWidget(output_text) + + self.setLayout(layout) + self.output_text = output_text + + def add_line(self, line): + self.output_text.append(line) + + def set_detail(self, node): + self.output_text.clear() + for log in node["_logs"]: + level = log["level"].lower() + + line_f = "{message}" + if level == "debug": + line_f = ( + " -" + " {{ {loggerName} }}: [" + " {message}" + " ]" + ) + elif level == "info": + line_f = ( + ">>> [" + " {message}" + " ]" + ) + elif level == "warning": + line_f = ( + "*** WRN:" + " >>> {{ {loggerName} }}: [" + " {message}" + " ]" + ) + elif level == "error": + line_f = ( + "!!! ERR:" + " {timestamp}" + " >>> {{ {loggerName} }}: [" + " {message}" + " ]" + ) + + exc = log.get("exception") + if exc: + log["message"] = exc["message"] + + line = line_f.format(**log) + + self.add_line(line) + + if not exc: + continue + for _line in exc["stackTrace"].split("\n"): + self.add_line(_line) + + class LogDetailWidget(QtWidgets.QWidget): """A Widget that display information about a specific version""" data_rows = [ @@ -418,5 +415,4 @@ class LogDetailWidget(QtWidgets.QWidget): value = detail_data.get(row) or "< Not set >" data[row] = value - self.detail_widget.setHtml(self.html_text.format(**data)) diff --git a/pype/modules/logging/tray/logging_module.py b/pype/modules/logging/tray/logging_module.py index 15bec1aad5..087a51f322 100644 --- a/pype/modules/logging/tray/logging_module.py +++ b/pype/modules/logging/tray/logging_module.py @@ -1,4 +1,3 @@ -import os from Qt import QtWidgets from pype.api import Logger diff --git a/pype/modules/muster/muster.py b/pype/modules/muster/muster.py index 1f92e57e83..629fb12635 100644 --- a/pype/modules/muster/muster.py +++ b/pype/modules/muster/muster.py @@ -1,5 +1,5 @@ import appdirs -from pypeapp import style +from avalon import style from Qt import QtWidgets import os import json diff --git a/pype/modules/muster/widget_login.py b/pype/modules/muster/widget_login.py index 88d769ef93..8de0d3136a 100644 --- a/pype/modules/muster/widget_login.py +++ b/pype/modules/muster/widget_login.py @@ -1,6 +1,6 @@ import os from Qt import QtCore, QtGui, QtWidgets -from pypeapp import style +from avalon import style class MusterLogin(QtWidgets.QWidget): diff --git a/pype/modules/standalonepublish/widgets/widget_component_item.py b/pype/modules/standalonepublish/widgets/widget_component_item.py index 6275238412..40298520b1 100644 --- a/pype/modules/standalonepublish/widgets/widget_component_item.py +++ b/pype/modules/standalonepublish/widgets/widget_component_item.py @@ -1,7 +1,7 @@ import os from . import QtCore, QtGui, QtWidgets from . import get_resource -from pypeapp import style +from avalon import style class ComponentItem(QtWidgets.QFrame): diff --git a/pype/modules/timers_manager/widget_user_idle.py b/pype/modules/timers_manager/widget_user_idle.py index f5efcec9bb..697c0a04d9 100644 --- a/pype/modules/timers_manager/widget_user_idle.py +++ b/pype/modules/timers_manager/widget_user_idle.py @@ -1,5 +1,5 @@ from pype.api import Logger -from pypeapp import style +from avalon import style from Qt import QtCore, QtGui, QtWidgets diff --git a/pype/modules/user/widget_user.py b/pype/modules/user/widget_user.py index 27faa857f5..1d43941345 100644 --- a/pype/modules/user/widget_user.py +++ b/pype/modules/user/widget_user.py @@ -1,6 +1,6 @@ -import os from Qt import QtCore, QtGui, QtWidgets -from pypeapp import style, resources +from pype.resources import get_resource +from avalon import style class UserWidget(QtWidgets.QWidget): @@ -14,7 +14,7 @@ class UserWidget(QtWidgets.QWidget): self.module = module # Style - icon = QtGui.QIcon(resources.get_resource("icon.png")) + icon = QtGui.QIcon(get_resource("icon.png")) self.setWindowIcon(icon) self.setWindowTitle("Username Settings") self.setMinimumWidth(self.MIN_WIDTH) diff --git a/pype/plugins/blender/create/create_layout.py b/pype/plugins/blender/create/create_layout.py new file mode 100644 index 0000000000..010eec539b --- /dev/null +++ b/pype/plugins/blender/create/create_layout.py @@ -0,0 +1,40 @@ +"""Create a layout asset.""" + +import bpy + +from avalon import api +from avalon.blender import Creator, lib +import pype.hosts.blender.plugin + + +class CreateLayout(Creator): + """Layout output for character rigs""" + + name = "layoutMain" + label = "Layout" + family = "layout" + icon = "cubes" + + def process(self): + + asset = self.data["asset"] + subset = self.data["subset"] + name = pype.hosts.blender.plugin.asset_name(asset, subset) + collection = bpy.data.collections.new(name=name) + bpy.context.scene.collection.children.link(collection) + self.data['task'] = api.Session.get('AVALON_TASK') + lib.imprint(collection, self.data) + + # Add the rig object and all the children meshes to + # a set and link them all at the end to avoid duplicates. + # Blender crashes if trying to link an object that is already linked. + # This links automatically the children meshes if they were not + # selected, and doesn't link them twice if they, insted, + # were manually selected by the user. + objects_to_link = set() + + if (self.options or {}).get("useSelection"): + for obj in lib.get_selection(): + collection.children.link(obj.users_collection[0]) + + return collection diff --git a/pype/plugins/blender/create/create_rig.py b/pype/plugins/blender/create/create_rig.py index 855c2ab461..5c85bf969d 100644 --- a/pype/plugins/blender/create/create_rig.py +++ b/pype/plugins/blender/create/create_rig.py @@ -31,22 +31,11 @@ class CreateRig(Creator): # This links automatically the children meshes if they were not # selected, and doesn't link them twice if they, insted, # were manually selected by the user. - objects_to_link = set() if (self.options or {}).get("useSelection"): - for obj in lib.get_selection(): - - objects_to_link.add(obj) - - if obj.type == 'ARMATURE': - - for subobj in obj.children: - - objects_to_link.add(subobj) - - for obj in objects_to_link: - - collection.objects.link(obj) + for child in obj.users_collection[0].children: + collection.children.link(child) + collection.objects.link(obj) return collection diff --git a/pype/plugins/blender/load/load_animation.py b/pype/plugins/blender/load/load_animation.py index a26f402d47..1c0e6e0906 100644 --- a/pype/plugins/blender/load/load_animation.py +++ b/pype/plugins/blender/load/load_animation.py @@ -29,7 +29,6 @@ class BlendAnimationLoader(pype.hosts.blender.plugin.AssetLoader): icon = "code-fork" color = "orange" - @staticmethod def _remove(self, objects, lib_container): for obj in objects: @@ -41,7 +40,6 @@ class BlendAnimationLoader(pype.hosts.blender.plugin.AssetLoader): bpy.data.collections.remove(bpy.data.collections[lib_container]) - @staticmethod def _process(self, libpath, lib_container, container_name): relative = bpy.context.preferences.filepaths.use_relative_paths @@ -131,7 +129,7 @@ class BlendAnimationLoader(pype.hosts.blender.plugin.AssetLoader): container_metadata["lib_container"] = lib_container objects_list = self._process( - self, libpath, lib_container, container_name) + libpath, lib_container, container_name) # Save the list of objects in the metadata container container_metadata["objects"] = objects_list @@ -205,14 +203,10 @@ class BlendAnimationLoader(pype.hosts.blender.plugin.AssetLoader): objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] - # Get the armature of the rig - armatures = [obj for obj in objects if obj.type == 'ARMATURE'] - assert(len(armatures) == 1) - - self._remove(self, objects, lib_container) + self._remove(objects, lib_container) objects_list = self._process( - self, str(libpath), lib_container, collection.name) + str(libpath), lib_container, collection.name) # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list @@ -249,7 +243,7 @@ class BlendAnimationLoader(pype.hosts.blender.plugin.AssetLoader): objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] - self._remove(self, objects, lib_container) + self._remove(objects, lib_container) bpy.data.collections.remove(collection) diff --git a/pype/plugins/blender/load/load_layout.py b/pype/plugins/blender/load/load_layout.py new file mode 100644 index 0000000000..0c1032c4fb --- /dev/null +++ b/pype/plugins/blender/load/load_layout.py @@ -0,0 +1,264 @@ +"""Load a layout in Blender.""" + +import logging +from pathlib import Path +from pprint import pformat +from typing import Dict, List, Optional + +from avalon import api, blender +import bpy +import pype.hosts.blender.plugin + + +logger = logging.getLogger("pype").getChild( + "blender").getChild("load_layout") + + +class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader): + """Load animations from a .blend file. + + Warning: + Loading the same asset more then once is not properly supported at the + moment. + """ + + families = ["layout"] + representations = ["blend"] + + label = "Link Layout" + icon = "code-fork" + color = "orange" + + def _remove(self, objects, lib_container): + + for obj in objects: + + if obj.type == 'ARMATURE': + bpy.data.armatures.remove(obj.data) + elif obj.type == 'MESH': + bpy.data.meshes.remove(obj.data) + + for element_container in bpy.data.collections[lib_container].children: + for child in element_container.children: + bpy.data.collections.remove(child) + bpy.data.collections.remove(element_container) + + bpy.data.collections.remove(bpy.data.collections[lib_container]) + + def _process(self, libpath, lib_container, container_name, actions): + + relative = bpy.context.preferences.filepaths.use_relative_paths + with bpy.data.libraries.load( + libpath, link=True, relative=relative + ) as (_, data_to): + data_to.collections = [lib_container] + + scene = bpy.context.scene + + scene.collection.children.link(bpy.data.collections[lib_container]) + + layout_container = scene.collection.children[lib_container].make_local() + + meshes = [] + armatures = [] + + objects_list = [] + + for element_container in layout_container.children: + element_container.make_local() + meshes.extend([obj for obj in element_container.objects if obj.type == 'MESH']) + armatures.extend([obj for obj in element_container.objects if obj.type == 'ARMATURE']) + for child in element_container.children: + child.make_local() + meshes.extend(child.objects) + + # Link meshes first, then armatures. + # The armature is unparented for all the non-local meshes, + # when it is made local. + for obj in meshes + armatures: + obj = obj.make_local() + obj.data.make_local() + + if not obj.get(blender.pipeline.AVALON_PROPERTY): + obj[blender.pipeline.AVALON_PROPERTY] = dict() + + avalon_info = obj[blender.pipeline.AVALON_PROPERTY] + avalon_info.update({"container_name": container_name}) + + action = actions.get( obj.name, None ) + + if obj.type == 'ARMATURE' and action is not None: + obj.animation_data.action = action + + objects_list.append(obj) + + layout_container.pop(blender.pipeline.AVALON_PROPERTY) + + bpy.ops.object.select_all(action='DESELECT') + + return objects_list + + def process_asset( + self, context: dict, name: str, namespace: Optional[str] = None, + options: Optional[Dict] = None + ) -> Optional[List]: + """ + Arguments: + name: Use pre-defined name + namespace: Use pre-defined namespace + context: Full parenthood of representation to load + options: Additional settings dictionary + """ + + libpath = self.fname + asset = context["asset"]["name"] + subset = context["subset"]["name"] + lib_container = pype.hosts.blender.plugin.asset_name(asset, subset) + container_name = pype.hosts.blender.plugin.asset_name( + asset, subset, namespace + ) + + container = bpy.data.collections.new(lib_container) + container.name = container_name + blender.pipeline.containerise_existing( + container, + name, + namespace, + context, + self.__class__.__name__, + ) + + container_metadata = container.get( + blender.pipeline.AVALON_PROPERTY) + + container_metadata["libpath"] = libpath + container_metadata["lib_container"] = lib_container + + objects_list = self._process( + libpath, lib_container, container_name, {}) + + # Save the list of objects in the metadata container + container_metadata["objects"] = objects_list + + nodes = list(container.objects) + nodes.append(container) + self[:] = nodes + return nodes + + def update(self, container: Dict, representation: Dict): + """Update the loaded asset. + + This will remove all objects of the current collection, load the new + ones and add them to the collection. + If the objects of the collection are used in another collection they + will not be removed, only unlinked. Normally this should not be the + case though. + + Warning: + No nested collections are supported at the moment! + """ + + collection = bpy.data.collections.get( + container["objectName"] + ) + + libpath = Path(api.get_representation_path(representation)) + extension = libpath.suffix.lower() + + logger.info( + "Container: %s\nRepresentation: %s", + pformat(container, indent=2), + pformat(representation, indent=2), + ) + + assert collection, ( + f"The asset is not loaded: {container['objectName']}" + ) + assert not (collection.children), ( + "Nested collections are not supported." + ) + assert libpath, ( + "No existing library file found for {container['objectName']}" + ) + assert libpath.is_file(), ( + f"The file doesn't exist: {libpath}" + ) + assert extension in pype.hosts.blender.plugin.VALID_EXTENSIONS, ( + f"Unsupported file: {libpath}" + ) + + collection_metadata = collection.get( + blender.pipeline.AVALON_PROPERTY) + + collection_libpath = collection_metadata["libpath"] + normalized_collection_libpath = ( + str(Path(bpy.path.abspath(collection_libpath)).resolve()) + ) + normalized_libpath = ( + str(Path(bpy.path.abspath(str(libpath))).resolve()) + ) + logger.debug( + "normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s", + normalized_collection_libpath, + normalized_libpath, + ) + if normalized_collection_libpath == normalized_libpath: + logger.info("Library already loaded, not updating...") + return + + objects = collection_metadata["objects"] + lib_container = collection_metadata["lib_container"] + + actions = {} + + for obj in objects: + + if obj.type == 'ARMATURE': + + actions[obj.name] = obj.animation_data.action + + self._remove(objects, lib_container) + + objects_list = self._process( + str(libpath), lib_container, collection.name, actions) + + # Save the list of objects in the metadata container + collection_metadata["objects"] = objects_list + collection_metadata["libpath"] = str(libpath) + collection_metadata["representation"] = str(representation["_id"]) + + bpy.ops.object.select_all(action='DESELECT') + + def remove(self, container: Dict) -> bool: + """Remove an existing container from a Blender scene. + + Arguments: + container (avalon-core:container-1.0): Container to remove, + from `host.ls()`. + + Returns: + bool: Whether the container was deleted. + + Warning: + No nested collections are supported at the moment! + """ + + collection = bpy.data.collections.get( + container["objectName"] + ) + if not collection: + return False + assert not (collection.children), ( + "Nested collections are not supported." + ) + + collection_metadata = collection.get( + blender.pipeline.AVALON_PROPERTY) + objects = collection_metadata["objects"] + lib_container = collection_metadata["lib_container"] + + self._remove(objects, lib_container) + + bpy.data.collections.remove(collection) + + return True diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index 2abc4175c6..4a8f43cd48 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -30,7 +30,6 @@ class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader): icon = "code-fork" color = "orange" - @staticmethod def _remove(self, objects, lib_container): for obj in objects: @@ -39,7 +38,6 @@ class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader): bpy.data.collections.remove(bpy.data.collections[lib_container]) - @staticmethod def _process(self, libpath, lib_container, container_name): relative = bpy.context.preferences.filepaths.use_relative_paths @@ -118,7 +116,7 @@ class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader): container_metadata["lib_container"] = lib_container objects_list = self._process( - self, libpath, lib_container, container_name) + libpath, lib_container, container_name) # Save the list of objects in the metadata container container_metadata["objects"] = objects_list @@ -189,10 +187,10 @@ class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader): logger.info("Library already loaded, not updating...") return - self._remove(self, objects, lib_container) + self._remove(objects, lib_container) objects_list = self._process( - self, str(libpath), lib_container, collection.name) + str(libpath), lib_container, collection.name) # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list @@ -226,7 +224,7 @@ class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader): objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] - self._remove(self, objects, lib_container) + self._remove(objects, lib_container) bpy.data.collections.remove(collection) diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index 634a627a4a..3e53ff0363 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -30,7 +30,6 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader): icon = "code-fork" color = "orange" - @staticmethod def _remove(self, objects, lib_container): for obj in objects: @@ -40,9 +39,11 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader): elif obj.type == 'MESH': bpy.data.meshes.remove(obj.data) + for child in bpy.data.collections[lib_container].children: + bpy.data.collections.remove(child) + bpy.data.collections.remove(bpy.data.collections[lib_container]) - @staticmethod def _process(self, libpath, lib_container, container_name, action): relative = bpy.context.preferences.filepaths.use_relative_paths @@ -57,32 +58,30 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader): rig_container = scene.collection.children[lib_container].make_local() - meshes = [obj for obj in rig_container.objects if obj.type == 'MESH'] + meshes = [] armatures = [ obj for obj in rig_container.objects if obj.type == 'ARMATURE'] objects_list = [] - assert(len(armatures) == 1) + for child in rig_container.children: + child.make_local() + meshes.extend( child.objects ) # Link meshes first, then armatures. # The armature is unparented for all the non-local meshes, # when it is made local. for obj in meshes + armatures: - obj = obj.make_local() - obj.data.make_local() if not obj.get(blender.pipeline.AVALON_PROPERTY): - obj[blender.pipeline.AVALON_PROPERTY] = dict() avalon_info = obj[blender.pipeline.AVALON_PROPERTY] avalon_info.update({"container_name": container_name}) if obj.type == 'ARMATURE' and action is not None: - obj.animation_data.action = action objects_list.append(obj) @@ -130,7 +129,7 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader): container_metadata["lib_container"] = lib_container objects_list = self._process( - self, libpath, lib_container, container_name, None) + libpath, lib_container, container_name, None) # Save the list of objects in the metadata container container_metadata["objects"] = objects_list @@ -209,10 +208,10 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader): action = armatures[0].animation_data.action - self._remove(self, objects, lib_container) + self._remove(objects, lib_container) objects_list = self._process( - self, str(libpath), lib_container, collection.name, action) + str(libpath), lib_container, collection.name, action) # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list @@ -249,7 +248,7 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader): objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] - self._remove(self, objects, lib_container) + self._remove(objects, lib_container) bpy.data.collections.remove(collection) diff --git a/pype/plugins/blender/publish/extract_blend.py b/pype/plugins/blender/publish/extract_blend.py index 5f3fdac293..0924763f12 100644 --- a/pype/plugins/blender/publish/extract_blend.py +++ b/pype/plugins/blender/publish/extract_blend.py @@ -9,7 +9,7 @@ class ExtractBlend(pype.api.Extractor): label = "Extract Blend" hosts = ["blender"] - families = ["animation", "model", "rig", "action"] + families = ["animation", "model", "rig", "action", "layout"] optional = True def process(self, instance): diff --git a/pype/plugins/blender/publish/increment_workfile_version.py b/pype/plugins/blender/publish/increment_workfile_version.py new file mode 100644 index 0000000000..5addca6392 --- /dev/null +++ b/pype/plugins/blender/publish/increment_workfile_version.py @@ -0,0 +1,25 @@ +import pyblish.api +import avalon.blender.workio + + +class IncrementWorkfileVersion(pyblish.api.ContextPlugin): + """Increment current workfile version.""" + + order = pyblish.api.IntegratorOrder + 0.9 + label = "Increment Workfile Version" + optional = True + hosts = ["blender"] + families = ["animation", "model", "rig", "action"] + + def process(self, context): + + assert all(result["success"] for result in context.data["results"]), ( + "Publishing not succesfull so version is not increased.") + + from pype.lib import version_up + path = context.data["currentFile"] + filepath = version_up(path) + + avalon.blender.workio.save_file(filepath, copy=False) + + self.log.info('Incrementing script version') diff --git a/pype/plugins/global/publish/collect_output_repre_config.py b/pype/plugins/global/publish/collect_output_repre_config.py deleted file mode 100644 index 063af9ba26..0000000000 --- a/pype/plugins/global/publish/collect_output_repre_config.py +++ /dev/null @@ -1,22 +0,0 @@ -""" -Requires: - config_data -> ftrack.output_representation - -Provides: - context -> output_repre_config (str) -""" - -import pyblish.api -from pype.api import config - - -class CollectOutputRepreConfig(pyblish.api.ContextPlugin): - """Inject the current working file into context""" - - order = pyblish.api.CollectorOrder - label = "Collect Config for representation" - hosts = ["shell", "standalonepublisher"] - - def process(self, context): - config_data = config.get_presets()["ftrack"]["output_representation"] - context.data['output_repre_config'] = config_data diff --git a/pype/plugins/global/publish/extract_jpeg.py b/pype/plugins/global/publish/extract_jpeg.py index 2931bb5835..9b775f8b6f 100644 --- a/pype/plugins/global/publish/extract_jpeg.py +++ b/pype/plugins/global/publish/extract_jpeg.py @@ -14,18 +14,28 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): families = ["imagesequence", "render", "render2d", "source"] enabled = False - def process(self, instance): + # presetable attribute + ffmpeg_args = None + def process(self, instance): self.log.info("subset {}".format(instance.data['subset'])) if 'crypto' in instance.data['subset']: return + # ffmpeg doesn't support multipart exrs + if instance.data.get("multipartExr") is True: + return + # get representation and loop them representations = instance.data["representations"] # filter out mov and img sequences representations_new = representations[:] + if instance.data.get("multipartExr"): + # ffmpeg doesn't support multipart exrs + return + for repre in representations: tags = repre.get("tags", []) self.log.debug(repre) @@ -33,11 +43,7 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): if not valid: continue - if not isinstance(repre['files'], list): - continue - - if instance.data.get("multipartExr") is True: - # ffmpeg doesn't support multipart exrs + if not isinstance(repre['files'], (list, tuple)): continue stagingdir = os.path.normpath(repre.get("stagingDir")) @@ -57,21 +63,19 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): self.log.info("output {}".format(full_output_path)) - config_data = instance.context.data['output_repre_config'] - - proj_name = os.environ.get('AVALON_PROJECT', '__default__') - profile = config_data.get(proj_name, config_data['__default__']) - ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg") + ffmpeg_args = self.ffmpeg_args or {} jpeg_items = [] jpeg_items.append(ffmpeg_path) # override file if already exists jpeg_items.append("-y") # use same input args like with mov - jpeg_items.extend(profile.get('input', [])) + jpeg_items.extend(ffmpeg_args.get("input") or []) # input file jpeg_items.append("-i {}".format(full_input_path)) + # output arguments from presets + jpeg_items.extend(ffmpeg_args.get("output") or []) # output file jpeg_items.append(full_output_path) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 82d60ae5ae..ce77da18a9 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -76,7 +76,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "gizmo", "source", "matchmove", - "image" + "image", "source", "assembly", "fbx", diff --git a/pype/plugins/maya/publish/collect_look.py b/pype/plugins/maya/publish/collect_look.py index 6dc66711da..7df85e4ba7 100644 --- a/pype/plugins/maya/publish/collect_look.py +++ b/pype/plugins/maya/publish/collect_look.py @@ -277,7 +277,13 @@ class CollectLook(pyblish.api.InstancePlugin): if looksets: for look in looksets: for at in shaderAttrs: - con = cmds.listConnections("{}.{}".format(look, at)) + try: + con = cmds.listConnections("{}.{}".format(look, at)) + except ValueError: + # skip attributes that are invalid in current + # context. For example in the case where + # Arnold is not enabled. + continue if con: materials.extend(con) diff --git a/pype/plugins/maya/publish/collect_render.py b/pype/plugins/maya/publish/collect_render.py index 15f756caec..bfb6e79cba 100644 --- a/pype/plugins/maya/publish/collect_render.py +++ b/pype/plugins/maya/publish/collect_render.py @@ -57,11 +57,12 @@ R_SINGLE_FRAME = re.compile(r"^(-?)\d+$") R_FRAME_RANGE = re.compile(r"^(?P(-?)\d+)-(?P(-?)\d+)$") R_FRAME_NUMBER = re.compile(r".+\.(?P[0-9]+)\..+") R_LAYER_TOKEN = re.compile( - r".*%l.*|.*.*|.*.*", re.IGNORECASE + r".*((?:%l)|(?:)|(?:)).*", re.IGNORECASE ) R_AOV_TOKEN = re.compile(r".*%a.*|.*.*|.*.*", re.IGNORECASE) R_SUBSTITUTE_AOV_TOKEN = re.compile(r"%a||", re.IGNORECASE) -R_REMOVE_AOV_TOKEN = re.compile(r"_%a|_|_", re.IGNORECASE) +R_REMOVE_AOV_TOKEN = re.compile(r"(?:_|\.)((?:%a)|(?:)|(?:))", + re.IGNORECASE) # to remove unused renderman tokens R_CLEAN_FRAME_TOKEN = re.compile(r"\.?\.?", re.IGNORECASE) R_CLEAN_EXT_TOKEN = re.compile(r"\.?\.?", re.IGNORECASE) @@ -514,20 +515,23 @@ class AExpectedFiles: } return scene_data - def _generate_single_file_sequence(self, layer_data): + def _generate_single_file_sequence(self, layer_data, aov_name=None): expected_files = [] file_prefix = layer_data["filePrefix"] for cam in layer_data["cameras"]: - mappings = ( + mappings = [ (R_SUBSTITUTE_SCENE_TOKEN, layer_data["sceneName"]), (R_SUBSTITUTE_LAYER_TOKEN, layer_data["layerName"]), (R_SUBSTITUTE_CAMERA_TOKEN, cam), - # this is required to remove unfilled aov token, for example - # in Redshift - (R_REMOVE_AOV_TOKEN, ""), (R_CLEAN_FRAME_TOKEN, ""), (R_CLEAN_EXT_TOKEN, ""), - ) + ] + # this is required to remove unfilled aov token, for example + # in Redshift + if aov_name: + mappings.append((R_SUBSTITUTE_AOV_TOKEN, aov_name)) + else: + mappings.append((R_REMOVE_AOV_TOKEN, "")) for regex, value in mappings: file_prefix = re.sub(regex, value, file_prefix) @@ -837,13 +841,17 @@ class ExpectedFilesRedshift(AExpectedFiles): # mapping redshift extension dropdown values to strings ext_mapping = ["iff", "exr", "tif", "png", "tga", "jpg"] + # name of aovs that are not merged into resulting exr and we need + # them specified in expectedFiles output. + unmerged_aovs = ["Cryptomatte"] + def __init__(self, layer): super(ExpectedFilesRedshift, self).__init__(layer) self.renderer = "redshift" def get_renderer_prefix(self): prefix = super(ExpectedFilesRedshift, self).get_renderer_prefix() - prefix = "{}_".format(prefix) + prefix = "{}.".format(prefix) return prefix def get_files(self): @@ -856,7 +864,17 @@ class ExpectedFilesRedshift(AExpectedFiles): if layer_data.get("enabledAOVs"): expected_files[0][u"beauty"] = self._generate_single_file_sequence( layer_data - ) # noqa: E501 + ) + + # Redshift doesn't merge Cryptomatte AOV to final exr. We need to check + # for such condition and add it to list of expected files. + + for aov in layer_data.get("enabledAOVs"): + if aov[0].lower() == "cryptomatte": + aov_name = aov[0] + expected_files.append( + {aov_name: self._generate_single_file_sequence( + layer_data, aov_name=aov_name)}) return expected_files @@ -864,23 +882,15 @@ class ExpectedFilesRedshift(AExpectedFiles): enabled_aovs = [] try: - if self.maya_is_true( - cmds.getAttr("redshiftOptions.exrForceMultilayer") - ): - # AOVs are merged in mutli-channel file - self.multipart = True - return enabled_aovs + default_ext = self.ext_mapping[ + cmds.getAttr("redshiftOptions.imageFormat") + ] except ValueError: # this occurs when Render Setting windows was not opened yet. In - # such case there are no Arnold options created so query for AOVs - # will fail. We terminate here as there are no AOVs specified then. - # This state will most probably fail later on some Validator - # anyway. - return enabled_aovs + # such case there are no Redshift options created so query + # will fail. + raise ValueError("Render settings are not initialized") - default_ext = self.ext_mapping[ - cmds.getAttr("redshiftOptions.imageFormat") - ] rs_aovs = [n for n in cmds.ls(type="RedshiftAOV")] # todo: find out how to detect multichannel exr for redshift @@ -892,9 +902,26 @@ class ExpectedFilesRedshift(AExpectedFiles): enabled = self.maya_is_true(override) if enabled: - enabled_aovs.append( - (cmds.getAttr("%s.name" % aov), default_ext) - ) + # If AOVs are merged into multipart exr, append AOV only if it + # is in the list of AOVs that renderer cannot (or will not) + # merge into final exr. + if self.maya_is_true( + cmds.getAttr("redshiftOptions.exrForceMultilayer") + ): + if cmds.getAttr("%s.name" % aov) in self.unmerged_aovs: + enabled_aovs.append( + (cmds.getAttr("%s.name" % aov), default_ext) + ) + else: + enabled_aovs.append( + (cmds.getAttr("%s.name" % aov), default_ext) + ) + + if self.maya_is_true( + cmds.getAttr("redshiftOptions.exrForceMultilayer") + ): + # AOVs are merged in mutli-channel file + self.multipart = True return enabled_aovs diff --git a/pype/plugins/photoshop/create/create_image.py b/pype/plugins/photoshop/create/create_image.py new file mode 100644 index 0000000000..a840dd13a7 --- /dev/null +++ b/pype/plugins/photoshop/create/create_image.py @@ -0,0 +1,12 @@ +from avalon import photoshop + + +class CreateImage(photoshop.Creator): + """Image folder for publish.""" + + name = "imageDefault" + label = "Image" + family = "image" + + def __init__(self, *args, **kwargs): + super(CreateImage, self).__init__(*args, **kwargs) diff --git a/pype/plugins/photoshop/load/load_image.py b/pype/plugins/photoshop/load/load_image.py new file mode 100644 index 0000000000..18efe750d5 --- /dev/null +++ b/pype/plugins/photoshop/load/load_image.py @@ -0,0 +1,43 @@ +from avalon import api, photoshop + + +class ImageLoader(api.Loader): + """Load images + + Stores the imported asset in a container named after the asset. + """ + + families = ["image"] + representations = ["*"] + + def load(self, context, name=None, namespace=None, data=None): + with photoshop.maintained_selection(): + layer = photoshop.import_smart_object(self.fname) + + self[:] = [layer] + + return photoshop.containerise( + name, + namespace, + layer, + context, + self.__class__.__name__ + ) + + def update(self, container, representation): + layer = container.pop("layer") + + with photoshop.maintained_selection(): + photoshop.replace_smart_object( + layer, api.get_representation_path(representation) + ) + + photoshop.imprint( + layer, {"representation": str(representation["_id"])} + ) + + def remove(self, container): + container["layer"].Delete() + + def switch(self, container, representation): + self.update(container, representation) diff --git a/pype/plugins/photoshop/publish/collect_current_file.py b/pype/plugins/photoshop/publish/collect_current_file.py new file mode 100644 index 0000000000..4308588559 --- /dev/null +++ b/pype/plugins/photoshop/publish/collect_current_file.py @@ -0,0 +1,17 @@ +import os + +import pyblish.api +from avalon import photoshop + + +class CollectCurrentFile(pyblish.api.ContextPlugin): + """Inject the current working file into context""" + + order = pyblish.api.CollectorOrder - 0.5 + label = "Current File" + hosts = ["photoshop"] + + def process(self, context): + context.data["currentFile"] = os.path.normpath( + photoshop.app().ActiveDocument.FullName + ).replace("\\", "/") diff --git a/pype/plugins/photoshop/publish/collect_instances.py b/pype/plugins/photoshop/publish/collect_instances.py new file mode 100644 index 0000000000..4937f2a1e4 --- /dev/null +++ b/pype/plugins/photoshop/publish/collect_instances.py @@ -0,0 +1,56 @@ +import pythoncom + +from avalon import photoshop + +import pyblish.api + + +class CollectInstances(pyblish.api.ContextPlugin): + """Gather instances by LayerSet and file metadata + + This collector takes into account assets that are associated with + an LayerSet and marked with a unique identifier; + + Identifier: + id (str): "pyblish.avalon.instance" + """ + + label = "Instances" + order = pyblish.api.CollectorOrder + hosts = ["photoshop"] + families_mapping = { + "image": [] + } + + def process(self, context): + # Necessary call when running in a different thread which pyblish-qml + # can be. + pythoncom.CoInitialize() + + for layer in photoshop.get_layers_in_document(): + layer_data = photoshop.read(layer) + + # Skip layers without metadata. + if layer_data is None: + continue + + # Skip containers. + if "container" in layer_data["id"]: + continue + + child_layers = [*layer.Layers] + if not child_layers: + self.log.info("%s skipped, it was empty." % layer.Name) + continue + + instance = context.create_instance(layer.Name) + instance.append(layer) + instance.data.update(layer_data) + instance.data["families"] = self.families_mapping[ + layer_data["family"] + ] + instance.data["publish"] = layer.Visible + + # Produce diagnostic message for any graphical + # user interface interested in visualising it. + self.log.info("Found: \"%s\" " % instance.data["name"]) diff --git a/pype/plugins/photoshop/publish/collect_workfile.py b/pype/plugins/photoshop/publish/collect_workfile.py new file mode 100644 index 0000000000..766be02354 --- /dev/null +++ b/pype/plugins/photoshop/publish/collect_workfile.py @@ -0,0 +1,39 @@ +import pyblish.api +import os + + +class CollectWorkfile(pyblish.api.ContextPlugin): + """Collect current script for publish.""" + + order = pyblish.api.CollectorOrder + 0.1 + label = "Collect Workfile" + hosts = ["photoshop"] + + def process(self, context): + family = "workfile" + task = os.getenv("AVALON_TASK", None) + subset = family + task.capitalize() + + file_path = context.data["currentFile"] + staging_dir = os.path.dirname(file_path) + base_name = os.path.basename(file_path) + + # Create instance + instance = context.create_instance(subset) + instance.data.update({ + "subset": subset, + "label": base_name, + "name": base_name, + "family": family, + "families": [], + "representations": [], + "asset": os.environ["AVALON_ASSET"] + }) + + # creating representation + instance.data["representations"].append({ + "name": "psd", + "ext": "psd", + "files": base_name, + "stagingDir": staging_dir, + }) diff --git a/pype/plugins/photoshop/publish/extract_image.py b/pype/plugins/photoshop/publish/extract_image.py new file mode 100644 index 0000000000..da3197c7da --- /dev/null +++ b/pype/plugins/photoshop/publish/extract_image.py @@ -0,0 +1,62 @@ +import os + +import pype.api +from avalon import photoshop + + +class ExtractImage(pype.api.Extractor): + """Produce a flattened image file from instance + + This plug-in takes into account only the layers in the group. + """ + + label = "Extract Image" + hosts = ["photoshop"] + families = ["image"] + + def process(self, instance): + + staging_dir = self.staging_dir(instance) + self.log.info("Outputting image to {}".format(staging_dir)) + + # Perform extraction + files = {} + with photoshop.maintained_selection(): + self.log.info("Extracting %s" % str(list(instance))) + with photoshop.maintained_visibility(): + # Hide all other layers. + extract_ids = [ + x.id for x in photoshop.get_layers_in_layers([instance[0]]) + ] + for layer in photoshop.get_layers_in_document(): + if layer.id not in extract_ids: + layer.Visible = False + + save_options = { + "png": photoshop.com_objects.PNGSaveOptions(), + "jpg": photoshop.com_objects.JPEGSaveOptions() + } + + for extension, save_option in save_options.items(): + photoshop.app().ActiveDocument.SaveAs( + staging_dir, save_option, True + ) + files[extension] = "{} copy.{}".format( + os.path.splitext( + photoshop.app().ActiveDocument.Name + )[0], + extension + ) + + representations = [] + for extension, filename in files.items(): + representations.append({ + "name": extension, + "ext": extension, + "files": filename, + "stagingDir": staging_dir + }) + instance.data["representations"] = representations + instance.data["stagingDir"] = staging_dir + + self.log.info(f"Extracted {instance} to {staging_dir}") diff --git a/pype/plugins/photoshop/publish/extract_save_scene.py b/pype/plugins/photoshop/publish/extract_save_scene.py new file mode 100644 index 0000000000..b3d4f0e447 --- /dev/null +++ b/pype/plugins/photoshop/publish/extract_save_scene.py @@ -0,0 +1,14 @@ +import pype.api +from avalon import photoshop + + +class ExtractSaveScene(pype.api.Extractor): + """Save scene before extraction.""" + + order = pype.api.Extractor.order - 0.49 + label = "Extract Save Scene" + hosts = ["photoshop"] + families = ["workfile"] + + def process(self, instance): + photoshop.app().ActiveDocument.Save() diff --git a/pype/plugins/photoshop/publish/validate_instance_asset.py b/pype/plugins/photoshop/publish/validate_instance_asset.py new file mode 100644 index 0000000000..ab1d02269f --- /dev/null +++ b/pype/plugins/photoshop/publish/validate_instance_asset.py @@ -0,0 +1,48 @@ +import os + +import pyblish.api +import pype.api +from avalon import photoshop + + +class ValidateInstanceAssetRepair(pyblish.api.Action): + """Repair the instance asset.""" + + label = "Repair" + icon = "wrench" + on = "failed" + + def process(self, context, plugin): + + # Get the errored instances + failed = [] + for result in context.data["results"]: + if (result["error"] is not None and result["instance"] is not None + and result["instance"] not in failed): + failed.append(result["instance"]) + + # Apply pyblish.logic to get the instances for the plug-in + instances = pyblish.api.instances_by_plugin(failed, plugin) + + for instance in instances: + data = photoshop.read(instance[0]) + data["asset"] = os.environ["AVALON_ASSET"] + photoshop.imprint(instance[0], data) + + +class ValidateInstanceAsset(pyblish.api.InstancePlugin): + """Validate the instance asset is the current asset.""" + + label = "Validate Instance Asset" + hosts = ["photoshop"] + actions = [ValidateInstanceAssetRepair] + order = pype.api.ValidateContentsOrder + + def process(self, instance): + instance_asset = instance.data["asset"] + current_asset = os.environ["AVALON_ASSET"] + msg = ( + "Instance asset is not the same as current asset:" + f"\nInstance: {instance_asset}\nCurrent: {current_asset}" + ) + assert instance_asset == current_asset, msg diff --git a/pype/plugins/resolve/publish/collect_host.py b/pype/plugins/resolve/publish/collect_host.py new file mode 100644 index 0000000000..a5c4b0936c --- /dev/null +++ b/pype/plugins/resolve/publish/collect_host.py @@ -0,0 +1,17 @@ +import pyblish.api +from pype.hosts.resolve.utils import get_resolve_module + + +class CollectProject(pyblish.api.ContextPlugin): + """Collect Project object""" + + order = pyblish.api.CollectorOrder - 0.1 + label = "Collect Project" + hosts = ["resolve"] + + def process(self, context): + resolve = get_resolve_module() + PM = resolve.GetProjectManager() + P = PM.GetCurrentProject() + + self.log.info(P.GetName()) diff --git a/pype/plugins/standalonepublisher/publish/extract_thumbnail.py b/pype/plugins/standalonepublisher/publish/extract_thumbnail.py index daa3936359..cddc9c3a82 100644 --- a/pype/plugins/standalonepublisher/publish/extract_thumbnail.py +++ b/pype/plugins/standalonepublisher/publish/extract_thumbnail.py @@ -18,6 +18,9 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): hosts = ["standalonepublisher"] order = pyblish.api.ExtractorOrder + # Presetable attribute + ffmpeg_args = None + def process(self, instance): repres = instance.data.get('representations') if not repres: @@ -66,27 +69,23 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): full_thumbnail_path = tempfile.mkstemp(suffix=".jpg")[1] self.log.info("output {}".format(full_thumbnail_path)) - config_data = instance.context.data.get("output_repre_config", {}) - - proj_name = os.environ.get("AVALON_PROJECT", "__default__") - profile = config_data.get( - proj_name, - config_data.get("__default__", {}) - ) - ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg") + ffmpeg_args = self.ffmpeg_args or {} + jpeg_items = [] jpeg_items.append(ffmpeg_path) # override file if already exists jpeg_items.append("-y") # add input filters from peresets - if profile: - jpeg_items.extend(profile.get('input', [])) + jpeg_items.extend(ffmpeg_args.get("input") or []) # input file jpeg_items.append("-i {}".format(full_input_path)) # extract only single file jpeg_items.append("-vframes 1") + + jpeg_items.extend(ffmpeg_args.get("output") or []) + # output file jpeg_items.append(full_thumbnail_path) diff --git a/pype/resources/__init__.py b/pype/resources/__init__.py new file mode 100644 index 0000000000..248614ae9d --- /dev/null +++ b/pype/resources/__init__.py @@ -0,0 +1,16 @@ +import os + + +def get_resource(*args): + """ Serves to simple resources access + + :param *args: should contain *subfolder* names and *filename* of + resource from resources folder + :type *args: list + """ + return os.path.normpath( + os.path.join( + os.path.dirname(__file__), + *args + ) + ) diff --git a/pype/resources/circle_green.png b/pype/resources/circle_green.png new file mode 100644 index 0000000000..b83369a9e3 Binary files /dev/null and b/pype/resources/circle_green.png differ diff --git a/pype/resources/circle_orange.png b/pype/resources/circle_orange.png new file mode 100644 index 0000000000..656f318e0c Binary files /dev/null and b/pype/resources/circle_orange.png differ diff --git a/pype/resources/circle_red.png b/pype/resources/circle_red.png new file mode 100644 index 0000000000..99d01b3baa Binary files /dev/null and b/pype/resources/circle_red.png differ diff --git a/pype/resources/icon.png b/pype/resources/icon.png new file mode 100644 index 0000000000..bfacf6eeed Binary files /dev/null and b/pype/resources/icon.png differ diff --git a/pype/resources/icon_dev.png b/pype/resources/icon_dev.png new file mode 100644 index 0000000000..b452eb2812 Binary files /dev/null and b/pype/resources/icon_dev.png differ diff --git a/pype/resources/splash.png b/pype/resources/splash.png new file mode 100644 index 0000000000..bfacf6eeed Binary files /dev/null and b/pype/resources/splash.png differ diff --git a/pype/resources/splash_dev.png b/pype/resources/splash_dev.png new file mode 100644 index 0000000000..b452eb2812 Binary files /dev/null and b/pype/resources/splash_dev.png differ diff --git a/pype/resources/working.svg b/pype/resources/working.svg new file mode 100644 index 0000000000..fe73f15a31 --- /dev/null +++ b/pype/resources/working.svg @@ -0,0 +1,17 @@ + + + + + + Working... + diff --git a/pype/tools/pyblish_pype/__init__.py b/pype/tools/pyblish_pype/__init__.py new file mode 100644 index 0000000000..ef507005a5 --- /dev/null +++ b/pype/tools/pyblish_pype/__init__.py @@ -0,0 +1,13 @@ +from .version import version, version_info, __version__ + +# This must be run prior to importing the application, due to the +# application requiring a discovered copy of Qt bindings. + +from .app import show + +__all__ = [ + 'show', + 'version', + 'version_info', + '__version__' +] diff --git a/pype/tools/pyblish_pype/__main__.py b/pype/tools/pyblish_pype/__main__.py new file mode 100644 index 0000000000..5fc1b44a35 --- /dev/null +++ b/pype/tools/pyblish_pype/__main__.py @@ -0,0 +1,19 @@ +from .app import show + + +if __name__ == '__main__': + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument("--debug", action="store_true") + + args = parser.parse_args() + + if args.debug: + from . import mock + import pyblish.api + + for Plugin in mock.plugins: + pyblish.api.register_plugin(Plugin) + + show() diff --git a/pype/tools/pyblish_pype/app.css b/pype/tools/pyblish_pype/app.css new file mode 100644 index 0000000000..b52d9efec8 --- /dev/null +++ b/pype/tools/pyblish_pype/app.css @@ -0,0 +1,493 @@ +/* Global CSS */ + +* { + outline: none; + color: #ddd; + font-family: "Open Sans"; + font-style: normal; +} + +/* General CSS */ + +QWidget { + background: #555; + background-position: center center; + background-repeat: no-repeat; + font-size: 12px; +} + +QMenu { + background-color: #555; /* sets background of the menu */ + border: 1px solid #222; +} + +QMenu::item { + /* sets background of menu item. set this to something non-transparent + if you want menu color and menu item color to be different */ + background-color: transparent; + padding: 5px; + padding-left: 30px; +} + +QMenu::item:selected { /* when user selects item using mouse or keyboard */ + background-color: #666; +} + +QDialog { + min-width: 300; + background: "#555"; +} + +QListView { + border: 0px; + background: "transparent" +} + +QTreeView { + border: 0px; + background: "transparent" +} + +QPushButton { + width: 27px; + height: 27px; + background: #555; + border: 1px solid #aaa; + border-radius: 4px; + font-family: "FontAwesome"; + font-size: 11pt; + color: white; + padding: 0px; +} + +QPushButton:pressed { + background: "#777"; +} + +QPushButton:hover { + color: white; + background: "#666"; +} + +QPushButton:disabled { + color: rgba(255, 255, 255, 50); +} + +QTextEdit, QLineEdit { + background: #555; + border: 1px solid #333; + font-size: 9pt; + color: #fff; +} + +QCheckBox { + min-width: 17px; + max-width: 17px; + border: 1px solid #222; + background: transparent; +} + +QCheckBox::indicator { + width: 15px; + height: 15px; + /*background: #444;*/ + background: transparent; + border: 1px solid #555; +} + +QCheckBox::indicator:checked { + background: #222; +} + +QComboBox { + background: #444; + color: #EEE; + font-size: 8pt; + border: 1px solid #333; + padding: 0px; +} + +QComboBox[combolist="true"]::drop-down { + background: transparent; +} + +QComboBox[combolist="true"]::down-arrow { + max-width: 0px; + width: 1px; +} + +QComboBox[combolist="true"] QAbstractItemView { + background: #555; +} + +QScrollBar:vertical { + border: none; + background: transparent; + width: 6px; + margin: 0; +} + +QScrollBar::handle:vertical { + background: #333; + border-radius: 3px; + min-height: 20px; +} + +QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { + height: 0px; +} + +QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical { + border: 1px solid #444; + width: 3px; + height: 3px; + background: white; +} + +QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { + background: none; +} + +QToolTip { + color: #eee; + background-color: #555; + border: none; + padding: 5px; +} + +QLabel { + border-radius: 0px; +} + +QToolButton { + background-color: transparent; + margin: 0px; + padding: 0px; + border-radius: 0px; + border: none; +} + +/* Specific CSS */ +#PerspectiveToggleBtn { + border-bottom: 3px solid lightblue; + border-top: 0px; + border-radius: 0px; + border-right: 1px solid #232323; + border-left: 0px; + font-size: 26pt; + font-family: "FontAwesome"; +} + +#Terminal QComboBox::drop-down { + width: 60px; +} + +#Header { + background: #555; + border: 1px solid #444; + padding: 0px; + margin: 0px; +} + +#Header QRadioButton { + border: 3px solid "transparent"; + border-right: 1px solid #333; + left: 2px; +} + +#Header QRadioButton::indicator { + width: 65px; + height: 40px; + background-repeat: no-repeat; + background-position: center center; + image: none; +} + +#Header QRadioButton:hover { + background-color: rgba(255, 255, 255, 10); +} + +#Header QRadioButton:checked { + background-color: rgba(255, 255, 255, 20); + border-bottom: 3px solid "lightblue"; +} + +#Body { + padding: 0px; + border: 1px solid #333; + background: #444; +} + +#Body QWidget { + background: #444; +} + +#Header #ArtistTab { + background-image: url("img/tab-home.png"); +} + +#Header #TerminalTab { + background-image: url("img/tab-terminal.png"); +} + +#Header #OverviewTab { + background-image: url("img/tab-overview.png"); +} + +#ButtonWithMenu { + background: #555; + border: 1px solid #fff; + border-radius: 4px; + font-family: "FontAwesome"; + font-size: 11pt; + color: white; +} + +#ButtonWithMenu:pressed { + background: #777; +} + +#ButtonWithMenu:hover { + color: white; + background: #666; +} +#ButtonWithMenu:disabled { + background: #666; + color: #999; + border: 1px solid #999; +} + +#FooterSpacer, #FooterInfo, #HeaderSpacer { + background: transparent; +} + +#Footer { + background: #555; + min-height: 43px; +} + +#Footer[success="1"] { + background: #458056 +} + +#Footer[success="0"] { + background-color: #AA5050 +} + +#Footer QPushButton { + background: #555; + border: 1px solid #aaa; + border-radius: 4px; + font-family: "FontAwesome"; + font-size: 11pt; + color: white; + padding: 0px; +} + +#Footer QPushButton:pressed:hover { + color: #3784c5; + background: #444; +} + +#Footer QPushButton:hover { + background: #505050; + border: 2px solid #3784c5; +} + +#Footer QPushButton:disabled { + border: 1px solid #888; + background: #666; + color: #999; +} + +#ClosingPlaceholder { + background: rgba(0, 0, 0, 50); +} + +#CommentIntentWidget { + background: transparent; +} + +#CommentBox, #CommentPlaceholder { + font-family: "Open Sans"; + font-size: 8pt; + padding: 5px; + background: #444; +} + +#CommentBox { + selection-background-color: #222; +} + +#CommentBox:disabled, #CommentPlaceholder:disabled, #IntentBox:disabled { + background: #555; +} + +#CommentPlaceholder { + color: #888 +} + +#IntentBox { + background: #444; + font-size: 8pt; + padding: 5px; + min-width: 75px; + color: #EEE; +} + +#IntentBox::drop-down:button { + border: 0px; + background: transparent; +} + +#IntentBox::down-arrow { + image: url("/img/down_arrow.png"); +} + +#IntentBox::down-arrow:disabled { + image: url(); +} + +#TerminalView { + background-color: transparent; +} + +#TerminalView:item { + background-color: transparent; +} + +#TerminalView:hover { + background-color: transparent; +} + +#TerminalView:selected { + background-color: transparent; +} + +#TerminalView:item:hover { + color: #ffffff; +} + +#TerminalView:item:selected { + color: #eeeeee; +} + +#TerminalView QTextEdit { + padding:3px; + color: #aaa; + border-radius: 7px; + border-color: #222; + border-style: solid; + border-width: 2px; + background-color: #333; +} + +#TerminalView QTextEdit:hover { + background-color: #353535; +} + +#TerminalView QTextEdit:selected { + background-color: #303030; +} + +#ExpandableWidgetContent { + border: none; + background-color: #232323; + color:#eeeeee; +} + +#EllidableLabel { + font-size: 16pt; + font-weight: normal; +} + +#PerspectiveScrollContent { + border: 1px solid #333; + border-radius: 0px; +} + +#PerspectiveWidgetContent{ + padding: 0px; +} + +#PerspectiveLabel { + background-color: transparent; + border: none; +} + +#PerspectiveIndicator { + font-size: 16pt; + font-weight: normal; + padding: 5px; + background-color: #ffffff; + color: #333333; +} + +#PerspectiveIndicator[state="warning"] { + background-color: #ff9900; + color: #ffffff; +} + +#PerspectiveIndicator[state="active"] { + background-color: #99CEEE; + color: #ffffff; +} + +#PerspectiveIndicator[state="error"] { + background-color: #cc4a4a; + color: #ffffff; +} + +#PerspectiveIndicator[state="ok"] { + background-color: #69a567; + color: #ffffff; +} + +#ExpandableHeader { + background-color: transparent; + margin: 0px; + padding: 0px; + border-radius: 0px; + border: none; +} + +#ExpandableHeader QWidget { + color: #ddd; +} + +#ExpandableHeader QWidget:hover { + color: #fff; +} + +#TerminalFilerBtn { + /* font: %(font_size_pt)spt; */ + font-family: "FontAwesome"; + text-align: center; + background-color: transparent; + border-width: 1px; + border-color: #777777; + border-style: none; + padding: 0px; + border-radius: 3px; +} + +#TerminalFilerBtn[type="info"]:checked {color: rgb(255, 255, 255);} +#TerminalFilerBtn[type="info"] {color: rgba(255, 255, 255, 63);} + +#TerminalFilerBtn[type="error"]:checked {color: rgb(255, 74, 74);} +#TerminalFilerBtn[type="error"] {color: rgba(255, 74, 74, 63);} + +#TerminalFilerBtn[type="log_debug"]:checked {color: rgb(255, 102, 232);} +#TerminalFilerBtn[type="log_debug"] {color: rgba(255, 102, 232, 63);} + +#TerminalFilerBtn[type="log_info"]:checked {color: rgb(102, 171, 255);} +#TerminalFilerBtn[type="log_info"] {color: rgba(102, 171, 255, 63);} + +#TerminalFilerBtn[type="log_warning"]:checked {color: rgb(255, 186, 102);} +#TerminalFilerBtn[type="log_warning"] {color: rgba(255, 186, 102, 63);} + +#TerminalFilerBtn[type="log_error"]:checked {color: rgb(255, 77, 88);} +#TerminalFilerBtn[type="log_error"] {color: rgba(255, 77, 88, 63);} + +#TerminalFilerBtn[type="log_critical"]:checked {color: rgb(255, 79, 117);} +#TerminalFilerBtn[type="log_critical"] {color: rgba(255, 79, 117, 63);} diff --git a/pype/tools/pyblish_pype/app.py b/pype/tools/pyblish_pype/app.py new file mode 100644 index 0000000000..8b77d2f93d --- /dev/null +++ b/pype/tools/pyblish_pype/app.py @@ -0,0 +1,104 @@ +from __future__ import print_function + +import contextlib +import os +import sys + +from . import compat, control, settings, util, window +from .vendor.Qt import QtCore, QtGui, QtWidgets + +self = sys.modules[__name__] + +# Maintain reference to currently opened window +self._window = None + + +@contextlib.contextmanager +def application(): + app = QtWidgets.QApplication.instance() + + if not app: + print("Starting new QApplication..") + app = QtWidgets.QApplication(sys.argv) + yield app + app.exec_() + else: + print("Using existing QApplication..") + yield app + if os.environ.get("PYBLISH_GUI_ALWAYS_EXEC"): + app.exec_() + + +def install_translator(app): + translator = QtCore.QTranslator(app) + translator.load(QtCore.QLocale.system(), "i18n/", + directory=util.root) + app.installTranslator(translator) + print("Installed translator") + + +def install_fonts(): + database = QtGui.QFontDatabase() + fonts = [ + "opensans/OpenSans-Bold.ttf", + "opensans/OpenSans-BoldItalic.ttf", + "opensans/OpenSans-ExtraBold.ttf", + "opensans/OpenSans-ExtraBoldItalic.ttf", + "opensans/OpenSans-Italic.ttf", + "opensans/OpenSans-Light.ttf", + "opensans/OpenSans-LightItalic.ttf", + "opensans/OpenSans-Regular.ttf", + "opensans/OpenSans-Semibold.ttf", + "opensans/OpenSans-SemiboldItalic.ttf", + "fontawesome/fontawesome-webfont.ttf" + ] + + for font in fonts: + path = util.get_asset("font", font) + + # TODO(marcus): Check if they are already installed first. + # In hosts, this will be called each time the GUI is shown, + # potentially installing a font each time. + if database.addApplicationFont(path) < 0: + sys.stderr.write("Could not install %s\n" % path) + else: + sys.stdout.write("Installed %s\n" % font) + + +def on_destroyed(): + """Remove internal reference to window on window destroyed""" + self._window = None + + +def show(parent=None): + with open(util.get_asset("app.css")) as f: + css = f.read() + + # Make relative paths absolute + root = util.get_asset("").replace("\\", "/") + css = css.replace("url(\"", "url(\"%s" % root) + + with application() as app: + compat.init() + + install_fonts() + install_translator(app) + + ctrl = control.Controller() + + if self._window is None: + self._window = window.Window(ctrl, parent) + self._window.destroyed.connect(on_destroyed) + + self._window.show() + self._window.activateWindow() + self._window.resize(*settings.WindowSize) + self._window.setWindowTitle(settings.WindowTitle) + + font = QtGui.QFont("Open Sans", 8, QtGui.QFont.Normal) + self._window.setFont(font) + self._window.setStyleSheet(css) + + self._window.reset() + + return self._window diff --git a/pype/tools/pyblish_pype/awesome.py b/pype/tools/pyblish_pype/awesome.py new file mode 100644 index 0000000000..c70f5b1064 --- /dev/null +++ b/pype/tools/pyblish_pype/awesome.py @@ -0,0 +1,733 @@ + +tags = { + "500px": u"\uf26e", + "adjust": u"\uf042", + "adn": u"\uf170", + "align-center": u"\uf037", + "align-justify": u"\uf039", + "align-left": u"\uf036", + "align-right": u"\uf038", + "amazon": u"\uf270", + "ambulance": u"\uf0f9", + "american-sign-language-interpreting": u"\uf2a3", + "anchor": u"\uf13d", + "android": u"\uf17b", + "angellist": u"\uf209", + "angle-double-down": u"\uf103", + "angle-double-left": u"\uf100", + "angle-double-right": u"\uf101", + "angle-double-up": u"\uf102", + "angle-down": u"\uf107", + "angle-left": u"\uf104", + "angle-right": u"\uf105", + "angle-up": u"\uf106", + "apple": u"\uf179", + "archive": u"\uf187", + "area-chart": u"\uf1fe", + "arrow-circle-down": u"\uf0ab", + "arrow-circle-left": u"\uf0a8", + "arrow-circle-o-down": u"\uf01a", + "arrow-circle-o-left": u"\uf190", + "arrow-circle-o-right": u"\uf18e", + "arrow-circle-o-up": u"\uf01b", + "arrow-circle-right": u"\uf0a9", + "arrow-circle-up": u"\uf0aa", + "arrow-down": u"\uf063", + "arrow-left": u"\uf060", + "arrow-right": u"\uf061", + "arrow-up": u"\uf062", + "arrows": u"\uf047", + "arrows-alt": u"\uf0b2", + "arrows-h": u"\uf07e", + "arrows-v": u"\uf07d", + "asl-interpreting (alias)": u"\uf2a3", + "assistive-listening-systems": u"\uf2a2", + "asterisk": u"\uf069", + "at": u"\uf1fa", + "audio-description": u"\uf29e", + "automobile (alias)": u"\uf1b9", + "backward": u"\uf04a", + "balance-scale": u"\uf24e", + "ban": u"\uf05e", + "bank (alias)": u"\uf19c", + "bar-chart": u"\uf080", + "bar-chart-o (alias)": u"\uf080", + "barcode": u"\uf02a", + "bars": u"\uf0c9", + "battery-0 (alias)": u"\uf244", + "battery-1 (alias)": u"\uf243", + "battery-2 (alias)": u"\uf242", + "battery-3 (alias)": u"\uf241", + "battery-4 (alias)": u"\uf240", + "battery-empty": u"\uf244", + "battery-full": u"\uf240", + "battery-half": u"\uf242", + "battery-quarter": u"\uf243", + "battery-three-quarters": u"\uf241", + "bed": u"\uf236", + "beer": u"\uf0fc", + "behance": u"\uf1b4", + "behance-square": u"\uf1b5", + "bell": u"\uf0f3", + "bell-o": u"\uf0a2", + "bell-slash": u"\uf1f6", + "bell-slash-o": u"\uf1f7", + "bicycle": u"\uf206", + "binoculars": u"\uf1e5", + "birthday-cake": u"\uf1fd", + "bitbucket": u"\uf171", + "bitbucket-square": u"\uf172", + "bitcoin (alias)": u"\uf15a", + "black-tie": u"\uf27e", + "blind": u"\uf29d", + "bluetooth": u"\uf293", + "bluetooth-b": u"\uf294", + "bold": u"\uf032", + "bolt": u"\uf0e7", + "bomb": u"\uf1e2", + "book": u"\uf02d", + "bookmark": u"\uf02e", + "bookmark-o": u"\uf097", + "braille": u"\uf2a1", + "briefcase": u"\uf0b1", + "btc": u"\uf15a", + "bug": u"\uf188", + "building": u"\uf1ad", + "building-o": u"\uf0f7", + "bullhorn": u"\uf0a1", + "bullseye": u"\uf140", + "bus": u"\uf207", + "buysellads": u"\uf20d", + "cab (alias)": u"\uf1ba", + "calculator": u"\uf1ec", + "calendar": u"\uf073", + "calendar-check-o": u"\uf274", + "calendar-minus-o": u"\uf272", + "calendar-o": u"\uf133", + "calendar-plus-o": u"\uf271", + "calendar-times-o": u"\uf273", + "camera": u"\uf030", + "camera-retro": u"\uf083", + "car": u"\uf1b9", + "caret-down": u"\uf0d7", + "caret-left": u"\uf0d9", + "caret-right": u"\uf0da", + "caret-square-o-down": u"\uf150", + "caret-square-o-left": u"\uf191", + "caret-square-o-right": u"\uf152", + "caret-square-o-up": u"\uf151", + "caret-up": u"\uf0d8", + "cart-arrow-down": u"\uf218", + "cart-plus": u"\uf217", + "cc": u"\uf20a", + "cc-amex": u"\uf1f3", + "cc-diners-club": u"\uf24c", + "cc-discover": u"\uf1f2", + "cc-jcb": u"\uf24b", + "cc-mastercard": u"\uf1f1", + "cc-paypal": u"\uf1f4", + "cc-stripe": u"\uf1f5", + "cc-visa": u"\uf1f0", + "certificate": u"\uf0a3", + "chain (alias)": u"\uf0c1", + "chain-broken": u"\uf127", + "check": u"\uf00c", + "check-circle": u"\uf058", + "check-circle-o": u"\uf05d", + "check-square": u"\uf14a", + "check-square-o": u"\uf046", + "chevron-circle-down": u"\uf13a", + "chevron-circle-left": u"\uf137", + "chevron-circle-right": u"\uf138", + "chevron-circle-up": u"\uf139", + "chevron-down": u"\uf078", + "chevron-left": u"\uf053", + "chevron-right": u"\uf054", + "chevron-up": u"\uf077", + "child": u"\uf1ae", + "chrome": u"\uf268", + "circle": u"\uf111", + "circle-o": u"\uf10c", + "circle-o-notch": u"\uf1ce", + "circle-thin": u"\uf1db", + "clipboard": u"\uf0ea", + "clock-o": u"\uf017", + "clone": u"\uf24d", + "close (alias)": u"\uf00d", + "cloud": u"\uf0c2", + "cloud-download": u"\uf0ed", + "cloud-upload": u"\uf0ee", + "cny (alias)": u"\uf157", + "code": u"\uf121", + "code-fork": u"\uf126", + "codepen": u"\uf1cb", + "codiepie": u"\uf284", + "coffee": u"\uf0f4", + "cog": u"\uf013", + "cogs": u"\uf085", + "columns": u"\uf0db", + "comment": u"\uf075", + "comment-o": u"\uf0e5", + "commenting": u"\uf27a", + "commenting-o": u"\uf27b", + "comments": u"\uf086", + "comments-o": u"\uf0e6", + "compass": u"\uf14e", + "compress": u"\uf066", + "connectdevelop": u"\uf20e", + "contao": u"\uf26d", + "copy (alias)": u"\uf0c5", + "copyright": u"\uf1f9", + "creative-commons": u"\uf25e", + "credit-card": u"\uf09d", + "credit-card-alt": u"\uf283", + "crop": u"\uf125", + "crosshairs": u"\uf05b", + "css3": u"\uf13c", + "cube": u"\uf1b2", + "cubes": u"\uf1b3", + "cut (alias)": u"\uf0c4", + "cutlery": u"\uf0f5", + "dashboard (alias)": u"\uf0e4", + "dashcube": u"\uf210", + "database": u"\uf1c0", + "deaf": u"\uf2a4", + "deafness (alias)": u"\uf2a4", + "dedent (alias)": u"\uf03b", + "delicious": u"\uf1a5", + "desktop": u"\uf108", + "deviantart": u"\uf1bd", + "diamond": u"\uf219", + "digg": u"\uf1a6", + "dollar (alias)": u"\uf155", + "dot-circle-o": u"\uf192", + "download": u"\uf019", + "dribbble": u"\uf17d", + "dropbox": u"\uf16b", + "drupal": u"\uf1a9", + "edge": u"\uf282", + "edit (alias)": u"\uf044", + "eject": u"\uf052", + "ellipsis-h": u"\uf141", + "ellipsis-v": u"\uf142", + "empire": u"\uf1d1", + "envelope": u"\uf0e0", + "envelope-o": u"\uf003", + "envelope-square": u"\uf199", + "envira": u"\uf299", + "eraser": u"\uf12d", + "eur": u"\uf153", + "euro (alias)": u"\uf153", + "exchange": u"\uf0ec", + "exclamation": u"\uf12a", + "exclamation-circle": u"\uf06a", + "exclamation-triangle": u"\uf071", + "expand": u"\uf065", + "expeditedssl": u"\uf23e", + "external-link": u"\uf08e", + "external-link-square": u"\uf14c", + "eye": u"\uf06e", + "eye-slash": u"\uf070", + "eyedropper": u"\uf1fb", + "fa (alias)": u"\uf2b4", + "facebook": u"\uf09a", + "facebook-f (alias)": u"\uf09a", + "facebook-official": u"\uf230", + "facebook-square": u"\uf082", + "fast-backward": u"\uf049", + "fast-forward": u"\uf050", + "fax": u"\uf1ac", + "feed (alias)": u"\uf09e", + "female": u"\uf182", + "fighter-jet": u"\uf0fb", + "file": u"\uf15b", + "file-archive-o": u"\uf1c6", + "file-audio-o": u"\uf1c7", + "file-code-o": u"\uf1c9", + "file-excel-o": u"\uf1c3", + "file-image-o": u"\uf1c5", + "file-movie-o (alias)": u"\uf1c8", + "file-o": u"\uf016", + "file-pdf-o": u"\uf1c1", + "file-photo-o (alias)": u"\uf1c5", + "file-picture-o (alias)": u"\uf1c5", + "file-powerpoint-o": u"\uf1c4", + "file-sound-o (alias)": u"\uf1c7", + "file-text": u"\uf15c", + "file-text-o": u"\uf0f6", + "file-video-o": u"\uf1c8", + "file-word-o": u"\uf1c2", + "file-zip-o (alias)": u"\uf1c6", + "files-o": u"\uf0c5", + "film": u"\uf008", + "filter": u"\uf0b0", + "fire": u"\uf06d", + "fire-extinguisher": u"\uf134", + "firefox": u"\uf269", + "first-order": u"\uf2b0", + "flag": u"\uf024", + "flag-checkered": u"\uf11e", + "flag-o": u"\uf11d", + "flash (alias)": u"\uf0e7", + "flask": u"\uf0c3", + "flickr": u"\uf16e", + "floppy-o": u"\uf0c7", + "folder": u"\uf07b", + "folder-o": u"\uf114", + "folder-open": u"\uf07c", + "folder-open-o": u"\uf115", + "font": u"\uf031", + "font-awesome": u"\uf2b4", + "fonticons": u"\uf280", + "fort-awesome": u"\uf286", + "forumbee": u"\uf211", + "forward": u"\uf04e", + "foursquare": u"\uf180", + "frown-o": u"\uf119", + "futbol-o": u"\uf1e3", + "gamepad": u"\uf11b", + "gavel": u"\uf0e3", + "gbp": u"\uf154", + "ge (alias)": u"\uf1d1", + "gear (alias)": u"\uf013", + "gears (alias)": u"\uf085", + "genderless": u"\uf22d", + "get-pocket": u"\uf265", + "gg": u"\uf260", + "gg-circle": u"\uf261", + "gift": u"\uf06b", + "git": u"\uf1d3", + "git-square": u"\uf1d2", + "github": u"\uf09b", + "github-alt": u"\uf113", + "github-square": u"\uf092", + "gitlab": u"\uf296", + "gittip (alias)": u"\uf184", + "glass": u"\uf000", + "glide": u"\uf2a5", + "glide-g": u"\uf2a6", + "globe": u"\uf0ac", + "google": u"\uf1a0", + "google-plus": u"\uf0d5", + "google-plus-circle (alias)": u"\uf2b3", + "google-plus-official": u"\uf2b3", + "google-plus-square": u"\uf0d4", + "google-wallet": u"\uf1ee", + "graduation-cap": u"\uf19d", + "gratipay": u"\uf184", + "group (alias)": u"\uf0c0", + "h-square": u"\uf0fd", + "hacker-news": u"\uf1d4", + "hand-grab-o (alias)": u"\uf255", + "hand-lizard-o": u"\uf258", + "hand-o-down": u"\uf0a7", + "hand-o-left": u"\uf0a5", + "hand-o-right": u"\uf0a4", + "hand-o-up": u"\uf0a6", + "hand-paper-o": u"\uf256", + "hand-peace-o": u"\uf25b", + "hand-pointer-o": u"\uf25a", + "hand-rock-o": u"\uf255", + "hand-scissors-o": u"\uf257", + "hand-spock-o": u"\uf259", + "hand-stop-o (alias)": u"\uf256", + "hard-of-hearing (alias)": u"\uf2a4", + "hashtag": u"\uf292", + "hdd-o": u"\uf0a0", + "header": u"\uf1dc", + "headphones": u"\uf025", + "heart": u"\uf004", + "heart-o": u"\uf08a", + "heartbeat": u"\uf21e", + "history": u"\uf1da", + "home": u"\uf015", + "hospital-o": u"\uf0f8", + "hotel (alias)": u"\uf236", + "hourglass": u"\uf254", + "hourglass-1 (alias)": u"\uf251", + "hourglass-2 (alias)": u"\uf252", + "hourglass-3 (alias)": u"\uf253", + "hourglass-end": u"\uf253", + "hourglass-half": u"\uf252", + "hourglass-o": u"\uf250", + "hourglass-start": u"\uf251", + "houzz": u"\uf27c", + "html5": u"\uf13b", + "i-cursor": u"\uf246", + "ils": u"\uf20b", + "image (alias)": u"\uf03e", + "inbox": u"\uf01c", + "indent": u"\uf03c", + "industry": u"\uf275", + "info": u"\uf129", + "info-circle": u"\uf05a", + "inr": u"\uf156", + "instagram": u"\uf16d", + "institution (alias)": u"\uf19c", + "internet-explorer": u"\uf26b", + "intersex (alias)": u"\uf224", + "ioxhost": u"\uf208", + "italic": u"\uf033", + "joomla": u"\uf1aa", + "jpy": u"\uf157", + "jsfiddle": u"\uf1cc", + "key": u"\uf084", + "keyboard-o": u"\uf11c", + "krw": u"\uf159", + "language": u"\uf1ab", + "laptop": u"\uf109", + "lastfm": u"\uf202", + "lastfm-square": u"\uf203", + "leaf": u"\uf06c", + "leanpub": u"\uf212", + "legal (alias)": u"\uf0e3", + "lemon-o": u"\uf094", + "level-down": u"\uf149", + "level-up": u"\uf148", + "life-bouy (alias)": u"\uf1cd", + "life-buoy (alias)": u"\uf1cd", + "life-ring": u"\uf1cd", + "life-saver (alias)": u"\uf1cd", + "lightbulb-o": u"\uf0eb", + "line-chart": u"\uf201", + "link": u"\uf0c1", + "linkedin": u"\uf0e1", + "linkedin-square": u"\uf08c", + "linux": u"\uf17c", + "list": u"\uf03a", + "list-alt": u"\uf022", + "list-ol": u"\uf0cb", + "list-ul": u"\uf0ca", + "location-arrow": u"\uf124", + "lock": u"\uf023", + "long-arrow-down": u"\uf175", + "long-arrow-left": u"\uf177", + "long-arrow-right": u"\uf178", + "long-arrow-up": u"\uf176", + "low-vision": u"\uf2a8", + "magic": u"\uf0d0", + "magnet": u"\uf076", + "mail-forward (alias)": u"\uf064", + "mail-reply (alias)": u"\uf112", + "mail-reply-all (alias)": u"\uf122", + "male": u"\uf183", + "map": u"\uf279", + "map-marker": u"\uf041", + "map-o": u"\uf278", + "map-pin": u"\uf276", + "map-signs": u"\uf277", + "mars": u"\uf222", + "mars-double": u"\uf227", + "mars-stroke": u"\uf229", + "mars-stroke-h": u"\uf22b", + "mars-stroke-v": u"\uf22a", + "maxcdn": u"\uf136", + "meanpath": u"\uf20c", + "medium": u"\uf23a", + "medkit": u"\uf0fa", + "meh-o": u"\uf11a", + "mercury": u"\uf223", + "microphone": u"\uf130", + "microphone-slash": u"\uf131", + "minus": u"\uf068", + "minus-circle": u"\uf056", + "minus-square": u"\uf146", + "minus-square-o": u"\uf147", + "mixcloud": u"\uf289", + "mobile": u"\uf10b", + "mobile-phone (alias)": u"\uf10b", + "modx": u"\uf285", + "money": u"\uf0d6", + "moon-o": u"\uf186", + "mortar-board (alias)": u"\uf19d", + "motorcycle": u"\uf21c", + "mouse-pointer": u"\uf245", + "music": u"\uf001", + "navicon (alias)": u"\uf0c9", + "neuter": u"\uf22c", + "newspaper-o": u"\uf1ea", + "object-group": u"\uf247", + "object-ungroup": u"\uf248", + "odnoklassniki": u"\uf263", + "odnoklassniki-square": u"\uf264", + "opencart": u"\uf23d", + "openid": u"\uf19b", + "opera": u"\uf26a", + "optin-monster": u"\uf23c", + "outdent": u"\uf03b", + "pagelines": u"\uf18c", + "paint-brush": u"\uf1fc", + "paper-plane": u"\uf1d8", + "paper-plane-o": u"\uf1d9", + "paperclip": u"\uf0c6", + "paragraph": u"\uf1dd", + "paste (alias)": u"\uf0ea", + "pause": u"\uf04c", + "pause-circle": u"\uf28b", + "pause-circle-o": u"\uf28c", + "paw": u"\uf1b0", + "paypal": u"\uf1ed", + "pencil": u"\uf040", + "pencil-square": u"\uf14b", + "pencil-square-o": u"\uf044", + "percent": u"\uf295", + "phone": u"\uf095", + "phone-square": u"\uf098", + "photo (alias)": u"\uf03e", + "picture-o": u"\uf03e", + "pie-chart": u"\uf200", + "pied-piper": u"\uf2ae", + "pied-piper-alt": u"\uf1a8", + "pied-piper-pp": u"\uf1a7", + "pinterest": u"\uf0d2", + "pinterest-p": u"\uf231", + "pinterest-square": u"\uf0d3", + "plane": u"\uf072", + "play": u"\uf04b", + "play-circle": u"\uf144", + "play-circle-o": u"\uf01d", + "plug": u"\uf1e6", + "plus": u"\uf067", + "plus-circle": u"\uf055", + "plus-square": u"\uf0fe", + "plus-square-o": u"\uf196", + "power-off": u"\uf011", + "print": u"\uf02f", + "product-hunt": u"\uf288", + "puzzle-piece": u"\uf12e", + "qq": u"\uf1d6", + "qrcode": u"\uf029", + "question": u"\uf128", + "question-circle": u"\uf059", + "question-circle-o": u"\uf29c", + "quote-left": u"\uf10d", + "quote-right": u"\uf10e", + "ra (alias)": u"\uf1d0", + "random": u"\uf074", + "rebel": u"\uf1d0", + "recycle": u"\uf1b8", + "reddit": u"\uf1a1", + "reddit-alien": u"\uf281", + "reddit-square": u"\uf1a2", + "refresh": u"\uf021", + "registered": u"\uf25d", + "remove (alias)": u"\uf00d", + "renren": u"\uf18b", + "reorder (alias)": u"\uf0c9", + "repeat": u"\uf01e", + "reply": u"\uf112", + "reply-all": u"\uf122", + "resistance (alias)": u"\uf1d0", + "retweet": u"\uf079", + "rmb (alias)": u"\uf157", + "road": u"\uf018", + "rocket": u"\uf135", + "rotate-left (alias)": u"\uf0e2", + "rotate-right (alias)": u"\uf01e", + "rouble (alias)": u"\uf158", + "rss": u"\uf09e", + "rss-square": u"\uf143", + "rub": u"\uf158", + "ruble (alias)": u"\uf158", + "rupee (alias)": u"\uf156", + "safari": u"\uf267", + "save (alias)": u"\uf0c7", + "scissors": u"\uf0c4", + "scribd": u"\uf28a", + "search": u"\uf002", + "search-minus": u"\uf010", + "search-plus": u"\uf00e", + "sellsy": u"\uf213", + "send (alias)": u"\uf1d8", + "send-o (alias)": u"\uf1d9", + "server": u"\uf233", + "share": u"\uf064", + "share-alt": u"\uf1e0", + "share-alt-square": u"\uf1e1", + "share-square": u"\uf14d", + "share-square-o": u"\uf045", + "shekel (alias)": u"\uf20b", + "sheqel (alias)": u"\uf20b", + "shield": u"\uf132", + "ship": u"\uf21a", + "shirtsinbulk": u"\uf214", + "shopping-bag": u"\uf290", + "shopping-basket": u"\uf291", + "shopping-cart": u"\uf07a", + "sign-in": u"\uf090", + "sign-language": u"\uf2a7", + "sign-out": u"\uf08b", + "signal": u"\uf012", + "signing (alias)": u"\uf2a7", + "simplybuilt": u"\uf215", + "sitemap": u"\uf0e8", + "skyatlas": u"\uf216", + "skype": u"\uf17e", + "slack": u"\uf198", + "sliders": u"\uf1de", + "slideshare": u"\uf1e7", + "smile-o": u"\uf118", + "snapchat": u"\uf2ab", + "snapchat-ghost": u"\uf2ac", + "snapchat-square": u"\uf2ad", + "soccer-ball-o (alias)": u"\uf1e3", + "sort": u"\uf0dc", + "sort-alpha-asc": u"\uf15d", + "sort-alpha-desc": u"\uf15e", + "sort-amount-asc": u"\uf160", + "sort-amount-desc": u"\uf161", + "sort-asc": u"\uf0de", + "sort-desc": u"\uf0dd", + "sort-down (alias)": u"\uf0dd", + "sort-numeric-asc": u"\uf162", + "sort-numeric-desc": u"\uf163", + "sort-up (alias)": u"\uf0de", + "soundcloud": u"\uf1be", + "space-shuttle": u"\uf197", + "spinner": u"\uf110", + "spoon": u"\uf1b1", + "spotify": u"\uf1bc", + "square": u"\uf0c8", + "square-o": u"\uf096", + "stack-exchange": u"\uf18d", + "stack-overflow": u"\uf16c", + "star": u"\uf005", + "star-half": u"\uf089", + "star-half-empty (alias)": u"\uf123", + "star-half-full (alias)": u"\uf123", + "star-half-o": u"\uf123", + "star-o": u"\uf006", + "steam": u"\uf1b6", + "steam-square": u"\uf1b7", + "step-backward": u"\uf048", + "step-forward": u"\uf051", + "stethoscope": u"\uf0f1", + "sticky-note": u"\uf249", + "sticky-note-o": u"\uf24a", + "stop": u"\uf04d", + "stop-circle": u"\uf28d", + "stop-circle-o": u"\uf28e", + "street-view": u"\uf21d", + "strikethrough": u"\uf0cc", + "stumbleupon": u"\uf1a4", + "stumbleupon-circle": u"\uf1a3", + "subscript": u"\uf12c", + "subway": u"\uf239", + "suitcase": u"\uf0f2", + "sun-o": u"\uf185", + "superscript": u"\uf12b", + "support (alias)": u"\uf1cd", + "table": u"\uf0ce", + "tablet": u"\uf10a", + "tachometer": u"\uf0e4", + "tag": u"\uf02b", + "tags": u"\uf02c", + "tasks": u"\uf0ae", + "taxi": u"\uf1ba", + "television": u"\uf26c", + "tencent-weibo": u"\uf1d5", + "terminal": u"\uf120", + "text-height": u"\uf034", + "text-width": u"\uf035", + "th": u"\uf00a", + "th-large": u"\uf009", + "th-list": u"\uf00b", + "themeisle": u"\uf2b2", + "thumb-tack": u"\uf08d", + "thumbs-down": u"\uf165", + "thumbs-o-down": u"\uf088", + "thumbs-o-up": u"\uf087", + "thumbs-up": u"\uf164", + "ticket": u"\uf145", + "times": u"\uf00d", + "times-circle": u"\uf057", + "times-circle-o": u"\uf05c", + "tint": u"\uf043", + "toggle-down (alias)": u"\uf150", + "toggle-left (alias)": u"\uf191", + "toggle-off": u"\uf204", + "toggle-on": u"\uf205", + "toggle-right (alias)": u"\uf152", + "toggle-up (alias)": u"\uf151", + "trademark": u"\uf25c", + "train": u"\uf238", + "transgender": u"\uf224", + "transgender-alt": u"\uf225", + "trash": u"\uf1f8", + "trash-o": u"\uf014", + "tree": u"\uf1bb", + "trello": u"\uf181", + "tripadvisor": u"\uf262", + "trophy": u"\uf091", + "truck": u"\uf0d1", + "try": u"\uf195", + "tty": u"\uf1e4", + "tumblr": u"\uf173", + "tumblr-square": u"\uf174", + "turkish-lira (alias)": u"\uf195", + "tv (alias)": u"\uf26c", + "twitch": u"\uf1e8", + "twitter": u"\uf099", + "twitter-square": u"\uf081", + "umbrella": u"\uf0e9", + "underline": u"\uf0cd", + "undo": u"\uf0e2", + "universal-access": u"\uf29a", + "university": u"\uf19c", + "unlink (alias)": u"\uf127", + "unlock": u"\uf09c", + "unlock-alt": u"\uf13e", + "unsorted (alias)": u"\uf0dc", + "upload": u"\uf093", + "usb": u"\uf287", + "usd": u"\uf155", + "user": u"\uf007", + "user-md": u"\uf0f0", + "user-plus": u"\uf234", + "user-secret": u"\uf21b", + "user-times": u"\uf235", + "users": u"\uf0c0", + "venus": u"\uf221", + "venus-double": u"\uf226", + "venus-mars": u"\uf228", + "viacoin": u"\uf237", + "viadeo": u"\uf2a9", + "viadeo-square": u"\uf2aa", + "video-camera": u"\uf03d", + "vimeo": u"\uf27d", + "vimeo-square": u"\uf194", + "vine": u"\uf1ca", + "vk": u"\uf189", + "volume-control-phone": u"\uf2a0", + "volume-down": u"\uf027", + "volume-off": u"\uf026", + "volume-up": u"\uf028", + "warning (alias)": u"\uf071", + "wechat (alias)": u"\uf1d7", + "weibo": u"\uf18a", + "weixin": u"\uf1d7", + "whatsapp": u"\uf232", + "wheelchair": u"\uf193", + "wheelchair-alt": u"\uf29b", + "wifi": u"\uf1eb", + "wikipedia-w": u"\uf266", + "windows": u"\uf17a", + "won (alias)": u"\uf159", + "wordpress": u"\uf19a", + "wpbeginner": u"\uf297", + "wpforms": u"\uf298", + "wrench": u"\uf0ad", + "xing": u"\uf168", + "xing-square": u"\uf169", + "y-combinator": u"\uf23b", + "y-combinator-square (alias)": u"\uf1d4", + "yahoo": u"\uf19e", + "yc (alias)": u"\uf23b", + "yc-square (alias)": u"\uf1d4", + "yelp": u"\uf1e9", + "yen (alias)": u"\uf157", + "yoast": u"\uf2b1", + "youtube": u"\uf167", + "youtube-play": u"\uf16a", + "youtube-square": u"\uf166" +} diff --git a/pype/tools/pyblish_pype/compat.py b/pype/tools/pyblish_pype/compat.py new file mode 100644 index 0000000000..bb520d65f5 --- /dev/null +++ b/pype/tools/pyblish_pype/compat.py @@ -0,0 +1,14 @@ +import os + + +def __windows_taskbar_compat(): + """Enable icon and taskbar grouping for Windows 7+""" + + import ctypes + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( + u"pyblish_pype") + + +def init(): + if os.name == "nt": + __windows_taskbar_compat() diff --git a/pype/tools/pyblish_pype/constants.py b/pype/tools/pyblish_pype/constants.py new file mode 100644 index 0000000000..077d93eec0 --- /dev/null +++ b/pype/tools/pyblish_pype/constants.py @@ -0,0 +1,95 @@ +from .vendor.Qt import QtCore + + +def flags(*args, **kwargs): + type_name = kwargs.pop("type_name", "Flags") + with_base = kwargs.pop("with_base", False) + enums = {} + for idx, attr_name in enumerate(args): + if with_base: + if idx == 0: + enums[attr_name] = 0 + continue + idx -= 1 + enums[attr_name] = 2**idx + + for attr_name, value in kwargs.items(): + enums[attr_name] = value + return type(type_name, (), enums) + + +def roles(*args, **kwargs): + type_name = kwargs.pop("type_name", "Roles") + enums = {} + for attr_name, value in kwargs.items(): + enums[attr_name] = value + + offset = 0 + for idx, attr_name in enumerate(args): + _idx = idx + QtCore.Qt.UserRole + offset + while _idx in enums.values(): + offset += 1 + _idx = idx + offset + + enums[attr_name] = _idx + + return type(type_name, (), enums) + + +Roles = roles( + "ObjectIdRole", + "ObjectUIdRole", + "TypeRole", + "PublishFlagsRole", + "LogRecordsRole", + + "IsOptionalRole", + "IsEnabledRole", + + "FamiliesRole", + + "DocstringRole", + "PathModuleRole", + "PluginActionsVisibleRole", + "PluginValidActionsRole", + "PluginActionProgressRole", + + "TerminalItemTypeRole", + + "IntentItemValue", + + type_name="ModelRoles" +) + +InstanceStates = flags( + "ContextType", + "InProgress", + "HasWarning", + "HasError", + "HasFinished", + type_name="InstanceState" +) + +PluginStates = flags( + "IsCompatible", + "InProgress", + "WasProcessed", + "WasSkipped", + "HasWarning", + "HasError", + type_name="PluginState" +) + +GroupStates = flags( + "HasWarning", + "HasError", + "HasFinished", + type_name="GroupStates" +) + +PluginActionStates = flags( + "InProgress", + "HasFailed", + "HasFinished", + type_name="PluginActionStates" +) diff --git a/pype/tools/pyblish_pype/control.py b/pype/tools/pyblish_pype/control.py new file mode 100644 index 0000000000..e64f3d5bfb --- /dev/null +++ b/pype/tools/pyblish_pype/control.py @@ -0,0 +1,414 @@ +"""The Controller in a Model/View/Controller-based application +The graphical components of Pyblish Lite use this object to perform +publishing. It communicates via the Qt Signals/Slots mechanism +and has no direct connection to any graphics. This is important, +because this is how unittests are able to run without requiring +an active window manager; such as via Travis-CI. +""" +import os +import sys +import traceback + +from .vendor.Qt import QtCore + +import pyblish.api +import pyblish.util +import pyblish.logic +import pyblish.lib +import pyblish.version + +from . import util +from .constants import InstanceStates +try: + from pypeapp.config import get_presets +except Exception: + get_presets = dict + + +class IterationBreak(Exception): + pass + + +class Controller(QtCore.QObject): + # Emitted when the GUI is about to start processing; + # e.g. resetting, validating or publishing. + about_to_process = QtCore.Signal(object, object) + + # ??? Emitted for each process + was_processed = QtCore.Signal(dict) + + # Emmited when reset + # - all data are reset (plugins, processing, pari yielder, etc.) + was_reset = QtCore.Signal() + + # Emmited when previous group changed + passed_group = QtCore.Signal(object) + + # Emmited when want to change state of instances + switch_toggleability = QtCore.Signal(bool) + + # On action finished + was_acted = QtCore.Signal(dict) + + # Emitted when processing has stopped + was_stopped = QtCore.Signal() + + # Emitted when processing has finished + was_finished = QtCore.Signal() + + # Emitted when plugin was skipped + was_skipped = QtCore.Signal(object) + + # store OrderGroups - now it is a singleton + order_groups = util.OrderGroups + + def __init__(self, parent=None): + super(Controller, self).__init__(parent) + self.context = None + self.plugins = {} + self.optional_default = {} + + def reset_variables(self): + # Data internal to the GUI itself + self.is_running = False + self.stopped = False + self.errored = False + + # Active producer of pairs + self.pair_generator = None + # Active pair + self.current_pair = None + + # Orders which changes GUI + # - passing collectors order disables plugin/instance toggle + self.collectors_order = None + self.collect_state = 0 + self.collected = False + + # - passing validators order disables validate button and gives ability + # to know when to stop on validate button press + self.validators_order = None + self.validated = False + + # Get collectors and validators order + self.order_groups.reset() + plugin_groups = self.order_groups.groups() + plugin_groups_keys = list(plugin_groups.keys()) + self.collectors_order = plugin_groups_keys[0] + self.validators_order = self.order_groups.validation_order() + next_group_order = None + if len(plugin_groups_keys) > 1: + next_group_order = plugin_groups_keys[1] + + # This is used to track whether or not to continue + # processing when, for example, validation has failed. + self.processing = { + "stop_on_validation": False, + # Used? + "last_plugin_order": None, + "current_group_order": self.collectors_order, + "next_group_order": next_group_order, + "nextOrder": None, + "ordersWithError": set() + } + + def presets_by_hosts(self): + # Get global filters as base + presets = get_presets().get("plugins", {}) + if not presets: + return {} + + result = presets.get("global", {}).get("filter", {}) + hosts = pyblish.api.registered_hosts() + for host in hosts: + host_presets = presets.get(host, {}).get("filter") + if not host_presets: + continue + + for key, value in host_presets.items(): + if value is None: + if key in result: + result.pop(key) + continue + + result[key] = value + + return result + + def reset_context(self): + self.context = pyblish.api.Context() + + self.context._publish_states = InstanceStates.ContextType + self.context.optional = False + + self.context.data["publish"] = True + self.context.data["label"] = "Context" + self.context.data["name"] = "context" + + self.context.data["host"] = reversed(pyblish.api.registered_hosts()) + self.context.data["port"] = int( + os.environ.get("PYBLISH_CLIENT_PORT", -1) + ) + self.context.data["connectTime"] = pyblish.lib.time(), + self.context.data["pyblishVersion"] = pyblish.version, + self.context.data["pythonVersion"] = sys.version + + self.context.data["icon"] = "book" + + self.context.families = ("__context__",) + + def reset(self): + """Discover plug-ins and run collection.""" + + self.reset_context() + self.reset_variables() + + self.possible_presets = self.presets_by_hosts() + + # Load plugins and set pair generator + self.load_plugins() + self.pair_generator = self._pair_yielder(self.plugins) + + self.was_reset.emit() + + # Process collectors load rest of plugins with collected instances + self.collect() + + def load_plugins(self): + self.test = pyblish.logic.registered_test() + self.optional_default = {} + + plugins = pyblish.api.discover() + + targets = pyblish.logic.registered_targets() or ["default"] + self.plugins = pyblish.logic.plugins_by_targets(plugins, targets) + + def on_published(self): + if self.is_running: + self.is_running = False + self.was_finished.emit() + + def stop(self): + self.stopped = True + + def act(self, plugin, action): + def on_next(): + result = pyblish.plugin.process( + plugin, self.context, None, action.id + ) + self.is_running = False + self.was_acted.emit(result) + + self.is_running = True + util.defer(100, on_next) + + def emit_(self, signal, kwargs): + pyblish.api.emit(signal, **kwargs) + + def _process(self, plugin, instance=None): + """Produce `result` from `plugin` and `instance` + :func:`process` shares state with :func:`_iterator` such that + an instance/plugin pair can be fetched and processed in isolation. + Arguments: + plugin (pyblish.api.Plugin): Produce result using plug-in + instance (optional, pyblish.api.Instance): Process this instance, + if no instance is provided, context is processed. + """ + + self.processing["nextOrder"] = plugin.order + + try: + result = pyblish.plugin.process(plugin, self.context, instance) + # Make note of the order at which the + # potential error error occured. + if result["error"] is not None: + self.processing["ordersWithError"].add(plugin.order) + + except Exception as exc: + raise Exception("Unknown error({}): {}".format( + plugin.__name__, str(exc) + )) + + return result + + def _pair_yielder(self, plugins): + for plugin in plugins: + if ( + self.processing["current_group_order"] is not None + and plugin.order > self.processing["current_group_order"] + ): + new_next_group_order = None + new_current_group_order = self.processing["next_group_order"] + if new_current_group_order is not None: + current_next_order_found = False + for order in self.order_groups.groups().keys(): + if current_next_order_found: + new_next_group_order = order + break + + if order == new_current_group_order: + current_next_order_found = True + + self.processing["next_group_order"] = new_next_group_order + self.processing["current_group_order"] = ( + new_current_group_order + ) + + if self.collect_state == 0: + self.collect_state = 1 + self.switch_toggleability.emit(True) + self.passed_group.emit(new_current_group_order) + yield IterationBreak("Collected") + + self.passed_group.emit(new_current_group_order) + if self.errored: + yield IterationBreak("Last group errored") + + if self.collect_state == 1: + self.collect_state = 2 + self.switch_toggleability.emit(False) + + if not self.validated and plugin.order > self.validators_order: + self.validated = True + if self.processing["stop_on_validation"]: + yield IterationBreak("Validated") + + # Stop if was stopped + if self.stopped: + self.stopped = False + yield IterationBreak("Stopped") + + # check test if will stop + self.processing["nextOrder"] = plugin.order + message = self.test(**self.processing) + if message: + yield IterationBreak("Stopped due to \"{}\"".format(message)) + + self.processing["last_plugin_order"] = plugin.order + if not plugin.active: + pyblish.logic.log.debug("%s was inactive, skipping.." % plugin) + self.was_skipped.emit(plugin) + continue + + if plugin.__instanceEnabled__: + instances = pyblish.logic.instances_by_plugin( + self.context, plugin + ) + if not instances: + self.was_skipped.emit(plugin) + continue + + for instance in instances: + if instance.data.get("publish") is False: + pyblish.logic.log.debug( + "%s was inactive, skipping.." % instance + ) + continue + yield (plugin, instance) + else: + families = util.collect_families_from_instances( + self.context, only_active=True + ) + plugins = pyblish.logic.plugins_by_families( + [plugin], families + ) + if not plugins: + self.was_skipped.emit(plugin) + continue + yield (plugin, None) + + self.passed_group.emit(self.processing["next_group_order"]) + + def iterate_and_process(self, on_finished=lambda: None): + """ Iterating inserted plugins with current context. + Collectors do not contain instances, they are None when collecting! + This process don't stop on one + """ + def on_next(): + try: + self.current_pair = next(self.pair_generator) + if isinstance(self.current_pair, IterationBreak): + raise self.current_pair + + except IterationBreak: + self.is_running = False + self.was_stopped.emit() + return + + except StopIteration: + self.is_running = False + # All pairs were processed successfully! + return util.defer(500, on_finished) + + except Exception: + # This is a bug + exc_type, exc_msg, exc_tb = sys.exc_info() + traceback.print_exception(exc_type, exc_msg, exc_tb) + self.is_running = False + self.was_stopped.emit() + return util.defer( + 500, lambda: on_unexpected_error(error=exc_msg) + ) + + self.about_to_process.emit(*self.current_pair) + util.defer(100, on_process) + + def on_process(): + try: + result = self._process(*self.current_pair) + if result["error"] is not None: + self.errored = True + + self.was_processed.emit(result) + + except Exception: + # TODO this should be handled much differently + exc_type, exc_msg, exc_tb = sys.exc_info() + traceback.print_exception(exc_type, exc_msg, exc_tb) + return util.defer( + 500, lambda: on_unexpected_error(error=exc_msg) + ) + + util.defer(10, on_next) + + def on_unexpected_error(error): + util.u_print(u"An unexpected error occurred:\n %s" % error) + return util.defer(500, on_finished) + + self.is_running = True + util.defer(10, on_next) + + def collect(self): + """ Iterate and process Collect plugins + - load_plugins method is launched again when finished + """ + self.iterate_and_process() + + def validate(self): + """ Process plugins to validations_order value.""" + self.processing["stop_on_validation"] = True + self.iterate_and_process() + + def publish(self): + """ Iterate and process all remaining plugins.""" + self.processing["stop_on_validation"] = False + self.iterate_and_process(self.on_published) + + def cleanup(self): + """Forcefully delete objects from memory + In an ideal world, this shouldn't be necessary. Garbage + collection guarantees that anything without reference + is automatically removed. + However, because this application is designed to be run + multiple times from the same interpreter process, extra + case must be taken to ensure there are no memory leaks. + Explicitly deleting objects shines a light on where objects + may still be referenced in the form of an error. No errors + means this was uneccesary, but that's ok. + """ + + for instance in self.context: + del(instance) + + for plugin in self.plugins: + del(plugin) diff --git a/pype/tools/pyblish_pype/delegate.py b/pype/tools/pyblish_pype/delegate.py new file mode 100644 index 0000000000..849495cdeb --- /dev/null +++ b/pype/tools/pyblish_pype/delegate.py @@ -0,0 +1,552 @@ +import platform + +from .vendor.Qt import QtWidgets, QtGui, QtCore + +from . import model +from .awesome import tags as awesome +from .constants import ( + PluginStates, InstanceStates, PluginActionStates, Roles +) + +colors = { + "error": QtGui.QColor("#ff4a4a"), + "warning": QtGui.QColor("#ff9900"), + "ok": QtGui.QColor("#77AE24"), + "active": QtGui.QColor("#99CEEE"), + "idle": QtCore.Qt.white, + "font": QtGui.QColor("#DDD"), + "inactive": QtGui.QColor("#888"), + "hover": QtGui.QColor(255, 255, 255, 10), + "selected": QtGui.QColor(255, 255, 255, 20), + "outline": QtGui.QColor("#333"), + "group": QtGui.QColor("#333") +} + +scale_factors = {"darwin": 1.5} +scale_factor = scale_factors.get(platform.system().lower(), 1.0) +fonts = { + "h3": QtGui.QFont("Open Sans", 10 * scale_factor, QtGui.QFont.Normal), + "h4": QtGui.QFont("Open Sans", 8 * scale_factor, QtGui.QFont.Normal), + "h5": QtGui.QFont("Open Sans", 8 * scale_factor, QtGui.QFont.DemiBold), + "awesome6": QtGui.QFont("FontAwesome", 6 * scale_factor), + "awesome10": QtGui.QFont("FontAwesome", 10 * scale_factor), + "smallAwesome": QtGui.QFont("FontAwesome", 8 * scale_factor), + "largeAwesome": QtGui.QFont("FontAwesome", 16 * scale_factor), +} +font_metrics = { + "awesome6": QtGui.QFontMetrics(fonts["awesome6"]), + "h4": QtGui.QFontMetrics(fonts["h4"]), + "h5": QtGui.QFontMetrics(fonts["h5"]) +} +icons = { + "action": awesome["adn"], + "angle-right": awesome["angle-right"], + "angle-left": awesome["angle-left"], + "plus-sign": awesome['plus'], + "minus-sign": awesome['minus'] +} + + +class PluginItemDelegate(QtWidgets.QStyledItemDelegate): + """Generic delegate for model items""" + + def paint(self, painter, option, index): + """Paint checkbox and text. + _ + |_| My label > + """ + + body_rect = QtCore.QRectF(option.rect) + + check_rect = QtCore.QRectF(body_rect) + check_rect.setWidth(check_rect.height()) + check_offset = (check_rect.height() / 4) + 1 + check_rect.adjust( + check_offset, check_offset, -check_offset, -check_offset + ) + + check_color = colors["idle"] + + perspective_icon = icons["angle-right"] + perspective_rect = QtCore.QRectF(body_rect) + perspective_rect.setWidth(perspective_rect.height()) + perspective_rect.adjust(0, 3, 0, 0) + perspective_rect.translate( + body_rect.width() - (perspective_rect.width() / 2 + 2), + 0 + ) + + publish_states = index.data(Roles.PublishFlagsRole) + if publish_states & PluginStates.InProgress: + check_color = colors["active"] + + elif publish_states & PluginStates.HasError: + check_color = colors["error"] + + elif publish_states & PluginStates.HasWarning: + check_color = colors["warning"] + + elif publish_states & PluginStates.WasProcessed: + check_color = colors["ok"] + + elif not index.data(Roles.IsEnabledRole): + check_color = colors["inactive"] + + offset = (body_rect.height() - font_metrics["h4"].height()) / 2 + label_rect = QtCore.QRectF(body_rect.adjusted( + check_rect.width() + 12, offset - 1, 0, 0 + )) + + assert label_rect.width() > 0 + + label = index.data(QtCore.Qt.DisplayRole) + label = font_metrics["h4"].elidedText( + label, + QtCore.Qt.ElideRight, + label_rect.width() - 20 + ) + + font_color = colors["idle"] + if not index.data(QtCore.Qt.CheckStateRole): + font_color = colors["inactive"] + + # Maintain reference to state, so we can restore it once we're done + painter.save() + + # Draw perspective icon + painter.setFont(fonts["awesome10"]) + painter.setPen(QtGui.QPen(font_color)) + painter.drawText(perspective_rect, perspective_icon) + + # Draw label + painter.setFont(fonts["h4"]) + painter.setPen(QtGui.QPen(font_color)) + painter.drawText(label_rect, label) + + # Draw action icon + if index.data(Roles.PluginActionsVisibleRole): + painter.save() + action_state = index.data(Roles.PluginActionProgressRole) + if action_state & PluginActionStates.HasFailed: + color = colors["error"] + elif action_state & PluginActionStates.HasFinished: + color = colors["ok"] + elif action_state & PluginActionStates.InProgress: + color = colors["active"] + else: + color = colors["idle"] + + painter.setFont(fonts["smallAwesome"]) + painter.setPen(QtGui.QPen(color)) + + icon_rect = QtCore.QRectF( + option.rect.adjusted( + label_rect.width() - perspective_rect.width() / 2, + label_rect.height() / 3, 0, 0 + ) + ) + painter.drawText(icon_rect, icons["action"]) + + painter.restore() + + # Draw checkbox + pen = QtGui.QPen(check_color, 1) + painter.setPen(pen) + + if index.data(Roles.IsOptionalRole): + painter.drawRect(check_rect) + + if index.data(QtCore.Qt.CheckStateRole): + optional_check_rect = QtCore.QRectF(check_rect) + optional_check_rect.adjust(2, 2, -1, -1) + painter.fillRect(optional_check_rect, check_color) + + else: + painter.fillRect(check_rect, check_color) + + if option.state & QtWidgets.QStyle.State_MouseOver: + painter.fillRect(body_rect, colors["hover"]) + + if option.state & QtWidgets.QStyle.State_Selected: + painter.fillRect(body_rect, colors["selected"]) + + # Ok, we're done, tidy up. + painter.restore() + + def sizeHint(self, option, index): + return QtCore.QSize(option.rect.width(), 20) + + +class InstanceItemDelegate(QtWidgets.QStyledItemDelegate): + """Generic delegate for model items""" + + def paint(self, painter, option, index): + """Paint checkbox and text. + _ + |_| My label > + """ + + body_rect = QtCore.QRectF(option.rect) + + check_rect = QtCore.QRectF(body_rect) + check_rect.setWidth(check_rect.height()) + offset = (check_rect.height() / 4) + 1 + check_rect.adjust(offset, offset, -(offset), -(offset)) + + check_color = colors["idle"] + + perspective_icon = icons["angle-right"] + perspective_rect = QtCore.QRectF(body_rect) + perspective_rect.setWidth(perspective_rect.height()) + perspective_rect.adjust(0, 3, 0, 0) + perspective_rect.translate( + body_rect.width() - (perspective_rect.width() / 2 + 2), + 0 + ) + + publish_states = index.data(Roles.PublishFlagsRole) + if publish_states & InstanceStates.InProgress: + check_color = colors["active"] + + elif publish_states & InstanceStates.HasError: + check_color = colors["error"] + + elif publish_states & InstanceStates.HasWarning: + check_color = colors["warning"] + + elif publish_states & InstanceStates.HasFinished: + check_color = colors["ok"] + + elif not index.data(Roles.IsEnabledRole): + check_color = colors["inactive"] + + offset = (body_rect.height() - font_metrics["h4"].height()) / 2 + label_rect = QtCore.QRectF(body_rect.adjusted( + check_rect.width() + 12, offset - 1, 0, 0 + )) + + assert label_rect.width() > 0 + + label = index.data(QtCore.Qt.DisplayRole) + label = font_metrics["h4"].elidedText( + label, + QtCore.Qt.ElideRight, + label_rect.width() - 20 + ) + + font_color = colors["idle"] + if not index.data(QtCore.Qt.CheckStateRole): + font_color = colors["inactive"] + + # Maintain reference to state, so we can restore it once we're done + painter.save() + + # Draw perspective icon + painter.setFont(fonts["awesome10"]) + painter.setPen(QtGui.QPen(font_color)) + painter.drawText(perspective_rect, perspective_icon) + + # Draw label + painter.setFont(fonts["h4"]) + painter.setPen(QtGui.QPen(font_color)) + painter.drawText(label_rect, label) + + # Draw checkbox + pen = QtGui.QPen(check_color, 1) + painter.setPen(pen) + + if index.data(Roles.IsOptionalRole): + painter.drawRect(check_rect) + + if index.data(QtCore.Qt.CheckStateRole): + optional_check_rect = QtCore.QRectF(check_rect) + optional_check_rect.adjust(2, 2, -1, -1) + painter.fillRect(optional_check_rect, check_color) + + else: + painter.fillRect(check_rect, check_color) + + if option.state & QtWidgets.QStyle.State_MouseOver: + painter.fillRect(body_rect, colors["hover"]) + + if option.state & QtWidgets.QStyle.State_Selected: + painter.fillRect(body_rect, colors["selected"]) + + # Ok, we're done, tidy up. + painter.restore() + + def sizeHint(self, option, index): + return QtCore.QSize(option.rect.width(), 20) + + +class OverviewGroupSection(QtWidgets.QStyledItemDelegate): + """Generic delegate for section header""" + + item_class = None + + def __init__(self, parent): + super(OverviewGroupSection, self).__init__(parent) + self.item_delegate = self.item_class(parent) + + def paint(self, painter, option, index): + if index.data(Roles.TypeRole) in ( + model.InstanceType, model.PluginType + ): + self.item_delegate.paint(painter, option, index) + return + + self.group_item_paint(painter, option, index) + + def group_item_paint(self, painter, option, index): + """Paint text + _ + My label + """ + body_rect = QtCore.QRectF(option.rect) + bg_rect = QtCore.QRectF( + body_rect.left(), body_rect.top() + 1, + body_rect.width() - 5, body_rect.height() - 2 + ) + radius = 8.0 + bg_path = QtGui.QPainterPath() + bg_path.addRoundedRect(bg_rect, radius, radius) + painter.fillPath(bg_path, colors["group"]) + + expander_rect = QtCore.QRectF(bg_rect) + expander_rect.setWidth(expander_rect.height()) + text_height = font_metrics["awesome6"].height() + adjust_value = (expander_rect.height() - text_height) / 2 + expander_rect.adjust( + adjust_value + 1.5, adjust_value - 0.5, + -adjust_value + 1.5, -adjust_value - 0.5 + ) + + offset = (bg_rect.height() - font_metrics["h5"].height()) / 2 + label_rect = QtCore.QRectF(bg_rect.adjusted( + expander_rect.width() + 12, offset - 1, 0, 0 + )) + + assert label_rect.width() > 0 + + expander_icon = icons["plus-sign"] + + expanded = self.parent().isExpanded(index) + if expanded: + expander_icon = icons["minus-sign"] + label = index.data(QtCore.Qt.DisplayRole) + label = font_metrics["h5"].elidedText( + label, QtCore.Qt.ElideRight, label_rect.width() + ) + + # Maintain reference to state, so we can restore it once we're done + painter.save() + + painter.setFont(fonts["awesome6"]) + painter.setPen(QtGui.QPen(colors["idle"])) + painter.drawText(expander_rect, expander_icon) + + # Draw label + painter.setFont(fonts["h5"]) + painter.drawText(label_rect, label) + + if option.state & QtWidgets.QStyle.State_MouseOver: + painter.fillPath(bg_path, colors["hover"]) + + if option.state & QtWidgets.QStyle.State_Selected: + painter.fillPath(bg_path, colors["selected"]) + + # Ok, we're done, tidy up. + painter.restore() + + def sizeHint(self, option, index): + return QtCore.QSize(option.rect.width(), 20) + + +class PluginDelegate(OverviewGroupSection): + """Generic delegate for model items in proxy tree view""" + item_class = PluginItemDelegate + + +class InstanceDelegate(OverviewGroupSection): + """Generic delegate for model items in proxy tree view""" + item_class = InstanceItemDelegate + + +class ArtistDelegate(QtWidgets.QStyledItemDelegate): + """Delegate used on Artist page""" + + def paint(self, painter, option, index): + """Paint checkbox and text + + _______________________________________________ + | | label | duration |arrow| + |toggle |_____________________| | to | + | | families | |persp| + |_______|_____________________|___________|_____| + + """ + + # Layout + spacing = 10 + + body_rect = QtCore.QRectF(option.rect).adjusted(2, 2, -8, -2) + content_rect = body_rect.adjusted(5, 5, -5, -5) + + perspective_rect = QtCore.QRectF(body_rect) + perspective_rect.setWidth(35) + perspective_rect.setHeight(35) + perspective_rect.translate( + content_rect.width() - (perspective_rect.width() / 2) + 10, + (content_rect.height() / 2) - (perspective_rect.height() / 2) + ) + + toggle_rect = QtCore.QRectF(body_rect) + toggle_rect.setWidth(7) + toggle_rect.adjust(1, 1, 0, -1) + + icon_rect = QtCore.QRectF(content_rect) + icon_rect.translate(toggle_rect.width() + spacing, 3) + icon_rect.setWidth(35) + icon_rect.setHeight(35) + + duration_rect = QtCore.QRectF(content_rect) + duration_rect.translate(content_rect.width() - 50, 0) + + # Colors + check_color = colors["idle"] + + publish_states = index.data(Roles.PublishFlagsRole) + if publish_states is None: + return + if publish_states & InstanceStates.InProgress: + check_color = colors["active"] + + elif publish_states & InstanceStates.HasError: + check_color = colors["error"] + + elif publish_states & InstanceStates.HasWarning: + check_color = colors["warning"] + + elif publish_states & InstanceStates.HasFinished: + check_color = colors["ok"] + + elif not index.data(Roles.IsEnabledRole): + check_color = colors["inactive"] + + perspective_icon = icons["angle-right"] + + if not index.data(QtCore.Qt.CheckStateRole): + font_color = colors["inactive"] + else: + font_color = colors["idle"] + + if ( + option.state + & ( + QtWidgets.QStyle.State_MouseOver + or QtWidgets.QStyle.State_Selected + ) + ): + perspective_color = colors["idle"] + else: + perspective_color = colors["inactive"] + # Maintan reference to state, so we can restore it once we're done + painter.save() + + # Draw background + painter.fillRect(body_rect, colors["hover"]) + + # Draw icon + icon = index.data(QtCore.Qt.DecorationRole) + + painter.setFont(fonts["largeAwesome"]) + painter.setPen(QtGui.QPen(font_color)) + painter.drawText(icon_rect, icon) + + # Draw label + painter.setFont(fonts["h3"]) + label_rect = QtCore.QRectF(content_rect) + label_x_offset = icon_rect.width() + spacing + label_rect.translate( + label_x_offset, + 0 + ) + metrics = painter.fontMetrics() + label_rect.setHeight(metrics.lineSpacing()) + label_rect.setWidth( + content_rect.width() + - label_x_offset + - perspective_rect.width() + ) + # Elide label + label = index.data(QtCore.Qt.DisplayRole) + label = metrics.elidedText( + label, QtCore.Qt.ElideRight, label_rect.width() + ) + painter.drawText(label_rect, label) + + # Draw families + painter.setFont(fonts["h5"]) + painter.setPen(QtGui.QPen(colors["inactive"])) + + families = ", ".join(index.data(Roles.FamiliesRole)) + families = painter.fontMetrics().elidedText( + families, QtCore.Qt.ElideRight, label_rect.width() + ) + + families_rect = QtCore.QRectF(label_rect) + families_rect.translate(0, label_rect.height() + spacing) + + painter.drawText(families_rect, families) + + painter.setFont(fonts["largeAwesome"]) + painter.setPen(QtGui.QPen(perspective_color)) + painter.drawText(perspective_rect, perspective_icon) + + # Draw checkbox + pen = QtGui.QPen(check_color, 1) + painter.setPen(pen) + + if index.data(Roles.IsOptionalRole): + painter.drawRect(toggle_rect) + + if index.data(QtCore.Qt.CheckStateRole): + painter.fillRect(toggle_rect, check_color) + + elif ( + index.data(QtCore.Qt.CheckStateRole) + ): + painter.fillRect(toggle_rect, check_color) + + if option.state & QtWidgets.QStyle.State_MouseOver: + painter.fillRect(body_rect, colors["hover"]) + + if option.state & QtWidgets.QStyle.State_Selected: + painter.fillRect(body_rect, colors["selected"]) + + painter.setPen(colors["outline"]) + painter.drawRect(body_rect) + + # Ok, we're done, tidy up. + painter.restore() + + def sizeHint(self, option, index): + return QtCore.QSize(option.rect.width(), 80) + + +class TerminalItem(QtWidgets.QStyledItemDelegate): + """Delegate used exclusively for the Terminal""" + + def paint(self, painter, option, index): + super(TerminalItem, self).paint(painter, option, index) + item_type = index.data(Roles.TypeRole) + if item_type == model.TerminalDetailType: + return + + hover = QtGui.QPainterPath() + hover.addRect(QtCore.QRectF(option.rect).adjusted(0, 0, -1, -1)) + if option.state & QtWidgets.QStyle.State_Selected: + painter.fillPath(hover, colors["selected"]) + + if option.state & QtWidgets.QStyle.State_MouseOver: + painter.fillPath(hover, colors["hover"]) diff --git a/pype/tools/pyblish_pype/font/fontawesome/fontawesome-webfont.ttf b/pype/tools/pyblish_pype/font/fontawesome/fontawesome-webfont.ttf new file mode 100644 index 0000000000..9d02852c14 Binary files /dev/null and b/pype/tools/pyblish_pype/font/fontawesome/fontawesome-webfont.ttf differ diff --git a/pype/tools/pyblish_pype/font/opensans/LICENSE.txt b/pype/tools/pyblish_pype/font/opensans/LICENSE.txt new file mode 100644 index 0000000000..d645695673 --- /dev/null +++ b/pype/tools/pyblish_pype/font/opensans/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/pype/tools/pyblish_pype/font/opensans/OpenSans-Bold.ttf b/pype/tools/pyblish_pype/font/opensans/OpenSans-Bold.ttf new file mode 100644 index 0000000000..fd79d43bea Binary files /dev/null and b/pype/tools/pyblish_pype/font/opensans/OpenSans-Bold.ttf differ diff --git a/pype/tools/pyblish_pype/font/opensans/OpenSans-BoldItalic.ttf b/pype/tools/pyblish_pype/font/opensans/OpenSans-BoldItalic.ttf new file mode 100644 index 0000000000..9bc800958a Binary files /dev/null and b/pype/tools/pyblish_pype/font/opensans/OpenSans-BoldItalic.ttf differ diff --git a/pype/tools/pyblish_pype/font/opensans/OpenSans-ExtraBold.ttf b/pype/tools/pyblish_pype/font/opensans/OpenSans-ExtraBold.ttf new file mode 100644 index 0000000000..21f6f84a07 Binary files /dev/null and b/pype/tools/pyblish_pype/font/opensans/OpenSans-ExtraBold.ttf differ diff --git a/pype/tools/pyblish_pype/font/opensans/OpenSans-ExtraBoldItalic.ttf b/pype/tools/pyblish_pype/font/opensans/OpenSans-ExtraBoldItalic.ttf new file mode 100644 index 0000000000..31cb688340 Binary files /dev/null and b/pype/tools/pyblish_pype/font/opensans/OpenSans-ExtraBoldItalic.ttf differ diff --git a/pype/tools/pyblish_pype/font/opensans/OpenSans-Italic.ttf b/pype/tools/pyblish_pype/font/opensans/OpenSans-Italic.ttf new file mode 100644 index 0000000000..c90da48ff3 Binary files /dev/null and b/pype/tools/pyblish_pype/font/opensans/OpenSans-Italic.ttf differ diff --git a/pype/tools/pyblish_pype/font/opensans/OpenSans-Light.ttf b/pype/tools/pyblish_pype/font/opensans/OpenSans-Light.ttf new file mode 100644 index 0000000000..0d381897da Binary files /dev/null and b/pype/tools/pyblish_pype/font/opensans/OpenSans-Light.ttf differ diff --git a/pype/tools/pyblish_pype/font/opensans/OpenSans-LightItalic.ttf b/pype/tools/pyblish_pype/font/opensans/OpenSans-LightItalic.ttf new file mode 100644 index 0000000000..68299c4bc6 Binary files /dev/null and b/pype/tools/pyblish_pype/font/opensans/OpenSans-LightItalic.ttf differ diff --git a/pype/tools/pyblish_pype/font/opensans/OpenSans-Regular.ttf b/pype/tools/pyblish_pype/font/opensans/OpenSans-Regular.ttf new file mode 100644 index 0000000000..db433349b7 Binary files /dev/null and b/pype/tools/pyblish_pype/font/opensans/OpenSans-Regular.ttf differ diff --git a/pype/tools/pyblish_pype/font/opensans/OpenSans-Semibold.ttf b/pype/tools/pyblish_pype/font/opensans/OpenSans-Semibold.ttf new file mode 100644 index 0000000000..1a7679e394 Binary files /dev/null and b/pype/tools/pyblish_pype/font/opensans/OpenSans-Semibold.ttf differ diff --git a/pype/tools/pyblish_pype/font/opensans/OpenSans-SemiboldItalic.ttf b/pype/tools/pyblish_pype/font/opensans/OpenSans-SemiboldItalic.ttf new file mode 100644 index 0000000000..59b6d16b06 Binary files /dev/null and b/pype/tools/pyblish_pype/font/opensans/OpenSans-SemiboldItalic.ttf differ diff --git a/pype/tools/pyblish_pype/i18n/pyblish_lite.pro b/pype/tools/pyblish_pype/i18n/pyblish_lite.pro new file mode 100644 index 0000000000..c8e2a5b56f --- /dev/null +++ b/pype/tools/pyblish_pype/i18n/pyblish_lite.pro @@ -0,0 +1,2 @@ +SOURCES = ../window.py +TRANSLATIONS = zh_CN.ts \ No newline at end of file diff --git a/pype/tools/pyblish_pype/i18n/zh_CN.qm b/pype/tools/pyblish_pype/i18n/zh_CN.qm new file mode 100644 index 0000000000..fed08d8a51 Binary files /dev/null and b/pype/tools/pyblish_pype/i18n/zh_CN.qm differ diff --git a/pype/tools/pyblish_pype/i18n/zh_CN.ts b/pype/tools/pyblish_pype/i18n/zh_CN.ts new file mode 100644 index 0000000000..18ba81f69f --- /dev/null +++ b/pype/tools/pyblish_pype/i18n/zh_CN.ts @@ -0,0 +1,96 @@ + + + + Window + + + Finishing up reset.. + 完成重置.. + + + + Comment.. + 备注.. + + + + Processing + 处理 + + + + Stopped due to error(s), see Terminal. + 因错误终止, 请查看终端。 + + + + Finished successfully! + 成功完成! + + + + About to reset.. + 即将重置.. + + + + Preparing validate.. + 准备校验.. + + + + Preparing publish.. + 准备发布.. + + + + Preparing + 准备 + + + + Action prepared. + 动作已就绪。 + + + + Cleaning up models.. + 清理数据模型.. + + + + Cleaning up terminal.. + 清理终端.. + + + + Cleaning up controller.. + 清理控制器.. + + + + All clean! + 清理完成! + + + + Good bye + 再见 + + + + ..as soon as processing is finished.. + ..处理即将完成.. + + + + Stopping.. + 正在停止.. + + + + Closing.. + 正在关闭.. + + + diff --git a/pype/tools/pyblish_pype/img/down_arrow.png b/pype/tools/pyblish_pype/img/down_arrow.png new file mode 100644 index 0000000000..e271f7f90b Binary files /dev/null and b/pype/tools/pyblish_pype/img/down_arrow.png differ diff --git a/pype/tools/pyblish_pype/img/logo-extrasmall.png b/pype/tools/pyblish_pype/img/logo-extrasmall.png new file mode 100644 index 0000000000..ebe45c4c6e Binary files /dev/null and b/pype/tools/pyblish_pype/img/logo-extrasmall.png differ diff --git a/pype/tools/pyblish_pype/img/tab-home.png b/pype/tools/pyblish_pype/img/tab-home.png new file mode 100644 index 0000000000..9133d06edc Binary files /dev/null and b/pype/tools/pyblish_pype/img/tab-home.png differ diff --git a/pype/tools/pyblish_pype/img/tab-overview.png b/pype/tools/pyblish_pype/img/tab-overview.png new file mode 100644 index 0000000000..443a750a7c Binary files /dev/null and b/pype/tools/pyblish_pype/img/tab-overview.png differ diff --git a/pype/tools/pyblish_pype/img/tab-terminal.png b/pype/tools/pyblish_pype/img/tab-terminal.png new file mode 100644 index 0000000000..ea1bcff98d Binary files /dev/null and b/pype/tools/pyblish_pype/img/tab-terminal.png differ diff --git a/pype/tools/pyblish_pype/mock.py b/pype/tools/pyblish_pype/mock.py new file mode 100644 index 0000000000..c85ff0f2ba --- /dev/null +++ b/pype/tools/pyblish_pype/mock.py @@ -0,0 +1,732 @@ +import os +import time +import subprocess + +import pyblish.api + + +class MyAction(pyblish.api.Action): + label = "My Action" + on = "processed" + + def process(self, context, plugin): + self.log.info("Running!") + + +class MyOtherAction(pyblish.api.Action): + label = "My Other Action" + + def process(self, context, plugin): + self.log.info("Running!") + + +class CollectComment(pyblish.api.ContextPlugin): + """This collector has a very long comment. + + The idea is that this comment should either be elided, or word- + wrapped in the corresponding view. + + """ + + order = pyblish.api.CollectorOrder + + def process(self, context): + context.data["comment"] = "" + + +class MyCollector(pyblish.api.ContextPlugin): + label = "My Collector" + order = pyblish.api.CollectorOrder + + def process(self, context): + context.create_instance("MyInstance 1", families=["myFamily"]) + context.create_instance("MyInstance 2", families=["myFamily 2"]) + context.create_instance( + "MyInstance 3", + families=["myFamily 2"], + publish=False + ) + + +class MyValidator(pyblish.api.InstancePlugin): + order = pyblish.api.ValidatorOrder + active = False + label = "My Validator" + actions = [MyAction, + MyOtherAction] + + def process(self, instance): + self.log.info("Validating: %s" % instance) + + +class MyExtractor(pyblish.api.InstancePlugin): + order = pyblish.api.ExtractorOrder + families = ["myFamily"] + label = "My Extractor" + + def process(self, instance): + self.log.info("Extracting: %s" % instance) + + +class CollectRenamed(pyblish.api.Collector): + def process(self, context): + i = context.create_instance("MyInstanceXYZ", family="MyFamily") + i.set_data("name", "My instance") + + +class CollectNegatron(pyblish.api.Collector): + """Negative collector adds Negatron""" + + order = pyblish.api.Collector.order - 0.49 + + def process_context(self, context): + self.log.info("Collecting Negatron") + context.create_instance("Negatron", family="MyFamily") + + +class CollectPositron(pyblish.api.Collector): + """Positive collector adds Positron""" + + order = pyblish.api.Collector.order + 0.49 + + def process_context(self, context): + self.log.info("Collecting Positron") + context.create_instance("Positron", family="MyFamily") + + +class SelectInstances(pyblish.api.Selector): + """Select debugging instances + + These instances are part of the evil plan to destroy the world. + Be weary, be vigilant, be sexy. + + """ + + def process_context(self, context): + self.log.info("Selecting instances..") + + for instance in instances[:-1]: + name, data = instance["name"], instance["data"] + self.log.info("Selecting: %s" % name) + instance = context.create_instance(name) + + for key, value in data.items(): + instance.set_data(key, value) + + +class SelectDiInstances(pyblish.api.Selector): + """Select DI instances""" + + name = "Select Dependency Instances" + + def process(self, context): + name, data = instances[-1]["name"], instances[-1]["data"] + self.log.info("Selecting: %s" % name) + instance = context.create_instance(name) + + for key, value in data.items(): + instance.set_data(key, value) + + +class SelectInstancesFailure(pyblish.api.Selector): + """Select some instances, but fail before adding anything to the context. + + That's right. I'm programmed to fail. Try me. + + """ + + __fail__ = True + + def process_context(self, context): + self.log.warning("I'm about to fail") + raise AssertionError("I was programmed to fail") + + +class SelectInstances2(pyblish.api.Selector): + def process(self, context): + self.log.warning("I'm good") + + +class ValidateNamespace(pyblish.api.Validator): + """Namespaces must be orange + + In case a namespace is not orange, report immediately to + your officer in charge, ask for a refund, do a backflip. + + This has been an example of: + + - A long doc-string + - With a list + - And plenty of newlines and tabs. + + """ + + families = ["B"] + + def process(self, instance): + self.log.info("Validating the namespace of %s" % instance.data("name")) + self.log.info("""And here's another message, quite long, in fact it's +too long to be displayed in a single row of text. +But that's how we roll down here. It's got \nnew lines\nas well. + +- And lists +- And more lists + + """) + + +class ValidateContext(pyblish.api.Validator): + families = ["A", "B"] + + def process_context(self, context): + self.log.info("Processing context..") + + +class ValidateContextFailure(pyblish.api.Validator): + optional = True + families = ["C"] + __fail__ = True + + def process_context(self, context): + self.log.info("About to fail..") + raise AssertionError("""I was programmed to fail + +The reason I failed was because the sun was not aligned with the tides, +and the moon is gray; not yellow. Try again when the moon is yellow.""") + + +class Validator1(pyblish.api.Validator): + """Test of the order attribute""" + order = pyblish.api.Validator.order + 0.1 + families = ["A"] + + def process_instance(self, instance): + pass + + +class Validator2(pyblish.api.Validator): + order = pyblish.api.Validator.order + 0.2 + families = ["B"] + + def process_instance(self, instance): + pass + + +class Validator3(pyblish.api.Validator): + order = pyblish.api.Validator.order + 0.3 + families = ["B"] + + def process_instance(self, instance): + pass + + +class ValidateFailureMock(pyblish.api.Validator): + """Plug-in that always fails""" + optional = True + order = pyblish.api.Validator.order + 0.1 + families = ["C"] + __fail__ = True + + def process_instance(self, instance): + self.log.debug("e = mc^2") + self.log.info("About to fail..") + self.log.warning("Failing.. soooon..") + self.log.critical("Ok, you're done.") + raise AssertionError("""ValidateFailureMock was destined to fail.. + +Here's some extended information about what went wrong. + +It has quite the long string associated with it, including +a few newlines and a list. + +- Item 1 +- Item 2 + +""") + + +class ValidateIsIncompatible(pyblish.api.Validator): + """This plug-in should never appear..""" + requires = False # This is invalid + + +class ValidateWithRepair(pyblish.api.Validator): + """A validator with repair functionality""" + optional = True + families = ["C"] + __fail__ = True + + def process_instance(self, instance): + raise AssertionError( + "%s is invalid, try repairing it!" % instance.name + ) + + def repair_instance(self, instance): + self.log.info("Attempting to repair..") + self.log.info("Success!") + + +class ValidateWithRepairFailure(pyblish.api.Validator): + """A validator with repair functionality that fails""" + optional = True + families = ["C"] + __fail__ = True + + def process_instance(self, instance): + raise AssertionError( + "%s is invalid, try repairing it!" % instance.name + ) + + def repair_instance(self, instance): + self.log.info("Attempting to repair..") + raise AssertionError("Could not repair due to X") + + +class ValidateWithVeryVeryVeryLongLongNaaaaame(pyblish.api.Validator): + """A validator with repair functionality that fails""" + families = ["A"] + + +class ValidateWithRepairContext(pyblish.api.Validator): + """A validator with repair functionality that fails""" + optional = True + families = ["C"] + __fail__ = True + + def process_context(self, context): + raise AssertionError("Could not validate context, try repairing it") + + def repair_context(self, context): + self.log.info("Attempting to repair..") + raise AssertionError("Could not repair") + + +class ExtractAsMa(pyblish.api.Extractor): + """Extract contents of each instance into .ma + + Serialise scene using Maya's own facilities and then put + it on the hard-disk. Once complete, this plug-in relies + on a Conformer to put it in it's final location, as this + extractor merely positions it in the users local temp- + directory. + + """ + + optional = True + __expected__ = { + "logCount": ">=4" + } + + def process_instance(self, instance): + self.log.info("About to extract scene to .ma..") + self.log.info("Extraction went well, now verifying the data..") + + if instance.name == "Richard05": + self.log.warning("You're almost running out of disk space!") + + self.log.info("About to finish up") + self.log.info("Finished successfully") + + +class ConformAsset(pyblish.api.Conformer): + """Conform the world + + Step 1: Conform all humans and Step 2: Conform all non-humans. + Once conforming has completed, rinse and repeat. + + """ + + optional = True + + def process_instance(self, instance): + self.log.info("About to conform all humans..") + + if instance.name == "Richard05": + self.log.warning("Richard05 is a conformist!") + + self.log.info("About to conform all non-humans..") + self.log.info("Conformed Successfully") + + +class ValidateInstancesDI(pyblish.api.Validator): + """Validate using the DI interface""" + families = ["diFamily"] + + def process(self, instance): + self.log.info("Validating %s.." % instance.data("name")) + + +class ValidateDIWithRepair(pyblish.api.Validator): + families = ["diFamily"] + optional = True + __fail__ = True + + def process(self, instance): + raise AssertionError("I was programmed to fail, for repair") + + def repair(self, instance): + self.log.info("Repairing %s" % instance.data("name")) + + +class ExtractInstancesDI(pyblish.api.Extractor): + """Extract using the DI interface""" + families = ["diFamily"] + + def process(self, instance): + self.log.info("Extracting %s.." % instance.data("name")) + + +class ValidateWithLabel(pyblish.api.Validator): + """Validate using the DI interface""" + label = "Validate with Label" + + +class ValidateWithLongLabel(pyblish.api.Validator): + """Validate using the DI interface""" + label = "Validate with Loooooooooooooooooooooong Label" + + +class SimplePlugin1(pyblish.api.Plugin): + """Validate using the simple-plugin interface""" + + def process(self): + self.log.info("I'm a simple plug-in, only processed once") + + +class SimplePlugin2(pyblish.api.Plugin): + """Validate using the simple-plugin interface + + It doesn't have an order, and will likely end up *before* all + other plug-ins. (due to how sorted([1, 2, 3, None]) works) + + """ + + def process(self, context): + self.log.info("Processing the context, simply: %s" % context) + + +class SimplePlugin3(pyblish.api.Plugin): + """Simply process every instance""" + + def process(self, instance): + self.log.info("Processing the instance, simply: %s" % instance) + + +class ContextAction(pyblish.api.Action): + label = "Context action" + + def process(self, context): + self.log.info("I have access to the context") + self.log.info("Context.instances: %s" % str(list(context))) + + +class FailingAction(pyblish.api.Action): + label = "Failing action" + + def process(self): + self.log.info("About to fail..") + raise Exception("I failed") + + +class LongRunningAction(pyblish.api.Action): + label = "Long-running action" + + def process(self): + self.log.info("Sleeping for 2 seconds..") + time.sleep(2) + self.log.info("Ah, that's better") + + +class IconAction(pyblish.api.Action): + label = "Icon action" + icon = "crop" + + def process(self): + self.log.info("I have an icon") + + +class PluginAction(pyblish.api.Action): + label = "Plugin action" + + def process(self, plugin): + self.log.info("I have access to my parent plug-in") + self.log.info("Which is %s" % plugin.id) + + +class LaunchExplorerAction(pyblish.api.Action): + label = "Open in Explorer" + icon = "folder-open" + + def process(self, context): + cwd = os.getcwd() + self.log.info("Opening %s in Explorer" % cwd) + result = subprocess.call("start .", cwd=cwd, shell=True) + self.log.debug(result) + + +class ProcessedAction(pyblish.api.Action): + label = "Success action" + icon = "check" + on = "processed" + + def process(self): + self.log.info("I am only available on a successful plug-in") + + +class FailedAction(pyblish.api.Action): + label = "Failure action" + icon = "close" + on = "failed" + + +class SucceededAction(pyblish.api.Action): + label = "Success action" + icon = "check" + on = "succeeded" + + def process(self): + self.log.info("I am only available on a successful plug-in") + + +class LongLabelAction(pyblish.api.Action): + label = "An incredibly, incredicly looooon label. Very long." + icon = "close" + + +class BadEventAction(pyblish.api.Action): + label = "Bad event action" + on = "not exist" + + +class InactiveAction(pyblish.api.Action): + active = False + + +class PluginWithActions(pyblish.api.Validator): + optional = True + actions = [ + pyblish.api.Category("General"), + ContextAction, + FailingAction, + LongRunningAction, + IconAction, + PluginAction, + pyblish.api.Category("Empty"), + pyblish.api.Category("OS"), + LaunchExplorerAction, + pyblish.api.Separator, + FailedAction, + SucceededAction, + pyblish.api.Category("Debug"), + BadEventAction, + InactiveAction, + LongLabelAction, + pyblish.api.Category("Empty"), + ] + + def process(self): + self.log.info("Ran PluginWithActions") + + +class FailingPluginWithActions(pyblish.api.Validator): + optional = True + actions = [ + FailedAction, + SucceededAction, + ] + + def process(self): + raise Exception("I was programmed to fail") + + +class ValidateDefaultOff(pyblish.api.Validator): + families = ["A", "B"] + active = False + optional = True + + def process(self, instance): + self.log.info("Processing instance..") + + +class ValidateWithHyperlinks(pyblish.api.Validator): + """To learn about Pyblish + + click here (http://pyblish.com) + + """ + + families = ["A", "B"] + + def process(self, instance): + self.log.info("Processing instance..") + + msg = "To learn about Pyblish, " + msg += "click here (http://pyblish.com)" + + self.log.info(msg) + + +class LongRunningCollector(pyblish.api.Collector): + """I will take at least 2 seconds...""" + def process(self, context): + self.log.info("Sleeping for 2 seconds..") + time.sleep(2) + self.log.info("Good morning") + + +class LongRunningValidator(pyblish.api.Validator): + """I will take at least 2 seconds...""" + def process(self, context): + self.log.info("Sleeping for 2 seconds..") + time.sleep(2) + self.log.info("Good morning") + + +class RearrangingPlugin(pyblish.api.ContextPlugin): + """Sort plug-ins by family, and then reverse it""" + order = pyblish.api.CollectorOrder + 0.2 + + def process(self, context): + self.log.info("Reversing instances in the context..") + context[:] = sorted( + context, + key=lambda i: i.data["family"], + reverse=True + ) + self.log.info("Reversed!") + + +class InactiveInstanceCollectorPlugin(pyblish.api.InstancePlugin): + """Special case of an InstancePlugin running as a Collector""" + order = pyblish.api.CollectorOrder + 0.1 + active = False + + def process(self, instance): + raise TypeError("I shouldn't have run in the first place") + + +class CollectWithIcon(pyblish.api.ContextPlugin): + order = pyblish.api.CollectorOrder + + def process(self, context): + instance = context.create_instance("With Icon") + instance.data["icon"] = "play" + + +instances = [ + { + "name": "Peter01", + "data": { + "family": "A", + "publish": False + } + }, + { + "name": "Richard05", + "data": { + "family": "A", + } + }, + { + "name": "Steven11", + "data": { + "family": "B", + } + }, + { + "name": "Piraya12", + "data": { + "family": "B", + } + }, + { + "name": "Marcus", + "data": { + "family": "C", + } + }, + { + "name": "Extra1", + "data": { + "family": "C", + } + }, + { + "name": "DependencyInstance", + "data": { + "family": "diFamily" + } + }, + { + "name": "NoFamily", + "data": {} + }, + { + "name": "Failure 1", + "data": { + "family": "failure", + "fail": False + } + }, + { + "name": "Failure 2", + "data": { + "family": "failure", + "fail": True + } + } +] + +plugins = [ + MyCollector, + MyValidator, + MyExtractor, + + CollectRenamed, + CollectNegatron, + CollectPositron, + SelectInstances, + SelectInstances2, + SelectDiInstances, + SelectInstancesFailure, + ValidateFailureMock, + ValidateNamespace, + # ValidateIsIncompatible, + ValidateWithVeryVeryVeryLongLongNaaaaame, + ValidateContext, + ValidateContextFailure, + Validator1, + Validator2, + Validator3, + ValidateWithRepair, + ValidateWithRepairFailure, + ValidateWithRepairContext, + ValidateWithLabel, + ValidateWithLongLabel, + ValidateDefaultOff, + ValidateWithHyperlinks, + ExtractAsMa, + ConformAsset, + + SimplePlugin1, + SimplePlugin2, + SimplePlugin3, + + ValidateInstancesDI, + ExtractInstancesDI, + ValidateDIWithRepair, + + PluginWithActions, + FailingPluginWithActions, + + # LongRunningCollector, + # LongRunningValidator, + + RearrangingPlugin, + InactiveInstanceCollectorPlugin, + + CollectComment, + CollectWithIcon, +] + +pyblish.api.sort_plugins(plugins) diff --git a/pype/tools/pyblish_pype/model.py b/pype/tools/pyblish_pype/model.py new file mode 100644 index 0000000000..58ab3ed0b7 --- /dev/null +++ b/pype/tools/pyblish_pype/model.py @@ -0,0 +1,1232 @@ +"""Qt models + +Description: + The model contains the original objects from Pyblish, such as + pyblish.api.Instance and pyblish.api.Plugin. The model then + provides an interface for reading and writing to those. + +GUI data: + Aside from original data, such as pyblish.api.Plugin.optional, + the GUI also hosts data internal to itself, such as whether or + not an item has processed such that it may be colored appropriately + in the view. This data is prefixed with two underscores (__). + + E.g. + + _has_processed + + This is so that the the GUI-only data doesn't accidentally overwrite + or cause confusion with existing data in plug-ins and instances. + +Roles: + Data is accessed via standard Qt "roles". You can think of a role + as the key of a dictionary, except they can only be integers. + +""" +from __future__ import unicode_literals + +import pyblish + +from . import settings, util +from .awesome import tags as awesome +from .vendor import Qt +from .vendor.Qt import QtCore, QtGui +from .vendor.six import text_type +from .vendor.six.moves import queue +from .vendor import qtawesome +from .constants import PluginStates, InstanceStates, GroupStates, Roles + +try: + from pypeapp import config + get_presets = config.get_presets +except Exception: + get_presets = dict + +# ItemTypes +InstanceType = QtGui.QStandardItem.UserType +PluginType = QtGui.QStandardItem.UserType + 1 +GroupType = QtGui.QStandardItem.UserType + 2 +TerminalLabelType = QtGui.QStandardItem.UserType + 3 +TerminalDetailType = QtGui.QStandardItem.UserType + 4 + + +class QAwesomeTextIconFactory: + icons = {} + @classmethod + def icon(cls, icon_name): + if icon_name not in cls.icons: + cls.icons[icon_name] = awesome.get(icon_name) + return cls.icons[icon_name] + + +class QAwesomeIconFactory: + icons = {} + @classmethod + def icon(cls, icon_name, icon_color): + if icon_name not in cls.icons: + cls.icons[icon_name] = {} + + if icon_color not in cls.icons[icon_name]: + cls.icons[icon_name][icon_color] = qtawesome.icon( + icon_name, + color=icon_color + ) + return cls.icons[icon_name][icon_color] + + +class IntentModel(QtGui.QStandardItemModel): + """Model for QComboBox with intents. + + It is expected that one inserted item is dictionary. + Key represents #Label and Value represent #Value. + + Example: + { + "Testing": "test", + "Publishing": "publish" + } + + First and default value is {"< Not Set >": None} + """ + + default_item = {"< Not Set >": None} + + def __init__(self, parent=None): + super(IntentModel, self).__init__(parent) + self._item_count = 0 + self.default_index = 0 + + @property + def has_items(self): + return self._item_count > 0 + + def reset(self): + self.clear() + self._item_count = 0 + self.default_index = 0 + + intents_preset = ( + get_presets() + .get("tools", {}) + .get("pyblish", {}) + .get("ui", {}) + .get("intents", {}) + ) + default = intents_preset.get("default") + items = intents_preset.get("items", {}) + if not items: + return + + for idx, item_value in enumerate(items.keys()): + if item_value == default: + self.default_index = idx + break + + self.add_items(items) + + def add_items(self, items): + for value, label in items.items(): + new_item = QtGui.QStandardItem() + new_item.setData(label, QtCore.Qt.DisplayRole) + new_item.setData(value, Roles.IntentItemValue) + + self.setItem(self._item_count, new_item) + self._item_count += 1 + + +class PluginItem(QtGui.QStandardItem): + """Plugin item implementation.""" + + def __init__(self, plugin): + super(PluginItem, self).__init__() + + item_text = plugin.__name__ + if settings.UseLabel: + if hasattr(plugin, "label") and plugin.label: + item_text = plugin.label + + self.plugin = plugin + + self.setData(item_text, QtCore.Qt.DisplayRole) + self.setData(False, Roles.IsEnabledRole) + self.setData(0, Roles.PublishFlagsRole) + self.setData(0, Roles.PluginActionProgressRole) + icon_name = "" + if hasattr(plugin, "icon") and plugin.icon: + icon_name = plugin.icon + icon = QAwesomeTextIconFactory.icon(icon_name) + self.setData(icon, QtCore.Qt.DecorationRole) + + actions = [] + if hasattr(plugin, "actions") and plugin.actions: + actions = list(plugin.actions) + plugin.actions = actions + + is_checked = True + is_optional = getattr(plugin, "optional", False) + if is_optional: + is_checked = getattr(plugin, "active", True) + + plugin.active = is_checked + plugin.optional = is_optional + + self.setData( + "{}.{}".format(plugin.__module__, plugin.__name__), + Roles.ObjectUIdRole + ) + + self.setFlags( + QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled + ) + + def type(self): + return PluginType + + def data(self, role=QtCore.Qt.DisplayRole): + if role == Roles.IsOptionalRole: + return self.plugin.optional + + if role == Roles.ObjectIdRole: + return self.plugin.id + + if role == Roles.TypeRole: + return self.type() + + if role == QtCore.Qt.CheckStateRole: + return self.plugin.active + + if role == Roles.PathModuleRole: + return self.plugin.__module__ + + if role == Roles.FamiliesRole: + return self.plugin.families + + if role == Roles.DocstringRole: + return self.plugin.__doc__ + + if role == Roles.PluginActionsVisibleRole: + return self._data_actions_visible() + + if role == Roles.PluginValidActionsRole: + return self._data_valid_actions() + + return super(PluginItem, self).data(role) + + def _data_actions_visible(self): + # Can only run actions on active plug-ins. + if not self.plugin.active or not self.plugin.actions: + return False + + publish_states = self.data(Roles.PublishFlagsRole) + if ( + not publish_states & PluginStates.IsCompatible + or publish_states & PluginStates.WasSkipped + ): + return False + + # Context specific actions + for action in self.plugin.actions: + if action.on == "failed": + if publish_states & PluginStates.HasError: + return True + + elif action.on == "succeeded": + if ( + publish_states & PluginStates.WasProcessed + and not publish_states & PluginStates.HasError + ): + return True + + elif action.on == "processed": + if publish_states & PluginStates.WasProcessed: + return True + + elif action.on == "notProcessed": + if not publish_states & PluginStates.WasProcessed: + return True + return False + + def _data_valid_actions(self): + valid_actions = [] + + # Can only run actions on active plug-ins. + if not self.plugin.active or not self.plugin.actions: + return valid_actions + + if not self.plugin.active or not self.plugin.actions: + return False + + publish_states = self.data(Roles.PublishFlagsRole) + if ( + not publish_states & PluginStates.IsCompatible + or publish_states & PluginStates.WasSkipped + ): + return False + + # Context specific actions + for action in self.plugin.actions: + valid = False + if action.on == "failed": + if publish_states & PluginStates.HasError: + valid = True + + elif action.on == "succeeded": + if ( + publish_states & PluginStates.WasProcessed + and not publish_states & PluginStates.HasError + ): + valid = True + + elif action.on == "processed": + if publish_states & PluginStates.WasProcessed: + valid = True + + elif action.on == "notProcessed": + if not publish_states & PluginStates.WasProcessed: + valid = True + + if valid: + valid_actions.append(action) + + if not valid_actions: + return valid_actions + + actions_len = len(valid_actions) + # Discard empty groups + indexex_to_remove = [] + for idx, action in enumerate(valid_actions): + if action.__type__ != "category": + continue + + next_id = idx + 1 + if next_id >= actions_len: + indexex_to_remove.append(idx) + continue + + next = valid_actions[next_id] + if next.__type__ != "action": + indexex_to_remove.append(idx) + + for idx in reversed(indexex_to_remove): + valid_actions.pop(idx) + + return valid_actions + + def setData(self, value, role=None): + if role is None: + role = QtCore.Qt.UserRole + 1 + + if role == QtCore.Qt.CheckStateRole: + if not self.data(Roles.IsEnabledRole): + return False + self.plugin.active = value + self.emitDataChanged() + return True + + elif role == Roles.PluginActionProgressRole: + if isinstance(value, list): + _value = self.data(Roles.PluginActionProgressRole) + for flag in value: + _value |= flag + value = _value + + elif isinstance(value, dict): + _value = self.data(Roles.PluginActionProgressRole) + for flag, _bool in value.items(): + if _bool is True: + _value |= flag + elif _value & flag: + _value ^= flag + value = _value + + elif role == Roles.PublishFlagsRole: + if isinstance(value, list): + _value = self.data(Roles.PublishFlagsRole) + for flag in value: + _value |= flag + value = _value + + elif isinstance(value, dict): + _value = self.data(Roles.PublishFlagsRole) + for flag, _bool in value.items(): + if _bool is True: + _value |= flag + elif _value & flag: + _value ^= flag + value = _value + + if value & PluginStates.HasWarning: + if self.parent(): + self.parent().setData( + {GroupStates.HasWarning: True}, + Roles.PublishFlagsRole + ) + if value & PluginStates.HasError: + if self.parent(): + self.parent().setData( + {GroupStates.HasError: True}, + Roles.PublishFlagsRole + ) + + return super(PluginItem, self).setData(value, role) + + +class GroupItem(QtGui.QStandardItem): + def __init__(self, *args, **kwargs): + self.order = kwargs.pop("order", None) + self.publish_states = 0 + super(GroupItem, self).__init__(*args, **kwargs) + + def flags(self): + return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled + + def data(self, role=QtCore.Qt.DisplayRole): + if role == Roles.PublishFlagsRole: + return self.publish_states + + if role == Roles.TypeRole: + return self.type() + + return super(GroupItem, self).data(role) + + def setData(self, value, role=(QtCore.Qt.UserRole + 1)): + if role == Roles.PublishFlagsRole: + if isinstance(value, list): + _value = self.data(Roles.PublishFlagsRole) + for flag in value: + _value |= flag + value = _value + + elif isinstance(value, dict): + _value = self.data(Roles.PublishFlagsRole) + for flag, _bool in value.items(): + if _bool is True: + _value |= flag + elif _value & flag: + _value ^= flag + value = _value + self.publish_states = value + self.emitDataChanged() + return True + + return super(GroupItem, self).setData(value, role) + + def type(self): + return GroupType + + +class PluginModel(QtGui.QStandardItemModel): + def __init__(self, controller, *args, **kwargs): + super(PluginModel, self).__init__(*args, **kwargs) + + self.controller = controller + self.checkstates = {} + self.group_items = {} + self.plugin_items = {} + + def reset(self): + self.group_items = {} + self.plugin_items = {} + self.clear() + + def append(self, plugin): + plugin_groups = self.controller.order_groups.groups() + label = None + order = None + for _order, _label in reversed(plugin_groups.items()): + if _order is None or plugin.order < _order: + label = _label + order = _order + else: + break + + if label is None: + label = "Other" + + if order is None: + order = 99999999999999 + + group_item = self.group_items.get(label) + if not group_item: + group_item = GroupItem(label, order=order) + self.appendRow(group_item) + self.group_items[label] = group_item + + new_item = PluginItem(plugin) + group_item.appendRow(new_item) + + self.plugin_items[plugin._id] = new_item + + def store_checkstates(self): + self.checkstates.clear() + + for plugin_item in self.plugin_items.values(): + if not plugin_item.plugin.optional: + continue + + uid = plugin_item.data(Roles.ObjectUIdRole) + self.checkstates[uid] = plugin_item.data(QtCore.Qt.CheckStateRole) + + def restore_checkstates(self): + for plugin_item in self.plugin_items.values(): + if not plugin_item.plugin.optional: + continue + + uid = plugin_item.data(Roles.ObjectUIdRole) + state = self.checkstates.get(uid) + if state is not None: + plugin_item.setData(state, QtCore.Qt.CheckStateRole) + + def update_with_result(self, result): + plugin = result["plugin"] + item = self.plugin_items[plugin.id] + + new_flag_states = { + PluginStates.InProgress: False, + PluginStates.WasProcessed: True + } + + publish_states = item.data(Roles.PublishFlagsRole) + + has_warning = publish_states & PluginStates.HasWarning + new_records = result.get("records") or [] + if not has_warning: + for record in new_records: + if not hasattr(record, "levelname"): + continue + + if str(record.levelname).lower() in [ + "warning", "critical", "error" + ]: + new_flag_states[PluginStates.HasWarning] = True + break + + if ( + not publish_states & PluginStates.HasError + and not result["success"] + ): + new_flag_states[PluginStates.HasError] = True + + item.setData(new_flag_states, Roles.PublishFlagsRole) + + records = item.data(Roles.LogRecordsRole) or [] + records.extend(new_records) + + item.setData(records, Roles.LogRecordsRole) + + return item + + def update_compatibility(self): + context = self.controller.context + + families = util.collect_families_from_instances(context, True) + for plugin_item in self.plugin_items.values(): + publish_states = plugin_item.data(Roles.PublishFlagsRole) + if ( + publish_states & PluginStates.WasProcessed + or publish_states & PluginStates.WasSkipped + ): + continue + + is_compatible = False + # A plugin should always show if it has processed. + if plugin_item.plugin.__instanceEnabled__: + compatible_instances = pyblish.logic.instances_by_plugin( + context, plugin_item.plugin + ) + for instance in context: + if not instance.data.get("publish"): + continue + + if instance in compatible_instances: + is_compatible = True + break + else: + plugins = pyblish.logic.plugins_by_families( + [plugin_item.plugin], families + ) + if plugins: + is_compatible = True + + current_is_compatible = publish_states & PluginStates.IsCompatible + if ( + (is_compatible and not current_is_compatible) + or (not is_compatible and current_is_compatible) + ): + new_flag = { + PluginStates.IsCompatible: is_compatible + } + plugin_item.setData(new_flag, Roles.PublishFlagsRole) + + +class PluginFilterProxy(QtCore.QSortFilterProxyModel): + def filterAcceptsRow(self, source_row, source_parent): + index = self.sourceModel().index(source_row, 0, source_parent) + item_type = index.data(Roles.TypeRole) + if item_type != PluginType: + return True + + publish_states = index.data(Roles.PublishFlagsRole) + if ( + publish_states & PluginStates.WasSkipped + or not publish_states & PluginStates.IsCompatible + ): + return False + return True + + +class InstanceItem(QtGui.QStandardItem): + """Instance item implementation.""" + + def __init__(self, instance): + super(InstanceItem, self).__init__() + + self.instance = instance + self.is_context = False + publish_states = getattr(instance, "_publish_states", 0) + if publish_states & InstanceStates.ContextType: + self.is_context = True + + instance._publish_states = publish_states + instance._logs = [] + instance.optional = getattr(instance, "optional", True) + instance.data["publish"] = instance.data.get("publish", True) + instance.data["label"] = ( + instance.data.get("label") + or getattr(instance, "label", None) + or instance.data["name"] + ) + + family = self.data(Roles.FamiliesRole)[0] + self.setData( + "{}.{}".format(family, self.instance.data["name"]), + Roles.ObjectUIdRole + ) + + def flags(self): + return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled + + def type(self): + return InstanceType + + def data(self, role=QtCore.Qt.DisplayRole): + if role == QtCore.Qt.DisplayRole: + if settings.UseLabel: + return self.instance.data["label"] + return self.instance.data["name"] + + if role == QtCore.Qt.DecorationRole: + icon_name = self.instance.data.get("icon") or "file" + return QAwesomeTextIconFactory.icon(icon_name) + + if role == Roles.TypeRole: + return self.type() + + if role == Roles.ObjectIdRole: + return self.instance.id + + if role == Roles.FamiliesRole: + if self.is_context: + return ["Context"] + + families = [] + family = self.instance.data.get("family") + if family: + families.append(family) + + _families = self.instance.data.get("families") or [] + for _family in _families: + if _family not in families: + families.append(_family) + + return families + + if role == Roles.IsOptionalRole: + return self.instance.optional + + if role == QtCore.Qt.CheckStateRole: + return self.instance.data["publish"] + + if role == Roles.PublishFlagsRole: + return self.instance._publish_states + + if role == Roles.LogRecordsRole: + return self.instance._logs + + return super(InstanceItem, self).data(role) + + def setData(self, value, role=(QtCore.Qt.UserRole + 1)): + if role == QtCore.Qt.CheckStateRole: + if not self.data(Roles.IsEnabledRole): + return False + self.instance.data["publish"] = value + self.emitDataChanged() + return True + + if role == Roles.IsEnabledRole: + if not self.instance.optional: + return False + + if role == Roles.PublishFlagsRole: + if isinstance(value, list): + _value = self.instance._publish_states + for flag in value: + _value |= flag + value = _value + + elif isinstance(value, dict): + _value = self.instance._publish_states + for flag, _bool in value.items(): + if _bool is True: + _value |= flag + elif _value & flag: + _value ^= flag + value = _value + + if value & InstanceStates.HasWarning: + if self.parent(): + self.parent().setData( + {GroupStates.HasWarning: True}, + Roles.PublishFlagsRole + ) + if value & InstanceStates.HasError: + if self.parent(): + self.parent().setData( + {GroupStates.HasError: True}, + Roles.PublishFlagsRole + ) + + self.instance._publish_states = value + self.emitDataChanged() + return True + + if role == Roles.LogRecordsRole: + self.instance._logs = value + self.emitDataChanged() + return True + + return super(InstanceItem, self).setData(value, role) + + +class InstanceModel(QtGui.QStandardItemModel): + + group_created = QtCore.Signal(QtCore.QModelIndex) + + def __init__(self, controller, *args, **kwargs): + super(InstanceModel, self).__init__(*args, **kwargs) + + self.controller = controller + self.checkstates = {} + self.group_items = {} + self.instance_items = {} + + def reset(self): + self.group_items = {} + self.instance_items = {} + self.clear() + + def append(self, instance): + new_item = InstanceItem(instance) + families = new_item.data(Roles.FamiliesRole) + group_item = self.group_items.get(families[0]) + if not group_item: + group_item = GroupItem(families[0]) + self.appendRow(group_item) + self.group_items[families[0]] = group_item + self.group_created.emit(group_item.index()) + + group_item.appendRow(new_item) + instance_id = instance.id + self.instance_items[instance_id] = new_item + + def remove(self, instance_id): + instance_item = self.instance_items.pop(instance_id) + parent_item = instance_item.parent() + parent_item.removeRow(instance_item.row()) + if parent_item.rowCount(): + return + + self.group_items.pop(parent_item.data(QtCore.Qt.DisplayRole)) + self.removeRow(parent_item.row()) + + def store_checkstates(self): + self.checkstates.clear() + + for instance_item in self.instance_items.values(): + if not instance_item.instance.optional: + continue + + uid = instance_item.data(Roles.ObjectUIdRole) + self.checkstates[uid] = instance_item.data( + QtCore.Qt.CheckStateRole + ) + + def restore_checkstates(self): + for instance_item in self.instance_items.values(): + if not instance_item.instance.optional: + continue + + uid = instance_item.data(Roles.ObjectUIdRole) + state = self.checkstates.get(uid) + if state is not None: + instance_item.setData(state, QtCore.Qt.CheckStateRole) + + def update_with_result(self, result): + instance = result["instance"] + if instance is None: + instance_id = self.controller.context.id + else: + instance_id = instance.id + + item = self.instance_items.get(instance_id) + if not item: + return + + new_flag_states = { + InstanceStates.InProgress: False + } + + publish_states = item.data(Roles.PublishFlagsRole) + has_warning = publish_states & InstanceStates.HasWarning + new_records = result.get("records") or [] + if not has_warning: + for record in new_records: + if not hasattr(record, "levelname"): + continue + + if str(record.levelname).lower() in [ + "warning", "critical", "error" + ]: + new_flag_states[InstanceStates.HasWarning] = True + break + + if ( + not publish_states & InstanceStates.HasError + and not result["success"] + ): + new_flag_states[InstanceStates.HasError] = True + + item.setData(new_flag_states, Roles.PublishFlagsRole) + + records = item.data(Roles.LogRecordsRole) or [] + records.extend(new_records) + + item.setData(records, Roles.LogRecordsRole) + + return item + + def update_compatibility(self, context, instances): + families = util.collect_families_from_instances(context, True) + for plugin_item in self.plugin_items.values(): + publish_states = plugin_item.data(Roles.PublishFlagsRole) + if ( + publish_states & PluginStates.WasProcessed + or publish_states & PluginStates.WasSkipped + ): + continue + + is_compatible = False + # A plugin should always show if it has processed. + if plugin_item.plugin.__instanceEnabled__: + compatibleInstances = pyblish.logic.instances_by_plugin( + context, plugin_item.plugin + ) + for instance in instances: + if not instance.data.get("publish"): + continue + + if instance in compatibleInstances: + is_compatible = True + break + else: + plugins = pyblish.logic.plugins_by_families( + [plugin_item.plugin], families + ) + if plugins: + is_compatible = True + + current_is_compatible = publish_states & PluginStates.IsCompatible + if ( + (is_compatible and not current_is_compatible) + or (not is_compatible and current_is_compatible) + ): + plugin_item.setData( + {PluginStates.IsCompatible: is_compatible}, + Roles.PublishFlagsRole + ) + + +class ArtistProxy(QtCore.QAbstractProxyModel): + def __init__(self, *args, **kwargs): + self.mapping_from = [] + self.mapping_to = [] + super(ArtistProxy, self).__init__(*args, **kwargs) + + def on_rows_inserted(self, parent_index, from_row, to_row): + if not parent_index.isValid(): + return + + parent_row = parent_index.row() + if parent_row >= len(self.mapping_from): + self.mapping_from.append(list()) + + new_from = None + new_to = None + for row_num in range(from_row, to_row + 1): + new_row = len(self.mapping_to) + new_to = new_row + if new_from is None: + new_from = new_row + + self.mapping_from[parent_row].insert(row_num, new_row) + self.mapping_to.insert(new_row, [parent_row, row_num]) + + self.rowsInserted.emit(self.parent(), new_from, new_to + 1) + + def _remove_rows(self, parent_row, from_row, to_row): + removed_rows = [] + increment_num = self.mapping_from[parent_row][from_row] + _emit_last = None + for row_num in reversed(range(from_row, to_row + 1)): + row = self.mapping_from[parent_row].pop(row_num) + _emit_last = row + removed_rows.append(row) + + _emit_first = int(increment_num) + mapping_from_len = len(self.mapping_from) + mapping_from_parent_len = len(self.mapping_from[parent_row]) + if parent_row < mapping_from_len: + for idx in range(from_row, mapping_from_parent_len): + self.mapping_from[parent_row][idx] = increment_num + increment_num += 1 + + if parent_row < mapping_from_len - 1: + for idx_i in range(parent_row + 1, mapping_from_len): + sub_values = self.mapping_from[idx_i] + if not sub_values: + continue + + for idx_j in range(0, len(sub_values)): + self.mapping_from[idx_i][idx_j] = increment_num + increment_num += 1 + + first_to_row = None + for row in removed_rows: + if first_to_row is None: + first_to_row = row + self.mapping_to.pop(row) + + return (_emit_first, _emit_last) + + def on_rows_removed(self, parent_index, from_row, to_row): + if parent_index.isValid(): + parent_row = parent_index.row() + _emit_first, _emit_last = self._remove_rows( + parent_row, from_row, to_row + ) + self.rowsRemoved.emit(self.parent(), _emit_first, _emit_last) + + else: + removed_rows = False + emit_first = None + emit_last = None + for row_num in reversed(range(from_row, to_row + 1)): + remaining_rows = self.mapping_from[row_num] + if remaining_rows: + removed_rows = True + _emit_first, _emit_last = self._remove_rows( + row_num, 0, len(remaining_rows) - 1 + ) + if emit_first is None: + emit_first = _emit_first + emit_last = _emit_last + + self.mapping_from.pop(row_num) + + diff = to_row - from_row + 1 + mapping_to_len = len(self.mapping_to) + if from_row < mapping_to_len: + for idx in range(from_row, mapping_to_len): + self.mapping_to[idx][0] -= diff + + if removed_rows: + self.rowsRemoved.emit(self.parent(), emit_first, emit_last) + + def on_reset(self): + self.modelReset.emit() + self.mapping_from = [] + self.mapping_to = [] + + def setSourceModel(self, source_model): + super(ArtistProxy, self).setSourceModel(source_model) + source_model.rowsInserted.connect(self.on_rows_inserted) + source_model.rowsRemoved.connect(self.on_rows_removed) + source_model.modelReset.connect(self.on_reset) + source_model.dataChanged.connect(self.on_data_changed) + + def on_data_changed(self, from_index, to_index, roles=None): + proxy_from_index = self.mapFromSource(from_index) + if from_index == to_index: + proxy_to_index = proxy_from_index + else: + proxy_to_index = self.mapFromSource(to_index) + + args = [proxy_from_index, proxy_to_index] + if Qt.__binding__ not in ("PyQt4", "PySide"): + args.append(roles or []) + self.dataChanged.emit(*args) + + def columnCount(self, parent=QtCore.QModelIndex()): + # This is not right for global proxy, but in this case it is enough + return self.sourceModel().columnCount() + + def rowCount(self, parent=QtCore.QModelIndex()): + if parent.isValid(): + return 0 + return len(self.mapping_to) + + def mapFromSource(self, index): + if not index.isValid(): + return QtCore.QModelIndex() + + parent_index = index.parent() + if not parent_index.isValid(): + return QtCore.QModelIndex() + + parent_idx = self.mapping_from[parent_index.row()] + my_row = parent_idx[index.row()] + return self.index(my_row, index.column()) + + def mapToSource(self, index): + if not index.isValid() or index.row() > len(self.mapping_to): + return self.sourceModel().index(index.row(), index.column()) + + parent_row, item_row = self.mapping_to[index.row()] + parent_index = self.sourceModel().index(parent_row, 0) + return self.sourceModel().index(item_row, 0, parent_index) + + def index(self, row, column, parent=QtCore.QModelIndex()): + return self.createIndex(row, column, QtCore.QModelIndex()) + + def parent(self, index=None): + return QtCore.QModelIndex() + + +class TerminalModel(QtGui.QStandardItemModel): + key_label_record_map = ( + ("instance", "Instance"), + ("msg", "Message"), + ("name", "Plugin"), + ("pathname", "Path"), + ("lineno", "Line"), + ("traceback", "Traceback"), + ("levelname", "Level"), + ("threadName", "Thread"), + ("msecs", "Millis") + ) + + item_icon_name = { + "info": "fa.info", + "record": "fa.circle", + "error": "fa.exclamation-triangle", + } + + item_icon_colors = { + "info": "#ffffff", + "error": "#ff4a4a", + "log_debug": "#ff66e8", + "log_info": "#66abff", + "log_warning": "#ffba66", + "log_error": "#ff4d58", + "log_critical": "#ff4f75", + None: "#333333" + } + + level_to_record = ( + (10, "log_debug"), + (20, "log_info"), + (30, "log_warning"), + (40, "log_error"), + (50, "log_critical") + + ) + + def __init__(self, *args, **kwargs): + super(TerminalModel, self).__init__(*args, **kwargs) + self.reset() + + def reset(self): + self.items_to_set_widget = queue.Queue() + self.clear() + + def prepare_records(self, result): + prepared_records = [] + instance_name = None + instance = result["instance"] + if instance is not None: + instance_name = instance.data["name"] + + for record in result.get("records") or []: + if isinstance(record, dict): + record_item = record + else: + record_item = { + "label": text_type(record.msg), + "type": "record", + "levelno": record.levelno, + "threadName": record.threadName, + "name": record.name, + "filename": record.filename, + "pathname": record.pathname, + "lineno": record.lineno, + "msg": text_type(record.msg), + "msecs": record.msecs, + "levelname": record.levelname + } + + if instance_name is not None: + record_item["instance"] = instance_name + + prepared_records.append(record_item) + + error = result.get("error") + if error: + fname, line_no, func, exc = error.traceback + error_item = { + "label": str(error), + "type": "error", + "filename": str(fname), + "lineno": str(line_no), + "func": str(func), + "traceback": error.formatted_traceback, + } + + if instance_name is not None: + error_item["instance"] = instance_name + + prepared_records.append(error_item) + + return prepared_records + + def append(self, record_item): + record_type = record_item["type"] + + terminal_item_type = None + if record_type == "record": + for level, _type in self.level_to_record: + if level > record_item["levelno"]: + break + terminal_item_type = _type + + else: + terminal_item_type = record_type + + icon_color = self.item_icon_colors.get(terminal_item_type) + icon_name = self.item_icon_name.get(record_type) + + top_item_icon = None + if icon_color and icon_name: + top_item_icon = QAwesomeIconFactory.icon(icon_name, icon_color) + + label = record_item["label"].split("\n")[0] + + top_item = QtGui.QStandardItem() + top_item.setData(TerminalLabelType, Roles.TypeRole) + top_item.setData(terminal_item_type, Roles.TerminalItemTypeRole) + top_item.setData(label, QtCore.Qt.DisplayRole) + top_item.setFlags( + QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled + ) + + if top_item_icon: + top_item.setData(top_item_icon, QtCore.Qt.DecorationRole) + + self.appendRow(top_item) + + detail_text = self.prepare_detail_text(record_item) + detail_item = QtGui.QStandardItem(detail_text) + detail_item.setData(TerminalDetailType, Roles.TypeRole) + top_item.appendRow(detail_item) + self.items_to_set_widget.put(detail_item) + + def update_with_result(self, result): + for record in result["records"]: + self.append(record) + + def prepare_detail_text(self, item_data): + if item_data["type"] == "info": + return item_data["label"] + + html_text = "" + for key, title in self.key_label_record_map: + if key not in item_data: + continue + value = item_data[key] + text = ( + str(value) + .replace("<", "<") + .replace(">", ">") + .replace('\n', '
') + .replace(' ', ' ') + ) + + title_tag = ( + '{}: ' + ' color:#fff;\" >{}: ' + ).format(title) + + html_text += ( + '{}' + '{}' + ).format(title_tag, text) + + html_text = '{}
'.format( + html_text + ) + return html_text + + +class TerminalProxy(QtCore.QSortFilterProxyModel): + filter_buttons_checks = { + "info": settings.TerminalFilters.get("info", True), + "log_debug": settings.TerminalFilters.get("log_debug", True), + "log_info": settings.TerminalFilters.get("log_info", True), + "log_warning": settings.TerminalFilters.get("log_warning", True), + "log_error": settings.TerminalFilters.get("log_error", True), + "log_critical": settings.TerminalFilters.get("log_critical", True), + "error": settings.TerminalFilters.get("error", True) + } + + instances = [] + + def __init__(self, view, *args, **kwargs): + super(self.__class__, self).__init__(*args, **kwargs) + self.__class__.instances.append(self) + # Store parent because by own `QSortFilterProxyModel` has `parent` + # method not returning parent QObject in PySide and PyQt4 + self.view = view + + @classmethod + def change_filter(cls, name, value): + cls.filter_buttons_checks[name] = value + + for instance in cls.instances: + try: + instance.invalidate() + if instance.view: + instance.view.updateGeometry() + + except RuntimeError: + # C++ Object was deleted + cls.instances.remove(instance) + + def filterAcceptsRow(self, source_row, source_parent): + index = self.sourceModel().index(source_row, 0, source_parent) + item_type = index.data(Roles.TypeRole) + if not item_type == TerminalLabelType: + return True + terminal_item_type = index.data(Roles.TerminalItemTypeRole) + return self.__class__.filter_buttons_checks.get( + terminal_item_type, True + ) diff --git a/pype/tools/pyblish_pype/settings.py b/pype/tools/pyblish_pype/settings.py new file mode 100644 index 0000000000..a3ae83ff0a --- /dev/null +++ b/pype/tools/pyblish_pype/settings.py @@ -0,0 +1,19 @@ +WindowTitle = "Pyblish" # Customize the window of the pyblish-lite window. +UseLabel = True # Customize whether to show label names for plugins. + +# Customize which tab to start on. Possible choices are: "artist", "overview" +# and "terminal". +InitialTab = "artist" + +# Customize the window size. +WindowSize = (430, 600) + +TerminalFilters = { + "info": True, + "log_debug": True, + "log_info": True, + "log_warning": True, + "log_error": True, + "log_critical": True, + "traceback": True, +} diff --git a/pype/tools/pyblish_pype/util.py b/pype/tools/pyblish_pype/util.py new file mode 100644 index 0000000000..82bf4eb51d --- /dev/null +++ b/pype/tools/pyblish_pype/util.py @@ -0,0 +1,311 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import os +import sys +import numbers +import copy +import collections + +from .vendor.Qt import QtCore +from .vendor.six import text_type +import pyblish.api + +root = os.path.dirname(__file__) + + +def get_asset(*path): + """Return path to asset, relative the install directory + + Usage: + >>> path = get_asset("dir", "to", "asset.png") + >>> path == os.path.join(root, "dir", "to", "asset.png") + True + + Arguments: + path (str): One or more paths, to be concatenated + + """ + + return os.path.join(root, *path) + + +def defer(delay, func): + """Append artificial delay to `func` + + This aids in keeping the GUI responsive, but complicates logic + when producing tests. To combat this, the environment variable ensures + that every operation is synchonous. + + Arguments: + delay (float): Delay multiplier; default 1, 0 means no delay + func (callable): Any callable + + """ + + delay *= float(os.getenv("PYBLISH_DELAY", 1)) + if delay > 0: + return QtCore.QTimer.singleShot(delay, func) + else: + return func() + + +def u_print(msg, **kwargs): + """`print` with encoded unicode. + + `print` unicode may cause UnicodeEncodeError + or non-readable result when `PYTHONIOENCODING` is not set. + this will fix it. + + Arguments: + msg (unicode): Message to print. + **kwargs: Keyword argument for `print` function. + """ + + if isinstance(msg, text_type): + encoding = None + try: + encoding = os.getenv('PYTHONIOENCODING', sys.stdout.encoding) + except AttributeError: + # `sys.stdout.encoding` may not exists. + pass + msg = msg.encode(encoding or 'utf-8', 'replace') + print(msg, **kwargs) + + +def collect_families_from_instances(instances, only_active=False): + all_families = set() + for instance in instances: + if only_active: + if instance.data.get("publish") is False: + continue + family = instance.data.get("family") + if family: + all_families.add(family) + + families = instance.data.get("families") or tuple() + for family in families: + all_families.add(family) + + return list(all_families) + + +class OrderGroups: + # Validator order can be set with environment "PYBLISH_VALIDATION_ORDER" + # - this variable sets when validation button will hide and proecssing + # of validation will end with ability to continue in process + default_validation_order = pyblish.api.ValidatorOrder + 0.5 + + # Group range can be set with environment "PYBLISH_GROUP_RANGE" + default_group_range = 1 + + # Group string can be set with environment "PYBLISH_GROUP_SETTING" + default_groups = { + pyblish.api.CollectorOrder + 0.5: "Collect", + pyblish.api.ValidatorOrder + 0.5: "Validate", + pyblish.api.ExtractorOrder + 0.5: "Extract", + pyblish.api.IntegratorOrder + 0.5: "Integrate", + None: "Other" + } + + # *** This example should have same result as is `default_groups` if + # `group_range` is set to "1" + __groups_str_example__ = ( + # half of `group_range` is added to 0 because number means it is Order + "0=Collect" + # if `<` is before than it means group range is not used + # but is expected that number is already max + ",<1.5=Validate" + # "Extractor" will be used in range `<1.5; 2.5)` + ",<2.5=Extract" + ",<3.5=Integrate" + # "Other" if number is not set than all remaining plugins are in + # - in this case Other's range is <3.5; infinity) + ",Other" + ) + + _groups = None + _validation_order = None + _group_range = None + + def __init__( + self, group_str=None, group_range=None, validation_order=None + ): + super(OrderGroups, self).__init__() + # Override class methods with object methods + self.groups = self._object_groups + self.validation_order = self._object_validation_order + self.group_range = self._object_group_range + self.reset = self._object_reset + + # set + if group_range is not None: + self._group_range = self.parse_group_range( + group_range + ) + + if group_str is not None: + self._groups = self.parse_group_str( + group_str + ) + + if validation_order is not None: + self._validation_order = self.parse_validation_order( + validation_order + ) + + @staticmethod + def _groups_method(obj): + if obj._groups is None: + obj._groups = obj.parse_group_str( + group_range=obj.group_range() + ) + return obj._groups + + @staticmethod + def _reset_method(obj): + obj._groups = None + obj._validation_order = None + obj._group_range = None + + @classmethod + def reset(cls): + return cls._reset_method(cls) + + def _object_reset(self): + return self._reset_method(self) + + @classmethod + def groups(cls): + return cls._groups_method(cls) + + def _object_groups(self): + return self._groups_method(self) + + @staticmethod + def _validation_order_method(obj): + if obj._validation_order is None: + obj._validation_order = obj.parse_validation_order( + group_range=obj.group_range() + ) + return obj._validation_order + + @classmethod + def validation_order(cls): + return cls._validation_order_method(cls) + + def _object_validation_order(self): + return self._validation_order_method(self) + + @staticmethod + def _group_range_method(obj): + if obj._group_range is None: + obj._group_range = obj.parse_group_range() + return obj._group_range + + @classmethod + def group_range(cls): + return cls._group_range_method(cls) + + def _object_group_range(self): + return self._group_range_method(self) + + @staticmethod + def sort_groups(_groups_dict): + sorted_dict = collections.OrderedDict() + + # make sure wont affect any dictionary as pointer + groups_dict = copy.deepcopy(_groups_dict) + last_order = None + if None in groups_dict: + last_order = groups_dict.pop(None) + + for key in sorted(groups_dict): + sorted_dict[key] = groups_dict[key] + + if last_order is not None: + sorted_dict[None] = last_order + + return sorted_dict + + @staticmethod + def parse_group_str(groups_str=None, group_range=None): + if groups_str is None: + groups_str = os.environ.get("PYBLISH_GROUP_SETTING") + + if groups_str is None: + return OrderGroups.sort_groups(OrderGroups.default_groups) + + items = groups_str.split(",") + groups = {} + for item in items: + if "=" not in item: + order = None + label = item + else: + order, label = item.split("=") + order = order.strip() + if not order: + order = None + elif order.startswith("<"): + order = float(order.replace("<", "")) + else: + if group_range is None: + group_range = OrderGroups.default_group_range + print( + "Using default Plugin group range \"{}\".".format( + OrderGroups.default_group_range + ) + ) + order = float(order) + float(group_range) / 2 + + if order in groups: + print(( + "Order \"{}\" is registered more than once." + " Using first found." + ).format(str(order))) + continue + + groups[order] = label + + return OrderGroups.sort_groups(groups) + + @staticmethod + def parse_validation_order(validation_order_value=None, group_range=None): + if validation_order_value is None: + validation_order_value = os.environ.get("PYBLISH_VALIDATION_ORDER") + + if validation_order_value is None: + return OrderGroups.default_validation_order + + if group_range is None: + group_range = OrderGroups.default_group_range + + group_range_half = float(group_range) / 2 + + if isinstance(validation_order_value, numbers.Integral): + return validation_order_value + group_range_half + + if validation_order_value.startswith("<"): + validation_order_value = float( + validation_order_value.replace("<", "") + ) + else: + validation_order_value = ( + float(validation_order_value) + + group_range_half + ) + return validation_order_value + + @staticmethod + def parse_group_range(group_range=None): + if group_range is None: + group_range = os.environ.get("PYBLISH_GROUP_RANGE") + + if group_range is None: + return OrderGroups.default_group_range + + if isinstance(group_range, numbers.Integral): + return group_range + + return float(group_range) diff --git a/pype/tools/pyblish_pype/vendor/Qt.py b/pype/tools/pyblish_pype/vendor/Qt.py new file mode 100644 index 0000000000..841c823c5c --- /dev/null +++ b/pype/tools/pyblish_pype/vendor/Qt.py @@ -0,0 +1,1827 @@ +"""Minimal Python 2 & 3 shim around all Qt bindings + +DOCUMENTATION + 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 + +LICENSE + + See end of file for license (MIT, BSD) information. + +""" + +import os +import sys +import types +import shutil + + +__version__ = "1.2.0.b2" + +# 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") + +try: + long +except NameError: + # Python 3 compatibility + long = int + + +"""Common members of all bindings + +This is where each member of Qt.py is explicitly defined. +It is based on a "lowest common denominator" of all bindings; +including members found in each of the 4 bindings. + +The "_common_members" dictionary is generated using the +build_membership.sh script. + +""" + +_common_members = { + "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", + "QItemSelectionRange", + "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", + "QT_TRANSLATE_NOOP", + "QT_TR_NOOP", + "QT_TR_NOOP_UTF8", + "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" + ], + "QtGui": [ + "QAbstractTextDocumentLayout", + "QActionEvent", + "QBitmap", + "QBrush", + "QClipboard", + "QCloseEvent", + "QColor", + "QConicalGradient", + "QContextMenuEvent", + "QCursor", + "QDesktopServices", + "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", + "QTouchEvent", + "QTransform", + "QValidator", + "QVector2D", + "QVector3D", + "QVector4D", + "QWhatsThisClickedEvent", + "QWheelEvent", + "QWindowStateChangeEvent", + "qAlpha", + "qBlue", + "qGray", + "qGreen", + "qIsGray", + "qRed", + "qRgb", + "qRgba" + ], + "QtHelp": [ + "QHelpContentItem", + "QHelpContentModel", + "QHelpContentWidget", + "QHelpEngine", + "QHelpEngineCore", + "QHelpIndexModel", + "QHelpIndexWidget", + "QHelpSearchEngine", + "QHelpSearchQuery", + "QHelpSearchQueryWidget", + "QHelpSearchResultWidget" + ], + "QtMultimedia": [ + "QAbstractVideoBuffer", + "QAbstractVideoSurface", + "QAudio", + "QAudioDeviceInfo", + "QAudioFormat", + "QAudioInput", + "QAudioOutput", + "QVideoFrame", + "QVideoSurfaceFormat" + ], + "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" + ], + "QtPrintSupport": [ + "QAbstractPrintDialog", + "QPageSetupDialog", + "QPrintDialog", + "QPrintEngine", + "QPrintPreviewDialog", + "QPrintPreviewWidget", + "QPrinter", + "QPrinterInfo" + ], + "QtSql": [ + "QSql", + "QSqlDatabase", + "QSqlDriver", + "QSqlDriverCreatorBase", + "QSqlError", + "QSqlField", + "QSqlIndex", + "QSqlQuery", + "QSqlQueryModel", + "QSqlRecord", + "QSqlRelation", + "QSqlRelationalDelegate", + "QSqlRelationalTableModel", + "QSqlResult", + "QSqlTableModel" + ], + "QtSvg": [ + "QGraphicsSvgItem", + "QSvgGenerator", + "QSvgRenderer", + "QSvgWidget" + ], + "QtTest": [ + "QTest" + ], + "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" + ], + "QtX11Extras": [ + "QX11Info" + ], + "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" + ], + "QtXmlPatterns": [ + "QAbstractMessageHandler", + "QAbstractUriResolver", + "QAbstractXmlNodeModel", + "QAbstractXmlReceiver", + "QSourceLocation", + "QXmlFormatter", + "QXmlItem", + "QXmlName", + "QXmlNamePool", + "QXmlNodeModelIndex", + "QXmlQuery", + "QXmlResultItems", + "QXmlSchema", + "QXmlSchemaValidator", + "QXmlSerializer" + ] +} + + +def _qInstallMessageHandler(handler): + """Install a message handler that works in all bindings + + Args: + handler: A function that takes 3 arguments, or None + """ + def messageOutputHandler(*args): + # In Qt4 bindings, message handlers are passed 2 arguments + # In Qt5 bindings, message handlers are passed 3 arguments + # The first argument is a QtMsgType + # The last argument is the message to be printed + # The Middle argument (if passed) is a QMessageLogContext + if len(args) == 3: + msgType, logContext, msg = args + elif len(args) == 2: + msgType, msg = args + logContext = None + else: + raise TypeError( + "handler expected 2 or 3 arguments, got {0}".format(len(args))) + + if isinstance(msg, bytes): + # In python 3, some bindings pass a bytestring, which cannot be + # used elsewhere. Decoding a python 2 or 3 bytestring object will + # consistently return a unicode object. + msg = msg.decode() + + handler(msgType, logContext, msg) + + passObject = messageOutputHandler if handler else handler + if Qt.IsPySide or Qt.IsPyQt4: + return Qt._QtCore.qInstallMsgHandler(passObject) + elif Qt.IsPySide2 or Qt.IsPyQt5: + return Qt._QtCore.qInstallMessageHandler(passObject) + + +def _getcpppointer(object): + if hasattr(Qt, "_shiboken2"): + return getattr(Qt, "_shiboken2").getCppPointer(object)[0] + elif hasattr(Qt, "_shiboken"): + return getattr(Qt, "_shiboken").getCppPointer(object)[0] + elif hasattr(Qt, "_sip"): + return getattr(Qt, "_sip").unwrapinstance(object) + raise AttributeError("'module' has no attribute 'getCppPointer'") + + +def _wrapinstance(ptr, base=None): + """Enable implicit cast of pointer to most suitable class + + This behaviour is available in sip per default. + + Based on http://nathanhorne.com/pyqtpyside-wrap-instance + + Usage: + This mechanism kicks in under these circumstances. + 1. Qt.py is using PySide 1 or 2. + 2. A `base` argument is not provided. + + See :func:`QtCompat.wrapInstance()` + + Arguments: + ptr (long): Pointer to QObject in memory + base (QObject, optional): Base class to wrap with. Defaults to QObject, + which should handle anything. + + """ + + assert isinstance(ptr, long), "Argument 'ptr' must be of type " + assert (base is None) or issubclass(base, Qt.QtCore.QObject), ( + "Argument 'base' must be of type ") + + if Qt.IsPyQt4 or Qt.IsPyQt5: + func = getattr(Qt, "_sip").wrapinstance + elif Qt.IsPySide2: + func = getattr(Qt, "_shiboken2").wrapInstance + elif Qt.IsPySide: + func = getattr(Qt, "_shiboken").wrapInstance + else: + raise AttributeError("'module' has no attribute 'wrapInstance'") + + if base is None: + q_object = func(long(ptr), Qt.QtCore.QObject) + meta_object = q_object.metaObject() + class_name = meta_object.className() + super_class_name = meta_object.superClass().className() + + if hasattr(Qt.QtWidgets, class_name): + base = getattr(Qt.QtWidgets, class_name) + + elif hasattr(Qt.QtWidgets, super_class_name): + base = getattr(Qt.QtWidgets, super_class_name) + + else: + base = Qt.QtCore.QObject + + return func(long(ptr), base) + + +def _translate(context, sourceText, *args): + # In Qt4 bindings, translate can be passed 2 or 3 arguments + # In Qt5 bindings, translate can be passed 2 arguments + # The first argument is disambiguation[str] + # The last argument is n[int] + # The middle argument can be encoding[QtCore.QCoreApplication.Encoding] + if len(args) == 3: + disambiguation, encoding, n = args + elif len(args) == 2: + disambiguation, n = args + encoding = None + else: + raise TypeError( + "Expected 4 or 5 arguments, got {0}.".format(len(args)+2)) + + if hasattr(Qt.QtCore, "QCoreApplication"): + app = getattr(Qt.QtCore, "QCoreApplication") + else: + raise NotImplementedError( + "Missing QCoreApplication implementation for {binding}".format( + binding=Qt.__binding__, + ) + ) + if Qt.__binding__ in ("PySide2", "PyQt5"): + sanitized_args = [context, sourceText, disambiguation, n] + else: + sanitized_args = [ + context, + sourceText, + disambiguation, + encoding or app.CodecForTr, + n + ] + return app.translate(*sanitized_args) + + +def _loadUi(uifile, baseinstance=None): + """Dynamically load a user interface from the given `uifile` + + This function calls `uic.loadUi` if using PyQt bindings, + else it implements a comparable binding for PySide. + + Documentation: + http://pyqt.sourceforge.net/Docs/PyQt5/designer.html#PyQt5.uic.loadUi + + Arguments: + uifile (str): Absolute path to Qt Designer file. + baseinstance (QWidget): Instantiated QWidget or subclass thereof + + Return: + baseinstance if `baseinstance` is not `None`. Otherwise + return the newly created instance of the user interface. + + """ + if hasattr(Qt, "_uic"): + return Qt._uic.loadUi(uifile, baseinstance) + + elif hasattr(Qt, "_QtUiTools"): + # Implement `PyQt5.uic.loadUi` for PySide(2) + + class _UiLoader(Qt._QtUiTools.QUiLoader): + """Create the user interface in a base instance. + + Unlike `Qt._QtUiTools.QUiLoader` itself this class does not + create a new instance of the top-level widget, but creates the user + interface in an existing instance of the top-level class if needed. + + This mimics the behaviour of `PyQt5.uic.loadUi`. + + """ + + def __init__(self, baseinstance): + super(_UiLoader, self).__init__(baseinstance) + self.baseinstance = baseinstance + + def load(self, uifile, *args, **kwargs): + from xml.etree.ElementTree import ElementTree + + # For whatever reason, if this doesn't happen then + # reading an invalid or non-existing .ui file throws + # a RuntimeError. + etree = ElementTree() + etree.parse(uifile) + + widget = Qt._QtUiTools.QUiLoader.load( + self, uifile, *args, **kwargs) + + # Workaround for PySide 1.0.9, see issue #208 + widget.parentWidget() + + return widget + + def createWidget(self, class_name, parent=None, name=""): + """Called for each widget defined in ui file + + Overridden here to populate `baseinstance` instead. + + """ + + if parent is None and self.baseinstance: + # Supposed to create the top-level widget, + # return the base instance instead + return self.baseinstance + + # For some reason, Line is not in the list of available + # widgets, but works fine, so we have to special case it here. + if class_name in self.availableWidgets() + ["Line"]: + # Create a new widget for child widgets + widget = Qt._QtUiTools.QUiLoader.createWidget(self, + class_name, + parent, + name) + + else: + raise Exception("Custom widget '%s' not supported" + % class_name) + + if self.baseinstance: + # Set an attribute for the new child widget on the base + # instance, just like PyQt5.uic.loadUi does. + setattr(self.baseinstance, name, widget) + + return widget + + widget = _UiLoader(baseinstance).load(uifile) + Qt.QtCore.QMetaObject.connectSlotsByName(widget) + + return widget + + else: + raise NotImplementedError("No implementation available for loadUi") + + +"""Misplaced members + +These members from the original submodule are misplaced relative PySide2 + +""" +_misplaced_members = { + "PySide2": { + "QtGui.QStringListModel": "QtCore.QStringListModel", + "QtCore.Property": "QtCore.Property", + "QtCore.Signal": "QtCore.Signal", + "QtCore.Slot": "QtCore.Slot", + "QtCore.QAbstractProxyModel": "QtCore.QAbstractProxyModel", + "QtCore.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel", + "QtCore.QItemSelection": "QtCore.QItemSelection", + "QtCore.QItemSelectionModel": "QtCore.QItemSelectionModel", + "QtCore.QItemSelectionRange": "QtCore.QItemSelectionRange", + "QtUiTools.QUiLoader": ["QtCompat.loadUi", _loadUi], + "shiboken2.wrapInstance": ["QtCompat.wrapInstance", _wrapinstance], + "shiboken2.getCppPointer": ["QtCompat.getCppPointer", _getcpppointer], + "QtWidgets.qApp": "QtWidgets.QApplication.instance()", + "QtCore.QCoreApplication.translate": [ + "QtCompat.translate", _translate + ], + "QtWidgets.QApplication.translate": [ + "QtCompat.translate", _translate + ], + "QtCore.qInstallMessageHandler": [ + "QtCompat.qInstallMessageHandler", _qInstallMessageHandler + ], + }, + "PyQt5": { + "QtCore.pyqtProperty": "QtCore.Property", + "QtCore.pyqtSignal": "QtCore.Signal", + "QtCore.pyqtSlot": "QtCore.Slot", + "QtCore.QAbstractProxyModel": "QtCore.QAbstractProxyModel", + "QtCore.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel", + "QtCore.QStringListModel": "QtCore.QStringListModel", + "QtCore.QItemSelection": "QtCore.QItemSelection", + "QtCore.QItemSelectionModel": "QtCore.QItemSelectionModel", + "QtCore.QItemSelectionRange": "QtCore.QItemSelectionRange", + "uic.loadUi": ["QtCompat.loadUi", _loadUi], + "sip.wrapinstance": ["QtCompat.wrapInstance", _wrapinstance], + "sip.unwrapinstance": ["QtCompat.getCppPointer", _getcpppointer], + "QtWidgets.qApp": "QtWidgets.QApplication.instance()", + "QtCore.QCoreApplication.translate": [ + "QtCompat.translate", _translate + ], + "QtWidgets.QApplication.translate": [ + "QtCompat.translate", _translate + ], + "QtCore.qInstallMessageHandler": [ + "QtCompat.qInstallMessageHandler", _qInstallMessageHandler + ], + }, + "PySide": { + "QtGui.QAbstractProxyModel": "QtCore.QAbstractProxyModel", + "QtGui.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel", + "QtGui.QStringListModel": "QtCore.QStringListModel", + "QtGui.QItemSelection": "QtCore.QItemSelection", + "QtGui.QItemSelectionModel": "QtCore.QItemSelectionModel", + "QtCore.Property": "QtCore.Property", + "QtCore.Signal": "QtCore.Signal", + "QtCore.Slot": "QtCore.Slot", + "QtGui.QItemSelectionRange": "QtCore.QItemSelectionRange", + "QtGui.QAbstractPrintDialog": "QtPrintSupport.QAbstractPrintDialog", + "QtGui.QPageSetupDialog": "QtPrintSupport.QPageSetupDialog", + "QtGui.QPrintDialog": "QtPrintSupport.QPrintDialog", + "QtGui.QPrintEngine": "QtPrintSupport.QPrintEngine", + "QtGui.QPrintPreviewDialog": "QtPrintSupport.QPrintPreviewDialog", + "QtGui.QPrintPreviewWidget": "QtPrintSupport.QPrintPreviewWidget", + "QtGui.QPrinter": "QtPrintSupport.QPrinter", + "QtGui.QPrinterInfo": "QtPrintSupport.QPrinterInfo", + "QtUiTools.QUiLoader": ["QtCompat.loadUi", _loadUi], + "shiboken.wrapInstance": ["QtCompat.wrapInstance", _wrapinstance], + "shiboken.unwrapInstance": ["QtCompat.getCppPointer", _getcpppointer], + "QtGui.qApp": "QtWidgets.QApplication.instance()", + "QtCore.QCoreApplication.translate": [ + "QtCompat.translate", _translate + ], + "QtGui.QApplication.translate": [ + "QtCompat.translate", _translate + ], + "QtCore.qInstallMsgHandler": [ + "QtCompat.qInstallMessageHandler", _qInstallMessageHandler + ], + }, + "PyQt4": { + "QtGui.QAbstractProxyModel": "QtCore.QAbstractProxyModel", + "QtGui.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel", + "QtGui.QItemSelection": "QtCore.QItemSelection", + "QtGui.QStringListModel": "QtCore.QStringListModel", + "QtGui.QItemSelectionModel": "QtCore.QItemSelectionModel", + "QtCore.pyqtProperty": "QtCore.Property", + "QtCore.pyqtSignal": "QtCore.Signal", + "QtCore.pyqtSlot": "QtCore.Slot", + "QtGui.QItemSelectionRange": "QtCore.QItemSelectionRange", + "QtGui.QAbstractPrintDialog": "QtPrintSupport.QAbstractPrintDialog", + "QtGui.QPageSetupDialog": "QtPrintSupport.QPageSetupDialog", + "QtGui.QPrintDialog": "QtPrintSupport.QPrintDialog", + "QtGui.QPrintEngine": "QtPrintSupport.QPrintEngine", + "QtGui.QPrintPreviewDialog": "QtPrintSupport.QPrintPreviewDialog", + "QtGui.QPrintPreviewWidget": "QtPrintSupport.QPrintPreviewWidget", + "QtGui.QPrinter": "QtPrintSupport.QPrinter", + "QtGui.QPrinterInfo": "QtPrintSupport.QPrinterInfo", + # "QtCore.pyqtSignature": "QtCore.Slot", + "uic.loadUi": ["QtCompat.loadUi", _loadUi], + "sip.wrapinstance": ["QtCompat.wrapInstance", _wrapinstance], + "sip.unwrapinstance": ["QtCompat.getCppPointer", _getcpppointer], + "QtCore.QString": "str", + "QtGui.qApp": "QtWidgets.QApplication.instance()", + "QtCore.QCoreApplication.translate": [ + "QtCompat.translate", _translate + ], + "QtGui.QApplication.translate": [ + "QtCompat.translate", _translate + ], + "QtCore.qInstallMsgHandler": [ + "QtCompat.qInstallMessageHandler", _qInstallMessageHandler + ], + } +} + +""" Compatibility Members + +This dictionary is used to build Qt.QtCompat objects that provide a consistent +interface for obsolete members, and differences in binding return values. + +{ + "binding": { + "classname": { + "targetname": "binding_namespace", + } + } +} +""" +_compatibility_members = { + "PySide2": { + "QWidget": { + "grab": "QtWidgets.QWidget.grab", + }, + "QHeaderView": { + "sectionsClickable": "QtWidgets.QHeaderView.sectionsClickable", + "setSectionsClickable": + "QtWidgets.QHeaderView.setSectionsClickable", + "sectionResizeMode": "QtWidgets.QHeaderView.sectionResizeMode", + "setSectionResizeMode": + "QtWidgets.QHeaderView.setSectionResizeMode", + "sectionsMovable": "QtWidgets.QHeaderView.sectionsMovable", + "setSectionsMovable": "QtWidgets.QHeaderView.setSectionsMovable", + }, + "QFileDialog": { + "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName", + "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames", + "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName", + }, + }, + "PyQt5": { + "QWidget": { + "grab": "QtWidgets.QWidget.grab", + }, + "QHeaderView": { + "sectionsClickable": "QtWidgets.QHeaderView.sectionsClickable", + "setSectionsClickable": + "QtWidgets.QHeaderView.setSectionsClickable", + "sectionResizeMode": "QtWidgets.QHeaderView.sectionResizeMode", + "setSectionResizeMode": + "QtWidgets.QHeaderView.setSectionResizeMode", + "sectionsMovable": "QtWidgets.QHeaderView.sectionsMovable", + "setSectionsMovable": "QtWidgets.QHeaderView.setSectionsMovable", + }, + "QFileDialog": { + "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName", + "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames", + "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName", + }, + }, + "PySide": { + "QWidget": { + "grab": "QtWidgets.QPixmap.grabWidget", + }, + "QHeaderView": { + "sectionsClickable": "QtWidgets.QHeaderView.isClickable", + "setSectionsClickable": "QtWidgets.QHeaderView.setClickable", + "sectionResizeMode": "QtWidgets.QHeaderView.resizeMode", + "setSectionResizeMode": "QtWidgets.QHeaderView.setResizeMode", + "sectionsMovable": "QtWidgets.QHeaderView.isMovable", + "setSectionsMovable": "QtWidgets.QHeaderView.setMovable", + }, + "QFileDialog": { + "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName", + "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames", + "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName", + }, + }, + "PyQt4": { + "QWidget": { + "grab": "QtWidgets.QPixmap.grabWidget", + }, + "QHeaderView": { + "sectionsClickable": "QtWidgets.QHeaderView.isClickable", + "setSectionsClickable": "QtWidgets.QHeaderView.setClickable", + "sectionResizeMode": "QtWidgets.QHeaderView.resizeMode", + "setSectionResizeMode": "QtWidgets.QHeaderView.setResizeMode", + "sectionsMovable": "QtWidgets.QHeaderView.isMovable", + "setSectionsMovable": "QtWidgets.QHeaderView.setMovable", + }, + "QFileDialog": { + "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName", + "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames", + "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName", + }, + }, +} + + +def _apply_site_config(): + try: + import QtSiteConfig + except ImportError: + # If no QtSiteConfig module found, no modifications + # to _common_members are needed. + pass + else: + # Provide the ability to modify the dicts used to build Qt.py + if hasattr(QtSiteConfig, 'update_members'): + QtSiteConfig.update_members(_common_members) + + if hasattr(QtSiteConfig, 'update_misplaced_members'): + QtSiteConfig.update_misplaced_members(members=_misplaced_members) + + if hasattr(QtSiteConfig, 'update_compatibility_members'): + QtSiteConfig.update_compatibility_members( + members=_compatibility_members) + + +def _new_module(name): + return types.ModuleType(__name__ + "." + name) + + +def _import_sub_module(module, name): + """import_sub_module will mimic the function of importlib.import_module""" + module = __import__(module.__name__ + "." + name) + for level in name.split("."): + module = getattr(module, level) + return module + + +def _setup(module, extras): + """Install common submodules""" + + Qt.__binding__ = module.__name__ + + for name in list(_common_members) + extras: + try: + submodule = _import_sub_module( + module, name) + except ImportError: + try: + # For extra modules like sip and shiboken that may not be + # children of the binding. + submodule = __import__(name) + except ImportError: + 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 _reassign_misplaced_members(binding): + """Apply misplaced members from `binding` to Qt.py + + Arguments: + binding (dict): Misplaced members + + """ + + for src, dst in _misplaced_members[binding].items(): + dst_value = None + + src_parts = src.split(".") + src_module = src_parts[0] + src_member = None + if len(src_parts) > 1: + src_member = src_parts[1:] + + if isinstance(dst, (list, tuple)): + dst, dst_value = dst + + dst_parts = dst.split(".") + dst_module = dst_parts[0] + dst_member = None + if len(dst_parts) > 1: + dst_member = dst_parts[1] + + # Get the member we want to store in the namesapce. + if not dst_value: + try: + _part = getattr(Qt, "_" + src_module) + while src_member: + member = src_member.pop(0) + _part = getattr(_part, member) + dst_value = _part + except AttributeError: + # If the member we want to store in the namespace does not + # exist, there is no need to continue. This can happen if a + # request was made to rename a member that didn't exist, for + # example if QtWidgets isn't available on the target platform. + _log("Misplaced member has no source: {}".format(src)) + continue + + try: + src_object = getattr(Qt, dst_module) + except AttributeError: + if dst_module not in _common_members: + # Only create the Qt parent module if its listed in + # _common_members. Without this check, if you remove QtCore + # from _common_members, the default _misplaced_members will add + # Qt.QtCore so it can add Signal, Slot, etc. + msg = 'Not creating missing member module "{m}" for "{c}"' + _log(msg.format(m=dst_module, c=dst_member)) + continue + # If the dst is valid but the Qt parent module does not exist + # then go ahead and create a new module to contain the member. + setattr(Qt, dst_module, _new_module(dst_module)) + src_object = getattr(Qt, dst_module) + # Enable direct import of the new module + sys.modules[__name__ + "." + dst_module] = src_object + + if not dst_value: + dst_value = getattr(Qt, "_" + src_module) + if src_member: + dst_value = getattr(dst_value, src_member) + + setattr( + src_object, + dst_member or dst_module, + dst_value + ) + + +def _build_compatibility_members(binding, decorators=None): + """Apply `binding` to QtCompat + + Arguments: + binding (str): Top level binding in _compatibility_members. + decorators (dict, optional): Provides the ability to decorate the + original Qt methods when needed by a binding. This can be used + to change the returned value to a standard value. The key should + be the classname, the value is a dict where the keys are the + target method names, and the values are the decorator functions. + + """ + + decorators = decorators or dict() + + # Allow optional site-level customization of the compatibility members. + # This method does not need to be implemented in QtSiteConfig. + try: + import QtSiteConfig + except ImportError: + pass + else: + if hasattr(QtSiteConfig, 'update_compatibility_decorators'): + QtSiteConfig.update_compatibility_decorators(binding, decorators) + + _QtCompat = type("QtCompat", (object,), {}) + + for classname, bindings in _compatibility_members[binding].items(): + attrs = {} + for target, binding in bindings.items(): + namespaces = binding.split('.') + try: + src_object = getattr(Qt, "_" + namespaces[0]) + except AttributeError as e: + _log("QtCompat: AttributeError: %s" % e) + # Skip reassignment of non-existing members. + # This can happen if a request was made to + # rename a member that didn't exist, for example + # if QtWidgets isn't available on the target platform. + continue + + # Walk down any remaining namespace getting the object assuming + # that if the first namespace exists the rest will exist. + for namespace in namespaces[1:]: + src_object = getattr(src_object, namespace) + + # decorate the Qt method if a decorator was provided. + if target in decorators.get(classname, []): + # staticmethod must be called on the decorated method to + # prevent a TypeError being raised when the decorated method + # is called. + src_object = staticmethod( + decorators[classname][target](src_object)) + + attrs[target] = src_object + + # Create the QtCompat class and install it into the namespace + compat_class = type(classname, (_QtCompat,), attrs) + setattr(Qt.QtCompat, classname, compat_class) + + +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 + extras = ["QtUiTools"] + try: + try: + # Before merge of PySide and shiboken + import shiboken2 + except ImportError: + # After merge of PySide and shiboken, May 2017 + from PySide2 import shiboken2 + extras.append("shiboken2") + except ImportError: + pass + + _setup(module, extras) + Qt.__binding_version__ = module.__version__ + + if hasattr(Qt, "_shiboken2"): + Qt.QtCompat.wrapInstance = _wrapinstance + Qt.QtCompat.getCppPointer = _getcpppointer + + if hasattr(Qt, "_QtUiTools"): + Qt.QtCompat.loadUi = _loadUi + + if hasattr(Qt, "_QtCore"): + Qt.__qt_version__ = Qt._QtCore.qVersion() + + if hasattr(Qt, "_QtWidgets"): + Qt.QtCompat.setSectionResizeMode = \ + Qt._QtWidgets.QHeaderView.setSectionResizeMode + + _reassign_misplaced_members("PySide2") + _build_compatibility_members("PySide2") + + +def _pyside(): + """Initialise PySide""" + + import PySide as module + extras = ["QtUiTools"] + try: + try: + # Before merge of PySide and shiboken + import shiboken + except ImportError: + # After merge of PySide and shiboken, May 2017 + from PySide import shiboken + extras.append("shiboken") + except ImportError: + pass + + _setup(module, extras) + Qt.__binding_version__ = module.__version__ + + if hasattr(Qt, "_shiboken"): + Qt.QtCompat.wrapInstance = _wrapinstance + Qt.QtCompat.getCppPointer = _getcpppointer + + if hasattr(Qt, "_QtUiTools"): + Qt.QtCompat.loadUi = _loadUi + + if hasattr(Qt, "_QtGui"): + setattr(Qt, "QtWidgets", _new_module("QtWidgets")) + setattr(Qt, "_QtWidgets", Qt._QtGui) + if hasattr(Qt._QtGui, "QX11Info"): + setattr(Qt, "QtX11Extras", _new_module("QtX11Extras")) + Qt.QtX11Extras.QX11Info = Qt._QtGui.QX11Info + + Qt.QtCompat.setSectionResizeMode = Qt._QtGui.QHeaderView.setResizeMode + + if hasattr(Qt, "_QtCore"): + Qt.__qt_version__ = Qt._QtCore.qVersion() + + _reassign_misplaced_members("PySide") + _build_compatibility_members("PySide") + + +def _pyqt5(): + """Initialise PyQt5""" + + import PyQt5 as module + extras = ["uic"] + try: + import sip + extras.append(sip.__name__) + except ImportError: + sip = None + + _setup(module, extras) + if hasattr(Qt, "_sip"): + Qt.QtCompat.wrapInstance = _wrapinstance + Qt.QtCompat.getCppPointer = _getcpppointer + + if hasattr(Qt, "_uic"): + Qt.QtCompat.loadUi = _loadUi + + if hasattr(Qt, "_QtCore"): + Qt.__binding_version__ = Qt._QtCore.PYQT_VERSION_STR + Qt.__qt_version__ = Qt._QtCore.QT_VERSION_STR + + if hasattr(Qt, "_QtWidgets"): + Qt.QtCompat.setSectionResizeMode = \ + Qt._QtWidgets.QHeaderView.setSectionResizeMode + + _reassign_misplaced_members("PyQt5") + _build_compatibility_members('PyQt5') + + +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 + extras = ["uic"] + try: + import sip + extras.append(sip.__name__) + except ImportError: + sip = None + + _setup(module, extras) + if hasattr(Qt, "_sip"): + Qt.QtCompat.wrapInstance = _wrapinstance + Qt.QtCompat.getCppPointer = _getcpppointer + + if hasattr(Qt, "_uic"): + Qt.QtCompat.loadUi = _loadUi + + if hasattr(Qt, "_QtGui"): + setattr(Qt, "QtWidgets", _new_module("QtWidgets")) + setattr(Qt, "_QtWidgets", Qt._QtGui) + if hasattr(Qt._QtGui, "QX11Info"): + setattr(Qt, "QtX11Extras", _new_module("QtX11Extras")) + Qt.QtX11Extras.QX11Info = Qt._QtGui.QX11Info + + Qt.QtCompat.setSectionResizeMode = \ + Qt._QtGui.QHeaderView.setResizeMode + + if hasattr(Qt, "_QtCore"): + Qt.__binding_version__ = Qt._QtCore.PYQT_VERSION_STR + Qt.__qt_version__ = Qt._QtCore.QT_VERSION_STR + + _reassign_misplaced_members("PyQt4") + + # QFileDialog QtCompat decorator + def _standardizeQFileDialog(some_function): + """Decorator that makes PyQt4 return conform to other bindings""" + def wrapper(*args, **kwargs): + ret = (some_function(*args, **kwargs)) + + # PyQt4 only returns the selected filename, force it to a + # standard return of the selected filename, and a empty string + # for the selected filter + return ret, '' + + wrapper.__doc__ = some_function.__doc__ + wrapper.__name__ = some_function.__name__ + + return wrapper + + decorators = { + "QFileDialog": { + "getOpenFileName": _standardizeQFileDialog, + "getOpenFileNames": _standardizeQFileDialog, + "getSaveFileName": _standardizeQFileDialog, + } + } + _build_compatibility_members('PyQt4', decorators) + + +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 uifile, baseinstance=None: 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 QtCompat,") + line = line.replace("QtWidgets.QApplication.translate", + "QtCompat.translate") + if "QtCore.SIGNAL" in line: + raise NotImplementedError("QtCore.SIGNAL is missing from PyQt5 " + "and so Qt.py does not support it: you " + "should avoid defining signals inside " + "your ui files.") + 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)) + + # Allow site-level customization of the available modules. + _apply_site_config() + + 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) + + # Enable direct import of QtCompat + sys.modules['Qt.QtCompat'] = Qt.QtCompat + + # Backwards compatibility + if hasattr(Qt.QtCompat, 'loadUi'): + Qt.QtCompat.load_ui = Qt.QtCompat.loadUi + + +_install() + +# Setup Binding Enum states +Qt.IsPySide2 = Qt.__binding__ == 'PySide2' +Qt.IsPyQt5 = Qt.__binding__ == 'PyQt5' +Qt.IsPySide = Qt.__binding__ == 'PySide' +Qt.IsPyQt4 = Qt.__binding__ == 'PyQt4' + +"""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:]) + + +# 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. +# +# In PySide(2), loadUi does not exist, so we implement it +# +# `_UiLoader` is adapted from the qtpy project, which was further influenced +# by qt-helpers which was released under a 3-clause BSD license which in turn +# is based on a solution at: +# +# - https://gist.github.com/cpbotha/1b42a20c8f3eb9bb7cb8 +# +# The License for this code is as follows: +# +# qt-helpers - a common front-end to various Qt modules +# +# Copyright (c) 2015, Chris Beaumont and Thomas Robitaille +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the +# distribution. +# * Neither the name of the Glue project nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# Which itself was based on the solution at +# +# https://gist.github.com/cpbotha/1b42a20c8f3eb9bb7cb8 +# +# which was released under the MIT license: +# +# Copyright (c) 2011 Sebastian Wiesner +# Modifications by Charl Botha +# +# 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. diff --git a/pype/tools/pyblish_pype/vendor/__init__.py b/pype/tools/pyblish_pype/vendor/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pype/tools/pyblish_pype/vendor/qtawesome/__init__.py b/pype/tools/pyblish_pype/vendor/qtawesome/__init__.py new file mode 100644 index 0000000000..4a0001ebb7 --- /dev/null +++ b/pype/tools/pyblish_pype/vendor/qtawesome/__init__.py @@ -0,0 +1,39 @@ +""" +qtawesome - use font-awesome in PyQt / PySide applications + +This is a port to Python of the C++ QtAwesome library by Rick Blommers +""" +from .iconic_font import IconicFont, set_global_defaults +from .animation import Pulse, Spin +from ._version import version_info, __version__ + +_resource = {'iconic': None, } + + +def _instance(): + if _resource['iconic'] is None: + _resource['iconic'] = IconicFont(('fa', 'fontawesome-webfont.ttf', 'fontawesome-webfont-charmap.json'), + ('ei', 'elusiveicons-webfont.ttf', 'elusiveicons-webfont-charmap.json')) + return _resource['iconic'] + + +def icon(*args, **kwargs): + return _instance().icon(*args, **kwargs) + + +def load_font(*args, **kwargs): + return _instance().load_font(*args, **kwargs) + + +def charmap(prefixed_name): + prefix, name = prefixed_name.split('.') + return _instance().charmap[prefix][name] + + +def font(*args, **kwargs): + return _instance().font(*args, **kwargs) + + +def set_defaults(**kwargs): + return set_global_defaults(**kwargs) + diff --git a/pype/tools/pyblish_pype/vendor/qtawesome/_version.py b/pype/tools/pyblish_pype/vendor/qtawesome/_version.py new file mode 100644 index 0000000000..7af886d1a0 --- /dev/null +++ b/pype/tools/pyblish_pype/vendor/qtawesome/_version.py @@ -0,0 +1,2 @@ +version_info = (0, 3, 0, 'dev') +__version__ = '.'.join(map(str, version_info)) diff --git a/pype/tools/pyblish_pype/vendor/qtawesome/animation.py b/pype/tools/pyblish_pype/vendor/qtawesome/animation.py new file mode 100644 index 0000000000..a9638d74b0 --- /dev/null +++ b/pype/tools/pyblish_pype/vendor/qtawesome/animation.py @@ -0,0 +1,41 @@ +from ..Qt import QtCore + + +class Spin: + + def __init__(self, parent_widget, interval=10, step=1): + self.parent_widget = parent_widget + self.interval, self.step = interval, step + self.info = {} + + def _update(self, parent_widget): + if self.parent_widget in self.info: + timer, angle, step = self.info[self.parent_widget] + + if angle >= 360: + angle = 0 + + angle += step + self.info[parent_widget] = timer, angle, step + parent_widget.update() + + def setup(self, icon_painter, painter, rect): + + if self.parent_widget not in self.info: + timer = QtCore.QTimer() + timer.timeout.connect(lambda: self._update(self.parent_widget)) + self.info[self.parent_widget] = [timer, 0, self.step] + timer.start(self.interval) + else: + timer, angle, self.step = self.info[self.parent_widget] + x_center = rect.width() * 0.5 + y_center = rect.height() * 0.5 + painter.translate(x_center, y_center) + painter.rotate(angle) + painter.translate(-x_center, -y_center) + + +class Pulse(Spin): + + def __init__(self, parent_widget): + Spin.__init__(self, parent_widget, interval=300, step=45) diff --git a/pype/tools/pyblish_pype/vendor/qtawesome/fonts/elusiveicons-webfont-charmap.json b/pype/tools/pyblish_pype/vendor/qtawesome/fonts/elusiveicons-webfont-charmap.json new file mode 100644 index 0000000000..099bcb818c --- /dev/null +++ b/pype/tools/pyblish_pype/vendor/qtawesome/fonts/elusiveicons-webfont-charmap.json @@ -0,0 +1,306 @@ +{ + "address-book": "0xf102", + "address-book-alt": "0xf101", + "adjust": "0xf104", + "adjust-alt": "0xf103", + "adult": "0xf105", + "align-center": "0xf106", + "align-justify": "0xf107", + "align-left": "0xf108", + "align-right": "0xf109", + "arrow-down": "0xf10a", + "arrow-left": "0xf10b", + "arrow-right": "0xf10c", + "arrow-up": "0xf10d", + "asl": "0xf10e", + "asterisk": "0xf10f", + "backward": "0xf110", + "ban-circle": "0xf111", + "barcode": "0xf112", + "behance": "0xf113", + "bell": "0xf114", + "blind": "0xf115", + "blogger": "0xf116", + "bold": "0xf117", + "book": "0xf118", + "bookmark": "0xf11a", + "bookmark-empty": "0xf119", + "braille": "0xf11b", + "briefcase": "0xf11c", + "broom": "0xf11d", + "brush": "0xf11e", + "bulb": "0xf11f", + "bullhorn": "0xf120", + "calendar": "0xf122", + "calendar-sign": "0xf121", + "camera": "0xf123", + "car": "0xf124", + "caret-down": "0xf125", + "caret-left": "0xf126", + "caret-right": "0xf127", + "caret-up": "0xf128", + "cc": "0xf129", + "certificate": "0xf12a", + "check": "0xf12c", + "check-empty": "0xf12b", + "chevron-down": "0xf12d", + "chevron-left": "0xf12e", + "chevron-right": "0xf12f", + "chevron-up": "0xf130", + "child": "0xf131", + "circle-arrow-down": "0xf132", + "circle-arrow-left": "0xf133", + "circle-arrow-right": "0xf134", + "circle-arrow-up": "0xf135", + "cloud": "0xf137", + "cloud-alt": "0xf136", + "cog": "0xf139", + "cog-alt": "0xf138", + "cogs": "0xf13a", + "comment": "0xf13c", + "comment-alt": "0xf13b", + "compass": "0xf13e", + "compass-alt": "0xf13d", + "credit-card": "0xf13f", + "css": "0xf140", + "dashboard": "0xf141", + "delicious": "0xf142", + "deviantart": "0xf143", + "digg": "0xf144", + "download": "0xf146", + "download-alt": "0xf145", + "dribbble": "0xf147", + "edit": "0xf148", + "eject": "0xf149", + "envelope": "0xf14b", + "envelope-alt": "0xf14a", + "error": "0xf14d", + "error-alt": "0xf14c", + "eur": "0xf14e", + "exclamation-sign": "0xf14f", + "eye-close": "0xf150", + "eye-open": "0xf151", + "facebook": "0xf152", + "facetime-video": "0xf153", + "fast-backward": "0xf154", + "fast-forward": "0xf155", + "female": "0xf156", + "file": "0xf15c", + "file-alt": "0xf157", + "file-edit": "0xf159", + "file-edit-alt": "0xf158", + "file-new": "0xf15b", + "file-new-alt": "0xf15a", + "film": "0xf15d", + "filter": "0xf15e", + "fire": "0xf15f", + "flag": "0xf161", + "flag-alt": "0xf160", + "flickr": "0xf162", + "folder": "0xf166", + "folder-close": "0xf163", + "folder-open": "0xf164", + "folder-sign": "0xf165", + "font": "0xf167", + "fontsize": "0xf168", + "fork": "0xf169", + "forward": "0xf16b", + "forward-alt": "0xf16a", + "foursquare": "0xf16c", + "friendfeed": "0xf16e", + "friendfeed-rect": "0xf16d", + "fullscreen": "0xf16f", + "gbp": "0xf170", + "gift": "0xf171", + "github": "0xf173", + "github-text": "0xf172", + "glass": "0xf174", + "glasses": "0xf175", + "globe": "0xf177", + "globe-alt": "0xf176", + "googleplus": "0xf178", + "graph": "0xf17a", + "graph-alt": "0xf179", + "group": "0xf17c", + "group-alt": "0xf17b", + "guidedog": "0xf17d", + "hand-down": "0xf17e", + "hand-left": "0xf17f", + "hand-right": "0xf180", + "hand-up": "0xf181", + "hdd": "0xf182", + "headphones": "0xf183", + "hearing-impaired": "0xf184", + "heart": "0xf187", + "heart-alt": "0xf185", + "heart-empty": "0xf186", + "home": "0xf189", + "home-alt": "0xf188", + "hourglass": "0xf18a", + "idea": "0xf18c", + "idea-alt": "0xf18b", + "inbox": "0xf18f", + "inbox-alt": "0xf18d", + "inbox-box": "0xf18e", + "indent-left": "0xf190", + "indent-right": "0xf191", + "info-circle": "0xf192", + "instagram": "0xf193", + "iphone-home": "0xf194", + "italic": "0xf195", + "key": "0xf196", + "laptop": "0xf198", + "laptop-alt": "0xf197", + "lastfm": "0xf199", + "leaf": "0xf19a", + "lines": "0xf19b", + "link": "0xf19c", + "linkedin": "0xf19d", + "list": "0xf19f", + "list-alt": "0xf19e", + "livejournal": "0xf1a0", + "lock": "0xf1a2", + "lock-alt": "0xf1a1", + "magic": "0xf1a3", + "magnet": "0xf1a4", + "male": "0xf1a5", + "map-marker": "0xf1a7", + "map-marker-alt": "0xf1a6", + "mic": "0xf1a9", + "mic-alt": "0xf1a8", + "minus": "0xf1ab", + "minus-sign": "0xf1aa", + "move": "0xf1ac", + "music": "0xf1ad", + "myspace": "0xf1ae", + "network": "0xf1af", + "off": "0xf1b0", + "ok": "0xf1b3", + "ok-circle": "0xf1b1", + "ok-sign": "0xf1b2", + "opensource": "0xf1b4", + "paper-clip": "0xf1b6", + "paper-clip-alt": "0xf1b5", + "path": "0xf1b7", + "pause": "0xf1b9", + "pause-alt": "0xf1b8", + "pencil": "0xf1bb", + "pencil-alt": "0xf1ba", + "person": "0xf1bc", + "phone": "0xf1be", + "phone-alt": "0xf1bd", + "photo": "0xf1c0", + "photo-alt": "0xf1bf", + "picasa": "0xf1c1", + "picture": "0xf1c2", + "pinterest": "0xf1c3", + "plane": "0xf1c4", + "play": "0xf1c7", + "play-alt": "0xf1c5", + "play-circle": "0xf1c6", + "plurk": "0xf1c9", + "plurk-alt": "0xf1c8", + "plus": "0xf1cb", + "plus-sign": "0xf1ca", + "podcast": "0xf1cc", + "print": "0xf1cd", + "puzzle": "0xf1ce", + "qrcode": "0xf1cf", + "question": "0xf1d1", + "question-sign": "0xf1d0", + "quote-alt": "0xf1d2", + "quote-right": "0xf1d4", + "quote-right-alt": "0xf1d3", + "quotes": "0xf1d5", + "random": "0xf1d6", + "record": "0xf1d7", + "reddit": "0xf1d8", + "redux": "0xf1d9", + "refresh": "0xf1da", + "remove": "0xf1dd", + "remove-circle": "0xf1db", + "remove-sign": "0xf1dc", + "repeat": "0xf1df", + "repeat-alt": "0xf1de", + "resize-full": "0xf1e0", + "resize-horizontal": "0xf1e1", + "resize-small": "0xf1e2", + "resize-vertical": "0xf1e3", + "return-key": "0xf1e4", + "retweet": "0xf1e5", + "reverse-alt": "0xf1e6", + "road": "0xf1e7", + "rss": "0xf1e8", + "scissors": "0xf1e9", + "screen": "0xf1eb", + "screen-alt": "0xf1ea", + "screenshot": "0xf1ec", + "search": "0xf1ee", + "search-alt": "0xf1ed", + "share": "0xf1f0", + "share-alt": "0xf1ef", + "shopping-cart": "0xf1f2", + "shopping-cart-sign": "0xf1f1", + "signal": "0xf1f3", + "skype": "0xf1f4", + "slideshare": "0xf1f5", + "smiley": "0xf1f7", + "smiley-alt": "0xf1f6", + "soundcloud": "0xf1f8", + "speaker": "0xf1f9", + "spotify": "0xf1fa", + "stackoverflow": "0xf1fb", + "star": "0xf1fe", + "star-alt": "0xf1fc", + "star-empty": "0xf1fd", + "step-backward": "0xf1ff", + "step-forward": "0xf200", + "stop": "0xf202", + "stop-alt": "0xf201", + "stumbleupon": "0xf203", + "tag": "0xf204", + "tags": "0xf205", + "tasks": "0xf206", + "text-height": "0xf207", + "text-width": "0xf208", + "th": "0xf20b", + "th-large": "0xf209", + "th-list": "0xf20a", + "thumbs-down": "0xf20c", + "thumbs-up": "0xf20d", + "time": "0xf20f", + "time-alt": "0xf20e", + "tint": "0xf210", + "torso": "0xf211", + "trash": "0xf213", + "trash-alt": "0xf212", + "tumblr": "0xf214", + "twitter": "0xf215", + "universal-access": "0xf216", + "unlock": "0xf218", + "unlock-alt": "0xf217", + "upload": "0xf219", + "usd": "0xf21a", + "user": "0xf21b", + "viadeo": "0xf21c", + "video": "0xf21f", + "video-alt": "0xf21d", + "video-chat": "0xf21e", + "view-mode": "0xf220", + "vimeo": "0xf221", + "vkontakte": "0xf222", + "volume-down": "0xf223", + "volume-off": "0xf224", + "volume-up": "0xf225", + "w3c": "0xf226", + "warning-sign": "0xf227", + "website": "0xf229", + "website-alt": "0xf228", + "wheelchair": "0xf22a", + "wordpress": "0xf22b", + "wrench": "0xf22d", + "wrench-alt": "0xf22c", + "youtube": "0xf22e", + "zoom-in": "0xf22f", + "zoom-out": "0xf230" +} diff --git a/pype/tools/pyblish_pype/vendor/qtawesome/fonts/elusiveicons-webfont.ttf b/pype/tools/pyblish_pype/vendor/qtawesome/fonts/elusiveicons-webfont.ttf new file mode 100644 index 0000000000..b6fe85d4b2 Binary files /dev/null and b/pype/tools/pyblish_pype/vendor/qtawesome/fonts/elusiveicons-webfont.ttf differ diff --git a/pype/tools/pyblish_pype/vendor/qtawesome/fonts/fontawesome-webfont-charmap.json b/pype/tools/pyblish_pype/vendor/qtawesome/fonts/fontawesome-webfont-charmap.json new file mode 100644 index 0000000000..0e97d031e6 --- /dev/null +++ b/pype/tools/pyblish_pype/vendor/qtawesome/fonts/fontawesome-webfont-charmap.json @@ -0,0 +1,696 @@ +{ + "500px": "f26e", + "adjust": "f042", + "adn": "f170", + "align-center": "f037", + "align-justify": "f039", + "align-left": "f036", + "align-right": "f038", + "amazon": "f270", + "ambulance": "f0f9", + "anchor": "f13d", + "android": "f17b", + "angellist": "f209", + "angle-double-down": "f103", + "angle-double-left": "f100", + "angle-double-right": "f101", + "angle-double-up": "f102", + "angle-down": "f107", + "angle-left": "f104", + "angle-right": "f105", + "angle-up": "f106", + "apple": "f179", + "archive": "f187", + "area-chart": "f1fe", + "arrow-circle-down": "f0ab", + "arrow-circle-left": "f0a8", + "arrow-circle-o-down": "f01a", + "arrow-circle-o-left": "f190", + "arrow-circle-o-right": "f18e", + "arrow-circle-o-up": "f01b", + "arrow-circle-right": "f0a9", + "arrow-circle-up": "f0aa", + "arrow-down": "f063", + "arrow-left": "f060", + "arrow-right": "f061", + "arrow-up": "f062", + "arrows": "f047", + "arrows-alt": "f0b2", + "arrows-h": "f07e", + "arrows-v": "f07d", + "asterisk": "f069", + "at": "f1fa", + "automobile": "f1b9", + "backward": "f04a", + "balance-scale": "f24e", + "ban": "f05e", + "bank": "f19c", + "bar-chart": "f080", + "bar-chart-o": "f080", + "barcode": "f02a", + "bars": "f0c9", + "battery-0": "f244", + "battery-1": "f243", + "battery-2": "f242", + "battery-3": "f241", + "battery-4": "f240", + "battery-empty": "f244", + "battery-full": "f240", + "battery-half": "f242", + "battery-quarter": "f243", + "battery-three-quarters": "f241", + "bed": "f236", + "beer": "f0fc", + "behance": "f1b4", + "behance-square": "f1b5", + "bell": "f0f3", + "bell-o": "f0a2", + "bell-slash": "f1f6", + "bell-slash-o": "f1f7", + "bicycle": "f206", + "binoculars": "f1e5", + "birthday-cake": "f1fd", + "bitbucket": "f171", + "bitbucket-square": "f172", + "bitcoin": "f15a", + "black-tie": "f27e", + "bluetooth": "f293", + "bluetooth-b": "f294", + "bold": "f032", + "bolt": "f0e7", + "bomb": "f1e2", + "book": "f02d", + "bookmark": "f02e", + "bookmark-o": "f097", + "briefcase": "f0b1", + "btc": "f15a", + "bug": "f188", + "building": "f1ad", + "building-o": "f0f7", + "bullhorn": "f0a1", + "bullseye": "f140", + "bus": "f207", + "buysellads": "f20d", + "cab": "f1ba", + "calculator": "f1ec", + "calendar": "f073", + "calendar-check-o": "f274", + "calendar-minus-o": "f272", + "calendar-o": "f133", + "calendar-plus-o": "f271", + "calendar-times-o": "f273", + "camera": "f030", + "camera-retro": "f083", + "car": "f1b9", + "caret-down": "f0d7", + "caret-left": "f0d9", + "caret-right": "f0da", + "caret-square-o-down": "f150", + "caret-square-o-left": "f191", + "caret-square-o-right": "f152", + "caret-square-o-up": "f151", + "caret-up": "f0d8", + "cart-arrow-down": "f218", + "cart-plus": "f217", + "cc": "f20a", + "cc-amex": "f1f3", + "cc-diners-club": "f24c", + "cc-discover": "f1f2", + "cc-jcb": "f24b", + "cc-mastercard": "f1f1", + "cc-paypal": "f1f4", + "cc-stripe": "f1f5", + "cc-visa": "f1f0", + "certificate": "f0a3", + "chain": "f0c1", + "chain-broken": "f127", + "check": "f00c", + "check-circle": "f058", + "check-circle-o": "f05d", + "check-square": "f14a", + "check-square-o": "f046", + "chevron-circle-down": "f13a", + "chevron-circle-left": "f137", + "chevron-circle-right": "f138", + "chevron-circle-up": "f139", + "chevron-down": "f078", + "chevron-left": "f053", + "chevron-right": "f054", + "chevron-up": "f077", + "child": "f1ae", + "chrome": "f268", + "circle": "f111", + "circle-o": "f10c", + "circle-o-notch": "f1ce", + "circle-thin": "f1db", + "clipboard": "f0ea", + "clock-o": "f017", + "clone": "f24d", + "close": "f00d", + "cloud": "f0c2", + "cloud-download": "f0ed", + "cloud-upload": "f0ee", + "cny": "f157", + "code": "f121", + "code-fork": "f126", + "codepen": "f1cb", + "codiepie": "f284", + "coffee": "f0f4", + "cog": "f013", + "cogs": "f085", + "columns": "f0db", + "comment": "f075", + "comment-o": "f0e5", + "commenting": "f27a", + "commenting-o": "f27b", + "comments": "f086", + "comments-o": "f0e6", + "compass": "f14e", + "compress": "f066", + "connectdevelop": "f20e", + "contao": "f26d", + "copy": "f0c5", + "copyright": "f1f9", + "creative-commons": "f25e", + "credit-card": "f09d", + "credit-card-alt": "f283", + "crop": "f125", + "crosshairs": "f05b", + "css3": "f13c", + "cube": "f1b2", + "cubes": "f1b3", + "cut": "f0c4", + "cutlery": "f0f5", + "dashboard": "f0e4", + "dashcube": "f210", + "database": "f1c0", + "dedent": "f03b", + "delicious": "f1a5", + "desktop": "f108", + "deviantart": "f1bd", + "diamond": "f219", + "digg": "f1a6", + "dollar": "f155", + "dot-circle-o": "f192", + "download": "f019", + "dribbble": "f17d", + "dropbox": "f16b", + "drupal": "f1a9", + "edge": "f282", + "edit": "f044", + "eject": "f052", + "ellipsis-h": "f141", + "ellipsis-v": "f142", + "empire": "f1d1", + "envelope": "f0e0", + "envelope-o": "f003", + "envelope-square": "f199", + "eraser": "f12d", + "eur": "f153", + "euro": "f153", + "exchange": "f0ec", + "exclamation": "f12a", + "exclamation-circle": "f06a", + "exclamation-triangle": "f071", + "expand": "f065", + "expeditedssl": "f23e", + "external-link": "f08e", + "external-link-square": "f14c", + "eye": "f06e", + "eye-slash": "f070", + "eyedropper": "f1fb", + "facebook": "f09a", + "facebook-f": "f09a", + "facebook-official": "f230", + "facebook-square": "f082", + "fast-backward": "f049", + "fast-forward": "f050", + "fax": "f1ac", + "feed": "f09e", + "female": "f182", + "fighter-jet": "f0fb", + "file": "f15b", + "file-archive-o": "f1c6", + "file-audio-o": "f1c7", + "file-code-o": "f1c9", + "file-excel-o": "f1c3", + "file-image-o": "f1c5", + "file-movie-o": "f1c8", + "file-o": "f016", + "file-pdf-o": "f1c1", + "file-photo-o": "f1c5", + "file-picture-o": "f1c5", + "file-powerpoint-o": "f1c4", + "file-sound-o": "f1c7", + "file-text": "f15c", + "file-text-o": "f0f6", + "file-video-o": "f1c8", + "file-word-o": "f1c2", + "file-zip-o": "f1c6", + "files-o": "f0c5", + "film": "f008", + "filter": "f0b0", + "fire": "f06d", + "fire-extinguisher": "f134", + "firefox": "f269", + "flag": "f024", + "flag-checkered": "f11e", + "flag-o": "f11d", + "flash": "f0e7", + "flask": "f0c3", + "flickr": "f16e", + "floppy-o": "f0c7", + "folder": "f07b", + "folder-o": "f114", + "folder-open": "f07c", + "folder-open-o": "f115", + "font": "f031", + "fonticons": "f280", + "fort-awesome": "f286", + "forumbee": "f211", + "forward": "f04e", + "foursquare": "f180", + "frown-o": "f119", + "futbol-o": "f1e3", + "gamepad": "f11b", + "gavel": "f0e3", + "gbp": "f154", + "ge": "f1d1", + "gear": "f013", + "gears": "f085", + "genderless": "f22d", + "get-pocket": "f265", + "gg": "f260", + "gg-circle": "f261", + "gift": "f06b", + "git": "f1d3", + "git-square": "f1d2", + "github": "f09b", + "github-alt": "f113", + "github-square": "f092", + "gittip": "f184", + "glass": "f000", + "globe": "f0ac", + "google": "f1a0", + "google-plus": "f0d5", + "google-plus-square": "f0d4", + "google-wallet": "f1ee", + "graduation-cap": "f19d", + "gratipay": "f184", + "group": "f0c0", + "h-square": "f0fd", + "hacker-news": "f1d4", + "hand-grab-o": "f255", + "hand-lizard-o": "f258", + "hand-o-down": "f0a7", + "hand-o-left": "f0a5", + "hand-o-right": "f0a4", + "hand-o-up": "f0a6", + "hand-paper-o": "f256", + "hand-peace-o": "f25b", + "hand-pointer-o": "f25a", + "hand-rock-o": "f255", + "hand-scissors-o": "f257", + "hand-spock-o": "f259", + "hand-stop-o": "f256", + "hashtag": "f292", + "hdd-o": "f0a0", + "header": "f1dc", + "headphones": "f025", + "heart": "f004", + "heart-o": "f08a", + "heartbeat": "f21e", + "history": "f1da", + "home": "f015", + "hospital-o": "f0f8", + "hotel": "f236", + "hourglass": "f254", + "hourglass-1": "f251", + "hourglass-2": "f252", + "hourglass-3": "f253", + "hourglass-end": "f253", + "hourglass-half": "f252", + "hourglass-o": "f250", + "hourglass-start": "f251", + "houzz": "f27c", + "html5": "f13b", + "i-cursor": "f246", + "ils": "f20b", + "image": "f03e", + "inbox": "f01c", + "indent": "f03c", + "industry": "f275", + "info": "f129", + "info-circle": "f05a", + "inr": "f156", + "instagram": "f16d", + "institution": "f19c", + "internet-explorer": "f26b", + "intersex": "f224", + "ioxhost": "f208", + "italic": "f033", + "joomla": "f1aa", + "jpy": "f157", + "jsfiddle": "f1cc", + "key": "f084", + "keyboard-o": "f11c", + "krw": "f159", + "language": "f1ab", + "laptop": "f109", + "lastfm": "f202", + "lastfm-square": "f203", + "leaf": "f06c", + "leanpub": "f212", + "legal": "f0e3", + "lemon-o": "f094", + "level-down": "f149", + "level-up": "f148", + "life-bouy": "f1cd", + "life-buoy": "f1cd", + "life-ring": "f1cd", + "life-saver": "f1cd", + "lightbulb-o": "f0eb", + "line-chart": "f201", + "link": "f0c1", + "linkedin": "f0e1", + "linkedin-square": "f08c", + "linux": "f17c", + "list": "f03a", + "list-alt": "f022", + "list-ol": "f0cb", + "list-ul": "f0ca", + "location-arrow": "f124", + "lock": "f023", + "long-arrow-down": "f175", + "long-arrow-left": "f177", + "long-arrow-right": "f178", + "long-arrow-up": "f176", + "magic": "f0d0", + "magnet": "f076", + "mail-forward": "f064", + "mail-reply": "f112", + "mail-reply-all": "f122", + "male": "f183", + "map": "f279", + "map-marker": "f041", + "map-o": "f278", + "map-pin": "f276", + "map-signs": "f277", + "mars": "f222", + "mars-double": "f227", + "mars-stroke": "f229", + "mars-stroke-h": "f22b", + "mars-stroke-v": "f22a", + "maxcdn": "f136", + "meanpath": "f20c", + "medium": "f23a", + "medkit": "f0fa", + "meh-o": "f11a", + "mercury": "f223", + "microphone": "f130", + "microphone-slash": "f131", + "minus": "f068", + "minus-circle": "f056", + "minus-square": "f146", + "minus-square-o": "f147", + "mixcloud": "f289", + "mobile": "f10b", + "mobile-phone": "f10b", + "modx": "f285", + "money": "f0d6", + "moon-o": "f186", + "mortar-board": "f19d", + "motorcycle": "f21c", + "mouse-pointer": "f245", + "music": "f001", + "navicon": "f0c9", + "neuter": "f22c", + "newspaper-o": "f1ea", + "object-group": "f247", + "object-ungroup": "f248", + "odnoklassniki": "f263", + "odnoklassniki-square": "f264", + "opencart": "f23d", + "openid": "f19b", + "opera": "f26a", + "optin-monster": "f23c", + "outdent": "f03b", + "pagelines": "f18c", + "paint-brush": "f1fc", + "paper-plane": "f1d8", + "paper-plane-o": "f1d9", + "paperclip": "f0c6", + "paragraph": "f1dd", + "paste": "f0ea", + "pause": "f04c", + "pause-circle": "f28b", + "pause-circle-o": "f28c", + "paw": "f1b0", + "paypal": "f1ed", + "pencil": "f040", + "pencil-square": "f14b", + "pencil-square-o": "f044", + "percent": "f295", + "phone": "f095", + "phone-square": "f098", + "photo": "f03e", + "picture-o": "f03e", + "pie-chart": "f200", + "pied-piper": "f1a7", + "pied-piper-alt": "f1a8", + "pinterest": "f0d2", + "pinterest-p": "f231", + "pinterest-square": "f0d3", + "plane": "f072", + "play": "f04b", + "play-circle": "f144", + "play-circle-o": "f01d", + "plug": "f1e6", + "plus": "f067", + "plus-circle": "f055", + "plus-square": "f0fe", + "plus-square-o": "f196", + "power-off": "f011", + "print": "f02f", + "product-hunt": "f288", + "puzzle-piece": "f12e", + "qq": "f1d6", + "qrcode": "f029", + "question": "f128", + "question-circle": "f059", + "quote-left": "f10d", + "quote-right": "f10e", + "ra": "f1d0", + "random": "f074", + "rebel": "f1d0", + "recycle": "f1b8", + "reddit": "f1a1", + "reddit-alien": "f281", + "reddit-square": "f1a2", + "refresh": "f021", + "registered": "f25d", + "remove": "f00d", + "renren": "f18b", + "reorder": "f0c9", + "repeat": "f01e", + "reply": "f112", + "reply-all": "f122", + "retweet": "f079", + "rmb": "f157", + "road": "f018", + "rocket": "f135", + "rotate-left": "f0e2", + "rotate-right": "f01e", + "rouble": "f158", + "rss": "f09e", + "rss-square": "f143", + "rub": "f158", + "ruble": "f158", + "rupee": "f156", + "safari": "f267", + "save": "f0c7", + "scissors": "f0c4", + "scribd": "f28a", + "search": "f002", + "search-minus": "f010", + "search-plus": "f00e", + "sellsy": "f213", + "send": "f1d8", + "send-o": "f1d9", + "server": "f233", + "share": "f064", + "share-alt": "f1e0", + "share-alt-square": "f1e1", + "share-square": "f14d", + "share-square-o": "f045", + "shekel": "f20b", + "sheqel": "f20b", + "shield": "f132", + "ship": "f21a", + "shirtsinbulk": "f214", + "shopping-bag": "f290", + "shopping-basket": "f291", + "shopping-cart": "f07a", + "sign-in": "f090", + "sign-out": "f08b", + "signal": "f012", + "simplybuilt": "f215", + "sitemap": "f0e8", + "skyatlas": "f216", + "skype": "f17e", + "slack": "f198", + "sliders": "f1de", + "slideshare": "f1e7", + "smile-o": "f118", + "soccer-ball-o": "f1e3", + "sort": "f0dc", + "sort-alpha-asc": "f15d", + "sort-alpha-desc": "f15e", + "sort-amount-asc": "f160", + "sort-amount-desc": "f161", + "sort-asc": "f0de", + "sort-desc": "f0dd", + "sort-down": "f0dd", + "sort-numeric-asc": "f162", + "sort-numeric-desc": "f163", + "sort-up": "f0de", + "soundcloud": "f1be", + "space-shuttle": "f197", + "spinner": "f110", + "spoon": "f1b1", + "spotify": "f1bc", + "square": "f0c8", + "square-o": "f096", + "stack-exchange": "f18d", + "stack-overflow": "f16c", + "star": "f005", + "star-half": "f089", + "star-half-empty": "f123", + "star-half-full": "f123", + "star-half-o": "f123", + "star-o": "f006", + "steam": "f1b6", + "steam-square": "f1b7", + "step-backward": "f048", + "step-forward": "f051", + "stethoscope": "f0f1", + "sticky-note": "f249", + "sticky-note-o": "f24a", + "stop": "f04d", + "stop-circle": "f28d", + "stop-circle-o": "f28e", + "street-view": "f21d", + "strikethrough": "f0cc", + "stumbleupon": "f1a4", + "stumbleupon-circle": "f1a3", + "subscript": "f12c", + "subway": "f239", + "suitcase": "f0f2", + "sun-o": "f185", + "superscript": "f12b", + "support": "f1cd", + "table": "f0ce", + "tablet": "f10a", + "tachometer": "f0e4", + "tag": "f02b", + "tags": "f02c", + "tasks": "f0ae", + "taxi": "f1ba", + "television": "f26c", + "tencent-weibo": "f1d5", + "terminal": "f120", + "text-height": "f034", + "text-width": "f035", + "th": "f00a", + "th-large": "f009", + "th-list": "f00b", + "thumb-tack": "f08d", + "thumbs-down": "f165", + "thumbs-o-down": "f088", + "thumbs-o-up": "f087", + "thumbs-up": "f164", + "ticket": "f145", + "times": "f00d", + "times-circle": "f057", + "times-circle-o": "f05c", + "tint": "f043", + "toggle-down": "f150", + "toggle-left": "f191", + "toggle-off": "f204", + "toggle-on": "f205", + "toggle-right": "f152", + "toggle-up": "f151", + "trademark": "f25c", + "train": "f238", + "transgender": "f224", + "transgender-alt": "f225", + "trash": "f1f8", + "trash-o": "f014", + "tree": "f1bb", + "trello": "f181", + "tripadvisor": "f262", + "trophy": "f091", + "truck": "f0d1", + "try": "f195", + "tty": "f1e4", + "tumblr": "f173", + "tumblr-square": "f174", + "turkish-lira": "f195", + "tv": "f26c", + "twitch": "f1e8", + "twitter": "f099", + "twitter-square": "f081", + "umbrella": "f0e9", + "underline": "f0cd", + "undo": "f0e2", + "university": "f19c", + "unlink": "f127", + "unlock": "f09c", + "unlock-alt": "f13e", + "unsorted": "f0dc", + "upload": "f093", + "usb": "f287", + "usd": "f155", + "user": "f007", + "user-md": "f0f0", + "user-plus": "f234", + "user-secret": "f21b", + "user-times": "f235", + "users": "f0c0", + "venus": "f221", + "venus-double": "f226", + "venus-mars": "f228", + "viacoin": "f237", + "video-camera": "f03d", + "vimeo": "f27d", + "vimeo-square": "f194", + "vine": "f1ca", + "vk": "f189", + "volume-down": "f027", + "volume-off": "f026", + "volume-up": "f028", + "warning": "f071", + "wechat": "f1d7", + "weibo": "f18a", + "weixin": "f1d7", + "whatsapp": "f232", + "wheelchair": "f193", + "wifi": "f1eb", + "wikipedia-w": "f266", + "windows": "f17a", + "won": "f159", + "wordpress": "f19a", + "wrench": "f0ad", + "xing": "f168", + "xing-square": "f169", + "y-combinator": "f23b", + "y-combinator-square": "f1d4", + "yahoo": "f19e", + "yc": "f23b", + "yc-square": "f1d4", + "yelp": "f1e9", + "yen": "f157", + "youtube": "f167", + "youtube-play": "f16a", + "youtube-square": "f166" +} \ No newline at end of file diff --git a/pype/tools/pyblish_pype/vendor/qtawesome/fonts/fontawesome-webfont.ttf b/pype/tools/pyblish_pype/vendor/qtawesome/fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000000..26dea7951a Binary files /dev/null and b/pype/tools/pyblish_pype/vendor/qtawesome/fonts/fontawesome-webfont.ttf differ diff --git a/pype/tools/pyblish_pype/vendor/qtawesome/iconic_font.py b/pype/tools/pyblish_pype/vendor/qtawesome/iconic_font.py new file mode 100644 index 0000000000..70f5ec2dec --- /dev/null +++ b/pype/tools/pyblish_pype/vendor/qtawesome/iconic_font.py @@ -0,0 +1,287 @@ +"""Classes handling iconic fonts""" + +from __future__ import print_function + +import json +import os + +from .. import six +from ..Qt import QtCore, QtGui + + +_default_options = { + 'color': QtGui.QColor(50, 50, 50), + 'color_disabled': QtGui.QColor(150, 150, 150), + 'opacity': 1.0, + 'scale_factor': 1.0, +} + + +def set_global_defaults(**kwargs): + """Set global defaults for all icons""" + valid_options = ['active', 'animation', 'color', 'color_active', + 'color_disabled', 'color_selected', 'disabled', 'offset', + 'scale_factor', 'selected'] + for kw in kwargs: + if kw in valid_options: + _default_options[kw] = kwargs[kw] + else: + error = "Invalid option '{0}'".format(kw) + raise KeyError(error) + + +class CharIconPainter: + + """Char icon painter""" + + def paint(self, iconic, painter, rect, mode, state, options): + """Main paint method""" + for opt in options: + self._paint_icon(iconic, painter, rect, mode, state, opt) + + def _paint_icon(self, iconic, painter, rect, mode, state, options): + """Paint a single icon""" + painter.save() + color, char = options['color'], options['char'] + + if mode == QtGui.QIcon.Disabled: + color = options.get('color_disabled', color) + char = options.get('disabled', char) + elif mode == QtGui.QIcon.Active: + color = options.get('color_active', color) + char = options.get('active', char) + elif mode == QtGui.QIcon.Selected: + color = options.get('color_selected', color) + char = options.get('selected', char) + + painter.setPen(QtGui.QColor(color)) + # A 16 pixel-high icon yields a font size of 14, which is pixel perfect + # for font-awesome. 16 * 0.875 = 14 + # The reason for not using full-sized glyphs is the negative bearing of + # fonts. + draw_size = 0.875 * round(rect.height() * options['scale_factor']) + prefix = options['prefix'] + + # Animation setup hook + animation = options.get('animation') + if animation is not None: + animation.setup(self, painter, rect) + + painter.setFont(iconic.font(prefix, draw_size)) + if 'offset' in options: + rect = QtCore.QRect(rect) + rect.translate(options['offset'][0] * rect.width(), + options['offset'][1] * rect.height()) + + painter.setOpacity(options.get('opacity', 1.0)) + + painter.drawText(rect, + QtCore.Qt.AlignCenter | QtCore.Qt.AlignVCenter, + char) + painter.restore() + + +class CharIconEngine(QtGui.QIconEngine): + + """Specialization of QtGui.QIconEngine used to draw font-based icons""" + + def __init__(self, iconic, painter, options): + super(CharIconEngine, self).__init__() + self.iconic = iconic + self.painter = painter + self.options = options + + def paint(self, painter, rect, mode, state): + self.painter.paint( + self.iconic, painter, rect, mode, state, self.options) + + def pixmap(self, size, mode, state): + pm = QtGui.QPixmap(size) + pm.fill(QtCore.Qt.transparent) + self.paint(QtGui.QPainter(pm), + QtCore.QRect(QtCore.QPoint(0, 0), size), + mode, + state) + return pm + + +class IconicFont(QtCore.QObject): + + """Main class for managing iconic fonts""" + + def __init__(self, *args): + """Constructor + + :param *args: tuples + Each positional argument is a tuple of 3 or 4 values + - The prefix string to be used when accessing a given font set + - The ttf font filename + - The json charmap filename + - Optionally, the directory containing these files. When not + provided, the files will be looked up in ./fonts/ + """ + super(IconicFont, self).__init__() + self.painter = CharIconPainter() + self.painters = {} + self.fontname = {} + self.charmap = {} + for fargs in args: + self.load_font(*fargs) + + def load_font(self, + prefix, + ttf_filename, + charmap_filename, + directory=None): + """Loads a font file and the associated charmap + + If `directory` is None, the files will be looked up in ./fonts/ + + Arguments + --------- + prefix: str + prefix string to be used when accessing a given font set + ttf_filename: str + ttf font filename + charmap_filename: str + charmap filename + directory: str or None, optional + directory for font and charmap files + """ + + def hook(obj): + result = {} + for key in obj: + result[key] = six.unichr(int(obj[key], 16)) + return result + + if directory is None: + directory = os.path.join( + os.path.dirname(os.path.realpath(__file__)), 'fonts') + + with open(os.path.join(directory, charmap_filename), 'r') as codes: + self.charmap[prefix] = json.load(codes, object_hook=hook) + + id_ = QtGui.QFontDatabase.addApplicationFont( + os.path.join(directory, ttf_filename)) + + loadedFontFamilies = QtGui.QFontDatabase.applicationFontFamilies(id_) + + if(loadedFontFamilies): + self.fontname[prefix] = loadedFontFamilies[0] + else: + print('Font is empty') + + def icon(self, *names, **kwargs): + """Returns a QtGui.QIcon object corresponding to the provided icon name + (including prefix) + + Arguments + --------- + names: list of str + icon name, of the form PREFIX.NAME + + options: dict + options to be passed to the icon painter + """ + options_list = kwargs.pop('options', [{}] * len(names)) + general_options = kwargs + + if len(options_list) != len(names): + error = '"options" must be a list of size {0}'.format(len(names)) + raise Exception(error) + + parsed_options = [] + for i in range(len(options_list)): + specific_options = options_list[i] + parsed_options.append(self._parse_options(specific_options, + general_options, + names[i])) + + # Process high level API + api_options = parsed_options + + return self._icon_by_painter(self.painter, api_options) + + def _parse_options(self, specific_options, general_options, name): + """ """ + options = dict(_default_options, **general_options) + options.update(specific_options) + + # Handle icons for states + icon_kw = ['disabled', 'active', 'selected', 'char'] + names = [options.get(kw, name) for kw in icon_kw] + prefix, chars = self._get_prefix_chars(names) + options.update(dict(zip(*(icon_kw, chars)))) + options.update({'prefix': prefix}) + + # Handle colors for states + color_kw = ['color_active', 'color_selected'] + colors = [options.get(kw, options['color']) for kw in color_kw] + options.update(dict(zip(*(color_kw, colors)))) + + return options + + def _get_prefix_chars(self, names): + """ """ + chars = [] + for name in names: + if '.' in name: + prefix, n = name.split('.') + if prefix in self.charmap: + if n in self.charmap[prefix]: + chars.append(self.charmap[prefix][n]) + else: + error = 'Invalid icon name "{0}" in font "{1}"'.format( + n, prefix) + raise Exception(error) + else: + error = 'Invalid font prefix "{0}"'.format(prefix) + raise Exception(error) + else: + raise Exception('Invalid icon name') + + return prefix, chars + + def font(self, prefix, size): + """Returns QtGui.QFont corresponding to the given prefix and size + + Arguments + --------- + prefix: str + prefix string of the loaded font + size: int + size for the font + """ + font = QtGui.QFont(self.fontname[prefix]) + font.setPixelSize(size) + return font + + def set_custom_icon(self, name, painter): + """Associates a user-provided CharIconPainter to an icon name + The custom icon can later be addressed by calling + icon('custom.NAME') where NAME is the provided name for that icon. + + Arguments + --------- + name: str + name of the custom icon + painter: CharIconPainter + The icon painter, implementing + `paint(self, iconic, painter, rect, mode, state, options)` + """ + self.painters[name] = painter + + def _custom_icon(self, name, **kwargs): + """Returns the custom icon corresponding to the given name""" + options = dict(_default_options, **kwargs) + if name in self.painters: + painter = self.painters[name] + return self._icon_by_painter(painter, options) + else: + return QtGui.QIcon() + + def _icon_by_painter(self, painter, options): + """Returns the icon corresponding to the given painter""" + engine = CharIconEngine(self, painter, options) + return QtGui.QIcon(engine) diff --git a/pype/tools/pyblish_pype/vendor/six.py b/pype/tools/pyblish_pype/vendor/six.py new file mode 100644 index 0000000000..190c0239cd --- /dev/null +++ b/pype/tools/pyblish_pype/vendor/six.py @@ -0,0 +1,868 @@ +"""Utilities for writing code that runs on Python 2 and 3""" + +# Copyright (c) 2010-2015 Benjamin Peterson +# +# 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. + +from __future__ import absolute_import + +import functools +import itertools +import operator +import sys +import types + +__author__ = "Benjamin Peterson " +__version__ = "1.10.0" + + +# Useful for very coarse version differentiation. +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 +PY34 = sys.version_info[0:2] >= (3, 4) + +if PY3: + string_types = str, + integer_types = int, + class_types = type, + text_type = str + binary_type = bytes + + MAXSIZE = sys.maxsize +else: + string_types = basestring, + integer_types = (int, long) + class_types = (type, types.ClassType) + text_type = unicode + binary_type = str + + if sys.platform.startswith("java"): + # Jython always uses 32 bits. + MAXSIZE = int((1 << 31) - 1) + else: + # It's possible to have sizeof(long) != sizeof(Py_ssize_t). + class X(object): + + def __len__(self): + return 1 << 31 + try: + len(X()) + except OverflowError: + # 32-bit + MAXSIZE = int((1 << 31) - 1) + else: + # 64-bit + MAXSIZE = int((1 << 63) - 1) + del X + + +def _add_doc(func, doc): + """Add documentation to a function.""" + func.__doc__ = doc + + +def _import_module(name): + """Import module, returning the module after the last dot.""" + __import__(name) + return sys.modules[name] + + +class _LazyDescr(object): + + def __init__(self, name): + self.name = name + + def __get__(self, obj, tp): + result = self._resolve() + setattr(obj, self.name, result) # Invokes __set__. + try: + # This is a bit ugly, but it avoids running this again by + # removing this descriptor. + delattr(obj.__class__, self.name) + except AttributeError: + pass + return result + + +class MovedModule(_LazyDescr): + + def __init__(self, name, old, new=None): + super(MovedModule, self).__init__(name) + if PY3: + if new is None: + new = name + self.mod = new + else: + self.mod = old + + def _resolve(self): + return _import_module(self.mod) + + def __getattr__(self, attr): + _module = self._resolve() + value = getattr(_module, attr) + setattr(self, attr, value) + return value + + +class _LazyModule(types.ModuleType): + + def __init__(self, name): + super(_LazyModule, self).__init__(name) + self.__doc__ = self.__class__.__doc__ + + def __dir__(self): + attrs = ["__doc__", "__name__"] + attrs += [attr.name for attr in self._moved_attributes] + return attrs + + # Subclasses should override this + _moved_attributes = [] + + +class MovedAttribute(_LazyDescr): + + def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): + super(MovedAttribute, self).__init__(name) + if PY3: + if new_mod is None: + new_mod = name + self.mod = new_mod + if new_attr is None: + if old_attr is None: + new_attr = name + else: + new_attr = old_attr + self.attr = new_attr + else: + self.mod = old_mod + if old_attr is None: + old_attr = name + self.attr = old_attr + + def _resolve(self): + module = _import_module(self.mod) + return getattr(module, self.attr) + + +class _SixMetaPathImporter(object): + + """ + A meta path importer to import six.moves and its submodules. + + This class implements a PEP302 finder and loader. It should be compatible + with Python 2.5 and all existing versions of Python3 + """ + + def __init__(self, six_module_name): + self.name = six_module_name + self.known_modules = {} + + def _add_module(self, mod, *fullnames): + for fullname in fullnames: + self.known_modules[self.name + "." + fullname] = mod + + def _get_module(self, fullname): + return self.known_modules[self.name + "." + fullname] + + def find_module(self, fullname, path=None): + if fullname in self.known_modules: + return self + return None + + def __get_module(self, fullname): + try: + return self.known_modules[fullname] + except KeyError: + raise ImportError("This loader does not know module " + fullname) + + def load_module(self, fullname): + try: + # in case of a reload + return sys.modules[fullname] + except KeyError: + pass + mod = self.__get_module(fullname) + if isinstance(mod, MovedModule): + mod = mod._resolve() + else: + mod.__loader__ = self + sys.modules[fullname] = mod + return mod + + def is_package(self, fullname): + """ + Return true, if the named module is a package. + + We need this method to get correct spec objects with + Python 3.4 (see PEP451) + """ + return hasattr(self.__get_module(fullname), "__path__") + + def get_code(self, fullname): + """Return None + + Required, if is_package is implemented""" + self.__get_module(fullname) # eventually raises ImportError + return None + get_source = get_code # same as get_code + +_importer = _SixMetaPathImporter(__name__) + + +class _MovedItems(_LazyModule): + + """Lazy loading of moved objects""" + __path__ = [] # mark as package + + +_moved_attributes = [ + MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), + MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), + MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"), + MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), + MovedAttribute("intern", "__builtin__", "sys"), + MovedAttribute("map", "itertools", "builtins", "imap", "map"), + MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"), + MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"), + MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"), + MovedAttribute("reduce", "__builtin__", "functools"), + MovedAttribute("shlex_quote", "pipes", "shlex", "quote"), + MovedAttribute("StringIO", "StringIO", "io"), + MovedAttribute("UserDict", "UserDict", "collections"), + MovedAttribute("UserList", "UserList", "collections"), + MovedAttribute("UserString", "UserString", "collections"), + MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), + MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"), + MovedModule("builtins", "__builtin__"), + MovedModule("configparser", "ConfigParser"), + MovedModule("copyreg", "copy_reg"), + MovedModule("dbm_gnu", "gdbm", "dbm.gnu"), + MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread"), + MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), + MovedModule("http_cookies", "Cookie", "http.cookies"), + MovedModule("html_entities", "htmlentitydefs", "html.entities"), + MovedModule("html_parser", "HTMLParser", "html.parser"), + MovedModule("http_client", "httplib", "http.client"), + MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), + MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"), + MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), + MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), + MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), + MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), + MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), + MovedModule("cPickle", "cPickle", "pickle"), + MovedModule("queue", "Queue"), + MovedModule("reprlib", "repr"), + MovedModule("socketserver", "SocketServer"), + MovedModule("_thread", "thread", "_thread"), + MovedModule("tkinter", "Tkinter"), + MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), + MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), + MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), + MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), + MovedModule("tkinter_tix", "Tix", "tkinter.tix"), + MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"), + MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), + MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), + MovedModule("tkinter_colorchooser", "tkColorChooser", + "tkinter.colorchooser"), + MovedModule("tkinter_commondialog", "tkCommonDialog", + "tkinter.commondialog"), + MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), + MovedModule("tkinter_font", "tkFont", "tkinter.font"), + MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), + MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", + "tkinter.simpledialog"), + MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"), + MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"), + MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"), + MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), + MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"), + MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"), +] +# Add windows specific modules. +if sys.platform == "win32": + _moved_attributes += [ + MovedModule("winreg", "_winreg"), + ] + +for attr in _moved_attributes: + setattr(_MovedItems, attr.name, attr) + if isinstance(attr, MovedModule): + _importer._add_module(attr, "moves." + attr.name) +del attr + +_MovedItems._moved_attributes = _moved_attributes + +moves = _MovedItems(__name__ + ".moves") +_importer._add_module(moves, "moves") + + +class Module_six_moves_urllib_parse(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_parse""" + + +_urllib_parse_moved_attributes = [ + MovedAttribute("ParseResult", "urlparse", "urllib.parse"), + MovedAttribute("SplitResult", "urlparse", "urllib.parse"), + MovedAttribute("parse_qs", "urlparse", "urllib.parse"), + MovedAttribute("parse_qsl", "urlparse", "urllib.parse"), + MovedAttribute("urldefrag", "urlparse", "urllib.parse"), + MovedAttribute("urljoin", "urlparse", "urllib.parse"), + MovedAttribute("urlparse", "urlparse", "urllib.parse"), + MovedAttribute("urlsplit", "urlparse", "urllib.parse"), + MovedAttribute("urlunparse", "urlparse", "urllib.parse"), + MovedAttribute("urlunsplit", "urlparse", "urllib.parse"), + MovedAttribute("quote", "urllib", "urllib.parse"), + MovedAttribute("quote_plus", "urllib", "urllib.parse"), + MovedAttribute("unquote", "urllib", "urllib.parse"), + MovedAttribute("unquote_plus", "urllib", "urllib.parse"), + MovedAttribute("urlencode", "urllib", "urllib.parse"), + MovedAttribute("splitquery", "urllib", "urllib.parse"), + MovedAttribute("splittag", "urllib", "urllib.parse"), + MovedAttribute("splituser", "urllib", "urllib.parse"), + MovedAttribute("uses_fragment", "urlparse", "urllib.parse"), + MovedAttribute("uses_netloc", "urlparse", "urllib.parse"), + MovedAttribute("uses_params", "urlparse", "urllib.parse"), + MovedAttribute("uses_query", "urlparse", "urllib.parse"), + MovedAttribute("uses_relative", "urlparse", "urllib.parse"), +] +for attr in _urllib_parse_moved_attributes: + setattr(Module_six_moves_urllib_parse, attr.name, attr) +del attr + +Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes + +_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"), + "moves.urllib_parse", "moves.urllib.parse") + + +class Module_six_moves_urllib_error(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_error""" + + +_urllib_error_moved_attributes = [ + MovedAttribute("URLError", "urllib2", "urllib.error"), + MovedAttribute("HTTPError", "urllib2", "urllib.error"), + MovedAttribute("ContentTooShortError", "urllib", "urllib.error"), +] +for attr in _urllib_error_moved_attributes: + setattr(Module_six_moves_urllib_error, attr.name, attr) +del attr + +Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes + +_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"), + "moves.urllib_error", "moves.urllib.error") + + +class Module_six_moves_urllib_request(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_request""" + + +_urllib_request_moved_attributes = [ + MovedAttribute("urlopen", "urllib2", "urllib.request"), + MovedAttribute("install_opener", "urllib2", "urllib.request"), + MovedAttribute("build_opener", "urllib2", "urllib.request"), + MovedAttribute("pathname2url", "urllib", "urllib.request"), + MovedAttribute("url2pathname", "urllib", "urllib.request"), + MovedAttribute("getproxies", "urllib", "urllib.request"), + MovedAttribute("Request", "urllib2", "urllib.request"), + MovedAttribute("OpenerDirector", "urllib2", "urllib.request"), + MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"), + MovedAttribute("ProxyHandler", "urllib2", "urllib.request"), + MovedAttribute("BaseHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"), + MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"), + MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"), + MovedAttribute("FileHandler", "urllib2", "urllib.request"), + MovedAttribute("FTPHandler", "urllib2", "urllib.request"), + MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"), + MovedAttribute("UnknownHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"), + MovedAttribute("urlretrieve", "urllib", "urllib.request"), + MovedAttribute("urlcleanup", "urllib", "urllib.request"), + MovedAttribute("URLopener", "urllib", "urllib.request"), + MovedAttribute("FancyURLopener", "urllib", "urllib.request"), + MovedAttribute("proxy_bypass", "urllib", "urllib.request"), +] +for attr in _urllib_request_moved_attributes: + setattr(Module_six_moves_urllib_request, attr.name, attr) +del attr + +Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes + +_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"), + "moves.urllib_request", "moves.urllib.request") + + +class Module_six_moves_urllib_response(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_response""" + + +_urllib_response_moved_attributes = [ + MovedAttribute("addbase", "urllib", "urllib.response"), + MovedAttribute("addclosehook", "urllib", "urllib.response"), + MovedAttribute("addinfo", "urllib", "urllib.response"), + MovedAttribute("addinfourl", "urllib", "urllib.response"), +] +for attr in _urllib_response_moved_attributes: + setattr(Module_six_moves_urllib_response, attr.name, attr) +del attr + +Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes + +_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"), + "moves.urllib_response", "moves.urllib.response") + + +class Module_six_moves_urllib_robotparser(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_robotparser""" + + +_urllib_robotparser_moved_attributes = [ + MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"), +] +for attr in _urllib_robotparser_moved_attributes: + setattr(Module_six_moves_urllib_robotparser, attr.name, attr) +del attr + +Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes + +_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"), + "moves.urllib_robotparser", "moves.urllib.robotparser") + + +class Module_six_moves_urllib(types.ModuleType): + + """Create a six.moves.urllib namespace that resembles the Python 3 namespace""" + __path__ = [] # mark as package + parse = _importer._get_module("moves.urllib_parse") + error = _importer._get_module("moves.urllib_error") + request = _importer._get_module("moves.urllib_request") + response = _importer._get_module("moves.urllib_response") + robotparser = _importer._get_module("moves.urllib_robotparser") + + def __dir__(self): + return ['parse', 'error', 'request', 'response', 'robotparser'] + +_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"), + "moves.urllib") + + +def add_move(move): + """Add an item to six.moves.""" + setattr(_MovedItems, move.name, move) + + +def remove_move(name): + """Remove item from six.moves.""" + try: + delattr(_MovedItems, name) + except AttributeError: + try: + del moves.__dict__[name] + except KeyError: + raise AttributeError("no such move, %r" % (name,)) + + +if PY3: + _meth_func = "__func__" + _meth_self = "__self__" + + _func_closure = "__closure__" + _func_code = "__code__" + _func_defaults = "__defaults__" + _func_globals = "__globals__" +else: + _meth_func = "im_func" + _meth_self = "im_self" + + _func_closure = "func_closure" + _func_code = "func_code" + _func_defaults = "func_defaults" + _func_globals = "func_globals" + + +try: + advance_iterator = next +except NameError: + def advance_iterator(it): + return it.next() +next = advance_iterator + + +try: + callable = callable +except NameError: + def callable(obj): + return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) + + +if PY3: + def get_unbound_function(unbound): + return unbound + + create_bound_method = types.MethodType + + def create_unbound_method(func, cls): + return func + + Iterator = object +else: + def get_unbound_function(unbound): + return unbound.im_func + + def create_bound_method(func, obj): + return types.MethodType(func, obj, obj.__class__) + + def create_unbound_method(func, cls): + return types.MethodType(func, None, cls) + + class Iterator(object): + + def next(self): + return type(self).__next__(self) + + callable = callable +_add_doc(get_unbound_function, + """Get the function out of a possibly unbound function""") + + +get_method_function = operator.attrgetter(_meth_func) +get_method_self = operator.attrgetter(_meth_self) +get_function_closure = operator.attrgetter(_func_closure) +get_function_code = operator.attrgetter(_func_code) +get_function_defaults = operator.attrgetter(_func_defaults) +get_function_globals = operator.attrgetter(_func_globals) + + +if PY3: + def iterkeys(d, **kw): + return iter(d.keys(**kw)) + + def itervalues(d, **kw): + return iter(d.values(**kw)) + + def iteritems(d, **kw): + return iter(d.items(**kw)) + + def iterlists(d, **kw): + return iter(d.lists(**kw)) + + viewkeys = operator.methodcaller("keys") + + viewvalues = operator.methodcaller("values") + + viewitems = operator.methodcaller("items") +else: + def iterkeys(d, **kw): + return d.iterkeys(**kw) + + def itervalues(d, **kw): + return d.itervalues(**kw) + + def iteritems(d, **kw): + return d.iteritems(**kw) + + def iterlists(d, **kw): + return d.iterlists(**kw) + + viewkeys = operator.methodcaller("viewkeys") + + viewvalues = operator.methodcaller("viewvalues") + + viewitems = operator.methodcaller("viewitems") + +_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.") +_add_doc(itervalues, "Return an iterator over the values of a dictionary.") +_add_doc(iteritems, + "Return an iterator over the (key, value) pairs of a dictionary.") +_add_doc(iterlists, + "Return an iterator over the (key, [values]) pairs of a dictionary.") + + +if PY3: + def b(s): + return s.encode("latin-1") + + def u(s): + return s + unichr = chr + import struct + int2byte = struct.Struct(">B").pack + del struct + byte2int = operator.itemgetter(0) + indexbytes = operator.getitem + iterbytes = iter + import io + StringIO = io.StringIO + BytesIO = io.BytesIO + _assertCountEqual = "assertCountEqual" + if sys.version_info[1] <= 1: + _assertRaisesRegex = "assertRaisesRegexp" + _assertRegex = "assertRegexpMatches" + else: + _assertRaisesRegex = "assertRaisesRegex" + _assertRegex = "assertRegex" +else: + def b(s): + return s + # Workaround for standalone backslash + + def u(s): + return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape") + unichr = unichr + int2byte = chr + + def byte2int(bs): + return ord(bs[0]) + + def indexbytes(buf, i): + return ord(buf[i]) + iterbytes = functools.partial(itertools.imap, ord) + import StringIO + StringIO = BytesIO = StringIO.StringIO + _assertCountEqual = "assertItemsEqual" + _assertRaisesRegex = "assertRaisesRegexp" + _assertRegex = "assertRegexpMatches" +_add_doc(b, """Byte literal""") +_add_doc(u, """Text literal""") + + +def assertCountEqual(self, *args, **kwargs): + return getattr(self, _assertCountEqual)(*args, **kwargs) + + +def assertRaisesRegex(self, *args, **kwargs): + return getattr(self, _assertRaisesRegex)(*args, **kwargs) + + +def assertRegex(self, *args, **kwargs): + return getattr(self, _assertRegex)(*args, **kwargs) + + +if PY3: + exec_ = getattr(moves.builtins, "exec") + + def reraise(tp, value, tb=None): + if value is None: + value = tp() + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + +else: + def exec_(_code_, _globs_=None, _locs_=None): + """Execute code in a namespace.""" + if _globs_ is None: + frame = sys._getframe(1) + _globs_ = frame.f_globals + if _locs_ is None: + _locs_ = frame.f_locals + del frame + elif _locs_ is None: + _locs_ = _globs_ + exec("""exec _code_ in _globs_, _locs_""") + + exec_("""def reraise(tp, value, tb=None): + raise tp, value, tb +""") + + +if sys.version_info[:2] == (3, 2): + exec_("""def raise_from(value, from_value): + if from_value is None: + raise value + raise value from from_value +""") +elif sys.version_info[:2] > (3, 2): + exec_("""def raise_from(value, from_value): + raise value from from_value +""") +else: + def raise_from(value, from_value): + raise value + + +print_ = getattr(moves.builtins, "print", None) +if print_ is None: + def print_(*args, **kwargs): + """The new-style print function for Python 2.4 and 2.5.""" + fp = kwargs.pop("file", sys.stdout) + if fp is None: + return + + def write(data): + if not isinstance(data, basestring): + data = str(data) + # If the file has an encoding, encode unicode with it. + if (isinstance(fp, file) and + isinstance(data, unicode) and + fp.encoding is not None): + errors = getattr(fp, "errors", None) + if errors is None: + errors = "strict" + data = data.encode(fp.encoding, errors) + fp.write(data) + want_unicode = False + sep = kwargs.pop("sep", None) + if sep is not None: + if isinstance(sep, unicode): + want_unicode = True + elif not isinstance(sep, str): + raise TypeError("sep must be None or a string") + end = kwargs.pop("end", None) + if end is not None: + if isinstance(end, unicode): + want_unicode = True + elif not isinstance(end, str): + raise TypeError("end must be None or a string") + if kwargs: + raise TypeError("invalid keyword arguments to print()") + if not want_unicode: + for arg in args: + if isinstance(arg, unicode): + want_unicode = True + break + if want_unicode: + newline = unicode("\n") + space = unicode(" ") + else: + newline = "\n" + space = " " + if sep is None: + sep = space + if end is None: + end = newline + for i, arg in enumerate(args): + if i: + write(sep) + write(arg) + write(end) +if sys.version_info[:2] < (3, 3): + _print = print_ + + def print_(*args, **kwargs): + fp = kwargs.get("file", sys.stdout) + flush = kwargs.pop("flush", False) + _print(*args, **kwargs) + if flush and fp is not None: + fp.flush() + +_add_doc(reraise, """Reraise an exception.""") + +if sys.version_info[0:2] < (3, 4): + def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS, + updated=functools.WRAPPER_UPDATES): + def wrapper(f): + f = functools.wraps(wrapped, assigned, updated)(f) + f.__wrapped__ = wrapped + return f + return wrapper +else: + wraps = functools.wraps + + +def with_metaclass(meta, *bases): + """Create a base class with a metaclass.""" + # This requires a bit of explanation: the basic idea is to make a dummy + # metaclass for one level of class instantiation that replaces itself with + # the actual metaclass. + class metaclass(meta): + + def __new__(cls, name, this_bases, d): + return meta(name, bases, d) + return type.__new__(metaclass, 'temporary_class', (), {}) + + +def add_metaclass(metaclass): + """Class decorator for creating a class with a metaclass.""" + def wrapper(cls): + orig_vars = cls.__dict__.copy() + slots = orig_vars.get('__slots__') + if slots is not None: + if isinstance(slots, str): + slots = [slots] + for slots_var in slots: + orig_vars.pop(slots_var) + orig_vars.pop('__dict__', None) + orig_vars.pop('__weakref__', None) + return metaclass(cls.__name__, cls.__bases__, orig_vars) + return wrapper + + +def python_2_unicode_compatible(klass): + """ + A decorator that defines __unicode__ and __str__ methods under Python 2. + Under Python 3 it does nothing. + + To support Python 2 and 3 with a single code base, define a __str__ method + returning text and apply this decorator to the class. + """ + if PY2: + if '__str__' not in klass.__dict__: + raise ValueError("@python_2_unicode_compatible cannot be applied " + "to %s because it doesn't define __str__()." % + klass.__name__) + klass.__unicode__ = klass.__str__ + klass.__str__ = lambda self: self.__unicode__().encode('utf-8') + return klass + + +# Complete the moves implementation. +# This code is at the end of this module to speed up module loading. +# Turn this module into a package. +__path__ = [] # required for PEP 302 and PEP 451 +__package__ = __name__ # see PEP 366 @ReservedAssignment +if globals().get("__spec__") is not None: + __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable +# Remove other six meta path importers, since they cause problems. This can +# happen if six is removed from sys.modules and then reloaded. (Setuptools does +# this for some reason.) +if sys.meta_path: + for i, importer in enumerate(sys.meta_path): + # Here's some real nastiness: Another "instance" of the six module might + # be floating around. Therefore, we can't use isinstance() to check for + # the six meta path importer, since the other six instance will have + # inserted an importer with different class. + if (type(importer).__name__ == "_SixMetaPathImporter" and + importer.name == __name__): + del sys.meta_path[i] + break + del i, importer +# Finally, add the importer to the meta path import hook. +sys.meta_path.append(_importer) diff --git a/pype/tools/pyblish_pype/version.py b/pype/tools/pyblish_pype/version.py new file mode 100644 index 0000000000..5f1dce8011 --- /dev/null +++ b/pype/tools/pyblish_pype/version.py @@ -0,0 +1,11 @@ + +VERSION_MAJOR = 2 +VERSION_MINOR = 9 +VERSION_PATCH = 0 + + +version_info = (VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH) +version = '%i.%i.%i' % version_info +__version__ = version + +__all__ = ['version', 'version_info', '__version__'] diff --git a/pype/tools/pyblish_pype/view.py b/pype/tools/pyblish_pype/view.py new file mode 100644 index 0000000000..86cefd4a55 --- /dev/null +++ b/pype/tools/pyblish_pype/view.py @@ -0,0 +1,212 @@ +from .vendor.Qt import QtCore, QtWidgets +from . import model +from .constants import Roles + + +class ArtistView(QtWidgets.QListView): + # An item is requesting to be toggled, with optional forced-state + toggled = QtCore.Signal(QtCore.QModelIndex, object) + show_perspective = QtCore.Signal(QtCore.QModelIndex) + + def __init__(self, parent=None): + super(ArtistView, self).__init__(parent) + + self.horizontalScrollBar().hide() + self.viewport().setAttribute(QtCore.Qt.WA_Hover, True) + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) + self.setResizeMode(QtWidgets.QListView.Adjust) + self.setVerticalScrollMode(QtWidgets.QListView.ScrollPerPixel) + + def event(self, event): + if not event.type() == QtCore.QEvent.KeyPress: + return super(ArtistView, self).event(event) + + elif event.key() == QtCore.Qt.Key_Space: + for index in self.selectionModel().selectedIndexes(): + self.toggled.emit(index, None) + + return True + + elif event.key() == QtCore.Qt.Key_Backspace: + for index in self.selectionModel().selectedIndexes(): + self.toggled.emit(index, False) + + return True + + elif event.key() == QtCore.Qt.Key_Return: + for index in self.selectionModel().selectedIndexes(): + self.toggled.emit(index, True) + + return True + + return super(ArtistView, self).event(event) + + def focusOutEvent(self, event): + self.selectionModel().clear() + + def mouseReleaseEvent(self, event): + if event.button() == QtCore.Qt.LeftButton: + indexes = self.selectionModel().selectedIndexes() + if len(indexes) <= 1 and event.pos().x() < 20: + for index in indexes: + self.toggled.emit(index, None) + if len(indexes) == 1 and event.pos().x() > self.width() - 40: + for index in indexes: + self.show_perspective.emit(index) + + return super(ArtistView, self).mouseReleaseEvent(event) + + +class OverviewView(QtWidgets.QTreeView): + # An item is requesting to be toggled, with optional forced-state + toggled = QtCore.Signal(QtCore.QModelIndex, object) + show_perspective = QtCore.Signal(QtCore.QModelIndex) + + def __init__(self, parent=None): + super(OverviewView, self).__init__(parent) + + self.horizontalScrollBar().hide() + self.viewport().setAttribute(QtCore.Qt.WA_Hover, True) + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) + self.setItemsExpandable(True) + self.setVerticalScrollMode(QtWidgets.QTreeView.ScrollPerPixel) + self.setHeaderHidden(True) + self.setRootIsDecorated(False) + self.setIndentation(0) + + self.clicked.connect(self.item_expand) + + def event(self, event): + if not event.type() == QtCore.QEvent.KeyPress: + return super(OverviewView, self).event(event) + + elif event.key() == QtCore.Qt.Key_Space: + for index in self.selectionModel().selectedIndexes(): + self.toggled.emit(index, None) + + return True + + elif event.key() == QtCore.Qt.Key_Backspace: + for index in self.selectionModel().selectedIndexes(): + self.toggled.emit(index, False) + + return True + + elif event.key() == QtCore.Qt.Key_Return: + for index in self.selectionModel().selectedIndexes(): + self.toggled.emit(index, True) + + return True + + return super(OverviewView, self).event(event) + + def focusOutEvent(self, event): + self.selectionModel().clear() + + def item_expand(self, index): + if index.data(Roles.TypeRole) == model.GroupType: + if self.isExpanded(index): + self.collapse(index) + else: + self.expand(index) + + def mouseReleaseEvent(self, event): + if event.button() == QtCore.Qt.LeftButton: + indexes = self.selectionModel().selectedIndexes() + if len(indexes) == 1: + index = indexes[0] + # If instance or Plugin + if index.data(Roles.TypeRole) in ( + model.InstanceType, model.PluginType + ): + if event.pos().x() < 20: + self.toggled.emit(index, None) + elif event.pos().x() > self.width() - 20: + self.show_perspective.emit(index) + + # Deselect all group labels + for index in indexes: + if index.data(Roles.TypeRole) == model.GroupType: + self.selectionModel().select( + index, QtCore.QItemSelectionModel.Deselect + ) + + return super(OverviewView, self).mouseReleaseEvent(event) + + +class TerminalView(QtWidgets.QTreeView): + # An item is requesting to be toggled, with optional forced-state + def __init__(self, parent=None): + super(TerminalView, self).__init__(parent) + self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) + self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) + self.setAutoScroll(False) + self.setHeaderHidden(True) + self.setIndentation(0) + self.setVerticalScrollMode(QtWidgets.QTreeView.ScrollPerPixel) + self.verticalScrollBar().setSingleStep(10) + self.setRootIsDecorated(False) + + self.clicked.connect(self.item_expand) + + def event(self, event): + if not event.type() == QtCore.QEvent.KeyPress: + return super(TerminalView, self).event(event) + + elif event.key() == QtCore.Qt.Key_Space: + for index in self.selectionModel().selectedIndexes(): + if self.isExpanded(index): + self.collapse(index) + else: + self.expand(index) + + elif event.key() == QtCore.Qt.Key_Backspace: + for index in self.selectionModel().selectedIndexes(): + self.collapse(index) + + elif event.key() == QtCore.Qt.Key_Return: + for index in self.selectionModel().selectedIndexes(): + self.expand(index) + + return super(TerminalView, self).event(event) + + def focusOutEvent(self, event): + self.selectionModel().clear() + + def item_expand(self, index): + if index.data(Roles.TypeRole) == model.TerminalLabelType: + if self.isExpanded(index): + self.collapse(index) + else: + self.expand(index) + self.model().layoutChanged.emit() + self.updateGeometry() + + def rowsInserted(self, parent, start, end): + """Automatically scroll to bottom on each new item added.""" + super(TerminalView, self).rowsInserted(parent, start, end) + self.updateGeometry() + self.scrollToBottom() + + def resizeEvent(self, event): + super(self.__class__, self).resizeEvent(event) + self.model().layoutChanged.emit() + + def sizeHint(self): + size = super(TerminalView, self).sizeHint() + height = ( + self.contentsMargins().top() + + self.contentsMargins().bottom() + ) + for idx_i in range(self.model().rowCount()): + index = self.model().index(idx_i, 0) + height += self.rowHeight(index) + if self.isExpanded(index): + for idx_j in range(index.model().rowCount(index)): + child_index = index.child(idx_j, 0) + height += self.rowHeight(child_index) + + size.setHeight(height) + return size diff --git a/pype/tools/pyblish_pype/widgets.py b/pype/tools/pyblish_pype/widgets.py new file mode 100644 index 0000000000..3a09249a86 --- /dev/null +++ b/pype/tools/pyblish_pype/widgets.py @@ -0,0 +1,558 @@ +import sys +from .vendor.Qt import QtCore, QtWidgets, QtGui +from . import model, delegate, view, awesome +from .constants import PluginStates, InstanceStates, Roles + + +class EllidableLabel(QtWidgets.QLabel): + def __init__(self, *args, **kwargs): + super(self.__class__, self).__init__(*args, **kwargs) + self.setObjectName("EllidableLabel") + + def paintEvent(self, event): + painter = QtGui.QPainter(self) + + metrics = QtGui.QFontMetrics(self.font()) + elided = metrics.elidedText( + self.text(), QtCore.Qt.ElideRight, self.width() + ) + painter.drawText(self.rect(), self.alignment(), elided) + + +class PerspectiveLabel(QtWidgets.QTextEdit): + def __init__(self, parent=None): + super(self.__class__, self).__init__(parent) + self.setObjectName("PerspectiveLabel") + + size_policy = self.sizePolicy() + size_policy.setHeightForWidth(True) + size_policy.setVerticalPolicy(QtWidgets.QSizePolicy.Preferred) + self.setSizePolicy(size_policy) + + self.textChanged.connect(self.on_text_changed) + + def on_text_changed(self, *args, **kwargs): + self.updateGeometry() + + def hasHeightForWidth(self): + return True + + def heightForWidth(self, width): + margins = self.contentsMargins() + + document_width = 0 + if width >= margins.left() + margins.right(): + document_width = width - margins.left() - margins.right() + + document = self.document().clone() + document.setTextWidth(document_width) + + return margins.top() + document.size().height() + margins.bottom() + + def sizeHint(self): + width = super(self.__class__, self).sizeHint().width() + return QtCore.QSize(width, self.heightForWidth(width)) + + +class PerspectiveWidget(QtWidgets.QWidget): + l_doc = "Documentation" + l_rec = "Records" + l_path = "Path" + + def __init__(self, parent): + super(PerspectiveWidget, self).__init__(parent) + + self.parent_widget = parent + main_layout = QtWidgets.QVBoxLayout(self) + + header_widget = QtWidgets.QWidget() + toggle_button = QtWidgets.QPushButton(parent=header_widget) + toggle_button.setObjectName("PerspectiveToggleBtn") + toggle_button.setText(delegate.icons["angle-left"]) + toggle_button.setMinimumHeight(50) + toggle_button.setFixedWidth(40) + + indicator = QtWidgets.QLabel("", parent=header_widget) + indicator.setFixedWidth(30) + indicator.setAlignment(QtCore.Qt.AlignCenter) + indicator.setObjectName("PerspectiveIndicator") + + name = EllidableLabel('*Name of inspected', parent=header_widget) + + header_layout = QtWidgets.QHBoxLayout(header_widget) + header_layout.setAlignment(QtCore.Qt.AlignLeft) + header_layout.addWidget(toggle_button) + header_layout.addWidget(indicator) + header_layout.addWidget(name) + header_layout.setContentsMargins(0, 0, 0, 0) + header_layout.setSpacing(10) + header_widget.setLayout(header_layout) + + main_layout.setAlignment(QtCore.Qt.AlignTop) + main_layout.addWidget(header_widget) + + scroll_widget = QtWidgets.QScrollArea(self) + scroll_widget.setObjectName("PerspectiveScrollContent") + + contents_widget = QtWidgets.QWidget(scroll_widget) + contents_widget.setObjectName("PerspectiveWidgetContent") + + layout = QtWidgets.QVBoxLayout() + layout.setAlignment(QtCore.Qt.AlignTop) + layout.setContentsMargins(0, 0, 0, 0) + + documentation = ExpandableWidget(self, self.l_doc) + doc_label = PerspectiveLabel() + documentation.set_content(doc_label) + layout.addWidget(documentation) + + path = ExpandableWidget(self, self.l_path) + path_label = PerspectiveLabel() + path.set_content(path_label) + layout.addWidget(path) + + records = ExpandableWidget(self, self.l_rec) + layout.addWidget(records) + + contents_widget.setLayout(layout) + + terminal_view = view.TerminalView() + terminal_view.setObjectName("TerminalView") + terminal_model = model.TerminalModel() + terminal_proxy = model.TerminalProxy(terminal_view) + terminal_proxy.setSourceModel(terminal_model) + + terminal_view.setModel(terminal_proxy) + terminal_delegate = delegate.TerminalItem() + terminal_view.setItemDelegate(terminal_delegate) + records.set_content(terminal_view) + + scroll_widget.setWidgetResizable(True) + scroll_widget.setWidget(contents_widget) + + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + main_layout.addWidget(scroll_widget) + self.setLayout(main_layout) + + self.terminal_view = terminal_view + self.terminal_model = terminal_model + self.terminal_proxy = terminal_proxy + + self.indicator = indicator + self.scroll_widget = scroll_widget + self.contents_widget = contents_widget + self.toggle_button = toggle_button + self.name_widget = name + self.documentation = documentation + self.path = path + self.records = records + + self.toggle_button.clicked.connect(self.toggle_me) + + self.last_type = None + self.last_item_id = None + self.last_id = None + + def trim(self, docstring): + if not docstring: + return "" + # Convert tabs to spaces (following the normal Python rules) + # and split into a list of lines: + lines = docstring.expandtabs().splitlines() + # Determine minimum indentation (first line doesn't count): + try: + indent = sys.maxint + max = sys.maxint + except Exception: + indent = sys.maxsize + max = sys.maxsize + + for line in lines[1:]: + stripped = line.lstrip() + if stripped: + indent = min(indent, len(line) - len(stripped)) + # Remove indentation (first line is special): + trimmed = [lines[0].strip()] + if indent < max: + for line in lines[1:]: + trimmed.append(line[indent:].rstrip()) + # Strip off trailing and leading blank lines: + while trimmed and not trimmed[-1]: + trimmed.pop() + while trimmed and not trimmed[0]: + trimmed.pop(0) + # Return a single string: + return "\n".join(trimmed) + + def set_indicator_state(self, state): + self.indicator.setProperty("state", state) + self.indicator.style().polish(self.indicator) + + def reset(self): + self.last_id = None + self.set_records(list()) + self.set_indicator_state(None) + + def update_context(self, plugin_item, instance_item): + if not self.last_item_id or not self.last_type: + return + + if self.last_type == model.PluginType: + if not self.last_id: + _item_id = plugin_item.data(Roles.ObjectUIdRole) + if _item_id != self.last_item_id: + return + self.last_id = plugin_item.plugin.id + + elif self.last_id != plugin_item.plugin.id: + return + + self.set_context(plugin_item.index()) + return + + if self.last_type == model.InstanceType: + if not self.last_id: + _item_id = instance_item.data(Roles.ObjectUIdRole) + if _item_id != self.last_item_id: + return + self.last_id = instance_item.instance.id + + elif self.last_id != instance_item.instance.id: + return + + self.set_context(instance_item.index()) + return + + def set_context(self, index): + if not index or not index.isValid(): + index_type = None + else: + index_type = index.data(Roles.TypeRole) + + if index_type == model.InstanceType: + item_id = index.data(Roles.ObjectIdRole) + publish_states = index.data(Roles.PublishFlagsRole) + if publish_states & InstanceStates.ContextType: + type_indicator = "C" + else: + type_indicator = "I" + + if publish_states & InstanceStates.InProgress: + self.set_indicator_state("active") + + elif publish_states & InstanceStates.HasError: + self.set_indicator_state("error") + + elif publish_states & InstanceStates.HasWarning: + self.set_indicator_state("warning") + + elif publish_states & InstanceStates.HasFinished: + self.set_indicator_state("ok") + else: + self.set_indicator_state(None) + + self.documentation.setVisible(False) + self.path.setVisible(False) + + elif index_type == model.PluginType: + item_id = index.data(Roles.ObjectIdRole) + type_indicator = "P" + + doc = index.data(Roles.DocstringRole) + doc_str = "" + if doc: + doc_str = self.trim(doc) + + publish_states = index.data(Roles.PublishFlagsRole) + if publish_states & PluginStates.InProgress: + self.set_indicator_state("active") + + elif publish_states & PluginStates.HasError: + self.set_indicator_state("error") + + elif publish_states & PluginStates.HasWarning: + self.set_indicator_state("warning") + + elif publish_states & PluginStates.WasProcessed: + self.set_indicator_state("ok") + + else: + self.set_indicator_state(None) + + self.documentation.toggle_content(bool(doc_str)) + self.documentation.content.setText(doc_str) + + path = index.data(Roles.PathModuleRole) or "" + self.path.toggle_content(path.strip() != "") + self.path.content.setText(path) + + self.documentation.setVisible(True) + self.path.setVisible(True) + + else: + self.last_type = None + self.last_id = None + self.indicator.setText("?") + self.set_indicator_state(None) + self.documentation.setVisible(False) + self.path.setVisible(False) + self.records.setVisible(False) + return + + self.last_type = index_type + self.last_id = item_id + self.last_item_id = index.data(Roles.ObjectUIdRole) + + self.indicator.setText(type_indicator) + + label = index.data(QtCore.Qt.DisplayRole) + self.name_widget.setText(label) + self.records.setVisible(True) + + records = index.data(Roles.LogRecordsRole) or [] + self.set_records(records) + + def set_records(self, records): + len_records = 0 + if records: + len_records += len(records) + + data = {"records": records} + self.terminal_model.reset() + self.terminal_model.update_with_result(data) + while not self.terminal_model.items_to_set_widget.empty(): + item = self.terminal_model.items_to_set_widget.get() + widget = TerminalDetail(item.data(QtCore.Qt.DisplayRole)) + index = self.terminal_proxy.mapFromSource(item.index()) + self.terminal_view.setIndexWidget(index, widget) + + self.records.button_toggle_text.setText( + "{} ({})".format(self.l_rec, len_records) + ) + self.records.toggle_content(len_records > 0) + + def toggle_me(self): + self.parent_widget.toggle_perspective_widget() + + +class ClickableWidget(QtWidgets.QLabel): + clicked = QtCore.Signal() + + def mouseReleaseEvent(self, event): + if event.button() == QtCore.Qt.LeftButton: + self.clicked.emit() + super(ClickableWidget, self).mouseReleaseEvent(event) + + +class ExpandableWidget(QtWidgets.QWidget): + + content = None + + def __init__(self, parent, title): + super(ExpandableWidget, self).__init__(parent) + + top_part = ClickableWidget(parent=self) + top_part.setObjectName("ExpandableHeader") + + button_size = QtCore.QSize(5, 5) + button_toggle = QtWidgets.QToolButton(parent=top_part) + button_toggle.setIconSize(button_size) + button_toggle.setArrowType(QtCore.Qt.RightArrow) + button_toggle.setCheckable(True) + button_toggle.setChecked(False) + + button_toggle_text = QtWidgets.QLabel(title, parent=top_part) + + layout = QtWidgets.QHBoxLayout(top_part) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(5) + layout.addWidget(button_toggle) + layout.addWidget(button_toggle_text) + top_part.setLayout(layout) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(9, 9, 9, 0) + + content = QtWidgets.QFrame(self) + content.setObjectName("ExpandableWidgetContent") + content.setVisible(False) + + content_layout = QtWidgets.QVBoxLayout(content) + + main_layout.addWidget(top_part) + main_layout.addWidget(content) + self.setLayout(main_layout) + + self.setAttribute(QtCore.Qt.WA_StyledBackground) + + self.top_part = top_part + self.button_toggle = button_toggle + self.button_toggle_text = button_toggle_text + + self.content_widget = content + self.content_layout = content_layout + + self.top_part.clicked.connect(self.top_part_clicked) + self.button_toggle.clicked.connect(self.toggle_content) + + def top_part_clicked(self): + self.toggle_content(not self.button_toggle.isChecked()) + + def toggle_content(self, *args): + if len(args) > 0: + checked = args[0] + else: + checked = self.button_toggle.isChecked() + arrow_type = QtCore.Qt.RightArrow + if checked: + arrow_type = QtCore.Qt.DownArrow + self.button_toggle.setChecked(checked) + self.button_toggle.setArrowType(arrow_type) + self.content_widget.setVisible(checked) + + def resizeEvent(self, event): + super(self.__class__, self).resizeEvent(event) + self.content.updateGeometry() + + def set_content(self, in_widget): + if self.content: + self.content.hide() + self.content_layout.removeWidget(self.content) + self.content_layout.addWidget(in_widget) + self.content = in_widget + + +class ButtonWithMenu(QtWidgets.QWidget): + def __init__(self, button_title, parent=None): + super(ButtonWithMenu, self).__init__(parent=parent) + self.setSizePolicy(QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum + )) + + self.layout = QtWidgets.QHBoxLayout(self) + self.layout.setContentsMargins(0, 0, 0, 0) + self.layout.setSpacing(0) + + self.menu = QtWidgets.QMenu() + # TODO move to stylesheets + self.menu.setStyleSheet(""" + *{color: #fff; background-color: #555; border: 1px solid #222;} + ::item {background-color: transparent;padding: 5px; + padding-left: 10px;padding-right: 10px;} + ::item:selected {background-color: #666;} + """) + + self.button = QtWidgets.QPushButton(button_title) + self.button.setObjectName("ButtonWithMenu") + + self.layout.addWidget(self.button) + + self.button.clicked.connect(self.btn_clicked) + + def btn_clicked(self): + self.menu.popup(self.button.mapToGlobal( + QtCore.QPoint(0, self.button.height()) + )) + + def addItem(self, text, callback): + self.menu.addAction(text, callback) + self.button.setToolTip("Select to apply predefined presets") + + def clearMenu(self): + self.menu.clear() + self.button.setToolTip("Presets not found") + + +class CommentBox(QtWidgets.QLineEdit): + + def __init__(self, placeholder_text, parent=None): + super(CommentBox, self).__init__(parent=parent) + self.placeholder = QtWidgets.QLabel(placeholder_text, self) + self.placeholder.move(2, 2) + + def focusInEvent(self, event): + self.placeholder.setVisible(False) + return super(CommentBox, self).focusInEvent(event) + + def focusOutEvent(self, event): + current_text = self.text() + current_text = current_text.strip(" ") + self.setText(current_text) + if not self.text(): + self.placeholder.setVisible(True) + return super(CommentBox, self).focusOutEvent(event) + + +class TerminalDetail(QtWidgets.QTextEdit): + def __init__(self, text, *args, **kwargs): + super(self.__class__, self).__init__(*args, **kwargs) + + self.setReadOnly(True) + self.setHtml(text) + self.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) + self.setWordWrapMode( + QtGui.QTextOption.WrapAtWordBoundaryOrAnywhere + ) + + def sizeHint(self): + content_margins = ( + self.contentsMargins().top() + + self.contentsMargins().bottom() + ) + size = self.document().documentLayout().documentSize().toSize() + size.setHeight(size.height() + content_margins) + return size + + +class FilterButton(QtWidgets.QPushButton): + def __init__(self, name, *args, **kwargs): + self.filter_name = name + + super(self.__class__, self).__init__(*args, **kwargs) + + self.toggled.connect(self.on_toggle) + + self.setProperty("type", name) + self.setObjectName("TerminalFilerBtn") + self.setCheckable(True) + self.setChecked( + model.TerminalProxy.filter_buttons_checks[name] + ) + + def on_toggle(self, toggle_state): + model.TerminalProxy.change_filter(self.filter_name, toggle_state) + + +class TerminalFilterWidget(QtWidgets.QWidget): + # timer.timeout.connect(lambda: self._update(self.parent_widget)) + def __init__(self, *args, **kwargs): + super(self.__class__, self).__init__(*args, **kwargs) + + self.filter_changed = QtCore.Signal() + + info_icon = awesome.tags["info"] + log_icon = awesome.tags["circle"] + error_icon = awesome.tags["exclamation-triangle"] + + filter_buttons = ( + FilterButton("info", info_icon), + FilterButton("log_debug", log_icon), + FilterButton("log_info", log_icon), + FilterButton("log_warning", log_icon), + FilterButton("log_error", log_icon), + FilterButton("log_critical", log_icon), + FilterButton("error", error_icon) + ) + + layout = QtWidgets.QHBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + # Add spacers + layout.addWidget(QtWidgets.QWidget(), 1) + + for btn in filter_buttons: + layout.addWidget(btn) + + self.setLayout(layout) + + self.filter_buttons = filter_buttons diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py new file mode 100644 index 0000000000..84003a88aa --- /dev/null +++ b/pype/tools/pyblish_pype/window.py @@ -0,0 +1,1055 @@ +"""Main Window + +States: + These are all possible states and their transitions. + + + reset + ' + ' + ' + ___v__ + | | reset + | Idle |--------------------. + | |<-------------------' + | | + | | _____________ + | | validate | | reset # TODO + | |----------------->| In-progress |-----------. + | | |_____________| ' + | |<-------------------------------------------' + | | + | | _____________ + | | publish | | + | |----------------->| In-progress |---. + | | |_____________| ' + | |<-----------------------------------' + |______| + + +Todo: + There are notes spread throughout this project with the syntax: + + - TODO(username) + + The `username` is a quick and dirty indicator of who made the note + and is by no means exclusive to that person in terms of seeing it + done. Feel free to do, or make your own TODO's as you code. Just + make sure the description is sufficient for anyone reading it for + the first time to understand how to actually to it! + +""" +from functools import partial + +from . import delegate, model, settings, util, view, widgets +from .awesome import tags as awesome + +from .vendor.Qt import QtCore, QtGui, QtWidgets +from .constants import ( + PluginStates, PluginActionStates, InstanceStates, GroupStates, Roles +) + + +class Window(QtWidgets.QDialog): + def __init__(self, controller, parent=None): + super(Window, self).__init__(parent=parent) + + # Use plastique style for specific ocations + # TODO set style name via environment variable + low_keys = { + key.lower(): key + for key in QtWidgets.QStyleFactory.keys() + } + if "plastique" in low_keys: + self.setStyle( + QtWidgets.QStyleFactory.create(low_keys["plastique"]) + ) + + icon = QtGui.QIcon(util.get_asset("img", "logo-extrasmall.png")) + if parent is None: + on_top_flag = QtCore.Qt.WindowStaysOnTopHint + else: + on_top_flag = QtCore.Qt.Dialog + + self.setWindowFlags( + self.windowFlags() + | QtCore.Qt.WindowTitleHint + | QtCore.Qt.WindowMaximizeButtonHint + | QtCore.Qt.WindowMinimizeButtonHint + | QtCore.Qt.WindowCloseButtonHint + | on_top_flag + ) + self.setWindowIcon(icon) + self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + + self.controller = controller + + main_widget = QtWidgets.QWidget(self) + + # General layout + header_widget = QtWidgets.QWidget(parent=main_widget) + + header_tab_widget = QtWidgets.QWidget(header_widget) + header_tab_artist = QtWidgets.QRadioButton(header_tab_widget) + header_tab_overview = QtWidgets.QRadioButton(header_tab_widget) + header_tab_terminal = QtWidgets.QRadioButton(header_tab_widget) + header_spacer = QtWidgets.QWidget(header_tab_widget) + + header_aditional_btns = QtWidgets.QWidget(header_tab_widget) + + aditional_btns_layout = QtWidgets.QHBoxLayout(header_aditional_btns) + + presets_button = widgets.ButtonWithMenu(awesome["filter"]) + presets_button.setEnabled(False) + aditional_btns_layout.addWidget(presets_button) + + layout_tab = QtWidgets.QHBoxLayout(header_tab_widget) + layout_tab.setContentsMargins(0, 0, 0, 0) + layout_tab.setSpacing(0) + layout_tab.addWidget(header_tab_artist, 0) + layout_tab.addWidget(header_tab_overview, 0) + layout_tab.addWidget(header_tab_terminal, 0) + # Compress items to the left + layout_tab.addWidget(header_spacer, 1) + layout_tab.addWidget(header_aditional_btns, 1) + + layout = QtWidgets.QHBoxLayout(header_widget) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + layout.addWidget(header_tab_widget) + + header_widget.setLayout(layout) + + # Artist Page + instance_model = model.InstanceModel(controller) + + artist_page = QtWidgets.QWidget() + + artist_view = view.ArtistView() + artist_view.show_perspective.connect(self.toggle_perspective_widget) + artist_proxy = model.ArtistProxy() + artist_proxy.setSourceModel(instance_model) + artist_view.setModel(artist_proxy) + + artist_delegate = delegate.ArtistDelegate() + artist_view.setItemDelegate(artist_delegate) + + layout = QtWidgets.QVBoxLayout(artist_page) + layout.addWidget(artist_view) + layout.setContentsMargins(5, 5, 5, 5) + layout.setSpacing(0) + + artist_page.setLayout(layout) + + # Overview Page + # TODO add parent + overview_page = QtWidgets.QWidget() + + overview_instance_view = view.OverviewView(parent=overview_page) + overview_instance_delegate = delegate.InstanceDelegate( + parent=overview_instance_view + ) + overview_instance_view.setItemDelegate(overview_instance_delegate) + overview_instance_view.setModel(instance_model) + + overview_plugin_view = view.OverviewView(parent=overview_page) + overview_plugin_delegate = delegate.PluginDelegate( + parent=overview_plugin_view + ) + overview_plugin_view.setItemDelegate(overview_plugin_delegate) + plugin_model = model.PluginModel(controller) + plugin_proxy = model.PluginFilterProxy() + plugin_proxy.setSourceModel(plugin_model) + overview_plugin_view.setModel(plugin_proxy) + + layout = QtWidgets.QHBoxLayout(overview_page) + layout.addWidget(overview_instance_view, 1) + layout.addWidget(overview_plugin_view, 1) + layout.setContentsMargins(5, 5, 5, 5) + layout.setSpacing(0) + overview_page.setLayout(layout) + + # Terminal + terminal_container = QtWidgets.QWidget() + + terminal_view = view.TerminalView() + terminal_model = model.TerminalModel() + terminal_proxy = model.TerminalProxy(terminal_view) + terminal_proxy.setSourceModel(terminal_model) + + terminal_view.setModel(terminal_proxy) + terminal_delegate = delegate.TerminalItem() + terminal_view.setItemDelegate(terminal_delegate) + + layout = QtWidgets.QVBoxLayout(terminal_container) + layout.addWidget(terminal_view) + layout.setContentsMargins(5, 5, 5, 5) + layout.setSpacing(0) + + terminal_container.setLayout(layout) + + terminal_page = QtWidgets.QWidget() + layout = QtWidgets.QVBoxLayout(terminal_page) + layout.addWidget(terminal_container) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + # Add some room between window borders and contents + body_widget = QtWidgets.QWidget(main_widget) + layout = QtWidgets.QHBoxLayout(body_widget) + layout.setContentsMargins(5, 5, 5, 1) + layout.addWidget(artist_page) + layout.addWidget(overview_page) + layout.addWidget(terminal_page) + + # Comment Box + comment_box = widgets.CommentBox("Comment...", self) + + intent_box = QtWidgets.QComboBox() + + intent_model = model.IntentModel() + intent_box.setModel(intent_model) + intent_box.currentIndexChanged.connect(self.on_intent_changed) + + comment_intent_widget = QtWidgets.QWidget() + comment_intent_layout = QtWidgets.QHBoxLayout(comment_intent_widget) + comment_intent_layout.setContentsMargins(0, 0, 0, 0) + comment_intent_layout.setSpacing(5) + comment_intent_layout.addWidget(comment_box) + comment_intent_layout.addWidget(intent_box) + + # Terminal filtering + terminal_filters_widget = widgets.TerminalFilterWidget() + + # Footer + footer_widget = QtWidgets.QWidget(main_widget) + + footer_info = QtWidgets.QLabel(footer_widget) + footer_spacer = QtWidgets.QWidget(footer_widget) + footer_button_reset = QtWidgets.QPushButton( + awesome["refresh"], footer_widget + ) + footer_button_validate = QtWidgets.QPushButton( + awesome["flask"], footer_widget + ) + footer_button_play = QtWidgets.QPushButton( + awesome["play"], footer_widget + ) + footer_button_stop = QtWidgets.QPushButton( + awesome["stop"], footer_widget + ) + + layout = QtWidgets.QHBoxLayout() + layout.setContentsMargins(5, 5, 5, 5) + layout.addWidget(footer_info, 0) + layout.addWidget(footer_spacer, 1) + layout.addWidget(footer_button_stop, 0) + layout.addWidget(footer_button_reset, 0) + layout.addWidget(footer_button_validate, 0) + layout.addWidget(footer_button_play, 0) + + footer_layout = QtWidgets.QVBoxLayout(footer_widget) + footer_layout.addWidget(comment_intent_widget) + footer_layout.addLayout(layout) + + footer_widget.setProperty("success", -1) + + # Placeholder for when GUI is closing + # TODO(marcus): Fade to black and the the user about what's happening + closing_placeholder = QtWidgets.QWidget(main_widget) + closing_placeholder.setSizePolicy( + QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Expanding + ) + closing_placeholder.hide() + + perspective_widget = widgets.PerspectiveWidget(self) + perspective_widget.hide() + + # Main layout + layout = QtWidgets.QVBoxLayout(main_widget) + layout.addWidget(header_widget, 0) + layout.addWidget(body_widget, 3) + layout.addWidget(perspective_widget, 3) + layout.addWidget(closing_placeholder, 1) + layout.addWidget(terminal_filters_widget, 0) + layout.addWidget(footer_widget, 0) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + main_widget.setLayout(layout) + + self.main_layout = QtWidgets.QVBoxLayout(self) + self.main_layout.setContentsMargins(0, 0, 0, 0) + self.main_layout.setSpacing(0) + self.main_layout.addWidget(main_widget) + + # Display info + info_effect = QtWidgets.QGraphicsOpacityEffect(footer_info) + footer_info.setGraphicsEffect(info_effect) + + on = QtCore.QPropertyAnimation(info_effect, b"opacity") + on.setDuration(0) + on.setStartValue(0) + on.setEndValue(1) + + off = QtCore.QPropertyAnimation(info_effect, b"opacity") + off.setDuration(0) + off.setStartValue(1) + off.setEndValue(0) + + fade = QtCore.QPropertyAnimation(info_effect, b"opacity") + fade.setDuration(500) + fade.setStartValue(1.0) + fade.setEndValue(0.0) + + animation_info_msg = QtCore.QSequentialAnimationGroup() + animation_info_msg.addAnimation(on) + animation_info_msg.addPause(50) + animation_info_msg.addAnimation(off) + animation_info_msg.addPause(50) + animation_info_msg.addAnimation(on) + animation_info_msg.addPause(2000) + animation_info_msg.addAnimation(fade) + + """Setup + + Widgets are referred to in CSS via their object-name. We + use the same mechanism internally to refer to objects; so rather + than storing widgets as self.my_widget, it is referred to as: + + >>> my_widget = self.findChild(QtWidgets.QWidget, "MyWidget") + + This way there is only ever a single method of referring to any widget. + """ + + names = { + # Main + "Header": header_widget, + "Body": body_widget, + "Footer": footer_widget, + + # Pages + "Artist": artist_page, + "Overview": overview_page, + "Terminal": terminal_page, + + # Tabs + "ArtistTab": header_tab_artist, + "OverviewTab": header_tab_overview, + "TerminalTab": header_tab_terminal, + + # Views + "TerminalView": terminal_view, + + # Buttons + "Play": footer_button_play, + "Validate": footer_button_validate, + "Reset": footer_button_reset, + "Stop": footer_button_stop, + + # Misc + "HeaderSpacer": header_spacer, + "FooterSpacer": footer_spacer, + "FooterInfo": footer_info, + "CommentIntentWidget": comment_intent_widget, + "CommentBox": comment_box, + "CommentPlaceholder": comment_box.placeholder, + "ClosingPlaceholder": closing_placeholder, + "IntentBox": intent_box + } + + for name, _widget in names.items(): + _widget.setObjectName(name) + + # Enable CSS on plain QWidget objects + for _widget in ( + header_widget, + body_widget, + artist_page, + comment_box, + overview_page, + terminal_page, + footer_widget, + footer_button_play, + footer_button_validate, + footer_button_stop, + footer_button_reset, + footer_spacer, + closing_placeholder + ): + _widget.setAttribute(QtCore.Qt.WA_StyledBackground) + + # Signals + header_tab_artist.toggled.connect( + lambda: self.on_tab_changed("artist") + ) + header_tab_overview.toggled.connect( + lambda: self.on_tab_changed("overview") + ) + header_tab_terminal.toggled.connect( + lambda: self.on_tab_changed("terminal") + ) + + overview_instance_view.show_perspective.connect( + self.toggle_perspective_widget + ) + overview_plugin_view.show_perspective.connect( + self.toggle_perspective_widget + ) + + controller.switch_toggleability.connect(self.change_toggleability) + + controller.was_reset.connect(self.on_was_reset) + # This is called synchronously on each process + controller.was_processed.connect(self.on_was_processed) + controller.passed_group.connect(self.on_passed_group) + controller.was_stopped.connect(self.on_was_stopped) + controller.was_finished.connect(self.on_was_finished) + + controller.was_skipped.connect(self.on_was_skipped) + controller.was_acted.connect(self.on_was_acted) + + # NOTE: Listeners to this signal are run in the main thread + controller.about_to_process.connect( + self.on_about_to_process, + QtCore.Qt.DirectConnection + ) + + artist_view.toggled.connect(self.on_item_toggled) + overview_instance_view.toggled.connect(self.on_item_toggled) + overview_plugin_view.toggled.connect(self.on_item_toggled) + + footer_button_stop.clicked.connect(self.on_stop_clicked) + footer_button_reset.clicked.connect(self.on_reset_clicked) + footer_button_validate.clicked.connect(self.on_validate_clicked) + footer_button_play.clicked.connect(self.on_play_clicked) + + comment_box.textChanged.connect(self.on_comment_entered) + comment_box.returnPressed.connect(self.on_play_clicked) + overview_plugin_view.customContextMenuRequested.connect( + self.on_plugin_action_menu_requested + ) + + instance_model.group_created.connect( + overview_instance_view.expand + ) + + self.main_widget = main_widget + + self.header_widget = header_widget + self.body_widget = body_widget + + self.terminal_filters_widget = terminal_filters_widget + + self.footer_widget = footer_widget + self.footer_button_reset = footer_button_reset + self.footer_button_validate = footer_button_validate + self.footer_button_play = footer_button_play + self.footer_button_stop = footer_button_stop + + self.overview_instance_view = overview_instance_view + self.overview_plugin_view = overview_plugin_view + self.plugin_model = plugin_model + self.plugin_proxy = plugin_proxy + self.instance_model = instance_model + + self.artist_proxy = artist_proxy + self.artist_view = artist_view + + self.presets_button = presets_button + + self.animation_info_msg = animation_info_msg + + self.terminal_model = terminal_model + self.terminal_proxy = terminal_proxy + self.terminal_view = terminal_view + + self.comment_main_widget = comment_intent_widget + self.comment_box = comment_box + self.intent_box = intent_box + self.intent_model = intent_model + + self.perspective_widget = perspective_widget + + self.tabs = { + "artist": header_tab_artist, + "overview": header_tab_overview, + "terminal": header_tab_terminal + } + self.pages = { + "artist": artist_page, + "overview": overview_page, + "terminal": terminal_page + } + + current_page = settings.InitialTab or "artist" + self.state = { + "is_closing": False, + "current_page": current_page + } + + self.tabs[current_page].setChecked(True) + + # ------------------------------------------------------------------------- + # + # Event handlers + # + # ------------------------------------------------------------------------- + def set_presets(self, key): + plugin_settings = self.controller.possible_presets.get(key) + if not plugin_settings: + return + + for plugin_item in self.plugin_model.plugin_items.values(): + if not plugin_item.plugin.optional: + continue + + value = plugin_settings.get( + plugin_item.plugin.__name__, + # if plugin is not in presets then set default value + self.controller.optional_default.get( + plugin_item.plugin.__name__ + ) + ) + if value is None: + continue + + plugin_item.setData(value, QtCore.Qt.CheckStateRole) + + def toggle_perspective_widget(self, index=None): + show = False + if index: + show = True + self.perspective_widget.set_context(index) + + self.body_widget.setVisible(not show) + self.header_widget.setVisible(not show) + + self.perspective_widget.setVisible(show) + self.terminal_filters_widget.setVisible(show) + + def change_toggleability(self, enable_value): + for plugin_item in self.plugin_model.plugin_items.values(): + plugin_item.setData(enable_value, Roles.IsEnabledRole) + + for instance_item in ( + self.instance_model.instance_items.values() + ): + instance_item.setData(enable_value, Roles.IsEnabledRole) + + def on_item_toggled(self, index, state=None): + """An item is requesting to be toggled""" + if not index.data(Roles.IsOptionalRole): + return self.info("This item is mandatory") + + if self.controller.collect_state != 1: + return self.info("Cannot toggle") + + if state is None: + state = not index.data(QtCore.Qt.CheckStateRole) + + index.model().setData(index, state, QtCore.Qt.CheckStateRole) + self.update_compatibility() + + def on_tab_changed(self, target): + self.comment_main_widget.setVisible(not target == "terminal") + self.terminal_filters_widget.setVisible(target == "terminal") + + for name, page in self.pages.items(): + if name != target: + page.hide() + + self.pages[target].show() + + self.state["current_page"] = target + + def on_validate_clicked(self): + self.comment_box.setEnabled(False) + self.intent_box.setEnabled(False) + + self.validate() + + def on_play_clicked(self): + self.comment_box.setEnabled(False) + self.intent_box.setEnabled(False) + + self.publish() + + def on_reset_clicked(self): + self.reset() + + def on_stop_clicked(self): + self.info("Stopping..") + self.controller.stop() + + # TODO checks + self.footer_button_reset.setEnabled(True) + self.footer_button_play.setEnabled(False) + self.footer_button_stop.setEnabled(False) + + def on_comment_entered(self): + """The user has typed a comment.""" + self.controller.context.data["comment"] = self.comment_box.text() + + def on_intent_changed(self): + idx = self.intent_model.index(self.intent_box.currentIndex(), 0) + intent_value = self.intent_model.data(idx, Roles.IntentItemValue) + intent_label = self.intent_model.data(idx, QtCore.Qt.DisplayRole) + + # TODO move to play + if self.controller.context: + self.controller.context.data["intent"] = { + "value": intent_value, + "label": intent_label + } + + def on_about_to_process(self, plugin, instance): + """Reflect currently running pair in GUI""" + if instance is None: + instance_id = self.controller.context.id + else: + instance_id = instance.id + + instance_item = ( + self.instance_model.instance_items[instance_id] + ) + instance_item.setData( + {InstanceStates.InProgress: True}, + Roles.PublishFlagsRole + ) + + plugin_item = self.plugin_model.plugin_items[plugin._id] + plugin_item.setData( + {PluginStates.InProgress: True}, + Roles.PublishFlagsRole + ) + + self.info("{} {}".format( + self.tr("Processing"), plugin_item.data(QtCore.Qt.DisplayRole) + )) + + def on_plugin_action_menu_requested(self, pos): + """The user right-clicked on a plug-in + __________ + | | + | Action 1 | + | Action 2 | + | Action 3 | + | | + |__________| + + """ + + index = self.overview_plugin_view.indexAt(pos) + actions = index.data(Roles.PluginValidActionsRole) + + if not actions: + return + + menu = QtWidgets.QMenu(self) + plugin_id = index.data(Roles.ObjectIdRole) + plugin_item = self.plugin_model.plugin_items[plugin_id] + print("plugin is: %s" % plugin_item.plugin) + + for action in actions: + qaction = QtWidgets.QAction(action.label or action.__name__, self) + qaction.triggered.connect(partial(self.act, plugin_item, action)) + menu.addAction(qaction) + + menu.popup(self.overview_plugin_view.viewport().mapToGlobal(pos)) + + def update_compatibility(self): + self.plugin_model.update_compatibility() + self.plugin_proxy.invalidateFilter() + + def on_was_reset(self): + # Append context object to instances model + self.instance_model.append(self.controller.context) + + for plugin in self.controller.plugins: + self.plugin_model.append(plugin) + + self.overview_instance_view.expandAll() + self.overview_plugin_view.expandAll() + + self.presets_button.clearMenu() + if self.controller.possible_presets: + self.presets_button.setEnabled(True) + for key in self.controller.possible_presets: + self.presets_button.addItem( + key, partial(self.set_presets, key) + ) + + self.instance_model.restore_checkstates() + self.plugin_model.restore_checkstates() + + self.perspective_widget.reset() + + # Append placeholder comment from Context + # This allows users to inject a comment from elsewhere, + # or to perhaps provide a placeholder comment/template + # for artists to fill in. + comment = self.controller.context.data.get("comment") + self.comment_box.setText(comment or None) + self.comment_box.setEnabled(True) + + if self.intent_model.has_items: + self.on_intent_changed() + self.intent_box.setEnabled(True) + + # Refresh tab + self.on_tab_changed(self.state["current_page"]) + self.update_compatibility() + + self.footer_button_validate.setEnabled(True) + self.footer_button_reset.setEnabled(True) + self.footer_button_stop.setEnabled(False) + self.footer_button_play.setEnabled(True) + self.footer_button_play.setFocus() + + def on_passed_group(self, order): + + for group_item in self.instance_model.group_items.values(): + if self.overview_instance_view.isExpanded(group_item.index()): + continue + + if group_item.publish_states & GroupStates.HasError: + self.overview_instance_view.expand(group_item.index()) + + for group_item in self.plugin_model.group_items.values(): + # TODO check only plugins from the group + if ( + group_item.publish_states & GroupStates.HasFinished + or (order is not None and group_item.order >= order) + ): + continue + + if group_item.publish_states & GroupStates.HasError: + self.overview_plugin_view.expand( + self.plugin_proxy.mapFromSource(group_item.index()) + ) + continue + + group_item.setData( + {GroupStates.HasFinished: True}, + Roles.PublishFlagsRole + ) + self.overview_plugin_view.collapse( + self.plugin_proxy.mapFromSource(group_item.index()) + ) + + def on_was_stopped(self): + errored = self.controller.errored + self.footer_button_play.setEnabled(not errored) + self.footer_button_validate.setEnabled( + not errored and not self.controller.validated + ) + self.footer_button_reset.setEnabled(True) + self.footer_button_stop.setEnabled(False) + if errored: + self.footer_widget.setProperty("success", 0) + self.footer_widget.style().polish(self.footer_widget) + + def on_was_skipped(self, plugin): + plugin_item = self.plugin_model.plugin_items[plugin.id] + plugin_item.setData( + {PluginStates.WasSkipped: True}, + Roles.PublishFlagsRole + ) + + def on_was_finished(self): + self.footer_button_play.setEnabled(False) + self.footer_button_validate.setEnabled(False) + self.footer_button_reset.setEnabled(True) + self.footer_button_stop.setEnabled(False) + + if self.controller.errored: + success_val = 0 + self.info(self.tr("Stopped due to error(s), see Terminal.")) + self.comment_box.setEnabled(False) + self.intent_box.setEnabled(False) + + else: + success_val = 1 + self.info(self.tr("Finished successfully!")) + + self.footer_widget.setProperty("success", success_val) + self.footer_widget.style().polish(self.footer_widget) + + for instance_item in ( + self.instance_model.instance_items.values() + ): + instance_item.setData( + {InstanceStates.HasFinished: True}, + Roles.PublishFlagsRole + ) + + for group_item in self.instance_model.group_items.values(): + group_item.setData( + {GroupStates.HasFinished: True}, + Roles.PublishFlagsRole + ) + + self.update_compatibility() + + def on_was_processed(self, result): + existing_ids = set(self.instance_model.instance_items.keys()) + existing_ids.remove(self.controller.context.id) + for instance in self.controller.context: + if instance.id not in existing_ids: + self.instance_model.append(instance) + else: + existing_ids.remove(instance.id) + + for instance_id in existing_ids: + self.instance_model.remove(instance_id) + + if result.get("error"): + # Toggle from artist to overview tab on error + if self.tabs["artist"].isChecked(): + self.tabs["overview"].toggle() + + result["records"] = self.terminal_model.prepare_records(result) + + plugin_item = self.plugin_model.update_with_result(result) + instance_item = self.instance_model.update_with_result(result) + + self.terminal_model.update_with_result(result) + while not self.terminal_model.items_to_set_widget.empty(): + item = self.terminal_model.items_to_set_widget.get() + widget = widgets.TerminalDetail(item.data(QtCore.Qt.DisplayRole)) + index = self.terminal_proxy.mapFromSource(item.index()) + self.terminal_view.setIndexWidget(index, widget) + + self.update_compatibility() + + if self.perspective_widget.isVisible(): + self.perspective_widget.update_context( + plugin_item, instance_item + ) + + # ------------------------------------------------------------------------- + # + # Functions + # + # ------------------------------------------------------------------------- + + def reset(self): + """Prepare GUI for reset""" + self.info(self.tr("About to reset..")) + + self.presets_button.setEnabled(False) + self.footer_widget.setProperty("success", -1) + self.footer_widget.style().polish(self.footer_widget) + + self.instance_model.store_checkstates() + self.plugin_model.store_checkstates() + + # Reset current ids to secure no previous instances get mixed in. + self.instance_model.reset() + self.plugin_model.reset() + self.intent_model.reset() + self.terminal_model.reset() + + self.footer_button_stop.setEnabled(False) + self.footer_button_reset.setEnabled(False) + self.footer_button_validate.setEnabled(False) + self.footer_button_play.setEnabled(False) + + self.intent_box.setVisible(self.intent_model.has_items) + if self.intent_model.has_items: + self.intent_box.setCurrentIndex(self.intent_model.default_index) + + self.comment_box.placeholder.setVisible(False) + self.comment_box.placeholder.setVisible(True) + # Launch controller reset + util.defer(500, self.controller.reset) + + def validate(self): + self.info(self.tr("Preparing validate..")) + self.footer_button_stop.setEnabled(True) + self.footer_button_reset.setEnabled(False) + self.footer_button_validate.setEnabled(False) + self.footer_button_play.setEnabled(False) + + util.defer(5, self.controller.validate) + + def publish(self): + self.info(self.tr("Preparing publish..")) + + self.footer_button_stop.setEnabled(True) + self.footer_button_reset.setEnabled(False) + self.footer_button_validate.setEnabled(False) + self.footer_button_play.setEnabled(False) + + util.defer(5, self.controller.publish) + + def act(self, plugin_item, action): + self.info("%s %s.." % (self.tr("Preparing"), action)) + + self.footer_button_stop.setEnabled(True) + self.footer_button_reset.setEnabled(False) + self.footer_button_validate.setEnabled(False) + self.footer_button_play.setEnabled(False) + + # Cause view to update, but it won't visually + # happen until Qt is given time to idle.. + plugin_item.setData( + PluginActionStates.InProgress, Roles.PluginActionProgressRole + ) + + # Give Qt time to draw + util.defer(100, lambda: self.controller.act( + plugin_item.plugin, action + )) + + self.info(self.tr("Action prepared.")) + + def on_was_acted(self, result): + self.footer_button_reset.setEnabled(True) + self.footer_button_stop.setEnabled(False) + + # Update action with result + plugin_item = self.plugin_model.plugin_items[result["plugin"].id] + action_state = plugin_item.data(Roles.PluginActionProgressRole) + action_state |= PluginActionStates.HasFinished + + error = result.get("error") + if error: + records = result.get("records") or [] + action_state |= PluginActionStates.HasFailed + fname, line_no, func, exc = error.traceback + + records.append({ + "label": str(error), + "type": "error", + "filename": str(fname), + "lineno": str(line_no), + "func": str(func), + "traceback": error.formatted_traceback + }) + + result["records"] = records + + plugin_item.setData(action_state, Roles.PluginActionProgressRole) + + self.plugin_model.update_with_result(result) + self.instance_model.update_with_result(result) + self.terminal_model.update_with_result(result) + + def closeEvent(self, event): + """Perform post-flight checks before closing + + Make sure processing of any kind is wrapped up before closing + + """ + + # Make it snappy, but take care to clean it all up. + # TODO(marcus): Enable GUI to return on problem, such + # as asking whether or not the user really wants to quit + # given there are things currently running. + self.hide() + + if self.state["is_closing"]: + + # Explicitly clear potentially referenced data + self.info(self.tr("Cleaning up models..")) + self.intent_model.deleteLater() + self.plugin_model.deleteLater() + self.terminal_model.deleteLater() + self.terminal_proxy.deleteLater() + self.plugin_proxy.deleteLater() + + self.artist_view.setModel(None) + self.overview_instance_view.setModel(None) + self.overview_plugin_view.setModel(None) + self.terminal_view.setModel(None) + + self.info(self.tr("Cleaning up controller..")) + self.controller.cleanup() + + self.info(self.tr("All clean!")) + self.info(self.tr("Good bye")) + return super(Window, self).closeEvent(event) + + self.info(self.tr("Closing..")) + + def on_problem(): + self.heads_up( + "Warning", "Had trouble closing down. " + "Please tell someone and try again." + ) + self.show() + + if self.controller.is_running: + self.info(self.tr("..as soon as processing is finished..")) + self.controller.stop() + self.finished.connect(self.close) + util.defer(2000, on_problem) + return event.ignore() + + self.state["is_closing"] = True + + util.defer(200, self.close) + return event.ignore() + + def reject(self): + """Handle ESC key""" + + if self.controller.is_running: + self.info(self.tr("Stopping..")) + self.controller.stop() + + # ------------------------------------------------------------------------- + # + # Feedback + # + # ------------------------------------------------------------------------- + + def info(self, message): + """Print user-facing information + + Arguments: + message (str): Text message for the user + + """ + + info = self.findChild(QtWidgets.QLabel, "FooterInfo") + info.setText(message) + + # Include message in terminal + self.terminal_model.append({ + "label": message, + "type": "info" + }) + + self.animation_info_msg.stop() + self.animation_info_msg.start() + + # TODO(marcus): Should this be configurable? Do we want + # the shell to fill up with these messages? + util.u_print(message) + + def warning(self, message): + """Block processing and print warning until user hits "Continue" + + Arguments: + message (str): Message to display + + """ + + # TODO(marcus): Implement this. + self.info(message) + + def heads_up(self, title, message, command=None): + """Provide a front-and-center message with optional command + + Arguments: + title (str): Bold and short message + message (str): Extended message + command (optional, callable): Function is provided as a button + + """ + + # TODO(marcus): Implement this. + self.info(message) diff --git a/pype/tools/tray/__main__.py b/pype/tools/tray/__main__.py new file mode 100644 index 0000000000..d0006c0afe --- /dev/null +++ b/pype/tools/tray/__main__.py @@ -0,0 +1,4 @@ +import sys +import pype_tray + +sys.exit(pype_tray.PypeTrayApplication().exec_()) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py new file mode 100644 index 0000000000..eec8f61cc4 --- /dev/null +++ b/pype/tools/tray/pype_tray.py @@ -0,0 +1,506 @@ +import os +import sys +import platform +from avalon import style +from Qt import QtCore, QtGui, QtWidgets, QtSvg +from pype.resources import get_resource +from pype.api import config, Logger + + +class TrayManager: + """Cares about context of application. + + Load submenus, actions, separators and modules into tray's context. + """ + modules = {} + services = {} + services_submenu = None + + errors = [] + items = ( + config.get_presets(first_run=True) + .get('tray', {}) + .get('menu_items', []) + ) + available_sourcetypes = ['python', 'file'] + + def __init__(self, tray_widget, main_window): + self.tray_widget = tray_widget + self.main_window = main_window + self.log = Logger().get_logger(self.__class__.__name__) + + self.icon_run = QtGui.QIcon(get_resource('circle_green.png')) + self.icon_stay = QtGui.QIcon(get_resource('circle_orange.png')) + self.icon_failed = QtGui.QIcon(get_resource('circle_red.png')) + + self.services_thread = None + + def process_presets(self): + """Add modules to tray by presets. + + This is start up method for TrayManager. Loads presets and import + modules described in "menu_items.json". In `item_usage` key you can + specify by item's title or import path if you want to import it. + Example of "menu_items.json" file: + { + "item_usage": { + "Statics Server": false + } + }, { + "item_import": [{ + "title": "Ftrack", + "type": "module", + "import_path": "pype.ftrack.tray", + "fromlist": ["pype", "ftrack"] + }, { + "title": "Statics Server", + "type": "module", + "import_path": "pype.services.statics_server", + "fromlist": ["pype","services"] + }] + } + In this case `Statics Server` won't be used. + """ + # Backwards compatible presets loading + if isinstance(self.items, list): + items = self.items + else: + items = [] + # Get booleans is module should be used + usages = self.items.get("item_usage") or {} + for item in self.items.get("item_import", []): + import_path = item.get("import_path") + title = item.get("title") + + item_usage = usages.get(title) + if item_usage is None: + item_usage = usages.get(import_path, True) + + if item_usage: + items.append(item) + else: + if not title: + title = import_path + self.log.debug("{} - Module ignored".format(title)) + + if items: + self.process_items(items, self.tray_widget.menu) + + # Add services if they are + if self.services_submenu is not None: + self.tray_widget.menu.addMenu(self.services_submenu) + + # Add separator + if items and self.services_submenu is not None: + self.add_separator(self.tray_widget.menu) + + # Add Exit action to menu + aExit = QtWidgets.QAction("&Exit", self.tray_widget) + aExit.triggered.connect(self.tray_widget.exit) + self.tray_widget.menu.addAction(aExit) + + # Tell each module which modules were imported + self.connect_modules() + self.start_modules() + + def process_items(self, items, parent_menu): + """ Loop through items and add them to parent_menu. + + :param items: contains dictionary objects representing each item + :type items: list + :param parent_menu: menu where items will be add + :type parent_menu: QtWidgets.QMenu + """ + for item in items: + i_type = item.get('type', None) + result = False + if i_type is None: + continue + elif i_type == 'module': + result = self.add_module(item, parent_menu) + elif i_type == 'action': + result = self.add_action(item, parent_menu) + elif i_type == 'menu': + result = self.add_menu(item, parent_menu) + elif i_type == 'separator': + result = self.add_separator(parent_menu) + + if result is False: + self.errors.append(item) + + def add_module(self, item, parent_menu): + """Inicialize object of module and add it to context. + + :param item: item from presets containing information about module + :type item: dict + :param parent_menu: menu where module's submenus/actions will be add + :type parent_menu: QtWidgets.QMenu + :returns: success of module implementation + :rtype: bool + + REQUIRED KEYS (item): + :import_path (*str*): + - full import path as python's import + - e.g. *"path.to.module"* + :fromlist (*list*): + - subparts of import_path (as from is used) + - e.g. *["path", "to"]* + OPTIONAL KEYS (item): + :title (*str*): + - represents label shown in services menu + - import_path is used if title is not set + - title is not used at all if module is not a service + + .. note:: + Module is added as **service** if object does not have + *tray_menu* method. + """ + import_path = item.get('import_path', None) + title = item.get('title', import_path) + fromlist = item.get('fromlist', []) + try: + module = __import__( + "{}".format(import_path), + fromlist=fromlist + ) + obj = module.tray_init(self.tray_widget, self.main_window) + name = obj.__class__.__name__ + if hasattr(obj, 'tray_menu'): + obj.tray_menu(parent_menu) + else: + if self.services_submenu is None: + self.services_submenu = QtWidgets.QMenu( + 'Services', self.tray_widget.menu + ) + action = QtWidgets.QAction(title, self.services_submenu) + action.setIcon(self.icon_run) + self.services_submenu.addAction(action) + if hasattr(obj, 'set_qaction'): + obj.set_qaction(action, self.icon_failed) + self.modules[name] = obj + self.log.info("{} - Module imported".format(title)) + except ImportError as ie: + if self.services_submenu is None: + self.services_submenu = QtWidgets.QMenu( + 'Services', self.tray_widget.menu + ) + action = QtWidgets.QAction(title, self.services_submenu) + action.setIcon(self.icon_failed) + self.services_submenu.addAction(action) + self.log.warning( + "{} - Module import Error: {}".format(title, str(ie)), + exc_info=True + ) + return False + return True + + def add_action(self, item, parent_menu): + """Adds action to parent_menu. + + :param item: item from presets containing information about action + :type item: dictionary + :param parent_menu: menu where action will be added + :type parent_menu: QtWidgets.QMenu + :returns: success of adding item to parent_menu + :rtype: bool + + REQUIRED KEYS (item): + :title (*str*): + - represents label shown in menu + :sourcetype (*str*): + - type of action *enum["file", "python"]* + :command (*str*): + - filepath to script *(sourcetype=="file")* + - python code as string *(sourcetype=="python")* + OPTIONAL KEYS (item): + :tooltip (*str*): + - will be shown when hover over action + """ + sourcetype = item.get('sourcetype', None) + command = item.get('command', None) + title = item.get('title', '*ERROR*') + tooltip = item.get('tooltip', None) + + if sourcetype not in self.available_sourcetypes: + self.log.error('item "{}" has invalid sourcetype'.format(title)) + return False + if command is None or command.strip() == '': + self.log.error('item "{}" has invalid command'.format(title)) + return False + + new_action = QtWidgets.QAction(title, parent_menu) + if tooltip is not None and tooltip.strip() != '': + new_action.setToolTip(tooltip) + + if sourcetype == 'python': + new_action.triggered.connect( + lambda: exec(command) + ) + elif sourcetype == 'file': + command = os.path.normpath(command) + if '$' in command: + command_items = command.split(os.path.sep) + for i in range(len(command_items)): + if command_items[i].startswith('$'): + # TODO: raise error if environment was not found? + command_items[i] = os.environ.get( + command_items[i].replace('$', ''), command_items[i] + ) + command = os.path.sep.join(command_items) + + new_action.triggered.connect( + lambda: exec(open(command).read(), globals()) + ) + + parent_menu.addAction(new_action) + + def add_menu(self, item, parent_menu): + """ Adds submenu to parent_menu. + + :param item: item from presets containing information about menu + :type item: dictionary + :param parent_menu: menu where submenu will be added + :type parent_menu: QtWidgets.QMenu + :returns: success of adding item to parent_menu + :rtype: bool + + REQUIRED KEYS (item): + :title (*str*): + - represents label shown in menu + :items (*list*): + - list of submenus / actions / separators / modules *(dict)* + """ + try: + title = item.get('title', None) + if title is None or title.strip() == '': + self.log.error('Missing title in menu from presets') + return False + new_menu = QtWidgets.QMenu(title, parent_menu) + new_menu.setProperty('submenu', 'on') + parent_menu.addMenu(new_menu) + + self.process_items(item.get('items', []), new_menu) + return True + except Exception: + return False + + def add_separator(self, parent_menu): + """ Adds separator to parent_menu. + + :param parent_menu: menu where submenu will be added + :type parent_menu: QtWidgets.QMenu + :returns: success of adding item to parent_menu + :rtype: bool + """ + try: + parent_menu.addSeparator() + return True + except Exception: + return False + + def connect_modules(self): + """Sends all imported modules to imported modules + which have process_modules method. + """ + for obj in self.modules.values(): + if hasattr(obj, 'process_modules'): + obj.process_modules(self.modules) + + def start_modules(self): + """Modules which can be modified by another modules and + must be launched after *connect_modules* should have tray_start + to start their process afterwards. (e.g. Ftrack actions) + """ + for obj in self.modules.values(): + if hasattr(obj, 'tray_start'): + obj.tray_start() + + def on_exit(self): + for obj in self.modules.values(): + if hasattr(obj, 'tray_exit'): + try: + obj.tray_exit() + except Exception: + self.log.error("Failed to exit module {}".format( + obj.__class__.__name__ + )) + + +class SystemTrayIcon(QtWidgets.QSystemTrayIcon): + """Tray widget. + + :param parent: Main widget that cares about all GUIs + :type parent: QtWidgets.QMainWindow + """ + def __init__(self, parent): + if os.getenv("PYPE_DEV"): + icon_file_name = "icon_dev.png" + else: + icon_file_name = "icon.png" + + self.icon = QtGui.QIcon(get_resource(icon_file_name)) + + QtWidgets.QSystemTrayIcon.__init__(self, self.icon, parent) + + # Store parent - QtWidgets.QMainWindow() + self.parent = parent + + # Setup menu in Tray + self.menu = QtWidgets.QMenu() + self.menu.setStyleSheet(style.load_stylesheet()) + + # Set modules + self.tray_man = TrayManager(self, self.parent) + self.tray_man.process_presets() + + # Catch activate event + self.activated.connect(self.on_systray_activated) + # Add menu to Context of SystemTrayIcon + self.setContextMenu(self.menu) + + def on_systray_activated(self, reason): + # show contextMenu if left click + if platform.system().lower() == "darwin": + return + if reason == QtWidgets.QSystemTrayIcon.Trigger: + position = QtGui.QCursor().pos() + self.contextMenu().popup(position) + + def exit(self): + """ Exit whole application. + + - Icon won't stay in tray after exit. + """ + self.hide() + self.tray_man.on_exit() + QtCore.QCoreApplication.exit() + + +class TrayMainWindow(QtWidgets.QMainWindow): + """ TrayMainWindow is base of Pype application. + + Every widget should have set this window as parent because + QSystemTrayIcon widget is not allowed to be a parent of any widget. + + :param app: Qt application manages application's control flow + :type app: QtWidgets.QApplication + + .. note:: + *TrayMainWindow* has ability to show **working** widget. + Calling methods: + - ``show_working()`` + - ``hide_working()`` + .. todo:: Hide working widget if idle is too long + """ + def __init__(self, app): + super().__init__() + self.app = app + + self.set_working_widget() + + self.trayIcon = SystemTrayIcon(self) + self.trayIcon.show() + + def set_working_widget(self): + image_file = get_resource('working.svg') + img_pix = QtGui.QPixmap(image_file) + if image_file.endswith('.svg'): + widget = QtSvg.QSvgWidget(image_file) + else: + widget = QtWidgets.QLabel() + widget.setPixmap(img_pix) + + # Set widget properties + widget.setGeometry(img_pix.rect()) + widget.setMask(img_pix.mask()) + widget.setWindowFlags( + QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.FramelessWindowHint + ) + widget.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) + + self.center_widget(widget) + self._working_widget = widget + self.helper = DragAndDropHelper(self._working_widget) + + def center_widget(self, widget): + frame_geo = widget.frameGeometry() + screen = self.app.desktop().cursor().pos() + center_point = self.app.desktop().screenGeometry( + self.app.desktop().screenNumber(screen) + ).center() + frame_geo.moveCenter(center_point) + widget.move(frame_geo.topLeft()) + + def show_working(self): + self._working_widget.show() + + def hide_working(self): + self.center_widget(self._working_widget) + self._working_widget.hide() + + +class DragAndDropHelper: + """ Helper adds to widget drag and drop ability + + :param widget: Qt Widget where drag and drop ability will be added + """ + def __init__(self, widget): + self.widget = widget + self.widget.mousePressEvent = self.mousePressEvent + self.widget.mouseMoveEvent = self.mouseMoveEvent + self.widget.mouseReleaseEvent = self.mouseReleaseEvent + + def mousePressEvent(self, event): + self.__mousePressPos = None + self.__mouseMovePos = None + if event.button() == QtCore.Qt.LeftButton: + self.__mousePressPos = event.globalPos() + self.__mouseMovePos = event.globalPos() + + def mouseMoveEvent(self, event): + if event.buttons() == QtCore.Qt.LeftButton: + # adjust offset from clicked point to origin of widget + currPos = self.widget.mapToGlobal( + self.widget.pos() + ) + globalPos = event.globalPos() + diff = globalPos - self.__mouseMovePos + newPos = self.widget.mapFromGlobal(currPos + diff) + self.widget.move(newPos) + self.__mouseMovePos = globalPos + + def mouseReleaseEvent(self, event): + if self.__mousePressPos is not None: + moved = event.globalPos() - self.__mousePressPos + if moved.manhattanLength() > 3: + event.ignore() + return + + +class PypeTrayApplication(QtWidgets.QApplication): + """Qt application manages application's control flow.""" + def __init__(self): + super(self.__class__, self).__init__(sys.argv) + # Allows to close widgets without exiting app + self.setQuitOnLastWindowClosed(False) + # Sets up splash + splash_widget = self.set_splash() + + splash_widget.show() + self.processEvents() + self.main_window = TrayMainWindow(self) + splash_widget.hide() + + def set_splash(self): + if os.getenv("PYPE_DEV"): + splash_file_name = "splash_dev.png" + else: + splash_file_name = "splash.png" + splash_pix = QtGui.QPixmap(get_resource(splash_file_name)) + splash = QtWidgets.QSplashScreen(splash_pix) + splash.setMask(splash_pix.mask()) + splash.setEnabled(False) + splash.setWindowFlags( + QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.FramelessWindowHint + ) + return splash diff --git a/res/app_icons/photoshop.png b/res/app_icons/photoshop.png new file mode 100644 index 0000000000..c7e9d14711 Binary files /dev/null and b/res/app_icons/photoshop.png differ diff --git a/res/app_icons/resolve.png b/res/app_icons/resolve.png new file mode 100644 index 0000000000..d084288d90 Binary files /dev/null and b/res/app_icons/resolve.png differ diff --git a/res/app_icons/storyboardpro.png b/res/app_icons/storyboardpro.png new file mode 100644 index 0000000000..ac9526f163 Binary files /dev/null and b/res/app_icons/storyboardpro.png differ