diff --git a/CHANGELOG.md b/CHANGELOG.md index 95792f8a7a..68409c4db8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,36 @@ # Changelog +## [3.6.0-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.5.0...HEAD) + +**πŸš€ Enhancements** + +- Tools: Experimental tools [\#2167](https://github.com/pypeclub/OpenPype/pull/2167) +- Loader: Refactor and use OpenPype stylesheets [\#2166](https://github.com/pypeclub/OpenPype/pull/2166) +- Add loader for linked smart objects in photoshop [\#2149](https://github.com/pypeclub/OpenPype/pull/2149) +- Burnins: DNxHD profiles handling [\#2142](https://github.com/pypeclub/OpenPype/pull/2142) +- Tools: Single access point for host tools [\#2139](https://github.com/pypeclub/OpenPype/pull/2139) + +**πŸ› Bug fixes** + +- MacOS: Launching of applications may cause Permissions error [\#2175](https://github.com/pypeclub/OpenPype/pull/2175) +- Blender: Fix 'Deselect All' with object not in 'Object Mode' [\#2163](https://github.com/pypeclub/OpenPype/pull/2163) +- Tools: Stylesheets are applied after tool show [\#2161](https://github.com/pypeclub/OpenPype/pull/2161) +- Maya: Collect render - fix UNC path support πŸ› [\#2158](https://github.com/pypeclub/OpenPype/pull/2158) +- Maya: Fix hotbox broken by scriptsmenu [\#2151](https://github.com/pypeclub/OpenPype/pull/2151) +- Ftrack: Ignore save warnings exception in Prepare project action [\#2150](https://github.com/pypeclub/OpenPype/pull/2150) +- Loader thumbnails with smooth edges [\#2147](https://github.com/pypeclub/OpenPype/pull/2147) +- Added validator for source files for Standalone Publisher [\#2138](https://github.com/pypeclub/OpenPype/pull/2138) + +**Merged pull requests:** + +- Bump pillow from 8.2.0 to 8.3.2 [\#2162](https://github.com/pypeclub/OpenPype/pull/2162) +- Bump axios from 0.21.1 to 0.21.4 in /website [\#2059](https://github.com/pypeclub/OpenPype/pull/2059) + ## [3.5.0](https://github.com/pypeclub/OpenPype/tree/3.5.0) (2021-10-17) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.4.1...3.5.0) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.5.0-nightly.8...3.5.0) **Deprecated:** @@ -66,10 +94,6 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.4.1-nightly.1...3.4.1) -**πŸ†• New features** - -- Settings: Flag project as deactivated and hide from tools' view [\#2008](https://github.com/pypeclub/OpenPype/pull/2008) - **πŸš€ Enhancements** - General: Startup validations [\#2054](https://github.com/pypeclub/OpenPype/pull/2054) @@ -82,8 +106,6 @@ - Loader & Library loader: Use tools from OpenPype [\#2038](https://github.com/pypeclub/OpenPype/pull/2038) - Adding predefined project folders creation in PM [\#2030](https://github.com/pypeclub/OpenPype/pull/2030) - WebserverModule: Removed interface of webserver module [\#2028](https://github.com/pypeclub/OpenPype/pull/2028) -- TimersManager: Removed interface of timers manager [\#2024](https://github.com/pypeclub/OpenPype/pull/2024) -- Feature Maya import asset from scene inventory [\#2018](https://github.com/pypeclub/OpenPype/pull/2018) **πŸ› Bug fixes** @@ -101,21 +123,10 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.4.0-nightly.6...3.4.0) -**πŸ†• New features** - -- Nuke: Compatibility with Nuke 13 [\#2003](https://github.com/pypeclub/OpenPype/pull/2003) - **πŸš€ Enhancements** - Added possibility to configure of synchronization of workfile version… [\#2041](https://github.com/pypeclub/OpenPype/pull/2041) - General: Task types in profiles [\#2036](https://github.com/pypeclub/OpenPype/pull/2036) -- Console interpreter: Handle invalid sizes on initialization [\#2022](https://github.com/pypeclub/OpenPype/pull/2022) -- Ftrack: Show OpenPype versions in event server status [\#2019](https://github.com/pypeclub/OpenPype/pull/2019) -- General: Staging icon [\#2017](https://github.com/pypeclub/OpenPype/pull/2017) -- Ftrack: Sync to avalon actions have jobs [\#2015](https://github.com/pypeclub/OpenPype/pull/2015) -- Modules: Connect method is not required [\#2009](https://github.com/pypeclub/OpenPype/pull/2009) -- Settings UI: Number with configurable steps [\#2001](https://github.com/pypeclub/OpenPype/pull/2001) -- Moving project folder structure creation out of ftrack module \#1989 [\#1996](https://github.com/pypeclub/OpenPype/pull/1996) **πŸ› Bug fixes** @@ -124,12 +135,6 @@ - Nuke: typo on a button [\#2034](https://github.com/pypeclub/OpenPype/pull/2034) - Hiero: Fix "none" named tags [\#2033](https://github.com/pypeclub/OpenPype/pull/2033) - FFmpeg: Subprocess arguments as list [\#2032](https://github.com/pypeclub/OpenPype/pull/2032) -- General: Fix Python 2 breaking line [\#2016](https://github.com/pypeclub/OpenPype/pull/2016) -- Bugfix/webpublisher task type [\#2006](https://github.com/pypeclub/OpenPype/pull/2006) - -### πŸ“– Documentation - -- Documentation: Ftrack launch argsuments update [\#2014](https://github.com/pypeclub/OpenPype/pull/2014) ## [3.3.1](https://github.com/pypeclub/OpenPype/tree/3.3.1) (2021-08-20) diff --git a/openpype/cli.py b/openpype/cli.py index d6d9cbacd4..58630bdefb 100644 --- a/openpype/cli.py +++ b/openpype/cli.py @@ -158,6 +158,25 @@ def publish(debug, paths, targets): PypeCommands.publish(list(paths), targets) +@main.command() +@click.argument("path") +@click.option("-d", "--debug", is_flag=True, help="Print debug messages") +@click.option("-h", "--host", help="Host") +@click.option("-u", "--user", help="User email address") +@click.option("-p", "--project", help="Project") +@click.option("-t", "--targets", help="Targets", default=None, + multiple=True) +def remotepublishfromapp(debug, project, path, host, targets=None, user=None): + """Start CLI publishing. + + Publish collects json from paths provided as an argument. + More than one path is allowed. + """ + if debug: + os.environ['OPENPYPE_DEBUG'] = '3' + PypeCommands.remotepublishfromapp(project, path, host, user, + targets=targets) + @main.command() @click.argument("path") @click.option("-d", "--debug", is_flag=True, help="Print debug messages") diff --git a/openpype/hooks/pre_foundry_apps.py b/openpype/hooks/pre_foundry_apps.py index 85f68c6b60..7df1a6a833 100644 --- a/openpype/hooks/pre_foundry_apps.py +++ b/openpype/hooks/pre_foundry_apps.py @@ -13,7 +13,7 @@ class LaunchFoundryAppsWindows(PreLaunchHook): # Should be as last hook because must change launch arguments to string order = 1000 - app_groups = ["nuke", "nukex", "hiero", "nukestudio"] + app_groups = ["nuke", "nukex", "hiero", "nukestudio", "photoshop"] platforms = ["windows"] def execute(self): diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 50b73ade2b..6d437059b8 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -95,6 +95,30 @@ def get_local_collection_with_name(name): return None +def deselect_all(): + """Deselect all objects in the scene. + + Blender gives context error if trying to deselect object that it isn't + in object mode. + """ + modes = [] + active = bpy.context.view_layer.objects.active + + for obj in bpy.data.objects: + if obj.mode != 'OBJECT': + modes.append((obj, obj.mode)) + bpy.context.view_layer.objects.active = obj + bpy.ops.object.mode_set(mode='OBJECT') + + bpy.ops.object.select_all(action='DESELECT') + + for p in modes: + bpy.context.view_layer.objects.active = p[0] + bpy.ops.object.mode_set(mode=p[1]) + + bpy.context.view_layer.objects.active = active + + class Creator(PypeCreatorMixin, blender.Creator): pass diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index c7fea30787..98ccca313c 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -3,11 +3,12 @@ import bpy from avalon import api -from avalon.blender import lib -import openpype.hosts.blender.api.plugin +from avalon.blender import lib, ops +from avalon.blender.pipeline import AVALON_INSTANCES +from openpype.hosts.blender.api import plugin -class CreateCamera(openpype.hosts.blender.api.plugin.Creator): +class CreateCamera(plugin.Creator): """Polygonal static geometry""" name = "cameraMain" @@ -16,17 +17,46 @@ class CreateCamera(openpype.hosts.blender.api.plugin.Creator): icon = "video-camera" def process(self): + """ Run the creator on Blender main thread""" + mti = ops.MainThreadItem(self._process) + ops.execute_in_main_thread(mti) + def _process(self): + # Get Instance Containter or create it if it does not exist + instances = bpy.data.collections.get(AVALON_INSTANCES) + if not instances: + instances = bpy.data.collections.new(name=AVALON_INSTANCES) + bpy.context.scene.collection.children.link(instances) + + # Create instance object asset = self.data["asset"] subset = self.data["subset"] - name = openpype.hosts.blender.api.plugin.asset_name(asset, subset) - collection = bpy.data.collections.new(name=name) - bpy.context.scene.collection.children.link(collection) + name = plugin.asset_name(asset, subset) + + camera = bpy.data.cameras.new(subset) + camera_obj = bpy.data.objects.new(subset, camera) + + instances.objects.link(camera_obj) + + asset_group = bpy.data.objects.new(name=name, object_data=None) + asset_group.empty_display_type = 'SINGLE_ARROW' + instances.objects.link(asset_group) self.data['task'] = api.Session.get('AVALON_TASK') - lib.imprint(collection, self.data) + print(f"self.data: {self.data}") + lib.imprint(asset_group, self.data) if (self.options or {}).get("useSelection"): - for obj in lib.get_selection(): - collection.objects.link(obj) + bpy.context.view_layer.objects.active = asset_group + selected = lib.get_selection() + for obj in selected: + obj.select_set(True) + selected.append(asset_group) + bpy.ops.object.parent_set(keep_transform=True) + else: + plugin.deselect_all() + camera_obj.select_set(True) + asset_group.select_set(True) + bpy.context.view_layer.objects.active = asset_group + bpy.ops.object.parent_set(keep_transform=True) - return collection + return asset_group diff --git a/openpype/hosts/blender/plugins/load/load_abc.py b/openpype/hosts/blender/plugins/load/load_abc.py index 92656fac9e..5969432c36 100644 --- a/openpype/hosts/blender/plugins/load/load_abc.py +++ b/openpype/hosts/blender/plugins/load/load_abc.py @@ -47,7 +47,7 @@ class CacheModelLoader(plugin.AssetLoader): bpy.data.objects.remove(empty) def _process(self, libpath, asset_group, group_name): - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() collection = bpy.context.view_layer.active_layer_collection.collection @@ -109,7 +109,7 @@ class CacheModelLoader(plugin.AssetLoader): avalon_info = obj[AVALON_PROPERTY] avalon_info.update({"container_name": group_name}) - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() return objects diff --git a/openpype/hosts/blender/plugins/load/load_camera.py b/openpype/hosts/blender/plugins/load/load_camera.py deleted file mode 100644 index 30300100e0..0000000000 --- a/openpype/hosts/blender/plugins/load/load_camera.py +++ /dev/null @@ -1,247 +0,0 @@ -"""Load a camera asset 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 openpype.hosts.blender.api.plugin - -logger = logging.getLogger("openpype").getChild("blender").getChild("load_camera") - - -class BlendCameraLoader(openpype.hosts.blender.api.plugin.AssetLoader): - """Load a camera from a .blend file. - - Warning: - Loading the same asset more then once is not properly supported at the - moment. - """ - - families = ["camera"] - representations = ["blend"] - - label = "Link Camera" - icon = "code-fork" - color = "orange" - - def _remove(self, objects, lib_container): - for obj in list(objects): - bpy.data.cameras.remove(obj.data) - - 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]) - - camera_container = scene.collection.children[lib_container].make_local() - - objects_list = [] - - for obj in camera_container.objects: - local_obj = obj.make_local() - local_obj.data.make_local() - - if not local_obj.get(blender.pipeline.AVALON_PROPERTY): - local_obj[blender.pipeline.AVALON_PROPERTY] = dict() - - avalon_info = local_obj[blender.pipeline.AVALON_PROPERTY] - avalon_info.update({"container_name": container_name}) - - if actions[0] is not None: - if local_obj.animation_data is None: - local_obj.animation_data_create() - local_obj.animation_data.action = actions[0] - - if actions[1] is not None: - if local_obj.data.animation_data is None: - local_obj.data.animation_data_create() - local_obj.data.animation_data.action = actions[1] - - objects_list.append(local_obj) - - camera_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 = openpype.hosts.blender.api.plugin.asset_name(asset, subset) - container_name = openpype.hosts.blender.api.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, (None, None)) - - # 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 openpype.hosts.blender.api.plugin.VALID_EXTENSIONS, ( - f"Unsupported file: {libpath}" - ) - - collection_metadata = collection.get( - blender.pipeline.AVALON_PROPERTY) - collection_libpath = collection_metadata["libpath"] - objects = collection_metadata["objects"] - lib_container = collection_metadata["lib_container"] - - 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 - - camera = objects[0] - - camera_action = None - camera_data_action = None - - if camera.animation_data and camera.animation_data.action: - camera_action = camera.animation_data.action - - if camera.data.animation_data and camera.data.animation_data.action: - camera_data_action = camera.data.animation_data.action - - actions = (camera_action, camera_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 (openpype: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/openpype/hosts/blender/plugins/load/load_camera_blend.py b/openpype/hosts/blender/plugins/load/load_camera_blend.py new file mode 100644 index 0000000000..834eb467d8 --- /dev/null +++ b/openpype/hosts/blender/plugins/load/load_camera_blend.py @@ -0,0 +1,252 @@ +"""Load a camera asset in Blender.""" + +import logging +from pathlib import Path +from pprint import pformat +from typing import Dict, List, Optional + +import bpy + +from avalon import api +from avalon.blender.pipeline import AVALON_CONTAINERS +from avalon.blender.pipeline import AVALON_CONTAINER_ID +from avalon.blender.pipeline import AVALON_PROPERTY +from openpype.hosts.blender.api import plugin + +logger = logging.getLogger("openpype").getChild( + "blender").getChild("load_camera") + + +class BlendCameraLoader(plugin.AssetLoader): + """Load a camera from a .blend file. + + Warning: + Loading the same asset more then once is not properly supported at the + moment. + """ + + families = ["camera"] + representations = ["blend"] + + label = "Link Camera (Blend)" + icon = "code-fork" + color = "orange" + + def _remove(self, asset_group): + objects = list(asset_group.children) + + for obj in objects: + if obj.type == 'CAMERA': + bpy.data.cameras.remove(obj.data) + + def _process(self, libpath, asset_group, group_name): + with bpy.data.libraries.load( + libpath, link=True, relative=False + ) as (data_from, data_to): + data_to.objects = data_from.objects + + parent = bpy.context.scene.collection + + empties = [obj for obj in data_to.objects if obj.type == 'EMPTY'] + + container = None + + for empty in empties: + if empty.get(AVALON_PROPERTY): + container = empty + break + + assert container, "No asset group found" + + # Children must be linked before parents, + # otherwise the hierarchy will break + objects = [] + nodes = list(container.children) + + for obj in nodes: + obj.parent = asset_group + + for obj in nodes: + objects.append(obj) + nodes.extend(list(obj.children)) + + objects.reverse() + + for obj in objects: + parent.objects.link(obj) + + for obj in objects: + local_obj = plugin.prepare_data(obj, group_name) + + if local_obj.type != 'EMPTY': + plugin.prepare_data(local_obj.data, group_name) + + if not local_obj.get(AVALON_PROPERTY): + local_obj[AVALON_PROPERTY] = dict() + + avalon_info = local_obj[AVALON_PROPERTY] + avalon_info.update({"container_name": group_name}) + + objects.reverse() + + bpy.data.orphans_purge(do_local_ids=False) + + plugin.deselect_all() + + return objects + + 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"] + + asset_name = plugin.asset_name(asset, subset) + unique_number = plugin.get_unique_number(asset, subset) + group_name = plugin.asset_name(asset, subset, unique_number) + namespace = namespace or f"{asset}_{unique_number}" + + avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) + if not avalon_container: + avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS) + bpy.context.scene.collection.children.link(avalon_container) + + asset_group = bpy.data.objects.new(group_name, object_data=None) + asset_group.empty_display_type = 'SINGLE_ARROW' + avalon_container.objects.link(asset_group) + + objects = self._process(libpath, asset_group, group_name) + + bpy.context.scene.collection.objects.link(asset_group) + + asset_group[AVALON_PROPERTY] = { + "schema": "openpype:container-2.0", + "id": AVALON_CONTAINER_ID, + "name": name, + "namespace": namespace or '', + "loader": str(self.__class__.__name__), + "representation": str(context["representation"]["_id"]), + "libpath": libpath, + "asset_name": asset_name, + "parent": str(context["representation"]["parent"]), + "family": context["representation"]["context"]["family"], + "objectName": group_name + } + + self[:] = objects + return objects + + def exec_update(self, container: Dict, representation: Dict): + """Update the loaded asset. + + This will remove all children of the asset group, load the new ones + and add them as children of the group. + """ + object_name = container["objectName"] + asset_group = bpy.data.objects.get(object_name) + libpath = Path(api.get_representation_path(representation)) + extension = libpath.suffix.lower() + + self.log.info( + "Container: %s\nRepresentation: %s", + pformat(container, indent=2), + pformat(representation, indent=2), + ) + + assert asset_group, ( + f"The asset is not loaded: {container['objectName']}" + ) + assert libpath, ( + "No existing library file found for {container['objectName']}" + ) + assert libpath.is_file(), ( + f"The file doesn't exist: {libpath}" + ) + assert extension in plugin.VALID_EXTENSIONS, ( + f"Unsupported file: {libpath}" + ) + + metadata = asset_group.get(AVALON_PROPERTY) + group_libpath = metadata["libpath"] + + normalized_group_libpath = ( + str(Path(bpy.path.abspath(group_libpath)).resolve()) + ) + normalized_libpath = ( + str(Path(bpy.path.abspath(str(libpath))).resolve()) + ) + self.log.debug( + "normalized_group_libpath:\n %s\nnormalized_libpath:\n %s", + normalized_group_libpath, + normalized_libpath, + ) + if normalized_group_libpath == normalized_libpath: + self.log.info("Library already loaded, not updating...") + return + + # Check how many assets use the same library + count = 0 + for obj in bpy.data.collections.get(AVALON_CONTAINERS).objects: + if obj.get(AVALON_PROPERTY).get('libpath') == group_libpath: + count += 1 + + mat = asset_group.matrix_basis.copy() + + self._remove(asset_group) + + # If it is the last object to use that library, remove it + if count == 1: + library = bpy.data.libraries.get(bpy.path.basename(group_libpath)) + if library: + bpy.data.libraries.remove(library) + + self._process(str(libpath), asset_group, object_name) + + asset_group.matrix_basis = mat + + metadata["libpath"] = str(libpath) + metadata["representation"] = str(representation["_id"]) + metadata["parent"] = str(representation["parent"]) + + def exec_remove(self, container: Dict) -> bool: + """Remove an existing container from a Blender scene. + + Arguments: + container (openpype:container-1.0): Container to remove, + from `host.ls()`. + + Returns: + bool: Whether the container was deleted. + """ + object_name = container["objectName"] + asset_group = bpy.data.objects.get(object_name) + libpath = asset_group.get(AVALON_PROPERTY).get('libpath') + + # Check how many assets use the same library + count = 0 + for obj in bpy.data.collections.get(AVALON_CONTAINERS).objects: + if obj.get(AVALON_PROPERTY).get('libpath') == libpath: + count += 1 + + if not asset_group: + return False + + self._remove(asset_group) + + bpy.data.objects.remove(asset_group) + + # If it is the last object to use that library, remove it + if count == 1: + library = bpy.data.libraries.get(bpy.path.basename(libpath)) + bpy.data.libraries.remove(library) + + return True diff --git a/openpype/hosts/blender/plugins/load/load_camera_fbx.py b/openpype/hosts/blender/plugins/load/load_camera_fbx.py new file mode 100644 index 0000000000..5edba7ec0c --- /dev/null +++ b/openpype/hosts/blender/plugins/load/load_camera_fbx.py @@ -0,0 +1,218 @@ +"""Load an asset in Blender from an Alembic file.""" + +from pathlib import Path +from pprint import pformat +from typing import Dict, List, Optional + +import bpy + +from avalon import api +from avalon.blender import lib +from avalon.blender.pipeline import AVALON_CONTAINERS +from avalon.blender.pipeline import AVALON_CONTAINER_ID +from avalon.blender.pipeline import AVALON_PROPERTY +from openpype.hosts.blender.api import plugin + + +class FbxCameraLoader(plugin.AssetLoader): + """Load a camera from FBX. + + Stores the imported asset in an empty named after the asset. + """ + + families = ["camera"] + representations = ["fbx"] + + label = "Load Camera (FBX)" + icon = "code-fork" + color = "orange" + + def _remove(self, asset_group): + objects = list(asset_group.children) + + for obj in objects: + if obj.type == 'CAMERA': + bpy.data.cameras.remove(obj.data) + elif obj.type == 'EMPTY': + objects.extend(obj.children) + bpy.data.objects.remove(obj) + + def _process(self, libpath, asset_group, group_name): + plugin.deselect_all() + + collection = bpy.context.view_layer.active_layer_collection.collection + + bpy.ops.import_scene.fbx(filepath=libpath) + + parent = bpy.context.scene.collection + + objects = lib.get_selection() + + for obj in objects: + obj.parent = asset_group + + for obj in objects: + parent.objects.link(obj) + collection.objects.unlink(obj) + + for obj in objects: + name = obj.name + obj.name = f"{group_name}:{name}" + if obj.type != 'EMPTY': + name_data = obj.data.name + obj.data.name = f"{group_name}:{name_data}" + + if not obj.get(AVALON_PROPERTY): + obj[AVALON_PROPERTY] = dict() + + avalon_info = obj[AVALON_PROPERTY] + avalon_info.update({"container_name": group_name}) + + plugin.deselect_all() + + return objects + + 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"] + + asset_name = plugin.asset_name(asset, subset) + unique_number = plugin.get_unique_number(asset, subset) + group_name = plugin.asset_name(asset, subset, unique_number) + namespace = namespace or f"{asset}_{unique_number}" + + avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) + if not avalon_container: + avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS) + bpy.context.scene.collection.children.link(avalon_container) + + asset_group = bpy.data.objects.new(group_name, object_data=None) + avalon_container.objects.link(asset_group) + + objects = self._process(libpath, asset_group, group_name) + + objects = [] + nodes = list(asset_group.children) + + for obj in nodes: + objects.append(obj) + nodes.extend(list(obj.children)) + + bpy.context.scene.collection.objects.link(asset_group) + + asset_group[AVALON_PROPERTY] = { + "schema": "openpype:container-2.0", + "id": AVALON_CONTAINER_ID, + "name": name, + "namespace": namespace or '', + "loader": str(self.__class__.__name__), + "representation": str(context["representation"]["_id"]), + "libpath": libpath, + "asset_name": asset_name, + "parent": str(context["representation"]["parent"]), + "family": context["representation"]["context"]["family"], + "objectName": group_name + } + + self[:] = objects + return objects + + def exec_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! + """ + object_name = container["objectName"] + asset_group = bpy.data.objects.get(object_name) + libpath = Path(api.get_representation_path(representation)) + extension = libpath.suffix.lower() + + self.log.info( + "Container: %s\nRepresentation: %s", + pformat(container, indent=2), + pformat(representation, indent=2), + ) + + assert asset_group, ( + f"The asset is not loaded: {container['objectName']}" + ) + assert libpath, ( + "No existing library file found for {container['objectName']}" + ) + assert libpath.is_file(), ( + f"The file doesn't exist: {libpath}" + ) + assert extension in plugin.VALID_EXTENSIONS, ( + f"Unsupported file: {libpath}" + ) + + metadata = asset_group.get(AVALON_PROPERTY) + group_libpath = metadata["libpath"] + + normalized_group_libpath = ( + str(Path(bpy.path.abspath(group_libpath)).resolve()) + ) + normalized_libpath = ( + str(Path(bpy.path.abspath(str(libpath))).resolve()) + ) + self.log.debug( + "normalized_group_libpath:\n %s\nnormalized_libpath:\n %s", + normalized_group_libpath, + normalized_libpath, + ) + if normalized_group_libpath == normalized_libpath: + self.log.info("Library already loaded, not updating...") + return + + mat = asset_group.matrix_basis.copy() + + self._remove(asset_group) + self._process(str(libpath), asset_group, object_name) + + asset_group.matrix_basis = mat + + metadata["libpath"] = str(libpath) + metadata["representation"] = str(representation["_id"]) + + def exec_remove(self, container: Dict) -> bool: + """Remove an existing container from a Blender scene. + + Arguments: + container (openpype: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! + """ + object_name = container["objectName"] + asset_group = bpy.data.objects.get(object_name) + + if not asset_group: + return False + + self._remove(asset_group) + + bpy.data.objects.remove(asset_group) + + return True diff --git a/openpype/hosts/blender/plugins/load/load_fbx.py b/openpype/hosts/blender/plugins/load/load_fbx.py index b80dc69adc..5f69aecb1a 100644 --- a/openpype/hosts/blender/plugins/load/load_fbx.py +++ b/openpype/hosts/blender/plugins/load/load_fbx.py @@ -46,7 +46,7 @@ class FbxModelLoader(plugin.AssetLoader): bpy.data.objects.remove(obj) def _process(self, libpath, asset_group, group_name, action): - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() collection = bpy.context.view_layer.active_layer_collection.collection @@ -112,7 +112,7 @@ class FbxModelLoader(plugin.AssetLoader): avalon_info = obj[AVALON_PROPERTY] avalon_info.update({"container_name": group_name}) - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() return objects diff --git a/openpype/hosts/blender/plugins/load/load_layout_blend.py b/openpype/hosts/blender/plugins/load/load_layout_blend.py index 85cb4dfbd3..4c1f751a77 100644 --- a/openpype/hosts/blender/plugins/load/load_layout_blend.py +++ b/openpype/hosts/blender/plugins/load/load_layout_blend.py @@ -150,7 +150,7 @@ class BlendLayoutLoader(plugin.AssetLoader): bpy.data.orphans_purge(do_local_ids=False) - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() return objects diff --git a/openpype/hosts/blender/plugins/load/load_layout_json.py b/openpype/hosts/blender/plugins/load/load_layout_json.py index 1a4dbbb5cb..442cf05d85 100644 --- a/openpype/hosts/blender/plugins/load/load_layout_json.py +++ b/openpype/hosts/blender/plugins/load/load_layout_json.py @@ -12,6 +12,7 @@ from avalon.blender.pipeline import AVALON_CONTAINERS from avalon.blender.pipeline import AVALON_CONTAINER_ID from avalon.blender.pipeline import AVALON_PROPERTY from avalon.blender.pipeline import AVALON_INSTANCES +from openpype import lib from openpype.hosts.blender.api import plugin @@ -59,7 +60,7 @@ class JsonLayoutLoader(plugin.AssetLoader): return None def _process(self, libpath, asset, asset_group, actions): - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() with open(libpath, "r") as fp: data = json.load(fp) @@ -103,6 +104,21 @@ class JsonLayoutLoader(plugin.AssetLoader): options=options ) + # Create the camera asset and the camera instance + creator_plugin = lib.get_creator_by_name("CreateCamera") + if not creator_plugin: + raise ValueError("Creator plugin \"CreateCamera\" was " + "not found.") + + api.create( + creator_plugin, + name="camera", + # name=f"{unique_number}_{subset}_animation", + asset=asset, + options={"useSelection": False} + # data={"dependencies": str(context["representation"]["_id"])} + ) + def process_asset(self, context: dict, name: str, diff --git a/openpype/hosts/blender/plugins/load/load_model.py b/openpype/hosts/blender/plugins/load/load_model.py index af5591c299..c33c656dec 100644 --- a/openpype/hosts/blender/plugins/load/load_model.py +++ b/openpype/hosts/blender/plugins/load/load_model.py @@ -93,7 +93,7 @@ class BlendModelLoader(plugin.AssetLoader): bpy.data.orphans_purge(do_local_ids=False) - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() return objects @@ -126,7 +126,7 @@ class BlendModelLoader(plugin.AssetLoader): asset_group.empty_display_type = 'SINGLE_ARROW' avalon_container.objects.link(asset_group) - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() if options is not None: parent = options.get('parent') @@ -158,7 +158,7 @@ class BlendModelLoader(plugin.AssetLoader): bpy.ops.object.parent_set(keep_transform=True) - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() objects = self._process(libpath, asset_group, group_name) diff --git a/openpype/hosts/blender/plugins/load/load_rig.py b/openpype/hosts/blender/plugins/load/load_rig.py index 6062c293df..e80da8af45 100644 --- a/openpype/hosts/blender/plugins/load/load_rig.py +++ b/openpype/hosts/blender/plugins/load/load_rig.py @@ -156,7 +156,7 @@ class BlendRigLoader(plugin.AssetLoader): while bpy.data.orphans_purge(do_local_ids=False): pass - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() return objects @@ -191,7 +191,7 @@ class BlendRigLoader(plugin.AssetLoader): action = None - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() create_animation = False @@ -227,7 +227,7 @@ class BlendRigLoader(plugin.AssetLoader): bpy.ops.object.parent_set(keep_transform=True) - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() objects = self._process(libpath, asset_group, group_name, action) @@ -250,7 +250,7 @@ class BlendRigLoader(plugin.AssetLoader): data={"dependencies": str(context["representation"]["_id"])} ) - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() bpy.context.scene.collection.objects.link(asset_group) diff --git a/openpype/hosts/blender/plugins/publish/extract_abc.py b/openpype/hosts/blender/plugins/publish/extract_abc.py index 4696da3db4..b75bec4e28 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc.py @@ -28,7 +28,7 @@ class ExtractABC(api.Extractor): # Perform extraction self.log.info("Performing extraction..") - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() selected = [] asset_group = None @@ -50,7 +50,7 @@ class ExtractABC(api.Extractor): flatten=False ) - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() if "representations" not in instance.data: instance.data["representations"] = [] diff --git a/openpype/hosts/blender/plugins/publish/extract_camera.py b/openpype/hosts/blender/plugins/publish/extract_camera.py new file mode 100644 index 0000000000..a0e78178c8 --- /dev/null +++ b/openpype/hosts/blender/plugins/publish/extract_camera.py @@ -0,0 +1,73 @@ +import os + +from openpype import api +from openpype.hosts.blender.api import plugin + +import bpy + + +class ExtractCamera(api.Extractor): + """Extract as the camera as FBX.""" + + label = "Extract Camera" + hosts = ["blender"] + families = ["camera"] + optional = True + + def process(self, instance): + # Define extract output file path + stagingdir = self.staging_dir(instance) + filename = f"{instance.name}.fbx" + filepath = os.path.join(stagingdir, filename) + + # Perform extraction + self.log.info("Performing extraction..") + + plugin.deselect_all() + + selected = [] + + camera = None + + for obj in instance: + if obj.type == "CAMERA": + obj.select_set(True) + selected.append(obj) + camera = obj + break + + assert camera, "No camera found" + + context = plugin.create_blender_context( + active=camera, selected=selected) + + scale_length = bpy.context.scene.unit_settings.scale_length + bpy.context.scene.unit_settings.scale_length = 0.01 + + # We export the fbx + bpy.ops.export_scene.fbx( + context, + filepath=filepath, + use_active_collection=False, + use_selection=True, + object_types={'CAMERA'}, + bake_anim_simplify_factor=0.0 + ) + + bpy.context.scene.unit_settings.scale_length = scale_length + + plugin.deselect_all() + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'fbx', + 'ext': 'fbx', + 'files': filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + + self.log.info("Extracted instance '%s' to: %s", + instance.name, representation) diff --git a/openpype/hosts/blender/plugins/publish/extract_fbx.py b/openpype/hosts/blender/plugins/publish/extract_fbx.py index b91f2a75ef..f9ffdea1d1 100644 --- a/openpype/hosts/blender/plugins/publish/extract_fbx.py +++ b/openpype/hosts/blender/plugins/publish/extract_fbx.py @@ -24,7 +24,7 @@ class ExtractFBX(api.Extractor): # Perform extraction self.log.info("Performing extraction..") - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() selected = [] asset_group = None @@ -60,7 +60,7 @@ class ExtractFBX(api.Extractor): add_leaf_bones=False ) - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() for mat in new_materials: bpy.data.materials.remove(mat) diff --git a/openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py b/openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py new file mode 100644 index 0000000000..39b9b67511 --- /dev/null +++ b/openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py @@ -0,0 +1,48 @@ +from typing import List + +import mathutils + +import pyblish.api +import openpype.hosts.blender.api.action + + +class ValidateCameraZeroKeyframe(pyblish.api.InstancePlugin): + """Camera must have a keyframe at frame 0. + + Unreal shifts the first keyframe to frame 0. Forcing the camera to have + a keyframe at frame 0 will ensure that the animation will be the same + in Unreal and Blender. + """ + + order = openpype.api.ValidateContentsOrder + hosts = ["blender"] + families = ["camera"] + category = "geometry" + version = (0, 1, 0) + label = "Zero Keyframe" + actions = [openpype.hosts.blender.api.action.SelectInvalidAction] + + _identity = mathutils.Matrix() + + @classmethod + def get_invalid(cls, instance) -> List: + invalid = [] + for obj in [obj for obj in instance]: + if obj.type == "CAMERA": + if obj.animation_data and obj.animation_data.action: + action = obj.animation_data.action + frames_set = set() + for fcu in action.fcurves: + for kp in fcu.keyframe_points: + frames_set.add(kp.co[0]) + frames = list(frames_set) + frames.sort() + if frames[0] != 0.0: + invalid.append(obj) + return invalid + + def process(self, instance): + invalid = self.get_invalid(instance) + if invalid: + raise RuntimeError( + f"Object found in instance is not in Object Mode: {invalid}") diff --git a/openpype/hosts/flame/__init__.py b/openpype/hosts/flame/__init__.py new file mode 100644 index 0000000000..48e8dc86c9 --- /dev/null +++ b/openpype/hosts/flame/__init__.py @@ -0,0 +1,105 @@ +from .api.utils import ( + setup +) + +from .api.pipeline import ( + install, + uninstall, + ls, + containerise, + update_container, + maintained_selection, + remove_instance, + list_instances, + imprint +) + +from .api.lib import ( + FlameAppFramework, + maintain_current_timeline, + get_project_manager, + get_current_project, + get_current_timeline, + create_bin, +) + +from .api.menu import ( + FlameMenuProjectConnect, + FlameMenuTimeline +) + +from .api.workio import ( + open_file, + save_file, + current_file, + has_unsaved_changes, + file_extensions, + work_root +) + +import os + +HOST_DIR = os.path.dirname( + os.path.abspath(__file__) +) +API_DIR = os.path.join(HOST_DIR, "api") +PLUGINS_DIR = os.path.join(HOST_DIR, "plugins") +PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") +LOAD_PATH = os.path.join(PLUGINS_DIR, "load") +CREATE_PATH = os.path.join(PLUGINS_DIR, "create") +INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") + +app_framework = None +apps = [] + + +__all__ = [ + "HOST_DIR", + "API_DIR", + "PLUGINS_DIR", + "PUBLISH_PATH", + "LOAD_PATH", + "CREATE_PATH", + "INVENTORY_PATH", + "INVENTORY_PATH", + + "app_framework", + "apps", + + # pipeline + "install", + "uninstall", + "ls", + "containerise", + "update_container", + "reload_pipeline", + "maintained_selection", + "remove_instance", + "list_instances", + "imprint", + + # utils + "setup", + + # lib + "FlameAppFramework", + "maintain_current_timeline", + "get_project_manager", + "get_current_project", + "get_current_timeline", + "create_bin", + + # menu + "FlameMenuProjectConnect", + "FlameMenuTimeline", + + # plugin + + # workio + "open_file", + "save_file", + "current_file", + "has_unsaved_changes", + "file_extensions", + "work_root" +] diff --git a/openpype/hosts/flame/api/__init__.py b/openpype/hosts/flame/api/__init__.py new file mode 100644 index 0000000000..50a6b3f098 --- /dev/null +++ b/openpype/hosts/flame/api/__init__.py @@ -0,0 +1,3 @@ +""" +OpenPype Autodesk Flame api +""" diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py new file mode 100644 index 0000000000..48331dcbc2 --- /dev/null +++ b/openpype/hosts/flame/api/lib.py @@ -0,0 +1,276 @@ +import sys +import os +import pickle +import contextlib +from pprint import pformat + +from openpype.api import Logger + +log = Logger().get_logger(__name__) + + +@contextlib.contextmanager +def io_preferences_file(klass, filepath, write=False): + try: + flag = "w" if write else "r" + yield open(filepath, flag) + + except IOError as _error: + klass.log.info("Unable to work with preferences `{}`: {}".format( + filepath, _error)) + + +class FlameAppFramework(object): + # flameAppFramework class takes care of preferences + + class prefs_dict(dict): + + def __init__(self, master, name, **kwargs): + self.name = name + self.master = master + if not self.master.get(self.name): + self.master[self.name] = {} + self.master[self.name].__init__() + + def __getitem__(self, k): + return self.master[self.name].__getitem__(k) + + def __setitem__(self, k, v): + return self.master[self.name].__setitem__(k, v) + + def __delitem__(self, k): + return self.master[self.name].__delitem__(k) + + def get(self, k, default=None): + return self.master[self.name].get(k, default) + + def setdefault(self, k, default=None): + return self.master[self.name].setdefault(k, default) + + def pop(self, k, v=object()): + if v is object(): + return self.master[self.name].pop(k) + return self.master[self.name].pop(k, v) + + def update(self, mapping=(), **kwargs): + self.master[self.name].update(mapping, **kwargs) + + def __contains__(self, k): + return self.master[self.name].__contains__(k) + + def copy(self): # don"t delegate w/ super - dict.copy() -> dict :( + return type(self)(self) + + def keys(self): + return self.master[self.name].keys() + + @classmethod + def fromkeys(cls, keys, v=None): + return cls.master[cls.name].fromkeys(keys, v) + + def __repr__(self): + return "{0}({1})".format( + type(self).__name__, self.master[self.name].__repr__()) + + def master_keys(self): + return self.master.keys() + + def __init__(self): + self.name = self.__class__.__name__ + self.bundle_name = "OpenPypeFlame" + # self.prefs scope is limited to flame project and user + self.prefs = {} + self.prefs_user = {} + self.prefs_global = {} + self.log = log + + try: + import flame + self.flame = flame + self.flame_project_name = self.flame.project.current_project.name + self.flame_user_name = flame.users.current_user.name + except Exception: + self.flame = None + self.flame_project_name = None + self.flame_user_name = None + + import socket + self.hostname = socket.gethostname() + + if sys.platform == "darwin": + self.prefs_folder = os.path.join( + os.path.expanduser("~"), + "Library", + "Caches", + "OpenPype", + self.bundle_name + ) + elif sys.platform.startswith("linux"): + self.prefs_folder = os.path.join( + os.path.expanduser("~"), + ".OpenPype", + self.bundle_name) + + self.prefs_folder = os.path.join( + self.prefs_folder, + self.hostname, + ) + + self.log.info("[{}] waking up".format(self.__class__.__name__)) + self.load_prefs() + + # menu auto-refresh defaults + + if not self.prefs_global.get("menu_auto_refresh"): + self.prefs_global["menu_auto_refresh"] = { + "media_panel": True, + "batch": True, + "main_menu": True, + "timeline_menu": True + } + + self.apps = [] + + def get_pref_file_paths(self): + + prefix = self.prefs_folder + os.path.sep + self.bundle_name + prefs_file_path = "_".join([ + prefix, self.flame_user_name, + self.flame_project_name]) + ".prefs" + prefs_user_file_path = "_".join([ + prefix, self.flame_user_name]) + ".prefs" + prefs_global_file_path = prefix + ".prefs" + + return (prefs_file_path, prefs_user_file_path, prefs_global_file_path) + + def load_prefs(self): + + (proj_pref_path, user_pref_path, + glob_pref_path) = self.get_pref_file_paths() + + with io_preferences_file(self, proj_pref_path) as prefs_file: + self.prefs = pickle.load(prefs_file) + self.log.info( + "Project - preferences contents:\n{}".format( + pformat(self.prefs) + )) + + with io_preferences_file(self, user_pref_path) as prefs_file: + self.prefs_user = pickle.load(prefs_file) + self.log.info( + "User - preferences contents:\n{}".format( + pformat(self.prefs_user) + )) + + with io_preferences_file(self, glob_pref_path) as prefs_file: + self.prefs_global = pickle.load(prefs_file) + self.log.info( + "Global - preferences contents:\n{}".format( + pformat(self.prefs_global) + )) + + return True + + def save_prefs(self): + # make sure the preference folder is available + if not os.path.isdir(self.prefs_folder): + try: + os.makedirs(self.prefs_folder) + except Exception: + self.log.info("Unable to create folder {}".format( + self.prefs_folder)) + return False + + # get all pref file paths + (proj_pref_path, user_pref_path, + glob_pref_path) = self.get_pref_file_paths() + + with io_preferences_file(self, proj_pref_path, True) as prefs_file: + pickle.dump(self.prefs, prefs_file) + self.log.info( + "Project - preferences contents:\n{}".format( + pformat(self.prefs) + )) + + with io_preferences_file(self, user_pref_path, True) as prefs_file: + pickle.dump(self.prefs_user, prefs_file) + self.log.info( + "User - preferences contents:\n{}".format( + pformat(self.prefs_user) + )) + + with io_preferences_file(self, glob_pref_path, True) as prefs_file: + pickle.dump(self.prefs_global, prefs_file) + self.log.info( + "Global - preferences contents:\n{}".format( + pformat(self.prefs_global) + )) + + return True + + +@contextlib.contextmanager +def maintain_current_timeline(to_timeline, from_timeline=None): + """Maintain current timeline selection during context + + Attributes: + from_timeline (resolve.Timeline)[optional]: + Example: + >>> print(from_timeline.GetName()) + timeline1 + >>> print(to_timeline.GetName()) + timeline2 + + >>> with maintain_current_timeline(to_timeline): + ... print(get_current_timeline().GetName()) + timeline2 + + >>> print(get_current_timeline().GetName()) + timeline1 + """ + # todo: this is still Resolve's implementation + project = get_current_project() + working_timeline = from_timeline or project.GetCurrentTimeline() + + # swith to the input timeline + project.SetCurrentTimeline(to_timeline) + + try: + # do a work + yield + finally: + # put the original working timeline to context + project.SetCurrentTimeline(working_timeline) + + +def get_project_manager(): + # TODO: get_project_manager + return + + +def get_media_storage(): + # TODO: get_media_storage + return + + +def get_current_project(): + # TODO: get_current_project + return + + +def get_current_timeline(new=False): + # TODO: get_current_timeline + return + + +def create_bin(name, root=None): + # TODO: create_bin + return + + +def rescan_hooks(): + import flame + try: + flame.execute_shortcut('Rescan Python Hooks') + except Exception: + pass diff --git a/openpype/hosts/flame/api/menu.py b/openpype/hosts/flame/api/menu.py new file mode 100644 index 0000000000..b4f1728acf --- /dev/null +++ b/openpype/hosts/flame/api/menu.py @@ -0,0 +1,208 @@ +import os +from Qt import QtWidgets +from copy import deepcopy + +from openpype.tools.utils.host_tools import HostToolsHelper + + +menu_group_name = 'OpenPype' + +default_flame_export_presets = { + 'Publish': { + 'PresetVisibility': 2, + 'PresetType': 0, + 'PresetFile': 'OpenEXR/OpenEXR (16-bit fp PIZ).xml' + }, + 'Preview': { + 'PresetVisibility': 3, + 'PresetType': 2, + 'PresetFile': 'Generate Preview.xml' + }, + 'Thumbnail': { + 'PresetVisibility': 3, + 'PresetType': 0, + 'PresetFile': 'Generate Thumbnail.xml' + } +} + + +class _FlameMenuApp(object): + def __init__(self, framework): + self.name = self.__class__.__name__ + self.framework = framework + self.log = framework.log + self.menu_group_name = menu_group_name + self.dynamic_menu_data = {} + + # flame module is only avaliable when a + # flame project is loaded and initialized + self.flame = None + try: + import flame + self.flame = flame + except ImportError: + self.flame = None + + self.flame_project_name = flame.project.current_project.name + self.prefs = self.framework.prefs_dict(self.framework.prefs, self.name) + self.prefs_user = self.framework.prefs_dict( + self.framework.prefs_user, self.name) + self.prefs_global = self.framework.prefs_dict( + self.framework.prefs_global, self.name) + + self.mbox = QtWidgets.QMessageBox() + + self.menu = { + "actions": [{ + 'name': os.getenv("AVALON_PROJECT", "project"), + 'isEnabled': False + }], + "name": self.menu_group_name + } + self.tools_helper = HostToolsHelper() + + def __getattr__(self, name): + def method(*args, **kwargs): + print('calling %s' % name) + return method + + def rescan(self, *args, **kwargs): + if not self.flame: + try: + import flame + self.flame = flame + except ImportError: + self.flame = None + + if self.flame: + self.flame.execute_shortcut('Rescan Python Hooks') + self.log.info('Rescan Python Hooks') + + +class FlameMenuProjectConnect(_FlameMenuApp): + + # flameMenuProjectconnect app takes care of the preferences dialog as well + + def __init__(self, framework): + _FlameMenuApp.__init__(self, framework) + + def __getattr__(self, name): + def method(*args, **kwargs): + project = self.dynamic_menu_data.get(name) + if project: + self.link_project(project) + return method + + def build_menu(self): + if not self.flame: + return [] + + flame_project_name = self.flame_project_name + self.log.info("______ {} ______".format(flame_project_name)) + + menu = deepcopy(self.menu) + + menu['actions'].append({ + "name": "Workfiles ...", + "execute": lambda x: self.tools_helper.show_workfiles() + }) + menu['actions'].append({ + "name": "Create ...", + "execute": lambda x: self.tools_helper.show_creator() + }) + menu['actions'].append({ + "name": "Publish ...", + "execute": lambda x: self.tools_helper.show_publish() + }) + menu['actions'].append({ + "name": "Load ...", + "execute": lambda x: self.tools_helper.show_loader() + }) + menu['actions'].append({ + "name": "Manage ...", + "execute": lambda x: self.tools_helper.show_scene_inventory() + }) + menu['actions'].append({ + "name": "Library ...", + "execute": lambda x: self.tools_helper.show_library_loader() + }) + return menu + + def get_projects(self, *args, **kwargs): + pass + + def refresh(self, *args, **kwargs): + self.rescan() + + def rescan(self, *args, **kwargs): + if not self.flame: + try: + import flame + self.flame = flame + except ImportError: + self.flame = None + + if self.flame: + self.flame.execute_shortcut('Rescan Python Hooks') + self.log.info('Rescan Python Hooks') + + +class FlameMenuTimeline(_FlameMenuApp): + + # flameMenuProjectconnect app takes care of the preferences dialog as well + + def __init__(self, framework): + _FlameMenuApp.__init__(self, framework) + + def __getattr__(self, name): + def method(*args, **kwargs): + project = self.dynamic_menu_data.get(name) + if project: + self.link_project(project) + return method + + def build_menu(self): + if not self.flame: + return [] + + flame_project_name = self.flame_project_name + self.log.info("______ {} ______".format(flame_project_name)) + + menu = deepcopy(self.menu) + + menu['actions'].append({ + "name": "Create ...", + "execute": lambda x: self.tools_helper.show_creator() + }) + menu['actions'].append({ + "name": "Publish ...", + "execute": lambda x: self.tools_helper.show_publish() + }) + menu['actions'].append({ + "name": "Load ...", + "execute": lambda x: self.tools_helper.show_loader() + }) + menu['actions'].append({ + "name": "Manage ...", + "execute": lambda x: self.tools_helper.show_scene_inventory() + }) + + return menu + + def get_projects(self, *args, **kwargs): + pass + + def refresh(self, *args, **kwargs): + self.rescan() + + def rescan(self, *args, **kwargs): + if not self.flame: + try: + import flame + self.flame = flame + except ImportError: + self.flame = None + + if self.flame: + self.flame.execute_shortcut('Rescan Python Hooks') + self.log.info('Rescan Python Hooks') diff --git a/openpype/hosts/flame/api/pipeline.py b/openpype/hosts/flame/api/pipeline.py new file mode 100644 index 0000000000..26dfe7c032 --- /dev/null +++ b/openpype/hosts/flame/api/pipeline.py @@ -0,0 +1,155 @@ +""" +Basic avalon integration +""" +import contextlib +from avalon import api as avalon +from pyblish import api as pyblish +from openpype.api import Logger + +AVALON_CONTAINERS = "AVALON_CONTAINERS" + +log = Logger().get_logger(__name__) + + +def install(): + from .. import ( + PUBLISH_PATH, + LOAD_PATH, + CREATE_PATH, + INVENTORY_PATH + ) + # TODO: install + + # Disable all families except for the ones we explicitly want to see + family_states = [ + "imagesequence", + "render2d", + "plate", + "render", + "mov", + "clip" + ] + avalon.data["familiesStateDefault"] = False + avalon.data["familiesStateToggled"] = family_states + + log.info("openpype.hosts.flame installed") + + pyblish.register_host("flame") + pyblish.register_plugin_path(PUBLISH_PATH) + log.info("Registering Flame 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) + + # register callback for switching publishable + pyblish.register_callback("instanceToggled", on_pyblish_instance_toggled) + + +def uninstall(): + from .. import ( + PUBLISH_PATH, + LOAD_PATH, + CREATE_PATH, + INVENTORY_PATH + ) + + # TODO: uninstall + pyblish.deregister_host("flame") + 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) + + # register callback for switching publishable + pyblish.deregister_callback("instanceToggled", on_pyblish_instance_toggled) + + +def containerise(tl_segment, + name, + namespace, + context, + loader=None, + data=None): + # TODO: containerise + pass + + +def ls(): + """List available containers. + """ + # TODO: ls + pass + + +def parse_container(tl_segment, validate=True): + """Return container data from timeline_item's openpype tag. + """ + # TODO: parse_container + pass + + +def update_container(tl_segment, data=None): + """Update container data to input timeline_item's openpype tag. + """ + # TODO: update_container + pass + + +@contextlib.contextmanager +def maintained_selection(): + """Maintain selection during context + + Example: + >>> with maintained_selection(): + ... node['selected'].setValue(True) + >>> print(node['selected'].value()) + False + """ + # TODO: maintained_selection + remove undo steps + + try: + # do the operation + yield + finally: + pass + + +def reset_selection(): + """Deselect all selected nodes + """ + pass + + +def on_pyblish_instance_toggled(instance, old_value, new_value): + """Toggle node passthrough states on instance toggles.""" + + log.info("instance toggle: {}, old_value: {}, new_value:{} ".format( + instance, old_value, new_value)) + + # from openpype.hosts.resolve import ( + # set_publish_attribute + # ) + + # # Whether instances should be passthrough based on new value + # timeline_item = instance.data["item"] + # set_publish_attribute(timeline_item, new_value) + + +def remove_instance(instance): + """Remove instance marker from track item.""" + # TODO: remove_instance + pass + + +def list_instances(): + """List all created instances from current workfile.""" + # TODO: list_instances + pass + + +def imprint(item, data=None): + # TODO: imprint + pass diff --git a/openpype/hosts/flame/api/plugin.py b/openpype/hosts/flame/api/plugin.py new file mode 100644 index 0000000000..2a28a20a75 --- /dev/null +++ b/openpype/hosts/flame/api/plugin.py @@ -0,0 +1,3 @@ +# Creator plugin functions +# Publishing plugin functions +# Loader plugin functions diff --git a/openpype/hosts/flame/api/utils.py b/openpype/hosts/flame/api/utils.py new file mode 100644 index 0000000000..a750046362 --- /dev/null +++ b/openpype/hosts/flame/api/utils.py @@ -0,0 +1,108 @@ +""" +Flame utils for syncing scripts +""" + +import os +import shutil +from openpype.api import Logger +log = Logger().get_logger(__name__) + + +def _sync_utility_scripts(env=None): + """ Synchronizing basic utlility scripts for flame. + + To be able to run start OpenPype within Flame we have to copy + all utility_scripts and additional FLAME_SCRIPT_DIR into + `/opt/Autodesk/shared/python`. This will be always synchronizing those + folders. + """ + from .. import HOST_DIR + + env = env or os.environ + + # initiate inputs + scripts = {} + fsd_env = env.get("FLAME_SCRIPT_DIRS", "") + flame_shared_dir = "/opt/Autodesk/shared/python" + + fsd_paths = [os.path.join( + HOST_DIR, + "utility_scripts" + )] + + # collect script dirs + log.info("FLAME_SCRIPT_DIRS: `{fsd_env}`".format(**locals())) + log.info("fsd_paths: `{fsd_paths}`".format(**locals())) + + # add application environment setting for FLAME_SCRIPT_DIR + # to script path search + for _dirpath in fsd_env.split(os.pathsep): + if not os.path.isdir(_dirpath): + log.warning("Path is not a valid dir: `{_dirpath}`".format( + **locals())) + continue + fsd_paths.append(_dirpath) + + # collect scripts from dirs + for path in fsd_paths: + scripts.update({path: os.listdir(path)}) + + remove_black_list = [] + for _k, s_list in scripts.items(): + remove_black_list += s_list + + log.info("remove_black_list: `{remove_black_list}`".format(**locals())) + log.info("Additional Flame script paths: `{fsd_paths}`".format(**locals())) + log.info("Flame Scripts: `{scripts}`".format(**locals())) + + # make sure no script file is in folder + if next(iter(os.listdir(flame_shared_dir)), None): + for _itm in os.listdir(flame_shared_dir): + skip = False + + # skip all scripts and folders which are not maintained + if _itm not in remove_black_list: + skip = True + + # do not skyp if pyc in extension + if not os.path.isdir(_itm) and "pyc" in os.path.splitext(_itm)[-1]: + skip = False + + # continue if skip in true + if skip: + continue + + path = os.path.join(flame_shared_dir, _itm) + log.info("Removing `{path}`...".format(**locals())) + if os.path.isdir(path): + shutil.rmtree(path, onerror=None) + else: + os.remove(path) + + # copy scripts into Resolve's utility scripts dir + for dirpath, scriptlist in scripts.items(): + # directory and scripts list + for _script in scriptlist: + # script in script list + src = os.path.join(dirpath, _script) + dst = os.path.join(flame_shared_dir, _script) + log.info("Copying `{src}` to `{dst}`...".format(**locals())) + if os.path.isdir(src): + shutil.copytree( + src, dst, symlinks=False, + ignore=None, ignore_dangling_symlinks=False + ) + else: + shutil.copy2(src, dst) + + +def setup(env=None): + """ Wrapper installer started from + `flame/hooks/pre_flame_setup.py` + """ + env = env or os.environ + + # synchronize resolve utility scripts + _sync_utility_scripts(env) + + log.info("Flame OpenPype wrapper has been installed") diff --git a/openpype/hosts/flame/api/workio.py b/openpype/hosts/flame/api/workio.py new file mode 100644 index 0000000000..d2e2408798 --- /dev/null +++ b/openpype/hosts/flame/api/workio.py @@ -0,0 +1,37 @@ +"""Host API required Work Files tool""" + +import os +from openpype.api import Logger +# from .. import ( +# get_project_manager, +# get_current_project +# ) + + +log = Logger().get_logger(__name__) + +exported_projet_ext = ".otoc" + + +def file_extensions(): + return [exported_projet_ext] + + +def has_unsaved_changes(): + pass + + +def save_file(filepath): + pass + + +def open_file(filepath): + pass + + +def current_file(): + pass + + +def work_root(session): + return os.path.normpath(session["AVALON_WORKDIR"]).replace("\\", "/") diff --git a/openpype/hosts/flame/hooks/pre_flame_setup.py b/openpype/hosts/flame/hooks/pre_flame_setup.py new file mode 100644 index 0000000000..368a70f395 --- /dev/null +++ b/openpype/hosts/flame/hooks/pre_flame_setup.py @@ -0,0 +1,132 @@ +import os +import json +import tempfile +import contextlib +from openpype.lib import ( + PreLaunchHook, get_openpype_username) +from openpype.hosts import flame as opflame +import openpype +from pprint import pformat + + +class FlamePrelaunch(PreLaunchHook): + """ Flame prelaunch hook + + Will make sure flame_script_dirs are coppied to user's folder defined + in environment var FLAME_SCRIPT_DIR. + """ + app_groups = ["flame"] + + # todo: replace version number with avalon launch app version + flame_python_exe = "/opt/Autodesk/python/2021/bin/python2.7" + + wtc_script_path = os.path.join( + opflame.HOST_DIR, "scripts", "wiretap_com.py") + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.signature = "( {} )".format(self.__class__.__name__) + + def execute(self): + """Hook entry method.""" + project_doc = self.data["project_doc"] + user_name = get_openpype_username() + + self.log.debug("Collected user \"{}\"".format(user_name)) + self.log.info(pformat(project_doc)) + _db_p_data = project_doc["data"] + width = _db_p_data["resolutionWidth"] + height = _db_p_data["resolutionHeight"] + fps = int(_db_p_data["fps"]) + + project_data = { + "Name": project_doc["name"], + "Nickname": _db_p_data["code"], + "Description": "Created by OpenPype", + "SetupDir": project_doc["name"], + "FrameWidth": int(width), + "FrameHeight": int(height), + "AspectRatio": float((width / height) * _db_p_data["pixelAspect"]), + "FrameRate": "{} fps".format(fps), + "FrameDepth": "16-bit fp", + "FieldDominance": "PROGRESSIVE" + } + + data_to_script = { + # from settings + "host_name": "localhost", + "volume_name": "stonefs", + "group_name": "staff", + "color_policy": "ACES 1.1", + + # from project + "project_name": project_doc["name"], + "user_name": user_name, + "project_data": project_data + } + app_arguments = self._get_launch_arguments(data_to_script) + + self.log.info(pformat(dict(self.launch_context.env))) + + opflame.setup(self.launch_context.env) + + self.launch_context.launch_args.extend(app_arguments) + + def _get_launch_arguments(self, script_data): + # Dump data to string + dumped_script_data = json.dumps(script_data) + + with make_temp_file(dumped_script_data) as tmp_json_path: + # Prepare subprocess arguments + args = [ + self.flame_python_exe, + self.wtc_script_path, + tmp_json_path + ] + self.log.info("Executing: {}".format(" ".join(args))) + + process_kwargs = { + "logger": self.log, + "env": {} + } + + openpype.api.run_subprocess(args, **process_kwargs) + + # process returned json file to pass launch args + return_json_data = open(tmp_json_path).read() + returned_data = json.loads(return_json_data) + app_args = returned_data.get("app_args") + self.log.info("____ app_args: `{}`".format(app_args)) + + if not app_args: + RuntimeError("App arguments were not solved") + + return app_args + + +@contextlib.contextmanager +def make_temp_file(data): + try: + # Store dumped json to temporary file + temporary_json_file = tempfile.NamedTemporaryFile( + mode="w", suffix=".json", delete=False + ) + temporary_json_file.write(data) + temporary_json_file.close() + temporary_json_filepath = temporary_json_file.name.replace( + "\\", "/" + ) + + yield temporary_json_filepath + + except IOError as _error: + raise IOError( + "Not able to create temp json file: {}".format( + _error + ) + ) + + finally: + # Remove the temporary json + os.remove(temporary_json_filepath) diff --git a/openpype/hosts/flame/scripts/wiretap_com.py b/openpype/hosts/flame/scripts/wiretap_com.py new file mode 100644 index 0000000000..d8dc1884cf --- /dev/null +++ b/openpype/hosts/flame/scripts/wiretap_com.py @@ -0,0 +1,490 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- + +from __future__ import absolute_import +import os +import sys +import subprocess +import json +import xml.dom.minidom as minidom +from copy import deepcopy +import datetime + +try: + from libwiretapPythonClientAPI import ( + WireTapClientInit) +except ImportError: + flame_python_path = "/opt/Autodesk/flame_2021/python" + flame_exe_path = ( + "/opt/Autodesk/flame_2021/bin/flame.app" + "/Contents/MacOS/startApp") + + sys.path.append(flame_python_path) + + from libwiretapPythonClientAPI import ( + WireTapClientInit, + WireTapClientUninit, + WireTapNodeHandle, + WireTapServerHandle, + WireTapInt, + WireTapStr + ) + + +class WireTapCom(object): + """ + Comunicator class wrapper for talking to WireTap db. + + This way we are able to set new project with settings and + correct colorspace policy. Also we are able to create new user + or get actuall user with similar name (users are usually cloning + their profiles and adding date stamp into suffix). + """ + + def __init__(self, host_name=None, volume_name=None, group_name=None): + """Initialisation of WireTap communication class + + Args: + host_name (str, optional): Name of host server. Defaults to None. + volume_name (str, optional): Name of volume. Defaults to None. + group_name (str, optional): Name of user group. Defaults to None. + """ + # set main attributes of server + # if there are none set the default installation + self.host_name = host_name or "localhost" + self.volume_name = volume_name or "stonefs" + self.group_name = group_name or "staff" + + # initialize WireTap client + WireTapClientInit() + + # add the server to shared variable + self._server = WireTapServerHandle("{}:IFFFS".format(self.host_name)) + print("WireTap connected at '{}'...".format( + self.host_name)) + + def close(self): + self._server = None + WireTapClientUninit() + print("WireTap closed...") + + def get_launch_args( + self, project_name, project_data, user_name, *args, **kwargs): + """Forming launch arguments for OpenPype launcher. + + Args: + project_name (str): name of project + project_data (dict): Flame compatible project data + user_name (str): name of user + + Returns: + list: arguments + """ + + workspace_name = kwargs.get("workspace_name") + color_policy = kwargs.get("color_policy") + + self._project_prep(project_name) + self._set_project_settings(project_name, project_data) + self._set_project_colorspace(project_name, color_policy) + user_name = self._user_prep(user_name) + + if workspace_name is None: + # default workspace + print("Using a default workspace") + return [ + "--start-project={}".format(project_name), + "--start-user={}".format(user_name), + "--create-workspace" + ] + + else: + print( + "Using a custom workspace '{}'".format(workspace_name)) + + self._workspace_prep(project_name, workspace_name) + return [ + "--start-project={}".format(project_name), + "--start-user={}".format(user_name), + "--create-workspace", + "--start-workspace={}".format(workspace_name) + ] + + def _workspace_prep(self, project_name, workspace_name): + """Preparing a workspace + + In case it doesn not exists it will create one + + Args: + project_name (str): project name + workspace_name (str): workspace name + + Raises: + AttributeError: unable to create workspace + """ + workspace_exists = self._child_is_in_parent_path( + "/projects/{}".format(project_name), workspace_name, "WORKSPACE" + ) + if not workspace_exists: + project = WireTapNodeHandle( + self._server, "/projects/{}".format(project_name)) + + workspace_node = WireTapNodeHandle() + created_workspace = project.createNode( + workspace_name, "WORKSPACE", workspace_node) + + if not created_workspace: + raise AttributeError( + "Cannot create workspace `{}` in " + "project `{}`: `{}`".format( + workspace_name, project_name, project.lastError()) + ) + + print( + "Workspace `{}` is successfully created".format(workspace_name)) + + def _project_prep(self, project_name): + """Preparing a project + + In case it doesn not exists it will create one + + Args: + project_name (str): project name + + Raises: + AttributeError: unable to create project + """ + # test if projeft exists + project_exists = self._child_is_in_parent_path( + "/projects", project_name, "PROJECT") + + if not project_exists: + volumes = self._get_all_volumes() + + if len(volumes) == 0: + raise AttributeError( + "Not able to create new project. No Volumes existing" + ) + + # check if volumes exists + if self.volume_name not in volumes: + raise AttributeError( + ("Volume '{}' does not exist '{}'").format( + self.volume_name, volumes) + ) + + # form cmd arguments + project_create_cmd = [ + os.path.join( + "/opt/Autodesk/", + "wiretap", + "tools", + "2021", + "wiretap_create_node", + ), + '-n', + os.path.join("/volumes", self.volume_name), + '-d', + project_name, + '-g', + ] + + project_create_cmd.append(self.group_name) + + print(project_create_cmd) + + exit_code = subprocess.call( + project_create_cmd, + cwd=os.path.expanduser('~')) + + if exit_code != 0: + RuntimeError("Cannot create project in flame db") + + print( + "A new project '{}' is created.".format(project_name)) + + def _get_all_volumes(self): + """Request all available volumens from WireTap + + Returns: + list: all available volumes in server + + Rises: + AttributeError: unable to get any volumes childs from server + """ + root = WireTapNodeHandle(self._server, "/volumes") + children_num = WireTapInt(0) + + get_children_num = root.getNumChildren(children_num) + if not get_children_num: + raise AttributeError( + "Cannot get number of volumes: {}".format(root.lastError()) + ) + + volumes = [] + + # go trough all children and get volume names + child_obj = WireTapNodeHandle() + for child_idx in range(children_num): + + # get a child + if not root.getChild(child_idx, child_obj): + raise AttributeError( + "Unable to get child: {}".format(root.lastError())) + + node_name = WireTapStr() + get_children_name = child_obj.getDisplayName(node_name) + + if not get_children_name: + raise AttributeError( + "Unable to get child name: {}".format( + child_obj.lastError()) + ) + + volumes.append(node_name.c_str()) + + return volumes + + def _user_prep(self, user_name): + """Ensuring user does exists in user's stack + + Args: + user_name (str): name of a user + + Raises: + AttributeError: unable to create user + """ + + # get all used usernames in db + used_names = self._get_usernames() + print(">> used_names: {}".format(used_names)) + + # filter only those which are sharing input user name + filtered_users = [user for user in used_names if user_name in user] + + if filtered_users: + # todo: need to find lastly created following regex patern for + # date used in name + return filtered_users.pop() + + # create new user name with date in suffix + now = datetime.datetime.now() # current date and time + date = now.strftime("%Y%m%d") + new_user_name = "{}_{}".format(user_name, date) + print(new_user_name) + + if not self._child_is_in_parent_path("/users", new_user_name, "USER"): + # Create the new user + users = WireTapNodeHandle(self._server, "/users") + + user_node = WireTapNodeHandle() + created_user = users.createNode(new_user_name, "USER", user_node) + if not created_user: + raise AttributeError( + "User {} cannot be created: {}".format( + new_user_name, users.lastError()) + ) + + print("User `{}` is created".format(new_user_name)) + return new_user_name + + def _get_usernames(self): + """Requesting all available users from WireTap + + Returns: + list: all available user names + + Raises: + AttributeError: there are no users in server + """ + root = WireTapNodeHandle(self._server, "/users") + children_num = WireTapInt(0) + + get_children_num = root.getNumChildren(children_num) + if not get_children_num: + raise AttributeError( + "Cannot get number of volumes: {}".format(root.lastError()) + ) + + usernames = [] + + # go trough all children and get volume names + child_obj = WireTapNodeHandle() + for child_idx in range(children_num): + + # get a child + if not root.getChild(child_idx, child_obj): + raise AttributeError( + "Unable to get child: {}".format(root.lastError())) + + node_name = WireTapStr() + get_children_name = child_obj.getDisplayName(node_name) + + if not get_children_name: + raise AttributeError( + "Unable to get child name: {}".format( + child_obj.lastError()) + ) + + usernames.append(node_name.c_str()) + + return usernames + + def _child_is_in_parent_path(self, parent_path, child_name, child_type): + """Checking if a given child is in parent path. + + Args: + parent_path (str): db path to parent + child_name (str): name of child + child_type (str): type of child + + Raises: + AttributeError: Not able to get number of children + AttributeError: Not able to get children form parent + AttributeError: Not able to get children name + AttributeError: Not able to get children type + + Returns: + bool: True if child is in parent path + """ + parent = WireTapNodeHandle(self._server, parent_path) + + # iterate number of children + children_num = WireTapInt(0) + requested = parent.getNumChildren(children_num) + if not requested: + raise AttributeError(( + "Error: Cannot request number of " + "childrens from the node {}. Make sure your " + "wiretap service is running: {}").format( + parent_path, parent.lastError()) + ) + + # iterate children + child_obj = WireTapNodeHandle() + for child_idx in range(children_num): + if not parent.getChild(child_idx, child_obj): + raise AttributeError( + "Cannot get child: {}".format( + parent.lastError())) + + node_name = WireTapStr() + node_type = WireTapStr() + + if not child_obj.getDisplayName(node_name): + raise AttributeError( + "Unable to get child name: %s" % child_obj.lastError() + ) + if not child_obj.getNodeTypeStr(node_type): + raise AttributeError( + "Unable to obtain child type: %s" % child_obj.lastError() + ) + + if (node_name.c_str() == child_name) and ( + node_type.c_str() == child_type): + return True + + return False + + def _set_project_settings(self, project_name, project_data): + """Setting project attributes. + + Args: + project_name (str): name of project + project_data (dict): data with project attributes + (flame compatible) + + Raises: + AttributeError: Not able to set project attributes + """ + # generated xml from project_data dict + _xml = "" + for key, value in project_data.items(): + _xml += "<{}>{}".format(key, value, key) + _xml += "" + + pretty_xml = minidom.parseString(_xml).toprettyxml() + print("__ xml: {}".format(pretty_xml)) + + # set project data to wiretap + project_node = WireTapNodeHandle( + self._server, "/projects/{}".format(project_name)) + + if not project_node.setMetaData("XML", _xml): + raise AttributeError( + "Not able to set project attributes {}. Error: {}".format( + project_name, project_node.lastError()) + ) + + print("Project settings successfully set.") + + def _set_project_colorspace(self, project_name, color_policy): + """Set project's colorspace policy. + + Args: + project_name (str): name of project + color_policy (str): name of policy + + Raises: + RuntimeError: Not able to set colorspace policy + """ + color_policy = color_policy or "Legacy" + project_colorspace_cmd = [ + os.path.join( + "/opt/Autodesk/", + "wiretap", + "tools", + "2021", + "wiretap_duplicate_node", + ), + "-s", + "/syncolor/policies/Autodesk/{}".format(color_policy), + "-n", + "/projects/{}/syncolor".format(project_name) + ] + + print(project_colorspace_cmd) + + exit_code = subprocess.call( + project_colorspace_cmd, + cwd=os.path.expanduser('~')) + + if exit_code != 0: + RuntimeError("Cannot set colorspace {} on project {}".format( + color_policy, project_name + )) + + +if __name__ == "__main__": + # get json exchange data + json_path = sys.argv[-1] + json_data = open(json_path).read() + in_data = json.loads(json_data) + out_data = deepcopy(in_data) + + # get main server attributes + host_name = in_data.pop("host_name") + volume_name = in_data.pop("volume_name") + group_name = in_data.pop("group_name") + + # initialize class + wiretap_handler = WireTapCom(host_name, volume_name, group_name) + + try: + app_args = wiretap_handler.get_launch_args( + project_name=in_data.pop("project_name"), + project_data=in_data.pop("project_data"), + user_name=in_data.pop("user_name"), + **in_data + ) + finally: + wiretap_handler.close() + + # set returned args back to out data + out_data.update({ + "app_args": app_args + }) + + # write it out back to the exchange json file + with open(json_path, "w") as file_stream: + json.dump(out_data, file_stream, indent=4) diff --git a/openpype/hosts/flame/utility_scripts/openpype_in_flame.py b/openpype/hosts/flame/utility_scripts/openpype_in_flame.py new file mode 100644 index 0000000000..c5fa881f3c --- /dev/null +++ b/openpype/hosts/flame/utility_scripts/openpype_in_flame.py @@ -0,0 +1,191 @@ +from __future__ import print_function +import sys +from Qt import QtWidgets +from pprint import pformat +import atexit +import openpype +import avalon +import openpype.hosts.flame as opflame + +flh = sys.modules[__name__] +flh._project = None + + +def openpype_install(): + """Registering OpenPype in context + """ + openpype.install() + avalon.api.install(opflame) + print("Avalon registred hosts: {}".format( + avalon.api.registered_host())) + + +# Exception handler +def exeption_handler(exctype, value, _traceback): + """Exception handler for improving UX + + Args: + exctype (str): type of exception + value (str): exception value + tb (str): traceback to show + """ + import traceback + msg = "OpenPype: Python exception {} in {}".format(value, exctype) + mbox = QtWidgets.QMessageBox() + mbox.setText(msg) + mbox.setDetailedText( + pformat(traceback.format_exception(exctype, value, _traceback))) + mbox.setStyleSheet('QLabel{min-width: 800px;}') + mbox.exec_() + sys.__excepthook__(exctype, value, _traceback) + + +# add exception handler into sys module +sys.excepthook = exeption_handler + + +# register clean up logic to be called at Flame exit +def cleanup(): + """Cleaning up Flame framework context + """ + if opflame.apps: + print('`{}` cleaning up apps:\n {}\n'.format( + __file__, pformat(opflame.apps))) + while len(opflame.apps): + app = opflame.apps.pop() + print('`{}` removing : {}'.format(__file__, app.name)) + del app + opflame.apps = [] + + if opflame.app_framework: + print('PYTHON\t: %s cleaning up' % opflame.app_framework.bundle_name) + opflame.app_framework.save_prefs() + opflame.app_framework = None + + +atexit.register(cleanup) + + +def load_apps(): + """Load available apps into Flame framework + """ + opflame.apps.append(opflame.FlameMenuProjectConnect(opflame.app_framework)) + opflame.apps.append(opflame.FlameMenuTimeline(opflame.app_framework)) + opflame.app_framework.log.info("Apps are loaded") + + +def project_changed_dict(info): + """Hook for project change action + + Args: + info (str): info text + """ + cleanup() + + +def app_initialized(parent=None): + """Inicialization of Framework + + Args: + parent (obj, optional): Parent object. Defaults to None. + """ + opflame.app_framework = opflame.FlameAppFramework() + + print("{} initializing".format( + opflame.app_framework.bundle_name)) + + load_apps() + + +""" +Initialisation of the hook is starting from here + +First it needs to test if it can import the flame modul. +This will happen only in case a project has been loaded. +Then `app_initialized` will load main Framework which will load +all menu objects as apps. +""" + +try: + import flame # noqa + app_initialized(parent=None) +except ImportError: + print("!!!! not able to import flame module !!!!") + + +def rescan_hooks(): + import flame # noqa + flame.execute_shortcut('Rescan Python Hooks') + + +def _build_app_menu(app_name): + """Flame menu object generator + + Args: + app_name (str): name of menu object app + + Returns: + list: menu object + """ + menu = [] + + # first find the relative appname + app = None + for _app in opflame.apps: + if _app.__class__.__name__ == app_name: + app = _app + + if app: + menu.append(app.build_menu()) + + if opflame.app_framework: + menu_auto_refresh = opflame.app_framework.prefs_global.get( + 'menu_auto_refresh', {}) + if menu_auto_refresh.get('timeline_menu', True): + try: + import flame # noqa + flame.schedule_idle_event(rescan_hooks) + except ImportError: + print("!-!!! not able to import flame module !!!!") + + return menu + + +""" Flame hooks are starting here +""" + + +def project_saved(project_name, save_time, is_auto_save): + """Hook to activate when project is saved + + Args: + project_name (str): name of project + save_time (str): time when it was saved + is_auto_save (bool): autosave is on or off + """ + if opflame.app_framework: + opflame.app_framework.save_prefs() + + +def get_main_menu_custom_ui_actions(): + """Hook to create submenu in start menu + + Returns: + list: menu object + """ + # install openpype and the host + openpype_install() + + return _build_app_menu("FlameMenuProjectConnect") + + +def get_timeline_custom_ui_actions(): + """Hook to create submenu in timeline + + Returns: + list: menu object + """ + # install openpype and the host + openpype_install() + + return _build_app_menu("FlameMenuTimeline") diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index b24235447f..b16774a8d6 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -437,7 +437,8 @@ def empty_sets(sets, force=False): cmds.connectAttr(src, dest) # Restore original members - for origin_set, members in original.iteritems(): + _iteritems = getattr(original, "iteritems", original.items) + for origin_set, members in _iteritems(): cmds.sets(members, forceElement=origin_set) @@ -581,7 +582,7 @@ def get_shader_assignments_from_shapes(shapes, components=True): # Build a mapping from parent to shapes to include in lookup. transforms = {shape.rsplit("|", 1)[0]: shape for shape in shapes} - lookup = set(shapes + transforms.keys()) + lookup = set(shapes) | set(transforms.keys()) component_assignments = defaultdict(list) for shading_group in assignments.keys(): @@ -669,7 +670,8 @@ def displaySmoothness(nodes, yield finally: # Revert state - for node, state in originals.iteritems(): + _iteritems = getattr(originals, "iteritems", originals.items) + for node, state in _iteritems(): if state: cmds.displaySmoothness(node, **state) @@ -712,7 +714,8 @@ def no_display_layers(nodes): yield finally: # Restore original members - for layer, members in original.iteritems(): + _iteritems = getattr(original, "iteritems", original.items) + for layer, members in _iteritems(): cmds.editDisplayLayerMembers(layer, members, noRecurse=True) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 4f0966abfd..4e69e41dca 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -21,10 +21,8 @@ def _get_menu(menu_name=None): if menu_name is None: menu_name = pipeline._menu - widgets = dict(( - w.objectName(), w) for w in QtWidgets.QApplication.allWidgets()) - menu = widgets.get(menu_name) - return menu + widgets = {w.objectName(): w for w in QtWidgets.QApplication.allWidgets()} + return widgets.get(menu_name) def deferred(): @@ -46,6 +44,43 @@ def deferred(): ) ) + def add_experimental_item(): + cmds.menuItem( + "Experimental tools...", + parent=pipeline._menu, + command=lambda *args: host_tools.show_experimental_tools_dialog( + pipeline._parent + ) + ) + + def add_scripts_menu(): + try: + import scriptsmenu.launchformaya as launchformaya + except ImportError: + log.warning( + "Skipping studio.menu install, because " + "'scriptsmenu' module seems unavailable." + ) + return + + # load configuration of custom menu + project_settings = get_project_settings(os.getenv("AVALON_PROJECT")) + config = project_settings["maya"]["scriptsmenu"]["definition"] + _menu = project_settings["maya"]["scriptsmenu"]["name"] + + if not config: + log.warning("Skipping studio menu, no definition found.") + return + + # run the launcher for Maya menu + studio_menu = launchformaya.main( + title=_menu.title(), + objectName=_menu.title().lower().replace(" ", "_") + ) + + # apply configuration + studio_menu.build_from_configuration(studio_menu, config) + def modify_workfiles(): # Find the pipeline menu top_menu = _get_menu() @@ -101,38 +136,13 @@ def deferred(): log.info("Attempting to install scripts menu ...") + # add_scripts_menu() add_build_workfiles_item() add_look_assigner_item() + add_experimental_item() modify_workfiles() remove_project_manager() - - try: - import scriptsmenu.launchformaya as launchformaya - import scriptsmenu.scriptsmenu as scriptsmenu - except ImportError: - log.warning( - "Skipping studio.menu install, because " - "'scriptsmenu' module seems unavailable." - ) - return - - # load configuration of custom menu - project_settings = get_project_settings(os.getenv("AVALON_PROJECT")) - config = project_settings["maya"]["scriptsmenu"]["definition"] - _menu = project_settings["maya"]["scriptsmenu"]["name"] - - if not config: - log.warning("Skipping studio menu, no definition found.") - return - - # run the launcher for Maya menu - studio_menu = launchformaya.main( - title=_menu.title(), - objectName=_menu.title().lower().replace(" ", "_") - ) - - # apply configuration - studio_menu.build_from_configuration(studio_menu, config) + add_scripts_menu() def uninstall(): @@ -153,7 +163,7 @@ def install(): return # Allow time for uninstallation to finish. - cmds.evalDeferred(deferred) + cmds.evalDeferred(deferred, lowestPriority=True) def popup(): diff --git a/openpype/hosts/maya/api/setdress.py b/openpype/hosts/maya/api/setdress.py index be26572039..3537fa3837 100644 --- a/openpype/hosts/maya/api/setdress.py +++ b/openpype/hosts/maya/api/setdress.py @@ -5,6 +5,7 @@ import os import contextlib import copy +import six from maya import cmds from avalon import api, io @@ -69,7 +70,8 @@ def unlocked(nodes): yield finally: # Reapply original states - for uuid, state in states.iteritems(): + _iteritems = getattr(states, "iteritems", states.items) + for uuid, state in _iteritems(): nodes_from_id = cmds.ls(uuid, long=True) if nodes_from_id: node = nodes_from_id[0] @@ -94,7 +96,7 @@ def load_package(filepath, name, namespace=None): # Define a unique namespace for the package namespace = os.path.basename(filepath).split(".")[0] unique_namespace(namespace) - assert isinstance(namespace, basestring) + assert isinstance(namespace, six.string_types) # Load the setdress package data with open(filepath, "r") as fp: diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx.py b/openpype/hosts/maya/plugins/publish/extract_fbx.py index e5f3b0cda4..720a61b0a7 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx.py @@ -183,7 +183,8 @@ class ExtractFBX(openpype.api.Extractor): # Apply the FBX overrides through MEL since the commands # only work correctly in MEL according to online # available discussions on the topic - for option, value in options.iteritems(): + _iteritems = getattr(options, "iteritems", options.items) + for option, value in _iteritems(): key = option[0].upper() + option[1:] # uppercase first letter # Boolean must be passed as lower-case strings diff --git a/openpype/hosts/maya/plugins/publish/submit_maya_muster.py b/openpype/hosts/maya/plugins/publish/submit_maya_muster.py index 1c97f0faf7..207cf56cfe 100644 --- a/openpype/hosts/maya/plugins/publish/submit_maya_muster.py +++ b/openpype/hosts/maya/plugins/publish/submit_maya_muster.py @@ -383,7 +383,7 @@ class MayaSubmitMuster(pyblish.api.InstancePlugin): "attributes": { "environmental_variables": { "value": ", ".join("{!s}={!r}".format(k, v) - for (k, v) in env.iteritems()), + for (k, v) in env.items()), "state": True, "subst": False diff --git a/openpype/hosts/maya/plugins/publish/validate_instance_subset.py b/openpype/hosts/maya/plugins/publish/validate_instance_subset.py index a8c16425d6..539f3f9d3c 100644 --- a/openpype/hosts/maya/plugins/publish/validate_instance_subset.py +++ b/openpype/hosts/maya/plugins/publish/validate_instance_subset.py @@ -2,6 +2,8 @@ import pyblish.api import openpype.api import string +import six + # Allow only characters, numbers and underscore allowed = set(string.ascii_lowercase + string.ascii_uppercase + @@ -29,7 +31,7 @@ class ValidateSubsetName(pyblish.api.InstancePlugin): raise RuntimeError("Instance is missing subset " "name: {0}".format(subset)) - if not isinstance(subset, basestring): + if not isinstance(subset, six.string_types): raise TypeError("Instance subset name must be string, " "got: {0} ({1})".format(subset, type(subset))) diff --git a/openpype/hosts/maya/plugins/publish/validate_node_ids_unique.py b/openpype/hosts/maya/plugins/publish/validate_node_ids_unique.py index 39bb148911..ed9ef526d6 100644 --- a/openpype/hosts/maya/plugins/publish/validate_node_ids_unique.py +++ b/openpype/hosts/maya/plugins/publish/validate_node_ids_unique.py @@ -52,7 +52,8 @@ class ValidateNodeIdsUnique(pyblish.api.InstancePlugin): # Take only the ids with more than one member invalid = list() - for _ids, members in ids.iteritems(): + _iteritems = getattr(ids, "iteritems", ids.items) + for _ids, members in _iteritems(): if len(members) > 1: cls.log.error("ID found on multiple nodes: '%s'" % members) invalid.extend(members) diff --git a/openpype/hosts/maya/plugins/publish/validate_node_no_ghosting.py b/openpype/hosts/maya/plugins/publish/validate_node_no_ghosting.py index 671c744a22..38f3ab1e68 100644 --- a/openpype/hosts/maya/plugins/publish/validate_node_no_ghosting.py +++ b/openpype/hosts/maya/plugins/publish/validate_node_no_ghosting.py @@ -32,7 +32,10 @@ class ValidateNodeNoGhosting(pyblish.api.InstancePlugin): nodes = cmds.ls(instance, long=True, type=['transform', 'shape']) invalid = [] for node in nodes: - for attr, required_value in cls._attributes.iteritems(): + _iteritems = getattr( + cls._attributes, "iteritems", cls._attributes.items + ) + for attr, required_value in _iteritems(): if cmds.attributeQuery(attr, node=node, exists=True): value = cmds.getAttr('{0}.{1}'.format(node, attr)) diff --git a/openpype/hosts/maya/plugins/publish/validate_shape_render_stats.py b/openpype/hosts/maya/plugins/publish/validate_shape_render_stats.py index 667a1f13be..714451bb98 100644 --- a/openpype/hosts/maya/plugins/publish/validate_shape_render_stats.py +++ b/openpype/hosts/maya/plugins/publish/validate_shape_render_stats.py @@ -33,7 +33,8 @@ class ValidateShapeRenderStats(pyblish.api.Validator): shapes = cmds.ls(instance, long=True, type='surfaceShape') invalid = [] for shape in shapes: - for attr, default_value in cls.defaults.iteritems(): + _iteritems = getattr(cls.defaults, "iteritems", cls.defaults.items) + for attr, default_value in _iteritems(): if cmds.attributeQuery(attr, node=shape, exists=True): value = cmds.getAttr('{}.{}'.format(shape, attr)) if value != default_value: @@ -52,7 +53,8 @@ class ValidateShapeRenderStats(pyblish.api.Validator): @classmethod def repair(cls, instance): for shape in cls.get_invalid(instance): - for attr, default_value in cls.defaults.iteritems(): + _iteritems = getattr(cls.defaults, "iteritems", cls.defaults.items) + for attr, default_value in _iteritems(): if cmds.attributeQuery(attr, node=shape, exists=True): plug = '{0}.{1}'.format(shape, attr) diff --git a/openpype/hosts/nuke/api/menu.py b/openpype/hosts/nuke/api/menu.py index 87990c5e92..3e74893589 100644 --- a/openpype/hosts/nuke/api/menu.py +++ b/openpype/hosts/nuke/api/menu.py @@ -84,6 +84,12 @@ def install(): ) log.debug("Adding menu item: {}".format(name)) + # Add experimental tools action + menu.addSeparator() + menu.addCommand( + "Experimental tools...", + host_tools.show_experimental_tools_dialog + ) # adding shortcuts add_shortcuts_from_presets() diff --git a/openpype/hosts/photoshop/plugins/load/load_image.py b/openpype/hosts/photoshop/plugins/load/load_image.py index d043323768..981a1ed204 100644 --- a/openpype/hosts/photoshop/plugins/load/load_image.py +++ b/openpype/hosts/photoshop/plugins/load/load_image.py @@ -6,7 +6,6 @@ from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name stub = photoshop.stub() - class ImageLoader(api.Loader): """Load images @@ -21,7 +20,7 @@ class ImageLoader(api.Loader): context["asset"]["name"], name) with photoshop.maintained_selection(): - layer = stub.import_smart_object(self.fname, layer_name) + layer = self.import_layer(self.fname, layer_name) self[:] = [layer] namespace = namespace or layer_name @@ -45,8 +44,9 @@ class ImageLoader(api.Loader): layer_name = "{}_{}".format(context["asset"], context["subset"]) # switching assets if namespace_from_container != layer_name: - layer_name = self._get_unique_layer_name(context["asset"], - context["subset"]) + layer_name = get_unique_layer_name(stub.get_layers(), + context["asset"], + context["subset"]) else: # switching version - keep same name layer_name = container["namespace"] @@ -72,3 +72,6 @@ class ImageLoader(api.Loader): def switch(self, container, representation): self.update(container, representation) + + def import_layer(self, file_name, layer_name): + return stub.import_smart_object(file_name, layer_name) diff --git a/openpype/hosts/photoshop/plugins/load/load_reference.py b/openpype/hosts/photoshop/plugins/load/load_reference.py new file mode 100644 index 0000000000..0cb4e4a69f --- /dev/null +++ b/openpype/hosts/photoshop/plugins/load/load_reference.py @@ -0,0 +1,82 @@ +import re + +from avalon import api, photoshop + +from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name + +stub = photoshop.stub() + + +class ReferenceLoader(api.Loader): + """Load reference images + + Stores the imported asset in a container named after the asset. + + Inheriting from 'load_image' didn't work because of + "Cannot write to closing transport", possible refactor. + """ + + families = ["image", "render"] + representations = ["*"] + + def load(self, context, name=None, namespace=None, data=None): + layer_name = get_unique_layer_name(stub.get_layers(), + context["asset"]["name"], + name) + with photoshop.maintained_selection(): + layer = self.import_layer(self.fname, layer_name) + + self[:] = [layer] + namespace = namespace or layer_name + + return photoshop.containerise( + name, + namespace, + layer, + context, + self.__class__.__name__ + ) + + def update(self, container, representation): + """ Switch asset or change version """ + layer = container.pop("layer") + + context = representation.get("context", {}) + + namespace_from_container = re.sub(r'_\d{3}$', '', + container["namespace"]) + layer_name = "{}_{}".format(context["asset"], context["subset"]) + # switching assets + if namespace_from_container != layer_name: + layer_name = get_unique_layer_name(stub.get_layers(), + context["asset"], + context["subset"]) + else: # switching version - keep same name + layer_name = container["namespace"] + + path = api.get_representation_path(representation) + with photoshop.maintained_selection(): + stub.replace_smart_object( + layer, path, layer_name + ) + + stub.imprint( + layer, {"representation": str(representation["_id"])} + ) + + def remove(self, container): + """ + Removes element from scene: deletes layer + removes from Headline + Args: + container (dict): container to be removed - used to get layer_id + """ + layer = container.pop("layer") + stub.imprint(layer, {}) + stub.delete_layer(layer.id) + + def switch(self, container, representation): + self.update(container, representation) + + def import_layer(self, file_name, layer_name): + return stub.import_smart_object(file_name, layer_name, + as_reference=True) diff --git a/openpype/hosts/photoshop/plugins/publish/closePS.py b/openpype/hosts/photoshop/plugins/publish/closePS.py new file mode 100644 index 0000000000..19994a0db8 --- /dev/null +++ b/openpype/hosts/photoshop/plugins/publish/closePS.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +"""Close PS after publish. For Webpublishing only.""" +import os + +import pyblish.api + +from avalon import photoshop + + +class ClosePS(pyblish.api.ContextPlugin): + """Close PS after publish. For Webpublishing only. + """ + + order = pyblish.api.IntegratorOrder + 14 + label = "Close PS" + optional = True + active = True + + hosts = ["photoshop"] + + def process(self, context): + self.log.info("ClosePS") + if not os.environ.get("IS_HEADLESS"): + return + + stub = photoshop.stub() + self.log.info("Shutting down PS") + stub.save() + stub.close() + self.log.info("PS closed") diff --git a/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py new file mode 100644 index 0000000000..12f9fa5ab5 --- /dev/null +++ b/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py @@ -0,0 +1,135 @@ +import pyblish.api +import os +import re + +from avalon import photoshop +from openpype.lib import prepare_template_data +from openpype.lib.plugin_tools import parse_json + + +class CollectRemoteInstances(pyblish.api.ContextPlugin): + """Gather instances configured color code of a layer. + + Used in remote publishing when artists marks publishable layers by color- + coding. + + Identifier: + id (str): "pyblish.avalon.instance" + """ + order = pyblish.api.CollectorOrder + 0.100 + + label = "Instances" + order = pyblish.api.CollectorOrder + hosts = ["photoshop"] + + # configurable by Settings + color_code_mapping = [] + + def process(self, context): + self.log.info("CollectRemoteInstances") + self.log.info("mapping:: {}".format(self.color_code_mapping)) + if not os.environ.get("IS_HEADLESS"): + self.log.debug("Not headless publishing, skipping.") + return + + # parse variant if used in webpublishing, comes from webpublisher batch + batch_dir = os.environ.get("OPENPYPE_PUBLISH_DATA") + variant = "Main" + if batch_dir and os.path.exists(batch_dir): + # TODO check if batch manifest is same as tasks manifests + task_data = parse_json(os.path.join(batch_dir, + "manifest.json")) + if not task_data: + raise ValueError( + "Cannot parse batch meta in {} folder".format(batch_dir)) + variant = task_data["variant"] + + stub = photoshop.stub() + layers = stub.get_layers() + + instance_names = [] + for layer in layers: + self.log.info("Layer:: {}".format(layer)) + resolved_family, resolved_subset_template = self._resolve_mapping( + layer + ) + self.log.info("resolved_family {}".format(resolved_family)) + self.log.info("resolved_subset_template {}".format( + resolved_subset_template)) + + if not resolved_subset_template or not resolved_family: + self.log.debug("!!! Not marked, skip") + continue + + if layer.parents: + self.log.debug("!!! Not a top layer, skip") + continue + + instance = context.create_instance(layer.name) + instance.append(layer) + instance.data["family"] = resolved_family + instance.data["publish"] = layer.visible + instance.data["asset"] = context.data["assetEntity"]["name"] + instance.data["task"] = context.data["taskType"] + + fill_pairs = { + "variant": variant, + "family": instance.data["family"], + "task": instance.data["task"], + "layer": layer.name + } + subset = resolved_subset_template.format( + **prepare_template_data(fill_pairs)) + instance.data["subset"] = subset + + instance_names.append(layer.name) + + # Produce diagnostic message for any graphical + # user interface interested in visualising it. + self.log.info("Found: \"%s\" " % instance.data["name"]) + self.log.info("instance: {} ".format(instance.data)) + + if len(instance_names) != len(set(instance_names)): + self.log.warning("Duplicate instances found. " + + "Remove unwanted via SubsetManager") + + def _resolve_mapping(self, layer): + """Matches 'layer' color code and name to mapping. + + If both color code AND name regex is configured, BOTH must be valid + If layer matches to multiple mappings, only first is used! + """ + family_list = [] + family = None + subset_name_list = [] + resolved_subset_template = None + for mapping in self.color_code_mapping: + if mapping["color_code"] and \ + layer.color_code not in mapping["color_code"]: + continue + + if mapping["layer_name_regex"] and \ + not any(re.search(pattern, layer.name) + for pattern in mapping["layer_name_regex"]): + continue + + family_list.append(mapping["family"]) + subset_name_list.append(mapping["subset_template_name"]) + + if len(subset_name_list) > 1: + self.log.warning("Multiple mappings found for '{}'". + format(layer.name)) + self.log.warning("Only first subset name template used!") + subset_name_list[:] = subset_name_list[0] + + if len(family_list) > 1: + self.log.warning("Multiple mappings found for '{}'". + format(layer.name)) + self.log.warning("Only first family used!") + family_list[:] = family_list[0] + if subset_name_list: + resolved_subset_template = subset_name_list.pop() + if family_list: + family = family_list.pop() + + return family, resolved_subset_template diff --git a/openpype/hosts/photoshop/plugins/publish/extract_image.py b/openpype/hosts/photoshop/plugins/publish/extract_image.py index 87574d1269..ae9892e290 100644 --- a/openpype/hosts/photoshop/plugins/publish/extract_image.py +++ b/openpype/hosts/photoshop/plugins/publish/extract_image.py @@ -12,7 +12,7 @@ class ExtractImage(openpype.api.Extractor): label = "Extract Image" hosts = ["photoshop"] - families = ["image"] + families = ["image", "background"] formats = ["png", "jpg"] def process(self, instance): diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_sources.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_sources.py new file mode 100644 index 0000000000..eec675e97f --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_sources.py @@ -0,0 +1,37 @@ +import pyblish.api +import openpype.api + +import os + + +class ValidateSources(pyblish.api.InstancePlugin): + """Validates source files. + + Loops through all 'files' in 'stagingDir' if actually exist. They might + got deleted between starting of SP and now. + + """ + + order = openpype.api.ValidateContentsOrder + label = "Check source files" + + optional = True # only for unforeseeable cases + + hosts = ["standalonepublisher"] + + def process(self, instance): + self.log.info("instance {}".format(instance.data)) + + for repre in instance.data.get("representations") or []: + files = [] + if isinstance(repre["files"], str): + files.append(repre["files"]) + else: + files = list(repre["files"]) + + for file_name in files: + source_file = os.path.join(repre["stagingDir"], + file_name) + + if not os.path.exists(source_file): + raise ValueError("File {} not found".format(source_file)) diff --git a/openpype/hosts/unreal/api/lib.py b/openpype/hosts/unreal/api/lib.py index 7e34c3ff15..c0fafbb667 100644 --- a/openpype/hosts/unreal/api/lib.py +++ b/openpype/hosts/unreal/api/lib.py @@ -253,6 +253,7 @@ def create_unreal_project(project_name: str, "Plugins": [ {"Name": "PythonScriptPlugin", "Enabled": True}, {"Name": "EditorScriptingUtilities", "Enabled": True}, + {"Name": "SequencerScripting", "Enabled": True}, {"Name": "Avalon", "Enabled": True} ] } diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index 01b8b6bc05..880dba5cfb 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -6,7 +6,9 @@ from pathlib import Path from openpype.lib import ( PreLaunchHook, ApplicationLaunchFailed, - ApplicationNotFound + ApplicationNotFound, + get_workdir_data, + get_workfile_template_key ) from openpype.hosts.unreal.api import lib as unreal_lib @@ -25,13 +27,46 @@ class UnrealPrelaunchHook(PreLaunchHook): self.signature = "( {} )".format(self.__class__.__name__) + def _get_work_filename(self): + # Use last workfile if was found + if self.data.get("last_workfile_path"): + last_workfile = Path(self.data.get("last_workfile_path")) + if last_workfile and last_workfile.exists(): + return last_workfile.name + + # Prepare data for fill data and for getting workfile template key + task_name = self.data["task_name"] + anatomy = self.data["anatomy"] + asset_doc = self.data["asset_doc"] + project_doc = self.data["project_doc"] + + asset_tasks = asset_doc.get("data", {}).get("tasks") or {} + task_info = asset_tasks.get(task_name) or {} + task_type = task_info.get("type") + + workdir_data = get_workdir_data( + project_doc, asset_doc, task_name, self.host_name + ) + # QUESTION raise exception if version is part of filename template? + workdir_data["version"] = 1 + workdir_data["ext"] = "uproject" + + # Get workfile template key for current context + workfile_template_key = get_workfile_template_key( + task_type, + self.host_name, + project_name=project_doc["name"] + ) + # Fill templates + filled_anatomy = anatomy.format(workdir_data) + + # Return filename + return filled_anatomy[workfile_template_key]["file"] + def execute(self): """Hook entry method.""" - asset_name = self.data["asset_name"] - task_name = self.data["task_name"] workdir = self.launch_context.env["AVALON_WORKDIR"] engine_version = self.app_name.split("/")[-1].replace("-", ".") - unreal_project_name = f"{asset_name}_{task_name}" try: if int(engine_version.split(".")[0]) < 4 and \ int(engine_version.split(".")[1]) < 26: @@ -45,6 +80,8 @@ class UnrealPrelaunchHook(PreLaunchHook): # so lets keep it quite. ... + unreal_project_filename = self._get_work_filename() + unreal_project_name = os.path.splitext(unreal_project_filename)[0] # Unreal is sensitive about project names longer then 20 chars if len(unreal_project_name) > 20: self.log.warning(( @@ -89,10 +126,10 @@ class UnrealPrelaunchHook(PreLaunchHook): ue4_path = unreal_lib.get_editor_executable_path( Path(detected[engine_version])) - self.launch_context.launch_args.append(ue4_path.as_posix()) + self.launch_context.launch_args = [ue4_path.as_posix()] project_path.mkdir(parents=True, exist_ok=True) - project_file = project_path / f"{unreal_project_name}.uproject" + project_file = project_path / unreal_project_filename if not project_file.is_file(): engine_path = detected[engine_version] self.log.info(( diff --git a/openpype/hosts/unreal/plugins/create/create_camera.py b/openpype/hosts/unreal/plugins/create/create_camera.py new file mode 100644 index 0000000000..eda2b52be3 --- /dev/null +++ b/openpype/hosts/unreal/plugins/create/create_camera.py @@ -0,0 +1,43 @@ +import unreal +from unreal import EditorAssetLibrary as eal +from unreal import EditorLevelLibrary as ell + +from openpype.hosts.unreal.api.plugin import Creator +from avalon.unreal import ( + instantiate, +) + + +class CreateCamera(Creator): + """Layout output for character rigs""" + + name = "layoutMain" + label = "Camera" + family = "camera" + icon = "cubes" + + root = "/Game/Avalon/Instances" + suffix = "_INS" + + def __init__(self, *args, **kwargs): + super(CreateCamera, self).__init__(*args, **kwargs) + + def process(self): + data = self.data + + name = data["subset"] + + data["level"] = ell.get_editor_world().get_path_name() + + if not eal.does_directory_exist(self.root): + eal.make_directory(self.root) + + factory = unreal.LevelSequenceFactoryNew() + tools = unreal.AssetToolsHelpers().get_asset_tools() + tools.create_asset(name, f"{self.root}/{name}", None, factory) + + asset_name = f"{self.root}/{name}/{name}.{name}" + + data["members"] = [asset_name] + + instantiate(f"{self.root}", name, data, None, self.suffix) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py new file mode 100644 index 0000000000..b2b25eec73 --- /dev/null +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -0,0 +1,206 @@ +import os + +from avalon import api, io, pipeline +from avalon.unreal import lib +from avalon.unreal import pipeline as unreal_pipeline +import unreal + + +class CameraLoader(api.Loader): + """Load Unreal StaticMesh from FBX""" + + families = ["camera"] + label = "Load Camera" + representations = ["fbx"] + icon = "cube" + color = "orange" + + def load(self, context, name, namespace, data): + """ + Load and containerise representation into Content Browser. + + This is two step process. First, import FBX to temporary path and + then call `containerise()` on it - this moves all content to new + directory and then it will create AssetContainer there and imprint it + with metadata. This will mark this path as container. + + Args: + context (dict): application context + name (str): subset name + namespace (str): in Unreal this is basically path to container. + This is not passed here, so namespace is set + by `containerise()` because only then we know + real path. + data (dict): Those would be data to be imprinted. This is not used + now, data are imprinted by `containerise()`. + + Returns: + list(str): list of container content + """ + + # Create directory for asset and avalon container + root = "/Game/Avalon/Assets" + asset = context.get('asset').get('name') + suffix = "_CON" + if asset: + asset_name = "{}_{}".format(asset, name) + else: + asset_name = "{}".format(name) + + tools = unreal.AssetToolsHelpers().get_asset_tools() + + unique_number = 1 + + if unreal.EditorAssetLibrary.does_directory_exist(f"{root}/{asset}"): + asset_content = unreal.EditorAssetLibrary.list_assets( + f"{root}/{asset}", recursive=False, include_folder=True + ) + + # Get highest number to make a unique name + folders = [a for a in asset_content + if a[-1] == "/" and f"{name}_" in a] + f_numbers = [] + for f in folders: + # Get number from folder name. Splits the string by "_" and + # removes the last element (which is a "/"). + f_numbers.append(int(f.split("_")[-1][:-1])) + f_numbers.sort() + if not f_numbers: + unique_number = 1 + else: + unique_number = f_numbers[-1] + 1 + + asset_dir, container_name = tools.create_unique_asset_name( + f"{root}/{asset}/{name}_{unique_number:02d}", suffix="") + + container_name += suffix + + unreal.EditorAssetLibrary.make_directory(asset_dir) + + sequence = tools.create_asset( + asset_name=asset_name, + package_path=asset_dir, + asset_class=unreal.LevelSequence, + factory=unreal.LevelSequenceFactoryNew() + ) + + io_asset = io.Session["AVALON_ASSET"] + asset_doc = io.find_one({ + "type": "asset", + "name": io_asset + }) + + data = asset_doc.get("data") + + if data: + sequence.set_display_rate(unreal.FrameRate(data.get("fps"), 1.0)) + sequence.set_playback_start(data.get("frameStart")) + sequence.set_playback_end(data.get("frameEnd")) + + settings = unreal.MovieSceneUserImportFBXSettings() + settings.set_editor_property('reduce_keys', False) + + unreal.SequencerTools.import_fbx( + unreal.EditorLevelLibrary.get_editor_world(), + sequence, + sequence.get_bindings(), + settings, + self.fname + ) + + # Create Asset Container + lib.create_avalon_container(container=container_name, path=asset_dir) + + data = { + "schema": "openpype:container-2.0", + "id": pipeline.AVALON_CONTAINER_ID, + "asset": asset, + "namespace": asset_dir, + "container_name": container_name, + "asset_name": asset_name, + "loader": str(self.__class__.__name__), + "representation": context["representation"]["_id"], + "parent": context["representation"]["parent"], + "family": context["representation"]["context"]["family"] + } + unreal_pipeline.imprint( + "{}/{}".format(asset_dir, container_name), data) + + asset_content = unreal.EditorAssetLibrary.list_assets( + asset_dir, recursive=True, include_folder=True + ) + + for a in asset_content: + unreal.EditorAssetLibrary.save_asset(a) + + return asset_content + + def update(self, container, representation): + path = container["namespace"] + + ar = unreal.AssetRegistryHelpers.get_asset_registry() + tools = unreal.AssetToolsHelpers().get_asset_tools() + + asset_content = unreal.EditorAssetLibrary.list_assets( + path, recursive=False, include_folder=False + ) + asset_name = "" + for a in asset_content: + asset = ar.get_asset_by_object_path(a) + if a.endswith("_CON"): + loaded_asset = unreal.EditorAssetLibrary.load_asset(a) + unreal.EditorAssetLibrary.set_metadata_tag( + loaded_asset, "representation", str(representation["_id"]) + ) + unreal.EditorAssetLibrary.set_metadata_tag( + loaded_asset, "parent", str(representation["parent"]) + ) + asset_name = unreal.EditorAssetLibrary.get_metadata_tag( + loaded_asset, "asset_name" + ) + elif asset.asset_class == "LevelSequence": + unreal.EditorAssetLibrary.delete_asset(a) + + sequence = tools.create_asset( + asset_name=asset_name, + package_path=path, + asset_class=unreal.LevelSequence, + factory=unreal.LevelSequenceFactoryNew() + ) + + io_asset = io.Session["AVALON_ASSET"] + asset_doc = io.find_one({ + "type": "asset", + "name": io_asset + }) + + data = asset_doc.get("data") + + if data: + sequence.set_display_rate(unreal.FrameRate(data.get("fps"), 1.0)) + sequence.set_playback_start(data.get("frameStart")) + sequence.set_playback_end(data.get("frameEnd")) + + settings = unreal.MovieSceneUserImportFBXSettings() + settings.set_editor_property('reduce_keys', False) + + unreal.SequencerTools.import_fbx( + unreal.EditorLevelLibrary.get_editor_world(), + sequence, + sequence.get_bindings(), + settings, + str(representation["data"]["path"]) + ) + + def remove(self, container): + path = container["namespace"] + parent_path = os.path.dirname(path) + + unreal.EditorAssetLibrary.delete_directory(path) + + asset_content = unreal.EditorAssetLibrary.list_assets( + parent_path, recursive=False, include_folder=True + ) + + if len(asset_content) == 0: + unreal.EditorAssetLibrary.delete_directory(parent_path) diff --git a/openpype/hosts/unreal/plugins/publish/extract_camera.py b/openpype/hosts/unreal/plugins/publish/extract_camera.py new file mode 100644 index 0000000000..10862fc0ef --- /dev/null +++ b/openpype/hosts/unreal/plugins/publish/extract_camera.py @@ -0,0 +1,54 @@ +import os + +import unreal +from unreal import EditorAssetLibrary as eal +from unreal import EditorLevelLibrary as ell + +import openpype.api + + +class ExtractCamera(openpype.api.Extractor): + """Extract a camera.""" + + label = "Extract Camera" + hosts = ["unreal"] + families = ["camera"] + optional = True + + def process(self, instance): + # Define extract output file path + stagingdir = self.staging_dir(instance) + fbx_filename = "{}.fbx".format(instance.name) + + # Perform extraction + self.log.info("Performing extraction..") + + # Check if the loaded level is the same of the instance + current_level = ell.get_editor_world().get_path_name() + assert current_level == instance.data.get("level"), \ + "Wrong level loaded" + + for member in instance[:]: + data = eal.find_asset_data(member) + if data.asset_class == "LevelSequence": + ar = unreal.AssetRegistryHelpers.get_asset_registry() + sequence = ar.get_asset_by_object_path(member).get_asset() + unreal.SequencerTools.export_fbx( + ell.get_editor_world(), + sequence, + sequence.get_bindings(), + unreal.FbxExportOption(), + os.path.join(stagingdir, fbx_filename) + ) + break + + if "representations" not in instance.data: + instance.data["representations"] = [] + + fbx_representation = { + 'name': 'fbx', + 'ext': 'fbx', + 'files': fbx_filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(fbx_representation) diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py index 7e9b98956a..ecd65ebae4 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py @@ -15,6 +15,7 @@ import tempfile import pyblish.api from avalon import io from openpype.lib import prepare_template_data +from openpype.lib.plugin_tools import parse_json, get_batch_asset_task_info class CollectPublishedFiles(pyblish.api.ContextPlugin): @@ -33,22 +34,6 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): # from Settings task_type_to_family = {} - def _load_json(self, path): - path = path.strip('\"') - assert os.path.isfile(path), ( - "Path to json file doesn't exist. \"{}\"".format(path) - ) - data = None - with open(path, "r") as json_file: - try: - data = json.load(json_file) - except Exception as exc: - self.log.error( - "Error loading json: " - "{} - Exception: {}".format(path, exc) - ) - return data - def _process_batch(self, dir_url): task_subfolders = [ os.path.join(dir_url, o) @@ -56,22 +41,15 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): if os.path.isdir(os.path.join(dir_url, o))] self.log.info("task_sub:: {}".format(task_subfolders)) for task_dir in task_subfolders: - task_data = self._load_json(os.path.join(task_dir, - "manifest.json")) + task_data = parse_json(os.path.join(task_dir, + "manifest.json")) self.log.info("task_data:: {}".format(task_data)) ctx = task_data["context"] - task_type = "default_task_type" - task_name = None - if ctx["type"] == "task": - items = ctx["path"].split('/') - asset = items[-2] - os.environ["AVALON_TASK"] = ctx["name"] - task_name = ctx["name"] - task_type = ctx["attributes"]["type"] - else: - asset = ctx["name"] - os.environ["AVALON_TASK"] = "" + asset, task_name, task_type = get_batch_asset_task_info(ctx) + + if task_name: + os.environ["AVALON_TASK"] = task_name is_sequence = len(task_data["files"]) > 1 @@ -261,7 +239,7 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): assert batch_dir, ( "Missing `OPENPYPE_PUBLISH_DATA`") - assert batch_dir, \ + assert os.path.exists(batch_dir), \ "Folder {} doesn't exist".format(batch_dir) project_name = os.environ.get("AVALON_PROJECT") diff --git a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py index 0014d1b344..920ed042dc 100644 --- a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py +++ b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py @@ -11,6 +11,7 @@ from avalon.api import AvalonMongoDB from openpype.lib import OpenPypeMongoConnection from openpype_modules.avalon_apps.rest_api import _RestApiEndpoint +from openpype.lib.plugin_tools import parse_json from openpype.lib import PypeLogger @@ -175,6 +176,9 @@ class TaskNode(Node): class WebpublisherBatchPublishEndpoint(_RestApiEndpoint): """Triggers headless publishing of batch.""" async def post(self, request) -> Response: + # for postprocessing in host, currently only PS + host_map = {"photoshop": [".psd", ".psb"]} + output = {} log.info("WebpublisherBatchPublishEndpoint called") content = await request.json() @@ -182,10 +186,44 @@ class WebpublisherBatchPublishEndpoint(_RestApiEndpoint): batch_path = os.path.join(self.resource.upload_dir, content["batch"]) + add_args = { + "host": "webpublisher", + "project": content["project_name"], + "user": content["user"] + } + + command = "remotepublish" + + if content.get("studio_processing"): + log.info("Post processing called") + + batch_data = parse_json(os.path.join(batch_path, "manifest.json")) + if not batch_data: + raise ValueError( + "Cannot parse batch meta in {} folder".format(batch_path)) + task_dir_name = batch_data["tasks"][0] + task_data = parse_json(os.path.join(batch_path, task_dir_name, + "manifest.json")) + if not task_data: + raise ValueError( + "Cannot parse batch meta in {} folder".format(task_data)) + + command = "remotepublishfromapp" + for host, extensions in host_map.items(): + for ext in extensions: + for file_name in task_data["files"]: + if ext in file_name: + add_args["host"] = host + break + + if not add_args.get("host"): + raise ValueError( + "Couldn't discern host from {}".format(task_data["files"])) + openpype_app = self.resource.executable args = [ openpype_app, - 'remotepublish', + command, batch_path ] @@ -193,12 +231,6 @@ class WebpublisherBatchPublishEndpoint(_RestApiEndpoint): msg = "Non existent OpenPype executable {}".format(openpype_app) raise RuntimeError(msg) - add_args = { - "host": "webpublisher", - "project": content["project_name"], - "user": content["user"] - } - for key, value in add_args.items(): args.append("--{}".format(key)) args.append(value) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index cc8cb8e7be..b9bcecd3a0 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -461,13 +461,8 @@ class ApplicationExecutable: # On MacOS check if exists path to executable when ends with `.app` # - it is common that path will lead to "/Applications/Blender" but # real path is "/Applications/Blender.app" - if ( - platform.system().lower() == "darwin" - and not os.path.exists(executable) - ): - _executable = executable + ".app" - if os.path.exists(_executable): - executable = _executable + if platform.system().lower() == "darwin": + executable = self.macos_executable_prep(executable) self.executable_path = executable @@ -477,6 +472,45 @@ class ApplicationExecutable: def __repr__(self): return "<{}> {}".format(self.__class__.__name__, self.executable_path) + @staticmethod + def macos_executable_prep(executable): + """Try to find full path to executable file. + + Real executable is stored in '*.app/Contents/MacOS/'. + + Having path to '*.app' gives ability to read it's plist info and + use "CFBundleExecutable" key from plist to know what is "executable." + + Plist is stored in '*.app/Contents/Info.plist'. + + This is because some '*.app' directories don't have same permissions + as real executable. + """ + # Try to find if there is `.app` file + if not os.path.exists(executable): + _executable = executable + ".app" + if os.path.exists(_executable): + executable = _executable + + # Try to find real executable if executable has `Contents` subfolder + contents_dir = os.path.join(executable, "Contents") + if os.path.exists(contents_dir): + executable_filename = None + # Load plist file and check for bundle executable + plist_filepath = os.path.join(contents_dir, "Info.plist") + if os.path.exists(plist_filepath): + import plistlib + + parsed_plist = plistlib.readPlist(plist_filepath) + executable_filename = parsed_plist.get("CFBundleExecutable") + + if executable_filename: + executable = os.path.join( + contents_dir, "MacOS", executable_filename + ) + + return executable + def as_args(self): return [self.executable_path] diff --git a/openpype/lib/plugin_tools.py b/openpype/lib/plugin_tools.py index 4eabb4d1ca..47e6641731 100644 --- a/openpype/lib/plugin_tools.py +++ b/openpype/lib/plugin_tools.py @@ -487,3 +487,48 @@ def should_decompress(file_url): "compression: \"dwab\"" in output return False + + +def parse_json(path): + """Parses json file at 'path' location + + Returns: + (dict) or None if unparsable + Raises: + AsssertionError if 'path' doesn't exist + """ + path = path.strip('\"') + assert os.path.isfile(path), ( + "Path to json file doesn't exist. \"{}\"".format(path) + ) + data = None + with open(path, "r") as json_file: + try: + data = json.load(json_file) + except Exception as exc: + log.error( + "Error loading json: " + "{} - Exception: {}".format(path, exc) + ) + return data + + +def get_batch_asset_task_info(ctx): + """Parses context data from webpublisher's batch metadata + + Returns: + (tuple): asset, task_name (Optional), task_type + """ + task_type = "default_task_type" + task_name = None + asset = None + + if ctx["type"] == "task": + items = ctx["path"].split('/') + asset = items[-2] + task_name = ctx["name"] + task_type = ctx["attributes"]["type"] + else: + asset = ctx["name"] + + return asset, task_name, task_type diff --git a/openpype/lib/remote_publish.py b/openpype/lib/remote_publish.py new file mode 100644 index 0000000000..51007cfad2 --- /dev/null +++ b/openpype/lib/remote_publish.py @@ -0,0 +1,159 @@ +import os +from datetime import datetime +import sys +from bson.objectid import ObjectId + +import pyblish.util +import pyblish.api + +from openpype import uninstall +from openpype.lib.mongo import OpenPypeMongoConnection + + +def get_webpublish_conn(): + """Get connection to OP 'webpublishes' collection.""" + mongo_client = OpenPypeMongoConnection.get_mongo_client() + database_name = os.environ["OPENPYPE_DATABASE_NAME"] + return mongo_client[database_name]["webpublishes"] + + +def start_webpublish_log(dbcon, batch_id, user): + """Start new log record for 'batch_id' + + Args: + dbcon (OpenPypeMongoConnection) + batch_id (str) + user (str) + Returns + (ObjectId) from DB + """ + return dbcon.insert_one({ + "batch_id": batch_id, + "start_date": datetime.now(), + "user": user, + "status": "in_progress" + }).inserted_id + + +def publish_and_log(dbcon, _id, log, close_plugin_name=None): + """Loops through all plugins, logs ok and fails into OP DB. + + Args: + dbcon (OpenPypeMongoConnection) + _id (str) + log (OpenPypeLogger) + close_plugin_name (str): name of plugin with responsibility to + close host app + """ + # Error exit as soon as any error occurs. + error_format = "Failed {plugin.__name__}: {error} -- {error.traceback}" + + close_plugin = _get_close_plugin(close_plugin_name, log) + + if isinstance(_id, str): + _id = ObjectId(_id) + + log_lines = [] + for result in pyblish.util.publish_iter(): + for record in result["records"]: + log_lines.append("{}: {}".format( + result["plugin"].label, record.msg)) + + if result["error"]: + log.error(error_format.format(**result)) + uninstall() + log_lines.append(error_format.format(**result)) + dbcon.update_one( + {"_id": _id}, + {"$set": + { + "finish_date": datetime.now(), + "status": "error", + "log": os.linesep.join(log_lines) + + }} + ) + if close_plugin: # close host app explicitly after error + context = pyblish.api.Context() + close_plugin().process(context) + sys.exit(1) + else: + dbcon.update_one( + {"_id": _id}, + {"$set": + { + "progress": max(result["progress"], 0.95), + "log": os.linesep.join(log_lines) + }} + ) + + # final update + dbcon.update_one( + {"_id": _id}, + {"$set": + { + "finish_date": datetime.now(), + "status": "finished_ok", + "progress": 1, + "log": os.linesep.join(log_lines) + }} + ) + + +def fail_batch(_id, batches_in_progress, dbcon): + """Set current batch as failed as there are some stuck batches.""" + running_batches = [str(batch["_id"]) + for batch in batches_in_progress + if batch["_id"] != _id] + msg = "There are still running batches {}\n". \ + format("\n".join(running_batches)) + msg += "Ask admin to check them and reprocess current batch" + dbcon.update_one( + {"_id": _id}, + {"$set": + { + "finish_date": datetime.now(), + "status": "error", + "log": msg + + }} + ) + raise ValueError(msg) + + +def find_variant_key(application_manager, host): + """Searches for latest installed variant for 'host' + + Args: + application_manager (ApplicationManager) + host (str) + Returns + (string) (optional) + Raises: + (ValueError) if no variant found + """ + app_group = application_manager.app_groups.get(host) + if not app_group or not app_group.enabled: + raise ValueError("No application {} configured".format(host)) + + found_variant_key = None + # finds most up-to-date variant if any installed + for variant_key, variant in app_group.variants.items(): + for executable in variant.executables: + if executable.exists(): + found_variant_key = variant_key + + if not found_variant_key: + raise ValueError("No executable for {} found".format(host)) + + return found_variant_key + + +def _get_close_plugin(close_plugin_name, log): + if close_plugin_name: + plugins = pyblish.api.discover() + for plugin in plugins: + if plugin.__name__ == close_plugin_name: + return plugin + + log.warning("Close plugin not found, app might not close.") diff --git a/openpype/modules/default_modules/avalon_apps/avalon_app.py b/openpype/modules/default_modules/avalon_apps/avalon_app.py index d21b37e520..9e650a097e 100644 --- a/openpype/modules/default_modules/avalon_apps/avalon_app.py +++ b/openpype/modules/default_modules/avalon_apps/avalon_app.py @@ -1,6 +1,5 @@ import os import openpype -from openpype import resources from openpype.modules import OpenPypeModule from openpype_interfaces import ITrayModule @@ -52,16 +51,12 @@ class AvalonModule(OpenPypeModule, ITrayModule): def tray_init(self): # Add library tool try: - from Qt import QtGui - from avalon import style from openpype.tools.libraryloader import LibraryLoaderWindow self.libraryloader = LibraryLoaderWindow( - icon=QtGui.QIcon(resources.get_openpype_icon_filepath()), show_projects=True, show_libraries=True ) - self.libraryloader.setStyleSheet(style.load_stylesheet()) except Exception: self.log.warning( "Couldn't load Library loader tool for tray.", @@ -70,6 +65,9 @@ class AvalonModule(OpenPypeModule, ITrayModule): # Definition of Tray menu def tray_menu(self, tray_menu): + if self.libraryloader is None: + return + from Qt import QtWidgets # Actions action_library_loader = QtWidgets.QAction( @@ -87,6 +85,9 @@ class AvalonModule(OpenPypeModule, ITrayModule): return def show_library_loader(self): + if self.libraryloader is None: + return + self.libraryloader.show() # Raise and activate the window diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/action_prepare_project.py b/openpype/modules/default_modules/ftrack/event_handlers_server/action_prepare_project.py index 85317031b2..2e55be2743 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_server/action_prepare_project.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_server/action_prepare_project.py @@ -3,6 +3,7 @@ import json from avalon.api import AvalonMongoDB from openpype.api import ProjectSettings from openpype.lib import create_project +from openpype.settings import SaveWarningExc from openpype_modules.ftrack.lib import ( ServerAction, @@ -312,7 +313,6 @@ class PrepareProjectServer(ServerAction): if not in_data: return - root_values = {} root_key = "__root__" for key in tuple(in_data.keys()): @@ -392,7 +392,12 @@ class PrepareProjectServer(ServerAction): else: attributes_entity[key] = value - project_settings.save() + try: + project_settings.save() + except SaveWarningExc as exc: + self.log.info("Few warnings happened during settings save:") + for warning in exc.warnings: + self.log.info(str(warning)) # Change custom attributes on project if custom_attribute_values: diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_prepare_project.py b/openpype/modules/default_modules/ftrack/event_handlers_user/action_prepare_project.py index 87d3329179..3759bc81ac 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_prepare_project.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_user/action_prepare_project.py @@ -3,6 +3,7 @@ import json from avalon.api import AvalonMongoDB from openpype.api import ProjectSettings from openpype.lib import create_project +from openpype.settings import SaveWarningExc from openpype_modules.ftrack.lib import ( BaseAction, @@ -417,7 +418,12 @@ class PrepareProjectLocal(BaseAction): else: attributes_entity[key] = value - project_settings.save() + try: + project_settings.save() + except SaveWarningExc as exc: + self.log.info("Few warnings happened during settings save:") + for warning in exc.warnings: + self.log.info(str(warning)) # Change custom attributes on project if custom_attribute_values: diff --git a/openpype/modules/default_modules/ftrack/plugins/publish/collect_username.py b/openpype/modules/default_modules/ftrack/plugins/publish/collect_username.py index 39b7433e11..844a397066 100644 --- a/openpype/modules/default_modules/ftrack/plugins/publish/collect_username.py +++ b/openpype/modules/default_modules/ftrack/plugins/publish/collect_username.py @@ -26,14 +26,21 @@ class CollectUsername(pyblish.api.ContextPlugin): """ order = pyblish.api.CollectorOrder - 0.488 label = "Collect ftrack username" - hosts = ["webpublisher"] + hosts = ["webpublisher", "photoshop"] _context = None def process(self, context): + self.log.info("CollectUsername") + # photoshop could be triggered remotely in webpublisher fashion + if os.environ["AVALON_APP"] == "photoshop": + if not os.environ.get("IS_HEADLESS"): + self.log.debug("Regular process, skipping") + return + os.environ["FTRACK_API_USER"] = os.environ["FTRACK_BOT_API_USER"] os.environ["FTRACK_API_KEY"] = os.environ["FTRACK_BOT_API_KEY"] - self.log.info("CollectUsername") + for instance in context: email = instance.data["user_email"] self.log.info("email:: {}".format(email)) diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index 69e17a0180..c2153785a8 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -3,10 +3,18 @@ import os import sys import json -from datetime import datetime +import time from openpype.lib import PypeLogger from openpype.api import get_app_environments_for_context +from openpype.lib.plugin_tools import parse_json, get_batch_asset_task_info +from openpype.lib.remote_publish import ( + get_webpublish_conn, + start_webpublish_log, + publish_and_log, + fail_batch, + find_variant_key +) class PypeCommands: @@ -110,10 +118,116 @@ class PypeCommands: log.info("Publish finished.") uninstall() + @staticmethod + def remotepublishfromapp(project, batch_dir, host, user, targets=None): + """Opens installed variant of 'host' and run remote publish there. + + Currently implemented and tested for Photoshop where customer + wants to process uploaded .psd file and publish collected layers + from there. + + Checks if no other batches are running (status =='in_progress). If + so, it sleeps for SLEEP (this is separate process), + waits for WAIT_FOR seconds altogether. + + Requires installed host application on the machine. + + Runs publish process as user would, in automatic fashion. + """ + SLEEP = 5 # seconds for another loop check for concurrently runs + WAIT_FOR = 300 # seconds to wait for conc. runs + + from openpype import install, uninstall + from openpype.api import Logger + + log = Logger.get_logger() + + log.info("remotepublishphotoshop command") + + install() + + from openpype.lib import ApplicationManager + application_manager = ApplicationManager() + + found_variant_key = find_variant_key(application_manager, host) + + app_name = "{}/{}".format(host, found_variant_key) + + batch_data = None + if batch_dir and os.path.exists(batch_dir): + batch_data = parse_json(os.path.join(batch_dir, "manifest.json")) + + if not batch_data: + raise ValueError( + "Cannot parse batch meta in {} folder".format(batch_dir)) + + asset, task_name, _task_type = get_batch_asset_task_info( + batch_data["context"]) + + # processing from app expects JUST ONE task in batch and 1 workfile + task_dir_name = batch_data["tasks"][0] + task_data = parse_json(os.path.join(batch_dir, task_dir_name, + "manifest.json")) + + workfile_path = os.path.join(batch_dir, + task_dir_name, + task_data["files"][0]) + + print("workfile_path {}".format(workfile_path)) + + _, batch_id = os.path.split(batch_dir) + dbcon = get_webpublish_conn() + # safer to start logging here, launch might be broken altogether + _id = start_webpublish_log(dbcon, batch_id, user) + + in_progress = True + slept_times = 0 + while in_progress: + batches_in_progress = list(dbcon.find({ + "status": "in_progress" + })) + if len(batches_in_progress) > 1: + if slept_times * SLEEP >= WAIT_FOR: + fail_batch(_id, batches_in_progress, dbcon) + + print("Another batch running, sleeping for a bit") + time.sleep(SLEEP) + slept_times += 1 + else: + in_progress = False + + # must have for proper launch of app + env = get_app_environments_for_context( + project, + asset, + task_name, + app_name + ) + os.environ.update(env) + + os.environ["OPENPYPE_PUBLISH_DATA"] = batch_dir + os.environ["IS_HEADLESS"] = "true" + # must pass identifier to update log lines for a batch + os.environ["BATCH_LOG_ID"] = str(_id) + + data = { + "last_workfile_path": workfile_path, + "start_last_workfile": True + } + + launched_app = application_manager.launch(app_name, **data) + + while launched_app.poll() is None: + time.sleep(0.5) + + uninstall() + @staticmethod def remotepublish(project, batch_path, host, user, targets=None): """Start headless publishing. + Used to publish rendered assets, workfiles etc. + Publish use json from passed paths argument. Args: @@ -134,7 +248,6 @@ class PypeCommands: from openpype import install, uninstall from openpype.api import Logger - from openpype.lib import OpenPypeMongoConnection # Register target and host import pyblish.api @@ -166,62 +279,11 @@ class PypeCommands: log.info("Running publish ...") - # Error exit as soon as any error occurs. - error_format = "Failed {plugin.__name__}: {error} -- {error.traceback}" - - mongo_client = OpenPypeMongoConnection.get_mongo_client() - database_name = os.environ["OPENPYPE_DATABASE_NAME"] - dbcon = mongo_client[database_name]["webpublishes"] - _, batch_id = os.path.split(batch_path) - _id = dbcon.insert_one({ - "batch_id": batch_id, - "start_date": datetime.now(), - "user": user, - "status": "in_progress" - }).inserted_id + dbcon = get_webpublish_conn() + _id = start_webpublish_log(dbcon, batch_id, user) - log_lines = [] - for result in pyblish.util.publish_iter(): - for record in result["records"]: - log_lines.append("{}: {}".format( - result["plugin"].label, record.msg)) - - if result["error"]: - log.error(error_format.format(**result)) - uninstall() - log_lines.append(error_format.format(**result)) - dbcon.update_one( - {"_id": _id}, - {"$set": - { - "finish_date": datetime.now(), - "status": "error", - "log": os.linesep.join(log_lines) - - }} - ) - sys.exit(1) - else: - dbcon.update_one( - {"_id": _id}, - {"$set": - { - "progress": max(result["progress"], 0.95), - "log": os.linesep.join(log_lines) - }} - ) - - dbcon.update_one( - {"_id": _id}, - {"$set": - { - "finish_date": datetime.now(), - "status": "finished_ok", - "progress": 1, - "log": os.linesep.join(log_lines) - }} - ) + publish_and_log(dbcon, _id, log) log.info("Publish finished.") uninstall() diff --git a/openpype/resources/app_icons/flame.png b/openpype/resources/app_icons/flame.png new file mode 100644 index 0000000000..ba9b69e45f Binary files /dev/null and b/openpype/resources/app_icons/flame.png differ diff --git a/openpype/settings/defaults/project_anatomy/imageio.json b/openpype/settings/defaults/project_anatomy/imageio.json index 38313a3d84..25608f67c6 100644 --- a/openpype/settings/defaults/project_anatomy/imageio.json +++ b/openpype/settings/defaults/project_anatomy/imageio.json @@ -162,9 +162,7 @@ ] } ], - "customNodes": [ - - ] + "customNodes": [] }, "regexInputs": { "inputs": [ diff --git a/openpype/settings/defaults/project_settings/photoshop.json b/openpype/settings/defaults/project_settings/photoshop.json index 36c30bad6c..eb9f96e348 100644 --- a/openpype/settings/defaults/project_settings/photoshop.json +++ b/openpype/settings/defaults/project_settings/photoshop.json @@ -12,6 +12,16 @@ "optional": true, "active": true }, + "CollectRemoteInstances": { + "color_code_mapping": [ + { + "color_code": [], + "layer_name_regex": [], + "family": "", + "subset_template_name": "" + } + ] + }, "ExtractImage": { "formats": [ "png", diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index cfdeca4b87..79711f3067 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -97,6 +97,42 @@ } } }, + "flame": { + "enabled": true, + "label": "Flame", + "icon": "{}/app_icons/flame.png", + "host_name": "flame", + "environment": { + "FLAME_SCRIPT_DIRS": { + "windows": "", + "darwin": "", + "linux": "" + } + }, + "variants": { + "2021": { + "use_python_2": true, + "executables": { + "windows": [], + "darwin": [ + "/opt/Autodesk/flame_2021/bin/flame.app/Contents/MacOS/startApp" + ], + "linux": [ + "/opt/Autodesk/flame_2021/bin/flame" + ] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": {} + }, + "__dynamic_keys_labels__": { + "2021": "2021 (Testing Only)" + } + } + }, "nuke": { "enabled": true, "label": "Nuke", @@ -620,12 +656,12 @@ "FUSION_UTILITY_SCRIPTS_SOURCE_DIR": [], "FUSION_UTILITY_SCRIPTS_DIR": { "windows": "{PROGRAMDATA}/Blackmagic Design/Fusion/Scripts/Comp", - "darvin": "/Library/Application Support/Blackmagic Design/Fusion/Scripts/Comp", + "darwin": "/Library/Application Support/Blackmagic Design/Fusion/Scripts/Comp", "linux": "/opt/Fusion/Scripts/Comp" }, "PYTHON36": { "windows": "{LOCALAPPDATA}/Programs/Python/Python36", - "darvin": "~/Library/Python/3.6/bin", + "darwin": "~/Library/Python/3.6/bin", "linux": "/opt/Python/3.6/bin" }, "PYTHONPATH": [ @@ -686,22 +722,22 @@ "RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR": [], "RESOLVE_SCRIPT_API": { "windows": "{PROGRAMDATA}/Blackmagic Design/DaVinci Resolve/Support/Developer/Scripting", - "darvin": "/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting", + "darwin": "/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting", "linux": "/opt/resolve/Developer/Scripting" }, "RESOLVE_SCRIPT_LIB": { "windows": "C:/Program Files/Blackmagic Design/DaVinci Resolve/fusionscript.dll", - "darvin": "/Applications/DaVinci Resolve/DaVinci Resolve.app/Contents/Libraries/Fusion/fusionscript.so", + "darwin": "/Applications/DaVinci Resolve/DaVinci Resolve.app/Contents/Libraries/Fusion/fusionscript.so", "linux": "/opt/resolve/libs/Fusion/fusionscript.so" }, "RESOLVE_UTILITY_SCRIPTS_DIR": { "windows": "{PROGRAMDATA}/Blackmagic Design/DaVinci Resolve/Fusion/Scripts/Comp", - "darvin": "/Library/Application Support/Blackmagic Design/DaVinci Resolve/Fusion/Scripts/Comp", + "darwin": "/Library/Application Support/Blackmagic Design/DaVinci Resolve/Fusion/Scripts/Comp", "linux": "/opt/resolve/Fusion/Scripts/Comp" }, "PYTHON36_RESOLVE": { "windows": "{LOCALAPPDATA}/Programs/Python/Python36", - "darvin": "~/Library/Python/3.6/bin", + "darwin": "~/Library/Python/3.6/bin", "linux": "/opt/Python/3.6/bin" }, "PYTHONPATH": [ diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index a5e734f039..f5d832f918 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -143,6 +143,7 @@ class HostsEnumEntity(BaseEnumEntity): "aftereffects", "blender", "celaction", + "flame", "fusion", "harmony", "hiero", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json index 6f5577650c..f00bf78fe4 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json @@ -43,6 +43,61 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "is_group": true, + "key": "CollectRemoteInstances", + "label": "Collect Instances for Webpublish", + "children": [ + { + "type": "label", + "label": "Set color for publishable layers, set publishable families." + }, + { + "type": "list", + "key": "color_code_mapping", + "label": "Color code mappings", + "use_label_wrap": false, + "collapsible": false, + "object_type": { + "type": "dict", + "children": [ + { + "type": "list", + "key": "color_code", + "label": "Color codes for layers", + "object_type": "text" + }, + { + "type": "list", + "key": "layer_name_regex", + "label": "Layer name regex", + "object_type": "text" + }, + { + "type": "splitter" + }, + { + "key": "family", + "label": "Resulting family", + "type": "enum", + "enum_items": [ + { + "image": "image" + } + ] + }, + { + "type": "text", + "key": "subset_template_name", + "label": "Subset template name" + } + ] + } + } + ] + }, { "type": "dict", "collapsible": true, diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/schema_flame.json b/openpype/settings/entities/schemas/system_schema/host_settings/schema_flame.json new file mode 100644 index 0000000000..1a9d8d4716 --- /dev/null +++ b/openpype/settings/entities/schemas/system_schema/host_settings/schema_flame.json @@ -0,0 +1,39 @@ +{ + "type": "dict", + "key": "flame", + "label": "Autodesk Flame", + "collapsible": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "schema_template", + "name": "template_host_unchangables" + }, + { + "key": "environment", + "label": "Environment", + "type": "raw-json" + }, + { + "type": "dict-modifiable", + "key": "variants", + "collapsible_key": true, + "use_label_wrap": false, + "object_type": { + "type": "dict", + "collapsible": true, + "children": [ + { + "type": "schema_template", + "name": "template_host_variant_items" + } + ] + } + } + ] +} diff --git a/openpype/settings/entities/schemas/system_schema/schema_applications.json b/openpype/settings/entities/schemas/system_schema/schema_applications.json index efdd021ede..1767250aae 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_applications.json +++ b/openpype/settings/entities/schemas/system_schema/schema_applications.json @@ -9,6 +9,10 @@ "type": "schema", "name": "schema_maya" }, + { + "type": "schema", + "name": "schema_flame" + }, { "type": "schema_template", "name": "template_nuke", diff --git a/openpype/style/__init__.py b/openpype/style/__init__.py index 0d7904d133..fd39e93b5d 100644 --- a/openpype/style/__init__.py +++ b/openpype/style/__init__.py @@ -2,6 +2,8 @@ import os import json import collections from openpype import resources +import six +from .color_defs import parse_color _STYLESHEET_CACHE = None @@ -10,7 +12,71 @@ _FONT_IDS = None current_dir = os.path.dirname(os.path.abspath(__file__)) +def _get_colors_raw_data(): + """Read data file with stylesheet fill values. + + Returns: + dict: Loaded data for stylesheet. + """ + data_path = os.path.join(current_dir, "data.json") + with open(data_path, "r") as data_stream: + data = json.load(data_stream) + return data + + +def get_colors_data(): + """Only color data from stylesheet data.""" + data = _get_colors_raw_data() + return data.get("color") or {} + + +def _convert_color_values_to_objects(value): + """Parse all string values in dictionary to Color definitions. + + Recursive function calling itself if value is dictionary. + + Args: + value (dict, str): String is parsed into color definition object and + dictionary is passed into this function. + + Raises: + TypeError: If value in color data do not contain string of dictionary. + """ + if isinstance(value, dict): + output = {} + for _key, _value in value.items(): + output[_key] = _convert_color_values_to_objects(_value) + return output + + if not isinstance(value, six.string_types): + raise TypeError(( + "Unexpected type in colors data '{}'. Expected 'str' or 'dict'." + ).format(str(type(value)))) + return parse_color(value) + + +def get_objected_colors(): + """Colors parsed from stylesheet data into color definitions. + + Returns: + dict: Parsed color objects by keys in data. + """ + colors_data = get_colors_data() + output = {} + for key, value in colors_data.items(): + output[key] = _convert_color_values_to_objects(value) + return output + + def _load_stylesheet(): + """Load strylesheet and trigger all related callbacks. + + Style require more than a stylesheet string. Stylesheet string + contains paths to resources which must be registered into Qt application + and load fonts used in stylesheets. + + Also replace values from stylesheet data into stylesheet text. + """ from . import qrc_resources qrc_resources.qInitResources() @@ -19,9 +85,7 @@ def _load_stylesheet(): with open(style_path, "r") as style_file: stylesheet = style_file.read() - data_path = os.path.join(current_dir, "data.json") - with open(data_path, "r") as data_stream: - data = json.load(data_stream) + data = _get_colors_raw_data() data_deque = collections.deque() for item in data.items(): @@ -44,6 +108,7 @@ def _load_stylesheet(): def _load_font(): + """Load and register fonts into Qt application.""" from Qt import QtGui global _FONT_IDS @@ -83,6 +148,7 @@ def _load_font(): def load_stylesheet(): + """Load and return OpenPype Qt stylesheet.""" global _STYLESHEET_CACHE if _STYLESHEET_CACHE is None: _STYLESHEET_CACHE = _load_stylesheet() @@ -91,4 +157,5 @@ def load_stylesheet(): def app_icon_path(): + """Path to OpenPype icon.""" return resources.get_openpype_icon_filepath() diff --git a/openpype/style/color_defs.py b/openpype/style/color_defs.py new file mode 100644 index 0000000000..0f4e145ca0 --- /dev/null +++ b/openpype/style/color_defs.py @@ -0,0 +1,391 @@ +"""Color definitions that can be used to parse strings for stylesheet. + +Each definition must have available method `get_qcolor` which should return +`QtGui.QColor` representation of the color. + +# TODO create abstract class to force this method implementation + +Usage: Some colors may be not be used only in stylesheet but is required to +use them in code too. To not hardcode these color values into code it is better +to use same colors that are available fro stylesheets. + +It is possible that some colors may not be used in stylesheet at all and thei +definition is used only in code. +""" + +import re + + +def parse_color(value): + """Parse string value of color to one of objected representation. + + Args: + value(str): Color definition usable in stylesheet. + """ + modified_value = value.strip().lower() + if modified_value.startswith("hsla"): + return HSLAColor(value) + + if modified_value.startswith("hsl"): + return HSLColor(value) + + if modified_value.startswith("#"): + return HEXColor(value) + + if modified_value.startswith("rgba"): + return RGBAColor(value) + + if modified_value.startswith("rgb"): + return RGBColor(value) + return UnknownColor(value) + + +def create_qcolor(*args): + """Create QtGui.QColor object. + + Args: + *args (tuple): It is possible to pass initialization arguments for + Qcolor. + """ + from Qt import QtGui + + return QtGui.QColor(*args) + + +def min_max_check(value, min_value, max_value): + """Validate number value if is in passed range. + + Args: + value (int, float): Value which is validated. + min_value (int, float): Minimum possible value. Validation is skipped + if passed value is None. + max_value (int, float): Maximum possible value. Validation is skipped + if passed value is None. + + Raises: + ValueError: When 'value' is out of specified range. + """ + if min_value is not None and value < min_value: + raise ValueError("Minimum expected value is '{}' got '{}'".format( + min_value, value + )) + + if max_value is not None and value > max_value: + raise ValueError("Maximum expected value is '{}' got '{}'".format( + min_value, value + )) + + +def int_validation(value, min_value=None, max_value=None): + """Validation of integer value within range. + + Args: + value (int): Validated value. + min_value (int): Minimum possible value. + max_value (int): Maximum possible value. + + Raises: + TypeError: If 'value' is not 'int' type. + """ + if not isinstance(value, int): + raise TypeError(( + "Invalid type of hue expected 'int' got {}" + ).format(str(type(value)))) + + min_max_check(value, min_value, max_value) + + +def float_validation(value, min_value=None, max_value=None): + """Validation of float value within range. + + Args: + value (float): Validated value. + min_value (float): Minimum possible value. + max_value (float): Maximum possible value. + + Raises: + TypeError: If 'value' is not 'float' type. + """ + if not isinstance(value, float): + raise TypeError(( + "Invalid type of hue expected 'int' got {}" + ).format(str(type(value)))) + + min_max_check(value, min_value, max_value) + + +class UnknownColor: + """Color from stylesheet data without known color definition. + + This is backup for unknown color definitions which may be for example + constants or definition not yet defined by class. + """ + def __init__(self, value): + self.value = value + + def get_qcolor(self): + return create_qcolor(self.value) + + +class HEXColor: + """Hex color definition. + + Hex color is defined by '#' and 3 or 6 hex values (0-F). + + Examples: + "#fff" + "#f3f3f3" + """ + regex = re.compile(r"[a-fA-F0-9]{3}(?:[a-fA-F0-9]{3})?$") + + def __init__(self, color_string): + red, green, blue = self.hex_to_rgb(color_string) + + self._color_string = color_string + self._red = red + self._green = green + self._blue = blue + + @property + def red(self): + return self._red + + @property + def green(self): + return self._green + + @property + def blue(self): + return self._blue + + def to_stylesheet_str(self): + return self._color_string + + @classmethod + def hex_to_rgb(cls, value): + """Convert hex value to rgb.""" + hex_value = value.lstrip("#") + if not cls.regex.match(hex_value): + raise ValueError("\"{}\" is not a valid HEX code.".format(value)) + + output = [] + if len(hex_value) == 3: + for char in hex_value: + output.append(int(char * 2, 16)) + else: + for idx in range(3): + start_idx = idx * 2 + output.append(int(hex_value[start_idx:start_idx + 2], 16)) + return output + + def get_qcolor(self): + return create_qcolor(self.red, self.green, self.blue) + + +class RGBColor: + """Color defined by red green and blue values. + + Each color has possible integer range 0-255. + + Examples: + "rgb(255, 127, 0)" + """ + def __init__(self, value): + modified_color = value.lower().strip() + content = modified_color.rstrip(")").lstrip("rgb(") + red_str, green_str, blue_str = ( + item.strip() for item in content.split(",") + ) + red = int(red_str) + green = int(green_str) + blue = int(blue_str) + + int_validation(red, 0, 255) + int_validation(green, 0, 255) + int_validation(blue, 0, 255) + + self._red = red + self._green = green + self._blue = blue + + @property + def red(self): + return self._red + + @property + def green(self): + return self._green + + @property + def blue(self): + return self._blue + + def get_qcolor(self): + return create_qcolor(self.red, self.green, self.blue) + + +class RGBAColor: + """Color defined by red green, blue and alpha values. + + Each color has possible integer range 0-255. + + Examples: + "rgba(255, 127, 0, 127)" + """ + def __init__(self, value): + modified_color = value.lower().strip() + content = modified_color.rstrip(")").lstrip("rgba(") + red_str, green_str, blue_str, alpha_str = ( + item.strip() for item in content.split(",") + ) + red = int(red_str) + green = int(green_str) + blue = int(blue_str) + if "." in alpha_str: + alpha = int(float(alpha_str) * 100) + else: + alpha = int(alpha_str) + + int_validation(red, 0, 255) + int_validation(green, 0, 255) + int_validation(blue, 0, 255) + int_validation(alpha, 0, 255) + + self._red = red + self._green = green + self._blue = blue + self._alpha = alpha + + @property + def red(self): + return self._red + + @property + def green(self): + return self._green + + @property + def blue(self): + return self._blue + + @property + def alpha(self): + return self._alpha + + def get_qcolor(self): + return create_qcolor(self.red, self.green, self.blue, self.alpha) + + +class HSLColor: + """Color defined by hue, saturation and light values. + + Hue is defined as integer in rage 0-360. Saturation and light can be + defined as float or percent value. + + Examples: + "hsl(27, 0.7, 0.3)" + "hsl(27, 70%, 30%)" + """ + def __init__(self, value): + modified_color = value.lower().strip() + content = modified_color.rstrip(")").lstrip("hsl(") + hue_str, sat_str, light_str = ( + item.strip() for item in content.split(",") + ) + hue = int(hue_str) % 360 + if "%" in sat_str: + sat = float(sat_str.rstrip("%")) / 100 + else: + sat = float(sat) + + if "%" in light_str: + light = float(light_str.rstrip("%")) / 100 + else: + light = float(light_str) + + int_validation(hue, 0, 360) + float_validation(sat, 0, 1) + float_validation(light, 0, 1) + + self._hue = hue + self._saturation = sat + self._light = light + + @property + def hue(self): + return self._hue + + @property + def saturation(self): + return self._saturation + + @property + def light(self): + return self._light + + def get_qcolor(self): + color = create_qcolor() + color.setHslF(self.hue / 360, self.saturation, self.light) + return color + + +class HSLAColor: + """Color defined by hue, saturation, light and alpha values. + + Hue is defined as integer in rage 0-360. Saturation and light can be + defined as float (0-1 range) or percent value(0-100%). And alpha + as float (0-1 range). + + Examples: + "hsl(27, 0.7, 0.3)" + "hsl(27, 70%, 30%)" + """ + def __init__(self, value): + modified_color = value.lower().strip() + content = modified_color.rstrip(")").lstrip("hsla(") + hue_str, sat_str, light_str, alpha_str = ( + item.strip() for item in content.split(",") + ) + hue = int(hue_str) % 360 + if "%" in sat_str: + sat = float(sat_str.rstrip("%")) / 100 + else: + sat = float(sat) + + if "%" in light_str: + light = float(light_str.rstrip("%")) / 100 + else: + light = float(light_str) + + alpha = float(alpha_str) + + int_validation(hue, 0, 360) + float_validation(sat, 0, 1) + float_validation(light, 0, 1) + float_validation(alpha, 0, 1) + + self._hue = hue + self._saturation = sat + self._light = light + self._alpha = alpha + + @property + def hue(self): + return self._hue + + @property + def saturation(self): + return self._saturation + + @property + def light(self): + return self._light + + @property + def alpha(self): + return self._alpha + + def get_qcolor(self): + color = create_qcolor() + color.setHslF(self.hue / 360, self.saturation, self.light, self.alpha) + return color diff --git a/openpype/style/data.json b/openpype/style/data.json index a58829d946..c33c2eaa5e 100644 --- a/openpype/style/data.json +++ b/openpype/style/data.json @@ -28,25 +28,36 @@ "bg": "#2C313A", "bg-inputs": "#21252B", "bg-buttons": "#434a56", - "bg-button-hover": "hsla(220, 14%, 70%, .3)", + "bg-button-hover": "rgba(168, 175, 189, 0.3)", "bg-inputs-disabled": "#2C313A", "bg-buttons-disabled": "#434a56", + "bg-splitter": "#434a56", + "bg-splitter-hover": "rgba(168, 175, 189, 0.3)", + "bg-menu-separator": "rgba(75, 83, 98, 127)", "bg-scroll-handle": "#4B5362", "bg-view": "#21252B", "bg-view-header": "#373D48", - "bg-view-hover": "hsla(220, 14%, 70%, .3)", + "bg-view-hover": "rgba(168, 175, 189, .3)", "bg-view-alternate": "rgb(36, 42, 50)", "bg-view-disabled": "#434a56", "bg-view-alternate-disabled": "#2C313A", - "bg-view-selection": "hsla(200, 60%, 60%, .4)", - "bg-view-selection-hover": "hsla(200, 60%, 60%, .8)", + "bg-view-selection": "rgba(92, 173, 214, .4)", + "bg-view-selection-hover": "rgba(92, 173, 214, .8)", "border": "#373D48", - "border-hover": "hsla(220, 14%, 70%, .3)", - "border-focus": "hsl(200, 60%, 60%)" + "border-hover": "rgba(168, 175, 189, .3)", + "border-focus": "hsl(200, 60%, 60%)", + + "loader": { + "asset-view": { + "selected": "rgba(168, 175, 189, 0.6)", + "hover": "rgba(168, 175, 189, 0.3)", + "selected-hover": "rgba(168, 175, 189, 0.7)" + } + } } } diff --git a/openpype/style/images/transparent.png b/openpype/style/images/transparent.png new file mode 100644 index 0000000000..0f2e143b39 Binary files /dev/null and b/openpype/style/images/transparent.png differ diff --git a/openpype/style/pyqt5_resources.py b/openpype/style/pyqt5_resources.py index 3dc21be12a..5a3f901840 100644 --- a/openpype/style/pyqt5_resources.py +++ b/openpype/style/pyqt5_resources.py @@ -10,19 +10,7 @@ from PyQt5 import QtCore qt_resource_data = b"\ -\x00\x00\x00\xa0\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ -\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\ -\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x14\x1c\x1f\x24\ -\xc6\x09\x17\x00\x00\x00\x24\x49\x44\x41\x54\x08\xd7\x63\x60\x40\ -\x05\xff\xcf\xc3\x58\x4c\xc8\x5c\x26\x64\x59\x26\x64\xc5\x70\x0e\ -\xa3\x21\x9c\xc3\x68\x88\x61\x1a\x0a\x00\x00\x6d\x84\x09\x75\x37\ -\x9e\xd9\x23\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ -\x00\x00\x07\x30\ +\x00\x00\x07\x06\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ \x00\x00\x0a\x00\x00\x00\x07\x08\x06\x00\x00\x00\x31\xac\xdc\x63\ @@ -80,10 +68,10 @@ qt_resource_data = b"\ \x65\x3d\x22\x73\x52\x47\x42\x20\x49\x45\x43\x36\x31\x39\x36\x36\ \x2d\x32\x2e\x31\x22\x0a\x20\x20\x20\x78\x6d\x70\x3a\x4d\x6f\x64\ \x69\x66\x79\x44\x61\x74\x65\x3d\x22\x32\x30\x32\x31\x2d\x30\x35\ -\x2d\x33\x31\x54\x31\x32\x3a\x33\x33\x3a\x31\x34\x2b\x30\x32\x3a\ +\x2d\x33\x31\x54\x31\x32\x3a\x33\x30\x3a\x31\x31\x2b\x30\x32\x3a\ \x30\x30\x22\x0a\x20\x20\x20\x78\x6d\x70\x3a\x4d\x65\x74\x61\x64\ \x61\x74\x61\x44\x61\x74\x65\x3d\x22\x32\x30\x32\x31\x2d\x30\x35\ -\x2d\x33\x31\x54\x31\x32\x3a\x33\x33\x3a\x31\x34\x2b\x30\x32\x3a\ +\x2d\x33\x31\x54\x31\x32\x3a\x33\x30\x3a\x31\x31\x2b\x30\x32\x3a\ \x30\x30\x22\x3e\x0a\x20\x20\x20\x3c\x78\x6d\x70\x4d\x4d\x3a\x48\ \x69\x73\x74\x6f\x72\x79\x3e\x0a\x20\x20\x20\x20\x3c\x72\x64\x66\ \x3a\x53\x65\x71\x3e\x0a\x20\x20\x20\x20\x20\x3c\x72\x64\x66\x3a\ @@ -94,14 +82,14 @@ qt_resource_data = b"\ \x6e\x69\x74\x79\x20\x44\x65\x73\x69\x67\x6e\x65\x72\x20\x31\x2e\ \x39\x2e\x32\x22\x0a\x20\x20\x20\x20\x20\x20\x73\x74\x45\x76\x74\ \x3a\x77\x68\x65\x6e\x3d\x22\x32\x30\x32\x31\x2d\x30\x35\x2d\x33\ -\x31\x54\x31\x32\x3a\x33\x33\x3a\x31\x34\x2b\x30\x32\x3a\x30\x30\ +\x31\x54\x31\x32\x3a\x33\x30\x3a\x31\x31\x2b\x30\x32\x3a\x30\x30\ \x22\x2f\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x72\x64\x66\x3a\x53\x65\ \x71\x3e\x0a\x20\x20\x20\x3c\x2f\x78\x6d\x70\x4d\x4d\x3a\x48\x69\ \x73\x74\x6f\x72\x79\x3e\x0a\x20\x20\x3c\x2f\x72\x64\x66\x3a\x44\ \x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x0a\x20\x3c\x2f\x72\ \x64\x66\x3a\x52\x44\x46\x3e\x0a\x3c\x2f\x78\x3a\x78\x6d\x70\x6d\ \x65\x74\x61\x3e\x0a\x3c\x3f\x78\x70\x61\x63\x6b\x65\x74\x20\x65\ -\x6e\x64\x3d\x22\x72\x22\x3f\x3e\x48\x8b\x5b\x5e\x00\x00\x01\x83\ +\x6e\x64\x3d\x22\x72\x22\x3f\x3e\x85\x9d\x9f\x08\x00\x00\x01\x83\ \x69\x43\x43\x50\x73\x52\x47\x42\x20\x49\x45\x43\x36\x31\x39\x36\ \x36\x2d\x32\x2e\x31\x00\x00\x28\x91\x75\x91\xcf\x2b\x44\x51\x14\ \xc7\x3f\x66\x68\xfc\x18\x8d\x62\x61\x31\x65\x12\x16\x42\x83\x12\ @@ -128,30 +116,15 @@ qt_resource_data = b"\ \xfd\xec\x73\x74\x07\xd1\x35\xf9\xaa\x4b\xd8\xd9\x85\x0e\x39\xef\ \x59\xf8\x06\x8e\xfd\x67\xf8\xfd\x8a\x18\x97\x00\x00\x00\x09\x70\ \x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x97\x49\x44\x41\x54\x18\x95\x6d\xcf\xb1\x6a\x02\x41\ -\x14\x85\xe1\x6f\xb7\xb6\xd0\x27\x48\x3d\x56\x69\x03\xb1\xb4\x48\ -\x3b\x6c\xa5\xf1\x39\xf6\x59\x02\x56\x42\xba\x61\x0a\x0b\x3b\x1b\ -\x1b\x6b\x41\x18\x02\x29\x6d\xe3\xbe\x82\xcd\x06\x16\xd9\xdb\xdd\ -\x9f\xff\x5c\xee\xa9\x62\x2a\x13\x4c\x73\x13\x6e\x46\x26\xa6\xf2\ -\x82\xae\x46\x8b\xdf\x98\xca\xfb\x88\xb4\xc0\x0f\xda\x1a\x5b\x74\ -\xd8\xc7\x54\xc2\x40\x9a\x63\x8f\x3f\x7c\x55\x3d\x7c\xc5\x09\x77\ -\xbc\xa1\xc2\x19\x33\x2c\x72\x13\x2e\xd5\xe0\xc2\x12\x07\x5c\x51\ -\x23\xe0\x23\x37\xe1\xa8\x4f\x0e\x7f\xda\x60\xd7\xaf\x9f\xb9\x09\ -\xdf\x63\x05\xff\xe5\x75\x4c\x65\xf5\xcc\x1f\x0d\x33\x2c\x83\xb6\ -\x06\x44\x83\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ -\x00\x00\x00\xa5\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\ -\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02\x62\x4b\x47\x44\x00\x9c\x53\x34\xfc\x5d\x00\x00\x00\x09\x70\ -\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x0b\x02\x04\x6d\ -\x98\x1b\x69\x00\x00\x00\x29\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ -\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18\x32\x32\x30\x20\x0b\x32\x1a\ -\x32\x30\x30\x42\x98\x10\x41\x46\x43\x14\x13\x50\xb5\xa3\x01\x00\ -\xd6\x10\x07\xd2\x2f\x48\xdf\x4a\x00\x00\x00\x00\x49\x45\x4e\x44\ -\xae\x42\x60\x82\ +\x00\x00\x00\x6d\x49\x44\x41\x54\x18\x95\x75\xcf\xc1\x09\xc2\x50\ +\x10\x84\xe1\xd7\x85\x07\x9b\xd0\x43\x40\xd2\x82\x78\x14\x7b\x30\ +\x57\x21\x8d\x84\x60\x3f\x62\x4b\x7a\x48\xcc\x97\x83\xfb\x30\x04\ +\xdf\x9c\x86\x7f\x67\x99\xdd\x84\x0d\xaa\x54\x10\x6a\x6c\x13\x1e\ +\xbe\xba\xfe\x09\x35\x31\x7b\xe6\x8d\x0f\x26\x1c\x17\xa1\x53\xb0\ +\x11\x87\x0c\x2f\x01\x07\xec\xb0\x0f\x3f\xe1\xbc\xae\x69\xa3\xe6\ +\x85\x77\xf8\x5b\xe9\xf0\xbb\x9f\xfa\xd2\x83\x39\xdc\xa3\x5b\xf3\ +\x19\x2e\xa8\x89\xb5\x30\xf7\x43\xa0\x00\x00\x00\x00\x49\x45\x4e\ +\x44\xae\x42\x60\x82\ \x00\x00\x00\xa0\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ @@ -164,6 +137,19 @@ qt_resource_data = b"\ \x05\x73\x3e\xc0\x58\x4c\xc8\x5c\x26\x64\x59\x26\x64\xc5\x70\x4e\ \x8a\x00\x9c\x93\x22\x80\x61\x1a\x0a\x00\x00\x29\x95\x08\xaf\x88\ \xac\xba\x34\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x00\xa6\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\ +\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x08\x15\x3b\xdc\ +\x3b\x0c\x9b\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ +\x00\x8c\x0c\x0c\x73\x3e\x20\x0b\xa4\x08\x30\x32\x30\x20\x0b\xa6\ +\x08\x30\x30\x30\x42\x98\x10\xc1\x14\x01\x14\x13\x50\xb5\xa3\x01\ +\x00\xc6\xb9\x07\x90\x5d\x66\x1f\x83\x00\x00\x00\x00\x49\x45\x4e\ +\x44\xae\x42\x60\x82\ \x00\x00\x07\xad\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ @@ -289,6 +275,45 @@ qt_resource_data = b"\ \x5e\x78\xa2\x9e\x0e\xa7\x20\x74\x47\x39\x1d\xf6\xe1\x95\x2b\xd6\ \xb1\x44\x8e\x0e\xcb\x58\xf0\x0f\x52\x8a\x79\x18\xdc\xe2\x02\x70\ \x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x00\xa6\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\ +\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x08\x15\x3b\xdc\ +\x3b\x0c\x9b\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ +\x00\x8c\x0c\x0c\x73\x3e\x20\x0b\xa4\x08\x30\x32\x30\x20\x0b\xa6\ +\x08\x30\x30\x30\x42\x98\x10\xc1\x14\x01\x14\x13\x50\xb5\xa3\x01\ +\x00\xc6\xb9\x07\x90\x5d\x66\x1f\x83\x00\x00\x00\x00\x49\x45\x4e\ +\x44\xae\x42\x60\x82\ +\x00\x00\x00\xa6\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\ +\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x14\x1d\x00\xb0\ +\xd5\x35\xa3\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ +\x06\xfe\x9f\x67\x60\x60\x42\x30\xa1\x1c\x08\x93\x81\x81\x09\xc1\ +\x64\x60\x60\x62\x60\x60\x34\x44\xe2\x20\x73\x19\x90\x8d\x40\x02\ +\x00\x64\x40\x09\x75\x86\xb3\xad\x9c\x00\x00\x00\x00\x49\x45\x4e\ +\x44\xae\x42\x60\x82\ +\x00\x00\x00\xa6\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\ +\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x08\x15\x3b\xdc\ +\x3b\x0c\x9b\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ +\x00\x8c\x0c\x0c\x73\x3e\x20\x0b\xa4\x08\x30\x32\x30\x20\x0b\xa6\ +\x08\x30\x30\x30\x42\x98\x10\xc1\x14\x01\x14\x13\x50\xb5\xa3\x01\ +\x00\xc6\xb9\x07\x90\x5d\x66\x1f\x83\x00\x00\x00\x00\x49\x45\x4e\ +\x44\xae\x42\x60\x82\ \x00\x00\x00\xa0\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ @@ -313,18 +338,26 @@ qt_resource_data = b"\ \x0d\xe6\x7c\x80\xb1\x18\x91\x05\x52\x04\xe0\x42\x08\x15\x29\x02\ \x0c\x0c\x8c\xc8\x02\x08\x95\x68\x00\x00\xac\xac\x07\x90\x4e\x65\ \x34\xac\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ -\x00\x00\x00\x9e\ +\x00\x00\x00\x45\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90\x77\x53\xde\ +\x00\x00\x00\x0c\x49\x44\x41\x54\x08\x99\x63\x60\x60\x60\x00\x00\ +\x00\x04\x00\x01\xa3\x0a\x15\xe3\x00\x00\x00\x00\x49\x45\x4e\x44\ +\xae\x42\x60\x82\ +\x00\x00\x00\xa5\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ \x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\ \x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\ +\x02\x62\x4b\x47\x44\x00\x9c\x53\x34\xfc\x5d\x00\x00\x00\x09\x70\ \x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x08\x15\x0f\xfd\ -\x8f\xf8\x2e\x00\x00\x00\x22\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ -\x0d\xfe\x9f\x87\xb1\x18\x91\x05\x18\x0d\xe1\x42\x48\x2a\x0c\x19\ -\x18\x18\x91\x05\x10\x2a\xd1\x00\x00\xca\xb5\x07\xd2\x76\xbb\xb2\ -\xc5\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x0b\x02\x04\x6d\ +\x98\x1b\x69\x00\x00\x00\x29\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ +\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18\x32\x32\x30\x20\x0b\x32\x1a\ +\x32\x30\x30\x42\x98\x10\x41\x46\x43\x14\x13\x50\xb5\xa3\x01\x00\ +\xd6\x10\x07\xd2\x2f\x48\xdf\x4a\x00\x00\x00\x00\x49\x45\x4e\x44\ +\xae\x42\x60\x82\ \x00\x00\x00\xa5\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ @@ -341,15 +374,170 @@ qt_resource_data = b"\ \x00\x00\x00\xa6\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\ +\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ \x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ \x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\ \x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x08\x15\x3b\xdc\ -\x3b\x0c\x9b\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ -\x00\x8c\x0c\x0c\x73\x3e\x20\x0b\xa4\x08\x30\x32\x30\x20\x0b\xa6\ -\x08\x30\x30\x30\x42\x98\x10\xc1\x14\x01\x14\x13\x50\xb5\xa3\x01\ -\x00\xc6\xb9\x07\x90\x5d\x66\x1f\x83\x00\x00\x00\x00\x49\x45\x4e\ +\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x14\x1f\x20\xb9\ +\x8d\x77\xe9\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ +\x06\xe6\x7c\x60\x60\x60\x42\x30\xa1\x1c\x08\x93\x81\x81\x09\xc1\ +\x64\x60\x60\x62\x60\x48\x11\x40\xe2\x20\x73\x19\x90\x8d\x40\x02\ +\x00\x23\xed\x08\xaf\x64\x9f\x0f\x15\x00\x00\x00\x00\x49\x45\x4e\ +\x44\xae\x42\x60\x82\ +\x00\x00\x00\xa5\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02\x62\x4b\x47\x44\x00\x9c\x53\x34\xfc\x5d\x00\x00\x00\x09\x70\ +\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x0b\x02\x04\x6d\ +\x98\x1b\x69\x00\x00\x00\x29\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ +\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18\x32\x32\x30\x20\x0b\x32\x1a\ +\x32\x30\x30\x42\x98\x10\x41\x46\x43\x14\x13\x50\xb5\xa3\x01\x00\ +\xd6\x10\x07\xd2\x2f\x48\xdf\x4a\x00\x00\x00\x00\x49\x45\x4e\x44\ +\xae\x42\x60\x82\ +\x00\x00\x07\x30\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x0a\x00\x00\x00\x07\x08\x06\x00\x00\x00\x31\xac\xdc\x63\ +\x00\x00\x04\xb0\x69\x54\x58\x74\x58\x4d\x4c\x3a\x63\x6f\x6d\x2e\ +\x61\x64\x6f\x62\x65\x2e\x78\x6d\x70\x00\x00\x00\x00\x00\x3c\x3f\ +\x78\x70\x61\x63\x6b\x65\x74\x20\x62\x65\x67\x69\x6e\x3d\x22\xef\ +\xbb\xbf\x22\x20\x69\x64\x3d\x22\x57\x35\x4d\x30\x4d\x70\x43\x65\ +\x68\x69\x48\x7a\x72\x65\x53\x7a\x4e\x54\x63\x7a\x6b\x63\x39\x64\ +\x22\x3f\x3e\x0a\x3c\x78\x3a\x78\x6d\x70\x6d\x65\x74\x61\x20\x78\ +\x6d\x6c\x6e\x73\x3a\x78\x3d\x22\x61\x64\x6f\x62\x65\x3a\x6e\x73\ +\x3a\x6d\x65\x74\x61\x2f\x22\x20\x78\x3a\x78\x6d\x70\x74\x6b\x3d\ +\x22\x58\x4d\x50\x20\x43\x6f\x72\x65\x20\x35\x2e\x35\x2e\x30\x22\ +\x3e\x0a\x20\x3c\x72\x64\x66\x3a\x52\x44\x46\x20\x78\x6d\x6c\x6e\ +\x73\x3a\x72\x64\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\ +\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x31\x39\x39\x39\x2f\x30\x32\ +\x2f\x32\x32\x2d\x72\x64\x66\x2d\x73\x79\x6e\x74\x61\x78\x2d\x6e\ +\x73\x23\x22\x3e\x0a\x20\x20\x3c\x72\x64\x66\x3a\x44\x65\x73\x63\ +\x72\x69\x70\x74\x69\x6f\x6e\x20\x72\x64\x66\x3a\x61\x62\x6f\x75\ +\x74\x3d\x22\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x65\ +\x78\x69\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x6e\x73\x2e\x61\ +\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x65\x78\x69\x66\x2f\x31\x2e\ +\x30\x2f\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x74\x69\ +\x66\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x6e\x73\x2e\x61\x64\ +\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x74\x69\x66\x66\x2f\x31\x2e\x30\ +\x2f\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x70\x68\x6f\ +\x74\x6f\x73\x68\x6f\x70\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x6e\ +\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x70\x68\x6f\x74\ +\x6f\x73\x68\x6f\x70\x2f\x31\x2e\x30\x2f\x22\x0a\x20\x20\x20\x20\ +\x78\x6d\x6c\x6e\x73\x3a\x78\x6d\x70\x3d\x22\x68\x74\x74\x70\x3a\ +\x2f\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x78\ +\x61\x70\x2f\x31\x2e\x30\x2f\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\ +\x6e\x73\x3a\x78\x6d\x70\x4d\x4d\x3d\x22\x68\x74\x74\x70\x3a\x2f\ +\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x78\x61\ +\x70\x2f\x31\x2e\x30\x2f\x6d\x6d\x2f\x22\x0a\x20\x20\x20\x20\x78\ +\x6d\x6c\x6e\x73\x3a\x73\x74\x45\x76\x74\x3d\x22\x68\x74\x74\x70\ +\x3a\x2f\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\ +\x78\x61\x70\x2f\x31\x2e\x30\x2f\x73\x54\x79\x70\x65\x2f\x52\x65\ +\x73\x6f\x75\x72\x63\x65\x45\x76\x65\x6e\x74\x23\x22\x0a\x20\x20\ +\x20\x65\x78\x69\x66\x3a\x50\x69\x78\x65\x6c\x58\x44\x69\x6d\x65\ +\x6e\x73\x69\x6f\x6e\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\x65\x78\ +\x69\x66\x3a\x50\x69\x78\x65\x6c\x59\x44\x69\x6d\x65\x6e\x73\x69\ +\x6f\x6e\x3d\x22\x37\x22\x0a\x20\x20\x20\x65\x78\x69\x66\x3a\x43\ +\x6f\x6c\x6f\x72\x53\x70\x61\x63\x65\x3d\x22\x31\x22\x0a\x20\x20\ +\x20\x74\x69\x66\x66\x3a\x49\x6d\x61\x67\x65\x57\x69\x64\x74\x68\ +\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\x74\x69\x66\x66\x3a\x49\x6d\ +\x61\x67\x65\x4c\x65\x6e\x67\x74\x68\x3d\x22\x37\x22\x0a\x20\x20\ +\x20\x74\x69\x66\x66\x3a\x52\x65\x73\x6f\x6c\x75\x74\x69\x6f\x6e\ +\x55\x6e\x69\x74\x3d\x22\x32\x22\x0a\x20\x20\x20\x74\x69\x66\x66\ +\x3a\x58\x52\x65\x73\x6f\x6c\x75\x74\x69\x6f\x6e\x3d\x22\x37\x32\ +\x2e\x30\x22\x0a\x20\x20\x20\x74\x69\x66\x66\x3a\x59\x52\x65\x73\ +\x6f\x6c\x75\x74\x69\x6f\x6e\x3d\x22\x37\x32\x2e\x30\x22\x0a\x20\ +\x20\x20\x70\x68\x6f\x74\x6f\x73\x68\x6f\x70\x3a\x43\x6f\x6c\x6f\ +\x72\x4d\x6f\x64\x65\x3d\x22\x33\x22\x0a\x20\x20\x20\x70\x68\x6f\ +\x74\x6f\x73\x68\x6f\x70\x3a\x49\x43\x43\x50\x72\x6f\x66\x69\x6c\ +\x65\x3d\x22\x73\x52\x47\x42\x20\x49\x45\x43\x36\x31\x39\x36\x36\ +\x2d\x32\x2e\x31\x22\x0a\x20\x20\x20\x78\x6d\x70\x3a\x4d\x6f\x64\ +\x69\x66\x79\x44\x61\x74\x65\x3d\x22\x32\x30\x32\x31\x2d\x30\x35\ +\x2d\x33\x31\x54\x31\x32\x3a\x33\x33\x3a\x31\x34\x2b\x30\x32\x3a\ +\x30\x30\x22\x0a\x20\x20\x20\x78\x6d\x70\x3a\x4d\x65\x74\x61\x64\ +\x61\x74\x61\x44\x61\x74\x65\x3d\x22\x32\x30\x32\x31\x2d\x30\x35\ +\x2d\x33\x31\x54\x31\x32\x3a\x33\x33\x3a\x31\x34\x2b\x30\x32\x3a\ +\x30\x30\x22\x3e\x0a\x20\x20\x20\x3c\x78\x6d\x70\x4d\x4d\x3a\x48\ +\x69\x73\x74\x6f\x72\x79\x3e\x0a\x20\x20\x20\x20\x3c\x72\x64\x66\ +\x3a\x53\x65\x71\x3e\x0a\x20\x20\x20\x20\x20\x3c\x72\x64\x66\x3a\ +\x6c\x69\x0a\x20\x20\x20\x20\x20\x20\x73\x74\x45\x76\x74\x3a\x61\ +\x63\x74\x69\x6f\x6e\x3d\x22\x70\x72\x6f\x64\x75\x63\x65\x64\x22\ +\x0a\x20\x20\x20\x20\x20\x20\x73\x74\x45\x76\x74\x3a\x73\x6f\x66\ +\x74\x77\x61\x72\x65\x41\x67\x65\x6e\x74\x3d\x22\x41\x66\x66\x69\ +\x6e\x69\x74\x79\x20\x44\x65\x73\x69\x67\x6e\x65\x72\x20\x31\x2e\ +\x39\x2e\x32\x22\x0a\x20\x20\x20\x20\x20\x20\x73\x74\x45\x76\x74\ +\x3a\x77\x68\x65\x6e\x3d\x22\x32\x30\x32\x31\x2d\x30\x35\x2d\x33\ +\x31\x54\x31\x32\x3a\x33\x33\x3a\x31\x34\x2b\x30\x32\x3a\x30\x30\ +\x22\x2f\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x72\x64\x66\x3a\x53\x65\ +\x71\x3e\x0a\x20\x20\x20\x3c\x2f\x78\x6d\x70\x4d\x4d\x3a\x48\x69\ +\x73\x74\x6f\x72\x79\x3e\x0a\x20\x20\x3c\x2f\x72\x64\x66\x3a\x44\ +\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x0a\x20\x3c\x2f\x72\ +\x64\x66\x3a\x52\x44\x46\x3e\x0a\x3c\x2f\x78\x3a\x78\x6d\x70\x6d\ +\x65\x74\x61\x3e\x0a\x3c\x3f\x78\x70\x61\x63\x6b\x65\x74\x20\x65\ +\x6e\x64\x3d\x22\x72\x22\x3f\x3e\x48\x8b\x5b\x5e\x00\x00\x01\x83\ +\x69\x43\x43\x50\x73\x52\x47\x42\x20\x49\x45\x43\x36\x31\x39\x36\ +\x36\x2d\x32\x2e\x31\x00\x00\x28\x91\x75\x91\xcf\x2b\x44\x51\x14\ +\xc7\x3f\x66\x68\xfc\x18\x8d\x62\x61\x31\x65\x12\x16\x42\x83\x12\ +\x1b\x8b\x99\x18\x0a\x8b\x99\x51\x7e\x6d\x66\x9e\x79\x33\x6a\xde\ +\x78\xbd\x37\xd2\x64\xab\x6c\xa7\x28\xb1\xf1\x6b\xc1\x5f\xc0\x56\ +\x59\x2b\x45\xa4\x64\xa7\xac\x89\x0d\x7a\xce\x9b\x51\x23\x99\x73\ +\x3b\xf7\x7c\xee\xf7\xde\x73\xba\xf7\x5c\x70\x44\xd3\x8a\x66\x56\ +\xfa\x41\xcb\x64\x8d\x70\x28\xe0\x9b\x99\x9d\xf3\xb9\x9e\xa8\xa2\ +\x85\x1a\x3a\xf1\xc6\x14\x53\x9f\x8c\x8c\x46\x29\x6b\xef\xb7\x54\ +\xd8\xf1\xba\xdb\xae\x55\xfe\xdc\xbf\x56\xb7\x98\x30\x15\xa8\xa8\ +\x16\x1e\x56\x74\x23\x2b\x3c\x26\x3c\xb1\x9a\xd5\x6d\xde\x12\x6e\ +\x52\x52\xb1\x45\xe1\x13\xe1\x2e\x43\x2e\x28\x7c\x63\xeb\xf1\x22\ +\x3f\xdb\x9c\x2c\xf2\xa7\xcd\x46\x34\x1c\x04\x47\x83\xb0\x2f\xf9\ +\x8b\xe3\xbf\x58\x49\x19\x9a\xb0\xbc\x9c\x36\x2d\xbd\xa2\xfc\xdc\ +\xc7\x7e\x89\x3b\x91\x99\x8e\x48\x6c\x15\xf7\x62\x12\x26\x44\x00\ +\x1f\xe3\x8c\x10\x64\x80\x5e\x86\x64\x1e\xa0\x9b\x3e\x7a\x64\x45\ +\x99\x7c\x7f\x21\x7f\x8a\x65\xc9\x55\x64\xd6\xc9\x61\xb0\x44\x92\ +\x14\x59\xba\x44\x5d\x91\xea\x09\x89\xaa\xe8\x09\x19\x69\x72\x76\ +\xff\xff\xf6\xd5\x54\xfb\xfb\x8a\xd5\xdd\x01\xa8\x7a\xb4\xac\xd7\ +\x76\x70\x6d\xc2\x57\xde\xb2\x3e\x0e\x2c\xeb\xeb\x10\x9c\x0f\x70\ +\x9e\x29\xe5\x2f\xef\xc3\xe0\x9b\xe8\xf9\x92\xd6\xb6\x07\x9e\x75\ +\x38\xbd\x28\x69\xf1\x6d\x38\xdb\x80\xe6\x7b\x3d\x66\xc4\x0a\x92\ +\x53\xdc\xa1\xaa\xf0\x72\x0c\xf5\xb3\xd0\x78\x05\xb5\xf3\xc5\x9e\ +\xfd\xec\x73\x74\x07\xd1\x35\xf9\xaa\x4b\xd8\xd9\x85\x0e\x39\xef\ +\x59\xf8\x06\x8e\xfd\x67\xf8\xfd\x8a\x18\x97\x00\x00\x00\x09\x70\ +\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x97\x49\x44\x41\x54\x18\x95\x6d\xcf\xb1\x6a\x02\x41\ +\x14\x85\xe1\x6f\xb7\xb6\xd0\x27\x48\x3d\x56\x69\x03\xb1\xb4\x48\ +\x3b\x6c\xa5\xf1\x39\xf6\x59\x02\x56\x42\xba\x61\x0a\x0b\x3b\x1b\ +\x1b\x6b\x41\x18\x02\x29\x6d\xe3\xbe\x82\xcd\x06\x16\xd9\xdb\xdd\ +\x9f\xff\x5c\xee\xa9\x62\x2a\x13\x4c\x73\x13\x6e\x46\x26\xa6\xf2\ +\x82\xae\x46\x8b\xdf\x98\xca\xfb\x88\xb4\xc0\x0f\xda\x1a\x5b\x74\ +\xd8\xc7\x54\xc2\x40\x9a\x63\x8f\x3f\x7c\x55\x3d\x7c\xc5\x09\x77\ +\xbc\xa1\xc2\x19\x33\x2c\x72\x13\x2e\xd5\xe0\xc2\x12\x07\x5c\x51\ +\x23\xe0\x23\x37\xe1\xa8\x4f\x0e\x7f\xda\x60\xd7\xaf\x9f\xb9\x09\ +\xdf\x63\x05\xff\xe5\x75\x4c\x65\xf5\xcc\x1f\x0d\x33\x2c\x83\xb6\ +\x06\x44\x83\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x00\xa0\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\ +\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x14\x1c\x1f\x24\ +\xc6\x09\x17\x00\x00\x00\x24\x49\x44\x41\x54\x08\xd7\x63\x60\x40\ +\x05\xff\xcf\xc3\x58\x4c\xc8\x5c\x26\x64\x59\x26\x64\xc5\x70\x0e\ +\xa3\x21\x9c\xc3\x68\x88\x61\x1a\x0a\x00\x00\x6d\x84\x09\x75\x37\ +\x9e\xd9\x23\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x00\xa6\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\ +\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x14\x1d\x00\xb0\ +\xd5\x35\xa3\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ +\x06\xfe\x9f\x67\x60\x60\x42\x30\xa1\x1c\x08\x93\x81\x81\x09\xc1\ +\x64\x60\x60\x62\x60\x60\x34\x44\xe2\x20\x73\x19\x90\x8d\x40\x02\ +\x00\x64\x40\x09\x75\x86\xb3\xad\x9c\x00\x00\x00\x00\x49\x45\x4e\ \x44\xae\x42\x60\x82\ \x00\x00\x07\xdd\ \x89\ @@ -479,45 +667,6 @@ qt_resource_data = b"\ \x71\x5b\x73\x5c\x40\x48\xa5\xdd\x61\x81\x0d\x9e\x6b\x8e\xff\xfd\ \xcf\x3f\xcc\x31\xe9\x01\x1c\x00\x73\x52\x2d\x71\xe4\x4a\x1b\x69\ \x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ -\x00\x00\x00\xa6\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\ -\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\ -\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x08\x15\x3b\xdc\ -\x3b\x0c\x9b\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ -\x00\x8c\x0c\x0c\x73\x3e\x20\x0b\xa4\x08\x30\x32\x30\x20\x0b\xa6\ -\x08\x30\x30\x30\x42\x98\x10\xc1\x14\x01\x14\x13\x50\xb5\xa3\x01\ -\x00\xc6\xb9\x07\x90\x5d\x66\x1f\x83\x00\x00\x00\x00\x49\x45\x4e\ -\x44\xae\x42\x60\x82\ -\x00\x00\x00\xa5\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\ -\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02\x62\x4b\x47\x44\x00\x9c\x53\x34\xfc\x5d\x00\x00\x00\x09\x70\ -\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x0b\x02\x04\x6d\ -\x98\x1b\x69\x00\x00\x00\x29\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ -\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18\x32\x32\x30\x20\x0b\x32\x1a\ -\x32\x30\x30\x42\x98\x10\x41\x46\x43\x14\x13\x50\xb5\xa3\x01\x00\ -\xd6\x10\x07\xd2\x2f\x48\xdf\x4a\x00\x00\x00\x00\x49\x45\x4e\x44\ -\xae\x42\x60\x82\ -\x00\x00\x00\xa6\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ -\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\ -\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x14\x1f\x20\xb9\ -\x8d\x77\xe9\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ -\x06\xe6\x7c\x60\x60\x60\x42\x30\xa1\x1c\x08\x93\x81\x81\x09\xc1\ -\x64\x60\x60\x62\x60\x48\x11\x40\xe2\x20\x73\x19\x90\x8d\x40\x02\ -\x00\x23\xed\x08\xaf\x64\x9f\x0f\x15\x00\x00\x00\x00\x49\x45\x4e\ -\x44\xae\x42\x60\x82\ \x00\x00\x00\x9e\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ @@ -530,160 +679,18 @@ qt_resource_data = b"\ \x0d\xfe\x9f\x87\xb1\x18\x91\x05\x18\x0d\xe1\x42\x48\x2a\x0c\x19\ \x18\x18\x91\x05\x10\x2a\xd1\x00\x00\xca\xb5\x07\xd2\x76\xbb\xb2\ \xc5\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ -\x00\x00\x00\xa6\ +\x00\x00\x00\x9e\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ \x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\ \x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ \x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\ \x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x08\x15\x3b\xdc\ -\x3b\x0c\x9b\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ -\x00\x8c\x0c\x0c\x73\x3e\x20\x0b\xa4\x08\x30\x32\x30\x20\x0b\xa6\ -\x08\x30\x30\x30\x42\x98\x10\xc1\x14\x01\x14\x13\x50\xb5\xa3\x01\ -\x00\xc6\xb9\x07\x90\x5d\x66\x1f\x83\x00\x00\x00\x00\x49\x45\x4e\ -\x44\xae\x42\x60\x82\ -\x00\x00\x00\xa6\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ -\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\ -\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x14\x1d\x00\xb0\ -\xd5\x35\xa3\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ -\x06\xfe\x9f\x67\x60\x60\x42\x30\xa1\x1c\x08\x93\x81\x81\x09\xc1\ -\x64\x60\x60\x62\x60\x60\x34\x44\xe2\x20\x73\x19\x90\x8d\x40\x02\ -\x00\x64\x40\x09\x75\x86\xb3\xad\x9c\x00\x00\x00\x00\x49\x45\x4e\ -\x44\xae\x42\x60\x82\ -\x00\x00\x00\xa6\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ -\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\ -\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x14\x1d\x00\xb0\ -\xd5\x35\xa3\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ -\x06\xfe\x9f\x67\x60\x60\x42\x30\xa1\x1c\x08\x93\x81\x81\x09\xc1\ -\x64\x60\x60\x62\x60\x60\x34\x44\xe2\x20\x73\x19\x90\x8d\x40\x02\ -\x00\x64\x40\x09\x75\x86\xb3\xad\x9c\x00\x00\x00\x00\x49\x45\x4e\ -\x44\xae\x42\x60\x82\ -\x00\x00\x07\x06\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x0a\x00\x00\x00\x07\x08\x06\x00\x00\x00\x31\xac\xdc\x63\ -\x00\x00\x04\xb0\x69\x54\x58\x74\x58\x4d\x4c\x3a\x63\x6f\x6d\x2e\ -\x61\x64\x6f\x62\x65\x2e\x78\x6d\x70\x00\x00\x00\x00\x00\x3c\x3f\ -\x78\x70\x61\x63\x6b\x65\x74\x20\x62\x65\x67\x69\x6e\x3d\x22\xef\ -\xbb\xbf\x22\x20\x69\x64\x3d\x22\x57\x35\x4d\x30\x4d\x70\x43\x65\ -\x68\x69\x48\x7a\x72\x65\x53\x7a\x4e\x54\x63\x7a\x6b\x63\x39\x64\ -\x22\x3f\x3e\x0a\x3c\x78\x3a\x78\x6d\x70\x6d\x65\x74\x61\x20\x78\ -\x6d\x6c\x6e\x73\x3a\x78\x3d\x22\x61\x64\x6f\x62\x65\x3a\x6e\x73\ -\x3a\x6d\x65\x74\x61\x2f\x22\x20\x78\x3a\x78\x6d\x70\x74\x6b\x3d\ -\x22\x58\x4d\x50\x20\x43\x6f\x72\x65\x20\x35\x2e\x35\x2e\x30\x22\ -\x3e\x0a\x20\x3c\x72\x64\x66\x3a\x52\x44\x46\x20\x78\x6d\x6c\x6e\ -\x73\x3a\x72\x64\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\ -\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x31\x39\x39\x39\x2f\x30\x32\ -\x2f\x32\x32\x2d\x72\x64\x66\x2d\x73\x79\x6e\x74\x61\x78\x2d\x6e\ -\x73\x23\x22\x3e\x0a\x20\x20\x3c\x72\x64\x66\x3a\x44\x65\x73\x63\ -\x72\x69\x70\x74\x69\x6f\x6e\x20\x72\x64\x66\x3a\x61\x62\x6f\x75\ -\x74\x3d\x22\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x65\ -\x78\x69\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x6e\x73\x2e\x61\ -\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x65\x78\x69\x66\x2f\x31\x2e\ -\x30\x2f\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x74\x69\ -\x66\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x6e\x73\x2e\x61\x64\ -\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x74\x69\x66\x66\x2f\x31\x2e\x30\ -\x2f\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x70\x68\x6f\ -\x74\x6f\x73\x68\x6f\x70\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x6e\ -\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x70\x68\x6f\x74\ -\x6f\x73\x68\x6f\x70\x2f\x31\x2e\x30\x2f\x22\x0a\x20\x20\x20\x20\ -\x78\x6d\x6c\x6e\x73\x3a\x78\x6d\x70\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x78\ -\x61\x70\x2f\x31\x2e\x30\x2f\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\ -\x6e\x73\x3a\x78\x6d\x70\x4d\x4d\x3d\x22\x68\x74\x74\x70\x3a\x2f\ -\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x78\x61\ -\x70\x2f\x31\x2e\x30\x2f\x6d\x6d\x2f\x22\x0a\x20\x20\x20\x20\x78\ -\x6d\x6c\x6e\x73\x3a\x73\x74\x45\x76\x74\x3d\x22\x68\x74\x74\x70\ -\x3a\x2f\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\ -\x78\x61\x70\x2f\x31\x2e\x30\x2f\x73\x54\x79\x70\x65\x2f\x52\x65\ -\x73\x6f\x75\x72\x63\x65\x45\x76\x65\x6e\x74\x23\x22\x0a\x20\x20\ -\x20\x65\x78\x69\x66\x3a\x50\x69\x78\x65\x6c\x58\x44\x69\x6d\x65\ -\x6e\x73\x69\x6f\x6e\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\x65\x78\ -\x69\x66\x3a\x50\x69\x78\x65\x6c\x59\x44\x69\x6d\x65\x6e\x73\x69\ -\x6f\x6e\x3d\x22\x37\x22\x0a\x20\x20\x20\x65\x78\x69\x66\x3a\x43\ -\x6f\x6c\x6f\x72\x53\x70\x61\x63\x65\x3d\x22\x31\x22\x0a\x20\x20\ -\x20\x74\x69\x66\x66\x3a\x49\x6d\x61\x67\x65\x57\x69\x64\x74\x68\ -\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\x74\x69\x66\x66\x3a\x49\x6d\ -\x61\x67\x65\x4c\x65\x6e\x67\x74\x68\x3d\x22\x37\x22\x0a\x20\x20\ -\x20\x74\x69\x66\x66\x3a\x52\x65\x73\x6f\x6c\x75\x74\x69\x6f\x6e\ -\x55\x6e\x69\x74\x3d\x22\x32\x22\x0a\x20\x20\x20\x74\x69\x66\x66\ -\x3a\x58\x52\x65\x73\x6f\x6c\x75\x74\x69\x6f\x6e\x3d\x22\x37\x32\ -\x2e\x30\x22\x0a\x20\x20\x20\x74\x69\x66\x66\x3a\x59\x52\x65\x73\ -\x6f\x6c\x75\x74\x69\x6f\x6e\x3d\x22\x37\x32\x2e\x30\x22\x0a\x20\ -\x20\x20\x70\x68\x6f\x74\x6f\x73\x68\x6f\x70\x3a\x43\x6f\x6c\x6f\ -\x72\x4d\x6f\x64\x65\x3d\x22\x33\x22\x0a\x20\x20\x20\x70\x68\x6f\ -\x74\x6f\x73\x68\x6f\x70\x3a\x49\x43\x43\x50\x72\x6f\x66\x69\x6c\ -\x65\x3d\x22\x73\x52\x47\x42\x20\x49\x45\x43\x36\x31\x39\x36\x36\ -\x2d\x32\x2e\x31\x22\x0a\x20\x20\x20\x78\x6d\x70\x3a\x4d\x6f\x64\ -\x69\x66\x79\x44\x61\x74\x65\x3d\x22\x32\x30\x32\x31\x2d\x30\x35\ -\x2d\x33\x31\x54\x31\x32\x3a\x33\x30\x3a\x31\x31\x2b\x30\x32\x3a\ -\x30\x30\x22\x0a\x20\x20\x20\x78\x6d\x70\x3a\x4d\x65\x74\x61\x64\ -\x61\x74\x61\x44\x61\x74\x65\x3d\x22\x32\x30\x32\x31\x2d\x30\x35\ -\x2d\x33\x31\x54\x31\x32\x3a\x33\x30\x3a\x31\x31\x2b\x30\x32\x3a\ -\x30\x30\x22\x3e\x0a\x20\x20\x20\x3c\x78\x6d\x70\x4d\x4d\x3a\x48\ -\x69\x73\x74\x6f\x72\x79\x3e\x0a\x20\x20\x20\x20\x3c\x72\x64\x66\ -\x3a\x53\x65\x71\x3e\x0a\x20\x20\x20\x20\x20\x3c\x72\x64\x66\x3a\ -\x6c\x69\x0a\x20\x20\x20\x20\x20\x20\x73\x74\x45\x76\x74\x3a\x61\ -\x63\x74\x69\x6f\x6e\x3d\x22\x70\x72\x6f\x64\x75\x63\x65\x64\x22\ -\x0a\x20\x20\x20\x20\x20\x20\x73\x74\x45\x76\x74\x3a\x73\x6f\x66\ -\x74\x77\x61\x72\x65\x41\x67\x65\x6e\x74\x3d\x22\x41\x66\x66\x69\ -\x6e\x69\x74\x79\x20\x44\x65\x73\x69\x67\x6e\x65\x72\x20\x31\x2e\ -\x39\x2e\x32\x22\x0a\x20\x20\x20\x20\x20\x20\x73\x74\x45\x76\x74\ -\x3a\x77\x68\x65\x6e\x3d\x22\x32\x30\x32\x31\x2d\x30\x35\x2d\x33\ -\x31\x54\x31\x32\x3a\x33\x30\x3a\x31\x31\x2b\x30\x32\x3a\x30\x30\ -\x22\x2f\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x72\x64\x66\x3a\x53\x65\ -\x71\x3e\x0a\x20\x20\x20\x3c\x2f\x78\x6d\x70\x4d\x4d\x3a\x48\x69\ -\x73\x74\x6f\x72\x79\x3e\x0a\x20\x20\x3c\x2f\x72\x64\x66\x3a\x44\ -\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x0a\x20\x3c\x2f\x72\ -\x64\x66\x3a\x52\x44\x46\x3e\x0a\x3c\x2f\x78\x3a\x78\x6d\x70\x6d\ -\x65\x74\x61\x3e\x0a\x3c\x3f\x78\x70\x61\x63\x6b\x65\x74\x20\x65\ -\x6e\x64\x3d\x22\x72\x22\x3f\x3e\x85\x9d\x9f\x08\x00\x00\x01\x83\ -\x69\x43\x43\x50\x73\x52\x47\x42\x20\x49\x45\x43\x36\x31\x39\x36\ -\x36\x2d\x32\x2e\x31\x00\x00\x28\x91\x75\x91\xcf\x2b\x44\x51\x14\ -\xc7\x3f\x66\x68\xfc\x18\x8d\x62\x61\x31\x65\x12\x16\x42\x83\x12\ -\x1b\x8b\x99\x18\x0a\x8b\x99\x51\x7e\x6d\x66\x9e\x79\x33\x6a\xde\ -\x78\xbd\x37\xd2\x64\xab\x6c\xa7\x28\xb1\xf1\x6b\xc1\x5f\xc0\x56\ -\x59\x2b\x45\xa4\x64\xa7\xac\x89\x0d\x7a\xce\x9b\x51\x23\x99\x73\ -\x3b\xf7\x7c\xee\xf7\xde\x73\xba\xf7\x5c\x70\x44\xd3\x8a\x66\x56\ -\xfa\x41\xcb\x64\x8d\x70\x28\xe0\x9b\x99\x9d\xf3\xb9\x9e\xa8\xa2\ -\x85\x1a\x3a\xf1\xc6\x14\x53\x9f\x8c\x8c\x46\x29\x6b\xef\xb7\x54\ -\xd8\xf1\xba\xdb\xae\x55\xfe\xdc\xbf\x56\xb7\x98\x30\x15\xa8\xa8\ -\x16\x1e\x56\x74\x23\x2b\x3c\x26\x3c\xb1\x9a\xd5\x6d\xde\x12\x6e\ -\x52\x52\xb1\x45\xe1\x13\xe1\x2e\x43\x2e\x28\x7c\x63\xeb\xf1\x22\ -\x3f\xdb\x9c\x2c\xf2\xa7\xcd\x46\x34\x1c\x04\x47\x83\xb0\x2f\xf9\ -\x8b\xe3\xbf\x58\x49\x19\x9a\xb0\xbc\x9c\x36\x2d\xbd\xa2\xfc\xdc\ -\xc7\x7e\x89\x3b\x91\x99\x8e\x48\x6c\x15\xf7\x62\x12\x26\x44\x00\ -\x1f\xe3\x8c\x10\x64\x80\x5e\x86\x64\x1e\xa0\x9b\x3e\x7a\x64\x45\ -\x99\x7c\x7f\x21\x7f\x8a\x65\xc9\x55\x64\xd6\xc9\x61\xb0\x44\x92\ -\x14\x59\xba\x44\x5d\x91\xea\x09\x89\xaa\xe8\x09\x19\x69\x72\x76\ -\xff\xff\xf6\xd5\x54\xfb\xfb\x8a\xd5\xdd\x01\xa8\x7a\xb4\xac\xd7\ -\x76\x70\x6d\xc2\x57\xde\xb2\x3e\x0e\x2c\xeb\xeb\x10\x9c\x0f\x70\ -\x9e\x29\xe5\x2f\xef\xc3\xe0\x9b\xe8\xf9\x92\xd6\xb6\x07\x9e\x75\ -\x38\xbd\x28\x69\xf1\x6d\x38\xdb\x80\xe6\x7b\x3d\x66\xc4\x0a\x92\ -\x53\xdc\xa1\xaa\xf0\x72\x0c\xf5\xb3\xd0\x78\x05\xb5\xf3\xc5\x9e\ -\xfd\xec\x73\x74\x07\xd1\x35\xf9\xaa\x4b\xd8\xd9\x85\x0e\x39\xef\ -\x59\xf8\x06\x8e\xfd\x67\xf8\xfd\x8a\x18\x97\x00\x00\x00\x09\x70\ -\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x6d\x49\x44\x41\x54\x18\x95\x75\xcf\xc1\x09\xc2\x50\ -\x10\x84\xe1\xd7\x85\x07\x9b\xd0\x43\x40\xd2\x82\x78\x14\x7b\x30\ -\x57\x21\x8d\x84\x60\x3f\x62\x4b\x7a\x48\xcc\x97\x83\xfb\x30\x04\ -\xdf\x9c\x86\x7f\x67\x99\xdd\x84\x0d\xaa\x54\x10\x6a\x6c\x13\x1e\ -\xbe\xba\xfe\x09\x35\x31\x7b\xe6\x8d\x0f\x26\x1c\x17\xa1\x53\xb0\ -\x11\x87\x0c\x2f\x01\x07\xec\xb0\x0f\x3f\xe1\xbc\xae\x69\xa3\xe6\ -\x85\x77\xf8\x5b\xe9\xf0\xbb\x9f\xfa\xd2\x83\x39\xdc\xa3\x5b\xf3\ -\x19\x2e\xa8\x89\xb5\x30\xf7\x43\xa0\x00\x00\x00\x00\x49\x45\x4e\ -\x44\xae\x42\x60\x82\ +\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x08\x15\x0f\xfd\ +\x8f\xf8\x2e\x00\x00\x00\x22\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ +\x0d\xfe\x9f\x87\xb1\x18\x91\x05\x18\x0d\xe1\x42\x48\x2a\x0c\x19\ +\x18\x18\x91\x05\x10\x2a\xd1\x00\x00\xca\xb5\x07\xd2\x76\xbb\xb2\ +\xc5\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ " qt_resource_name = b"\ @@ -696,119 +703,124 @@ qt_resource_name = b"\ \x00\x69\ \x00\x6d\x00\x61\x00\x67\x00\x65\x00\x73\ \x00\x0f\ -\x02\x9f\x05\x87\ -\x00\x72\ -\x00\x69\x00\x67\x00\x68\x00\x74\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\x00\x12\ -\x05\x8f\x9d\x07\ +\x06\x53\x25\xa7\ \x00\x62\ -\x00\x72\x00\x61\x00\x6e\x00\x63\x00\x68\x00\x5f\x00\x6f\x00\x70\x00\x65\x00\x6e\x00\x5f\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\ -\x00\x67\ -\x00\x1b\ -\x03\x5a\x32\x27\ -\x00\x63\ -\x00\x6f\x00\x6d\x00\x62\x00\x6f\x00\x62\x00\x6f\x00\x78\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x64\x00\x69\ -\x00\x73\x00\x61\x00\x62\x00\x6c\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x72\x00\x61\x00\x6e\x00\x63\x00\x68\x00\x5f\x00\x6f\x00\x70\x00\x65\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\ \x00\x18\ \x03\x8e\xde\x67\ \x00\x72\ \x00\x69\x00\x67\x00\x68\x00\x74\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x64\x00\x69\x00\x73\x00\x61\x00\x62\ \x00\x6c\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\x00\x11\ -\x0b\xda\x30\xa7\ -\x00\x62\ -\x00\x72\x00\x61\x00\x6e\x00\x63\x00\x68\x00\x5f\x00\x63\x00\x6c\x00\x6f\x00\x73\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\ -\x00\x12\ -\x03\x8d\x04\x47\ -\x00\x72\ -\x00\x69\x00\x67\x00\x68\x00\x74\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\ -\x00\x67\ -\x00\x15\ -\x0f\xf3\xc0\x07\ -\x00\x75\ -\x00\x70\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x64\x00\x69\x00\x73\x00\x61\x00\x62\x00\x6c\x00\x65\x00\x64\ -\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\x00\x0f\ -\x01\x73\x8b\x07\ -\x00\x75\ -\x00\x70\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\x00\x0e\ -\x04\xa2\xfc\xa7\ -\x00\x64\ -\x00\x6f\x00\x77\x00\x6e\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\x00\x12\ -\x01\x2e\x03\x27\ -\x00\x63\ -\x00\x6f\x00\x6d\x00\x62\x00\x6f\x00\x62\x00\x6f\x00\x78\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x70\x00\x6e\ -\x00\x67\ -\x00\x14\ -\x04\x5e\x2d\xa7\ -\x00\x62\ -\x00\x72\x00\x61\x00\x6e\x00\x63\x00\x68\x00\x5f\x00\x63\x00\x6c\x00\x6f\x00\x73\x00\x65\x00\x64\x00\x5f\x00\x6f\x00\x6e\x00\x2e\ -\x00\x70\x00\x6e\x00\x67\ -\x00\x17\ -\x0c\xab\x51\x07\ -\x00\x64\ -\x00\x6f\x00\x77\x00\x6e\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x64\x00\x69\x00\x73\x00\x61\x00\x62\x00\x6c\ -\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\x00\x11\ -\x01\x1f\xc3\x87\ -\x00\x64\ -\x00\x6f\x00\x77\x00\x6e\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\ -\x00\x17\ -\x0c\x65\xce\x07\ -\x00\x6c\ -\x00\x65\x00\x66\x00\x74\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x64\x00\x69\x00\x73\x00\x61\x00\x62\x00\x6c\ -\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\x00\x0c\ -\x06\xe6\xe6\x67\ -\x00\x75\ -\x00\x70\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x70\x00\x6e\x00\x67\ \x00\x15\ \x03\x27\x72\x67\ \x00\x63\ \x00\x6f\x00\x6d\x00\x62\x00\x6f\x00\x62\x00\x6f\x00\x78\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x6f\x00\x6e\ \x00\x2e\x00\x70\x00\x6e\x00\x67\ \x00\x11\ +\x0b\xda\x30\xa7\ +\x00\x62\ +\x00\x72\x00\x61\x00\x6e\x00\x63\x00\x68\x00\x5f\x00\x63\x00\x6c\x00\x6f\x00\x73\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\ +\x00\x17\ +\x0c\xab\x51\x07\ +\x00\x64\ +\x00\x6f\x00\x77\x00\x6e\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x64\x00\x69\x00\x73\x00\x61\x00\x62\x00\x6c\ +\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x11\ \x00\xb8\x8c\x07\ \x00\x6c\ \x00\x65\x00\x66\x00\x74\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\ \ +\x00\x12\ +\x01\x2e\x03\x27\ +\x00\x63\ +\x00\x6f\x00\x6d\x00\x62\x00\x6f\x00\x62\x00\x6f\x00\x78\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x70\x00\x6e\ +\x00\x67\ +\x00\x0f\ +\x02\x9f\x05\x87\ +\x00\x72\ +\x00\x69\x00\x67\x00\x68\x00\x74\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x15\ +\x0f\xf3\xc0\x07\ +\x00\x75\ +\x00\x70\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x64\x00\x69\x00\x73\x00\x61\x00\x62\x00\x6c\x00\x65\x00\x64\ +\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x0f\ +\x0c\xe2\x68\x67\ +\x00\x74\ +\x00\x72\x00\x61\x00\x6e\x00\x73\x00\x70\x00\x61\x00\x72\x00\x65\x00\x6e\x00\x74\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x0e\ +\x04\xa2\xfc\xa7\ +\x00\x64\ +\x00\x6f\x00\x77\x00\x6e\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x1b\ +\x03\x5a\x32\x27\ +\x00\x63\ +\x00\x6f\x00\x6d\x00\x62\x00\x6f\x00\x62\x00\x6f\x00\x78\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x64\x00\x69\ +\x00\x73\x00\x61\x00\x62\x00\x6c\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x17\ +\x0c\x65\xce\x07\ +\x00\x6c\ +\x00\x65\x00\x66\x00\x74\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x64\x00\x69\x00\x73\x00\x61\x00\x62\x00\x6c\ +\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x11\ +\x01\x1f\xc3\x87\ +\x00\x64\ +\x00\x6f\x00\x77\x00\x6e\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\ +\x00\x12\ +\x05\x8f\x9d\x07\ +\x00\x62\ +\x00\x72\x00\x61\x00\x6e\x00\x63\x00\x68\x00\x5f\x00\x6f\x00\x70\x00\x65\x00\x6e\x00\x5f\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\ +\x00\x67\ +\x00\x12\ +\x03\x8d\x04\x47\ +\x00\x72\ +\x00\x69\x00\x67\x00\x68\x00\x74\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\ +\x00\x67\ \x00\x0e\ \x0e\xde\xfa\xc7\ \x00\x6c\ \x00\x65\x00\x66\x00\x74\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\x00\x0f\ -\x06\x53\x25\xa7\ +\x00\x14\ +\x04\x5e\x2d\xa7\ \x00\x62\ -\x00\x72\x00\x61\x00\x6e\x00\x63\x00\x68\x00\x5f\x00\x6f\x00\x70\x00\x65\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x72\x00\x61\x00\x6e\x00\x63\x00\x68\x00\x5f\x00\x63\x00\x6c\x00\x6f\x00\x73\x00\x65\x00\x64\x00\x5f\x00\x6f\x00\x6e\x00\x2e\ +\x00\x70\x00\x6e\x00\x67\ +\x00\x0c\ +\x06\xe6\xe6\x67\ +\x00\x75\ +\x00\x70\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x0f\ +\x01\x73\x8b\x07\ +\x00\x75\ +\x00\x70\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\ " qt_resource_struct_v1 = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ -\x00\x00\x00\x16\x00\x02\x00\x00\x00\x13\x00\x00\x00\x03\ -\x00\x00\x02\xe6\x00\x00\x00\x00\x00\x01\x00\x00\x1f\x3c\ -\x00\x00\x02\x3c\x00\x00\x00\x00\x00\x01\x00\x00\x1c\x9d\ -\x00\x00\x01\xb0\x00\x00\x00\x00\x00\x01\x00\x00\x13\x68\ -\x00\x00\x01\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x12\x1d\ +\x00\x00\x00\x16\x00\x02\x00\x00\x00\x14\x00\x00\x00\x03\ +\x00\x00\x01\x0e\x00\x00\x00\x00\x00\x01\x00\x00\x10\xb3\ +\x00\x00\x02\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x15\x93\ +\x00\x00\x01\x36\x00\x00\x00\x00\x00\x01\x00\x00\x11\x5d\ +\x00\x00\x03\x54\x00\x00\x00\x00\x00\x01\x00\x00\x27\x41\ +\x00\x00\x01\x60\x00\x00\x00\x00\x00\x01\x00\x00\x12\x07\ +\x00\x00\x00\x82\x00\x00\x00\x00\x00\x01\x00\x00\x07\xae\ +\x00\x00\x01\xfa\x00\x00\x00\x00\x00\x01\x00\x00\x14\x40\ +\x00\x00\x02\xbc\x00\x00\x00\x00\x00\x01\x00\x00\x1d\x70\ +\x00\x00\x00\x4c\x00\x00\x00\x00\x00\x01\x00\x00\x07\x0a\ +\x00\x00\x03\x08\x00\x00\x00\x00\x00\x01\x00\x00\x1e\xbe\ +\x00\x00\x01\xd8\x00\x00\x00\x00\x00\x01\x00\x00\x13\x97\ +\x00\x00\x02\x92\x00\x00\x00\x00\x00\x01\x00\x00\x16\x3c\ \x00\x00\x00\x28\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x02\xb6\x00\x00\x00\x00\x00\x01\x00\x00\x1e\x92\ -\x00\x00\x00\x76\x00\x00\x00\x00\x00\x01\x00\x00\x07\xd8\ -\x00\x00\x01\x10\x00\x00\x00\x00\x00\x01\x00\x00\x10\xd6\ -\x00\x00\x00\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x08\x81\ -\x00\x00\x01\xda\x00\x00\x00\x00\x00\x01\x00\x00\x14\x12\ -\x00\x00\x01\x8e\x00\x00\x00\x00\x00\x01\x00\x00\x12\xbf\ -\x00\x00\x00\x4c\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa4\ -\x00\x00\x03\x30\x00\x00\x00\x00\x00\x01\x00\x00\x20\x90\ -\x00\x00\x02\x98\x00\x00\x00\x00\x00\x01\x00\x00\x1d\xf0\ -\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x01\x00\x00\x09\x25\ -\x00\x00\x02\x64\x00\x00\x00\x00\x00\x01\x00\x00\x1d\x46\ -\x00\x00\x02\x08\x00\x00\x00\x00\x00\x01\x00\x00\x1b\xf3\ -\x00\x00\x03\x0e\x00\x00\x00\x00\x00\x01\x00\x00\x1f\xe6\ -\x00\x00\x01\x3a\x00\x00\x00\x00\x00\x01\x00\x00\x11\x7a\ +\x00\x00\x03\x36\x00\x00\x00\x00\x00\x01\x00\x00\x26\x9f\ +\x00\x00\x00\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x08\x58\ +\x00\x00\x02\x36\x00\x00\x00\x00\x00\x01\x00\x00\x14\xe9\ +\x00\x00\x00\xda\x00\x00\x00\x00\x00\x01\x00\x00\x10\x09\ +\x00\x00\x01\xb4\x00\x00\x00\x00\x00\x01\x00\x00\x13\x4e\ +\x00\x00\x02\xe6\x00\x00\x00\x00\x00\x01\x00\x00\x1e\x14\ +\x00\x00\x01\x84\x00\x00\x00\x00\x00\x01\x00\x00\x12\xab\ " qt_resource_struct_v2 = b"\ @@ -816,49 +828,50 @@ qt_resource_struct_v2 = b"\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x16\x00\x02\x00\x00\x00\x13\x00\x00\x00\x03\ +\x00\x00\x00\x16\x00\x02\x00\x00\x00\x14\x00\x00\x00\x03\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x02\xe6\x00\x00\x00\x00\x00\x01\x00\x00\x1f\x3c\ -\x00\x00\x01\x76\x41\x9d\xa2\x35\ -\x00\x00\x02\x3c\x00\x00\x00\x00\x00\x01\x00\x00\x1c\x9d\ -\x00\x00\x01\x76\x41\x9d\xa2\x35\ -\x00\x00\x01\xb0\x00\x00\x00\x00\x00\x01\x00\x00\x13\x68\ -\x00\x00\x01\x79\xb4\x72\xcc\x9c\ -\x00\x00\x01\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x12\x1d\ -\x00\x00\x01\x76\x41\x9d\xa2\x39\ +\x00\x00\x01\x0e\x00\x00\x00\x00\x00\x01\x00\x00\x10\xb3\ +\x00\x00\x01\x79\xec\x37\x3f\xbc\ +\x00\x00\x02\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x15\x93\ +\x00\x00\x01\x79\xec\x37\x3f\xba\ +\x00\x00\x01\x36\x00\x00\x00\x00\x00\x01\x00\x00\x11\x5d\ +\x00\x00\x01\x79\xec\x37\x3f\xb6\ +\x00\x00\x03\x54\x00\x00\x00\x00\x00\x01\x00\x00\x27\x41\ +\x00\x00\x01\x79\xec\x37\x3f\xc0\ +\x00\x00\x01\x60\x00\x00\x00\x00\x00\x01\x00\x00\x12\x07\ +\x00\x00\x01\x79\xec\x37\x3f\xbc\ +\x00\x00\x00\x82\x00\x00\x00\x00\x00\x01\x00\x00\x07\xae\ +\x00\x00\x01\x79\xec\x37\x3f\xb7\ +\x00\x00\x01\xfa\x00\x00\x00\x00\x00\x01\x00\x00\x14\x40\ +\x00\x00\x01\x79\xec\x37\x3f\xb7\ +\x00\x00\x02\xbc\x00\x00\x00\x00\x00\x01\x00\x00\x1d\x70\ +\x00\x00\x01\x79\xec\x37\x3f\xbe\ +\x00\x00\x00\x4c\x00\x00\x00\x00\x00\x01\x00\x00\x07\x0a\ +\x00\x00\x01\x79\xec\x37\x3f\xbd\ +\x00\x00\x03\x08\x00\x00\x00\x00\x00\x01\x00\x00\x1e\xbe\ +\x00\x00\x01\x79\xec\x37\x3f\xb4\ +\x00\x00\x01\xd8\x00\x00\x00\x00\x00\x01\x00\x00\x13\x97\ +\x00\x00\x01\x79\xec\x37\x3f\xb8\ +\x00\x00\x02\x92\x00\x00\x00\x00\x00\x01\x00\x00\x16\x3c\ +\x00\x00\x01\x79\xec\x37\x3f\xb5\ \x00\x00\x00\x28\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01\x76\x41\x9d\xa2\x37\ -\x00\x00\x02\xb6\x00\x00\x00\x00\x00\x01\x00\x00\x1e\x92\ -\x00\x00\x01\x79\xb4\x72\xcc\x9c\ -\x00\x00\x00\x76\x00\x00\x00\x00\x00\x01\x00\x00\x07\xd8\ -\x00\x00\x01\x79\xb4\x72\xcc\x9c\ -\x00\x00\x01\x10\x00\x00\x00\x00\x00\x01\x00\x00\x10\xd6\ -\x00\x00\x01\x76\x41\x9d\xa2\x37\ -\x00\x00\x00\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x08\x81\ -\x00\x00\x01\x76\x41\x9d\xa2\x37\ -\x00\x00\x01\xda\x00\x00\x00\x00\x00\x01\x00\x00\x14\x12\ -\x00\x00\x01\x79\xc2\x05\x2b\x60\ -\x00\x00\x01\x8e\x00\x00\x00\x00\x00\x01\x00\x00\x12\xbf\ -\x00\x00\x01\x76\x41\x9d\xa2\x35\ -\x00\x00\x00\x4c\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa4\ -\x00\x00\x01\x79\xc1\xfc\x16\x91\ -\x00\x00\x03\x30\x00\x00\x00\x00\x00\x01\x00\x00\x20\x90\ -\x00\x00\x01\x79\xc1\xf9\x4b\x78\ -\x00\x00\x02\x98\x00\x00\x00\x00\x00\x01\x00\x00\x1d\xf0\ -\x00\x00\x01\x76\x41\x9d\xa2\x39\ -\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x01\x00\x00\x09\x25\ -\x00\x00\x01\x79\xc2\x05\x91\x2a\ -\x00\x00\x02\x64\x00\x00\x00\x00\x00\x01\x00\x00\x1d\x46\ -\x00\x00\x01\x76\x41\x9d\xa2\x35\ -\x00\x00\x02\x08\x00\x00\x00\x00\x00\x01\x00\x00\x1b\xf3\ -\x00\x00\x01\x76\x41\x9d\xa2\x35\ -\x00\x00\x03\x0e\x00\x00\x00\x00\x00\x01\x00\x00\x1f\xe6\ -\x00\x00\x01\x76\x41\x9d\xa2\x35\ -\x00\x00\x01\x3a\x00\x00\x00\x00\x00\x01\x00\x00\x11\x7a\ -\x00\x00\x01\x76\x41\x9d\xa2\x39\ +\x00\x00\x01\x79\xec\x37\x3f\xb5\ +\x00\x00\x03\x36\x00\x00\x00\x00\x00\x01\x00\x00\x26\x9f\ +\x00\x00\x01\x79\xec\x37\x3f\xbe\ +\x00\x00\x00\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x08\x58\ +\x00\x00\x01\x79\xec\x37\x3f\xb3\ +\x00\x00\x02\x36\x00\x00\x00\x00\x00\x01\x00\x00\x14\xe9\ +\x00\x00\x01\x79\xec\x37\x3f\xbb\ +\x00\x00\x00\xda\x00\x00\x00\x00\x00\x01\x00\x00\x10\x09\ +\x00\x00\x01\x79\xec\x37\x3f\xb9\ +\x00\x00\x01\xb4\x00\x00\x00\x00\x00\x01\x00\x00\x13\x4e\ +\x00\x00\x01\x7c\xa7\x41\xfc\x00\ +\x00\x00\x02\xe6\x00\x00\x00\x00\x00\x01\x00\x00\x1e\x14\ +\x00\x00\x01\x79\xec\x37\x3f\xbb\ +\x00\x00\x01\x84\x00\x00\x00\x00\x00\x01\x00\x00\x12\xab\ +\x00\x00\x01\x79\xec\x37\x3f\xbf\ " - qt_version = [int(v) for v in QtCore.qVersion().split('.')] if qt_version < [5, 8, 0]: rcc_version = 1 diff --git a/openpype/style/pyside2_resources.py b/openpype/style/pyside2_resources.py index ee68a74b8e..dff01eec49 100644 --- a/openpype/style/pyside2_resources.py +++ b/openpype/style/pyside2_resources.py @@ -1,75 +1,15 @@ -# Resource object code (Python 3) -# Created by: object code -# Created by: The Resource Compiler for Qt version 5.15.2 +# -*- coding: utf-8 -*- + +# Resource object code +# +# Created: Fri Oct 22 11:42:52 2021 +# by: The Resource Compiler for PySide2 (Qt v5.6.1) +# # WARNING! All changes made in this file will be lost! from PySide2 import QtCore - qt_resource_data = b"\ -\x00\x00\x00\x9f\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ -\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ -HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x14\x1f\xf9\ -#\xd9\x0b\x00\x00\x00#IDAT\x08\xd7c`\xc0\ -\x0d\xe6|\x80\xb1\x18\x91\x05R\x04\xe0B\x08\x15)\x02\ -\x0c\x0c\x8c\xc8\x02\x08\x95h\x00\x00\xac\xac\x07\x90Ne\ -4\xac\x00\x00\x00\x00IEND\xaeB`\x82\ -\x00\x00\x00\xa6\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ -\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ -HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15;\xdc\ -;\x0c\x9b\x00\x00\x00*IDAT\x08\xd7c`\xc0\ -\x00\x8c\x0c\x0cs> \x0b\xa4\x08020 \x0b\xa6\ -\x08000B\x98\x10\xc1\x14\x01\x14\x13P\xb5\xa3\x01\ -\x00\xc6\xb9\x07\x90]f\x1f\x83\x00\x00\x00\x00IEN\ -D\xaeB`\x82\ -\x00\x00\x00\xa5\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ -\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02bKGD\x00\x9cS4\xfc]\x00\x00\x00\x09p\ -HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b\x02\x04m\ -\x98\x1bi\x00\x00\x00)IDAT\x08\xd7c`\xc0\ -\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18220 \x0b2\x1a\ -200B\x98\x10AFC\x14\x13P\xb5\xa3\x01\x00\ -\xd6\x10\x07\xd2/H\xdfJ\x00\x00\x00\x00IEND\ -\xaeB`\x82\ -\x00\x00\x00\xa5\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ -\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02bKGD\x00\x9cS4\xfc]\x00\x00\x00\x09p\ -HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b\x02\x04m\ -\x98\x1bi\x00\x00\x00)IDAT\x08\xd7c`\xc0\ -\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18220 \x0b2\x1a\ -200B\x98\x10AFC\x14\x13P\xb5\xa3\x01\x00\ -\xd6\x10\x07\xd2/H\xdfJ\x00\x00\x00\x00IEND\ -\xaeB`\x82\ -\x00\x00\x00\xa0\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ -\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ -HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1f\x0d\xfc\ -R+\x9c\x00\x00\x00$IDAT\x08\xd7c`@\ -\x05s>\xc0XL\xc8\x5c&dY&d\xc5pN\ -\x8a\x00\x9c\x93\x22\x80a\x1a\x0a\x00\x00)\x95\x08\xaf\x88\ -\xac\xba4\x00\x00\x00\x00IEND\xaeB`\x82\ \x00\x00\x00\xa6\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -83,31 +23,33 @@ HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ d``b``4D\xe2 s\x19\x90\x8d@\x02\ \x00d@\x09u\x86\xb3\xad\x9c\x00\x00\x00\x00IEN\ D\xaeB`\x82\ -\x00\x00\x00\x9e\ +\x00\x00\x00\xa5\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\x9cS4\xfc]\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b\x02\x04m\ +\x98\x1bi\x00\x00\x00)IDAT\x08\xd7c`\xc0\ +\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18220 \x0b2\x1a\ +200B\x98\x10AFC\x14\x13P\xb5\xa3\x01\x00\ +\xd6\x10\x07\xd2/H\xdfJ\x00\x00\x00\x00IEND\ +\xaeB`\x82\ +\x00\x00\x00\xa6\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ \x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ \x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ \x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15\x0f\xfd\ -\x8f\xf8.\x00\x00\x00\x22IDAT\x08\xd7c`\xc0\ -\x0d\xfe\x9f\x87\xb1\x18\x91\x05\x18\x0d\xe1BH*\x0c\x19\ -\x18\x18\x91\x05\x10*\xd1\x00\x00\xca\xb5\x07\xd2v\xbb\xb2\ -\xc5\x00\x00\x00\x00IEND\xaeB`\x82\ -\x00\x00\x00\x9e\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ -\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ -HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15\x0f\xfd\ -\x8f\xf8.\x00\x00\x00\x22IDAT\x08\xd7c`\xc0\ -\x0d\xfe\x9f\x87\xb1\x18\x91\x05\x18\x0d\xe1BH*\x0c\x19\ -\x18\x18\x91\x05\x10*\xd1\x00\x00\xca\xb5\x07\xd2v\xbb\xb2\ -\xc5\x00\x00\x00\x00IEND\xaeB`\x82\ -\x00\x00\x07\x06\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15;\xdc\ +;\x0c\x9b\x00\x00\x00*IDAT\x08\xd7c`\xc0\ +\x00\x8c\x0c\x0cs> \x0b\xa4\x08020 \x0b\xa6\ +\x08000B\x98\x10\xc1\x14\x01\x14\x13P\xb5\xa3\x01\ +\x00\xc6\xb9\x07\x90]f\x1f\x83\x00\x00\x00\x00IEN\ +D\xaeB`\x82\ +\x00\x00\x070\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ \x00\x00\x0a\x00\x00\x00\x07\x08\x06\x00\x00\x001\xac\xdcc\ @@ -165,10 +107,10 @@ toshop:ICCProfil\ e=\x22sRGB IEC61966\ -2.1\x22\x0a xmp:Mod\ ifyDate=\x222021-05\ --31T12:30:11+02:\ +-31T12:33:14+02:\ 00\x22\x0a xmp:Metad\ ataDate=\x222021-05\ --31T12:30:11+02:\ +-31T12:33:14+02:\ 00\x22>\x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a\x0a\x85\x9d\x9f\x08\x00\x00\x01\x83\ +nd=\x22r\x22?>H\x8b[^\x00\x00\x01\x83\ iCCPsRGB IEC6196\ 6-2.1\x00\x00(\x91u\x91\xcf+DQ\x14\ \xc7?fh\xfc\x18\x8dba1e\x12\x16B\x83\x12\ @@ -213,28 +155,54 @@ S\xdc\xa1\xaa\xf0r\x0c\xf5\xb3\xd0x\x05\xb5\xf3\xc5\x9e\ \xfd\xecst\x07\xd15\xf9\xaaK\xd8\xd9\x85\x0e9\xef\ Y\xf8\x06\x8e\xfdg\xf8\xfd\x8a\x18\x97\x00\x00\x00\x09p\ HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00mIDAT\x18\x95u\xcf\xc1\x09\xc2P\ -\x10\x84\xe1\xd7\x85\x07\x9b\xd0C@\xd2\x82x\x14{0\ -W!\x8d\x84`?bKzH\xcc\x97\x83\xfb0\x04\ -\xdf\x9c\x86\x7fg\x99\xdd\x84\x0d\xaaT\x10jl\x13\x1e\ -\xbe\xba\xfe\x0951{\xe6\x8d\x0f&\x1c\x17\xa1S\xb0\ -\x11\x87\x0c/\x01\x07\xec\xb0\x0f?\xe1\xbc\xaei\xa3\xe6\ -\x85w\xf8[\xe9\xf0\xbb\x9f\xfa\xd2\x839\xdc\xa3[\xf3\ -\x19.\xa8\x89\xb50\xf7C\xa0\x00\x00\x00\x00IEN\ -D\xaeB`\x82\ -\x00\x00\x00\xa6\ +\x00\x00\x00\x97IDAT\x18\x95m\xcf\xb1j\x02A\ +\x14\x85\xe1o\xb7\xb6\xd0'H=Vi\x03\xb1\xb4H\ +;l\xa5\xf19\xf6Y\x02VB\xbaa\x0a\x0b;\x1b\ +\x1bkA\x18\x02)m\xe3\xbe\x82\xcd\x06\x16\xd9\xdb\xdd\ +\x9f\xff\x5c\xee\xa9b*\x13Ls\x13nF&\xa6\xf2\ +\x82\xaeF\x8b\xdf\x98\xca\xfb\x88\xb4\xc0\x0f\xda\x1a[t\ +\xd8\xc7T\xc2@\x9ac\x8f?|U=|\xc5\x09w\ +\xbc\xa1\xc2\x193,r\x13.\xd5\xe0\xc2\x12\x07\x5cQ\ +#\xe0#7\xe1\xa8O\x0e\x7f\xda`\xd7\xaf\x9f\xb9\x09\ +\xdfc\x05\xff\xe5uLe\xf5\xcc\x1f\x0d3,\x83\xb6\ +\x06D\x83\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x00\xa0\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ \x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ \x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ \x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1f \xb9\ -\x8dw\xe9\x00\x00\x00*IDAT\x08\xd7c`\xc0\ -\x06\xe6|```B0\xa1\x1c\x08\x93\x81\x81\x09\xc1\ -d``b`H\x11@\xe2 s\x19\x90\x8d@\x02\ -\x00#\xed\x08\xafd\x9f\x0f\x15\x00\x00\x00\x00IEN\ -D\xaeB`\x82\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1c\x1f$\ +\xc6\x09\x17\x00\x00\x00$IDAT\x08\xd7c`@\ +\x05\xff\xcf\xc3XL\xc8\x5c&dY&d\xc5p\x0e\ +\xa3!\x9c\xc3h\x88a\x1a\x0a\x00\x00m\x84\x09u7\ +\x9e\xd9#\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x00\x9e\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15\x0f\xfd\ +\x8f\xf8.\x00\x00\x00\x22IDAT\x08\xd7c`\xc0\ +\x0d\xfe\x9f\x87\xb1\x18\x91\x05\x18\x0d\xe1BH*\x0c\x19\ +\x18\x18\x91\x05\x10*\xd1\x00\x00\xca\xb5\x07\xd2v\xbb\xb2\ +\xc5\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x00\xa5\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\x9cS4\xfc]\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b\x02\x04m\ +\x98\x1bi\x00\x00\x00)IDAT\x08\xd7c`\xc0\ +\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18220 \x0b2\x1a\ +200B\x98\x10AFC\x14\x13P\xb5\xa3\x01\x00\ +\xd6\x10\x07\xd2/H\xdfJ\x00\x00\x00\x00IEND\ +\xaeB`\x82\ \x00\x00\x07\xdd\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -363,6 +331,221 @@ zpp\xf0\xe3\x0e.\xa4\xd2\xae\xf0\x8a\xf7\x9a\xe3V\ q[s\x5c@H\xa5\xdda\x81\x0d\x9ek\x8e\xff\xfd\ \xcf?\xcc1\xe9\x01\x1c\x00sR-q\xe4J\x1bi\ \x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x00\x9e\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15\x0f\xfd\ +\x8f\xf8.\x00\x00\x00\x22IDAT\x08\xd7c`\xc0\ +\x0d\xfe\x9f\x87\xb1\x18\x91\x05\x18\x0d\xe1BH*\x0c\x19\ +\x18\x18\x91\x05\x10*\xd1\x00\x00\xca\xb5\x07\xd2v\xbb\xb2\ +\xc5\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x00\xa6\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15;\xdc\ +;\x0c\x9b\x00\x00\x00*IDAT\x08\xd7c`\xc0\ +\x00\x8c\x0c\x0cs> \x0b\xa4\x08020 \x0b\xa6\ +\x08000B\x98\x10\xc1\x14\x01\x14\x13P\xb5\xa3\x01\ +\x00\xc6\xb9\x07\x90]f\x1f\x83\x00\x00\x00\x00IEN\ +D\xaeB`\x82\ +\x00\x00\x00\xa6\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15;\xdc\ +;\x0c\x9b\x00\x00\x00*IDAT\x08\xd7c`\xc0\ +\x00\x8c\x0c\x0cs> \x0b\xa4\x08020 \x0b\xa6\ +\x08000B\x98\x10\xc1\x14\x01\x14\x13P\xb5\xa3\x01\ +\x00\xc6\xb9\x07\x90]f\x1f\x83\x00\x00\x00\x00IEN\ +D\xaeB`\x82\ +\x00\x00\x00\xa5\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\x9cS4\xfc]\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b\x02\x04m\ +\x98\x1bi\x00\x00\x00)IDAT\x08\xd7c`\xc0\ +\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18220 \x0b2\x1a\ +200B\x98\x10AFC\x14\x13P\xb5\xa3\x01\x00\ +\xd6\x10\x07\xd2/H\xdfJ\x00\x00\x00\x00IEND\ +\xaeB`\x82\ +\x00\x00\x00\xa0\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1f\x0d\xfc\ +R+\x9c\x00\x00\x00$IDAT\x08\xd7c`@\ +\x05s>\xc0XL\xc8\x5c&dY&d\xc5pN\ +\x8a\x00\x9c\x93\x22\x80a\x1a\x0a\x00\x00)\x95\x08\xaf\x88\ +\xac\xba4\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x00\x9f\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x14\x1f\xf9\ +#\xd9\x0b\x00\x00\x00#IDAT\x08\xd7c`\xc0\ +\x0d\xe6|\x80\xb1\x18\x91\x05R\x04\xe0B\x08\x15)\x02\ +\x0c\x0c\x8c\xc8\x02\x08\x95h\x00\x00\xac\xac\x07\x90Ne\ +4\xac\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x07\x06\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x0a\x00\x00\x00\x07\x08\x06\x00\x00\x001\xac\xdcc\ +\x00\x00\x04\xb0iTXtXML:com.\ +adobe.xmp\x00\x00\x00\x00\x00\x0a\x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a\x0a\x85\x9d\x9f\x08\x00\x00\x01\x83\ +iCCPsRGB IEC6196\ +6-2.1\x00\x00(\x91u\x91\xcf+DQ\x14\ +\xc7?fh\xfc\x18\x8dba1e\x12\x16B\x83\x12\ +\x1b\x8b\x99\x18\x0a\x8b\x99Q~mf\x9ey3j\xde\ +x\xbd7\xd2d\xabl\xa7(\xb1\xf1k\xc1_\xc0V\ +Y+E\xa4d\xa7\xac\x89\x0dz\xce\x9bQ#\x99s\ +;\xf7|\xee\xf7\xdes\xba\xf7\x5cpD\xd3\x8afV\ +\xfaA\xcbd\x8dp(\xe0\x9b\x99\x9d\xf3\xb9\x9e\xa8\xa2\ +\x85\x1a:\xf1\xc6\x14S\x9f\x8c\x8cF)k\xef\xb7T\ +\xd8\xf1\xba\xdb\xaeU\xfe\xdc\xbfV\xb7\x980\x15\xa8\xa8\ +\x16\x1eVt#+<&<\xb1\x9a\xd5m\xde\x12n\ +RR\xb1E\xe1\x13\xe1.C.(|c\xeb\xf1\x22\ +?\xdb\x9c,\xf2\xa7\xcdF4\x1c\x04G\x83\xb0/\xf9\ +\x8b\xe3\xbfXI\x19\x9a\xb0\xbc\x9c6-\xbd\xa2\xfc\xdc\ +\xc7~\x89;\x91\x99\x8eHl\x15\xf7b\x12&D\x00\ +\x1f\xe3\x8c\x10d\x80^\x86d\x1e\xa0\x9b>zdE\ +\x99|\x7f!\x7f\x8ae\xc9Ud\xd6\xc9a\xb0D\x92\ +\x14Y\xbaD]\x91\xea\x09\x89\xaa\xe8\x09\x19irv\ +\xff\xff\xf6\xd5T\xfb\xfb\x8a\xd5\xdd\x01\xa8z\xb4\xac\xd7\ +vpm\xc2W\xde\xb2>\x0e,\xeb\xeb\x10\x9c\x0fp\ +\x9e)\xe5/\xef\xc3\xe0\x9b\xe8\xf9\x92\xd6\xb6\x07\x9eu\ +8\xbd(i\xf1m8\xdb\x80\xe6{=f\xc4\x0a\x92\ +S\xdc\xa1\xaa\xf0r\x0c\xf5\xb3\xd0x\x05\xb5\xf3\xc5\x9e\ +\xfd\xecst\x07\xd15\xf9\xaaK\xd8\xd9\x85\x0e9\xef\ +Y\xf8\x06\x8e\xfdg\xf8\xfd\x8a\x18\x97\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00mIDAT\x18\x95u\xcf\xc1\x09\xc2P\ +\x10\x84\xe1\xd7\x85\x07\x9b\xd0C@\xd2\x82x\x14{0\ +W!\x8d\x84`?bKzH\xcc\x97\x83\xfb0\x04\ +\xdf\x9c\x86\x7fg\x99\xdd\x84\x0d\xaaT\x10jl\x13\x1e\ +\xbe\xba\xfe\x0951{\xe6\x8d\x0f&\x1c\x17\xa1S\xb0\ +\x11\x87\x0c/\x01\x07\xec\xb0\x0f?\xe1\xbc\xaei\xa3\xe6\ +\x85w\xf8[\xe9\xf0\xbb\x9f\xfa\xd2\x839\xdc\xa3[\xf3\ +\x19.\xa8\x89\xb50\xf7C\xa0\x00\x00\x00\x00IEN\ +D\xaeB`\x82\ +\x00\x00\x00\xa6\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1f \xb9\ +\x8dw\xe9\x00\x00\x00*IDAT\x08\xd7c`\xc0\ +\x06\xe6|```B0\xa1\x1c\x08\x93\x81\x81\x09\xc1\ +d``b`H\x11@\xe2 s\x19\x90\x8d@\x02\ +\x00#\xed\x08\xafd\x9f\x0f\x15\x00\x00\x00\x00IEN\ +D\xaeB`\x82\ +\x00\x00\x00\xa0\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1c\x1f$\ +\xc6\x09\x17\x00\x00\x00$IDAT\x08\xd7c`@\ +\x05\xff\xcf\xc3XL\xc8\x5c&dY&d\xc5p\x0e\ +\xa3!\x9c\xc3h\x88a\x1a\x0a\x00\x00m\x84\x09u7\ +\x9e\xd9#\x00\x00\x00\x00IEND\xaeB`\x82\ \x00\x00\x07\xad\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -501,186 +684,6 @@ HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ d``b``4D\xe2 s\x19\x90\x8d@\x02\ \x00d@\x09u\x86\xb3\xad\x9c\x00\x00\x00\x00IEN\ D\xaeB`\x82\ -\x00\x00\x00\xa5\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ -\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02bKGD\x00\x9cS4\xfc]\x00\x00\x00\x09p\ -HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b\x02\x04m\ -\x98\x1bi\x00\x00\x00)IDAT\x08\xd7c`\xc0\ -\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18220 \x0b2\x1a\ -200B\x98\x10AFC\x14\x13P\xb5\xa3\x01\x00\ -\xd6\x10\x07\xd2/H\xdfJ\x00\x00\x00\x00IEND\ -\xaeB`\x82\ -\x00\x00\x00\xa0\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ -\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ -HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1c\x1f$\ -\xc6\x09\x17\x00\x00\x00$IDAT\x08\xd7c`@\ -\x05\xff\xcf\xc3XL\xc8\x5c&dY&d\xc5p\x0e\ -\xa3!\x9c\xc3h\x88a\x1a\x0a\x00\x00m\x84\x09u7\ -\x9e\xd9#\x00\x00\x00\x00IEND\xaeB`\x82\ -\x00\x00\x070\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x0a\x00\x00\x00\x07\x08\x06\x00\x00\x001\xac\xdcc\ -\x00\x00\x04\xb0iTXtXML:com.\ -adobe.xmp\x00\x00\x00\x00\x00\x0a\x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a\x0aH\x8b[^\x00\x00\x01\x83\ -iCCPsRGB IEC6196\ -6-2.1\x00\x00(\x91u\x91\xcf+DQ\x14\ -\xc7?fh\xfc\x18\x8dba1e\x12\x16B\x83\x12\ -\x1b\x8b\x99\x18\x0a\x8b\x99Q~mf\x9ey3j\xde\ -x\xbd7\xd2d\xabl\xa7(\xb1\xf1k\xc1_\xc0V\ -Y+E\xa4d\xa7\xac\x89\x0dz\xce\x9bQ#\x99s\ -;\xf7|\xee\xf7\xdes\xba\xf7\x5cpD\xd3\x8afV\ -\xfaA\xcbd\x8dp(\xe0\x9b\x99\x9d\xf3\xb9\x9e\xa8\xa2\ -\x85\x1a:\xf1\xc6\x14S\x9f\x8c\x8cF)k\xef\xb7T\ -\xd8\xf1\xba\xdb\xaeU\xfe\xdc\xbfV\xb7\x980\x15\xa8\xa8\ -\x16\x1eVt#+<&<\xb1\x9a\xd5m\xde\x12n\ -RR\xb1E\xe1\x13\xe1.C.(|c\xeb\xf1\x22\ -?\xdb\x9c,\xf2\xa7\xcdF4\x1c\x04G\x83\xb0/\xf9\ -\x8b\xe3\xbfXI\x19\x9a\xb0\xbc\x9c6-\xbd\xa2\xfc\xdc\ -\xc7~\x89;\x91\x99\x8eHl\x15\xf7b\x12&D\x00\ -\x1f\xe3\x8c\x10d\x80^\x86d\x1e\xa0\x9b>zdE\ -\x99|\x7f!\x7f\x8ae\xc9Ud\xd6\xc9a\xb0D\x92\ -\x14Y\xbaD]\x91\xea\x09\x89\xaa\xe8\x09\x19irv\ -\xff\xff\xf6\xd5T\xfb\xfb\x8a\xd5\xdd\x01\xa8z\xb4\xac\xd7\ -vpm\xc2W\xde\xb2>\x0e,\xeb\xeb\x10\x9c\x0fp\ -\x9e)\xe5/\xef\xc3\xe0\x9b\xe8\xf9\x92\xd6\xb6\x07\x9eu\ -8\xbd(i\xf1m8\xdb\x80\xe6{=f\xc4\x0a\x92\ -S\xdc\xa1\xaa\xf0r\x0c\xf5\xb3\xd0x\x05\xb5\xf3\xc5\x9e\ -\xfd\xecst\x07\xd15\xf9\xaaK\xd8\xd9\x85\x0e9\xef\ -Y\xf8\x06\x8e\xfdg\xf8\xfd\x8a\x18\x97\x00\x00\x00\x09p\ -HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x97IDAT\x18\x95m\xcf\xb1j\x02A\ -\x14\x85\xe1o\xb7\xb6\xd0'H=Vi\x03\xb1\xb4H\ -;l\xa5\xf19\xf6Y\x02VB\xbaa\x0a\x0b;\x1b\ -\x1bkA\x18\x02)m\xe3\xbe\x82\xcd\x06\x16\xd9\xdb\xdd\ -\x9f\xff\x5c\xee\xa9b*\x13Ls\x13nF&\xa6\xf2\ -\x82\xaeF\x8b\xdf\x98\xca\xfb\x88\xb4\xc0\x0f\xda\x1a[t\ -\xd8\xc7T\xc2@\x9ac\x8f?|U=|\xc5\x09w\ -\xbc\xa1\xc2\x193,r\x13.\xd5\xe0\xc2\x12\x07\x5cQ\ -#\xe0#7\xe1\xa8O\x0e\x7f\xda`\xd7\xaf\x9f\xb9\x09\ -\xdfc\x05\xff\xe5uLe\xf5\xcc\x1f\x0d3,\x83\xb6\ -\x06D\x83\x00\x00\x00\x00IEND\xaeB`\x82\ -\x00\x00\x00\xa6\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ -\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ -HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15;\xdc\ -;\x0c\x9b\x00\x00\x00*IDAT\x08\xd7c`\xc0\ -\x00\x8c\x0c\x0cs> \x0b\xa4\x08020 \x0b\xa6\ -\x08000B\x98\x10\xc1\x14\x01\x14\x13P\xb5\xa3\x01\ -\x00\xc6\xb9\x07\x90]f\x1f\x83\x00\x00\x00\x00IEN\ -D\xaeB`\x82\ -\x00\x00\x00\xa0\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ -\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ -HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1c\x1f$\ -\xc6\x09\x17\x00\x00\x00$IDAT\x08\xd7c`@\ -\x05\xff\xcf\xc3XL\xc8\x5c&dY&d\xc5p\x0e\ -\xa3!\x9c\xc3h\x88a\x1a\x0a\x00\x00m\x84\x09u7\ -\x9e\xd9#\x00\x00\x00\x00IEND\xaeB`\x82\ -\x00\x00\x00\xa6\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ -\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ -HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15;\xdc\ -;\x0c\x9b\x00\x00\x00*IDAT\x08\xd7c`\xc0\ -\x00\x8c\x0c\x0cs> \x0b\xa4\x08020 \x0b\xa6\ -\x08000B\x98\x10\xc1\x14\x01\x14\x13P\xb5\xa3\x01\ -\x00\xc6\xb9\x07\x90]f\x1f\x83\x00\x00\x00\x00IEN\ -D\xaeB`\x82\ " qt_resource_name = b"\ @@ -692,62 +695,6 @@ qt_resource_name = b"\ \x07\x03}\xc3\ \x00i\ \x00m\x00a\x00g\x00e\x00s\ -\x00\x15\ -\x0f\xf3\xc0\x07\ -\x00u\ -\x00p\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\x00l\x00e\x00d\ -\x00.\x00p\x00n\x00g\ -\x00\x12\ -\x01.\x03'\ -\x00c\ -\x00o\x00m\x00b\x00o\x00b\x00o\x00x\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\ -\x00g\ -\x00\x0e\ -\x04\xa2\xfc\xa7\ -\x00d\ -\x00o\x00w\x00n\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\x00g\ -\x00\x1b\ -\x03Z2'\ -\x00c\ -\x00o\x00m\x00b\x00o\x00b\x00o\x00x\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\ -\x00s\x00a\x00b\x00l\x00e\x00d\x00.\x00p\x00n\x00g\ -\x00\x18\ -\x03\x8e\xdeg\ -\x00r\ -\x00i\x00g\x00h\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\ -\x00l\x00e\x00d\x00.\x00p\x00n\x00g\ -\x00\x11\ -\x00\xb8\x8c\x07\ -\x00l\ -\x00e\x00f\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\x00.\x00p\x00n\x00g\ -\ -\x00\x0f\ -\x01s\x8b\x07\ -\x00u\ -\x00p\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\x00.\x00p\x00n\x00g\ -\x00\x0c\ -\x06\xe6\xe6g\ -\x00u\ -\x00p\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\x00g\ -\x00\x0f\ -\x06S%\xa7\ -\x00b\ -\x00r\x00a\x00n\x00c\x00h\x00_\x00o\x00p\x00e\x00n\x00.\x00p\x00n\x00g\ -\x00\x17\ -\x0ce\xce\x07\ -\x00l\ -\x00e\x00f\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\x00l\ -\x00e\x00d\x00.\x00p\x00n\x00g\ -\x00\x14\ -\x04^-\xa7\ -\x00b\ -\x00r\x00a\x00n\x00c\x00h\x00_\x00c\x00l\x00o\x00s\x00e\x00d\x00_\x00o\x00n\x00.\ -\x00p\x00n\x00g\ -\x00\x11\ -\x0b\xda0\xa7\ -\x00b\ -\x00r\x00a\x00n\x00c\x00h\x00_\x00c\x00l\x00o\x00s\x00e\x00d\x00.\x00p\x00n\x00g\ -\ \x00\x0e\ \x0e\xde\xfa\xc7\ \x00l\ @@ -757,87 +704,121 @@ qt_resource_name = b"\ \x00d\ \x00o\x00w\x00n\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\x00.\x00p\x00n\x00g\ \ -\x00\x0f\ -\x02\x9f\x05\x87\ -\x00r\ -\x00i\x00g\x00h\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\x00g\ +\x00\x12\ +\x01.\x03'\ +\x00c\ +\x00o\x00m\x00b\x00o\x00b\x00o\x00x\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\ +\x00g\ \x00\x12\ \x05\x8f\x9d\x07\ \x00b\ \x00r\x00a\x00n\x00c\x00h\x00_\x00o\x00p\x00e\x00n\x00_\x00o\x00n\x00.\x00p\x00n\ \x00g\ -\x00\x17\ -\x0c\xabQ\x07\ -\x00d\ -\x00o\x00w\x00n\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\x00l\ -\x00e\x00d\x00.\x00p\x00n\x00g\ \x00\x12\ \x03\x8d\x04G\ \x00r\ \x00i\x00g\x00h\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\x00.\x00p\x00n\ \x00g\ +\x00\x0f\ +\x01s\x8b\x07\ +\x00u\ +\x00p\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\x00.\x00p\x00n\x00g\ +\x00\x1b\ +\x03Z2'\ +\x00c\ +\x00o\x00m\x00b\x00o\x00b\x00o\x00x\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\ +\x00s\x00a\x00b\x00l\x00e\x00d\x00.\x00p\x00n\x00g\ +\x00\x14\ +\x04^-\xa7\ +\x00b\ +\x00r\x00a\x00n\x00c\x00h\x00_\x00c\x00l\x00o\x00s\x00e\x00d\x00_\x00o\x00n\x00.\ +\x00p\x00n\x00g\ +\x00\x0c\ +\x06\xe6\xe6g\ +\x00u\ +\x00p\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\x00g\ +\x00\x17\ +\x0c\xabQ\x07\ +\x00d\ +\x00o\x00w\x00n\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\x00l\ +\x00e\x00d\x00.\x00p\x00n\x00g\ \x00\x15\ \x03'rg\ \x00c\ \x00o\x00m\x00b\x00o\x00b\x00o\x00x\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\ \x00.\x00p\x00n\x00g\ +\x00\x0e\ +\x04\xa2\xfc\xa7\ +\x00d\ +\x00o\x00w\x00n\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\x00g\ +\x00\x18\ +\x03\x8e\xdeg\ +\x00r\ +\x00i\x00g\x00h\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\ +\x00l\x00e\x00d\x00.\x00p\x00n\x00g\ +\x00\x15\ +\x0f\xf3\xc0\x07\ +\x00u\ +\x00p\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\x00l\x00e\x00d\ +\x00.\x00p\x00n\x00g\ +\x00\x0f\ +\x06S%\xa7\ +\x00b\ +\x00r\x00a\x00n\x00c\x00h\x00_\x00o\x00p\x00e\x00n\x00.\x00p\x00n\x00g\ +\x00\x17\ +\x0ce\xce\x07\ +\x00l\ +\x00e\x00f\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\x00l\ +\x00e\x00d\x00.\x00p\x00n\x00g\ +\x00\x0f\ +\x02\x9f\x05\x87\ +\x00r\ +\x00i\x00g\x00h\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\x00g\ +\x00\x11\ +\x0b\xda0\xa7\ +\x00b\ +\x00r\x00a\x00n\x00c\x00h\x00_\x00c\x00l\x00o\x00s\x00e\x00d\x00.\x00p\x00n\x00g\ +\ +\x00\x11\ +\x00\xb8\x8c\x07\ +\x00l\ +\x00e\x00f\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\x00.\x00p\x00n\x00g\ +\ " qt_resource_struct = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ -\x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x16\x00\x02\x00\x00\x00\x13\x00\x00\x00\x03\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x16\x00\x00\x00\x00\x00\x01\x00\x00\x03C\ -\x00\x00\x01vA\x9d\xa25\ -\x00\x00\x02P\x00\x00\x00\x00\x00\x01\x00\x00\x1d!\ -\x00\x00\x01vA\x9d\xa25\ -\x00\x00\x00X\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa3\ -\x00\x00\x01y\xb4r\xcc\x9c\ -\x00\x00\x01>\x00\x00\x00\x00\x00\x01\x00\x00\x03\xed\ -\x00\x00\x01vA\x9d\xa29\ -\x00\x00\x02x\x00\x00\x00\x00\x00\x01\x00\x00\x1d\xca\ -\x00\x00\x01vA\x9d\xa27\ -\x00\x00\x03$\x00\x00\x00\x00\x00\x01\x00\x00&\xf0\ -\x00\x00\x01y\xb4r\xcc\x9c\ -\x00\x00\x00\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x01\xf6\ -\x00\x00\x01y\xb4r\xcc\x9c\ -\x00\x00\x02\xfa\x00\x00\x00\x00\x00\x01\x00\x00&L\ -\x00\x00\x01vA\x9d\xa27\ -\x00\x00\x00\xe0\x00\x00\x00\x00\x00\x01\x00\x00\x02\x9f\ -\x00\x00\x01vA\x9d\xa27\ -\x00\x00\x01\xd8\x00\x00\x00\x00\x00\x01\x00\x00\x0c\xe5\ -\x00\x00\x01y\xc2\x05+`\ -\x00\x00\x00\x82\x00\x00\x00\x00\x00\x01\x00\x00\x01M\ -\x00\x00\x01vA\x9d\xa25\ -\x00\x00\x02\x9c\x00\x00\x00\x00\x00\x01\x00\x00\x1en\ -\x00\x00\x01y\xc1\xfc\x16\x91\ -\x00\x00\x01\x80\x00\x00\x00\x00\x00\x01\x00\x00\x051\ -\x00\x00\x01y\xc1\xf9Kx\ -\x00\x00\x01b\x00\x00\x00\x00\x00\x01\x00\x00\x04\x8f\ -\x00\x00\x01vA\x9d\xa29\ -\x00\x00\x02\x06\x00\x00\x00\x00\x00\x01\x00\x00\x14\xc6\ -\x00\x00\x01y\xc2\x05\x91*\ -\x00\x00\x01\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x0c;\ -\x00\x00\x01vA\x9d\xa25\ -\x00\x00\x02\xc6\x00\x00\x00\x00\x00\x01\x00\x00%\xa2\ -\x00\x00\x01vA\x9d\xa25\ -\x00\x00\x02.\x00\x00\x00\x00\x00\x01\x00\x00\x1cw\ -\x00\x00\x01vA\x9d\xa25\ +\x00\x00\x03,\x00\x00\x00\x00\x00\x01\x00\x00&\xf0\ +\x00\x00\x00J\x00\x00\x00\x00\x00\x01\x00\x00\x00\xaa\ +\x00\x00\x00r\x00\x00\x00\x00\x00\x01\x00\x00\x01S\ +\x00\x00\x00\xf0\x00\x00\x00\x00\x00\x01\x00\x00\x09\xd5\ +\x00\x00\x02\xe0\x00\x00\x00\x00\x00\x01\x00\x00\x1e\x9b\ +\x00\x00\x01\xd0\x00\x00\x00\x00\x00\x01\x00\x00\x14M\ +\x00\x00\x01\x14\x00\x00\x00\x00\x00\x01\x00\x00\x0aw\ +\x00\x00\x00\xc6\x00\x00\x00\x00\x00\x01\x00\x00\x091\ +\x00\x00\x02\x22\x00\x00\x00\x00\x00\x01\x00\x00\x15\xa0\ +\x00\x00\x01P\x00\x00\x00\x00\x00\x01\x00\x00\x0b \ +\x00\x00\x02\x00\x00\x00\x00\x00\x00\x01\x00\x00\x14\xf7\ +\x00\x00\x00\x9c\x00\x00\x00\x00\x00\x01\x00\x00\x01\xfd\ +\x00\x00\x02\x88\x00\x00\x00\x00\x00\x01\x00\x00\x16\xe7\ +\x00\x00\x01~\x00\x00\x00\x00\x00\x01\x00\x00\x13\x01\ +\x00\x00\x03\x04\x00\x00\x00\x00\x00\x01\x00\x00\x1f?\ +\x00\x00\x02\xac\x00\x00\x00\x00\x00\x01\x00\x00\x1d\xf1\ +\x00\x00\x01\x9c\x00\x00\x00\x00\x00\x01\x00\x00\x13\xa3\ \x00\x00\x00(\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01vA\x9d\xa29\ +\x00\x00\x02X\x00\x00\x00\x00\x00\x01\x00\x00\x16D\ " def qInitResources(): QtCore.qRegisterResourceData( - 0x03, qt_resource_struct, qt_resource_name, qt_resource_data + 0x01, qt_resource_struct, qt_resource_name, qt_resource_data ) def qCleanupResources(): QtCore.qUnregisterResourceData( - 0x03, qt_resource_struct, qt_resource_name, qt_resource_data + 0x01, qt_resource_struct, qt_resource_name, qt_resource_data ) diff --git a/openpype/style/resources.qrc b/openpype/style/resources.qrc index a583d9458e..e2e69711f4 100644 --- a/openpype/style/resources.qrc +++ b/openpype/style/resources.qrc @@ -19,5 +19,6 @@ images/up_arrow.png images/up_arrow_disabled.png images/up_arrow_on.png + images/transparent.png diff --git a/openpype/style/style.css b/openpype/style/style.css index 830ed85f9b..d6f2460a27 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -200,12 +200,28 @@ QComboBox::down-arrow, QComboBox::down-arrow:on, QComboBox::down-arrow:hover, QC } /* Splitter */ -QSplitter { - border: none; +QSplitter::handle { + border: 3px solid transparent; } -QSplitter::handle { - border: 1px dotted {color:bg-menu-separator}; +QSplitter::handle:horizontal { + /* must be single like because of Nuke*/ + background: qlineargradient(x1:0, y1:0, x2:1, y2:0,stop:0.3 rgba(0, 0, 0, 0),stop:0.5 {color:bg-splitter},stop:0.7 rgba(0, 0, 0, 0)); +} + +QSplitter::handle:vertical { + /* must be single like because of Nuke*/ + background: qlineargradient(x1:0, y1:0, x2:0, y2:1,stop:0.3 rgba(0, 0, 0, 0),stop:0.5 {color:bg-splitter},stop:0.7 rgba(0, 0, 0, 0)); +} + +QSplitter::handle:horizontal:hover { + /* must be single like because of Nuke*/ + background: qlineargradient(x1:0, y1:0, x2:1, y2:0,stop:0.3 rgba(0, 0, 0, 0),stop:0.5 {color:bg-splitter-hover},stop:0.7 rgba(0, 0, 0, 0)); +} + +QSplitter::handle:vertical:hover { + /* must be single like because of Nuke*/ + background: qlineargradient(x1:0, y1:0, x2:0, y2:1,stop:0.3 rgba(0, 0, 0, 0),stop:0.5 {color:bg-splitter-hover},stop:0.7 rgba(0, 0, 0, 0)); } /* SLider */ @@ -232,18 +248,15 @@ QSlider::groove:focus { border-color: {color:border-focus}; } QSlider::handle { - background: qlineargradient( - x1: 0, y1: 0.5, - x2: 1, y2: 0.5, - stop: 0 {palette:blue-base}, - stop: 1 {palette:green-base} - ); + /* must be single like because of Nuke*/ + background: qlineargradient(x1: 0, y1: 0.5, x2: 1, y2: 0.5,stop: 0 {palette:blue-base},stop: 1 {palette:green-base}); border: 1px solid #5c5c5c; width: 10px; height: 10px; border-radius: 5px; } + QSlider::handle:horizontal { margin: -2px 0; } @@ -252,12 +265,8 @@ QSlider::handle:vertical { } QSlider::handle:disabled { - background: qlineargradient( - x1:0, y1:0, - x2:1, y2:1, - stop:0 {color:bg-buttons}, - stop:1 {color:bg-buttons-disabled} - ); + /* must be single like because of Nuke*/ + background: qlineargradient(x1:0, y1:0,x2:1, y2:1,stop:0 {color:bg-buttons},stop:1 {color:bg-buttons-disabled}); } /* Tab widget*/ @@ -275,19 +284,15 @@ QTabBar::tab { border-left: 3px solid transparent; border-top: 1px solid {color:border}; border-right: 1px solid {color:border}; - background: qlineargradient( - x1: 0, y1: 1, x2: 0, y2: 0, - stop: 0.5 {color:bg}, stop: 1.0 {color:bg-inputs} - ); + /* must be single like because of Nuke*/ + background: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0,stop: 0.5 {color:bg}, stop: 1.0 {color:bg-inputs}); } QTabBar::tab:selected { background: {color:grey-lighter}; border-left: 3px solid {color:border-focus}; - background: qlineargradient( - x1: 0, y1: 1, x2: 0, y2: 0, - stop: 0.5 {color:bg}, stop: 1.0 {color:border} - ); + /* must be single like because of Nuke*/ + background: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0,stop: 0.5 {color:bg}, stop: 1.0 {color:border}); } QTabBar::tab:!selected { @@ -335,6 +340,15 @@ QHeaderView::section:first { QHeaderView::section:last { border-right: none; } + +QHeaderView::down-arrow { + image: url(:/openpype/images/down_arrow.png); +} + +QHeaderView::up-arrow { + image: url(:/openpype/images/up_arrow.png); +} + /* Views QListView QTreeView QTableView */ QAbstractItemView { border: 0px solid {color:border}; @@ -393,23 +407,42 @@ QAbstractItemView::branch:open:has-children:has-siblings { QAbstractItemView::branch:open:has-children:!has-siblings:hover, QAbstractItemView::branch:open:has-children:has-siblings:hover { border-image: none; - image: url(:/openpype/images//branch_open_on.png); + image: url(:/openpype/images/branch_open_on.png); background: transparent; } QAbstractItemView::branch:has-children:!has-siblings:closed, QAbstractItemView::branch:closed:has-children:has-siblings { border-image: none; - image: url(:/openpype/images//branch_closed.png); + image: url(:/openpype/images/branch_closed.png); background: transparent; } QAbstractItemView::branch:has-children:!has-siblings:closed:hover, QAbstractItemView::branch:closed:has-children:has-siblings:hover { border-image: none; - image: url(:/openpype/images//branch_closed_on.png); + image: url(:/openpype/images/branch_closed_on.png); background: transparent; } +QAbstractItemView::branch:has-siblings:!adjoins-item { + border-image: none; + image: url(:/openpype/images/transparent.png); + background: transparent; +} + +QAbstractItemView::branch:has-siblings:adjoins-item { + border-image: none; + image: url(:/openpype/images/transparent.png); + background: transparent; +} + +QAbstractItemView::branch:!has-children:!has-siblings:adjoins-item { + border-image: none; + image: url(:/openpype/images/transparent.png); + background: transparent; +} + + /* Progress bar */ QProgressBar { border: 1px solid {color:border}; @@ -425,12 +458,8 @@ QProgressBar:vertical { } QProgressBar::chunk { - background: qlineargradient( - x1: 0, y1: 0.5, - x2: 1, y2: 0.5, - stop: 0 {palette:blue-base}, - stop: 1 {palette:green-base} - ); + /* must be single like because of Nuke*/ + background: qlineargradient(x1: 0, y1: 0.5,x2: 1, y2: 0.5,stop: 0 {palette:blue-base},stop: 1 {palette:green-base}); } /* Scroll bars */ @@ -629,3 +658,16 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { #PythonInterpreterOutput, #PythonCodeEditor { font-family: "Roboto Mono"; } + +#SubsetView::item, #RepresentationView:item { + padding: 5px 1px; + border: 0px; +} + +#OptionalActionBody, #OptionalActionOption { + background: transparent; +} + +#OptionalActionBody[state="hover"], #OptionalActionOption[state="hover"] { + background: {color:bg-view-hover}; +} diff --git a/openpype/tools/experimental_tools/__init__.py b/openpype/tools/experimental_tools/__init__.py new file mode 100644 index 0000000000..d6315e4655 --- /dev/null +++ b/openpype/tools/experimental_tools/__init__.py @@ -0,0 +1,14 @@ +from .tools_def import ( + ExperimentalTools, + LOCAL_EXPERIMENTAL_KEY +) + +from .dialog import ExperimentalToolsDialog + + +__all__ = ( + "ExperimentalTools", + "LOCAL_EXPERIMENTAL_KEY", + + "ExperimentalToolsDialog" +) diff --git a/openpype/tools/experimental_tools/dialog.py b/openpype/tools/experimental_tools/dialog.py new file mode 100644 index 0000000000..0fd170b31e --- /dev/null +++ b/openpype/tools/experimental_tools/dialog.py @@ -0,0 +1,212 @@ +from Qt import QtWidgets, QtCore, QtGui + +from openpype.style import ( + load_stylesheet, + app_icon_path +) + +from .tools_def import ExperimentalTools + + +class ToolButton(QtWidgets.QPushButton): + triggered = QtCore.Signal(str) + + def __init__(self, identifier, *args, **kwargs): + super(ToolButton, self).__init__(*args, **kwargs) + self._identifier = identifier + + self.clicked.connect(self._on_click) + + def _on_click(self): + self.triggered.emit(self._identifier) + + +class ExperimentalToolsDialog(QtWidgets.QDialog): + refresh_interval = 3000 + + def __init__(self, parent=None): + super(ExperimentalToolsDialog, self).__init__(parent) + self.setWindowTitle("OpenPype Experimental tools") + icon = QtGui.QIcon(app_icon_path()) + self.setWindowIcon(icon) + + # Widgets for cases there are not available experimental tools + empty_widget = QtWidgets.QWidget(self) + + empty_label = QtWidgets.QLabel( + "There are no experimental tools available...", empty_widget + ) + + empty_btns_layout = QtWidgets.QHBoxLayout() + ok_btn = QtWidgets.QPushButton("OK", empty_widget) + + empty_btns_layout.setContentsMargins(0, 0, 0, 0) + empty_btns_layout.addStretch(1) + empty_btns_layout.addWidget(ok_btn, 0) + + empty_layout = QtWidgets.QVBoxLayout(empty_widget) + empty_layout.setContentsMargins(0, 0, 0, 0) + empty_layout.addWidget(empty_label) + empty_layout.addStretch(1) + empty_layout.addLayout(empty_btns_layout) + + # Content of Experimental tools + + # Layout where buttons are added + content_layout = QtWidgets.QVBoxLayout() + content_layout.setContentsMargins(0, 0, 0, 0) + + # Separator line + separator_widget = QtWidgets.QWidget(self) + separator_widget.setObjectName("Separator") + separator_widget.setMinimumHeight(2) + separator_widget.setMaximumHeight(2) + + # Label describing how to turn off tools + tool_btns_widget = QtWidgets.QWidget(self) + tool_btns_label = QtWidgets.QLabel( + ( + "You can enable these features in" + "
OpenPype tray -> Settings -> Experimental tools" + ), + tool_btns_widget + ) + tool_btns_label.setAlignment(QtCore.Qt.AlignCenter) + + tool_btns_layout = QtWidgets.QVBoxLayout(tool_btns_widget) + tool_btns_layout.setContentsMargins(0, 0, 0, 0) + tool_btns_layout.addLayout(content_layout) + tool_btns_layout.addStretch(1) + tool_btns_layout.addWidget(separator_widget, 0) + tool_btns_layout.addWidget(tool_btns_label, 0) + + experimental_tools = ExperimentalTools() + + # Main layout + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(empty_widget, 1) + layout.addWidget(tool_btns_widget, 1) + + refresh_timer = QtCore.QTimer() + refresh_timer.setInterval(self.refresh_interval) + refresh_timer.timeout.connect(self._on_refresh_timeout) + + ok_btn.clicked.connect(self._on_ok_click) + + self._empty_widget = empty_widget + self._tool_btns_widget = tool_btns_widget + self._content_layout = content_layout + + self._experimental_tools = experimental_tools + self._buttons_by_tool_identifier = {} + + self._refresh_timer = refresh_timer + + # Is dialog first shown + self._first_show = True + # Trigger refresh when window get's activity + self._refresh_on_active = True + # Is window active + self._window_is_active = False + + def refresh(self): + self._experimental_tools.refresh_availability() + + buttons_to_remove = set(self._buttons_by_tool_identifier.keys()) + for idx, tool in enumerate(self._experimental_tools.tools): + identifier = tool.identifier + if identifier in buttons_to_remove: + buttons_to_remove.remove(identifier) + is_new = False + button = self._buttons_by_tool_identifier[identifier] + else: + is_new = True + button = ToolButton(identifier, self._tool_btns_widget) + button.triggered.connect(self._on_btn_trigger) + self._buttons_by_tool_identifier[identifier] = button + self._content_layout.insertWidget(idx, button) + + if button.text() != tool.label: + button.setText(tool.label) + + if tool.enabled: + button.setToolTip(tool.tooltip) + + elif is_new or button.isEnabled(): + button.setToolTip(( + "You can enable this tool in local settings." + "\n\nOpenPype Tray > Settings > Experimental Tools" + )) + + if tool.enabled != button.isEnabled(): + button.setEnabled(tool.enabled) + + for identifier in buttons_to_remove: + button = self._buttons_by_tool_identifier.pop(identifier) + button.setVisible(False) + idx = self._content_layout.indexOf(button) + self._content_layout.takeAt(idx) + button.deleteLater() + + self._set_visibility() + + def _is_content_visible(self): + return len(self._buttons_by_tool_identifier) > 0 + + def _set_visibility(self): + content_visible = self._is_content_visible() + self._tool_btns_widget.setVisible(content_visible) + self._empty_widget.setVisible(not content_visible) + + def _on_ok_click(self): + self.close() + + def _on_btn_trigger(self, identifier): + tool = self._experimental_tools.tools_by_identifier.get(identifier) + if tool is not None: + tool.execute() + + def showEvent(self, event): + super(ExperimentalToolsDialog, self).showEvent(event) + + if self._refresh_on_active: + # Start/Restart timer + self._refresh_timer.start() + # Refresh + self.refresh() + + elif not self._refresh_timer.isActive(): + self._refresh_timer.start() + + if self._first_show: + self._first_show = False + # Set stylesheet + self.setStyleSheet(load_stylesheet()) + # Resize dialog if there is not content + if not self._is_content_visible(): + size = self.size() + size.setWidth(size.width() + size.width() / 3) + self.resize(size) + + def changeEvent(self, event): + if event.type() == QtCore.QEvent.ActivationChange: + self._window_is_active = self.isActiveWindow() + if self._window_is_active and self._refresh_on_active: + self._refresh_timer.start() + self.refresh() + + super(ExperimentalToolsDialog, self).changeEvent(event) + + def _on_refresh_timeout(self): + # Stop timer if window is not visible + if not self.isVisible(): + self._refresh_on_active = True + self._refresh_timer.stop() + + # Skip refreshing if window is not active + elif not self._window_is_active: + self._refresh_on_active = True + + # Window is active and visible so we're refreshing buttons + else: + self.refresh() diff --git a/openpype/tools/experimental_tools/tools_def.py b/openpype/tools/experimental_tools/tools_def.py new file mode 100644 index 0000000000..254f542c4d --- /dev/null +++ b/openpype/tools/experimental_tools/tools_def.py @@ -0,0 +1,142 @@ +import os +from openpype.settings import get_local_settings + +# Constant key under which local settings are stored +LOCAL_EXPERIMENTAL_KEY = "experimental_tools" + + +class ExperimentalTool: + """Definition of experimental tool. + + Definition is used in local settings and in experimental tools dialog. + + Args: + identifier (str): String identifier of tool (unique). + label (str): Label shown in UI. + callback (function): Callback for UI button. + tooltip (str): Tooltip showed on button. + hosts_filter (list): List of host names for which is tool available. + Some tools may not be available in all hosts. + """ + def __init__( + self, identifier, label, callback, tooltip, hosts_filter=None + ): + self.identifier = identifier + self.label = label + self.callback = callback + self.tooltip = tooltip + self.hosts_filter = hosts_filter + self._enabled = True + + def is_available_for_host(self, host_name): + if self.hosts_filter: + return host_name in self.hosts_filter + return True + + @property + def enabled(self): + """Is tool enabled and button is clickable.""" + return self._enabled + + def set_enabled(self, enabled=True): + """Change if tool is enabled.""" + self._enabled = enabled + + def execute(self): + """Trigger registerd callback.""" + self.callback() + + +class ExperimentalTools: + """Wrapper around experimental tools. + + To add/remove experimental tool just add/remove tool to + `experimental_tools` variable in __init__ function. + + Args: + parent (QtWidgets.QWidget): Parent widget for tools. + host_name (str): Name of host in which context we're now. Environment + value 'AVALON_APP' is used when not passed. + filter_hosts (bool): Should filter tools. By default is set to 'True' + when 'host_name' is passed. Is always set to 'False' if 'host_name' + is not defined. + """ + def __init__(self, parent=None, host_name=None, filter_hosts=None): + # Definition of experimental tools + experimental_tools = [] + + # --- Example tool (callback will just print on click) --- + # def example_callback(*args): + # print("Triggered tool") + # + # experimental_tools = [ + # ExperimentalTool( + # "example", + # "Example experimental tool", + # example_callback, + # "Example tool tooltip." + # ) + # ] + + # Try to get host name from env variable `AVALON_APP` + if not host_name: + host_name = os.environ.get("AVALON_APP") + + # Decide if filtering by host name should happen + if filter_hosts is None: + filter_hosts = host_name is not None + + if filter_hosts and not host_name: + filter_hosts = False + + # Filter tools by host name + if filter_hosts: + experimental_tools = [ + tool + for tool in experimental_tools + if tool.is_available_for_host(host_name) + ] + + # Store tools by identifier + tools_by_identifier = {} + for tool in experimental_tools: + if tool.identifier in tools_by_identifier: + raise KeyError(( + "Duplicated experimental tool identifier \"{}\"" + ).format(tool.identifier)) + tools_by_identifier[tool.identifier] = tool + + self._tools_by_identifier = tools_by_identifier + self._tools = experimental_tools + self._parent_widget = parent + + @property + def tools(self): + """Tools in list. + + Returns: + list: Tools filtered by host name if filtering was enabled + on initialization. + """ + return self._tools + + @property + def tools_by_identifier(self): + """Tools by their identifier. + + Returns: + dict: Tools by identifier filtered by host name if filtering + was enabled on initialization. + """ + return self._tools_by_identifier + + def refresh_availability(self): + """Reload local settings and check if any tool changed ability.""" + local_settings = get_local_settings() + experimental_settings = ( + local_settings.get(LOCAL_EXPERIMENTAL_KEY) + ) or {} + + for identifier, eperimental_tool in self.tools_by_identifier.items(): + enabled = experimental_settings.get(identifier, False) + eperimental_tool.set_enabled(enabled) diff --git a/openpype/tools/libraryloader/app.py b/openpype/tools/libraryloader/app.py index 3f11157418..d7c6c162e6 100644 --- a/openpype/tools/libraryloader/app.py +++ b/openpype/tools/libraryloader/app.py @@ -2,8 +2,8 @@ import sys from Qt import QtWidgets, QtCore, QtGui -from avalon import style from avalon.api import AvalonMongoDB +from openpype import style from openpype.tools.utils import lib as tools_lib from openpype.tools.loader.widgets import ( ThumbnailWidget, @@ -28,155 +28,182 @@ class LibraryLoaderWindow(QtWidgets.QDialog): tool_title = "Library Loader 0.5" tool_name = "library_loader" + message_timeout = 5000 + def __init__( self, parent=None, icon=None, show_projects=False, show_libraries=True ): super(LibraryLoaderWindow, self).__init__(parent) - self._initial_refresh = False - self._ignore_project_change = False - - # Enable minimize and maximize for app + # Window modifications self.setWindowTitle(self.tool_title) window_flags = QtCore.Qt.Window if not parent: window_flags |= QtCore.Qt.WindowStaysOnTopHint self.setWindowFlags(window_flags) self.setFocusPolicy(QtCore.Qt.StrongFocus) - if icon is not None: - self.setWindowIcon(icon) - # self.setAttribute(QtCore.Qt.WA_DeleteOnClose) - body = QtWidgets.QWidget() - footer = QtWidgets.QWidget() - footer.setFixedHeight(20) + icon = QtGui.QIcon(style.app_icon_path()) + self.setWindowIcon(icon) - container = QtWidgets.QWidget() + self._first_show = True + self._initial_refresh = False + self._ignore_project_change = False - self.dbcon = AvalonMongoDB() - self.dbcon.install() - self.dbcon.Session["AVALON_PROJECT"] = None + dbcon = AvalonMongoDB() + dbcon.install() + dbcon.Session["AVALON_PROJECT"] = None + + self.dbcon = dbcon self.show_projects = show_projects self.show_libraries = show_libraries # Groups config - self.groups_config = tools_lib.GroupsConfig(self.dbcon) - self.family_config_cache = tools_lib.FamilyConfigCache(self.dbcon) + self.groups_config = tools_lib.GroupsConfig(dbcon) + self.family_config_cache = tools_lib.FamilyConfigCache(dbcon) - assets = AssetWidget( - self.dbcon, multiselection=True, parent=self + # UI initialization + main_splitter = QtWidgets.QSplitter(self) + + # --- Left part --- + left_side_splitter = QtWidgets.QSplitter(main_splitter) + left_side_splitter.setOrientation(QtCore.Qt.Vertical) + + # Project combobox + projects_combobox = QtWidgets.QComboBox(left_side_splitter) + combobox_delegate = QtWidgets.QStyledItemDelegate(self) + projects_combobox.setItemDelegate(combobox_delegate) + + # Assets widget + assets_widget = AssetWidget( + dbcon, multiselection=True, parent=left_side_splitter ) - families = FamilyListView( - self.dbcon, self.family_config_cache, parent=self + + # Families widget + families_filter_view = FamilyListView( + dbcon, self.family_config_cache, left_side_splitter ) - subsets = LibrarySubsetWidget( - self.dbcon, + left_side_splitter.addWidget(projects_combobox) + left_side_splitter.addWidget(assets_widget) + left_side_splitter.addWidget(families_filter_view) + left_side_splitter.setStretchFactor(1, 65) + left_side_splitter.setStretchFactor(2, 35) + + # --- Middle part --- + # Subsets widget + subsets_widget = LibrarySubsetWidget( + dbcon, self.groups_config, self.family_config_cache, tool_name=self.tool_name, parent=self ) - version = VersionWidget(self.dbcon) - thumbnail = ThumbnailWidget(self.dbcon) - - # Project - self.combo_projects = QtWidgets.QComboBox() - - # Create splitter to show / hide family filters - asset_filter_splitter = QtWidgets.QSplitter() - asset_filter_splitter.setOrientation(QtCore.Qt.Vertical) - asset_filter_splitter.addWidget(self.combo_projects) - asset_filter_splitter.addWidget(assets) - asset_filter_splitter.addWidget(families) - asset_filter_splitter.setStretchFactor(1, 65) - asset_filter_splitter.setStretchFactor(2, 35) - - manager = ModulesManager() - sync_server = manager.modules_by_name["sync_server"] - - representations = RepresentationWidget(self.dbcon) - thumb_ver_splitter = QtWidgets.QSplitter() + # --- Right part --- + thumb_ver_splitter = QtWidgets.QSplitter(main_splitter) thumb_ver_splitter.setOrientation(QtCore.Qt.Vertical) - thumb_ver_splitter.addWidget(thumbnail) - thumb_ver_splitter.addWidget(version) - if sync_server.enabled: - thumb_ver_splitter.addWidget(representations) + + thumbnail_widget = ThumbnailWidget(dbcon, parent=thumb_ver_splitter) + version_info_widget = VersionWidget(dbcon, parent=thumb_ver_splitter) + + thumb_ver_splitter.addWidget(thumbnail_widget) + thumb_ver_splitter.addWidget(version_info_widget) + thumb_ver_splitter.setStretchFactor(0, 30) thumb_ver_splitter.setStretchFactor(1, 35) - container_layout = QtWidgets.QHBoxLayout(container) - container_layout.setContentsMargins(0, 0, 0, 0) - split = QtWidgets.QSplitter() - split.addWidget(asset_filter_splitter) - split.addWidget(subsets) - split.addWidget(thumb_ver_splitter) - split.setSizes([180, 950, 200]) - container_layout.addWidget(split) + manager = ModulesManager() + sync_server = manager.modules_by_name.get("sync_server") + sync_server_enabled = False + if sync_server is not None: + sync_server_enabled = sync_server.enabled - body_layout = QtWidgets.QHBoxLayout(body) - body_layout.addWidget(container) - body_layout.setContentsMargins(0, 0, 0, 0) + repres_widget = None + if sync_server_enabled: + repres_widget = RepresentationWidget( + dbcon, self.tool_name, parent=thumb_ver_splitter + ) + thumb_ver_splitter.addWidget(repres_widget) - message = QtWidgets.QLabel() - message.hide() + main_splitter.addWidget(left_side_splitter) + main_splitter.addWidget(subsets_widget) + main_splitter.addWidget(thumb_ver_splitter) + if sync_server_enabled: + main_splitter.setSizes([250, 1000, 550]) + else: + main_splitter.setSizes([250, 850, 200]) - footer_layout = QtWidgets.QVBoxLayout(footer) - footer_layout.addWidget(message) + # --- Footer --- + footer_widget = QtWidgets.QWidget(self) + footer_widget.setFixedHeight(20) + + message_label = QtWidgets.QLabel(footer_widget) + + footer_layout = QtWidgets.QVBoxLayout(footer_widget) footer_layout.setContentsMargins(0, 0, 0, 0) + footer_layout.addWidget(message_label) layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(body) - layout.addWidget(footer) + layout.addWidget(main_splitter) + layout.addWidget(footer_widget) self.data = { - "widgets": { - "families": families, - "assets": assets, - "subsets": subsets, - "version": version, - "thumbnail": thumbnail, - "representations": representations - }, - "label": { - "message": message, - }, "state": { "assetIds": None } } - families.active_changed.connect(subsets.set_family_filters) - assets.selection_changed.connect(self.on_assetschanged) - assets.refresh_triggered.connect(self.on_assetschanged) - assets.view.clicked.connect(self.on_assetview_click) - subsets.active_changed.connect(self.on_subsetschanged) - subsets.version_changed.connect(self.on_versionschanged) - subsets.refreshed.connect(self._on_subset_refresh) - self.combo_projects.currentTextChanged.connect(self.on_project_change) + message_timer = QtCore.QTimer() + message_timer.setInterval(self.message_timeout) + message_timer.setSingleShot(True) + + message_timer.timeout.connect(self._on_message_timeout) + + families_filter_view.active_changed.connect( + self._on_family_filter_change + ) + assets_widget.selection_changed.connect(self.on_assetschanged) + assets_widget.refresh_triggered.connect(self.on_assetschanged) + assets_widget.view.clicked.connect(self.on_assetview_click) + subsets_widget.active_changed.connect(self.on_subsetschanged) + subsets_widget.version_changed.connect(self.on_versionschanged) + subsets_widget.refreshed.connect(self._on_subset_refresh) + projects_combobox.currentTextChanged.connect(self.on_project_change) self.sync_server = sync_server + self._sync_server_enabled = sync_server_enabled - # Set default thumbnail on start - thumbnail.set_thumbnail(None) + self._combobox_delegate = combobox_delegate + self._projects_combobox = projects_combobox + self._assets_widget = assets_widget + self._families_filter_view = families_filter_view - # Defaults - if sync_server.enabled: - split.setSizes([250, 1000, 550]) - self.resize(1800, 900) - else: - split.setSizes([250, 850, 200]) - self.resize(1300, 700) + self._subsets_widget = subsets_widget + + self._version_info_widget = version_info_widget + self._thumbnail_widget = thumbnail_widget + self._repres_widget = repres_widget + + self._message_label = message_label + self._message_timer = message_timer def showEvent(self, event): super(LibraryLoaderWindow, self).showEvent(event) + if self._first_show: + self._first_show = False + self.setStyleSheet(style.load_stylesheet()) + if self._sync_server_enabled: + self.resize(1800, 900) + else: + self.resize(1300, 700) + if not self._initial_refresh: + self._initial_refresh = True self.refresh() def on_assetview_click(self, *args): - subsets_widget = self.data["widgets"]["subsets"] - selection_model = subsets_widget.view.selectionModel() + selection_model = self._subsets_widget.view.selectionModel() if selection_model.selectedIndexes(): selection_model.clearSelection() @@ -187,7 +214,7 @@ class LibraryLoaderWindow(QtWidgets.QDialog): self._ignore_project_change = True # Cleanup - self.combo_projects.clear() + self._projects_combobox.clear() # Fill combobox with projects select_project_item = QtGui.QStandardItem("< Select project >") @@ -202,18 +229,18 @@ class LibraryLoaderWindow(QtWidgets.QDialog): item.setData(project_name, QtCore.Qt.UserRole + 1) combobox_items.append(item) - root_item = self.combo_projects.model().invisibleRootItem() + root_item = self._projects_combobox.model().invisibleRootItem() root_item.appendRows(combobox_items) index = 0 self._ignore_project_change = False if old_project_name: - index = self.combo_projects.findText( + index = self._projects_combobox.findText( old_project_name, QtCore.Qt.MatchFixedString ) - self.combo_projects.setCurrentIndex(index) + self._projects_combobox.setCurrentIndex(index) def get_filtered_projects(self): projects = list() @@ -231,8 +258,8 @@ class LibraryLoaderWindow(QtWidgets.QDialog): if self._ignore_project_change: return - row = self.combo_projects.currentIndex() - index = self.combo_projects.model().index(row, 0) + row = self._projects_combobox.currentIndex() + index = self._projects_combobox.model().index(row, 0) project_name = index.data(QtCore.Qt.UserRole + 1) self.dbcon.Session["AVALON_PROJECT"] = project_name @@ -245,11 +272,9 @@ class LibraryLoaderWindow(QtWidgets.QDialog): "Config `%s` has no function `install`" % _config.__name__ ) - subsets = self.data["widgets"]["subsets"] - representations = self.data["widgets"]["representations"] - - subsets.on_project_change(self.dbcon.Session["AVALON_PROJECT"]) - representations.on_project_change(self.dbcon.Session["AVALON_PROJECT"]) + self._subsets_widget.on_project_change(project_name) + if self._repres_widget: + self._repres_widget.on_project_change(project_name) self.family_config_cache.refresh() self.groups_config.refresh() @@ -263,13 +288,7 @@ class LibraryLoaderWindow(QtWidgets.QDialog): @property def current_project(self): - if ( - not self.dbcon.active_project() or - self.dbcon.active_project() == "" - ): - return None - - return self.dbcon.active_project() + return self.dbcon.active_project() or None # ------------------------------- # Delay calling blocking methods @@ -292,12 +311,11 @@ class LibraryLoaderWindow(QtWidgets.QDialog): tools_lib.schedule(self._versionschanged, 150, channel="mongo") def _on_subset_refresh(self, has_item): - subsets_widget = self.data["widgets"]["subsets"] - families_view = self.data["widgets"]["families"] - - subsets_widget.set_loading_state(loading=False, empty=not has_item) - families = subsets_widget.get_subsets_families() - families_view.set_enabled_families(families) + self._subsets_widget.set_loading_state( + loading=False, empty=not has_item + ) + families = self._subsets_widget.get_subsets_families() + self._families_filter_view.set_enabled_families(families) def set_context(self, context, refresh=True): self.echo("Setting context: {}".format(context)) @@ -307,6 +325,9 @@ class LibraryLoaderWindow(QtWidgets.QDialog): ) # ------------------------------ + def _on_family_filter_change(self, families): + self._subsets_widget.set_family_filters(families) + def _refresh(self): if not self._initial_refresh: self._initial_refresh = True @@ -322,74 +343,69 @@ class LibraryLoaderWindow(QtWidgets.QDialog): ) assert project_doc, "This is a bug" - assets_widget = self.data["widgets"]["assets"] - families_view = self.data["widgets"]["families"] - families_view.set_enabled_families(set()) - families_view.refresh() + self._families_filter_view.set_enabled_families(set()) + self._families_filter_view.refresh() - assets_widget.model.stop_fetch_thread() - assets_widget.refresh() - assets_widget.setFocus() + self._assets_widget.model.stop_fetch_thread() + self._assets_widget.refresh() + self._assets_widget.setFocus() def clear_assets_underlines(self): last_asset_ids = self.data["state"]["assetIds"] if not last_asset_ids: return - assets_widget = self.data["widgets"]["assets"] - id_role = assets_widget.model.ObjectIdRole + assets_model = self._assets_widget.model + id_role = assets_model.ObjectIdRole - for index in tools_lib.iter_model_rows(assets_widget.model, 0): + for index in tools_lib.iter_model_rows(assets_model, 0): if index.data(id_role) not in last_asset_ids: continue - assets_widget.model.setData( - index, [], assets_widget.model.subsetColorsRole + assets_model.setData( + index, [], assets_model.subsetColorsRole ) def _assetschanged(self): """Selected assets have changed""" - assets_widget = self.data["widgets"]["assets"] - subsets_widget = self.data["widgets"]["subsets"] - subsets_model = subsets_widget.model + subsets_model = self._subsets_widget.model subsets_model.clear() self.clear_assets_underlines() if not self.dbcon.Session.get("AVALON_PROJECT"): - subsets_widget.set_loading_state( + self._subsets_widget.set_loading_state( loading=False, empty=True ) return # filter None docs they are silo - asset_docs = assets_widget.get_selected_assets() + asset_docs = self._assets_widget.get_selected_assets() if len(asset_docs) == 0: return asset_ids = [asset_doc["_id"] for asset_doc in asset_docs] # Start loading - subsets_widget.set_loading_state( + self._subsets_widget.set_loading_state( loading=bool(asset_ids), empty=True ) subsets_model.set_assets(asset_ids) - subsets_widget.view.setColumnHidden( + self._subsets_widget.view.setColumnHidden( subsets_model.Columns.index("asset"), len(asset_ids) < 2 ) # Clear the version information on asset change - self.data["widgets"]["version"].set_version(None) - self.data["widgets"]["thumbnail"].set_thumbnail(asset_docs) + self._version_info_widget.set_version(None) + self._thumbnail_widget.set_thumbnail(asset_docs) self.data["state"]["assetIds"] = asset_ids - representations = self.data["widgets"]["representations"] # reset repre list - representations.set_version_ids([]) + self._repres_widget.set_version_ids([]) def _subsetschanged(self): asset_ids = self.data["state"]["assetIds"] @@ -398,8 +414,9 @@ class LibraryLoaderWindow(QtWidgets.QDialog): self._versionschanged() return - subsets = self.data["widgets"]["subsets"] - selected_subsets = subsets.selected_subsets(_merged=True, _other=False) + selected_subsets = self._subsets_widget.selected_subsets( + _merged=True, _other=False + ) asset_models = {} asset_ids = [] @@ -420,26 +437,24 @@ class LibraryLoaderWindow(QtWidgets.QDialog): self.clear_assets_underlines() - assets_widget = self.data["widgets"]["assets"] - indexes = assets_widget.view.selectionModel().selectedRows() + indexes = self._assets_widget.view.selectionModel().selectedRows() + assets_model = self._assets_widget.model for index in indexes: - id = index.data(assets_widget.model.ObjectIdRole) + id = index.data(assets_model.ObjectIdRole) if id not in asset_models: continue - assets_widget.model.setData( - index, asset_models[id], assets_widget.model.subsetColorsRole + assets_model.setData( + index, asset_models[id], assets_model.subsetColorsRole ) # Trigger repaint - assets_widget.view.updateGeometries() + self._assets_widget.view.updateGeometries() # Set version in Version Widget self._versionschanged() def _versionschanged(self): - - subsets = self.data["widgets"]["subsets"] - selection = subsets.view.selectionModel() + selection = self._subsets_widget.view.selectionModel() # Active must be in the selected rows otherwise we # assume it's not actually an "active" current index. @@ -448,7 +463,7 @@ class LibraryLoaderWindow(QtWidgets.QDialog): active = selection.currentIndex() rows = selection.selectedRows(column=active.column()) if active and active in rows: - item = active.data(subsets.model.ItemRole) + item = active.data(self._subsets_widget.model.ItemRole) if ( item is not None and not (item.get("isGroup") or item.get("isMerged")) @@ -460,7 +475,7 @@ class LibraryLoaderWindow(QtWidgets.QDialog): for index in rows: if not index or not index.isValid(): continue - item = index.data(subsets.model.ItemRole) + item = index.data(self._subsets_widget.model.ItemRole) if ( item is None or item.get("isGroup") @@ -469,20 +484,18 @@ class LibraryLoaderWindow(QtWidgets.QDialog): continue version_docs.append(item["version_document"]) - self.data["widgets"]["version"].set_version(version_doc) + self._version_info_widget.set_version(version_doc) thumbnail_docs = version_docs if not thumbnail_docs: - assets_widget = self.data["widgets"]["assets"] - asset_docs = assets_widget.get_selected_assets() + asset_docs = self._assets_widget.get_selected_assets() if len(asset_docs) > 0: thumbnail_docs = asset_docs - self.data["widgets"]["thumbnail"].set_thumbnail(thumbnail_docs) + self._thumbnail_widget.set_thumbnail(thumbnail_docs) - representations = self.data["widgets"]["representations"] version_ids = [doc["_id"] for doc in version_docs or []] - representations.set_version_ids(version_ids) + self._repres_widget.set_version_ids(version_ids) def _set_context(self, context, refresh=True): """Set the selection in the interface using a context. @@ -510,16 +523,15 @@ class LibraryLoaderWindow(QtWidgets.QDialog): # scheduled refresh and the silo tabs are not shown. self._refresh_assets() - asset_widget = self.data["widgets"]["assets"] - asset_widget.select_assets(asset) + self._assets_widget.select_assets(asset) + + def _on_message_timeout(self): + self._message_label.setText("") def echo(self, message): - widget = self.data["label"]["message"] - widget.setText(str(message)) - widget.show() + self._message_label.setText(str(message)) print(message) - - tools_lib.schedule(widget.hide, 5000, channel="message") + self._message_timer.start() def closeEvent(self, event): # Kill on holding SHIFT @@ -576,7 +588,6 @@ def show( window = LibraryLoaderWindow( parent, icon, show_projects, show_libraries ) - window.setStyleSheet(style.load_stylesheet()) window.show() module.window = window diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index bc0eef3bca..04da08326f 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -1,10 +1,10 @@ import sys from Qt import QtWidgets, QtCore -from avalon import api, io, style, pipeline +from avalon import api, io, pipeline +from openpype import style from openpype.tools.utils.widgets import AssetWidget - from openpype.tools.utils import lib from .widgets import ( @@ -37,6 +37,7 @@ class LoaderWindow(QtWidgets.QDialog): """Asset loader interface""" tool_name = "loader" + message_timeout = 5000 def __init__(self, parent=None): super(LoaderWindow, self).__init__(parent) @@ -57,83 +58,85 @@ class LoaderWindow(QtWidgets.QDialog): self.setWindowFlags(window_flags) self.setFocusPolicy(QtCore.Qt.StrongFocus) - body = QtWidgets.QWidget() - footer = QtWidgets.QWidget() - footer.setFixedHeight(20) + main_splitter = QtWidgets.QSplitter(self) - container = QtWidgets.QWidget() + # --- Left part --- + left_side_splitter = QtWidgets.QSplitter(main_splitter) + left_side_splitter.setOrientation(QtCore.Qt.Vertical) - assets = AssetWidget(io, multiselection=True, parent=self) - assets.set_current_asset_btn_visibility(True) + # Assets widget + assets_widget = AssetWidget( + io, multiselection=True, parent=left_side_splitter + ) + assets_widget.set_current_asset_btn_visibility(True) - families = FamilyListView(io, self.family_config_cache, self) - subsets = SubsetWidget( + # Families widget + families_filter_view = FamilyListView( + io, self.family_config_cache, left_side_splitter + ) + left_side_splitter.addWidget(assets_widget) + left_side_splitter.addWidget(families_filter_view) + left_side_splitter.setStretchFactor(0, 65) + left_side_splitter.setStretchFactor(1, 35) + + # --- Middle part --- + # Subsets widget + subsets_widget = SubsetWidget( io, self.groups_config, self.family_config_cache, tool_name=self.tool_name, - parent=self + parent=main_splitter ) - version = VersionWidget(io) - thumbnail = ThumbnailWidget(io) - representations = RepresentationWidget(io, self.tool_name) - manager = ModulesManager() - sync_server = manager.modules_by_name["sync_server"] - - thumb_ver_splitter = QtWidgets.QSplitter() + # --- Right part --- + thumb_ver_splitter = QtWidgets.QSplitter(main_splitter) thumb_ver_splitter.setOrientation(QtCore.Qt.Vertical) - thumb_ver_splitter.addWidget(thumbnail) - thumb_ver_splitter.addWidget(version) - if sync_server.enabled: - thumb_ver_splitter.addWidget(representations) + + thumbnail_widget = ThumbnailWidget(io, parent=thumb_ver_splitter) + version_info_widget = VersionWidget(io, parent=thumb_ver_splitter) + + thumb_ver_splitter.addWidget(thumbnail_widget) + thumb_ver_splitter.addWidget(version_info_widget) + thumb_ver_splitter.setStretchFactor(0, 30) thumb_ver_splitter.setStretchFactor(1, 35) - # Create splitter to show / hide family filters - asset_filter_splitter = QtWidgets.QSplitter() - asset_filter_splitter.setOrientation(QtCore.Qt.Vertical) - asset_filter_splitter.addWidget(assets) - asset_filter_splitter.addWidget(families) - asset_filter_splitter.setStretchFactor(0, 65) - asset_filter_splitter.setStretchFactor(1, 35) + manager = ModulesManager() + sync_server = manager.modules_by_name.get("sync_server") + sync_server_enabled = False + if sync_server is not None: + sync_server_enabled = sync_server.enabled - container_layout = QtWidgets.QHBoxLayout(container) - container_layout.setContentsMargins(0, 0, 0, 0) - split = QtWidgets.QSplitter() - split.addWidget(asset_filter_splitter) - split.addWidget(subsets) - split.addWidget(thumb_ver_splitter) + repres_widget = None + if sync_server_enabled: + repres_widget = RepresentationWidget( + io, self.tool_name, parent=thumb_ver_splitter + ) + thumb_ver_splitter.addWidget(repres_widget) - container_layout.addWidget(split) + main_splitter.addWidget(left_side_splitter) + main_splitter.addWidget(subsets_widget) + main_splitter.addWidget(thumb_ver_splitter) - body_layout = QtWidgets.QHBoxLayout(body) - body_layout.addWidget(container) - body_layout.setContentsMargins(0, 0, 0, 0) + if sync_server_enabled: + main_splitter.setSizes([250, 1000, 550]) + else: + main_splitter.setSizes([250, 850, 200]) - message = QtWidgets.QLabel() - message.hide() + footer_widget = QtWidgets.QWidget(self) - footer_layout = QtWidgets.QVBoxLayout(footer) - footer_layout.addWidget(message) + message_label = QtWidgets.QLabel(footer_widget) + + footer_layout = QtWidgets.QHBoxLayout(footer_widget) footer_layout.setContentsMargins(0, 0, 0, 0) + footer_layout.addWidget(message_label, 1) layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(body) - layout.addWidget(footer) + layout.addWidget(main_splitter, 1) + layout.addWidget(footer_widget, 0) self.data = { - "widgets": { - "families": families, - "assets": assets, - "subsets": subsets, - "version": version, - "thumbnail": thumbnail, - "representations": representations - }, - "label": { - "message": message, - }, "state": { "assetIds": None } @@ -142,19 +145,44 @@ class LoaderWindow(QtWidgets.QDialog): overlay_frame = OverlayFrame("Loading...", self) overlay_frame.setVisible(False) - families.active_changed.connect(subsets.set_family_filters) - assets.selection_changed.connect(self.on_assetschanged) - assets.refresh_triggered.connect(self.on_assetschanged) - assets.view.clicked.connect(self.on_assetview_click) - subsets.active_changed.connect(self.on_subsetschanged) - subsets.version_changed.connect(self.on_versionschanged) - subsets.refreshed.connect(self._on_subset_refresh) + message_timer = QtCore.QTimer() + message_timer.setInterval(self.message_timeout) + message_timer.setSingleShot(True) - subsets.load_started.connect(self._on_load_start) - subsets.load_ended.connect(self._on_load_end) - representations.load_started.connect(self._on_load_start) - representations.load_ended.connect(self._on_load_end) + message_timer.timeout.connect(self._on_message_timeout) + families_filter_view.active_changed.connect( + self._on_family_filter_change + ) + assets_widget.selection_changed.connect(self.on_assetschanged) + assets_widget.refresh_triggered.connect(self.on_assetschanged) + # TODO do not touch view in asset widget + assets_widget.view.clicked.connect(self.on_assetview_click) + subsets_widget.active_changed.connect(self.on_subsetschanged) + subsets_widget.version_changed.connect(self.on_versionschanged) + subsets_widget.refreshed.connect(self._on_subset_refresh) + + subsets_widget.load_started.connect(self._on_load_start) + subsets_widget.load_ended.connect(self._on_load_end) + if repres_widget: + repres_widget.load_started.connect(self._on_load_start) + repres_widget.load_ended.connect(self._on_load_end) + + self._sync_server_enabled = sync_server_enabled + + self._assets_widget = assets_widget + self._families_filter_view = families_filter_view + + self._subsets_widget = subsets_widget + + self._version_info_widget = version_info_widget + self._thumbnail_widget = thumbnail_widget + self._repres_widget = repres_widget + + self._message_label = message_label + self._message_timer = message_timer + + # TODO add overlay using stack widget self._overlay_frame = overlay_frame self.family_config_cache.refresh() @@ -163,13 +191,7 @@ class LoaderWindow(QtWidgets.QDialog): self._refresh() self._assetschanged() - # Defaults - if sync_server.enabled: - split.setSizes([250, 1000, 550]) - self.resize(1800, 900) - else: - split.setSizes([250, 850, 200]) - self.resize(1300, 700) + self._first_show = True def resizeEvent(self, event): super(LoaderWindow, self).resizeEvent(event) @@ -179,13 +201,23 @@ class LoaderWindow(QtWidgets.QDialog): super(LoaderWindow, self).moveEvent(event) self._overlay_frame.move(0, 0) + def showEvent(self, event): + super(LoaderWindow, self).showEvent(event) + if self._first_show: + self._first_show = False + self.setStyleSheet(style.load_stylesheet()) + if self._sync_server_enabled: + self.resize(1800, 900) + else: + self.resize(1300, 700) + # ------------------------------- # Delay calling blocking methods # ------------------------------- def on_assetview_click(self, *args): - subsets_widget = self.data["widgets"]["subsets"] - selection_model = subsets_widget.view.selectionModel() + # TODO do not touch inner attributes of subset widget + selection_model = self._subsets_widget.view.selectionModel() if selection_model.selectedIndexes(): selection_model.clearSelection() @@ -219,12 +251,11 @@ class LoaderWindow(QtWidgets.QDialog): self._overlay_frame.setVisible(False) def _on_subset_refresh(self, has_item): - subsets_widget = self.data["widgets"]["subsets"] - families_view = self.data["widgets"]["families"] - - subsets_widget.set_loading_state(loading=False, empty=not has_item) - families = subsets_widget.get_subsets_families() - families_view.set_enabled_families(families) + self._subsets_widget.set_loading_state( + loading=False, empty=not has_item + ) + families = self._subsets_widget.get_subsets_families() + self._families_filter_view.set_enabled_families(families) def _on_load_end(self): # Delay hiding as click events happened during loading should be @@ -232,14 +263,14 @@ class LoaderWindow(QtWidgets.QDialog): QtCore.QTimer.singleShot(100, self._hide_overlay) # ------------------------------ + def _on_family_filter_change(self, families): + self._subsets_widget.set_family_filters(families) def on_context_task_change(self, *args, **kwargs): - assets_widget = self.data["widgets"]["assets"] - families_view = self.data["widgets"]["families"] # Refresh families config - families_view.refresh() + self._families_filter_view.refresh() # Change to context asset on context change - assets_widget.select_assets(io.Session["AVALON_ASSET"]) + self._assets_widget.select_assets(io.Session["AVALON_ASSET"]) def _refresh(self): """Load assets from database""" @@ -248,12 +279,10 @@ class LoaderWindow(QtWidgets.QDialog): project = io.find_one({"type": "project"}, {"type": 1}) assert project, "Project was not found! This is a bug" - assets_widget = self.data["widgets"]["assets"] - assets_widget.refresh() - assets_widget.setFocus() + self._assets_widget.refresh() + self._assets_widget.setFocus() - families_view = self.data["widgets"]["families"] - families_view.refresh() + self._families_filter_view.refresh() def clear_assets_underlines(self): """Clear colors from asset data to remove colored underlines @@ -261,11 +290,12 @@ class LoaderWindow(QtWidgets.QDialog): own selected subsets. These colors must be cleared from asset data on selection change so they match current selection. """ - last_asset_ids = self.data["state"]["assetIds"] + # TODO do not touch inner attributes of asset widget + last_asset_ids = self.data["state"]["assetIds"] or [] if not last_asset_ids: return - assets_widget = self.data["widgets"]["assets"] + assets_widget = self._assets_widget id_role = assets_widget.model.ObjectIdRole for index in lib.iter_model_rows(assets_widget.model, 0): @@ -278,15 +308,15 @@ class LoaderWindow(QtWidgets.QDialog): def _assetschanged(self): """Selected assets have changed""" - assets_widget = self.data["widgets"]["assets"] - subsets_widget = self.data["widgets"]["subsets"] + subsets_widget = self._subsets_widget + # TODO do not touch subset widget inner attributes subsets_model = subsets_widget.model subsets_model.clear() self.clear_assets_underlines() # filter None docs they are silo - asset_docs = assets_widget.get_selected_assets() + asset_docs = self._assets_widget.get_selected_assets() asset_ids = [asset_doc["_id"] for asset_doc in asset_docs] # Start loading @@ -302,14 +332,14 @@ class LoaderWindow(QtWidgets.QDialog): ) # Clear the version information on asset change - self.data["widgets"]["version"].set_version(None) - self.data["widgets"]["thumbnail"].set_thumbnail(asset_docs) + self._thumbnail_widget.set_thumbnail(asset_docs) + self._version_info_widget.set_version(None) self.data["state"]["assetIds"] = asset_ids - representations = self.data["widgets"]["representations"] # reset repre list - representations.set_version_ids([]) + if self._repres_widget is not None: + self._repres_widget.set_version_ids([]) def _subsetschanged(self): asset_ids = self.data["state"]["assetIds"] @@ -318,8 +348,9 @@ class LoaderWindow(QtWidgets.QDialog): self._versionschanged() return - subsets = self.data["widgets"]["subsets"] - selected_subsets = subsets.selected_subsets(_merged=True, _other=False) + selected_subsets = self._subsets_widget.selected_subsets( + _merged=True, _other=False + ) asset_models = {} asset_ids = [] @@ -340,7 +371,8 @@ class LoaderWindow(QtWidgets.QDialog): self.clear_assets_underlines() - assets_widget = self.data["widgets"]["assets"] + # TODO do not use inner attributes of asset widget + assets_widget = self._assets_widget indexes = assets_widget.view.selectionModel().selectedRows() for index in indexes: @@ -357,7 +389,7 @@ class LoaderWindow(QtWidgets.QDialog): self._versionschanged() def _versionschanged(self): - subsets = self.data["widgets"]["subsets"] + subsets = self._subsets_widget selection = subsets.view.selectionModel() # Active must be in the selected rows otherwise we @@ -389,23 +421,24 @@ class LoaderWindow(QtWidgets.QDialog): else: version_docs.append(item["version_document"]) - self.data["widgets"]["version"].set_version(version_doc) + self._version_info_widget.set_version(version_doc) thumbnail_docs = version_docs - assets_widget = self.data["widgets"]["assets"] - asset_docs = assets_widget.get_selected_assets() + asset_docs = self._assets_widget.get_selected_assets() if not thumbnail_docs: if len(asset_docs) > 0: thumbnail_docs = asset_docs - self.data["widgets"]["thumbnail"].set_thumbnail(thumbnail_docs) + self._thumbnail_widget.set_thumbnail(thumbnail_docs) - representations = self.data["widgets"]["representations"] - version_ids = [doc["_id"] for doc in version_docs or []] - representations.set_version_ids(version_ids) + if self._repres_widget is not None: + version_ids = [doc["_id"] for doc in version_docs or []] + self._repres_widget.set_version_ids(version_ids) - # representations.change_visibility("subset", len(rows) > 1) - # representations.change_visibility("asset", len(asset_docs) > 1) + # self._repres_widget.change_visibility("subset", len(rows) > 1) + # self._repres_widget.change_visibility( + # "asset", len(asset_docs) > 1 + # ) def _set_context(self, context, refresh=True): """Set the selection in the interface using a context. @@ -438,16 +471,15 @@ class LoaderWindow(QtWidgets.QDialog): # scheduled refresh and the silo tabs are not shown. self._refresh() - asset_widget = self.data["widgets"]["assets"] - asset_widget.select_assets(asset) + self._assets_widget.select_assets(asset) + + def _on_message_timeout(self): + self._message_label.setText("") def echo(self, message): - widget = self.data["label"]["message"] - widget.setText(str(message)) - widget.show() + self._message_label.setText(str(message)) print(message) - - lib.schedule(widget.hide, 5000, channel="message") + self._message_timer.start() def closeEvent(self, event): # Kill on holding SHIFT @@ -475,7 +507,7 @@ class LoaderWindow(QtWidgets.QDialog): event.setAccepted(True) # Avoid interfering other widgets def show_grouping_dialog(self): - subsets = self.data["widgets"]["subsets"] + subsets = self._subsets_widget if not subsets.is_groupable(): self.echo("Grouping not enabled.") return @@ -514,7 +546,8 @@ class SubsetGroupingDialog(QtWidgets.QDialog): self.items = items self.groups_config = groups_config - self.subsets = parent.data["widgets"]["subsets"] + # TODO do not touch inner attributes + self.subsets = parent._subsets_widget self.asset_ids = parent.data["state"]["assetIds"] name = QtWidgets.QLineEdit() @@ -633,7 +666,6 @@ def show(debug=False, parent=None, use_context=False): with lib.application(): window = LoaderWindow(parent) - window.setStyleSheet(style.load_stylesheet()) window.show() if use_context: diff --git a/openpype/tools/loader/images/default_thumbnail.png b/openpype/tools/loader/images/default_thumbnail.png index 97bd958e0d..adea862e5b 100644 Binary files a/openpype/tools/loader/images/default_thumbnail.png and b/openpype/tools/loader/images/default_thumbnail.png differ diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 1ccbb5796d..4c075382ac 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -37,12 +37,13 @@ class OverlayFrame(QtWidgets.QFrame): super(OverlayFrame, self).__init__(parent) label_widget = QtWidgets.QLabel(label, self) + label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + main_layout = QtWidgets.QVBoxLayout(self) main_layout.addWidget(label_widget, 1, QtCore.Qt.AlignCenter) self.label_widget = label_widget - label_widget.setStyleSheet("background: transparent;") self.setStyleSheet(( "background: rgba(0, 0, 0, 127);" "font-size: 60pt;" @@ -159,36 +160,40 @@ class SubsetWidget(QtWidgets.QWidget): grouping=enable_grouping ) proxy = SubsetFilterProxyModel() + proxy.setSourceModel(model) + proxy.setDynamicSortFilter(True) + proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) + family_proxy = FamiliesFilterProxyModel() family_proxy.setSourceModel(proxy) - subset_filter = QtWidgets.QLineEdit() + subset_filter = QtWidgets.QLineEdit(self) subset_filter.setPlaceholderText("Filter subsets..") - groupable = QtWidgets.QCheckBox("Enable Grouping") - groupable.setChecked(enable_grouping) + group_checkbox = QtWidgets.QCheckBox("Enable Grouping", self) + group_checkbox.setChecked(enable_grouping) top_bar_layout = QtWidgets.QHBoxLayout() top_bar_layout.addWidget(subset_filter) - top_bar_layout.addWidget(groupable) + top_bar_layout.addWidget(group_checkbox) - view = TreeViewSpinner() + view = TreeViewSpinner(self) + view.setModel(family_proxy) view.setObjectName("SubsetView") view.setIndentation(20) - view.setStyleSheet(""" - QTreeView::item{ - padding: 5px 1px; - border: 0px; - } - """) view.setAllColumnsShowFocus(True) + view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) + view.setSortingEnabled(True) + view.sortByColumn(1, QtCore.Qt.AscendingOrder) + view.setAlternatingRowColors(True) # Set view delegates - version_delegate = VersionDelegate(self.dbcon) + version_delegate = VersionDelegate(self.dbcon, view) column = model.Columns.index("version") view.setItemDelegateForColumn(column, version_delegate) - time_delegate = PrettyTimeDelegate() + time_delegate = PrettyTimeDelegate(view) column = model.Columns.index("time") view.setItemDelegateForColumn(column, time_delegate) @@ -197,54 +202,39 @@ class SubsetWidget(QtWidgets.QWidget): layout.addLayout(top_bar_layout) layout.addWidget(view) - view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) - view.setSortingEnabled(True) - view.sortByColumn(1, QtCore.Qt.AscendingOrder) - view.setAlternatingRowColors(True) - - self.data = { - "delegates": { - "version": version_delegate, - "time": time_delegate - }, - "state": { - "groupable": groupable - } - } - - self.proxy = proxy - self.model = model - self.view = view - self.filter = subset_filter - self.family_proxy = family_proxy - # settings and connections - self.proxy.setSourceModel(self.model) - self.proxy.setDynamicSortFilter(True) - self.proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) - - self.view.setModel(self.family_proxy) - self.view.customContextMenuRequested.connect(self.on_context_menu) - for column_name, width in self.default_widths: idx = model.Columns.index(column_name) view.setColumnWidth(idx, width) + self.model = model + self.view = view + actual_project = dbcon.Session["AVALON_PROJECT"] self.on_project_change(actual_project) + view.customContextMenuRequested.connect(self.on_context_menu) + selection = view.selectionModel() selection.selectionChanged.connect(self.active_changed) version_delegate.version_changed.connect(self.version_changed) - groupable.stateChanged.connect(self.set_grouping) + group_checkbox.stateChanged.connect(self.set_grouping) - self.filter.textChanged.connect(self.proxy.setFilterRegExp) - self.filter.textChanged.connect(self.view.expandAll) + subset_filter.textChanged.connect(proxy.setFilterRegExp) + subset_filter.textChanged.connect(view.expandAll) model.refreshed.connect(self.refreshed) + self.proxy = proxy + self.family_proxy = family_proxy + + self._subset_filter = subset_filter + self._group_checkbox = group_checkbox + + self._version_delegate = version_delegate + self._time_delegate = time_delegate + self.model.refresh() def get_subsets_families(self): @@ -254,7 +244,7 @@ class SubsetWidget(QtWidgets.QWidget): self.family_proxy.setFamiliesFilter(families) def is_groupable(self): - return self.data["state"]["groupable"].checkState() + return self._group_checkbox.isChecked() def set_grouping(self, state): with tools_lib.preserve_selection(tree_view=self.view, @@ -755,6 +745,7 @@ class ThumbnailWidget(QtWidgets.QLabel): "default_thumbnail.png" ) self.default_pix = QtGui.QPixmap(default_pix_path) + self.set_pixmap() def height(self): width = self.width() @@ -1131,7 +1122,8 @@ class RepresentationWidget(QtWidgets.QWidget): label = QtWidgets.QLabel("Representations", self) - tree_view = DeselectableTreeView() + tree_view = DeselectableTreeView(parent=self) + tree_view.setObjectName("RepresentationView") tree_view.setModel(proxy_model) tree_view.setAllColumnsShowFocus(True) tree_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) @@ -1141,12 +1133,6 @@ class RepresentationWidget(QtWidgets.QWidget): tree_view.sortByColumn(1, QtCore.Qt.AscendingOrder) tree_view.setAlternatingRowColors(True) tree_view.setIndentation(20) - tree_view.setStyleSheet(""" - QTreeView::item{ - padding: 5px 1px; - border: 0px; - } - """) tree_view.collapseAll() for column_name, width in self.default_widths: diff --git a/openpype/tools/mayalookassigner/app.py b/openpype/tools/mayalookassigner/app.py index 1fa3a3868a..0f5a930902 100644 --- a/openpype/tools/mayalookassigner/app.py +++ b/openpype/tools/mayalookassigner/app.py @@ -2,20 +2,27 @@ import sys import time import logging +from Qt import QtWidgets, QtCore + from openpype.hosts.maya.api.lib import assign_look_by_version from avalon import style, io from avalon.tools import lib -from avalon.vendor.Qt import QtWidgets, QtCore from maya import cmds # old api for MFileIO import maya.OpenMaya import maya.api.OpenMaya as om -from . import widgets -from . import commands -from . vray_proxies import vrayproxy_assign_look +from .widgets import ( + AssetOutliner, + LookOutliner +) +from .commands import ( + get_workfile, + remove_unused_looks +) +from .vray_proxies import vrayproxy_assign_look module = sys.modules[__name__] @@ -32,7 +39,7 @@ class App(QtWidgets.QWidget): # Store callback references self._callbacks = [] - filename = commands.get_workfile() + filename = get_workfile() self.setObjectName("lookManager") self.setWindowTitle("Look Manager 1.3.0 - [{}]".format(filename)) @@ -57,13 +64,13 @@ class App(QtWidgets.QWidget): """Build the UI""" # Assets (left) - asset_outliner = widgets.AssetOutliner() + asset_outliner = AssetOutliner() # Looks (right) looks_widget = QtWidgets.QWidget() looks_layout = QtWidgets.QVBoxLayout(looks_widget) - look_outliner = widgets.LookOutliner() # Database look overview + look_outliner = LookOutliner() # Database look overview assign_selected = QtWidgets.QCheckBox("Assign to selected only") assign_selected.setToolTip("Whether to assign only to selected nodes " @@ -124,7 +131,7 @@ class App(QtWidgets.QWidget): lambda: self.echo("Loaded assets..")) self.look_outliner.menu_apply_action.connect(self.on_process_selected) - self.remove_unused.clicked.connect(commands.remove_unused_looks) + self.remove_unused.clicked.connect(remove_unused_looks) # Maya renderlayer switch callback callback = om.MEventMessage.addEventCallback( diff --git a/openpype/tools/mayalookassigner/commands.py b/openpype/tools/mayalookassigner/commands.py index a53251cdef..f7d26f9adb 100644 --- a/openpype/tools/mayalookassigner/commands.py +++ b/openpype/tools/mayalookassigner/commands.py @@ -9,7 +9,7 @@ from openpype.hosts.maya.api import lib from avalon import io, api -import vray_proxies +from .vray_proxies import get_alembic_ids_cache log = logging.getLogger(__name__) @@ -146,7 +146,7 @@ def create_items_from_nodes(nodes): vray_proxy_nodes = cmds.ls(nodes, type="VRayProxy") for vp in vray_proxy_nodes: path = cmds.getAttr("{}.fileName".format(vp)) - ids = vray_proxies.get_alembic_ids_cache(path) + ids = get_alembic_ids_cache(path) parent_id = {} for k, _ in ids.items(): pid = k.split(":")[0] diff --git a/openpype/tools/mayalookassigner/models.py b/openpype/tools/mayalookassigner/models.py index 7c5133de82..80de6c1897 100644 --- a/openpype/tools/mayalookassigner/models.py +++ b/openpype/tools/mayalookassigner/models.py @@ -1,7 +1,8 @@ from collections import defaultdict -from avalon.tools import models -from avalon.vendor.Qt import QtCore +from Qt import QtCore + +from avalon.tools import models from avalon.vendor import qtawesome from avalon.style import colors diff --git a/openpype/tools/mayalookassigner/views.py b/openpype/tools/mayalookassigner/views.py index decf04ee57..993023bb45 100644 --- a/openpype/tools/mayalookassigner/views.py +++ b/openpype/tools/mayalookassigner/views.py @@ -1,4 +1,4 @@ -from avalon.vendor.Qt import QtWidgets, QtCore +from Qt import QtWidgets, QtCore DEFAULT_COLOR = "#fb9c15" diff --git a/openpype/tools/mayalookassigner/widgets.py b/openpype/tools/mayalookassigner/widgets.py index 2dab266af9..625e9ef8c6 100644 --- a/openpype/tools/mayalookassigner/widgets.py +++ b/openpype/tools/mayalookassigner/widgets.py @@ -1,13 +1,16 @@ import logging from collections import defaultdict -from avalon.vendor.Qt import QtWidgets, QtCore +from Qt import QtWidgets, QtCore # TODO: expose this better in avalon core from avalon.tools import lib from avalon.tools.models import TreeModel -from . import models +from .models import ( + AssetModel, + LookModel +) from . import commands from . import views @@ -30,7 +33,7 @@ class AssetOutliner(QtWidgets.QWidget): title.setAlignment(QtCore.Qt.AlignCenter) title.setStyleSheet("font-weight: bold; font-size: 12px") - model = models.AssetModel() + model = AssetModel() view = views.View() view.setModel(model) view.customContextMenuRequested.connect(self.right_mouse_menu) @@ -201,7 +204,7 @@ class LookOutliner(QtWidgets.QWidget): title.setStyleSheet("font-weight: bold; font-size: 12px") title.setAlignment(QtCore.Qt.AlignCenter) - model = models.LookModel() + model = LookModel() # Proxy for dynamic sorting proxy = QtCore.QSortFilterProxyModel() @@ -257,5 +260,3 @@ class LookOutliner(QtWidgets.QWidget): menu.addAction(apply_action) menu.exec_(globalpos) - - diff --git a/openpype/tools/settings/local_settings/experimental_widget.py b/openpype/tools/settings/local_settings/experimental_widget.py new file mode 100644 index 0000000000..e863d9afb0 --- /dev/null +++ b/openpype/tools/settings/local_settings/experimental_widget.py @@ -0,0 +1,65 @@ +from Qt import QtWidgets +from openpype.tools.experimental_tools import ( + ExperimentalTools, + LOCAL_EXPERIMENTAL_KEY +) + + +__all__ = ( + "LocalExperimentalToolsWidgets", + "LOCAL_EXPERIMENTAL_KEY" +) + + +class LocalExperimentalToolsWidgets(QtWidgets.QWidget): + def __init__(self, parent): + super(LocalExperimentalToolsWidgets, self).__init__(parent) + + self._loading_local_settings = False + + layout = QtWidgets.QFormLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + + # Label that says there are no experimental tools available + empty_label = QtWidgets.QLabel(self) + empty_label.setText( + "There are no experimental tools available..." + ) + + layout.addRow(empty_label) + + experimental_defs = ExperimentalTools(filter_hosts=False) + checkboxes_by_identifier = {} + for tool in experimental_defs.tools: + checkbox = QtWidgets.QCheckBox(self) + label_widget = QtWidgets.QLabel(tool.label, self) + checkbox.setToolTip(tool.tooltip) + label_widget.setToolTip(tool.tooltip) + layout.addRow(label_widget, checkbox) + + checkboxes_by_identifier[tool.identifier] = checkbox + + empty_label.setVisible(len(checkboxes_by_identifier) == 0) + + self._empty_label = empty_label + self._checkboxes_by_identifier = checkboxes_by_identifier + self._experimental_defs = experimental_defs + + def update_local_settings(self, value): + self._loading_local_settings = True + value = value or {} + + for identifier, checkbox in self._checkboxes_by_identifier.items(): + checked = value.get(identifier, False) + checkbox.setChecked(checked) + + self._loading_local_settings = False + + def settings_value(self): + # Add changed + # If these have changed then + output = {} + for identifier, checkbox in self._checkboxes_by_identifier.items(): + if checkbox.isChecked(): + output[identifier] = True + return output diff --git a/openpype/tools/settings/local_settings/window.py b/openpype/tools/settings/local_settings/window.py index 9e8fd89b23..f22e397323 100644 --- a/openpype/tools/settings/local_settings/window.py +++ b/openpype/tools/settings/local_settings/window.py @@ -20,6 +20,10 @@ from .widgets import ( ) from .mongo_widget import OpenPypeMongoWidget from .general_widget import LocalGeneralWidgets +from .experimental_widget import ( + LocalExperimentalToolsWidgets, + LOCAL_EXPERIMENTAL_KEY +) from .apps_widget import LocalApplicationsWidgets from .projects_widget import ProjectSettingsWidget @@ -44,11 +48,13 @@ class LocalSettingsWidget(QtWidgets.QWidget): self.pype_mongo_widget = None self.general_widget = None + self.experimental_widget = None self.apps_widget = None self.projects_widget = None self._create_pype_mongo_ui() self._create_general_ui() + self._create_experimental_ui() self._create_app_ui() self._create_project_ui() @@ -85,6 +91,26 @@ class LocalSettingsWidget(QtWidgets.QWidget): self.general_widget = general_widget + def _create_experimental_ui(self): + # General + experimental_expand_widget = ExpandingWidget( + "Experimental tools", self + ) + + experimental_content = QtWidgets.QWidget(self) + experimental_layout = QtWidgets.QVBoxLayout(experimental_content) + experimental_layout.setContentsMargins(CHILD_OFFSET, 5, 0, 0) + experimental_expand_widget.set_content_widget(experimental_content) + + experimental_widget = LocalExperimentalToolsWidgets( + experimental_content + ) + experimental_layout.addWidget(experimental_widget) + + self.main_layout.addWidget(experimental_expand_widget) + + self.experimental_widget = experimental_widget + def _create_app_ui(self): # Applications app_expand_widget = ExpandingWidget("Applications", self) @@ -135,6 +161,9 @@ class LocalSettingsWidget(QtWidgets.QWidget): self.projects_widget.update_local_settings( value.get(LOCAL_PROJECTS_KEY) ) + self.experimental_widget.update_local_settings( + value.get(LOCAL_EXPERIMENTAL_KEY) + ) def settings_value(self): output = {} @@ -149,6 +178,10 @@ class LocalSettingsWidget(QtWidgets.QWidget): projects_value = self.projects_widget.settings_value() if projects_value: output[LOCAL_PROJECTS_KEY] = projects_value + + experimental_value = self.experimental_widget.settings_value() + if experimental_value: + output[LOCAL_EXPERIMENTAL_KEY] = experimental_value return output diff --git a/openpype/tools/tray_app/app.py b/openpype/tools/tray_app/app.py index 03f8321464..f1363d0cab 100644 --- a/openpype/tools/tray_app/app.py +++ b/openpype/tools/tray_app/app.py @@ -142,18 +142,23 @@ class ConsoleTrayApp: self.tray_reconnect = False ConsoleTrayApp.webserver_client.close() - def _send_text(self, new_text): + def _send_text_queue(self): + """Sends lines and purges queue""" + lines = tuple(self.new_text) + self.new_text.clear() + + if lines: + self._send_lines(lines) + + def _send_lines(self, lines): """ Send console content. """ if not ConsoleTrayApp.webserver_client: return - if isinstance(new_text, str): - new_text = collections.deque(new_text.split("\n")) - payload = { "host": self.host_id, "action": host_console_listener.MsgAction.ADD, - "text": "\n".join(new_text) + "text": "\n".join(lines) } self._send(payload) @@ -174,14 +179,7 @@ class ConsoleTrayApp: if self.tray_reconnect: self._connect() # reconnect - if ConsoleTrayApp.webserver_client and self.new_text: - self._send_text(self.new_text) - self.new_text = collections.deque() - - if self.new_text: # no webserver_client, text keeps stashing - start = max(len(self.new_text) - self.MAX_LINES, 0) - self.new_text = itertools.islice(self.new_text, - start, self.MAX_LINES) + self._send_text_queue() if not self.initialized: if self.initializing: @@ -191,7 +189,7 @@ class ConsoleTrayApp: elif not host_connected: text = "{} process is not alive. Exiting".format(self.host) print(text) - self._send_text([text]) + self._send_lines([text]) ConsoleTrayApp.websocket_server.stop() sys.exit(1) elif host_connected: @@ -205,14 +203,15 @@ class ConsoleTrayApp: self.initializing = True self.launch_method(*self.subprocess_args) - elif ConsoleTrayApp.process.poll() is not None: - self.exit() - elif ConsoleTrayApp.callback_queue: + elif ConsoleTrayApp.callback_queue and \ + not ConsoleTrayApp.callback_queue.empty(): try: callback = ConsoleTrayApp.callback_queue.get(block=False) callback() except queue.Empty: pass + elif ConsoleTrayApp.process.poll() is not None: + self.exit() @classmethod def execute_in_main_thread(cls, func_to_call_from_main_thread): @@ -232,8 +231,9 @@ class ConsoleTrayApp: self._close() if ConsoleTrayApp.websocket_server: ConsoleTrayApp.websocket_server.stop() - ConsoleTrayApp.process.kill() - ConsoleTrayApp.process.wait() + if ConsoleTrayApp.process: + ConsoleTrayApp.process.kill() + ConsoleTrayApp.process.wait() if self.timer: self.timer.stop() QtCore.QCoreApplication.exit() diff --git a/openpype/tools/utils/delegates.py b/openpype/tools/utils/delegates.py index 1827bc7e9b..96353c44c6 100644 --- a/openpype/tools/utils/delegates.py +++ b/openpype/tools/utils/delegates.py @@ -7,6 +7,7 @@ import Qt from Qt import QtWidgets, QtGui, QtCore from avalon.lib import HeroVersionType +from openpype.style import get_objected_colors from .models import ( AssetModel, TreeModel @@ -24,6 +25,19 @@ log = logging.getLogger(__name__) class AssetDelegate(QtWidgets.QItemDelegate): bar_height = 3 + def __init__(self, *args, **kwargs): + super(AssetDelegate, self).__init__(*args, **kwargs) + asset_view_colors = get_objected_colors()["loader"]["asset-view"] + self._selected_color = ( + asset_view_colors["selected"].get_qcolor() + ) + self._hover_color = ( + asset_view_colors["hover"].get_qcolor() + ) + self._selected_hover_color = ( + asset_view_colors["selected-hover"].get_qcolor() + ) + def sizeHint(self, option, index): result = super(AssetDelegate, self).sizeHint(option, index) height = result.height() @@ -66,17 +80,20 @@ class AssetDelegate(QtWidgets.QItemDelegate): counter += 1 # Background - bg_color = QtGui.QColor(60, 60, 60) if option.state & QtWidgets.QStyle.State_Selected: if len(subset_colors) == 0: item_rect.setTop(item_rect.top() + (self.bar_height / 2)) + if option.state & QtWidgets.QStyle.State_MouseOver: - bg_color.setRgb(70, 70, 70) + bg_color = self._selected_hover_color + else: + bg_color = self._selected_color else: item_rect.setTop(item_rect.top() + (self.bar_height / 2)) if option.state & QtWidgets.QStyle.State_MouseOver: - bg_color.setAlpha(100) + bg_color = self._hover_color else: + bg_color = QtGui.QColor() bg_color.setAlpha(0) # When not needed to do a rounded corners (easier and without diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index ee184ccf2d..d5202c8435 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -28,6 +28,7 @@ class HostToolsHelper: self._scene_inventory_tool = None self._library_loader_tool = None self._look_assigner_tool = None + self._experimental_tools_dialog = None @property def log(self): @@ -40,7 +41,6 @@ class HostToolsHelper: def get_workfiles_tool(self, parent): """Create, cache and return workfiles tool window.""" if self._workfiles_tool is None: - from avalon import style from openpype.tools.workfiles.app import ( Window, validate_host_requirements ) @@ -49,13 +49,14 @@ class HostToolsHelper: validate_host_requirements(host) workfiles_window = Window(parent=parent) - workfiles_window.setStyleSheet(style.load_stylesheet()) self._workfiles_tool = workfiles_window return self._workfiles_tool def show_workfiles(self, parent=None, use_context=None, save=None): """Workfiles tool for changing context and saving workfiles.""" + from avalon import style + if use_context is None: use_context = True @@ -79,24 +80,28 @@ class HostToolsHelper: # Pull window to the front. workfiles_tool.raise_() workfiles_tool.activateWindow() + workfiles_tool.setStyleSheet(style.load_stylesheet()) def get_loader_tool(self, parent): """Create, cache and return loader tool window.""" if self._loader_tool is None: - from avalon import style from openpype.tools.loader import LoaderWindow loader_window = LoaderWindow(parent=parent or self._parent) - loader_window.setStyleSheet(style.load_stylesheet()) self._loader_tool = loader_window return self._loader_tool def show_loader(self, parent=None, use_context=None): """Loader tool for loading representations.""" + loader_tool = self.get_loader_tool(parent) + + loader_tool.show() + loader_tool.raise_() + loader_tool.activateWindow() + if use_context is None: use_context = False - loader_tool = self.get_loader_tool(parent) if use_context: context = {"asset": avalon.api.Session["AVALON_ASSET"]} @@ -104,29 +109,26 @@ class HostToolsHelper: else: loader_tool.refresh() - loader_tool.show() - loader_tool.raise_() - loader_tool.activateWindow() - loader_tool.refresh() - def get_creator_tool(self, parent): """Create, cache and return creator tool window.""" if self._creator_tool is None: - from avalon import style from avalon.tools.creator.app import Window creator_window = Window(parent=parent or self._parent) - creator_window.setStyleSheet(style.load_stylesheet()) self._creator_tool = creator_window return self._creator_tool def show_creator(self, parent=None): """Show tool to create new instantes for publishing.""" + from avalon import style + creator_tool = self.get_creator_tool(parent) creator_tool.refresh() creator_tool.show() + creator_tool.setStyleSheet(style.load_stylesheet()) + # Pull window to the front. creator_tool.raise_() creator_tool.activateWindow() @@ -134,20 +136,22 @@ class HostToolsHelper: def get_subset_manager_tool(self, parent): """Create, cache and return subset manager tool window.""" if self._subset_manager_tool is None: - from avalon import style from avalon.tools.subsetmanager import Window subset_manager_window = Window(parent=parent or self._parent) - subset_manager_window.setStyleSheet(style.load_stylesheet()) self._subset_manager_tool = subset_manager_window return self._subset_manager_tool def show_subset_manager(self, parent=None): """Show tool display/remove existing created instances.""" + from avalon import style + subset_manager_tool = self.get_subset_manager_tool(parent) subset_manager_tool.show() + subset_manager_tool.setStyleSheet(style.load_stylesheet()) + # Pull window to the front. subset_manager_tool.raise_() subset_manager_tool.activateWindow() @@ -155,20 +159,21 @@ class HostToolsHelper: def get_scene_inventory_tool(self, parent): """Create, cache and return scene inventory tool window.""" if self._scene_inventory_tool is None: - from avalon import style from avalon.tools.sceneinventory.app import Window scene_inventory_window = Window(parent=parent or self._parent) - scene_inventory_window.setStyleSheet(style.load_stylesheet()) self._scene_inventory_tool = scene_inventory_window return self._scene_inventory_tool def show_scene_inventory(self, parent=None): """Show tool maintain loaded containers.""" + from avalon import style + scene_inventory_tool = self.get_scene_inventory_tool(parent) scene_inventory_tool.show() scene_inventory_tool.refresh() + scene_inventory_tool.setStyleSheet(style.load_stylesheet()) # Pull window to the front. scene_inventory_tool.raise_() @@ -177,13 +182,11 @@ class HostToolsHelper: def get_library_loader_tool(self, parent): """Create, cache and return library loader tool window.""" if self._library_loader_tool is None: - from avalon import style from openpype.tools.libraryloader import LibraryLoaderWindow library_window = LibraryLoaderWindow( parent=parent or self._parent ) - library_window.setStyleSheet(style.load_stylesheet()) self._library_loader_tool = library_window return self._library_loader_tool @@ -205,18 +208,46 @@ class HostToolsHelper: def get_look_assigner_tool(self, parent): """Create, cache and return look assigner tool window.""" if self._look_assigner_tool is None: - from avalon import style import mayalookassigner mayalookassigner_window = mayalookassigner.App(parent) - mayalookassigner_window.setStyleSheet(style.load_stylesheet()) self._look_assigner_tool = mayalookassigner_window return self._look_assigner_tool def show_look_assigner(self, parent=None): """Look manager is Maya specific tool for look management.""" + from avalon import style + look_assigner_tool = self.get_look_assigner_tool(parent) look_assigner_tool.show() + look_assigner_tool.setStyleSheet(style.load_stylesheet()) + + def get_experimental_tools_dialog(self, parent=None): + """Dialog of experimental tools. + + For some hosts it is not easy to modify menu of tools. For + those cases was addded experimental tools dialog which is Qt based + and can dynamically filled by experimental tools so + host need only single "Experimental tools" button to see them. + + Dialog can be also empty with a message that there are not available + experimental tools. + """ + if self._experimental_tools_dialog is None: + from openpype.tools.experimental_tools import ( + ExperimentalToolsDialog + ) + + self._experimental_tools_dialog = ExperimentalToolsDialog(parent) + return self._experimental_tools_dialog + + def show_experimental_tools_dialog(self, parent=None): + """Show dialog with experimental tools.""" + dialog = self.get_experimental_tools_dialog(parent) + + dialog.show() + dialog.raise_() + dialog.activateWindow() def get_tool_by_name(self, tool_name, parent=None, *args, **kwargs): """Show tool by it's name. @@ -247,6 +278,9 @@ class HostToolsHelper: elif tool_name == "publish": self.log.info("Can't return publish tool window.") + elif tool_name == "experimental_tools": + return self.get_experimental_tools_dialog(parent, *args, **kwargs) + else: self.log.warning( "Can't show unknown tool name: \"{}\"".format(tool_name) @@ -281,6 +315,9 @@ class HostToolsHelper: elif tool_name == "publish": self.show_publish(parent, *args, **kwargs) + elif tool_name == "experimental_tools": + self.show_experimental_tools_dialog(parent, *args, **kwargs) + else: self.log.warning( "Can't show unknown tool name: \"{}\"".format(tool_name) @@ -355,3 +392,7 @@ def show_look_assigner(parent=None): def show_publish(parent=None): _SingletonPoint.show_tool_by_name("publish", parent) + + +def show_experimental_tools_dialog(parent=None): + _SingletonPoint.show_tool_by_name("experimental_tools", parent) diff --git a/openpype/tools/utils/views.py b/openpype/tools/utils/views.py index bed5655647..89e49fe142 100644 --- a/openpype/tools/utils/views.py +++ b/openpype/tools/utils/views.py @@ -68,8 +68,8 @@ class AssetsView(TreeViewSpinner, DeselectableTreeView): This implements a context menu. """ - def __init__(self): - super(AssetsView, self).__init__() + def __init__(self, parent=None): + super(AssetsView, self).__init__(parent) self.setIndentation(15) self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.setHeaderHidden(True) diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py index b9b542c123..15bcbeff90 100644 --- a/openpype/tools/utils/widgets.py +++ b/openpype/tools/utils/widgets.py @@ -35,28 +35,19 @@ class AssetWidget(QtWidgets.QWidget): self.dbcon = dbcon - self.setContentsMargins(0, 0, 0, 0) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(4) - # Tree View model = AssetModel(dbcon=self.dbcon, parent=self) proxy = RecursiveSortFilterProxyModel() proxy.setSourceModel(model) proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) - view = AssetsView() + view = AssetsView(self) view.setModel(proxy) if multiselection: asset_delegate = AssetDelegate() view.setSelectionMode(view.ExtendedSelection) view.setItemDelegate(asset_delegate) - # Header - header = QtWidgets.QHBoxLayout() - icon = qtawesome.icon("fa.arrow-down", color=style.colors.light) set_current_asset_btn = QtWidgets.QPushButton(icon, "") set_current_asset_btn.setToolTip("Go to Asset from current Session") @@ -64,22 +55,28 @@ class AssetWidget(QtWidgets.QWidget): set_current_asset_btn.setVisible(False) icon = qtawesome.icon("fa.refresh", color=style.colors.light) - refresh = QtWidgets.QPushButton(icon, "") + refresh = QtWidgets.QPushButton(icon, "", parent=self) refresh.setToolTip("Refresh items") - filter = QtWidgets.QLineEdit() - filter.textChanged.connect(proxy.setFilterFixedString) - filter.setPlaceholderText("Filter assets..") + filter_input = QtWidgets.QLineEdit(self) + filter_input.setPlaceholderText("Filter assets..") - header.addWidget(filter) - header.addWidget(set_current_asset_btn) - header.addWidget(refresh) + # Header + header_layout = QtWidgets.QHBoxLayout() + header_layout.addWidget(filter_input) + header_layout.addWidget(set_current_asset_btn) + header_layout.addWidget(refresh) # Layout - layout.addLayout(header) + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(4) + layout.addLayout(header_layout) layout.addWidget(view) # Signals/Slots + filter_input.textChanged.connect(proxy.setFilterFixedString) + selection = view.selectionModel() selection.selectionChanged.connect(self.selection_changed) selection.currentChanged.connect(self.current_changed) @@ -313,7 +310,6 @@ class OptionalMenu(QtWidgets.QMenu): actions that were instances of `QtWidgets.QWidgetAction`. """ - def mouseReleaseEvent(self, event): """Emit option clicked signal if mouse released on it""" active = self.actionAt(event.pos()) @@ -352,6 +348,7 @@ class OptionalAction(QtWidgets.QWidgetAction): self.use_option = use_option self.option_tip = "" self.optioned = False + self.widget = None def createWidget(self, parent): widget = OptionalActionWidget(self.label, parent) @@ -377,20 +374,10 @@ class OptionalAction(QtWidgets.QWidgetAction): self.optioned = True def set_highlight(self, state, global_pos=None): - body = self.widget.body - option = self.widget.option - - role = QtGui.QPalette.Highlight if state else QtGui.QPalette.Window - body.setBackgroundRole(role) - body.setAutoFillBackground(state) - - if not self.use_option: - return - - state = option.is_hovered(global_pos) - role = QtGui.QPalette.Highlight if state else QtGui.QPalette.Window - option.setBackgroundRole(role) - option.setAutoFillBackground(state) + option_state = False + if self.use_option: + option_state = self.widget.option.is_hovered(global_pos) + self.widget.set_hover_properties(state, option_state) class OptionalActionWidget(QtWidgets.QWidget): @@ -399,30 +386,33 @@ class OptionalActionWidget(QtWidgets.QWidget): def __init__(self, label, parent=None): super(OptionalActionWidget, self).__init__(parent) - body = QtWidgets.QWidget() - body.setStyleSheet("background: transparent;") + body_widget = QtWidgets.QWidget(self) + body_widget.setObjectName("OptionalActionBody") - icon = QtWidgets.QLabel() - label = QtWidgets.QLabel(label) - option = OptionBox(body) + icon = QtWidgets.QLabel(body_widget) + label = QtWidgets.QLabel(label, body_widget) + # (NOTE) For removing ugly QLable shadow FX when highlighted in Nuke. + # See https://stackoverflow.com/q/52838690/4145300 + label.setStyle(QtWidgets.QStyleFactory.create("Plastique")) + option = OptionBox(body_widget) + option.setObjectName("OptionalActionOption") icon.setFixedSize(24, 16) option.setFixedSize(30, 30) - layout = QtWidgets.QHBoxLayout(body) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(2) - layout.addWidget(icon) - layout.addWidget(label) - layout.addSpacing(6) + body_layout = QtWidgets.QHBoxLayout(body_widget) + body_layout.setContentsMargins(4, 0, 4, 0) + body_layout.setSpacing(2) + body_layout.addWidget(icon) + body_layout.addWidget(label) layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(6, 1, 2, 1) + layout.setContentsMargins(2, 1, 2, 1) layout.setSpacing(0) - layout.addWidget(body) + layout.addWidget(body_widget) layout.addWidget(option) - body.setMouseTracking(True) + body_widget.setMouseTracking(True) label.setMouseTracking(True) option.setMouseTracking(True) self.setMouseTracking(True) @@ -431,11 +421,24 @@ class OptionalActionWidget(QtWidgets.QWidget): self.icon = icon self.label = label self.option = option - self.body = body + self.body = body_widget - # (NOTE) For removing ugly QLable shadow FX when highlighted in Nuke. - # See https://stackoverflow.com/q/52838690/4145300 - label.setStyle(QtWidgets.QStyleFactory.create("Plastique")) + def set_hover_properties(self, hovered, option_hovered): + body_state = "" + option_state = "" + if hovered: + body_state = "hover" + + if option_hovered: + option_state = "hover" + + if self.body.property("state") != body_state: + self.body.setProperty("state", body_state) + self.body.style().polish(self.body) + + if self.option.property("state") != option_state: + self.option.setProperty("state", option_state) + self.option.style().polish(self.option) def setIcon(self, icon): pixmap = icon.pixmap(16, 16) @@ -456,8 +459,6 @@ class OptionBox(QtWidgets.QLabel): pixmap = icon.pixmap(18, 18) self.setPixmap(pixmap) - self.setStyleSheet("background: transparent;") - def is_hovered(self, global_pos): if global_pos is None: return False @@ -476,20 +477,20 @@ class OptionDialog(QtWidgets.QDialog): def create(self, options): parser = qargparse.QArgumentParser(arguments=options) - decision = QtWidgets.QWidget() - accept = QtWidgets.QPushButton("Accept") - cancel = QtWidgets.QPushButton("Cancel") + decision_widget = QtWidgets.QWidget(self) + accept_btn = QtWidgets.QPushButton("Accept", decision_widget) + cancel_btn = QtWidgets.QPushButton("Cancel", decision_widget) - layout = QtWidgets.QHBoxLayout(decision) - layout.addWidget(accept) - layout.addWidget(cancel) + decision_layout = QtWidgets.QHBoxLayout(decision_widget) + decision_layout.addWidget(accept_btn) + decision_layout.addWidget(cancel_btn) layout = QtWidgets.QVBoxLayout(self) layout.addWidget(parser) - layout.addWidget(decision) + layout.addWidget(decision_widget) - accept.clicked.connect(self.accept) - cancel.clicked.connect(self.reject) + accept_btn.clicked.connect(self.accept) + cancel_btn.clicked.connect(self.reject) parser.changed.connect(self.on_changed) def on_changed(self, argument): diff --git a/openpype/vendor/python/common/capture.py b/openpype/vendor/python/common/capture.py index 83816ec92a..02d86952df 100644 --- a/openpype/vendor/python/common/capture.py +++ b/openpype/vendor/python/common/capture.py @@ -161,37 +161,62 @@ def capture(camera=None, cmds.currentTime(cmds.currentTime(query=True)) padding = 10 # Extend panel to accommodate for OS window manager + with _independent_panel(width=width + padding, height=height + padding, off_screen=off_screen) as panel: cmds.setFocus(panel) - with contextlib.nested( - _disabled_inview_messages(), - _maintain_camera(panel, camera), - _applied_viewport_options(viewport_options, panel), - _applied_camera_options(camera_options, panel), - _applied_display_options(display_options), - _applied_viewport2_options(viewport2_options), - _isolated_nodes(isolate, panel), - _maintained_time()): + all_playblast_kwargs = { + "compression": compression, + "format": format, + "percent": 100, + "quality": quality, + "viewer": viewer, + "startTime": start_frame, + "endTime": end_frame, + "offScreen": off_screen, + "showOrnaments": show_ornaments, + "forceOverwrite": overwrite, + "filename": filename, + "widthHeight": [width, height], + "rawFrameNumbers": raw_frame_numbers, + "framePadding": frame_padding + } + all_playblast_kwargs.update(playblast_kwargs) - output = cmds.playblast( - compression=compression, - format=format, - percent=100, - quality=quality, - viewer=viewer, - startTime=start_frame, - endTime=end_frame, - offScreen=off_screen, - showOrnaments=show_ornaments, - forceOverwrite=overwrite, - filename=filename, - widthHeight=[width, height], - rawFrameNumbers=raw_frame_numbers, - framePadding=frame_padding, - **playblast_kwargs) + if getattr(contextlib, "nested", None): + with contextlib.nested( + _disabled_inview_messages(), + _maintain_camera(panel, camera), + _applied_viewport_options(viewport_options, panel), + _applied_camera_options(camera_options, panel), + _applied_display_options(display_options), + _applied_viewport2_options(viewport2_options), + _isolated_nodes(isolate, panel), + _maintained_time() + ): + output = cmds.playblast(**all_playblast_kwargs) + else: + with contextlib.ExitStack() as stack: + stack.enter_context(_disabled_inview_messages()) + stack.enter_context(_maintain_camera(panel, camera)) + stack.enter_context( + _applied_viewport_options(viewport_options, panel) + ) + stack.enter_context( + _applied_camera_options(camera_options, panel) + ) + stack.enter_context( + _applied_display_options(display_options) + ) + stack.enter_context( + _applied_viewport2_options(viewport2_options) + ) + stack.enter_context(_isolated_nodes(isolate, panel)) + stack.enter_context(_maintained_time()) + + output = cmds.playblast(**all_playblast_kwargs) return output @@ -364,7 +389,8 @@ def apply_view(panel, **options): # Display options display_options = options.get("display_options", {}) - for key, value in display_options.iteritems(): + _iteritems = getattr(display_options, "iteritems", display_options.items) + for key, value in _iteritems(): if key in _DisplayOptionsRGB: cmds.displayRGBColor(key, *value) else: @@ -372,16 +398,21 @@ def apply_view(panel, **options): # Camera options camera_options = options.get("camera_options", {}) - for key, value in camera_options.iteritems(): + _iteritems = getattr(camera_options, "iteritems", camera_options.items) + for key, value in _iteritems: cmds.setAttr("{0}.{1}".format(camera, key), value) # Viewport options viewport_options = options.get("viewport_options", {}) - for key, value in viewport_options.iteritems(): + _iteritems = getattr(viewport_options, "iteritems", viewport_options.items) + for key, value in _iteritems(): cmds.modelEditor(panel, edit=True, **{key: value}) viewport2_options = options.get("viewport2_options", {}) - for key, value in viewport2_options.iteritems(): + _iteritems = getattr( + viewport2_options, "iteritems", viewport2_options.items + ) + for key, value in _iteritems(): attr = "hardwareRenderingGlobals.{0}".format(key) cmds.setAttr(attr, value) @@ -629,14 +660,16 @@ def _applied_camera_options(options, panel): "for capture: %s" % opt) options.pop(opt) - for opt, value in options.iteritems(): + _iteritems = getattr(options, "iteritems", options.items) + for opt, value in _iteritems(): cmds.setAttr(camera + "." + opt, value) try: yield finally: if old_options: - for opt, value in old_options.iteritems(): + _iteritems = getattr(old_options, "iteritems", old_options.items) + for opt, value in _iteritems(): cmds.setAttr(camera + "." + opt, value) @@ -722,14 +755,16 @@ def _applied_viewport2_options(options): options.pop(opt) # Apply settings - for opt, value in options.iteritems(): + _iteritems = getattr(options, "iteritems", options.items) + for opt, value in _iteritems(): cmds.setAttr("hardwareRenderingGlobals." + opt, value) try: yield finally: # Restore previous settings - for opt, value in original.iteritems(): + _iteritems = getattr(original, "iteritems", original.items) + for opt, value in _iteritems(): cmds.setAttr("hardwareRenderingGlobals." + opt, value) @@ -769,7 +804,8 @@ def _maintain_camera(panel, camera): try: yield finally: - for camera, renderable in state.iteritems(): + _iteritems = getattr(state, "iteritems", state.items) + for camera, renderable in _iteritems(): cmds.setAttr(camera + ".rnd", renderable) diff --git a/openpype/version.py b/openpype/version.py index d88d79b995..6eb58f6fcc 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.5.0" +__version__ = "3.6.0-nightly.1" diff --git a/poetry.lock b/poetry.lock index e5f5919a01..36105f4213 100644 --- a/poetry.lock +++ b/poetry.lock @@ -782,7 +782,7 @@ six = "*" [[package]] name = "pillow" -version = "8.2.0" +version = "8.3.2" description = "Python Imaging Library (Fork)" category = "main" optional = false @@ -1538,7 +1538,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt [metadata] lock-version = "1.1" python-versions = "3.7.*" -content-hash = "ff2bfa35a7304378917a0c25d7d7af9f81a130288d95789bdf7429f071e80b69" +content-hash = "fb6db80d126fe7ef2d1d06d0381b6d11445d6d3e54b33585f6b0a0b6b0b9d372" [metadata.files] acre = [] @@ -2058,40 +2058,59 @@ pathlib2 = [ {file = "pathlib2-2.3.5.tar.gz", hash = "sha256:6cd9a47b597b37cc57de1c05e56fb1a1c9cc9fab04fe78c29acd090418529868"}, ] pillow = [ - {file = "Pillow-8.2.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:dc38f57d8f20f06dd7c3161c59ca2c86893632623f33a42d592f097b00f720a9"}, - {file = "Pillow-8.2.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a013cbe25d20c2e0c4e85a9daf438f85121a4d0344ddc76e33fd7e3965d9af4b"}, - {file = "Pillow-8.2.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8bb1e155a74e1bfbacd84555ea62fa21c58e0b4e7e6b20e4447b8d07990ac78b"}, - {file = "Pillow-8.2.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c5236606e8570542ed424849f7852a0ff0bce2c4c8d0ba05cc202a5a9c97dee9"}, - {file = "Pillow-8.2.0-cp36-cp36m-win32.whl", hash = "sha256:12e5e7471f9b637762453da74e390e56cc43e486a88289995c1f4c1dc0bfe727"}, - {file = "Pillow-8.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5afe6b237a0b81bd54b53f835a153770802f164c5570bab5e005aad693dab87f"}, - {file = "Pillow-8.2.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:cb7a09e173903541fa888ba010c345893cd9fc1b5891aaf060f6ca77b6a3722d"}, - {file = "Pillow-8.2.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0d19d70ee7c2ba97631bae1e7d4725cdb2ecf238178096e8c82ee481e189168a"}, - {file = "Pillow-8.2.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:083781abd261bdabf090ad07bb69f8f5599943ddb539d64497ed021b2a67e5a9"}, - {file = "Pillow-8.2.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:c6b39294464b03457f9064e98c124e09008b35a62e3189d3513e5148611c9388"}, - {file = "Pillow-8.2.0-cp37-cp37m-win32.whl", hash = "sha256:01425106e4e8cee195a411f729cff2a7d61813b0b11737c12bd5991f5f14bcd5"}, - {file = "Pillow-8.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3b570f84a6161cf8865c4e08adf629441f56e32f180f7aa4ccbd2e0a5a02cba2"}, - {file = "Pillow-8.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:031a6c88c77d08aab84fecc05c3cde8414cd6f8406f4d2b16fed1e97634cc8a4"}, - {file = "Pillow-8.2.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:66cc56579fd91f517290ab02c51e3a80f581aba45fd924fcdee01fa06e635812"}, - {file = "Pillow-8.2.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c32cc3145928c4305d142ebec682419a6c0a8ce9e33db900027ddca1ec39178"}, - {file = "Pillow-8.2.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:624b977355cde8b065f6d51b98497d6cd5fbdd4f36405f7a8790e3376125e2bb"}, - {file = "Pillow-8.2.0-cp38-cp38-win32.whl", hash = "sha256:5cbf3e3b1014dddc45496e8cf38b9f099c95a326275885199f427825c6522232"}, - {file = "Pillow-8.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:463822e2f0d81459e113372a168f2ff59723e78528f91f0bd25680ac185cf797"}, - {file = "Pillow-8.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:95d5ef984eff897850f3a83883363da64aae1000e79cb3c321915468e8c6add5"}, - {file = "Pillow-8.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b91c36492a4bbb1ee855b7d16fe51379e5f96b85692dc8210831fbb24c43e484"}, - {file = "Pillow-8.2.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d68cb92c408261f806b15923834203f024110a2e2872ecb0bd2a110f89d3c602"}, - {file = "Pillow-8.2.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f217c3954ce5fd88303fc0c317af55d5e0204106d86dea17eb8205700d47dec2"}, - {file = "Pillow-8.2.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:5b70110acb39f3aff6b74cf09bb4169b167e2660dabc304c1e25b6555fa781ef"}, - {file = "Pillow-8.2.0-cp39-cp39-win32.whl", hash = "sha256:a7d5e9fad90eff8f6f6106d3b98b553a88b6f976e51fce287192a5d2d5363713"}, - {file = "Pillow-8.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:238c197fc275b475e87c1453b05b467d2d02c2915fdfdd4af126145ff2e4610c"}, - {file = "Pillow-8.2.0-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:0e04d61f0064b545b989126197930807c86bcbd4534d39168f4aa5fda39bb8f9"}, - {file = "Pillow-8.2.0-pp36-pypy36_pp73-manylinux2010_i686.whl", hash = "sha256:63728564c1410d99e6d1ae8e3b810fe012bc440952168af0a2877e8ff5ab96b9"}, - {file = "Pillow-8.2.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:c03c07ed32c5324939b19e36ae5f75c660c81461e312a41aea30acdd46f93a7c"}, - {file = "Pillow-8.2.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:4d98abdd6b1e3bf1a1cbb14c3895226816e666749ac040c4e2554231068c639b"}, - {file = "Pillow-8.2.0-pp37-pypy37_pp73-manylinux2010_i686.whl", hash = "sha256:aac00e4bc94d1b7813fe882c28990c1bc2f9d0e1aa765a5f2b516e8a6a16a9e4"}, - {file = "Pillow-8.2.0-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:22fd0f42ad15dfdde6c581347eaa4adb9a6fc4b865f90b23378aa7914895e120"}, - {file = "Pillow-8.2.0-pp37-pypy37_pp73-win32.whl", hash = "sha256:e98eca29a05913e82177b3ba3d198b1728e164869c613d76d0de4bde6768a50e"}, - {file = "Pillow-8.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:8b56553c0345ad6dcb2e9b433ae47d67f95fc23fe28a0bde15a120f25257e291"}, - {file = "Pillow-8.2.0.tar.gz", hash = "sha256:a787ab10d7bb5494e5f76536ac460741788f1fbce851068d73a87ca7c35fc3e1"}, + {file = "Pillow-8.3.2-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:c691b26283c3a31594683217d746f1dad59a7ae1d4cfc24626d7a064a11197d4"}, + {file = "Pillow-8.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f514c2717012859ccb349c97862568fdc0479aad85b0270d6b5a6509dbc142e2"}, + {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be25cb93442c6d2f8702c599b51184bd3ccd83adebd08886b682173e09ef0c3f"}, + {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d675a876b295afa114ca8bf42d7f86b5fb1298e1b6bb9a24405a3f6c8338811c"}, + {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59697568a0455764a094585b2551fd76bfd6b959c9f92d4bdec9d0e14616303a"}, + {file = "Pillow-8.3.2-cp310-cp310-win32.whl", hash = "sha256:2d5e9dc0bf1b5d9048a94c48d0813b6c96fccfa4ccf276d9c36308840f40c228"}, + {file = "Pillow-8.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:11c27e74bab423eb3c9232d97553111cc0be81b74b47165f07ebfdd29d825875"}, + {file = "Pillow-8.3.2-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:11eb7f98165d56042545c9e6db3ce394ed8b45089a67124298f0473b29cb60b2"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f23b2d3079522fdf3c09de6517f625f7a964f916c956527bed805ac043799b8"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19ec4cfe4b961edc249b0e04b5618666c23a83bc35842dea2bfd5dfa0157f81b"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5a31c07cea5edbaeb4bdba6f2b87db7d3dc0f446f379d907e51cc70ea375629"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15ccb81a6ffc57ea0137f9f3ac2737ffa1d11f786244d719639df17476d399a7"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8f284dc1695caf71a74f24993b7c7473d77bc760be45f776a2c2f4e04c170550"}, + {file = "Pillow-8.3.2-cp36-cp36m-win32.whl", hash = "sha256:4abc247b31a98f29e5224f2d31ef15f86a71f79c7f4d2ac345a5d551d6393073"}, + {file = "Pillow-8.3.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a048dad5ed6ad1fad338c02c609b862dfaa921fcd065d747194a6805f91f2196"}, + {file = "Pillow-8.3.2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:06d1adaa284696785375fa80a6a8eb309be722cf4ef8949518beb34487a3df71"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd24054aaf21e70a51e2a2a5ed1183560d3a69e6f9594a4bfe360a46f94eba83"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a330bf7014ee034046db43ccbb05c766aa9e70b8d6c5260bfc38d73103b0ba"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13654b521fb98abdecec105ea3fb5ba863d1548c9b58831dd5105bb3873569f1"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1bd983c565f92779be456ece2479840ec39d386007cd4ae83382646293d681b"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4326ea1e2722f3dc00ed77c36d3b5354b8fb7399fb59230249ea6d59cbed90da"}, + {file = "Pillow-8.3.2-cp37-cp37m-win32.whl", hash = "sha256:085a90a99404b859a4b6c3daa42afde17cb3ad3115e44a75f0d7b4a32f06a6c9"}, + {file = "Pillow-8.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:18a07a683805d32826c09acfce44a90bf474e6a66ce482b1c7fcd3757d588df3"}, + {file = "Pillow-8.3.2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4e59e99fd680e2b8b11bbd463f3c9450ab799305d5f2bafb74fefba6ac058616"}, + {file = "Pillow-8.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4d89a2e9219a526401015153c0e9dd48319ea6ab9fe3b066a20aa9aee23d9fd3"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56fd98c8294f57636084f4b076b75f86c57b2a63a8410c0cd172bc93695ee979"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b11c9d310a3522b0fd3c35667914271f570576a0e387701f370eb39d45f08a4"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0412516dcc9de9b0a1e0ae25a280015809de8270f134cc2c1e32c4eeb397cf30"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bcb04ff12e79b28be6c9988f275e7ab69f01cc2ba319fb3114f87817bb7c74b6"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0b9911ec70731711c3b6ebcde26caea620cbdd9dcb73c67b0730c8817f24711b"}, + {file = "Pillow-8.3.2-cp38-cp38-win32.whl", hash = "sha256:ce2e5e04bb86da6187f96d7bab3f93a7877830981b37f0287dd6479e27a10341"}, + {file = "Pillow-8.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:35d27687f027ad25a8d0ef45dd5208ef044c588003cdcedf05afb00dbc5c2deb"}, + {file = "Pillow-8.3.2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:04835e68ef12904bc3e1fd002b33eea0779320d4346082bd5b24bec12ad9c3e9"}, + {file = "Pillow-8.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:10e00f7336780ca7d3653cf3ac26f068fa11b5a96894ea29a64d3dc4b810d630"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cde7a4d3687f21cffdf5bb171172070bb95e02af448c4c8b2f223d783214056"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c3ff00110835bdda2b1e2b07f4a2548a39744bb7de5946dc8e95517c4fb2ca6"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35d409030bf3bd05fa66fb5fdedc39c521b397f61ad04309c90444e893d05f7d"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bff50ba9891be0a004ef48828e012babaaf7da204d81ab9be37480b9020a82b"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7dbfbc0020aa1d9bc1b0b8bcf255a7d73f4ad0336f8fd2533fcc54a4ccfb9441"}, + {file = "Pillow-8.3.2-cp39-cp39-win32.whl", hash = "sha256:963ebdc5365d748185fdb06daf2ac758116deecb2277ec5ae98139f93844bc09"}, + {file = "Pillow-8.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:cc9d0dec711c914ed500f1d0d3822868760954dce98dfb0b7382a854aee55d19"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2c661542c6f71dfd9dc82d9d29a8386287e82813b0375b3a02983feac69ef864"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:548794f99ff52a73a156771a0402f5e1c35285bd981046a502d7e4793e8facaa"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b68f565a4175e12e68ca900af8910e8fe48aaa48fd3ca853494f384e11c8bcd"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:838eb85de6d9307c19c655c726f8d13b8b646f144ca6b3771fa62b711ebf7624"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:feb5db446e96bfecfec078b943cc07744cc759893cef045aa8b8b6d6aaa8274e"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:fc0db32f7223b094964e71729c0361f93db43664dd1ec86d3df217853cedda87"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd4fd83aa912d7b89b4b4a1580d30e2a4242f3936882a3f433586e5ab97ed0d5"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d0c8ebbfd439c37624db98f3877d9ed12c137cadd99dde2d2eae0dab0bbfc355"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6cb3dd7f23b044b0737317f892d399f9e2f0b3a02b22b2c692851fb8120d82c6"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a66566f8a22561fc1a88dc87606c69b84fa9ce724f99522cf922c801ec68f5c1"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ce651ca46d0202c302a535d3047c55a0131a720cf554a578fc1b8a2aff0e7d96"}, + {file = "Pillow-8.3.2.tar.gz", hash = "sha256:dde3f3ed8d00c72631bc19cbfff8ad3b6215062a5eed402381ad365f82f0c18c"}, ] pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, @@ -2294,6 +2313,7 @@ pynput = [ pyobjc-core = [ {file = "pyobjc-core-7.3.tar.gz", hash = "sha256:5081aedf8bb40aac1a8ad95adac9e44e148a882686ded614adf46bb67fd67574"}, {file = "pyobjc_core-7.3-1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a1f1e6b457127cbf2b5bd2b94520a7c89fb590b739911eadb2b0499a3a5b0e6f"}, + {file = "pyobjc_core-7.3-1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:ed708cc47bae8b711f81f252af09898a5f986c7a38cec5ad5623d571d328bff8"}, {file = "pyobjc_core-7.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e93ad769a20b908778fe950f62a843a6d8f0fa71996e5f3cc9fab5ae7d17771"}, {file = "pyobjc_core-7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9f63fd37bbf3785af4ddb2f86cad5ca81c62cfc7d1c0099637ca18343c3656c1"}, {file = "pyobjc_core-7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9b1311f72f2e170742a7ee3a8149f52c35158dc024a21e88d6f1e52ba5d718b"}, @@ -2303,6 +2323,7 @@ pyobjc-core = [ pyobjc-framework-cocoa = [ {file = "pyobjc-framework-Cocoa-7.3.tar.gz", hash = "sha256:b18d05e7a795a3455ad191c3e43d6bfa673c2a4fd480bb1ccf57191051b80b7e"}, {file = "pyobjc_framework_Cocoa-7.3-1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1e31376806e5de883a1d7c7c87d9ff2a8b09fc05d267e0dfce6e42409fb70c67"}, + {file = "pyobjc_framework_Cocoa-7.3-1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:d999387927284346035cb63ebb51f86331abc41f9376f9a6970e7f18207db392"}, {file = "pyobjc_framework_Cocoa-7.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9edffdfa6dd1f71f21b531c3e61fdd3e4d5d3bf6c5a528c98e88828cd60bac11"}, {file = "pyobjc_framework_Cocoa-7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:35a6340437a4e0109a302150b7d1f6baf57004ccf74834f9e6062fcafe2fd8d7"}, {file = "pyobjc_framework_Cocoa-7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7c3886f2608ab3ed02482f8b2ebf9f782b324c559e84b52cfd92dba8a1109872"}, @@ -2312,6 +2333,7 @@ pyobjc-framework-cocoa = [ pyobjc-framework-quartz = [ {file = "pyobjc-framework-Quartz-7.3.tar.gz", hash = "sha256:98812844c34262def980bdf60923a875cd43428a8375b6fd53bd2cd800eccf0b"}, {file = "pyobjc_framework_Quartz-7.3-1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1139bc6874c0f8b58f0b8602015e0994198bc506a6bcec1071208de32b55ed26"}, + {file = "pyobjc_framework_Quartz-7.3-1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:d94a3ed7051266c52392ec07d3b5adbf28d4be83341a24df0d88639344dcd84f"}, {file = "pyobjc_framework_Quartz-7.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ef18f5a16511ded65980bf4f5983ea5d35c88224dbad1b3112abd29c60413ea"}, {file = "pyobjc_framework_Quartz-7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b41eec8d4b10c7c7e011e2f9051367f5499ef315ba52dfbae573c3a2e05469c"}, {file = "pyobjc_framework_Quartz-7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c65456ed045dfe1711d0298734e5a3ad670f8c770f7eb3b19979256c388bdd2"}, diff --git a/pyproject.toml b/pyproject.toml index 085538d306..1a112d2071 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.0.0" +version = "3.6.0-nightly.1" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" @@ -45,7 +45,7 @@ jsonschema = "^3.2.0" keyring = "^22.0.1" log4mongo = "^1.7" pathlib2= "^2.3.5" # deadline submit publish job only (single place, maybe not needed?) -Pillow = "^8.1" # only used for slates prototype +Pillow = "^8.3" # only used for slates prototype pyblish-base = "^1.8.8" pynput = "^1.7.2" # idle manager in tray pymongo = "^3.11.2" diff --git a/tests/integration/hosts/photoshop/test_publish_in_photoshop.py b/tests/integration/hosts/photoshop/test_publish_in_photoshop.py new file mode 100644 index 0000000000..396468a966 --- /dev/null +++ b/tests/integration/hosts/photoshop/test_publish_in_photoshop.py @@ -0,0 +1,94 @@ +import pytest +import os +import shutil + +from tests.lib.testing_classes import PublishTest + + +class TestPublishInPhotoshop(PublishTest): + """Basic test case for publishing in Photoshop + + Uses generic TestCase to prepare fixtures for test data, testing DBs, + env vars. + + Opens Maya, run publish on prepared workile. + + Then checks content of DB (if subset, version, representations were + created. + Checks tmp folder if all expected files were published. + + """ + PERSIST = True + + TEST_FILES = [ + ("1Bciy2pCwMKl1UIpxuPnlX_LHMo_Xkq0K", "test_photoshop_publish.zip", "") + ] + + APP = "photoshop" + APP_VARIANT = "2020" + + APP_NAME = "{}/{}".format(APP, APP_VARIANT) + + TIMEOUT = 120 # publish timeout + + @pytest.fixture(scope="module") + def last_workfile_path(self, download_test_data): + """Get last_workfile_path from source data. + + Maya expects workfile in proper folder, so copy is done first. + """ + src_path = os.path.join(download_test_data, + "input", + "workfile", + "test_project_test_asset_TestTask_v001.psd") + dest_folder = os.path.join(download_test_data, + self.PROJECT, + self.ASSET, + "work", + self.TASK) + os.makedirs(dest_folder) + dest_path = os.path.join(dest_folder, + "test_project_test_asset_TestTask_v001.psd") + shutil.copy(src_path, dest_path) + + yield dest_path + + @pytest.fixture(scope="module") + def startup_scripts(self, monkeypatch_session, download_test_data): + """Points Maya to userSetup file from input data""" + os.environ["IS_HEADLESS"] = "true" + + def test_db_asserts(self, dbcon, publish_finished): + """Host and input data dependent expected results in DB.""" + print("test_db_asserts") + assert 5 == dbcon.count_documents({"type": "version"}), \ + "Not expected no of versions" + + assert 0 == dbcon.count_documents({"type": "version", + "name": {"$ne": 1}}), \ + "Only versions with 1 expected" + + assert 1 == dbcon.count_documents({"type": "subset", + "name": "modelMain"}), \ + "modelMain subset must be present" + + assert 1 == dbcon.count_documents({"type": "subset", + "name": "workfileTest_task"}), \ + "workfileTest_task subset must be present" + + assert 11 == dbcon.count_documents({"type": "representation"}), \ + "Not expected no of representations" + + assert 2 == dbcon.count_documents({"type": "representation", + "context.subset": "modelMain", + "context.ext": "abc"}), \ + "Not expected no of representations with ext 'abc'" + + assert 2 == dbcon.count_documents({"type": "representation", + "context.subset": "modelMain", + "context.ext": "ma"}), \ + "Not expected no of representations with ext 'abc'" + + +if __name__ == "__main__": + test_case = TestPublishInPhotoshop() diff --git a/tests/lib/testing_classes.py b/tests/lib/testing_classes.py index 1832efb7ed..59d4abb3aa 100644 --- a/tests/lib/testing_classes.py +++ b/tests/lib/testing_classes.py @@ -228,6 +228,7 @@ class PublishTest(ModuleUnitTest): while launched_app.poll() is None: time.sleep(0.5) if time.time() - time_start > self.TIMEOUT: + launched_app.terminate() raise ValueError("Timeout reached") # some clean exit test possible?