From b12e4f42c89f784bdc02ecf70a00b131d941cdf6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 20 Apr 2021 13:48:07 +0200 Subject: [PATCH 001/161] make set concatenation python 2 and 3 compatible --- openpype/hosts/maya/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index ae2d329a97..8387bdeaac 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -581,7 +581,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(): From 3bef54dcf9fbeb082e0ea807ffc666eb0798c986 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 20 Apr 2021 13:49:35 +0200 Subject: [PATCH 002/161] use six.string_types instead of basestring --- openpype/hosts/maya/api/setdress.py | 3 ++- .../hosts/maya/plugins/publish/validate_instance_subset.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/setdress.py b/openpype/hosts/maya/api/setdress.py index be26572039..111d33da8c 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 @@ -94,7 +95,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/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))) From f6aa39af120054b4752087847fead6af0778f981 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 20 Apr 2021 13:54:48 +0200 Subject: [PATCH 003/161] first version of iteritems replacement --- openpype/hosts/maya/api/lib.py | 9 ++++-- openpype/hosts/maya/api/setdress.py | 3 +- .../hosts/maya/plugins/publish/extract_fbx.py | 3 +- .../plugins/publish/submit_maya_muster.py | 2 +- .../publish/validate_node_ids_unique.py | 3 +- .../publish/validate_node_no_ghosting.py | 5 +++- .../publish/validate_shape_render_stats.py | 6 ++-- openpype/vendor/python/common/capture.py | 29 +++++++++++++------ 8 files changed, 41 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 8387bdeaac..2070e14683 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) @@ -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/setdress.py b/openpype/hosts/maya/api/setdress.py index 111d33da8c..3537fa3837 100644 --- a/openpype/hosts/maya/api/setdress.py +++ b/openpype/hosts/maya/api/setdress.py @@ -70,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] 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_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/vendor/python/common/capture.py b/openpype/vendor/python/common/capture.py index 83816ec92a..59aafcb57a 100644 --- a/openpype/vendor/python/common/capture.py +++ b/openpype/vendor/python/common/capture.py @@ -364,7 +364,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 +373,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 +635,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 +730,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 +779,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) From 66ec28ca964a653dab3db017a3392e6776873c9d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 20 Apr 2021 13:55:00 +0200 Subject: [PATCH 004/161] fix context manager in maya capture --- openpype/vendor/python/common/capture.py | 75 ++++++++++++++++-------- 1 file changed, 50 insertions(+), 25 deletions(-) diff --git a/openpype/vendor/python/common/capture.py b/openpype/vendor/python/common/capture.py index 59aafcb57a..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 From 1d6b10bb1d98e48f3badb6f8da84f06607ca455c Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 7 Sep 2021 15:51:36 +0100 Subject: [PATCH 005/161] Camera handling between Blender and Unreal implemented --- .../blender/plugins/create/create_camera.py | 51 ++++- .../blender/plugins/load/load_layout_json.py | 18 ++ .../blender/plugins/publish/extract_camera.py | 72 +++++++ .../hosts/unreal/plugins/load/load_camera.py | 175 ++++++++++++++++++ 4 files changed, 306 insertions(+), 10 deletions(-) create mode 100644 openpype/hosts/blender/plugins/publish/extract_camera.py create mode 100644 openpype/hosts/unreal/plugins/load/load_camera.py diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index c7fea30787..cf95c6b6d1 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,47 @@ 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: + bpy.ops.object.select_all(action='DESELECT') + 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_layout_json.py b/openpype/hosts/blender/plugins/load/load_layout_json.py index 1a4dbbb5cb..0b3e69ada8 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,6 +60,8 @@ class JsonLayoutLoader(plugin.AssetLoader): return None def _process(self, libpath, asset, asset_group, actions): + print(f"asset: {asset}") + bpy.ops.object.select_all(action='DESELECT') with open(libpath, "r") as fp: @@ -103,6 +106,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/publish/extract_camera.py b/openpype/hosts/blender/plugins/publish/extract_camera.py new file mode 100644 index 0000000000..3523498808 --- /dev/null +++ b/openpype/hosts/blender/plugins/publish/extract_camera.py @@ -0,0 +1,72 @@ +import os + +from openpype import api +from openpype.hosts.blender.api import plugin + +import bpy + + +class ExtractFBX(api.Extractor): + """Extract as FBX.""" + + label = "Extract FBX" + 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..") + + bpy.ops.object.select_all(action='DESELECT') + + 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'} + ) + + bpy.context.scene.unit_settings.scale_length = scale_length + + bpy.ops.object.select_all(action='DESELECT') + + 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/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py new file mode 100644 index 0000000000..3d6cde7daf --- /dev/null +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -0,0 +1,175 @@ +import os + +from avalon import api, 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() + 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() + ) + + settings = unreal.MovieSceneUserImportFBXSettings() + + 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() + ) + + settings = unreal.MovieSceneUserImportFBXSettings() + + 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) From eae7079b087f71e5d6e21fc85da9998eaf448368 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 7 Sep 2021 16:00:05 +0100 Subject: [PATCH 006/161] Hound fixes --- openpype/hosts/blender/plugins/create/create_camera.py | 3 +-- openpype/hosts/unreal/plugins/load/load_camera.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index cf95c6b6d1..fad827d85a 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -21,14 +21,13 @@ class CreateCamera(plugin.Creator): mti = ops.MainThreadItem(self._process) ops.execute_in_main_thread(mti) - def process(self): + 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"] diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 3d6cde7daf..bf89ecd23e 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -57,8 +57,8 @@ class CameraLoader(api.Loader): ) # Get highest number to make a unique name - folders = [a for a in asset_content - if a[-1] == "/" and f"{name}_" in a] + 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 From a290d77a41b40ac89235e48bb9f1521fc041bc6c Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 13 Sep 2021 11:18:25 +0100 Subject: [PATCH 007/161] Fixed Unreal support for templates --- .../unreal/hooks/pre_workfile_preparation.py | 50 ++++++++++++++++--- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index 01b8b6bc05..0c7146634f 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,45 @@ class UnrealPrelaunchHook(PreLaunchHook): self.signature = "( {} )".format(self.__class__.__name__) + def _get_work_filename(self): + # Use last workfile if was found + last_workfile = self.data.get("last_workfile_path") + if last_workfile and os.path.exists(last_workfile): + return os.path.basename(last_workfile) + + # 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 +79,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(( @@ -55,7 +91,7 @@ class UnrealPrelaunchHook(PreLaunchHook): # of the project name. This is because project name is then used # in various places inside c++ code and there variable names cannot # start with non-alpha. We append 'P' before project name to solve it. - # 😱 + # 😱 if not unreal_project_name[:1].isalpha(): self.log.warning(( "Project name doesn't start with alphabet " @@ -89,10 +125,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(( From 8c27675b4382ee8277b6c325f7d3b912b684752c Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 23 Sep 2021 16:26:59 +0100 Subject: [PATCH 008/161] Implemented camera publishing in Unreal and loading in Blender --- .../{load_camera.py => load_camera_blend.py} | 2 +- .../blender/plugins/load/load_camera_fbx.py | 218 ++++++++++++++++++ .../blender/plugins/publish/extract_camera.py | 6 +- .../unreal/plugins/create/create_camera.py | 56 +++++ .../unreal/plugins/publish/extract_camera.py | 55 +++++ 5 files changed, 333 insertions(+), 4 deletions(-) rename openpype/hosts/blender/plugins/load/{load_camera.py => load_camera_blend.py} (99%) create mode 100644 openpype/hosts/blender/plugins/load/load_camera_fbx.py create mode 100644 openpype/hosts/unreal/plugins/create/create_camera.py create mode 100644 openpype/hosts/unreal/plugins/publish/extract_camera.py diff --git a/openpype/hosts/blender/plugins/load/load_camera.py b/openpype/hosts/blender/plugins/load/load_camera_blend.py similarity index 99% rename from openpype/hosts/blender/plugins/load/load_camera.py rename to openpype/hosts/blender/plugins/load/load_camera_blend.py index 30300100e0..6c3348a1a6 100644 --- a/openpype/hosts/blender/plugins/load/load_camera.py +++ b/openpype/hosts/blender/plugins/load/load_camera_blend.py @@ -23,7 +23,7 @@ class BlendCameraLoader(openpype.hosts.blender.api.plugin.AssetLoader): families = ["camera"] representations = ["blend"] - label = "Link Camera" + label = "Link Camera (Blend)" icon = "code-fork" color = "orange" 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..88a8931784 --- /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): + bpy.ops.object.select_all(action='DESELECT') + + 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}) + + bpy.ops.object.select_all(action='DESELECT') + + 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, action) + + 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/publish/extract_camera.py b/openpype/hosts/blender/plugins/publish/extract_camera.py index 3523498808..f7e128f227 100644 --- a/openpype/hosts/blender/plugins/publish/extract_camera.py +++ b/openpype/hosts/blender/plugins/publish/extract_camera.py @@ -6,10 +6,10 @@ from openpype.hosts.blender.api import plugin import bpy -class ExtractFBX(api.Extractor): - """Extract as FBX.""" +class ExtractCamera(api.Extractor): + """Extract as the camera as FBX.""" - label = "Extract FBX" + label = "Extract Camera" hosts = ["blender"] families = ["camera"] optional = True 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..74a3c2876b --- /dev/null +++ b/openpype/hosts/unreal/plugins/create/create_camera.py @@ -0,0 +1,56 @@ +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"] + + # selection = [] + # # if (self.options or {}).get("useSelection"): + # # sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() + # # selection = [a.get_path_name() for a in sel_objects] + + # data["level"] = ell.get_editor_world().get_path_name() + + data["level"] = ell.get_editor_world().get_path_name() + + # if (self.options or {}).get("useSelection"): + # # Set as members the selected actors + # for actor in ell.get_selected_level_actors(): + # data["members"].append("{}.{}".format( + # actor.get_outer().get_name(), actor.get_name())) + + if not eal.does_directory_exist(self.root): + eal.make_directory(self.root) + + factory = unreal.LevelSequenceFactoryNew() + tools = unreal.AssetToolsHelpers().get_asset_tools() + asset = 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/publish/extract_camera.py b/openpype/hosts/unreal/plugins/publish/extract_camera.py new file mode 100644 index 0000000000..fe5326cf02 --- /dev/null +++ b/openpype/hosts/unreal/plugins/publish/extract_camera.py @@ -0,0 +1,55 @@ +import os + +import unreal +from unreal import EditorAssetLibrary as eal +from unreal import EditorLevelLibrary as ell + +import openpype.api +from avalon import io + + +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) From abc03a402a498b35c43e2ab2940171bafe5a04c7 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 23 Sep 2021 16:30:54 +0100 Subject: [PATCH 009/161] Fix: camera was created once per asset when layout was loaded --- .../blender/plugins/load/load_layout_json.py | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_layout_json.py b/openpype/hosts/blender/plugins/load/load_layout_json.py index 0b3e69ada8..dac74ec5f0 100644 --- a/openpype/hosts/blender/plugins/load/load_layout_json.py +++ b/openpype/hosts/blender/plugins/load/load_layout_json.py @@ -106,20 +106,20 @@ 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.") + # 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"])} - ) + 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, From c1eeee9fdc81a255f84dd98338d90ea818e2ec77 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 23 Sep 2021 16:49:51 +0100 Subject: [PATCH 010/161] Apply suggestions from code review --- openpype/hosts/unreal/hooks/pre_workfile_preparation.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index 0c7146634f..880dba5cfb 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -29,9 +29,10 @@ class UnrealPrelaunchHook(PreLaunchHook): def _get_work_filename(self): # Use last workfile if was found - last_workfile = self.data.get("last_workfile_path") - if last_workfile and os.path.exists(last_workfile): - return os.path.basename(last_workfile) + 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"] @@ -91,7 +92,7 @@ class UnrealPrelaunchHook(PreLaunchHook): # of the project name. This is because project name is then used # in various places inside c++ code and there variable names cannot # start with non-alpha. We append 'P' before project name to solve it. - # 😱 + # 😱 if not unreal_project_name[:1].isalpha(): self.log.warning(( "Project name doesn't start with alphabet " From fe0d4e8347a2c7a32ee700ca8de3fae9a2dd84be Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 23 Sep 2021 16:56:33 +0100 Subject: [PATCH 011/161] Hound fixes --- .../hosts/blender/plugins/load/load_camera_fbx.py | 2 +- .../blender/plugins/load/load_layout_json.py | 2 +- .../hosts/unreal/plugins/create/create_camera.py | 15 +-------------- .../unreal/plugins/publish/extract_camera.py | 1 - 4 files changed, 3 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_camera_fbx.py b/openpype/hosts/blender/plugins/load/load_camera_fbx.py index 88a8931784..c6d491870d 100644 --- a/openpype/hosts/blender/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/blender/plugins/load/load_camera_fbx.py @@ -185,7 +185,7 @@ class FbxCameraLoader(plugin.AssetLoader): mat = asset_group.matrix_basis.copy() self._remove(asset_group) - self._process(str(libpath), asset_group, object_name, action) + self._process(str(libpath), asset_group, object_name) asset_group.matrix_basis = mat diff --git a/openpype/hosts/blender/plugins/load/load_layout_json.py b/openpype/hosts/blender/plugins/load/load_layout_json.py index dac74ec5f0..9c7a75ad7f 100644 --- a/openpype/hosts/blender/plugins/load/load_layout_json.py +++ b/openpype/hosts/blender/plugins/load/load_layout_json.py @@ -110,7 +110,7 @@ class JsonLayoutLoader(plugin.AssetLoader): creator_plugin = lib.get_creator_by_name("CreateCamera") if not creator_plugin: raise ValueError("Creator plugin \"CreateCamera\" was " - "not found.") + "not found.") api.create( creator_plugin, diff --git a/openpype/hosts/unreal/plugins/create/create_camera.py b/openpype/hosts/unreal/plugins/create/create_camera.py index 74a3c2876b..eda2b52be3 100644 --- a/openpype/hosts/unreal/plugins/create/create_camera.py +++ b/openpype/hosts/unreal/plugins/create/create_camera.py @@ -27,27 +27,14 @@ class CreateCamera(Creator): name = data["subset"] - # selection = [] - # # if (self.options or {}).get("useSelection"): - # # sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() - # # selection = [a.get_path_name() for a in sel_objects] - - # data["level"] = ell.get_editor_world().get_path_name() - data["level"] = ell.get_editor_world().get_path_name() - # if (self.options or {}).get("useSelection"): - # # Set as members the selected actors - # for actor in ell.get_selected_level_actors(): - # data["members"].append("{}.{}".format( - # actor.get_outer().get_name(), actor.get_name())) - if not eal.does_directory_exist(self.root): eal.make_directory(self.root) factory = unreal.LevelSequenceFactoryNew() tools = unreal.AssetToolsHelpers().get_asset_tools() - asset = tools.create_asset(name, f"{self.root}/{name}", None, factory) + tools.create_asset(name, f"{self.root}/{name}", None, factory) asset_name = f"{self.root}/{name}/{name}.{name}" diff --git a/openpype/hosts/unreal/plugins/publish/extract_camera.py b/openpype/hosts/unreal/plugins/publish/extract_camera.py index fe5326cf02..10862fc0ef 100644 --- a/openpype/hosts/unreal/plugins/publish/extract_camera.py +++ b/openpype/hosts/unreal/plugins/publish/extract_camera.py @@ -5,7 +5,6 @@ from unreal import EditorAssetLibrary as eal from unreal import EditorLevelLibrary as ell import openpype.api -from avalon import io class ExtractCamera(openpype.api.Extractor): From 5d1915aff068977ed9ccbe9a2779038be8568a18 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 24 Sep 2021 11:04:14 +0100 Subject: [PATCH 012/161] Fix: problem when loading camera and no existing folders --- openpype/hosts/unreal/plugins/load/load_camera.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index bf89ecd23e..3b7377f848 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -65,7 +65,10 @@ class CameraLoader(api.Loader): # removes the last element (which is a "/"). f_numbers.append(int(f.split("_")[-1][:-1])) f_numbers.sort() - unique_number = f_numbers[-1] + 1 + 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="") From e382b17622c9ffa7ef0db656272b2be0b270f739 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 6 Oct 2021 11:12:02 +0100 Subject: [PATCH 013/161] SequencerScripting plugin is automatically enabled when creating project This plugin is essential to handle camera assets in Unreal --- openpype/hosts/unreal/api/lib.py | 1 + 1 file changed, 1 insertion(+) 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} ] } From efe8915842d5c07a5c049b220612b2d3171857fb Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 6 Oct 2021 16:20:49 +0100 Subject: [PATCH 014/161] Changed fbx settings for extraction of the camera --- openpype/hosts/blender/plugins/publish/extract_camera.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_camera.py b/openpype/hosts/blender/plugins/publish/extract_camera.py index f7e128f227..7888ddad6a 100644 --- a/openpype/hosts/blender/plugins/publish/extract_camera.py +++ b/openpype/hosts/blender/plugins/publish/extract_camera.py @@ -50,7 +50,8 @@ class ExtractCamera(api.Extractor): filepath=filepath, use_active_collection=False, use_selection=True, - object_types={'CAMERA'} + object_types={'CAMERA'}, + bake_anim_simplify_factor=0.0 ) bpy.context.scene.unit_settings.scale_length = scale_length From 31c46152899d895fbc6293779fe0c691a189bf14 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 6 Oct 2021 16:22:05 +0100 Subject: [PATCH 015/161] Use info from database for fps and frame range when loading Camera --- openpype/hosts/unreal/plugins/load/load_camera.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 3b7377f848..ea55c491e2 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -1,6 +1,6 @@ import os -from avalon import api, pipeline +from avalon import api, io, pipeline from avalon.unreal import lib from avalon.unreal import pipeline as unreal_pipeline import unreal @@ -84,6 +84,19 @@ class CameraLoader(api.Loader): factory=unreal.LevelSequenceFactoryNew() ) + asset_name = io.Session["AVALON_ASSET"] + asset_doc = io.find_one({ + "type": "asset", + "name": asset_name + }) + + 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() unreal.SequencerTools.import_fbx( From ff8b69f09413c0cc11090689d6a67f7aac8904d5 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 7 Oct 2021 11:10:38 +0100 Subject: [PATCH 016/161] Get frame range and fps from database for update as well --- .../hosts/unreal/plugins/load/load_camera.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index ea55c491e2..afacb92a05 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -84,10 +84,10 @@ class CameraLoader(api.Loader): factory=unreal.LevelSequenceFactoryNew() ) - asset_name = io.Session["AVALON_ASSET"] + io_asset = io.Session["AVALON_ASSET"] asset_doc = io.find_one({ "type": "asset", - "name": asset_name + "name": io_asset }) data = asset_doc.get("data") @@ -167,6 +167,19 @@ class CameraLoader(api.Loader): 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() unreal.SequencerTools.import_fbx( From 0689783f607c55d9ed237be1941e1d3f3a392aa5 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 7 Oct 2021 12:08:12 +0100 Subject: [PATCH 017/161] Updated FBX import settings for the camera in Unreal --- openpype/hosts/unreal/plugins/load/load_camera.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index afacb92a05..b2b25eec73 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -98,6 +98,7 @@ class CameraLoader(api.Loader): 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(), @@ -181,6 +182,7 @@ class CameraLoader(api.Loader): 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(), From 1f2225742304f213528182e8a34bc5e09a0b3383 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 7 Oct 2021 16:25:11 +0100 Subject: [PATCH 018/161] Added validator for camera animations to 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. --- .../publish/validate_camera_zero_keyframe.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 openpype/hosts/blender/plugins/publish/validate_camera_zero_keyframe.py 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}") From f92ee311b8fdb05ffae23194a64fa9e93edd4bfc Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 8 Oct 2021 17:23:10 +0200 Subject: [PATCH 019/161] PYPE-1901 - Fix ConsoleTrayApp sending lines --- openpype/tools/tray_app/app.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/openpype/tools/tray_app/app.py b/openpype/tools/tray_app/app.py index 03f8321464..476f061e26 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: From b90bcd9877e56cfd91040d110a1cc13fc7aea2c5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 8 Oct 2021 17:25:42 +0200 Subject: [PATCH 020/161] PYPE-1901 - New pluging for remote publishing (webpublish) Extracted _json_load method --- .../publish/collect_remote_instances.py | 88 +++++++++++++++++++ .../publish/collect_published_files.py | 23 +---- openpype/lib/plugin_tools.py | 17 ++++ 3 files changed, 109 insertions(+), 19 deletions(-) create mode 100644 openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py 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..a9d6d6fec6 --- /dev/null +++ b/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py @@ -0,0 +1,88 @@ +import pyblish.api +import os + +from avalon import photoshop +from openpype.lib import prepare_template_data + + +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 + families = ["background"] + color_code = ["red"] + subset_template_name = "" + + def process(self, context): + self.log.info("CollectRemoteInstances") + 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 = self.parse_json(os.path.join(batch_dir, + "manifest.json")) + variant = task_data["variant"] + + stub = photoshop.stub() + layers = stub.get_layers() + + instance_names = [] + for layer in layers: + self.log.info("!!!Layer:: {}".format(layer)) + if layer.color_code not in self.color_code: + 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"] = self.families[0] + instance.data["publish"] = layer.visible + + # populate data from context, coming from outside?? TODO + # TEMP + self.log.info("asset {}".format(context.data["assetEntity"])) + self.log.info("taskType {}".format(context.data["taskType"])) + 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 = self.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") diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py index 7e9b98956a..2b4a1273b8 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 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,8 +41,8 @@ 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" @@ -261,7 +246,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/lib/plugin_tools.py b/openpype/lib/plugin_tools.py index 9dccadc44e..2158a3e28d 100644 --- a/openpype/lib/plugin_tools.py +++ b/openpype/lib/plugin_tools.py @@ -483,3 +483,20 @@ def should_decompress(file_url): "compression: \"dwab\"" in output return False + + +def parse_json(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: + log.error( + "Error loading json: " + "{} - Exception: {}".format(path, exc) + ) + return data From 93766c1d0b9515764b1e57f80406948d843f62eb Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 8 Oct 2021 17:27:15 +0200 Subject: [PATCH 021/161] PYPE-1901 - Added plugin to close PS after remote publishing --- .../photoshop/plugins/publish/closePS.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 openpype/hosts/photoshop/plugins/publish/closePS.py diff --git a/openpype/hosts/photoshop/plugins/publish/closePS.py b/openpype/hosts/photoshop/plugins/publish/closePS.py new file mode 100644 index 0000000000..ce229c86bb --- /dev/null +++ b/openpype/hosts/photoshop/plugins/publish/closePS.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +"""Close PS after publish. For Webpublishing only.""" +import os + +import pyblish.api + +from avalon import photoshop + +class ClosePS(pyblish.api.InstancePlugin): + """Close PS after publish. For Webpublishing only. + """ + + order = pyblish.api.IntegratorOrder + 14 + label = "Close PS" + optional = True + active = True + + hosts = ["photoshop"] + + def process(self, instance): + 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() From 185d3ef399ebe25211b038dd0a8cfca7055ac81e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 8 Oct 2021 17:35:11 +0200 Subject: [PATCH 022/161] PYPE-1901 - Fix wrong variable used --- .../hosts/photoshop/plugins/publish/collect_remote_instances.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py index a9d6d6fec6..fa4364b700 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py @@ -72,7 +72,7 @@ class CollectRemoteInstances(pyblish.api.ContextPlugin): "task": instance.data["task"], "layer": layer.name } - subset = self.subset_template.format( + subset = self.subset_template_name.format( **prepare_template_data(fill_pairs)) instance.data["subset"] = subset From d2c4678fd45b8fb5d38d3a3e0382ff99ab9a0d06 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 8 Oct 2021 17:35:38 +0200 Subject: [PATCH 023/161] PYPE-1901 - Added background family to ExtractImage --- openpype/hosts/photoshop/plugins/publish/extract_image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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): From 57de11638c91c6518dd599db39c26347f00ad459 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 8 Oct 2021 17:36:13 +0200 Subject: [PATCH 024/161] PYPE-1901 - Added settings for CollectRemoteInstances --- .../defaults/project_settings/photoshop.json | 5 ++++ .../schema_project_photoshop.json | 29 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/openpype/settings/defaults/project_settings/photoshop.json b/openpype/settings/defaults/project_settings/photoshop.json index 4c36e4bd49..14c294c0c5 100644 --- a/openpype/settings/defaults/project_settings/photoshop.json +++ b/openpype/settings/defaults/project_settings/photoshop.json @@ -12,6 +12,11 @@ "optional": true, "active": true }, + "CollectRemoteInstances": { + "color_code": [], + "families": [], + "subset_template_name": "" + }, "ExtractImage": { "formats": [ "png", 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 3b65f08ac4..008f1a265d 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,35 @@ } ] }, + { + "type": "dict", + "collapsible": 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", + "label": "Color codes for layers", + "object_type": "text" + }, + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "type": "text", + "key": "subset_template_name", + "label": "Subset template name" + } + ] + }, { "type": "dict", "collapsible": true, From d6b85f1c1a91901581a410aba9688f2e92410aea Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 8 Oct 2021 18:46:06 +0200 Subject: [PATCH 025/161] PYPE-1901 - Added testing class for PS publishing --- .../photoshop/test_publish_in_photoshop.py | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 tests/integration/hosts/photoshop/test_publish_in_photoshop.py 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() From cff9c793464abb3211ef49780f7a0c94656c3831 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 8 Oct 2021 18:46:29 +0200 Subject: [PATCH 026/161] PYPE-1901 - Added WIP for PS webpublishing --- openpype/cli.py | 19 +++++++++++++++++++ openpype/pype_commands.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/openpype/cli.py b/openpype/cli.py index c69407e295..8438703bd3 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/pype_commands.py b/openpype/pype_commands.py index 5288749e8b..318f2476ed 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -4,6 +4,7 @@ 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 @@ -110,6 +111,42 @@ class PypeCommands: log.info("Publish finished.") uninstall() + @staticmethod + def remotepublishfromapp(project, batch_path, host, user, targets=None): + from openpype import install, uninstall + from openpype.api import Logger + + log = Logger.get_logger() + + log.info("remotepublishphotoshop command") + + install() + + os.environ["OPENPYPE_PUBLISH_DATA"] = batch_path + os.environ["AVALON_PROJECT"] = project + os.environ["AVALON_APP"] = host + + os.environ["OPENPYPE_EXECUTABLE"] = sys.executable + os.environ["IS_HEADLESS"] = "true" + from openpype.lib import ApplicationManager + + application_manager = ApplicationManager() + data = { + "last_workfile_path": "c:/projects/test_project_test_asset_TestTask_v001.psd", + "start_last_workfile": True, + "project_name": project, + "asset_name": "test_asset", + "task_name": "test_task" + } + + launched_app = application_manager.launch( + os.environ["AVALON_APP"] + "/2020", **data) + + while launched_app.poll() is None: + time.sleep(0.5) + + print(launched_app) + @staticmethod def remotepublish(project, batch_path, host, user, targets=None): """Start headless publishing. From 6f3f0e6732f725d192f3f3d30032c04ab81e105d Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 11 Oct 2021 16:55:24 +0100 Subject: [PATCH 027/161] Improved loading of camera assets in Blender from blend files --- .../blender/plugins/load/load_camera_blend.py | 263 +++++++++--------- 1 file changed, 134 insertions(+), 129 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_camera_blend.py b/openpype/hosts/blender/plugins/load/load_camera_blend.py index 6c3348a1a6..1173e26d7b 100644 --- a/openpype/hosts/blender/plugins/load/load_camera_blend.py +++ b/openpype/hosts/blender/plugins/load/load_camera_blend.py @@ -5,14 +5,19 @@ 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") +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(openpype.hosts.blender.api.plugin.AssetLoader): +class BlendCameraLoader(plugin.AssetLoader): """Load a camera from a .blend file. Warning: @@ -27,55 +32,68 @@ class BlendCameraLoader(openpype.hosts.blender.api.plugin.AssetLoader): icon = "code-fork" color = "orange" - def _remove(self, objects, lib_container): - for obj in list(objects): - bpy.data.cameras.remove(obj.data) + def _remove(self, asset_group): + objects = list(asset_group.children) - bpy.data.collections.remove(bpy.data.collections[lib_container]) + for obj in objects: + if obj.type == 'CAMERA': + bpy.data.cameras.remove(obj.data) - def _process(self, libpath, lib_container, container_name, actions): - - relative = bpy.context.preferences.filepaths.use_relative_paths + def _process(self, libpath, asset_group, group_name): with bpy.data.libraries.load( - libpath, link=True, relative=relative - ) as (_, data_to): - data_to.collections = [lib_container] + libpath, link=True, relative=False + ) as (data_from, data_to): + data_to.objects = data_from.objects - scene = bpy.context.scene + parent = bpy.context.scene.collection - scene.collection.children.link(bpy.data.collections[lib_container]) + empties = [obj for obj in data_to.objects if obj.type == 'EMPTY'] - camera_container = scene.collection.children[lib_container].make_local() + container = None - objects_list = [] + for empty in empties: + if empty.get(AVALON_PROPERTY): + container = empty + break - for obj in camera_container.objects: - local_obj = obj.make_local() - local_obj.data.make_local() + assert container, "No asset group found" - if not local_obj.get(blender.pipeline.AVALON_PROPERTY): - local_obj[blender.pipeline.AVALON_PROPERTY] = dict() + # Children must be linked before parents, + # otherwise the hierarchy will break + objects = [] + nodes = list(container.children) - avalon_info = local_obj[blender.pipeline.AVALON_PROPERTY] - avalon_info.update({"container_name": container_name}) + for obj in nodes: + obj.parent = asset_group - 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] + for obj in nodes: + objects.append(obj) + nodes.extend(list(obj.children)) - 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.reverse() - objects_list.append(local_obj) + for obj in objects: + parent.objects.link(obj) - camera_container.pop(blender.pipeline.AVALON_PROPERTY) + 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) bpy.ops.object.select_all(action='DESELECT') - return objects_list + return objects def process_asset( self, context: dict, name: str, namespace: Optional[str] = None, @@ -88,131 +106,118 @@ class BlendCameraLoader(openpype.hosts.blender.api.plugin.AssetLoader): 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__, - ) + 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}" - container_metadata = container.get( - blender.pipeline.AVALON_PROPERTY) + 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) - container_metadata["libpath"] = libpath - container_metadata["lib_container"] = lib_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_list = self._process( - libpath, lib_container, container_name, (None, None)) + objects = self._process(libpath, asset_group, group_name) - # Save the list of objects in the metadata container - container_metadata["objects"] = objects_list + bpy.context.scene.collection.objects.link(asset_group) - nodes = list(container.objects) - nodes.append(container) - self[:] = nodes - return nodes + 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 + } - def update(self, container: Dict, representation: Dict): + 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! + This will remove all children of the asset group, load the new ones + and add them as children of the group. """ - - collection = bpy.data.collections.get( - container["objectName"] - ) - + object_name = container["objectName"] + asset_group = bpy.data.objects.get(object_name) libpath = Path(api.get_representation_path(representation)) extension = libpath.suffix.lower() - logger.info( + self.log.info( "Container: %s\nRepresentation: %s", pformat(container, indent=2), pformat(representation, indent=2), ) - assert collection, ( + assert asset_group, ( 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, ( + assert extension in 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"] + metadata = asset_group.get(AVALON_PROPERTY) + group_libpath = metadata["libpath"] - normalized_collection_libpath = ( - str(Path(bpy.path.abspath(collection_libpath)).resolve()) + normalized_group_libpath = ( + str(Path(bpy.path.abspath(group_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, + self.log.debug( + "normalized_group_libpath:\n %s\nnormalized_libpath:\n %s", + normalized_group_libpath, normalized_libpath, ) - if normalized_collection_libpath == normalized_libpath: - logger.info("Library already loaded, not updating...") + if normalized_group_libpath == normalized_libpath: + self.log.info("Library already loaded, not updating...") return - camera = objects[0] + # 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 - camera_action = None - camera_data_action = None + mat = asset_group.matrix_basis.copy() - if camera.animation_data and camera.animation_data.action: - camera_action = camera.animation_data.action + self._remove(asset_group) - if camera.data.animation_data and camera.data.animation_data.action: - camera_data_action = camera.data.animation_data.action + # 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) - actions = (camera_action, camera_data_action) + self._process(str(libpath), asset_group, object_name) - self._remove(objects, lib_container) + asset_group.matrix_basis = mat - objects_list = self._process( - str(libpath), lib_container, collection.name, actions) + metadata["libpath"] = str(libpath) + metadata["representation"] = str(representation["_id"]) + metadata["parent"] = str(representation["parent"]) - # 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: + def exec_remove(self, container: Dict) -> bool: """Remove an existing container from a Blender scene. Arguments: @@ -221,27 +226,27 @@ class BlendCameraLoader(openpype.hosts.blender.api.plugin.AssetLoader): 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) + libpath = asset_group.get(AVALON_PROPERTY).get('libpath') - collection = bpy.data.collections.get( - container["objectName"] - ) - if not collection: + # 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 - 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(asset_group) - self._remove(objects, lib_container) + bpy.data.objects.remove(asset_group) - bpy.data.collections.remove(collection) + # 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 From 280c7d96ea19ae3cc7076b0bde694eb6eff39509 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 12 Oct 2021 11:53:46 +0200 Subject: [PATCH 028/161] PYPE-1901 - wip of remotepublishfromapp --- openpype/pype_commands.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index 318f2476ed..4f3e173f3e 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -125,6 +125,17 @@ class PypeCommands: os.environ["OPENPYPE_PUBLISH_DATA"] = batch_path os.environ["AVALON_PROJECT"] = project os.environ["AVALON_APP"] = host + os.environ["AVALON_APP_NAME"] = os.environ["AVALON_APP"] + "/2020" + os.environ["AVALON_ASSET"] = "test_asset" + os.environ["AVALON_TASK"] = "test_task" + + env = get_app_environments_for_context( + os.environ["AVALON_PROJECT"], + os.environ["AVALON_ASSET"], + os.environ["AVALON_TASK"], + os.environ["AVALON_APP_NAME"] + ) + os.environ.update(env) os.environ["OPENPYPE_EXECUTABLE"] = sys.executable os.environ["IS_HEADLESS"] = "true" From 9607026daec25e2e0803dd19093b528b03cea9fd Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 12 Oct 2021 11:54:05 +0200 Subject: [PATCH 029/161] PYPE-1901 - terminate process after timeout --- tests/lib/testing_classes.py | 1 + 1 file changed, 1 insertion(+) 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? From eb4cd6d7c622175e1204a0a0c9da027b05e20b07 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 12 Oct 2021 18:27:32 +0200 Subject: [PATCH 030/161] PYPE-1901 - extracted method for task parsing --- .../publish/collect_remote_instances.py | 8 ++++-- .../publish/collect_published_files.py | 17 ++++------- openpype/lib/plugin_tools.py | 28 +++++++++++++++++++ 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py index fa4364b700..62d94483e5 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py @@ -3,6 +3,7 @@ import os from avalon import photoshop from openpype.lib import prepare_template_data +from openpype.lib.plugin_tools import parse_json class CollectRemoteInstances(pyblish.api.ContextPlugin): @@ -36,8 +37,11 @@ class CollectRemoteInstances(pyblish.api.ContextPlugin): variant = "Main" if batch_dir and os.path.exists(batch_dir): # TODO check if batch manifest is same as tasks manifests - task_data = self.parse_json(os.path.join(batch_dir, - "manifest.json")) + 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() diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py index 2b4a1273b8..ecd65ebae4 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py @@ -15,7 +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 +from openpype.lib.plugin_tools import parse_json, get_batch_asset_task_info class CollectPublishedFiles(pyblish.api.ContextPlugin): @@ -45,18 +45,11 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): "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 diff --git a/openpype/lib/plugin_tools.py b/openpype/lib/plugin_tools.py index 2158a3e28d..62a9d7c51e 100644 --- a/openpype/lib/plugin_tools.py +++ b/openpype/lib/plugin_tools.py @@ -486,6 +486,13 @@ def should_decompress(file_url): 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) @@ -500,3 +507,24 @@ def parse_json(path): "{} - 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 From 5d1a83a28ab886b624866920aa17158b3a56dc95 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 12 Oct 2021 18:42:57 +0200 Subject: [PATCH 031/161] PYPE-1901 - add resolving of ftrack username to remote publish of Photoshop too --- .../ftrack/plugins/publish/collect_username.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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..438ef2f31b 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,20 @@ 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") + 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)) From 7d3c1863c278bbbae27aac9c5a34f0eb0736bfca Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 12 Oct 2021 19:25:55 +0200 Subject: [PATCH 032/161] PYPE-1901 - working remotepublishfromapp command --- openpype/pype_commands.py | 83 ++++++++++++++++++++++++++++----------- 1 file changed, 59 insertions(+), 24 deletions(-) diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index 4f3e173f3e..e06ab5b493 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -8,6 +8,7 @@ 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 class PypeCommands: @@ -112,7 +113,17 @@ class PypeCommands: uninstall() @staticmethod - def remotepublishfromapp(project, batch_path, host, user, targets=None): + 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. + + Requires installed host application on the machine. + + Runs publish process as user would, in automatic fashion. + """ from openpype import install, uninstall from openpype.api import Logger @@ -122,36 +133,60 @@ class PypeCommands: install() - os.environ["OPENPYPE_PUBLISH_DATA"] = batch_path - os.environ["AVALON_PROJECT"] = project - os.environ["AVALON_APP"] = host - os.environ["AVALON_APP_NAME"] = os.environ["AVALON_APP"] + "/2020" - os.environ["AVALON_ASSET"] = "test_asset" - os.environ["AVALON_TASK"] = "test_task" + from openpype.lib import ApplicationManager + application_manager = ApplicationManager() + 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)) + + app_name = "{}/{}".format(host, found_variant_key) + + os.environ["OPENPYPE_PUBLISH_DATA"] = batch_dir + + batch_data = None + if batch_dir and os.path.exists(batch_dir): + # TODO check if batch manifest is same as tasks manifests + 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"]) + + workfile_path = os.path.join(batch_dir, + batch_data["task"], + batch_data["files"][0]) + print("workfile_path {}".format(workfile_path)) + data = { + "last_workfile_path": workfile_path, + "start_last_workfile": True + } + + # must have for proper launch of app env = get_app_environments_for_context( - os.environ["AVALON_PROJECT"], - os.environ["AVALON_ASSET"], - os.environ["AVALON_TASK"], - os.environ["AVALON_APP_NAME"] + project, + asset, + task_name, + app_name ) os.environ.update(env) - os.environ["OPENPYPE_EXECUTABLE"] = sys.executable os.environ["IS_HEADLESS"] = "true" - from openpype.lib import ApplicationManager - application_manager = ApplicationManager() - data = { - "last_workfile_path": "c:/projects/test_project_test_asset_TestTask_v001.psd", - "start_last_workfile": True, - "project_name": project, - "asset_name": "test_asset", - "task_name": "test_task" - } - - launched_app = application_manager.launch( - os.environ["AVALON_APP"] + "/2020", **data) + launched_app = application_manager.launch(app_name, **data) while launched_app.poll() is None: time.sleep(0.5) From d085e4ff7df89cfe4e770fd9ae0894e8506265c0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 13 Oct 2021 13:03:47 +0200 Subject: [PATCH 033/161] PYPE-1901 - switch to context plugin to limit double closing --- openpype/hosts/photoshop/plugins/publish/closePS.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/closePS.py b/openpype/hosts/photoshop/plugins/publish/closePS.py index ce229c86bb..fa9d27688b 100644 --- a/openpype/hosts/photoshop/plugins/publish/closePS.py +++ b/openpype/hosts/photoshop/plugins/publish/closePS.py @@ -6,7 +6,7 @@ import pyblish.api from avalon import photoshop -class ClosePS(pyblish.api.InstancePlugin): +class ClosePS(pyblish.api.ContextPlugin): """Close PS after publish. For Webpublishing only. """ @@ -17,7 +17,7 @@ class ClosePS(pyblish.api.InstancePlugin): hosts = ["photoshop"] - def process(self, instance): + def process(self, context): self.log.info("ClosePS") if not os.environ.get("IS_HEADLESS"): return @@ -26,3 +26,4 @@ class ClosePS(pyblish.api.InstancePlugin): self.log.info("Shutting down PS") stub.save() stub.close() + self.log.info("PS closed") From 15b67e239ba7a6619b13083ff461bf376aa34485 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 13 Oct 2021 14:44:51 +0200 Subject: [PATCH 034/161] PYPE-1901 - fix missing return for standard publishing --- .../default_modules/ftrack/plugins/publish/collect_username.py | 1 + 1 file changed, 1 insertion(+) 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 438ef2f31b..844a397066 100644 --- a/openpype/modules/default_modules/ftrack/plugins/publish/collect_username.py +++ b/openpype/modules/default_modules/ftrack/plugins/publish/collect_username.py @@ -36,6 +36,7 @@ class CollectUsername(pyblish.api.ContextPlugin): 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"] From a08b3da447f24be48c3448070ef5f493e8e51928 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 13 Oct 2021 16:52:33 +0200 Subject: [PATCH 035/161] PYPE-1901 - fix order of closing and processing from queue --- openpype/tools/tray_app/app.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/tools/tray_app/app.py b/openpype/tools/tray_app/app.py index 476f061e26..f1363d0cab 100644 --- a/openpype/tools/tray_app/app.py +++ b/openpype/tools/tray_app/app.py @@ -203,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): @@ -230,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() From 2150ac836edccf74420ee12d71c39bf496c9a26a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 13 Oct 2021 16:55:42 +0200 Subject: [PATCH 036/161] PYPE-1901 - extracted methods into remote_publish library Methods are used in both remote* approaches for logging and reusability. --- openpype/lib/remote_publish.py | 92 ++++++++++++++++++++++++++++++++++ openpype/pype_commands.py | 86 +++++++++---------------------- 2 files changed, 116 insertions(+), 62 deletions(-) create mode 100644 openpype/lib/remote_publish.py diff --git a/openpype/lib/remote_publish.py b/openpype/lib/remote_publish.py new file mode 100644 index 0000000000..aa8d8821a8 --- /dev/null +++ b/openpype/lib/remote_publish.py @@ -0,0 +1,92 @@ +import os +from datetime import datetime +import sys +from bson.objectid import ObjectId + +import pyblish.util + +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): + """Loops through all plugins, logs ok and fails into OP DB. + + Args: + dbcon (OpenPypeMongoConnection) + _id (str) + log (OpenPypeLogger) + """ + # Error exit as soon as any error occurs. + error_format = "Failed {plugin.__name__}: {error} -- {error.traceback}" + + 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) + + }} + ) + 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) + }} + ) \ No newline at end of file diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index e06ab5b493..e2869a956d 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -9,6 +9,11 @@ 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 +) class PypeCommands: @@ -152,8 +157,6 @@ class PypeCommands: app_name = "{}/{}".format(host, found_variant_key) - os.environ["OPENPYPE_PUBLISH_DATA"] = batch_dir - batch_data = None if batch_dir and os.path.exists(batch_dir): # TODO check if batch manifest is same as tasks manifests @@ -170,10 +173,6 @@ class PypeCommands: batch_data["task"], batch_data["files"][0]) print("workfile_path {}".format(workfile_path)) - data = { - "last_workfile_path": workfile_path, - "start_last_workfile": True - } # must have for proper launch of app env = get_app_environments_for_context( @@ -184,19 +183,34 @@ class PypeCommands: ) os.environ.update(env) + _, 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) + + 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) - print(launched_app) + 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: @@ -217,7 +231,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 @@ -249,62 +262,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() From b0705417ab1236bce923142c92cb3d2b0a0c463a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 13 Oct 2021 17:38:37 +0200 Subject: [PATCH 037/161] PYPE-1901 - close host if any plugin fails --- .../photoshop/plugins/publish/closePS.py | 1 + openpype/lib/remote_publish.py | 22 +++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/closePS.py b/openpype/hosts/photoshop/plugins/publish/closePS.py index fa9d27688b..19994a0db8 100644 --- a/openpype/hosts/photoshop/plugins/publish/closePS.py +++ b/openpype/hosts/photoshop/plugins/publish/closePS.py @@ -6,6 +6,7 @@ import pyblish.api from avalon import photoshop + class ClosePS(pyblish.api.ContextPlugin): """Close PS after publish. For Webpublishing only. """ diff --git a/openpype/lib/remote_publish.py b/openpype/lib/remote_publish.py index aa8d8821a8..6cca9e4217 100644 --- a/openpype/lib/remote_publish.py +++ b/openpype/lib/remote_publish.py @@ -4,6 +4,7 @@ import sys from bson.objectid import ObjectId import pyblish.util +import pyblish.api from openpype import uninstall from openpype.lib.mongo import OpenPypeMongoConnection @@ -34,17 +35,21 @@ def start_webpublish_log(dbcon, batch_id, user): }).inserted_id -def publish_and_log(dbcon, _id, log): +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) @@ -68,6 +73,9 @@ def publish_and_log(dbcon, _id, log): }} ) + if close_plugin: # close host app explicitly after error + context = pyblish.api.Context() + close_plugin(context).process() sys.exit(1) else: dbcon.update_one( @@ -89,4 +97,14 @@ def publish_and_log(dbcon, _id, log): "progress": 1, "log": os.linesep.join(log_lines) }} - ) \ No newline at end of file + ) + + +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.") From 6fe7517303a359fdf91ab54353769e69cca1677d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 14 Oct 2021 13:21:34 +0200 Subject: [PATCH 038/161] PYPE-1901 - fix close plugin --- openpype/lib/remote_publish.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/remote_publish.py b/openpype/lib/remote_publish.py index 6cca9e4217..4946e1bd53 100644 --- a/openpype/lib/remote_publish.py +++ b/openpype/lib/remote_publish.py @@ -75,7 +75,7 @@ def publish_and_log(dbcon, _id, log, close_plugin_name=None): ) if close_plugin: # close host app explicitly after error context = pyblish.api.Context() - close_plugin(context).process() + close_plugin().process(context) sys.exit(1) else: dbcon.update_one( From b0628953d8039a65ffcc366a28f5cbd5c564e10a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 14 Oct 2021 19:18:59 +0200 Subject: [PATCH 039/161] PYPE-1901 - reworked Settings Layer could be resolved to final family according to color_code OR name (or both). --- openpype/hooks/pre_foundry_apps.py | 2 +- .../publish/collect_remote_instances.py | 72 +++++++++++++++---- .../defaults/project_settings/photoshop.json | 11 ++- .../schema_project_photoshop.json | 49 +++++++++---- 4 files changed, 102 insertions(+), 32 deletions(-) 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/photoshop/plugins/publish/collect_remote_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py index 62d94483e5..9bb8e90350 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py @@ -1,5 +1,6 @@ import pyblish.api import os +import re from avalon import photoshop from openpype.lib import prepare_template_data @@ -22,12 +23,11 @@ class CollectRemoteInstances(pyblish.api.ContextPlugin): hosts = ["photoshop"] # configurable by Settings - families = ["background"] - color_code = ["red"] - subset_template_name = "" + 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 @@ -49,24 +49,26 @@ class CollectRemoteInstances(pyblish.api.ContextPlugin): instance_names = [] for layer in layers: - self.log.info("!!!Layer:: {}".format(layer)) - if layer.color_code not in self.color_code: - self.log.debug("Not marked, skip") + 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") + self.log.debug("!!! Not a top layer, skip") continue instance = context.create_instance(layer.name) instance.append(layer) - instance.data["family"] = self.families[0] + instance.data["family"] = resolved_family instance.data["publish"] = layer.visible - - # populate data from context, coming from outside?? TODO - # TEMP - self.log.info("asset {}".format(context.data["assetEntity"])) - self.log.info("taskType {}".format(context.data["taskType"])) instance.data["asset"] = context.data["assetEntity"]["name"] instance.data["task"] = context.data["taskType"] @@ -76,7 +78,7 @@ class CollectRemoteInstances(pyblish.api.ContextPlugin): "task": instance.data["task"], "layer": layer.name } - subset = self.subset_template_name.format( + subset = resolved_subset_template.format( **prepare_template_data(fill_pairs)) instance.data["subset"] = subset @@ -90,3 +92,45 @@ class CollectRemoteInstances(pyblish.api.ContextPlugin): 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"]: + break + + if mapping["layer_name_regex"] and \ + not any(re.search(pattern, layer.name) + for pattern in mapping["layer_name_regex"]): + break + + 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/settings/defaults/project_settings/photoshop.json b/openpype/settings/defaults/project_settings/photoshop.json index 14c294c0c5..03fcbc162c 100644 --- a/openpype/settings/defaults/project_settings/photoshop.json +++ b/openpype/settings/defaults/project_settings/photoshop.json @@ -13,9 +13,14 @@ "active": true }, "CollectRemoteInstances": { - "color_code": [], - "families": [], - "subset_template_name": "" + "color_code_mapping": [ + { + "color_code": [], + "layer_name_regex": [], + "family": "", + "subset_template_name": "" + } + ] }, "ExtractImage": { "formats": [ 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 008f1a265d..cd457ee21d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json @@ -46,6 +46,7 @@ { "type": "dict", "collapsible": true, + "is_group": true, "key": "CollectRemoteInstances", "label": "Collect Instances for Webpublish", "children": [ @@ -55,20 +56,40 @@ }, { "type": "list", - "key": "color_code", - "label": "Color codes for layers", - "object_type": "text" - }, - { - "key": "families", - "label": "Families", - "type": "list", - "object_type": "text" - }, - { - "type": "text", - "key": "subset_template_name", - "label": "Subset template name" + "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": "text" + }, + { + "type": "text", + "key": "subset_template_name", + "label": "Subset template name" + } + ] + } } ] }, From 6621b2c36e6d897b35421a7005857cbf51df053f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 15 Oct 2021 15:00:44 +0200 Subject: [PATCH 040/161] OP-1063 - added validator for source files for Standalone Publisher --- .../plugins/publish/validate_sources.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 openpype/hosts/standalonepublisher/plugins/publish/validate_sources.py 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..da424cfb45 --- /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 repr in instance.data["representations"]: + files = [] + if isinstance(repr["files"], str): + files.append(repr["files"]) + else: + files = list(repr["files"]) + + for file_name in files: + source_file = os.path.join(repr["stagingDir"], + file_name) + + if not os.path.exists(source_file): + raise ValueError("File {} not found".format(source_file)) From 9eaa0a5a380550203e55aaed36f9ed96ecf2946e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 18:58:37 +0200 Subject: [PATCH 041/161] pass parenting of widget properly in main window --- openpype/tools/loader/app.py | 96 ++++++++++++++++++++---------------- 1 file changed, 54 insertions(+), 42 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index c18b6e798a..e7a7d2c7ad 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -54,61 +54,72 @@ class LoaderWindow(QtWidgets.QDialog): self.setWindowFlags(QtCore.Qt.Window) self.setFocusPolicy(QtCore.Qt.StrongFocus) - body = QtWidgets.QWidget() - footer = QtWidgets.QWidget() - footer.setFixedHeight(20) + split = QtWidgets.QSplitter(self) - container = QtWidgets.QWidget() + asset_filter_splitter = QtWidgets.QSplitter(split) + asset_filter_splitter.setOrientation(QtCore.Qt.Vertical) - assets = AssetWidget(io, multiselection=True, parent=self) + # --- Left part --- + # Assets widget + assets = AssetWidget( + io, multiselection=True, parent=asset_filter_splitter + ) assets.set_current_asset_btn_visibility(True) - families = FamilyListView(io, self.family_config_cache, self) - subsets = SubsetWidget( - io, - self.groups_config, - self.family_config_cache, - tool_name=self.tool_name, - parent=self + # Families widget + families = FamilyListView( + io, self.family_config_cache, asset_filter_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() - 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) - 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) - container_layout = QtWidgets.QHBoxLayout(container) - container_layout.setContentsMargins(0, 0, 0, 0) - split = QtWidgets.QSplitter() + # --- Middle part --- + # Subsets widget + subsets = SubsetWidget( + io, + self.groups_config, + self.family_config_cache, + tool_name=self.tool_name, + parent=split + ) + + # --- Right part --- + thumb_ver_splitter = QtWidgets.QSplitter(split) + thumb_ver_splitter.setOrientation(QtCore.Qt.Vertical) + version = VersionWidget(io, parent=thumb_ver_splitter) + thumbnail = ThumbnailWidget(io, parent=thumb_ver_splitter) + + 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 + + thumb_ver_splitter.addWidget(thumbnail) + thumb_ver_splitter.addWidget(version) + + representations = None + if sync_server_enabled: + representations = RepresentationWidget( + io, self.tool_name, parent=thumb_ver_splitter + ) + thumb_ver_splitter.addWidget(representations) + + thumb_ver_splitter.setStretchFactor(0, 30) + thumb_ver_splitter.setStretchFactor(1, 35) + split.addWidget(asset_filter_splitter) split.addWidget(subsets) split.addWidget(thumb_ver_splitter) - container_layout.addWidget(split) + # TODO keep footer size by message size + footer = QtWidgets.QWidget(self) + footer.setFixedHeight(20) - body_layout = QtWidgets.QHBoxLayout(body) - body_layout.addWidget(container) - body_layout.setContentsMargins(0, 0, 0, 0) - - message = QtWidgets.QLabel() + # TODO Don't hide messsage just set label to empty string + message = QtWidgets.QLabel(footer) message.hide() footer_layout = QtWidgets.QVBoxLayout(footer) @@ -116,7 +127,7 @@ class LoaderWindow(QtWidgets.QDialog): footer_layout.setContentsMargins(0, 0, 0, 0) layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(body) + layout.addWidget(split) layout.addWidget(footer) self.data = { @@ -152,6 +163,7 @@ class LoaderWindow(QtWidgets.QDialog): representations.load_started.connect(self._on_load_start) representations.load_ended.connect(self._on_load_end) + # TODO add overlay using stack widget self._overlay_frame = overlay_frame self.family_config_cache.refresh() @@ -161,7 +173,7 @@ class LoaderWindow(QtWidgets.QDialog): self._assetschanged() # Defaults - if sync_server.enabled: + if sync_server_enabled: split.setSizes([250, 1000, 550]) self.resize(1800, 900) else: From 514e49ff239927128b298e054af8ed58328e60b3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 19:07:28 +0200 Subject: [PATCH 042/161] simplified message showing without scheduler --- openpype/tools/loader/app.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index e7a7d2c7ad..a7f66bdccb 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -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) @@ -118,12 +119,10 @@ class LoaderWindow(QtWidgets.QDialog): footer = QtWidgets.QWidget(self) footer.setFixedHeight(20) - # TODO Don't hide messsage just set label to empty string - message = QtWidgets.QLabel(footer) - message.hide() + message_label = QtWidgets.QLabel(footer) footer_layout = QtWidgets.QVBoxLayout(footer) - footer_layout.addWidget(message) + footer_layout.addWidget(message_label) footer_layout.setContentsMargins(0, 0, 0, 0) layout = QtWidgets.QVBoxLayout(self) @@ -139,9 +138,6 @@ class LoaderWindow(QtWidgets.QDialog): "thumbnail": thumbnail, "representations": representations }, - "label": { - "message": message, - }, "state": { "assetIds": None } @@ -150,6 +146,12 @@ class LoaderWindow(QtWidgets.QDialog): overlay_frame = OverlayFrame("Loading...", self) overlay_frame.setVisible(False) + message_timer = QtCore.QTimer() + message_timer.setInterval(self.message_timeout) + message_timer.setSingleShot(True) + + message_timer.timeout.connect(self._on_message_timeout) + families.active_changed.connect(subsets.set_family_filters) assets.selection_changed.connect(self.on_assetschanged) assets.refresh_triggered.connect(self.on_assetschanged) @@ -164,7 +166,10 @@ class LoaderWindow(QtWidgets.QDialog): representations.load_ended.connect(self._on_load_end) # TODO add overlay using stack widget + self._message_label = message_label + self._overlay_frame = overlay_frame + self._message_timer = message_timer self.family_config_cache.refresh() self.groups_config.refresh() @@ -450,13 +455,13 @@ class LoaderWindow(QtWidgets.QDialog): asset_widget = self.data["widgets"]["assets"] asset_widget.select_assets(asset) - def echo(self, message): - widget = self.data["label"]["message"] - widget.setText(str(message)) - widget.show() - print(message) + def _on_message_timeout(self): + self._message_label.setText("") - lib.schedule(widget.hide, 5000, channel="message") + def echo(self, message): + self._message_label.setText(str(message)) + print(message) + self._message_timer.start() def closeEvent(self, event): # Kill on holding SHIFT From 893ae73437d11dbce05428a9cdf6afa7507a706c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 19:13:51 +0200 Subject: [PATCH 043/161] fix repres widget --- openpype/tools/loader/app.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index a7f66bdccb..1b316c9223 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -101,12 +101,12 @@ class LoaderWindow(QtWidgets.QDialog): thumb_ver_splitter.addWidget(thumbnail) thumb_ver_splitter.addWidget(version) - representations = None + repres_widget = None if sync_server_enabled: - representations = RepresentationWidget( + repres_widget = RepresentationWidget( io, self.tool_name, parent=thumb_ver_splitter ) - thumb_ver_splitter.addWidget(representations) + thumb_ver_splitter.addWidget(repres_widget) thumb_ver_splitter.setStretchFactor(0, 30) thumb_ver_splitter.setStretchFactor(1, 35) @@ -136,7 +136,6 @@ class LoaderWindow(QtWidgets.QDialog): "subsets": subsets, "version": version, "thumbnail": thumbnail, - "representations": representations }, "state": { "assetIds": None @@ -162,12 +161,14 @@ class LoaderWindow(QtWidgets.QDialog): 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) + repres_widget.load_started.connect(self._on_load_start) + repres_widget.load_ended.connect(self._on_load_end) # TODO add overlay using stack widget self._message_label = message_label + self._repres_widget = repres_widget + self._overlay_frame = overlay_frame self._message_timer = message_timer @@ -321,9 +322,9 @@ class LoaderWindow(QtWidgets.QDialog): 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"] @@ -414,12 +415,14 @@ class LoaderWindow(QtWidgets.QDialog): self.data["widgets"]["thumbnail"].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. From 6d49ebf3a85ef169bedf764e59b5915f10436ac4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 19:16:16 +0200 Subject: [PATCH 044/161] change split to main_splitter --- openpype/tools/loader/app.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 1b316c9223..ea28304134 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -55,9 +55,9 @@ class LoaderWindow(QtWidgets.QDialog): self.setWindowFlags(QtCore.Qt.Window) self.setFocusPolicy(QtCore.Qt.StrongFocus) - split = QtWidgets.QSplitter(self) + main_splitter = QtWidgets.QSplitter(self) - asset_filter_splitter = QtWidgets.QSplitter(split) + asset_filter_splitter = QtWidgets.QSplitter(main_splitter) asset_filter_splitter.setOrientation(QtCore.Qt.Vertical) # --- Left part --- @@ -83,11 +83,11 @@ class LoaderWindow(QtWidgets.QDialog): self.groups_config, self.family_config_cache, tool_name=self.tool_name, - parent=split + parent=main_splitter ) # --- Right part --- - thumb_ver_splitter = QtWidgets.QSplitter(split) + thumb_ver_splitter = QtWidgets.QSplitter(main_splitter) thumb_ver_splitter.setOrientation(QtCore.Qt.Vertical) version = VersionWidget(io, parent=thumb_ver_splitter) thumbnail = ThumbnailWidget(io, parent=thumb_ver_splitter) @@ -111,9 +111,9 @@ class LoaderWindow(QtWidgets.QDialog): thumb_ver_splitter.setStretchFactor(0, 30) thumb_ver_splitter.setStretchFactor(1, 35) - split.addWidget(asset_filter_splitter) - split.addWidget(subsets) - split.addWidget(thumb_ver_splitter) + main_splitter.addWidget(asset_filter_splitter) + main_splitter.addWidget(subsets) + main_splitter.addWidget(thumb_ver_splitter) # TODO keep footer size by message size footer = QtWidgets.QWidget(self) @@ -126,7 +126,7 @@ class LoaderWindow(QtWidgets.QDialog): footer_layout.setContentsMargins(0, 0, 0, 0) layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(split) + layout.addWidget(main_splitter) layout.addWidget(footer) self.data = { @@ -180,10 +180,10 @@ class LoaderWindow(QtWidgets.QDialog): # Defaults if sync_server_enabled: - split.setSizes([250, 1000, 550]) + main_splitter.setSizes([250, 1000, 550]) self.resize(1800, 900) else: - split.setSizes([250, 850, 200]) + main_splitter.setSizes([250, 850, 200]) self.resize(1300, 700) def resizeEvent(self, event): From 98a6225e5ffbba710d829b95ee2dd3cb2ca0c1e5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 19:17:04 +0200 Subject: [PATCH 045/161] renamed 'footer' to 'footer_widget' --- openpype/tools/loader/app.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index ea28304134..9569c87e35 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -116,18 +116,18 @@ class LoaderWindow(QtWidgets.QDialog): main_splitter.addWidget(thumb_ver_splitter) # TODO keep footer size by message size - footer = QtWidgets.QWidget(self) - footer.setFixedHeight(20) + footer_widget = QtWidgets.QWidget(self) + footer_widget.setFixedHeight(20) - message_label = QtWidgets.QLabel(footer) + message_label = QtWidgets.QLabel(footer_widget) - footer_layout = QtWidgets.QVBoxLayout(footer) - footer_layout.addWidget(message_label) + footer_layout = QtWidgets.QVBoxLayout(footer_widget) footer_layout.setContentsMargins(0, 0, 0, 0) + footer_layout.addWidget(message_label) layout = QtWidgets.QVBoxLayout(self) layout.addWidget(main_splitter) - layout.addWidget(footer) + layout.addWidget(footer_widget) self.data = { "widgets": { From e144e3d9fd8b1cad9ac6c18ae4faf3169c0a9d58 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 18 Oct 2021 10:03:47 +0200 Subject: [PATCH 046/161] renamed version info widget and do not store it into data --- openpype/tools/loader/app.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 9569c87e35..0ec5a50f31 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -89,7 +89,8 @@ class LoaderWindow(QtWidgets.QDialog): # --- Right part --- thumb_ver_splitter = QtWidgets.QSplitter(main_splitter) thumb_ver_splitter.setOrientation(QtCore.Qt.Vertical) - version = VersionWidget(io, parent=thumb_ver_splitter) + + version_info_widget = VersionWidget(io, parent=thumb_ver_splitter) thumbnail = ThumbnailWidget(io, parent=thumb_ver_splitter) manager = ModulesManager() @@ -99,7 +100,7 @@ class LoaderWindow(QtWidgets.QDialog): sync_server_enabled = sync_server.enabled thumb_ver_splitter.addWidget(thumbnail) - thumb_ver_splitter.addWidget(version) + thumb_ver_splitter.addWidget(version_info_widget) repres_widget = None if sync_server_enabled: @@ -134,8 +135,7 @@ class LoaderWindow(QtWidgets.QDialog): "families": families, "assets": assets, "subsets": subsets, - "version": version, - "thumbnail": thumbnail, + "thumbnail": thumbnail }, "state": { "assetIds": None @@ -167,6 +167,7 @@ class LoaderWindow(QtWidgets.QDialog): # TODO add overlay using stack widget self._message_label = message_label + self._version_info_widget = version_info_widget self._repres_widget = repres_widget self._overlay_frame = overlay_frame @@ -317,7 +318,7 @@ class LoaderWindow(QtWidgets.QDialog): ) # Clear the version information on asset change - self.data["widgets"]["version"].set_version(None) + self._version_info_widget.set_version(None) self.data["widgets"]["thumbnail"].set_thumbnail(asset_docs) self.data["state"]["assetIds"] = asset_ids @@ -404,7 +405,7 @@ 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"] From c90cf1765c04719c306ef8894eb4f545a8cb7d32 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 18 Oct 2021 10:06:05 +0200 Subject: [PATCH 047/161] renamed thumbnail widget and do not store it into data --- openpype/tools/loader/app.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 0ec5a50f31..43e3750428 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -90,8 +90,8 @@ class LoaderWindow(QtWidgets.QDialog): thumb_ver_splitter = QtWidgets.QSplitter(main_splitter) thumb_ver_splitter.setOrientation(QtCore.Qt.Vertical) + thumbnail_widget = ThumbnailWidget(io, parent=thumb_ver_splitter) version_info_widget = VersionWidget(io, parent=thumb_ver_splitter) - thumbnail = ThumbnailWidget(io, parent=thumb_ver_splitter) manager = ModulesManager() sync_server = manager.modules_by_name.get("sync_server") @@ -99,7 +99,7 @@ class LoaderWindow(QtWidgets.QDialog): if sync_server is not None: sync_server_enabled = sync_server.enabled - thumb_ver_splitter.addWidget(thumbnail) + thumb_ver_splitter.addWidget(thumbnail_widget) thumb_ver_splitter.addWidget(version_info_widget) repres_widget = None @@ -134,8 +134,7 @@ class LoaderWindow(QtWidgets.QDialog): "widgets": { "families": families, "assets": assets, - "subsets": subsets, - "thumbnail": thumbnail + "subsets": subsets }, "state": { "assetIds": None @@ -168,6 +167,7 @@ class LoaderWindow(QtWidgets.QDialog): self._message_label = message_label self._version_info_widget = version_info_widget + self._thumbnail_widget = thumbnail_widget self._repres_widget = repres_widget self._overlay_frame = overlay_frame @@ -318,8 +318,8 @@ class LoaderWindow(QtWidgets.QDialog): ) # Clear the version information on asset change + self._thumbnail_widget.set_thumbnail(asset_docs) self._version_info_widget.set_version(None) - self.data["widgets"]["thumbnail"].set_thumbnail(asset_docs) self.data["state"]["assetIds"] = asset_ids @@ -414,7 +414,7 @@ class LoaderWindow(QtWidgets.QDialog): if len(asset_docs) > 0: thumbnail_docs = asset_docs - self.data["widgets"]["thumbnail"].set_thumbnail(thumbnail_docs) + self._thumbnail_widget.set_thumbnail(thumbnail_docs) if self._repres_widget is not None: version_ids = [doc["_id"] for doc in version_docs or []] From a85c93cc0fa599441e8eee0e3276029469be3a31 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 18 Oct 2021 10:12:18 +0200 Subject: [PATCH 048/161] rename families filter view and do not store it into data --- openpype/tools/loader/app.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 43e3750428..f10348eced 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -68,11 +68,11 @@ class LoaderWindow(QtWidgets.QDialog): assets.set_current_asset_btn_visibility(True) # Families widget - families = FamilyListView( + families_filter_view = FamilyListView( io, self.family_config_cache, asset_filter_splitter ) asset_filter_splitter.addWidget(assets) - asset_filter_splitter.addWidget(families) + asset_filter_splitter.addWidget(families_filter_view) asset_filter_splitter.setStretchFactor(0, 65) asset_filter_splitter.setStretchFactor(1, 35) @@ -132,7 +132,6 @@ class LoaderWindow(QtWidgets.QDialog): self.data = { "widgets": { - "families": families, "assets": assets, "subsets": subsets }, @@ -150,7 +149,7 @@ class LoaderWindow(QtWidgets.QDialog): message_timer.timeout.connect(self._on_message_timeout) - families.active_changed.connect(subsets.set_family_filters) + families_filter_view.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) @@ -163,15 +162,17 @@ class LoaderWindow(QtWidgets.QDialog): repres_widget.load_started.connect(self._on_load_start) repres_widget.load_ended.connect(self._on_load_end) - # TODO add overlay using stack widget self._message_label = message_label + self._message_timer = message_timer + + self._families_filter_view = families_filter_view self._version_info_widget = version_info_widget self._thumbnail_widget = thumbnail_widget self._repres_widget = repres_widget + # TODO add overlay using stack widget self._overlay_frame = overlay_frame - self._message_timer = message_timer self.family_config_cache.refresh() self.groups_config.refresh() @@ -236,11 +237,10 @@ class LoaderWindow(QtWidgets.QDialog): 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._families_filter_view.set_enabled_families(families) def _on_load_end(self): # Delay hiding as click events happened during loading should be @@ -251,9 +251,8 @@ class LoaderWindow(QtWidgets.QDialog): 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"]) @@ -268,8 +267,7 @@ class LoaderWindow(QtWidgets.QDialog): assets_widget.refresh() 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 From 6947c68bfe63014ab17a3e622e170ef27a949d81 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 18 Oct 2021 10:13:27 +0200 Subject: [PATCH 049/161] renamed 'asset_filter_splitter' to 'left_side_splitter' --- openpype/tools/loader/app.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index f10348eced..f81f54cc23 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -57,24 +57,24 @@ class LoaderWindow(QtWidgets.QDialog): main_splitter = QtWidgets.QSplitter(self) - asset_filter_splitter = QtWidgets.QSplitter(main_splitter) - asset_filter_splitter.setOrientation(QtCore.Qt.Vertical) - # --- Left part --- + left_side_splitter = QtWidgets.QSplitter(main_splitter) + left_side_splitter.setOrientation(QtCore.Qt.Vertical) + # Assets widget assets = AssetWidget( - io, multiselection=True, parent=asset_filter_splitter + io, multiselection=True, parent=left_side_splitter ) assets.set_current_asset_btn_visibility(True) # Families widget families_filter_view = FamilyListView( - io, self.family_config_cache, asset_filter_splitter + io, self.family_config_cache, left_side_splitter ) - asset_filter_splitter.addWidget(assets) - asset_filter_splitter.addWidget(families_filter_view) - asset_filter_splitter.setStretchFactor(0, 65) - asset_filter_splitter.setStretchFactor(1, 35) + left_side_splitter.addWidget(assets) + left_side_splitter.addWidget(families_filter_view) + left_side_splitter.setStretchFactor(0, 65) + left_side_splitter.setStretchFactor(1, 35) # --- Middle part --- # Subsets widget @@ -112,7 +112,7 @@ class LoaderWindow(QtWidgets.QDialog): thumb_ver_splitter.setStretchFactor(0, 30) thumb_ver_splitter.setStretchFactor(1, 35) - main_splitter.addWidget(asset_filter_splitter) + main_splitter.addWidget(left_side_splitter) main_splitter.addWidget(subsets) main_splitter.addWidget(thumb_ver_splitter) From 658c4a1b6f48fdb0cc96373032815bcd559432e5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 18 Oct 2021 10:21:44 +0200 Subject: [PATCH 050/161] rename asset widget and do not store it to data --- openpype/tools/loader/app.py | 45 +++++++++++++++++------------------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index f81f54cc23..3f3f03867f 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -62,16 +62,16 @@ class LoaderWindow(QtWidgets.QDialog): left_side_splitter.setOrientation(QtCore.Qt.Vertical) # Assets widget - assets = AssetWidget( + assets_widget = AssetWidget( io, multiselection=True, parent=left_side_splitter ) - assets.set_current_asset_btn_visibility(True) + assets_widget.set_current_asset_btn_visibility(True) # Families widget families_filter_view = FamilyListView( io, self.family_config_cache, left_side_splitter ) - left_side_splitter.addWidget(assets) + 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) @@ -132,7 +132,6 @@ class LoaderWindow(QtWidgets.QDialog): self.data = { "widgets": { - "assets": assets, "subsets": subsets }, "state": { @@ -150,9 +149,9 @@ class LoaderWindow(QtWidgets.QDialog): message_timer.timeout.connect(self._on_message_timeout) families_filter_view.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) + 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.active_changed.connect(self.on_subsetschanged) subsets.version_changed.connect(self.on_versionschanged) subsets.refreshed.connect(self._on_subset_refresh) @@ -162,15 +161,16 @@ class LoaderWindow(QtWidgets.QDialog): repres_widget.load_started.connect(self._on_load_start) repres_widget.load_ended.connect(self._on_load_end) - self._message_label = message_label - self._message_timer = message_timer - + self._assets_widget = assets_widget self._families_filter_view = families_filter_view 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 @@ -250,11 +250,10 @@ class LoaderWindow(QtWidgets.QDialog): # ------------------------------ def on_context_task_change(self, *args, **kwargs): - assets_widget = self.data["widgets"]["assets"] # Refresh families config 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""" @@ -263,9 +262,8 @@ 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() self._families_filter_view.refresh() @@ -275,11 +273,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): @@ -292,7 +291,6 @@ class LoaderWindow(QtWidgets.QDialog): def _assetschanged(self): """Selected assets have changed""" - assets_widget = self.data["widgets"]["assets"] subsets_widget = self.data["widgets"]["subsets"] subsets_model = subsets_widget.model @@ -300,7 +298,7 @@ class LoaderWindow(QtWidgets.QDialog): 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 @@ -354,7 +352,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: @@ -406,8 +405,7 @@ class LoaderWindow(QtWidgets.QDialog): 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 @@ -454,8 +452,7 @@ 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("") From 584fd49e7b02f5bc74c7173c9b1cce74a0f0765a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 18 Oct 2021 10:34:13 +0200 Subject: [PATCH 051/161] renamed subset widget and do not store it to data --- openpype/tools/loader/app.py | 46 ++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 3f3f03867f..a4b4b5eb28 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -78,7 +78,7 @@ class LoaderWindow(QtWidgets.QDialog): # --- Middle part --- # Subsets widget - subsets = SubsetWidget( + subsets_widget = SubsetWidget( io, self.groups_config, self.family_config_cache, @@ -113,7 +113,7 @@ class LoaderWindow(QtWidgets.QDialog): thumb_ver_splitter.setStretchFactor(1, 35) main_splitter.addWidget(left_side_splitter) - main_splitter.addWidget(subsets) + main_splitter.addWidget(subsets_widget) main_splitter.addWidget(thumb_ver_splitter) # TODO keep footer size by message size @@ -148,22 +148,27 @@ class LoaderWindow(QtWidgets.QDialog): message_timer.timeout.connect(self._on_message_timeout) - families_filter_view.active_changed.connect(subsets.set_family_filters) + 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.active_changed.connect(self.on_subsetschanged) - subsets.version_changed.connect(self.on_versionschanged) - subsets.refreshed.connect(self._on_subset_refresh) + 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.load_started.connect(self._on_load_start) - subsets.load_ended.connect(self._on_load_end) + subsets_widget.load_started.connect(self._on_load_start) + subsets_widget.load_ended.connect(self._on_load_end) repres_widget.load_started.connect(self._on_load_start) repres_widget.load_ended.connect(self._on_load_end) 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 @@ -236,10 +241,10 @@ class LoaderWindow(QtWidgets.QDialog): self._overlay_frame.setVisible(False) def _on_subset_refresh(self, has_item): - subsets_widget = self.data["widgets"]["subsets"] - - subsets_widget.set_loading_state(loading=False, empty=not has_item) - families = subsets_widget.get_subsets_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): @@ -248,6 +253,8 @@ 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): # Refresh families config @@ -291,7 +298,8 @@ class LoaderWindow(QtWidgets.QDialog): def _assetschanged(self): """Selected assets have changed""" - 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() @@ -330,8 +338,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 = [] @@ -370,7 +379,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 @@ -488,7 +497,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 @@ -527,7 +536,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() From 3fd25b14a770107d3282bf6116a0d72f37fbd163 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Oct 2021 11:26:25 +0200 Subject: [PATCH 052/161] OP-1206 - added reference loader wip --- .../photoshop/plugins/load/load_image.py | 5 ++++- .../photoshop/plugins/load/load_reference.py | 22 +++++++++++++++++++ .../collect_default_deadline_server.py | 2 +- .../defaults/project_anatomy/imageio.json | 4 +--- 4 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 openpype/hosts/photoshop/plugins/load/load_reference.py diff --git a/openpype/hosts/photoshop/plugins/load/load_image.py b/openpype/hosts/photoshop/plugins/load/load_image.py index d043323768..d97894b269 100644 --- a/openpype/hosts/photoshop/plugins/load/load_image.py +++ b/openpype/hosts/photoshop/plugins/load/load_image.py @@ -21,7 +21,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 @@ -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..306647c032 --- /dev/null +++ b/openpype/hosts/photoshop/plugins/load/load_reference.py @@ -0,0 +1,22 @@ +import re + +from avalon import api, photoshop + +from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name +from openpype.hosts.photoshop.plugins.load.load_image import ImageLoader + +stub = photoshop.stub() + + +class ReferenceLoader(ImageLoader): + """Load reference images + + Stores the imported asset in a container named after the asset. + """ + + families = ["image", "render"] + representations = ["*"] + + def import_layer(self, file_name, layer_name): + return stub.import_smart_object(file_name, layer_name, + as_reference=True) diff --git a/openpype/modules/default_modules/deadline/plugins/publish/collect_default_deadline_server.py b/openpype/modules/default_modules/deadline/plugins/publish/collect_default_deadline_server.py index afb8583069..53231bd7e4 100644 --- a/openpype/modules/default_modules/deadline/plugins/publish/collect_default_deadline_server.py +++ b/openpype/modules/default_modules/deadline/plugins/publish/collect_default_deadline_server.py @@ -6,7 +6,7 @@ import pyblish.api class CollectDefaultDeadlineServer(pyblish.api.ContextPlugin): """Collect default Deadline Webservice URL.""" - order = pyblish.api.CollectorOrder + order = pyblish.api.CollectorOrder + 0.01 label = "Default Deadline Webservice" def process(self, context): 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": [ From 86cd93c0acae803db01a48737924cfa292d038a5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 18 Oct 2021 16:10:16 +0200 Subject: [PATCH 053/161] fix lookassigner imports for python3 compatibility --- openpype/tools/mayalookassigner/app.py | 18 ++++++++++++------ openpype/tools/mayalookassigner/commands.py | 4 ++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/openpype/tools/mayalookassigner/app.py b/openpype/tools/mayalookassigner/app.py index 1fa3a3868a..83c779fde1 100644 --- a/openpype/tools/mayalookassigner/app.py +++ b/openpype/tools/mayalookassigner/app.py @@ -13,8 +13,14 @@ from maya import cmds import maya.OpenMaya import maya.api.OpenMaya as om -from . import widgets -from . import commands +from .widgets import ( + AssetOutliner, + LookOutliner +) +from .commands import ( + get_workfile, + remove_unused_looks +) from . vray_proxies import vrayproxy_assign_look @@ -32,7 +38,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 +63,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 +130,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] From 0dea134364b43c30267d8c51e78582c4e099bc11 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 18 Oct 2021 16:51:05 +0200 Subject: [PATCH 054/161] fix more imports --- openpype/tools/mayalookassigner/app.py | 5 +++-- openpype/tools/mayalookassigner/models.py | 5 +++-- openpype/tools/mayalookassigner/views.py | 2 +- openpype/tools/mayalookassigner/widgets.py | 13 +++++++------ 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/openpype/tools/mayalookassigner/app.py b/openpype/tools/mayalookassigner/app.py index 83c779fde1..0f5a930902 100644 --- a/openpype/tools/mayalookassigner/app.py +++ b/openpype/tools/mayalookassigner/app.py @@ -2,11 +2,12 @@ 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 @@ -21,7 +22,7 @@ from .commands import ( get_workfile, remove_unused_looks ) -from . vray_proxies import vrayproxy_assign_look +from .vray_proxies import vrayproxy_assign_look module = sys.modules[__name__] 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) - - From 62977a9b4f184fc3316a0073e0e317f3967b1632 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 19 Oct 2021 12:05:40 +0200 Subject: [PATCH 055/161] OP-1206 - added reference loader wip --- .../photoshop/plugins/load/load_image.py | 6 +- .../photoshop/plugins/load/load_reference.py | 65 ++++++++++++++++++- 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/load/load_image.py b/openpype/hosts/photoshop/plugins/load/load_image.py index d97894b269..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 @@ -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"] diff --git a/openpype/hosts/photoshop/plugins/load/load_reference.py b/openpype/hosts/photoshop/plugins/load/load_reference.py index 306647c032..1b54bd97f1 100644 --- a/openpype/hosts/photoshop/plugins/load/load_reference.py +++ b/openpype/hosts/photoshop/plugins/load/load_reference.py @@ -8,15 +8,76 @@ from openpype.hosts.photoshop.plugins.load.load_image import ImageLoader stub = photoshop.stub() -class ReferenceLoader(ImageLoader): +class ReferenceLoader(api.Loader): """Load reference images - Stores the imported asset in a container named after the asset. + 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) From 0188febe4f17066249d34f18c020deafd869f64a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 19 Oct 2021 13:01:28 +0200 Subject: [PATCH 056/161] PYPE-1901 - added check that no other batch is currently processed PS cannot be run twice on a host machine --- openpype/lib/remote_publish.py | 49 +++++++++++++++++++++++++++++++ openpype/pype_commands.py | 53 ++++++++++++++++++++-------------- 2 files changed, 81 insertions(+), 21 deletions(-) diff --git a/openpype/lib/remote_publish.py b/openpype/lib/remote_publish.py index 4946e1bd53..51007cfad2 100644 --- a/openpype/lib/remote_publish.py +++ b/openpype/lib/remote_publish.py @@ -100,6 +100,55 @@ def publish_and_log(dbcon, _id, log, close_plugin_name=None): ) +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() diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index e2869a956d..3071629ee5 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -3,7 +3,6 @@ import os import sys import json -from datetime import datetime import time from openpype.lib import PypeLogger @@ -12,7 +11,9 @@ 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 + publish_and_log, + fail_batch, + find_variant_key ) @@ -125,10 +126,17 @@ class PypeCommands: 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 @@ -141,25 +149,12 @@ class PypeCommands: from openpype.lib import ApplicationManager application_manager = ApplicationManager() - 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)) + 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): - # TODO check if batch manifest is same as tasks manifests batch_data = parse_json(os.path.join(batch_dir, "manifest.json")) if not batch_data: @@ -174,6 +169,27 @@ class PypeCommands: batch_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, @@ -183,11 +199,6 @@ class PypeCommands: ) os.environ.update(env) - _, 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) - os.environ["OPENPYPE_PUBLISH_DATA"] = batch_dir os.environ["IS_HEADLESS"] = "true" # must pass identifier to update log lines for a batch From 4e000a6cb425a56abd14a9d406f13099e8da3898 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 19 Oct 2021 13:22:58 +0200 Subject: [PATCH 057/161] PYPE-1901 - added proper selection of host based on studio_processing --- .../webserver_service/webpublish_routes.py | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py index 0014d1b344..3f67038fbf 100644 --- a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py +++ b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py @@ -175,6 +175,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 +185,28 @@ 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") + command = "remotepublishfromapp" + for host, extensions in host_map.items(): + for ext in extensions: + for file_name in content.get("files", []): + if ext in file_name: + add_args["host"] = host + break + openpype_app = self.resource.executable args = [ openpype_app, - 'remotepublish', + command, batch_path ] @@ -193,12 +214,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) From 2d687e5e6a923c4f34fac241f074867f6d85580b Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 19 Oct 2021 15:18:41 +0200 Subject: [PATCH 058/161] fix maya hotbox --- openpype/hosts/maya/api/menu.py | 66 ++++++++++++++++----------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index ad225dcd28..18d3e1e896 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -19,10 +19,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(): @@ -43,6 +41,34 @@ def deferred(): command=lambda *args: mayalookassigner.show() ) + 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(): from openpype.tools import workfiles @@ -109,38 +135,12 @@ def deferred(): log.info("Attempting to install scripts menu ...") + # add_scripts_menu() add_build_workfiles_item() add_look_assigner_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(): @@ -161,7 +161,7 @@ def install(): return # Allow time for uninstallation to finish. - cmds.evalDeferred(deferred) + cmds.evalDeferred(deferred, lowestPriority=True) def popup(): From 8b1952ca4be2d4a8a477fc0361913f259c88f83a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 19 Oct 2021 15:22:38 +0200 Subject: [PATCH 059/161] Hound --- openpype/hosts/photoshop/plugins/load/load_reference.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/photoshop/plugins/load/load_reference.py b/openpype/hosts/photoshop/plugins/load/load_reference.py index 1b54bd97f1..0cb4e4a69f 100644 --- a/openpype/hosts/photoshop/plugins/load/load_reference.py +++ b/openpype/hosts/photoshop/plugins/load/load_reference.py @@ -3,7 +3,6 @@ import re from avalon import api, photoshop from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name -from openpype.hosts.photoshop.plugins.load.load_image import ImageLoader stub = photoshop.stub() From 71d79a2dc91f707bf806f864367de92e54e3cd87 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 19 Oct 2021 15:25:16 +0200 Subject: [PATCH 060/161] ignore save warnings exception in prepare project --- .../event_handlers_server/action_prepare_project.py | 9 +++++++-- .../ftrack/event_handlers_user/action_prepare_project.py | 8 +++++++- 2 files changed, 14 insertions(+), 3 deletions(-) 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: From 9f35dd7763322a3c488cd827f372bae51080d800 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 19 Oct 2021 17:31:23 +0200 Subject: [PATCH 061/161] added basic of experimental tool definitions --- openpype/tools/experimental_tools/__init__.py | 9 ++ .../tools/experimental_tools/tools_def.py | 82 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 openpype/tools/experimental_tools/__init__.py create mode 100644 openpype/tools/experimental_tools/tools_def.py diff --git a/openpype/tools/experimental_tools/__init__.py b/openpype/tools/experimental_tools/__init__.py new file mode 100644 index 0000000000..d61c560886 --- /dev/null +++ b/openpype/tools/experimental_tools/__init__.py @@ -0,0 +1,9 @@ +from .tools_def import ( + ExperimentalTools, + LOCAL_EXPERIMENTAL_KEY +) + +__all__ = ( + "ExperimentalTools", + "LOCAL_EXPERIMENTAL_KEY" +) diff --git a/openpype/tools/experimental_tools/tools_def.py b/openpype/tools/experimental_tools/tools_def.py new file mode 100644 index 0000000000..ec4815c741 --- /dev/null +++ b/openpype/tools/experimental_tools/tools_def.py @@ -0,0 +1,82 @@ +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. + + 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. + + """ + def __init__(self, parent=None, host_name=None, filter_hosts=None): + experimental_tools = [] + if filter_hosts is None: + filter_hosts = host_name is not None + + if filter_hosts and not host_name: + filter_hosts = False + + if filter_hosts: + experimental_tools = [ + tool + for tool in experimental_tools + if tool.is_available_for_host(host_name) + ] + + self.tools_by_identifier = { + tool.identifier: tool + for tool in experimental_tools + } + self.experimental_tools = experimental_tools + self._parent_widget = parent + + def refresh_availability(self): + 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) From e1e2f8e9ddf60fe8b1a48a21bce67e7e2d47a3d7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 19 Oct 2021 17:33:07 +0200 Subject: [PATCH 062/161] base of dialog for experimental tools --- openpype/tools/experimental_tools/dialog.py | 146 ++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 openpype/tools/experimental_tools/dialog.py diff --git a/openpype/tools/experimental_tools/dialog.py b/openpype/tools/experimental_tools/dialog.py new file mode 100644 index 0000000000..db868c572f --- /dev/null +++ b/openpype/tools/experimental_tools/dialog.py @@ -0,0 +1,146 @@ +from Qt import QtWidgets + +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(ExperimentalDialog, self).__init__(*args, **kwargs) + self._identifier = identifier + + self.clicked.connect(self._on_click) + + def _on_click(self): + self.triggered.emit(self._identifier) + + +class ExperimentalDialog(QtWidgets.QDialog): + refresh_interval = 3000 + + def __init__(self, parent=None): + super(ExperimentalDialog, self).__init__(parent) + self.setWindowTitle("OpenPype Experimental tools") + self.setWindowIcon(app_icon_path()) + + empty_label = QtWidgets.QLabel( + "There are no experimental tools available.", self + ) + content_widget = QtWidgets.QWidget(self) + content_widget.setVisible(False) + + content_layout = QtWidgets.QHBoxLayout(content_widget) + content_layout.setContentsMargins(0, 0, 0, 0) + + experimental_tools = ExperimentalTools() + buttons_by_tool_identifier = {} + + layout = QtWidgets.QHBoxLayout(self) + layout.addWidget(empty_label) + layout.addWidget(content_widget) + + refresh_timer = QtCore.QTimer() + refresh_timer.setInterval(self.refresh_interval) + refresh_timer.timeout.connect(self._on_refresh_timeout) + + self._empty_label = empty_label + self._content_widget = content_widget + self._content_layout = content_layout + + self._experimental_tools = experimental_tools + self._buttons_by_tool_identifier = buttons_by_tool_identifier + + self._is_refreshing = False + self._refresh_on_active = True + self._window_is_active = False + self._refresh_timer = refresh_timer + + def refresh(self): + if self._is_refreshing: + return + self._is_refreshing = True + + self._experimental_tools.refresh_availability() + + buttons_to_remove = set(self._buttons_by_tool_identifier.keys()) + for idx, tool in enumerate(self._experimental_tools.experimental_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) + 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" + )) + + 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._empty_label.setVisible(not self._buttons_by_tool_identifier) + + self._is_refreshing = False + + 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(LauncherWindow, 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() + + 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(LauncherWindow, 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() From 6a68cfd4c93f84cbd99fd1a250606debcc4e92f0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 19 Oct 2021 17:36:46 +0200 Subject: [PATCH 063/161] added Experimental tools category to local settings --- .../local_settings/experimental_widget.py | 63 +++++++++++++++++++ .../tools/settings/local_settings/window.py | 33 ++++++++++ 2 files changed, 96 insertions(+) create mode 100644 openpype/tools/settings/local_settings/experimental_widget.py 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..953f8f75a9 --- /dev/null +++ b/openpype/tools/settings/local_settings/experimental_widget.py @@ -0,0 +1,63 @@ +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() + checkboxes_by_identifier = {} + for tool in experimental_defs.experimental_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 From 90de4cddb582ed50eea8ac65698d3517f6dd058c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 19 Oct 2021 17:50:10 +0200 Subject: [PATCH 064/161] store tools under 'tools' variable --- openpype/tools/experimental_tools/tools_def.py | 2 +- openpype/tools/settings/local_settings/experimental_widget.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/tools/experimental_tools/tools_def.py b/openpype/tools/experimental_tools/tools_def.py index ec4815c741..353ec8e1d5 100644 --- a/openpype/tools/experimental_tools/tools_def.py +++ b/openpype/tools/experimental_tools/tools_def.py @@ -68,7 +68,7 @@ class ExperimentalTools: tool.identifier: tool for tool in experimental_tools } - self.experimental_tools = experimental_tools + self.tools = experimental_tools self._parent_widget = parent def refresh_availability(self): diff --git a/openpype/tools/settings/local_settings/experimental_widget.py b/openpype/tools/settings/local_settings/experimental_widget.py index 953f8f75a9..741c173415 100644 --- a/openpype/tools/settings/local_settings/experimental_widget.py +++ b/openpype/tools/settings/local_settings/experimental_widget.py @@ -28,7 +28,7 @@ class LocalExperimentalToolsWidgets(QtWidgets.QWidget): experimental_defs = ExperimentalTools() checkboxes_by_identifier = {} - for tool in experimental_defs.experimental_tools: + for tool in experimental_defs.tools: checkbox = QtWidgets.QCheckBox(self) label_widget = QtWidgets.QLabel(tool.label, self) checkbox.setToolTip(tool.tooltip) From 27e9f20c07cc3a9550e2fdae70d2b00560e6ede8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 19 Oct 2021 17:58:10 +0200 Subject: [PATCH 065/161] fix dialog file --- openpype/tools/experimental_tools/dialog.py | 25 +++++++++++---------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/openpype/tools/experimental_tools/dialog.py b/openpype/tools/experimental_tools/dialog.py index db868c572f..14f3e6e48b 100644 --- a/openpype/tools/experimental_tools/dialog.py +++ b/openpype/tools/experimental_tools/dialog.py @@ -1,4 +1,4 @@ -from Qt import QtWidgets +from Qt import QtWidgets, QtCore, QtGui from openpype.style import ( load_stylesheet, @@ -12,7 +12,7 @@ class ToolButton(QtWidgets.QPushButton): triggered = QtCore.Signal(str) def __init__(self, identifier, *args, **kwargs): - super(ExperimentalDialog, self).__init__(*args, **kwargs) + super(ToolButton, self).__init__(*args, **kwargs) self._identifier = identifier self.clicked.connect(self._on_click) @@ -27,23 +27,22 @@ class ExperimentalDialog(QtWidgets.QDialog): def __init__(self, parent=None): super(ExperimentalDialog, self).__init__(parent) self.setWindowTitle("OpenPype Experimental tools") - self.setWindowIcon(app_icon_path()) + icon = QtGui.QIcon(app_icon_path()) + self.setWindowIcon(icon) empty_label = QtWidgets.QLabel( "There are no experimental tools available.", self ) content_widget = QtWidgets.QWidget(self) - content_widget.setVisible(False) content_layout = QtWidgets.QHBoxLayout(content_widget) content_layout.setContentsMargins(0, 0, 0, 0) experimental_tools = ExperimentalTools() - buttons_by_tool_identifier = {} layout = QtWidgets.QHBoxLayout(self) layout.addWidget(empty_label) - layout.addWidget(content_widget) + layout.addWidget(content_widget, 1) refresh_timer = QtCore.QTimer() refresh_timer.setInterval(self.refresh_interval) @@ -54,7 +53,7 @@ class ExperimentalDialog(QtWidgets.QDialog): self._content_layout = content_layout self._experimental_tools = experimental_tools - self._buttons_by_tool_identifier = buttons_by_tool_identifier + self._buttons_by_tool_identifier = {} self._is_refreshing = False self._refresh_on_active = True @@ -69,7 +68,7 @@ class ExperimentalDialog(QtWidgets.QDialog): self._experimental_tools.refresh_availability() buttons_to_remove = set(self._buttons_by_tool_identifier.keys()) - for idx, tool in enumerate(self._experimental_tools.experimental_tools): + for idx, tool in enumerate(self._experimental_tools.tools): identifier = tool.identifier if identifier in buttons_to_remove: buttons_to_remove.remove(identifier) @@ -90,7 +89,7 @@ class ExperimentalDialog(QtWidgets.QDialog): elif is_new or button.isEnabled(): button.setToolTip(( - "You can enable this tool in local settings. + "You can enable this tool in local settings." "\n\nOpenPype Tray > Settings > Experimental Tools" )) @@ -101,7 +100,9 @@ class ExperimentalDialog(QtWidgets.QDialog): self._content_layout.takeAt(idx) button.deleteLater() - self._empty_label.setVisible(not self._buttons_by_tool_identifier) + self._empty_label.setVisible( + len(self._buttons_by_tool_identifier) == 0 + ) self._is_refreshing = False @@ -111,7 +112,7 @@ class ExperimentalDialog(QtWidgets.QDialog): tool.execute() def showEvent(self, event): - super(LauncherWindow, self).showEvent(event) + super(ExperimentalDialog, self).showEvent(event) if self._refresh_on_active: # Start/Restart timer @@ -129,7 +130,7 @@ class ExperimentalDialog(QtWidgets.QDialog): self._refresh_timer.start() self.refresh() - super(LauncherWindow, self).changeEvent(event) + super(ExperimentalDialog, self).changeEvent(event) def _on_refresh_timeout(self): # Stop timer if window is not visible From 5026d300bbcae2eaa87dccf3ad2a0655a3ae4274 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 19 Oct 2021 17:58:22 +0200 Subject: [PATCH 066/161] import ExperimentalDialog to init file --- openpype/tools/experimental_tools/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/tools/experimental_tools/__init__.py b/openpype/tools/experimental_tools/__init__.py index d61c560886..75e3210aab 100644 --- a/openpype/tools/experimental_tools/__init__.py +++ b/openpype/tools/experimental_tools/__init__.py @@ -3,7 +3,12 @@ from .tools_def import ( LOCAL_EXPERIMENTAL_KEY ) +from .dialog import ExperimentalDialog + + __all__ = ( "ExperimentalTools", - "LOCAL_EXPERIMENTAL_KEY" + "LOCAL_EXPERIMENTAL_KEY", + + "ExperimentalDialog" ) From 7d3f4d315ff71f71403dddfd3f95d1ecd57e69a3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 19 Oct 2021 18:20:26 +0200 Subject: [PATCH 067/161] empty dialog has Ok btn --- openpype/tools/experimental_tools/dialog.py | 65 ++++++++++++++++----- 1 file changed, 49 insertions(+), 16 deletions(-) diff --git a/openpype/tools/experimental_tools/dialog.py b/openpype/tools/experimental_tools/dialog.py index 14f3e6e48b..237052c055 100644 --- a/openpype/tools/experimental_tools/dialog.py +++ b/openpype/tools/experimental_tools/dialog.py @@ -30,41 +30,59 @@ class ExperimentalDialog(QtWidgets.QDialog): icon = QtGui.QIcon(app_icon_path()) self.setWindowIcon(icon) + empty_widget = QtWidgets.QWidget(self) + empty_label = QtWidgets.QLabel( - "There are no experimental tools available.", self + "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_widget = QtWidgets.QWidget(self) - content_layout = QtWidgets.QHBoxLayout(content_widget) + content_layout = QtWidgets.QVBoxLayout(content_widget) content_layout.setContentsMargins(0, 0, 0, 0) experimental_tools = ExperimentalTools() - layout = QtWidgets.QHBoxLayout(self) - layout.addWidget(empty_label) + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(empty_widget, 1) layout.addWidget(content_widget, 1) refresh_timer = QtCore.QTimer() refresh_timer.setInterval(self.refresh_interval) refresh_timer.timeout.connect(self._on_refresh_timeout) - self._empty_label = empty_label + ok_btn.clicked.connect(self._on_ok_click) + + self._empty_widget = empty_widget self._content_widget = content_widget self._content_layout = content_layout self._experimental_tools = experimental_tools self._buttons_by_tool_identifier = {} - self._is_refreshing = False - self._refresh_on_active = True - self._window_is_active = False self._refresh_timer = refresh_timer - def refresh(self): - if self._is_refreshing: - return - self._is_refreshing = True + # 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()) @@ -100,11 +118,18 @@ class ExperimentalDialog(QtWidgets.QDialog): self._content_layout.takeAt(idx) button.deleteLater() - self._empty_label.setVisible( - len(self._buttons_by_tool_identifier) == 0 - ) + self._set_visibility() - self._is_refreshing = False + def _is_content_visible(self): + return len(self._buttons_by_tool_identifier) > 0 + + def _set_visibility(self): + content_visible = self._is_content_visible() + self._content_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) @@ -123,6 +148,14 @@ class ExperimentalDialog(QtWidgets.QDialog): elif not self._refresh_timer.isActive(): self._refresh_timer.start() + if self._first_show: + self._first_show = False + # 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() From c7df4e9edc7185a0d155a215a8822b3bed9dcbd7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 19 Oct 2021 20:12:23 +0200 Subject: [PATCH 068/161] changed label --- openpype/tools/settings/local_settings/experimental_widget.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/tools/settings/local_settings/experimental_widget.py b/openpype/tools/settings/local_settings/experimental_widget.py index 741c173415..72f999d886 100644 --- a/openpype/tools/settings/local_settings/experimental_widget.py +++ b/openpype/tools/settings/local_settings/experimental_widget.py @@ -22,7 +22,9 @@ class LocalExperimentalToolsWidgets(QtWidgets.QWidget): # Label that says there are no experimental tools available empty_label = QtWidgets.QLabel(self) - empty_label.setText("There are no experimental tools available.") + empty_label.setText( + "There are no experimental tools available..." + ) layout.addRow(empty_label) From 54a8b9d811e6d8602b2f2b729ae0db2701c678ef Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 11:28:54 +0200 Subject: [PATCH 069/161] removed "widget" key from data --- openpype/tools/loader/app.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index a4b4b5eb28..d7fa9640ac 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -131,9 +131,6 @@ class LoaderWindow(QtWidgets.QDialog): layout.addWidget(footer_widget) self.data = { - "widgets": { - "subsets": subsets - }, "state": { "assetIds": None } @@ -206,8 +203,8 @@ class LoaderWindow(QtWidgets.QDialog): # ------------------------------- 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() From 3a56ac5e5e1920dcffea9d54a0f043cc92c5e628 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 11:05:36 +0200 Subject: [PATCH 070/161] validate tool identifier keys --- openpype/tools/experimental_tools/tools_def.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/openpype/tools/experimental_tools/tools_def.py b/openpype/tools/experimental_tools/tools_def.py index 353ec8e1d5..2fa42bcc6e 100644 --- a/openpype/tools/experimental_tools/tools_def.py +++ b/openpype/tools/experimental_tools/tools_def.py @@ -64,10 +64,16 @@ class ExperimentalTools: if tool.is_available_for_host(host_name) ] - self.tools_by_identifier = { - tool.identifier: tool - for tool in experimental_tools - } + # 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 From 630299e340ab41af673dde31803288bb1587d93f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 11:05:46 +0200 Subject: [PATCH 071/161] fix 80char line --- openpype/tools/experimental_tools/tools_def.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/tools/experimental_tools/tools_def.py b/openpype/tools/experimental_tools/tools_def.py index 2fa42bcc6e..13283c157a 100644 --- a/openpype/tools/experimental_tools/tools_def.py +++ b/openpype/tools/experimental_tools/tools_def.py @@ -15,7 +15,9 @@ class ExperimentalTool: 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): + def __init__( + self, identifier, label, callback, tooltip, hosts_filter=None + ): self.identifier = identifier self.label = label self.callback = callback From b6bb7a9d09fb8e5922bfcaf40ceeaea5d5344d70 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 11:06:21 +0200 Subject: [PATCH 072/161] get host name from environment if not passed # Conflicts: # openpype/tools/experimental_tools/tools_def.py --- openpype/tools/experimental_tools/tools_def.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/tools/experimental_tools/tools_def.py b/openpype/tools/experimental_tools/tools_def.py index 13283c157a..0dcec7a871 100644 --- a/openpype/tools/experimental_tools/tools_def.py +++ b/openpype/tools/experimental_tools/tools_def.py @@ -1,3 +1,4 @@ +import os from openpype.settings import get_local_settings # Constant key under which local settings are stored @@ -53,6 +54,10 @@ class ExperimentalTools: """ def __init__(self, parent=None, host_name=None, filter_hosts=None): experimental_tools = [] + + if not host_name: + host_name = os.environ.get("AVALON_APP") + if filter_hosts is None: filter_hosts = host_name is not None From c9860711512a48d18fe9423a0b510e508c59eeb2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 11:06:29 +0200 Subject: [PATCH 073/161] added few docstrings # Conflicts: # openpype/tools/experimental_tools/tools_def.py --- openpype/tools/experimental_tools/tools_def.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openpype/tools/experimental_tools/tools_def.py b/openpype/tools/experimental_tools/tools_def.py index 0dcec7a871..5dd92151ca 100644 --- a/openpype/tools/experimental_tools/tools_def.py +++ b/openpype/tools/experimental_tools/tools_def.py @@ -51,19 +51,30 @@ class ExperimentalTools: 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 = [] + # 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 From dc9f901c7a83331f4495ac8230f195b371f43a24 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 11:18:13 +0200 Subject: [PATCH 074/161] use OpenPype stylesheet in experimental dialog --- openpype/tools/experimental_tools/dialog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/tools/experimental_tools/dialog.py b/openpype/tools/experimental_tools/dialog.py index 237052c055..a611416efc 100644 --- a/openpype/tools/experimental_tools/dialog.py +++ b/openpype/tools/experimental_tools/dialog.py @@ -29,6 +29,7 @@ class ExperimentalDialog(QtWidgets.QDialog): self.setWindowTitle("OpenPype Experimental tools") icon = QtGui.QIcon(app_icon_path()) self.setWindowIcon(icon) + self.setStyleSheet(load_stylesheet()) empty_widget = QtWidgets.QWidget(self) From 661ba6090967d87edf639fa197ad14e420392196 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 12:21:47 +0200 Subject: [PATCH 075/161] fix parenting in subset widget --- openpype/tools/loader/widgets.py | 70 +++++++++++++++----------------- 1 file changed, 33 insertions(+), 37 deletions(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 6b94fc6e44..d2b0a6b730 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -159,20 +159,25 @@ 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(""" @@ -192,59 +197,50 @@ class SubsetWidget(QtWidgets.QWidget): column = model.Columns.index("time") view.setItemDelegateForColumn(column, time_delegate) - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - 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 + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addLayout(top_bar_layout) + layout.addWidget(view) # 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 +250,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, @@ -1128,7 +1124,7 @@ class RepresentationWidget(QtWidgets.QWidget): label = QtWidgets.QLabel("Representations", self) - tree_view = DeselectableTreeView() + tree_view = DeselectableTreeView(parent=self) tree_view.setModel(proxy_model) tree_view.setAllColumnsShowFocus(True) tree_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) From c0ca4ea5893b6fa267aad72011e457d93de3722b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 12:29:19 +0200 Subject: [PATCH 076/161] add parenting in util widgets --- openpype/tools/utils/widgets.py | 79 ++++++++++++++++----------------- 1 file changed, 38 insertions(+), 41 deletions(-) diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py index b9b542c123..878a9b7c86 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) @@ -399,30 +396,30 @@ 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.setStyleSheet("background: transparent;") - icon = QtWidgets.QLabel() - label = QtWidgets.QLabel(label) - option = OptionBox(body) + icon = QtWidgets.QLabel(body_widget) + label = QtWidgets.QLabel(label, body_widget) + option = OptionBox(body_widget) 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(0, 0, 0, 0) + body_layout.setSpacing(2) + body_layout.addWidget(icon) + body_layout.addWidget(label) + body_layout.addSpacing(6) layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(6, 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,7 +428,7 @@ 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 @@ -476,20 +473,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): From 95838d120a2f834fe38031b4d3fa97b2919f0f4f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 12:32:23 +0200 Subject: [PATCH 077/161] use openpype stylesheet --- openpype/tools/loader/app.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index d7fa9640ac..74e121896e 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.style import load_stylesheet from openpype.tools.utils.widgets import AssetWidget - from openpype.tools.utils import lib from .widgets import ( @@ -46,6 +46,7 @@ class LoaderWindow(QtWidgets.QDialog): if project_name: title += " - {}".format(project_name) self.setWindowTitle(title) + self.setStyleSheet(load_stylesheet()) # Groups config self.groups_config = lib.GroupsConfig(io) @@ -653,7 +654,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: From a2485eb7bf581273ec0c33b7182dd19e3a33e329 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 12:32:35 +0200 Subject: [PATCH 078/161] add parenting to asset view --- openpype/tools/utils/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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) From ce755730d2a84684cf319d203754982aae8b585a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 12:50:30 +0200 Subject: [PATCH 079/161] use WA_TranslucentBackground --- openpype/tools/loader/widgets.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index d2b0a6b730..0946826dc4 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;" From 4b1f739f211d550276019bbc19d53364b22ca1e3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 12:50:51 +0200 Subject: [PATCH 080/161] move stylesheet of SubsetView to style.css --- openpype/style/style.css | 5 +++++ openpype/tools/loader/widgets.py | 6 ------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index 830ed85f9b..a7e82be567 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -629,3 +629,8 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { #PythonInterpreterOutput, #PythonCodeEditor { font-family: "Roboto Mono"; } + +#SubsetView::item { + padding: 5px 1px; + border: 0px; +} diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 0946826dc4..a55cfb6e43 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -181,12 +181,6 @@ class SubsetWidget(QtWidgets.QWidget): view.setModel(family_proxy) view.setObjectName("SubsetView") view.setIndentation(20) - view.setStyleSheet(""" - QTreeView::item{ - padding: 5px 1px; - border: 0px; - } - """) view.setAllColumnsShowFocus(True) # Set view delegates From e5e502502d35c6f692224bbb32564362d383634e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 20 Oct 2021 13:28:12 +0200 Subject: [PATCH 081/161] PYPE-1901 - updated parsing of host Payload doesn't contain all necessary data, manifests must be parsed --- .../webserver_service/webpublish_routes.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py index 3f67038fbf..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 @@ -195,14 +196,30 @@ class WebpublisherBatchPublishEndpoint(_RestApiEndpoint): 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 content.get("files", []): + 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, From 524844f98b78abf3d78cfc65526c8d124c1e3f6a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 20 Oct 2021 13:28:43 +0200 Subject: [PATCH 082/161] PYPE-1901 - updated parsing of workfile Payload doesn't contain all necessary data, manifests must be parsed --- openpype/pype_commands.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index 3071629ee5..4bef3b7a15 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -164,9 +164,15 @@ class PypeCommands: 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, - batch_data["task"], - batch_data["files"][0]) + task_dir_name, + task_data["files"][0]) + print("workfile_path {}".format(workfile_path)) _, batch_id = os.path.split(batch_dir) From 1c179510f7d402f69949faf84b4f6abbb4192e2f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 14:36:03 +0200 Subject: [PATCH 083/161] copied color definitions from new publisher PR --- openpype/style/__init__.py | 40 ++++- openpype/style/color_defs.py | 285 +++++++++++++++++++++++++++++++++++ 2 files changed, 322 insertions(+), 3 deletions(-) create mode 100644 openpype/style/color_defs.py diff --git a/openpype/style/__init__.py b/openpype/style/__init__.py index 0d7904d133..d763bfdc3c 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,6 +12,40 @@ _FONT_IDS = None current_dir = os.path.dirname(os.path.abspath(__file__)) +def get_colors_data(): + data = _get_colors_raw_data() + return data.get("color") or {} + + +def _convert_color_values_to_objects(value): + 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_data = get_colors_data() + output = {} + for key, value in colors_data.items(): + output[key] = _convert_color_values_to_objects(value) + return output + + +def _get_colors_raw_data(): + 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 _load_stylesheet(): from . import qrc_resources @@ -19,9 +55,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(): diff --git a/openpype/style/color_defs.py b/openpype/style/color_defs.py new file mode 100644 index 0000000000..4d726cc3f3 --- /dev/null +++ b/openpype/style/color_defs.py @@ -0,0 +1,285 @@ +import re + + +def parse_color(value): + 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): + from Qt import QtGui + + return QtGui.QColor(*args) + + +def min_max_check(value, min_value, max_value): + 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): + 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): + 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: + def __init__(self, value): + self.value = value + + def get_qcolor(self): + return create_qcolor(self.value) + + +class HEXColor: + 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): + 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: + 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: + 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) + 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: + 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: + 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) + + if isinstance(alpha, int): + alpha = float(alpha) + + 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 From 35435fedba310293bc3d0f5c2c7979b5caccc152 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 15:23:37 +0200 Subject: [PATCH 084/161] use rgba instead of hsla --- openpype/style/data.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/style/data.json b/openpype/style/data.json index a58829d946..5cac7e07db 100644 --- a/openpype/style/data.json +++ b/openpype/style/data.json @@ -28,7 +28,7 @@ "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", @@ -38,15 +38,15 @@ "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-hover": "rgba(168, 175, 189, .3)", "border-focus": "hsl(200, 60%, 60%)" } } From 66a1ba4033deb115db6627ec60fafaa0a9470bad Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 15:24:03 +0200 Subject: [PATCH 085/161] added RepresentationView to stylesheet --- openpype/style/style.css | 2 +- openpype/tools/loader/widgets.py | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index a7e82be567..6507cbe63b 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -630,7 +630,7 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { font-family: "Roboto Mono"; } -#SubsetView::item { +#SubsetView::item, #RepresentationView:item { padding: 5px 1px; border: 0px; } diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index a55cfb6e43..f6ba200eff 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -1120,6 +1120,7 @@ class RepresentationWidget(QtWidgets.QWidget): label = QtWidgets.QLabel("Representations", self) tree_view = DeselectableTreeView(parent=self) + tree_view.setObjectName("RepresentationView") tree_view.setModel(proxy_model) tree_view.setAllColumnsShowFocus(True) tree_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) @@ -1129,12 +1130,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: From dd0ed45d53efc57de2b7fabb3b08be495d70257f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 15:36:33 +0200 Subject: [PATCH 086/161] changed order of setting stylesheet --- openpype/tools/loader/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 74e121896e..b29f0970ca 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -46,7 +46,6 @@ class LoaderWindow(QtWidgets.QDialog): if project_name: title += " - {}".format(project_name) self.setWindowTitle(title) - self.setStyleSheet(load_stylesheet()) # Groups config self.groups_config = lib.GroupsConfig(io) @@ -191,6 +190,8 @@ class LoaderWindow(QtWidgets.QDialog): main_splitter.setSizes([250, 850, 200]) self.resize(1300, 700) + self.setStyleSheet(load_stylesheet()) + def resizeEvent(self, event): super(LoaderWindow, self).resizeEvent(event) self._overlay_frame.resize(self.size()) From 740328ef851cd5cc8aa51ec752256b586e27631e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 15:37:08 +0200 Subject: [PATCH 087/161] added parents to delegates --- openpype/tools/loader/widgets.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index f6ba200eff..9a639c3b85 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -182,22 +182,21 @@ class SubsetWidget(QtWidgets.QWidget): view.setObjectName("SubsetView") view.setIndentation(20) view.setAllColumnsShowFocus(True) - - # Set view delegates - version_delegate = VersionDelegate(self.dbcon) - column = model.Columns.index("version") - view.setItemDelegateForColumn(column, version_delegate) - - time_delegate = PrettyTimeDelegate() - column = model.Columns.index("time") - view.setItemDelegateForColumn(column, time_delegate) - 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, view) + column = model.Columns.index("version") + view.setItemDelegateForColumn(column, version_delegate) + + time_delegate = PrettyTimeDelegate(view) + column = model.Columns.index("time") + view.setItemDelegateForColumn(column, time_delegate) + layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.addLayout(top_bar_layout) From b888240bdb9b66e224e09bf445ba8b815f3e0837 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 15:37:42 +0200 Subject: [PATCH 088/161] use better margins for optional items --- openpype/tools/utils/widgets.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py index 878a9b7c86..de75de705b 100644 --- a/openpype/tools/utils/widgets.py +++ b/openpype/tools/utils/widgets.py @@ -407,14 +407,13 @@ class OptionalActionWidget(QtWidgets.QWidget): option.setFixedSize(30, 30) body_layout = QtWidgets.QHBoxLayout(body_widget) - body_layout.setContentsMargins(0, 0, 0, 0) + body_layout.setContentsMargins(4, 0, 4, 0) body_layout.setSpacing(2) body_layout.addWidget(icon) body_layout.addWidget(label) - body_layout.addSpacing(6) layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(6, 1, 2, 1) + layout.setContentsMargins(2, 1, 2, 1) layout.setSpacing(0) layout.addWidget(body_widget) layout.addWidget(option) From 7d70d22e2d76bdb0a7aa6c936e0c8284b7c63839 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 17:31:21 +0200 Subject: [PATCH 089/161] moved set of stylesheet after show of tool --- openpype/tools/utils/host_tools.py | 48 ++++++++++++++++++------------ 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index ee184ccf2d..599c25d6c8 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -40,7 +40,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 +48,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 +79,30 @@ 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.""" + from avalon import style + + 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 +110,28 @@ class HostToolsHelper: else: loader_tool.refresh() - loader_tool.show() - loader_tool.raise_() - loader_tool.activateWindow() - loader_tool.refresh() + loader_tool.setStyleSheet(style.load_stylesheet()) 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 +139,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 +162,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,24 +185,25 @@ 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 def show_library_loader(self, parent=None): """Loader tool for loading representations from library project.""" + from avalon import style + library_loader_tool = self.get_library_loader_tool(parent) library_loader_tool.show() library_loader_tool.raise_() library_loader_tool.activateWindow() library_loader_tool.refresh() + library_loader_tool.setStyleSheet(style.load_stylesheet()) def show_publish(self, parent=None): """Publish UI.""" @@ -205,18 +214,19 @@ 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_tool_by_name(self, tool_name, parent=None, *args, **kwargs): """Show tool by it's name. From 961a602e1c0d2bdd33c2b7b8e3fbe2ee762db5b5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 17:56:42 +0200 Subject: [PATCH 090/161] fix hovering stylesheet of optional action --- openpype/style/style.css | 8 ++++++ openpype/tools/utils/widgets.py | 47 ++++++++++++++++++--------------- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index 6507cbe63b..948ee8c7b7 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -634,3 +634,11 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { 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/utils/widgets.py b/openpype/tools/utils/widgets.py index de75de705b..15bcbeff90 100644 --- a/openpype/tools/utils/widgets.py +++ b/openpype/tools/utils/widgets.py @@ -310,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()) @@ -349,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) @@ -374,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): @@ -397,11 +387,15 @@ class OptionalActionWidget(QtWidgets.QWidget): super(OptionalActionWidget, self).__init__(parent) body_widget = QtWidgets.QWidget(self) - body_widget.setStyleSheet("background: transparent;") + body_widget.setObjectName("OptionalActionBody") 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) @@ -429,9 +423,22 @@ class OptionalActionWidget(QtWidgets.QWidget): self.option = option 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) @@ -452,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 From bcb27d5aebe6ccc8d5cae84dd4177f8d0c4aa7de Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 18:24:18 +0200 Subject: [PATCH 091/161] changed new thumbnail --- .../tools/loader/images/default_thumbnail.png | Bin 4018 -> 5118 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/openpype/tools/loader/images/default_thumbnail.png b/openpype/tools/loader/images/default_thumbnail.png index 97bd958e0daf01e740db9f426e1f9b3e22484c87..adea862e5b7169f8279bd398226a2fe6283e4925 100644 GIT binary patch literal 5118 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 aQ8TBK`_#VgnlokXr%hW!U;VAB9rQ1Cc_|D4 From acf88c7f16488da71ec5187025382a80c946919a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 18:35:09 +0200 Subject: [PATCH 092/161] alpha can have float number in rgba --- openpype/style/color_defs.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/style/color_defs.py b/openpype/style/color_defs.py index 4d726cc3f3..3f504a9d3b 100644 --- a/openpype/style/color_defs.py +++ b/openpype/style/color_defs.py @@ -155,7 +155,10 @@ class RGBAColor: red = int(red_str) green = int(green_str) blue = int(blue_str) - alpha = int(alpha_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) From 0f62d59d5d89ecc19375bbc3dfa1c410c6e0efdb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 18:35:33 +0200 Subject: [PATCH 093/161] use colors in asset delegate from openpype style --- openpype/style/data.json | 10 +++++++++- openpype/tools/utils/delegates.py | 23 ++++++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/openpype/style/data.json b/openpype/style/data.json index 5cac7e07db..143c6695af 100644 --- a/openpype/style/data.json +++ b/openpype/style/data.json @@ -47,6 +47,14 @@ "border": "#373D48", "border-hover": "rgba(168, 175, 189, .3)", - "border-focus": "hsl(200, 60%, 60%)" + "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/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 From ba97477f1102551bb32cecbf0747a9d064238d5c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 19:26:19 +0200 Subject: [PATCH 094/161] fix resources for maya --- openpype/style/pyside2_resources.py | 929 ++++++++++++++-------------- 1 file changed, 453 insertions(+), 476 deletions(-) diff --git a/openpype/style/pyside2_resources.py b/openpype/style/pyside2_resources.py index ee68a74b8e..c7328e7c91 100644 --- a/openpype/style/pyside2_resources.py +++ b/openpype/style/pyside2_resources.py @@ -1,24 +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: Wed Oct 20 19:25:24 2021 +# by: The Resource Compiler for PySide2 (Qt v5.12.5) +# # 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\ @@ -45,31 +36,6 @@ HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ 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,158 +49,43 @@ 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\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\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\ -\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\ +\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\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\x07\xdd\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -363,6 +214,289 @@ 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\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\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\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\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\x07\xad\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -488,6 +622,55 @@ v)`\x8b\x07>\xa8\xe6\xd1\xfe\x0b\x9d\x85\x8eW\x0d\ ^x\xa2\x9e\x0e\xa7 tG9\x1d\xf6\xe1\x95+\xd6\ \xb1D\x8e\x0e\xcbX\xf0\x0fR\x8ay\x18\xdc\xe2\x02p\ \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\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\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\ @@ -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"\ @@ -693,61 +696,15 @@ qt_resource_name = b"\ \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'\ +\x03'rg\ \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\ +\x00o\x00m\x00b\x00o\x00b\x00o\x00x\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\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 +714,107 @@ 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\ -\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\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\ \x03\x8d\x04G\ \x00r\ \x00i\x00g\x00h\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\x00.\x00p\x00n\ \x00g\ -\x00\x15\ -\x03'rg\ +\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\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\x0f\ +\x06S%\xa7\ +\x00b\ +\x00r\x00a\x00n\x00c\x00h\x00_\x00o\x00p\x00e\x00n\x00.\x00p\x00n\x00g\ +\x00\x12\ +\x01.\x03'\ \x00c\ -\x00o\x00m\x00b\x00o\x00b\x00o\x00x\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\ -\x00.\x00p\x00n\x00g\ +\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\x12\ +\x05\x8f\x9d\x07\ +\x00b\ +\x00r\x00a\x00n\x00c\x00h\x00_\x00o\x00p\x00e\x00n\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\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\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\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\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\x00\xb6\x00\x00\x00\x00\x00\x01\x00\x00\x01\xfd\ +\x00\x00\x01\xe2\x00\x00\x00\x00\x00\x01\x00\x00\x14&\ +\x00\x00\x02\xb6\x00\x00\x00\x00\x00\x01\x00\x00%\x02\ +\x00\x00\x01\x9a\x00\x00\x00\x00\x00\x01\x00\x00\x0cx\ \x00\x00\x00(\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01vA\x9d\xa29\ +\x00\x00\x00X\x00\x00\x00\x00\x00\x01\x00\x00\x00\xaa\ +\x00\x00\x01\x0e\x00\x00\x00\x00\x00\x01\x00\x00\x03I\ +\x00\x00\x02\x80\x00\x00\x00\x00\x00\x01\x00\x00$^\ +\x00\x00\x018\x00\x00\x00\x00\x00\x01\x00\x00\x03\xed\ +\x00\x00\x02\x0c\x00\x00\x00\x00\x00\x01\x00\x00\x14\xd0\ +\x00\x00\x02.\x00\x00\x00\x00\x00\x01\x00\x00\x15y\ +\x00\x00\x01\xbe\x00\x00\x00\x00\x00\x01\x00\x00\x0d\x1c\ +\x00\x00\x02\xda\x00\x00\x00\x00\x00\x01\x00\x00%\xa4\ +\x00\x00\x02X\x00\x00\x00\x00\x00\x01\x00\x00\x1c\xad\ +\x00\x00\x01f\x00\x00\x00\x00\x00\x01\x00\x00\x0b\xce\ +\x00\x00\x02\xf8\x00\x00\x00\x00\x00\x01\x00\x00&F\ +\x00\x00\x00\x94\x00\x00\x00\x00\x00\x01\x00\x00\x01S\ +\x00\x00\x00\xde\x00\x00\x00\x00\x00\x01\x00\x00\x02\xa6\ " - def qInitResources(): - QtCore.qRegisterResourceData( - 0x03, qt_resource_struct, qt_resource_name, qt_resource_data - ) - + QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) def qCleanupResources(): - QtCore.qUnregisterResourceData( - 0x03, qt_resource_struct, qt_resource_name, qt_resource_data - ) + QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + +qInitResources() From 08971bb73ccd34e2f787d6c3c1042e2a9fbc9ca7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Oct 2021 08:23:02 +0000 Subject: [PATCH 095/161] Bump pillow from 8.2.0 to 8.3.2 Bumps [pillow](https://github.com/python-pillow/Pillow) from 8.2.0 to 8.3.2. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/8.2.0...8.3.2) --- updated-dependencies: - dependency-name: pillow dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- poetry.lock | 94 +++++++++++++++++++++++++++++++------------------- pyproject.toml | 2 +- 2 files changed, 59 insertions(+), 37 deletions(-) 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..dade0a2f57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" From e47ed5e77713a970c3332cbc0ecf14be9e14cece Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 10:41:48 +0200 Subject: [PATCH 096/161] safer library loader action handling --- openpype/modules/default_modules/avalon_apps/avalon_app.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/modules/default_modules/avalon_apps/avalon_app.py b/openpype/modules/default_modules/avalon_apps/avalon_app.py index d21b37e520..a8e102f447 100644 --- a/openpype/modules/default_modules/avalon_apps/avalon_app.py +++ b/openpype/modules/default_modules/avalon_apps/avalon_app.py @@ -70,6 +70,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 +90,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 From 68ff9ea9c963df63a42594e0578c8bd7a210c88b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 10:43:02 +0200 Subject: [PATCH 097/161] converted library loader to be created same way as loader --- openpype/tools/libraryloader/app.py | 335 ++++++++++++++-------------- 1 file changed, 172 insertions(+), 163 deletions(-) diff --git a/openpype/tools/libraryloader/app.py b/openpype/tools/libraryloader/app.py index 3f11157418..69c5cb61e7 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,6 +28,8 @@ 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 ): @@ -36,6 +38,20 @@ class LibraryLoaderWindow(QtWidgets.QDialog): self._initial_refresh = False self._ignore_project_change = False + 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(dbcon) + self.family_config_cache = tools_lib.FamilyConfigCache(dbcon) + + # UI initialization # Enable minimize and maximize for app self.setWindowTitle(self.tool_title) window_flags = QtCore.Qt.Window @@ -43,140 +59,149 @@ class LibraryLoaderWindow(QtWidgets.QDialog): 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() + main_splitter = QtWidgets.QSplitter(self) - self.dbcon = AvalonMongoDB() - self.dbcon.install() - self.dbcon.Session["AVALON_PROJECT"] = None + # --- Left part --- + left_side_splitter = QtWidgets.QSplitter(main_splitter) + left_side_splitter.setOrientation(QtCore.Qt.Vertical) - self.show_projects = show_projects - self.show_libraries = show_libraries + # Project combobox + projects_combobox = QtWidgets.QComboBox(left_side_splitter) + combobox_delegate = QtWidgets.QStyledItemDelegate(self) + projects_combobox.setItemDelegate(combobox_delegate) - # Groups config - self.groups_config = tools_lib.GroupsConfig(self.dbcon) - self.family_config_cache = tools_lib.FamilyConfigCache(self.dbcon) - - assets = AssetWidget( - self.dbcon, multiselection=True, parent=self + # 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) - 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._combobox_delegate = combobox_delegate + self._projects_combobox = projects_combobox + 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 + # Set default thumbnail on start - thumbnail.set_thumbnail(None) + thumbnail_widget.set_thumbnail(None) # Defaults - if sync_server.enabled: - split.setSizes([250, 1000, 550]) + if sync_server_enabled: + main_splitter.setSizes([250, 1000, 550]) self.resize(1800, 900) else: - split.setSizes([250, 850, 200]) + main_splitter.setSizes([250, 850, 200]) self.resize(1300, 700) + self.setStyleSheet(style.load_stylesheet()) + def showEvent(self, event): super(LibraryLoaderWindow, self).showEvent(event) if not self._initial_refresh: 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 +212,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 +227,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 +256,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 +270,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 +286,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 +309,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 +323,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 +341,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 +412,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 +435,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 +461,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 +473,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 +482,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 +521,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 +586,6 @@ def show( window = LibraryLoaderWindow( parent, icon, show_projects, show_libraries ) - window.setStyleSheet(style.load_stylesheet()) window.show() module.window = window From b642d770e58643af253e96e739aabe7b8a9d230b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 10:44:28 +0200 Subject: [PATCH 098/161] reorganize initialization --- openpype/tools/loader/app.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 955242e551..54eafd8f6d 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -3,7 +3,7 @@ import sys from Qt import QtWidgets, QtCore from avalon import api, io, pipeline -from openpype.style import load_stylesheet +from openpype import style from openpype.tools.utils.widgets import AssetWidget from openpype.tools.utils import lib @@ -96,15 +96,18 @@ class LoaderWindow(QtWidgets.QDialog): 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) + 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 - thumb_ver_splitter.addWidget(thumbnail_widget) - thumb_ver_splitter.addWidget(version_info_widget) - repres_widget = None if sync_server_enabled: repres_widget = RepresentationWidget( @@ -112,9 +115,6 @@ class LoaderWindow(QtWidgets.QDialog): ) thumb_ver_splitter.addWidget(repres_widget) - thumb_ver_splitter.setStretchFactor(0, 30) - thumb_ver_splitter.setStretchFactor(1, 35) - main_splitter.addWidget(left_side_splitter) main_splitter.addWidget(subsets_widget) main_splitter.addWidget(thumb_ver_splitter) @@ -193,7 +193,7 @@ class LoaderWindow(QtWidgets.QDialog): main_splitter.setSizes([250, 850, 200]) self.resize(1300, 700) - self.setStyleSheet(load_stylesheet()) + self.setStyleSheet(style.load_stylesheet()) def resizeEvent(self, event): super(LoaderWindow, self).resizeEvent(event) From 7cedb5af316288abbb9481a13f6e0951170a11e1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 10:45:32 +0200 Subject: [PATCH 099/161] do not set stylesheets of library loader --- openpype/modules/default_modules/avalon_apps/avalon_app.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openpype/modules/default_modules/avalon_apps/avalon_app.py b/openpype/modules/default_modules/avalon_apps/avalon_app.py index a8e102f447..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.", From 80b0b590b8c824123b687702c5de5180f038de1b Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 21 Oct 2021 11:27:35 +0100 Subject: [PATCH 100/161] Implemented deselect function to handle objects not in 'object mode' --- openpype/hosts/blender/api/plugin.py | 24 +++++++++++++++++++ .../hosts/blender/plugins/load/load_abc.py | 4 ++-- .../hosts/blender/plugins/load/load_fbx.py | 4 ++-- .../blender/plugins/load/load_layout_blend.py | 2 +- .../blender/plugins/load/load_layout_json.py | 2 +- .../hosts/blender/plugins/load/load_model.py | 6 ++--- .../hosts/blender/plugins/load/load_rig.py | 8 +++---- .../blender/plugins/publish/extract_abc.py | 4 ++-- .../blender/plugins/publish/extract_fbx.py | 6 +++-- 9 files changed, 43 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 50b73ade2b..181e5972a8 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/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_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..38718fd9b2 100644 --- a/openpype/hosts/blender/plugins/load/load_layout_json.py +++ b/openpype/hosts/blender/plugins/load/load_layout_json.py @@ -59,7 +59,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) 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_fbx.py b/openpype/hosts/blender/plugins/publish/extract_fbx.py index b91f2a75ef..31d37da8e0 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,9 @@ class ExtractFBX(api.Extractor): add_leaf_bones=False ) - bpy.ops.object.select_all(action='DESELECT') + bpy.context.scene.unit_settings.scale_length = scale_length + + plugin.deselect_all() for mat in new_materials: bpy.data.materials.remove(mat) From a2397f48f032389184f8cee414c674cd8ae4d6c8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 12:40:33 +0200 Subject: [PATCH 101/161] set stylesheet on first show --- openpype/tools/libraryloader/app.py | 30 ++++++++++++++++------------- openpype/tools/loader/app.py | 8 +++++++- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/openpype/tools/libraryloader/app.py b/openpype/tools/libraryloader/app.py index 69c5cb61e7..700d3c05bd 100644 --- a/openpype/tools/libraryloader/app.py +++ b/openpype/tools/libraryloader/app.py @@ -35,6 +35,18 @@ class LibraryLoaderWindow(QtWidgets.QDialog): ): super(LibraryLoaderWindow, self).__init__(parent) + # 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) + + icon = QtGui.QIcon(style.app_icon_path()) + self.setWindowIcon(icon) + + self._first_show = True self._initial_refresh = False self._ignore_project_change = False @@ -52,17 +64,6 @@ class LibraryLoaderWindow(QtWidgets.QDialog): self.family_config_cache = tools_lib.FamilyConfigCache(dbcon) # UI initialization - # Enable minimize and maximize for app - 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) - - icon = QtGui.QIcon(style.app_icon_path()) - self.setWindowIcon(icon) - main_splitter = QtWidgets.QSplitter(self) # --- Left part --- @@ -193,11 +194,14 @@ class LibraryLoaderWindow(QtWidgets.QDialog): main_splitter.setSizes([250, 850, 200]) self.resize(1300, 700) - self.setStyleSheet(style.load_stylesheet()) - def showEvent(self, event): super(LibraryLoaderWindow, self).showEvent(event) + if self._first_show: + self._first_show = False + self.setStyleSheet(style.load_stylesheet()) + if not self._initial_refresh: + self._initial_refresh = True self.refresh() def on_assetview_click(self, *args): diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 54eafd8f6d..a98c7e2f2f 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -193,7 +193,7 @@ class LoaderWindow(QtWidgets.QDialog): main_splitter.setSizes([250, 850, 200]) self.resize(1300, 700) - self.setStyleSheet(style.load_stylesheet()) + self._first_show = True def resizeEvent(self, event): super(LoaderWindow, self).resizeEvent(event) @@ -203,6 +203,12 @@ 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()) + # ------------------------------- # Delay calling blocking methods # ------------------------------- From e545d431da9a8283b5a957c048fa298bd4ca848b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 12:40:42 +0200 Subject: [PATCH 102/161] modified splitter style --- openpype/style/style.css | 40 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index 948ee8c7b7..8013f38bea 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -200,12 +200,44 @@ 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 { + background: qlineargradient( + x1:0, y1:0, x2:1, y2:0, + stop:0.3 rgba(0, 0, 0, 0), + stop:0.5 {color:bg-buttons}, + stop:0.7 rgba(0, 0, 0, 0) + ); +} + +QSplitter::handle:vertical { + background: qlineargradient( + x1:0, y1:0, x2:0, y2:1, + stop:0.3 rgba(0, 0, 0, 0), + stop:0.5 {color:bg-buttons}, + stop:0.7 rgba(0, 0, 0, 0) + ); +} + +QSplitter::handle:horizontal:hover { + background: qlineargradient( + x1:0, y1:0, x2:1, y2:0, + stop:0.3 rgba(0, 0, 0, 0), + stop:0.5 {color:bg-button-hover}, + stop:0.7 rgba(0, 0, 0, 0) + ); +} + +QSplitter::handle:vertical:hover { + background: qlineargradient( + x1:0, y1:0, x2:0, y2:1, + stop:0.3 rgba(0, 0, 0, 0), + stop:0.5 {color:bg-button-hover}, + stop:0.7 rgba(0, 0, 0, 0) + ); } /* SLider */ From 52f474d62d1bfb22b0ada0908a768d90728841b7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 12:42:01 +0200 Subject: [PATCH 103/161] splitter has it's own key in data --- openpype/style/data.json | 3 +++ openpype/style/style.css | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/openpype/style/data.json b/openpype/style/data.json index 143c6695af..c33c2eaa5e 100644 --- a/openpype/style/data.json +++ b/openpype/style/data.json @@ -32,6 +32,9 @@ "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", diff --git a/openpype/style/style.css b/openpype/style/style.css index 8013f38bea..3f006fb845 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -208,7 +208,7 @@ QSplitter::handle:horizontal { background: qlineargradient( x1:0, y1:0, x2:1, y2:0, stop:0.3 rgba(0, 0, 0, 0), - stop:0.5 {color:bg-buttons}, + stop:0.5 {color:bg-splitter}, stop:0.7 rgba(0, 0, 0, 0) ); } @@ -217,7 +217,7 @@ QSplitter::handle:vertical { background: qlineargradient( x1:0, y1:0, x2:0, y2:1, stop:0.3 rgba(0, 0, 0, 0), - stop:0.5 {color:bg-buttons}, + stop:0.5 {color:bg-splitter}, stop:0.7 rgba(0, 0, 0, 0) ); } @@ -226,7 +226,7 @@ QSplitter::handle:horizontal:hover { background: qlineargradient( x1:0, y1:0, x2:1, y2:0, stop:0.3 rgba(0, 0, 0, 0), - stop:0.5 {color:bg-button-hover}, + stop:0.5 {color:bg-splitter-hover}, stop:0.7 rgba(0, 0, 0, 0) ); } @@ -235,7 +235,7 @@ QSplitter::handle:vertical:hover { background: qlineargradient( x1:0, y1:0, x2:0, y2:1, stop:0.3 rgba(0, 0, 0, 0), - stop:0.5 {color:bg-button-hover}, + stop:0.5 {color:bg-splitter-hover}, stop:0.7 rgba(0, 0, 0, 0) ); } From 36f49122416af0138f8acae4fd72792f6a1c673f Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 21 Oct 2021 11:51:12 +0100 Subject: [PATCH 104/161] Hound fixes --- openpype/hosts/blender/api/plugin.py | 4 ++-- openpype/hosts/blender/plugins/publish/extract_fbx.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 181e5972a8..6d437059b8 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -108,13 +108,13 @@ def deselect_all(): 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.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.ops.object.mode_set(mode=p[1]) bpy.context.view_layer.objects.active = active diff --git a/openpype/hosts/blender/plugins/publish/extract_fbx.py b/openpype/hosts/blender/plugins/publish/extract_fbx.py index 31d37da8e0..f9ffdea1d1 100644 --- a/openpype/hosts/blender/plugins/publish/extract_fbx.py +++ b/openpype/hosts/blender/plugins/publish/extract_fbx.py @@ -60,8 +60,6 @@ class ExtractFBX(api.Extractor): add_leaf_bones=False ) - bpy.context.scene.unit_settings.scale_length = scale_length - plugin.deselect_all() for mat in new_materials: From 196779d9b3a5f96f71ab7096724b042019b33160 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 21 Oct 2021 14:30:30 +0200 Subject: [PATCH 105/161] Flame: creating host folder with basic methods --- openpype/hosts/flame/__init__.py | 105 +++++++++ openpype/hosts/flame/api/__init__.py | 3 + openpype/hosts/flame/api/lib.py | 310 +++++++++++++++++++++++++++ openpype/hosts/flame/api/menu.py | 199 +++++++++++++++++ openpype/hosts/flame/api/pipeline.py | 162 ++++++++++++++ openpype/hosts/flame/api/plugin.py | 13 ++ openpype/hosts/flame/api/utils.py | 91 ++++++++ openpype/hosts/flame/api/workio.py | 37 ++++ 8 files changed, 920 insertions(+) create mode 100644 openpype/hosts/flame/__init__.py create mode 100644 openpype/hosts/flame/api/__init__.py create mode 100644 openpype/hosts/flame/api/lib.py create mode 100644 openpype/hosts/flame/api/menu.py create mode 100644 openpype/hosts/flame/api/pipeline.py create mode 100644 openpype/hosts/flame/api/plugin.py create mode 100644 openpype/hosts/flame/api/utils.py create mode 100644 openpype/hosts/flame/api/workio.py diff --git a/openpype/hosts/flame/__init__.py b/openpype/hosts/flame/__init__.py new file mode 100644 index 0000000000..dc3d3e7cba --- /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..9d24e94df8 --- /dev/null +++ b/openpype/hosts/flame/api/lib.py @@ -0,0 +1,310 @@ +import sys +import json +import re +import os +import pickle +import contextlib +from pprint import pprint, pformat +from opentimelineio import opentime +import openpype + + +# from ..otio import davinci_export as otio_export + +from openpype.api import Logger + +log = Logger().get_logger(__name__) + +self = sys.modules[__name__] +self.project_manager = None +self.media_storage = None + +# OpenPype sequencial rename variables +self.rename_index = 0 +self.rename_add = 0 + +self.publish_clip_color = "Pink" +self.pype_marker_workflow = True + +# OpenPype compound clip workflow variable +self.pype_tag_name = "VFX Notes" + +# OpenPype marker workflow variables +self.pype_marker_name = "OpenPypeData" +self.pype_marker_duration = 1 +self.pype_marker_color = "Mint" +self.temp_marker_frame = None + +# OpenPype default timeline +self.pype_timeline_name = "OpenPypeTimeline" + + +class FlameAppFramework(object): + # flameAppFramework class takes care of preferences + + class prefs_dict(dict): + # subclass of a dict() in order to directly link it + # to main framework prefs dictionaries + # when accessed directly it will operate on a dictionary under a "name" + # key in master dictionary. + # master = {} + # p = prefs(master, "app_name") + # p["key"] = "value" + # master - {"app_name": {"key", "value"}} + + 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: + 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 load_prefs(self): + prefix = self.prefs_folder + os.path.sep + self.bundle_name + prefs_file_path = (prefix + "." + self.flame_user_name + "." + + self.flame_project_name + ".prefs") + prefs_user_file_path = (prefix + "." + self.flame_user_name + + ".prefs") + prefs_global_file_path = prefix + ".prefs" + + try: + with open(prefs_file_path, "r") as prefs_file: + self.prefs = pickle.load(prefs_file) + + self.log.info("preferences loaded from {}".format(prefs_file_path)) + self.log.info("preferences contents:\n" + pformat(self.prefs)) + except: + self.log.info("unable to load preferences from {}".format( + prefs_file_path)) + + try: + with open(prefs_user_file_path, "r") as prefs_file: + self.prefs_user = pickle.load(prefs_file) + self.log.info("preferences loaded from {}".format( + prefs_user_file_path)) + self.log.info("preferences contents:\n" + pformat(self.prefs_user)) + except: + self.log.info("unable to load preferences from {}".format( + prefs_user_file_path)) + + try: + with open(prefs_global_file_path, "r") as prefs_file: + self.prefs_global = pickle.load(prefs_file) + self.log.info("preferences loaded from {}".format( + prefs_global_file_path)) + self.log.info("preferences contents:\n" + pformat(self.prefs_global)) + + except: + self.log.info("unable to load preferences from {}".format( + prefs_global_file_path)) + + return True + + def save_prefs(self): + import pickle + + if not os.path.isdir(self.prefs_folder): + try: + os.makedirs(self.prefs_folder) + except: + self.log.info("unable to create folder {}".format( + self.prefs_folder)) + return False + + prefix = self.prefs_folder + os.path.sep + self.bundle_name + prefs_file_path = prefix + "." + self.flame_user_name + "." + self.flame_project_name + ".prefs" + prefs_user_file_path = prefix + "." + self.flame_user_name + ".prefs" + prefs_global_file_path = prefix + ".prefs" + + try: + prefs_file = open(prefs_file_path, "w") + pickle.dump(self.prefs, prefs_file) + prefs_file.close() + self.log.info("preferences saved to {}".format(prefs_file_path)) + self.log.info("preferences contents:\n" + pformat(self.prefs)) + except: + self.log.info("unable to save preferences to {}".format(prefs_file_path)) + + try: + prefs_file = open(prefs_user_file_path, "w") + pickle.dump(self.prefs_user, prefs_file) + prefs_file.close() + self.log.info("preferences saved to {}".format(prefs_user_file_path)) + self.log.info("preferences contents:\n" + pformat(self.prefs_user)) + except: + self.log.info("unable to save preferences to {}".format(prefs_user_file_path)) + + try: + prefs_file = open(prefs_global_file_path, "w") + pickle.dump(self.prefs_global, prefs_file) + prefs_file.close() + self.log.info("preferences saved to {}".format(prefs_global_file_path)) + self.log.info("preferences contents:\n" + pformat(self.prefs_global)) + except: + self.log.info("unable to save preferences to {}".format(prefs_global_file_path)) + + 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 + """ + 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: + pass diff --git a/openpype/hosts/flame/api/menu.py b/openpype/hosts/flame/api/menu.py new file mode 100644 index 0000000000..65d1535beb --- /dev/null +++ b/openpype/hosts/flame/api/menu.py @@ -0,0 +1,199 @@ +import os +import sys +from Qt import QtWidgets, QtCore +from pprint import pprint, pformat +from copy import deepcopy + +from .lib import rescan_hooks +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: + 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: + 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: + 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: + 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..297ab0e44c --- /dev/null +++ b/openpype/hosts/flame/api/pipeline.py @@ -0,0 +1,162 @@ +""" +Basic avalon integration +""" +import os +import contextlib +from collections import OrderedDict +from avalon.tools import workfiles +from avalon import api as avalon +from avalon import schema +from avalon.pipeline import AVALON_CONTAINER_ID +from pyblish import api as pyblish +from openpype.api import Logger +from . import lib + +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 DaVinci Resovle plug-ins..") + + avalon.register_plugin_path(avalon.Loader, LOAD_PATH) + avalon.register_plugin_path(avalon.Creator, CREATE_PATH) + avalon.register_plugin_path(avalon.InventoryAction, INVENTORY_PATH) + + # 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..ac86c7c224 --- /dev/null +++ b/openpype/hosts/flame/api/plugin.py @@ -0,0 +1,13 @@ +import re +import uuid +from avalon import api +import openpype.api as pype +from openpype.hosts import resolve +from avalon.vendor import qargparse +from . import lib + +from Qt import QtWidgets, QtCore + +# 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..489b51e37c --- /dev/null +++ b/openpype/hosts/flame/api/utils.py @@ -0,0 +1,91 @@ +#! python3 + +""" +Resolve's tools for setting environment +""" + +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 resolve. + + 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 + + if not env: + env = os.environ + + # initiate inputs + scripts = {} + fsd_env = env.get("FLAME_SCRIPT_DIR", "") + flame_shared_dir = "/opt/Autodesk/shared/python" + + fsd_paths = [os.path.join( + HOST_DIR, + "utility_scripts" + )] + + # collect script dirs + log.info("FLAME_SCRIPT_DIR: `{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)}) + + 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 s in os.listdir(flame_shared_dir): + path = os.path.join(flame_shared_dir, s) + 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 pype.hooks.resolve.FlamePrelaunch() + """ + if not env: + env = 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..00fcdb9405 --- /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("\\", "/") From 48cbd28deb0444b53e207123b92f11848c5ec632 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 21 Oct 2021 14:30:54 +0200 Subject: [PATCH 106/161] Flame: adding prelauch hook --- openpype/hosts/flame/hooks/pre_flame_setup.py | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 openpype/hosts/flame/hooks/pre_flame_setup.py 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..aec9a15e30 --- /dev/null +++ b/openpype/hosts/flame/hooks/pre_flame_setup.py @@ -0,0 +1,116 @@ +import os +import json +import tempfile +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) + + # Store dumped json to temporary file + temporary_json_file = tempfile.NamedTemporaryFile( + mode="w", suffix=".json", delete=False + ) + temporary_json_file.write(dumped_script_data) + temporary_json_file.close() + temporary_json_filepath = temporary_json_file.name.replace( + "\\", "/" + ) + + # Prepare subprocess arguments + args = [ + self.flame_python_exe, + self.wtc_script_path, + temporary_json_filepath + ] + 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(temporary_json_filepath).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") + + # Remove the temporary json + os.remove(temporary_json_filepath) + + return app_args From 0ec42fca3fbecfca25b53959af0949cb1feb94f0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 21 Oct 2021 14:31:16 +0200 Subject: [PATCH 107/161] Flame: adding WireTap communication script --- openpype/hosts/flame/scripts/wiretap_com.py | 481 ++++++++++++++++++++ 1 file changed, 481 insertions(+) create mode 100644 openpype/hosts/flame/scripts/wiretap_com.py diff --git a/openpype/hosts/flame/scripts/wiretap_com.py b/openpype/hosts/flame/scripts/wiretap_com.py new file mode 100644 index 0000000000..a5925d0546 --- /dev/null +++ b/openpype/hosts/flame/scripts/wiretap_com.py @@ -0,0 +1,481 @@ +#!/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 + +# Todo: this has to be replaced with somehting more dynamic +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) From 8fac3175a0a23dbd6361bb6cc698a6eeb499406b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 21 Oct 2021 14:33:14 +0200 Subject: [PATCH 108/161] Flame: adding flame_hook utility script for launching withing flame --- .../hosts/flame/utility_scripts/flame_hook.py | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 openpype/hosts/flame/utility_scripts/flame_hook.py diff --git a/openpype/hosts/flame/utility_scripts/flame_hook.py b/openpype/hosts/flame/utility_scripts/flame_hook.py new file mode 100644 index 0000000000..b46109a609 --- /dev/null +++ b/openpype/hosts/flame/utility_scripts/flame_hook.py @@ -0,0 +1,131 @@ +import sys +from Qt import QtWidgets, QtCore +from pprint import pprint, pformat +import atexit +import openpype +import avalon +import openpype.hosts.flame as opflame + +flh = sys.modules[__name__] +flh._project = None + + +def openpype_install(): + openpype.install() + avalon.api.install(opflame) + print("<<<<<<<<<<< Avalon registred hosts: {} >>>>>>>>>>>>>>>".format( + avalon.api.registered_host())) + + +# Exception handler +def exeption_handler(exctype, value, tb): + import traceback + msg = "OpenPype: Python exception {} in {}".format(value, exctype) + mbox = QtWidgets.QMessageBox() + mbox.setText(msg) + mbox.setDetailedText( + pformat(traceback.format_exception(exctype, value, tb))) + mbox.setStyleSheet('QLabel{min-width: 800px;}') + mbox.exec_() + sys.__excepthook__(exctype, value, tb) + + +# add exception handler into sys module +sys.excepthook = exeption_handler + + +# register clean up logic to be called at Flame exit +def cleanup(): + 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(): + 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): + cleanup() + + +def app_initialized(parent=None): + opflame.app_framework = opflame.FlameAppFramework() + + print(">> flame_hook.py: {} initializing".format( + opflame.app_framework.bundle_name)) + + load_apps() + + +try: + import flame + app_initialized(parent=None) +except ImportError: + print("!!!! not able to import flame module !!!!") + + +def rescan_hooks(): + import flame + try: + flame.execute_shortcut('Rescan Python Hooks') + except: + pass + + +def _build_app_menu(app_name): + menu = [] + app = None + for _app in opflame.apps: + if _app.__class__.__name__ == app_name: + app = _app + + if app: + menu.append(app.build_menu()) + + print(">>_> `{}` was build: {}".format(app_name, pformat(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 + flame.schedule_idle_event(rescan_hooks) + except ImportError: + print("!-!!! not able to import flame module !!!!") + + return menu + +def project_saved(project_name, save_time, is_auto_save): + if opflame.app_framework: + opflame.app_framework.save_prefs() + + +def get_main_menu_custom_ui_actions(): + # install openpype and the host + openpype_install() + + return _build_app_menu("FlameMenuProjectconnect") + + +def get_timeline_custom_ui_actions(): + # install openpype and the host + openpype_install() + + return _build_app_menu("FlameMenuTimeline") From 1a1f6e7039e690678af4f31854fb9903e5b82b29 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 21 Oct 2021 14:34:02 +0200 Subject: [PATCH 109/161] Flame: adding settings and icon --- openpype/resources/app_icons/flame.png | Bin 0 -> 74845 bytes .../system_settings/applications.json | 36 ++++++++++++++++ openpype/settings/entities/enum_entity.py | 1 + .../host_settings/schema_flame.json | 39 ++++++++++++++++++ .../system_schema/schema_applications.json | 4 ++ 5 files changed, 80 insertions(+) create mode 100644 openpype/resources/app_icons/flame.png create mode 100644 openpype/settings/entities/schemas/system_schema/host_settings/schema_flame.json diff --git a/openpype/resources/app_icons/flame.png b/openpype/resources/app_icons/flame.png new file mode 100644 index 0000000000000000000000000000000000000000..ba9b69e45fa73d3f9298e77ec4a167a38f7ecf7b GIT binary patch literal 74845 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 mB{bSPYzgNTjDR9V!~XzB%rJgMHNZar0000 Date: Thu, 21 Oct 2021 14:44:39 +0200 Subject: [PATCH 110/161] added few docstrings --- openpype/style/__init__.py | 47 ++++++++++++--- openpype/style/color_defs.py | 109 ++++++++++++++++++++++++++++++++++- 2 files changed, 146 insertions(+), 10 deletions(-) diff --git a/openpype/style/__init__.py b/openpype/style/__init__.py index d763bfdc3c..fd39e93b5d 100644 --- a/openpype/style/__init__.py +++ b/openpype/style/__init__.py @@ -12,12 +12,36 @@ _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(): @@ -32,6 +56,11 @@ def _convert_color_values_to_objects(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(): @@ -39,14 +68,15 @@ def get_objected_colors(): return output -def _get_colors_raw_data(): - 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 _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() @@ -78,6 +108,7 @@ def _load_stylesheet(): def _load_font(): + """Load and register fonts into Qt application.""" from Qt import QtGui global _FONT_IDS @@ -117,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() @@ -125,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 index 3f504a9d3b..0f4e145ca0 100644 --- a/openpype/style/color_defs.py +++ b/openpype/style/color_defs.py @@ -1,7 +1,27 @@ +"""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) @@ -21,12 +41,30 @@ def parse_color(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 @@ -39,6 +77,16 @@ def min_max_check(value, min_value, max_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 {}" @@ -48,6 +96,16 @@ def int_validation(value, min_value=None, max_value=None): 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 {}" @@ -57,6 +115,11 @@ def float_validation(value, min_value=None, max_value=None): 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 @@ -65,6 +128,14 @@ class UnknownColor: 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): @@ -92,6 +163,7 @@ class HEXColor: @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)) @@ -111,6 +183,13 @@ class HEXColor: 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(") @@ -146,6 +225,13 @@ class RGBColor: 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(") @@ -191,6 +277,15 @@ class RGBAColor: 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(") @@ -235,6 +330,16 @@ class HSLColor: 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(") @@ -251,10 +356,8 @@ class HSLAColor: light = float(light_str.rstrip("%")) / 100 else: light = float(light_str) - alpha = float(alpha_str) - if isinstance(alpha, int): - alpha = float(alpha) + alpha = float(alpha_str) int_validation(hue, 0, 360) float_validation(sat, 0, 1) From 426c996b71e673ee39edbc84c86170241b9f38b3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 14:50:06 +0200 Subject: [PATCH 111/161] hound fixes in pyside2 resources --- openpype/style/pyside2_resources.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/style/pyside2_resources.py b/openpype/style/pyside2_resources.py index c7328e7c91..80f9b904fd 100644 --- a/openpype/style/pyside2_resources.py +++ b/openpype/style/pyside2_resources.py @@ -811,10 +811,14 @@ qt_resource_struct = b"\ \x00\x00\x00\xde\x00\x00\x00\x00\x00\x01\x00\x00\x02\xa6\ " + def qInitResources(): - QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + QtCore.qRegisterResourceData( + 0x01, qt_resource_struct, qt_resource_name, qt_resource_data + ) + def qCleanupResources(): - QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) - -qInitResources() + QtCore.qUnregisterResourceData( + 0x01, qt_resource_struct, qt_resource_name, qt_resource_data + ) From 610477961025d7dd7b149668f084baee857aa903 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 15:00:05 +0200 Subject: [PATCH 112/161] renamed 'ExperimentalDialog' to 'ExperimentalToolsDialog' --- openpype/tools/experimental_tools/__init__.py | 4 ++-- openpype/tools/experimental_tools/dialog.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/tools/experimental_tools/__init__.py b/openpype/tools/experimental_tools/__init__.py index 75e3210aab..d6315e4655 100644 --- a/openpype/tools/experimental_tools/__init__.py +++ b/openpype/tools/experimental_tools/__init__.py @@ -3,12 +3,12 @@ from .tools_def import ( LOCAL_EXPERIMENTAL_KEY ) -from .dialog import ExperimentalDialog +from .dialog import ExperimentalToolsDialog __all__ = ( "ExperimentalTools", "LOCAL_EXPERIMENTAL_KEY", - "ExperimentalDialog" + "ExperimentalToolsDialog" ) diff --git a/openpype/tools/experimental_tools/dialog.py b/openpype/tools/experimental_tools/dialog.py index a611416efc..6173deb693 100644 --- a/openpype/tools/experimental_tools/dialog.py +++ b/openpype/tools/experimental_tools/dialog.py @@ -21,11 +21,11 @@ class ToolButton(QtWidgets.QPushButton): self.triggered.emit(self._identifier) -class ExperimentalDialog(QtWidgets.QDialog): +class ExperimentalToolsDialog(QtWidgets.QDialog): refresh_interval = 3000 def __init__(self, parent=None): - super(ExperimentalDialog, self).__init__(parent) + super(ExperimentalToolsDialog, self).__init__(parent) self.setWindowTitle("OpenPype Experimental tools") icon = QtGui.QIcon(app_icon_path()) self.setWindowIcon(icon) @@ -138,7 +138,7 @@ class ExperimentalDialog(QtWidgets.QDialog): tool.execute() def showEvent(self, event): - super(ExperimentalDialog, self).showEvent(event) + super(ExperimentalToolsDialog, self).showEvent(event) if self._refresh_on_active: # Start/Restart timer @@ -164,7 +164,7 @@ class ExperimentalDialog(QtWidgets.QDialog): self._refresh_timer.start() self.refresh() - super(ExperimentalDialog, self).changeEvent(event) + super(ExperimentalToolsDialog, self).changeEvent(event) def _on_refresh_timeout(self): # Stop timer if window is not visible From 0c0809469cddf15bc5412856ed6f478f001cc716 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 15:00:16 +0200 Subject: [PATCH 113/161] added experimental dialog to host tools --- openpype/tools/utils/host_tools.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index ee184ccf2d..c0e6d71b73 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): @@ -218,6 +219,22 @@ class HostToolsHelper: look_assigner_tool = self.get_look_assigner_tool(parent) look_assigner_tool.show() + def get_experimental_tools_dialog(self, parent=None): + 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): + 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 +264,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 +301,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 +378,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) From d4729ab0695ac59673583f02efb9bb9fdfb136ce Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 15:05:11 +0200 Subject: [PATCH 114/161] disable filtering by host name when used in local settings --- openpype/tools/settings/local_settings/experimental_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/settings/local_settings/experimental_widget.py b/openpype/tools/settings/local_settings/experimental_widget.py index 72f999d886..e863d9afb0 100644 --- a/openpype/tools/settings/local_settings/experimental_widget.py +++ b/openpype/tools/settings/local_settings/experimental_widget.py @@ -28,7 +28,7 @@ class LocalExperimentalToolsWidgets(QtWidgets.QWidget): layout.addRow(empty_label) - experimental_defs = ExperimentalTools() + experimental_defs = ExperimentalTools(filter_hosts=False) checkboxes_by_identifier = {} for tool in experimental_defs.tools: checkbox = QtWidgets.QCheckBox(self) From 7a50a106e333ed472dcb8fa6dfd7acc7e71d5efb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 15:05:20 +0200 Subject: [PATCH 115/161] added few docstrings --- .../tools/experimental_tools/tools_def.py | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/openpype/tools/experimental_tools/tools_def.py b/openpype/tools/experimental_tools/tools_def.py index 5dd92151ca..6ae4637039 100644 --- a/openpype/tools/experimental_tools/tools_def.py +++ b/openpype/tools/experimental_tools/tools_def.py @@ -8,6 +8,8 @@ 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. @@ -91,11 +93,32 @@ class ExperimentalTools: ).format(tool.identifier)) tools_by_identifier[tool.identifier] = tool - self.tools_by_identifier = tools_by_identifier - self.tools = experimental_tools + 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) From 33764f6e16c3cc0e46bc75d7945db592acf0623f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 15:08:39 +0200 Subject: [PATCH 116/161] added docstrings to host tools --- openpype/tools/utils/host_tools.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index c0e6d71b73..2ac9d0c48b 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -220,6 +220,16 @@ class HostToolsHelper: look_assigner_tool.show() 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 @@ -229,6 +239,7 @@ class HostToolsHelper: 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() From 29d1c47a58751ba16f9a5f71ff5d35cf7839ac61 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 15:12:12 +0200 Subject: [PATCH 117/161] disable buttons of tools that are not turned on --- openpype/tools/experimental_tools/dialog.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/tools/experimental_tools/dialog.py b/openpype/tools/experimental_tools/dialog.py index 6173deb693..4923759249 100644 --- a/openpype/tools/experimental_tools/dialog.py +++ b/openpype/tools/experimental_tools/dialog.py @@ -112,6 +112,9 @@ class ExperimentalToolsDialog(QtWidgets.QDialog): "\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) From de9f0f7fa46fa757f1b6ce6d6b985360de98410d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 15:28:25 +0200 Subject: [PATCH 118/161] adde commented example tool --- openpype/tools/experimental_tools/tools_def.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/openpype/tools/experimental_tools/tools_def.py b/openpype/tools/experimental_tools/tools_def.py index 6ae4637039..3657c2385b 100644 --- a/openpype/tools/experimental_tools/tools_def.py +++ b/openpype/tools/experimental_tools/tools_def.py @@ -65,6 +65,19 @@ class ExperimentalTools: # 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", + # "Exmaple 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") From ee443cb735637b56be5367164309481f7defb35a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 15:29:28 +0200 Subject: [PATCH 119/161] added example tool into experimental tools --- openpype/hosts/maya/api/menu.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 4f0966abfd..5eb8882030 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -46,6 +46,15 @@ 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 modify_workfiles(): # Find the pipeline menu top_menu = _get_menu() @@ -103,6 +112,7 @@ def deferred(): add_build_workfiles_item() add_look_assigner_item() + add_experimental_item() modify_workfiles() remove_project_manager() From 501f0ed550e76ebbff0340b6bdf2bb14a75e06ce Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 15:50:10 +0200 Subject: [PATCH 120/161] added experimental tools into nuke menu --- openpype/hosts/nuke/api/menu.py | 6 ++++++ 1 file changed, 6 insertions(+) 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() From 6da7c65e295be27cab7fd1c594f78945247ec989 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 16:19:43 +0200 Subject: [PATCH 121/161] set stylesheet after show --- openpype/tools/experimental_tools/dialog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/tools/experimental_tools/dialog.py b/openpype/tools/experimental_tools/dialog.py index 4923759249..c7c8ce83fc 100644 --- a/openpype/tools/experimental_tools/dialog.py +++ b/openpype/tools/experimental_tools/dialog.py @@ -29,7 +29,6 @@ class ExperimentalToolsDialog(QtWidgets.QDialog): self.setWindowTitle("OpenPype Experimental tools") icon = QtGui.QIcon(app_icon_path()) self.setWindowIcon(icon) - self.setStyleSheet(load_stylesheet()) empty_widget = QtWidgets.QWidget(self) @@ -154,6 +153,8 @@ class ExperimentalToolsDialog(QtWidgets.QDialog): 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() From 3bb391f8701be3ed803794267eac6e95213f68ea Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 21 Oct 2021 15:35:46 +0100 Subject: [PATCH 122/161] Implemented fix for deselecting all objects --- openpype/hosts/blender/plugins/create/create_camera.py | 2 +- openpype/hosts/blender/plugins/load/load_camera_blend.py | 2 +- openpype/hosts/blender/plugins/load/load_camera_fbx.py | 4 ++-- openpype/hosts/blender/plugins/publish/extract_camera.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index fad827d85a..98ccca313c 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -53,7 +53,7 @@ class CreateCamera(plugin.Creator): selected.append(asset_group) bpy.ops.object.parent_set(keep_transform=True) else: - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() camera_obj.select_set(True) asset_group.select_set(True) bpy.context.view_layer.objects.active = asset_group diff --git a/openpype/hosts/blender/plugins/load/load_camera_blend.py b/openpype/hosts/blender/plugins/load/load_camera_blend.py index 1173e26d7b..834eb467d8 100644 --- a/openpype/hosts/blender/plugins/load/load_camera_blend.py +++ b/openpype/hosts/blender/plugins/load/load_camera_blend.py @@ -91,7 +91,7 @@ class BlendCameraLoader(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_camera_fbx.py b/openpype/hosts/blender/plugins/load/load_camera_fbx.py index c6d491870d..5edba7ec0c 100644 --- a/openpype/hosts/blender/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/blender/plugins/load/load_camera_fbx.py @@ -38,7 +38,7 @@ class FbxCameraLoader(plugin.AssetLoader): bpy.data.objects.remove(obj) 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 @@ -68,7 +68,7 @@ class FbxCameraLoader(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/publish/extract_camera.py b/openpype/hosts/blender/plugins/publish/extract_camera.py index 7888ddad6a..a0e78178c8 100644 --- a/openpype/hosts/blender/plugins/publish/extract_camera.py +++ b/openpype/hosts/blender/plugins/publish/extract_camera.py @@ -23,7 +23,7 @@ class ExtractCamera(api.Extractor): # Perform extraction self.log.info("Performing extraction..") - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() selected = [] @@ -56,7 +56,7 @@ class ExtractCamera(api.Extractor): bpy.context.scene.unit_settings.scale_length = scale_length - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() if "representations" not in instance.data: instance.data["representations"] = [] From 28bf0a23996dd5538b9843dfaa271904b41a416e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 17:14:17 +0200 Subject: [PATCH 123/161] resize after showing --- openpype/tools/libraryloader/app.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/openpype/tools/libraryloader/app.py b/openpype/tools/libraryloader/app.py index 700d3c05bd..3e4c5d5850 100644 --- a/openpype/tools/libraryloader/app.py +++ b/openpype/tools/libraryloader/app.py @@ -129,6 +129,10 @@ class LibraryLoaderWindow(QtWidgets.QDialog): 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 --- footer_widget = QtWidgets.QWidget(self) @@ -168,6 +172,7 @@ class LibraryLoaderWindow(QtWidgets.QDialog): projects_combobox.currentTextChanged.connect(self.on_project_change) self.sync_server = sync_server + self._sync_server_enabled = sync_server_enabled self._combobox_delegate = combobox_delegate self._projects_combobox = projects_combobox @@ -186,19 +191,15 @@ class LibraryLoaderWindow(QtWidgets.QDialog): # Set default thumbnail on start thumbnail_widget.set_thumbnail(None) - # Defaults - if sync_server_enabled: - main_splitter.setSizes([250, 1000, 550]) - self.resize(1800, 900) - else: - main_splitter.setSizes([250, 850, 200]) - self.resize(1300, 700) - 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 From 2b2f27b74e557a1b34e15bc30ffb8fc9ca23d489 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 17:15:40 +0200 Subject: [PATCH 124/161] set default thumbnail on initialization of ThumbnailWidget --- openpype/tools/libraryloader/app.py | 3 --- openpype/tools/loader/widgets.py | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/tools/libraryloader/app.py b/openpype/tools/libraryloader/app.py index 3e4c5d5850..d7c6c162e6 100644 --- a/openpype/tools/libraryloader/app.py +++ b/openpype/tools/libraryloader/app.py @@ -188,9 +188,6 @@ class LibraryLoaderWindow(QtWidgets.QDialog): self._message_label = message_label self._message_timer = message_timer - # Set default thumbnail on start - thumbnail_widget.set_thumbnail(None) - def showEvent(self, event): super(LibraryLoaderWindow, self).showEvent(event) if self._first_show: diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index b068dd95d1..4c075382ac 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -745,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() From 1b10ef39fd43e65a8ddcfcabb185066d88a578b9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 17:17:24 +0200 Subject: [PATCH 125/161] resize loader after showing --- openpype/tools/loader/app.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index a98c7e2f2f..bbf6719af5 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -119,6 +119,11 @@ class LoaderWindow(QtWidgets.QDialog): 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]) + # TODO keep footer size by message size footer_widget = QtWidgets.QWidget(self) footer_widget.setFixedHeight(20) @@ -164,6 +169,8 @@ class LoaderWindow(QtWidgets.QDialog): 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 @@ -185,14 +192,6 @@ class LoaderWindow(QtWidgets.QDialog): self._refresh() self._assetschanged() - # Defaults - if sync_server_enabled: - main_splitter.setSizes([250, 1000, 550]) - self.resize(1800, 900) - else: - main_splitter.setSizes([250, 850, 200]) - self.resize(1300, 700) - self._first_show = True def resizeEvent(self, event): @@ -208,6 +207,10 @@ class LoaderWindow(QtWidgets.QDialog): 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 From 5b95e21c6f4da5683043895ae97222c10fafcae6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 22 Oct 2021 10:53:03 +0200 Subject: [PATCH 126/161] do not apply avalon style on loader and library loader --- openpype/tools/utils/host_tools.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index 599c25d6c8..2a64e23883 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -93,8 +93,6 @@ class HostToolsHelper: def show_loader(self, parent=None, use_context=None): """Loader tool for loading representations.""" - from avalon import style - loader_tool = self.get_loader_tool(parent) loader_tool.show() @@ -110,8 +108,6 @@ class HostToolsHelper: else: loader_tool.refresh() - loader_tool.setStyleSheet(style.load_stylesheet()) - def get_creator_tool(self, parent): """Create, cache and return creator tool window.""" if self._creator_tool is None: @@ -196,14 +192,11 @@ class HostToolsHelper: def show_library_loader(self, parent=None): """Loader tool for loading representations from library project.""" - from avalon import style - library_loader_tool = self.get_library_loader_tool(parent) library_loader_tool.show() library_loader_tool.raise_() library_loader_tool.activateWindow() library_loader_tool.refresh() - library_loader_tool.setStyleSheet(style.load_stylesheet()) def show_publish(self, parent=None): """Publish UI.""" From 600c94f464d93edded5927873b86379838708a12 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 22 Oct 2021 11:49:15 +0200 Subject: [PATCH 127/161] fix double slashes --- openpype/style/style.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index 3f006fb845..6921a786f3 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -425,20 +425,20 @@ 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; } From c85fb71103c22a2be372a1e6ea1c55ed4522c1d1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 22 Oct 2021 12:26:12 +0200 Subject: [PATCH 128/161] qlineargradient are single line --- openpype/style/style.css | 69 +++++++++++----------------------------- 1 file changed, 19 insertions(+), 50 deletions(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index 6921a786f3..8e9827084e 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -205,39 +205,23 @@ QSplitter::handle { } QSplitter::handle:horizontal { - 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) - ); + /* 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 { - 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) - ); + /* 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 { - 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) - ); + /* 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 { - 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) - ); + /* 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 */ @@ -264,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; } @@ -284,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*/ @@ -307,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 { @@ -457,12 +430,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 */ From 3158710d43f06fd5e2793d84483ba7770c2a2753 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 22 Oct 2021 12:28:58 +0200 Subject: [PATCH 129/161] removed fixed height of footer widget --- openpype/tools/loader/app.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index bbf6719af5..dac5e11d4c 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -124,19 +124,17 @@ class LoaderWindow(QtWidgets.QDialog): else: main_splitter.setSizes([250, 850, 200]) - # TODO keep footer size by message size footer_widget = QtWidgets.QWidget(self) - footer_widget.setFixedHeight(20) message_label = QtWidgets.QLabel(footer_widget) - footer_layout = QtWidgets.QVBoxLayout(footer_widget) + footer_layout = QtWidgets.QHBoxLayout(footer_widget) footer_layout.setContentsMargins(0, 0, 0, 0) - footer_layout.addWidget(message_label) + footer_layout.addWidget(message_label, 1) layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(main_splitter) - layout.addWidget(footer_widget) + layout.addWidget(main_splitter, 1) + layout.addWidget(footer_widget, 0) self.data = { "state": { From 01d059a993af1e486bedb8ae051299f1cbcab07c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 22 Oct 2021 12:31:52 +0200 Subject: [PATCH 130/161] added transparent image to resources --- openpype/style/images/transparent.png | Bin 0 -> 69 bytes openpype/style/pyqt5_resources.py | 745 +++++++++++++------------- openpype/style/pyside2_resources.py | 602 +++++++++++---------- openpype/style/resources.qrc | 1 + 4 files changed, 679 insertions(+), 669 deletions(-) create mode 100644 openpype/style/images/transparent.png diff --git a/openpype/style/images/transparent.png b/openpype/style/images/transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..0f2e143b39a2e37e52841ff55d410a2000125eca GIT binary patch literal 69 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1SBVv2j2ryJf1F&AsjQ46A}`DJQfDV#ayC~ Pfh-13S3j3^P6 \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\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\ @@ -62,18 +36,136 @@ HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ 200B\x98\x10AFC\x14\x13P\xb5\xa3\x01\x00\ \xd6\x10\x07\xd2/H\xdfJ\x00\x00\x00\x00IEND\ \xaeB`\x82\ -\x00\x00\x00\x9f\ +\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\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\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\ +\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\xa0\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -86,6 +178,31 @@ HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ \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\ @@ -214,19 +331,57 @@ 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\xa6\ +\x00\x00\x00\x9e\ \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\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\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\ +\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\ @@ -234,11 +389,23 @@ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ \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\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\ @@ -357,146 +524,28 @@ 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\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\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\ +\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\xa5\ +\x00\x00\x00\xa0\ \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\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\x9cS4\xfc]\x00\x00\x00\x09p\ +\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\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\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\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\ @@ -622,55 +671,6 @@ v)`\x8b\x07>\xa8\xe6\xd1\xfe\x0b\x9d\x85\x8eW\x0d\ ^x\xa2\x9e\x0e\xa7 tG9\x1d\xf6\xe1\x95+\xd6\ \xb1D\x8e\x0e\xcbX\xf0\x0fR\x8ay\x18\xdc\xe2\x02p\ \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\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\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\ @@ -695,16 +695,6 @@ qt_resource_name = b"\ \x07\x03}\xc3\ \x00i\ \x00m\x00a\x00g\x00e\x00s\ -\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\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\x0e\ \x0e\xde\xfa\xc7\ \x00l\ @@ -714,62 +704,35 @@ qt_resource_name = b"\ \x00d\ \x00o\x00w\x00n\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\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\x12\ -\x03\x8d\x04G\ -\x00r\ -\x00i\x00g\x00h\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\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\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\x0f\ -\x06S%\xa7\ -\x00b\ -\x00r\x00a\x00n\x00c\x00h\x00_\x00o\x00p\x00e\x00n\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\x12\ \x05\x8f\x9d\x07\ \x00b\ \x00r\x00a\x00n\x00c\x00h\x00_\x00o\x00p\x00e\x00n\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\x18\ -\x03\x8e\xdeg\ +\x00\x12\ +\x03\x8d\x04G\ \x00r\ -\x00i\x00g\x00h\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\ -\x00l\x00e\x00d\x00.\x00p\x00n\x00g\ +\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\ @@ -779,6 +742,43 @@ qt_resource_name = b"\ \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\ @@ -791,34 +791,30 @@ qt_resource_struct = b"\ \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\x03,\x00\x00\x00\x00\x00\x01\x00\x00&\xf0\ -\x00\x00\x00\xb6\x00\x00\x00\x00\x00\x01\x00\x00\x01\xfd\ -\x00\x00\x01\xe2\x00\x00\x00\x00\x00\x01\x00\x00\x14&\ -\x00\x00\x02\xb6\x00\x00\x00\x00\x00\x01\x00\x00%\x02\ -\x00\x00\x01\x9a\x00\x00\x00\x00\x00\x01\x00\x00\x0cx\ +\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\x00X\x00\x00\x00\x00\x00\x01\x00\x00\x00\xaa\ -\x00\x00\x01\x0e\x00\x00\x00\x00\x00\x01\x00\x00\x03I\ -\x00\x00\x02\x80\x00\x00\x00\x00\x00\x01\x00\x00$^\ -\x00\x00\x018\x00\x00\x00\x00\x00\x01\x00\x00\x03\xed\ -\x00\x00\x02\x0c\x00\x00\x00\x00\x00\x01\x00\x00\x14\xd0\ -\x00\x00\x02.\x00\x00\x00\x00\x00\x01\x00\x00\x15y\ -\x00\x00\x01\xbe\x00\x00\x00\x00\x00\x01\x00\x00\x0d\x1c\ -\x00\x00\x02\xda\x00\x00\x00\x00\x00\x01\x00\x00%\xa4\ -\x00\x00\x02X\x00\x00\x00\x00\x00\x01\x00\x00\x1c\xad\ -\x00\x00\x01f\x00\x00\x00\x00\x00\x01\x00\x00\x0b\xce\ -\x00\x00\x02\xf8\x00\x00\x00\x00\x00\x01\x00\x00&F\ -\x00\x00\x00\x94\x00\x00\x00\x00\x00\x01\x00\x00\x01S\ -\x00\x00\x00\xde\x00\x00\x00\x00\x00\x01\x00\x00\x02\xa6\ +\x00\x00\x02X\x00\x00\x00\x00\x00\x01\x00\x00\x16D\ " - def qInitResources(): - QtCore.qRegisterResourceData( - 0x01, qt_resource_struct, qt_resource_name, qt_resource_data - ) - + QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) def qCleanupResources(): - QtCore.qUnregisterResourceData( - 0x01, qt_resource_struct, qt_resource_name, qt_resource_data - ) + QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + +qInitResources() 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 From 536c6383715627a7778927f58840db3f71d84d69 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 22 Oct 2021 12:32:09 +0200 Subject: [PATCH 131/161] use transparent image to hide branches in tree view --- openpype/style/style.css | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/openpype/style/style.css b/openpype/style/style.css index 8e9827084e..f8a61cbbd3 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -415,6 +415,25 @@ QAbstractItemView::branch:closed:has-children:has-siblings:hover { 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}; From 140c290c607ad0dd87cce1e5f44afce38091c0ef Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 22 Oct 2021 12:32:19 +0200 Subject: [PATCH 132/161] set down/up arrow for header --- openpype/style/style.css | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/style/style.css b/openpype/style/style.css index f8a61cbbd3..d6f2460a27 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -340,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}; From d4b4d2e8417e267a6381cf0fd883910b3b4bf5ad Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 22 Oct 2021 12:40:12 +0200 Subject: [PATCH 133/161] fix formatting --- openpype/style/pyside2_resources.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/style/pyside2_resources.py b/openpype/style/pyside2_resources.py index e4bbc50533..97ee781c5d 100644 --- a/openpype/style/pyside2_resources.py +++ b/openpype/style/pyside2_resources.py @@ -812,9 +812,13 @@ qt_resource_struct = b"\ " def qInitResources(): - QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + QtCore.qRegisterResourceData( + 0x01, qt_resource_struct, qt_resource_name, qt_resource_data + ) def qCleanupResources(): - QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + QtCore.qUnregisterResourceData( + 0x01, qt_resource_struct, qt_resource_name, qt_resource_data + ) qInitResources() From c39c3ecb045629160d809fd3e25bdb3bad173ea1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 22 Oct 2021 12:40:38 +0200 Subject: [PATCH 134/161] do not call pyside initialization on import --- openpype/style/pyside2_resources.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/style/pyside2_resources.py b/openpype/style/pyside2_resources.py index 97ee781c5d..dff01eec49 100644 --- a/openpype/style/pyside2_resources.py +++ b/openpype/style/pyside2_resources.py @@ -811,14 +811,14 @@ qt_resource_struct = b"\ \x00\x00\x02X\x00\x00\x00\x00\x00\x01\x00\x00\x16D\ " + def qInitResources(): QtCore.qRegisterResourceData( 0x01, qt_resource_struct, qt_resource_name, qt_resource_data ) + def qCleanupResources(): QtCore.qUnregisterResourceData( 0x01, qt_resource_struct, qt_resource_name, qt_resource_data ) - -qInitResources() From 765fcf701434a19c3ff4e0401a0bb904e6e8f1f5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 22 Oct 2021 14:24:25 +0200 Subject: [PATCH 135/161] fix typo in label --- openpype/tools/experimental_tools/tools_def.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/experimental_tools/tools_def.py b/openpype/tools/experimental_tools/tools_def.py index 3657c2385b..254f542c4d 100644 --- a/openpype/tools/experimental_tools/tools_def.py +++ b/openpype/tools/experimental_tools/tools_def.py @@ -72,7 +72,7 @@ class ExperimentalTools: # experimental_tools = [ # ExperimentalTool( # "example", - # "Exmaple experimental tool", + # "Example experimental tool", # example_callback, # "Example tool tooltip." # ) From dc5f2221b9c6d838ef383c648bea66e8fd0e2b69 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 22 Oct 2021 14:34:43 +0200 Subject: [PATCH 136/161] added label describing how to turn onoff experimental tools --- openpype/tools/experimental_tools/dialog.py | 39 +++++++++++++++++---- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/openpype/tools/experimental_tools/dialog.py b/openpype/tools/experimental_tools/dialog.py index c7c8ce83fc..0fd170b31e 100644 --- a/openpype/tools/experimental_tools/dialog.py +++ b/openpype/tools/experimental_tools/dialog.py @@ -30,6 +30,7 @@ class ExperimentalToolsDialog(QtWidgets.QDialog): 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( @@ -49,16 +50,42 @@ class ExperimentalToolsDialog(QtWidgets.QDialog): empty_layout.addStretch(1) empty_layout.addLayout(empty_btns_layout) - content_widget = QtWidgets.QWidget(self) + # Content of Experimental tools - content_layout = QtWidgets.QVBoxLayout(content_widget) + # 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(content_widget, 1) + layout.addWidget(tool_btns_widget, 1) refresh_timer = QtCore.QTimer() refresh_timer.setInterval(self.refresh_interval) @@ -67,7 +94,7 @@ class ExperimentalToolsDialog(QtWidgets.QDialog): ok_btn.clicked.connect(self._on_ok_click) self._empty_widget = empty_widget - self._content_widget = content_widget + self._tool_btns_widget = tool_btns_widget self._content_layout = content_layout self._experimental_tools = experimental_tools @@ -94,7 +121,7 @@ class ExperimentalToolsDialog(QtWidgets.QDialog): button = self._buttons_by_tool_identifier[identifier] else: is_new = True - button = ToolButton(identifier, self) + 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) @@ -128,7 +155,7 @@ class ExperimentalToolsDialog(QtWidgets.QDialog): def _set_visibility(self): content_visible = self._is_content_visible() - self._content_widget.setVisible(content_visible) + self._tool_btns_widget.setVisible(content_visible) self._empty_widget.setVisible(not content_visible) def _on_ok_click(self): From e320065986f25b5b8a814b59a56363edef1548b2 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 23 Oct 2021 03:40:10 +0000 Subject: [PATCH 137/161] [Automated] Bump version --- CHANGELOG.md | 51 +++++++++++++++++++++++++-------------------- openpype/version.py | 2 +- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95792f8a7a..eca2a8b423 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,34 @@ # Changelog +## [3.5.1-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) +- 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** + +- 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 +92,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) @@ -81,9 +103,7 @@ - Settings for Nuke IncrementScriptVersion [\#2039](https://github.com/pypeclub/OpenPype/pull/2039) - 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 +121,12 @@ [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) +- WebserverModule: Removed interface of webserver module [\#2028](https://github.com/pypeclub/OpenPype/pull/2028) - 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/version.py b/openpype/version.py index d88d79b995..49b61c755e 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.5.1-nightly.1" From db7c8b69909472e2a8de435c2518731e9a53cc25 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 25 Oct 2021 09:22:38 +0200 Subject: [PATCH 138/161] fix Ci expected format in pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index dade0a2f57..c49ecabdab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.0.0" +version = "3.5.1-nightly.1" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 9b529ce589c4b5e4cc80ce949677372146dcf1ee Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 25 Oct 2021 17:56:40 +0200 Subject: [PATCH 139/161] OP-1206 - fix no repres_widget issue without sync server --- openpype/tools/loader/app.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index dac5e11d4c..04da08326f 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -164,8 +164,9 @@ class LoaderWindow(QtWidgets.QDialog): subsets_widget.load_started.connect(self._on_load_start) subsets_widget.load_ended.connect(self._on_load_end) - repres_widget.load_started.connect(self._on_load_start) - repres_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 From db26055d563af7a92670aabd1c26121928527b80 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 25 Oct 2021 19:17:40 +0200 Subject: [PATCH 140/161] use plist info to get full path to executable --- openpype/lib/applications.py | 48 ++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 7 deletions(-) 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] From 5107689f64122f67f41368eec666b1737780675f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 26 Oct 2021 10:40:24 +0200 Subject: [PATCH 141/161] PYPE-1901 - fix multiple mapping configuration --- .../photoshop/plugins/publish/collect_remote_instances.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py index 9bb8e90350..12f9fa5ab5 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py @@ -106,12 +106,12 @@ class CollectRemoteInstances(pyblish.api.ContextPlugin): for mapping in self.color_code_mapping: if mapping["color_code"] and \ layer.color_code not in mapping["color_code"]: - break + continue if mapping["layer_name_regex"] and \ not any(re.search(pattern, layer.name) for pattern in mapping["layer_name_regex"]): - break + continue family_list.append(mapping["family"]) subset_name_list.append(mapping["subset_template_name"]) @@ -127,7 +127,6 @@ class CollectRemoteInstances(pyblish.api.ContextPlugin): 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: From 5b4f266eb07bf57355e408bf34a35ad405087c29 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 26 Oct 2021 16:17:34 +0200 Subject: [PATCH 142/161] Hound and Review comments --- openpype/hosts/flame/__init__.py | 4 +- openpype/hosts/flame/api/lib.py | 192 ++++++++---------- openpype/hosts/flame/api/menu.py | 2 +- openpype/hosts/flame/api/utils.py | 12 +- openpype/hosts/flame/hooks/pre_flame_setup.py | 66 +++--- .../hosts/flame/utility_scripts/flame_hook.py | 4 +- 6 files changed, 137 insertions(+), 143 deletions(-) diff --git a/openpype/hosts/flame/__init__.py b/openpype/hosts/flame/__init__.py index dc3d3e7cba..48e8dc86c9 100644 --- a/openpype/hosts/flame/__init__.py +++ b/openpype/hosts/flame/__init__.py @@ -24,7 +24,7 @@ from .api.lib import ( ) from .api.menu import ( - FlameMenuProjectconnect, + FlameMenuProjectConnect, FlameMenuTimeline ) @@ -90,7 +90,7 @@ __all__ = [ "create_bin", # menu - "FlameMenuProjectconnect", + "FlameMenuProjectConnect", "FlameMenuTimeline", # plugin diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index 9d24e94df8..a58b67d54a 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -1,56 +1,51 @@ import sys -import json -import re import os import pickle import contextlib -from pprint import pprint, pformat -from opentimelineio import opentime -import openpype - - -# from ..otio import davinci_export as otio_export +from pprint import pformat from openpype.api import Logger log = Logger().get_logger(__name__) -self = sys.modules[__name__] -self.project_manager = None -self.media_storage = None -# OpenPype sequencial rename variables -self.rename_index = 0 -self.rename_add = 0 +@contextlib.contextmanager +def load_preferences_file(klass, filepath, attribute): + try: + with open(filepath, "r") as prefs_file: + setattr(klass, attribute, pickle.load(prefs_file)) -self.publish_clip_color = "Pink" -self.pype_marker_workflow = True + yield -# OpenPype compound clip workflow variable -self.pype_tag_name = "VFX Notes" + except IOError: + klass.log.info("Unable to load preferences from {}".format( + filepath)) -# OpenPype marker workflow variables -self.pype_marker_name = "OpenPypeData" -self.pype_marker_duration = 1 -self.pype_marker_color = "Mint" -self.temp_marker_frame = None + finally: + klass.log.info("Preferences loaded from {}".format(filepath)) -# OpenPype default timeline -self.pype_timeline_name = "OpenPypeTimeline" + +@contextlib.contextmanager +def save_preferences_file(klass, filepath, attribute): + try: + with open(filepath, "w") as prefs_file: + attr = getattr(klass, attribute) + pickle.dump(attr, prefs_file) + + yield + + except IOError: + klass.log.info("Unable to save preferences to {}".format( + filepath)) + + finally: + klass.log.info("Preferences saved to {}".format(filepath)) class FlameAppFramework(object): # flameAppFramework class takes care of preferences class prefs_dict(dict): - # subclass of a dict() in order to directly link it - # to main framework prefs dictionaries - # when accessed directly it will operate on a dictionary under a "name" - # key in master dictionary. - # master = {} - # p = prefs(master, "app_name") - # p["key"] = "value" - # master - {"app_name": {"key", "value"}} def __init__(self, master, name, **kwargs): self.name = name @@ -85,7 +80,7 @@ class FlameAppFramework(object): def __contains__(self, k): return self.master[self.name].__contains__(k) - def copy(self): # don"t delegate w/ super - dict.copy() -> dict :( + def copy(self): # don"t delegate w/ super - dict.copy() -> dict :( return type(self)(self) def keys(self): @@ -96,7 +91,8 @@ class FlameAppFramework(object): return cls.master[cls.name].fromkeys(keys, v) def __repr__(self): - return "{0}({1})".format(type(self).__name__, self.master[self.name].__repr__()) + return "{0}({1})".format( + type(self).__name__, self.master[self.name].__repr__()) def master_keys(self): return self.master.keys() @@ -110,13 +106,12 @@ class FlameAppFramework(object): 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: + except Exception: self.flame = None self.flame_project_name = None self.flame_user_name = None @@ -127,10 +122,11 @@ class FlameAppFramework(object): if sys.platform == "darwin": self.prefs_folder = os.path.join( os.path.expanduser("~"), - "Library", - "Caches", - "OpenPype", - self.bundle_name) + "Library", + "Caches", + "OpenPype", + self.bundle_name + ) elif sys.platform.startswith("linux"): self.prefs_folder = os.path.join( os.path.expanduser("~"), @@ -157,89 +153,74 @@ class FlameAppFramework(object): self.apps = [] - def load_prefs(self): + def get_pref_file_paths(self): + prefix = self.prefs_folder + os.path.sep + self.bundle_name - prefs_file_path = (prefix + "." + self.flame_user_name + "." - + self.flame_project_name + ".prefs") - prefs_user_file_path = (prefix + "." + self.flame_user_name - + ".prefs") + 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" - try: - with open(prefs_file_path, "r") as prefs_file: - self.prefs = pickle.load(prefs_file) + return (prefs_file_path, prefs_user_file_path, prefs_global_file_path) - self.log.info("preferences loaded from {}".format(prefs_file_path)) - self.log.info("preferences contents:\n" + pformat(self.prefs)) - except: - self.log.info("unable to load preferences from {}".format( - prefs_file_path)) + def load_prefs(self): - try: - with open(prefs_user_file_path, "r") as prefs_file: - self.prefs_user = pickle.load(prefs_file) - self.log.info("preferences loaded from {}".format( - prefs_user_file_path)) - self.log.info("preferences contents:\n" + pformat(self.prefs_user)) - except: - self.log.info("unable to load preferences from {}".format( - prefs_user_file_path)) + (proj_pref_path, user_pref_path, + glob_pref_path) = self.get_pref_file_paths() - try: - with open(prefs_global_file_path, "r") as prefs_file: - self.prefs_global = pickle.load(prefs_file) - self.log.info("preferences loaded from {}".format( - prefs_global_file_path)) - self.log.info("preferences contents:\n" + pformat(self.prefs_global)) + with load_preferences_file(self, proj_pref_path, "prefs"): + self.log.info( + "Project - preferences contents:\n{}".format( + pformat(self.prefs) + )) - except: - self.log.info("unable to load preferences from {}".format( - prefs_global_file_path)) + with load_preferences_file(self, user_pref_path, "prefs_user"): + self.log.info( + "User - preferences contents:\n{}".format( + pformat(self.prefs_user) + )) + + with load_preferences_file(self, glob_pref_path, "prefs_global"): + self.log.info( + "Global - preferences contents:\n{}".format( + pformat(self.prefs_global) + )) return True def save_prefs(self): - import pickle - + # make sure the preference folder is available if not os.path.isdir(self.prefs_folder): try: os.makedirs(self.prefs_folder) - except: - self.log.info("unable to create folder {}".format( + except Exception: + self.log.info("Unable to create folder {}".format( self.prefs_folder)) return False - prefix = self.prefs_folder + os.path.sep + self.bundle_name - prefs_file_path = prefix + "." + self.flame_user_name + "." + self.flame_project_name + ".prefs" - prefs_user_file_path = prefix + "." + self.flame_user_name + ".prefs" - prefs_global_file_path = prefix + ".prefs" + # get all pref file paths + (proj_pref_path, user_pref_path, + glob_pref_path) = self.get_pref_file_paths() - try: - prefs_file = open(prefs_file_path, "w") - pickle.dump(self.prefs, prefs_file) - prefs_file.close() - self.log.info("preferences saved to {}".format(prefs_file_path)) - self.log.info("preferences contents:\n" + pformat(self.prefs)) - except: - self.log.info("unable to save preferences to {}".format(prefs_file_path)) + with save_preferences_file(self, proj_pref_path, "prefs"): + self.log.info( + "Project - preferences contents:\n{}".format( + pformat(self.prefs) + )) - try: - prefs_file = open(prefs_user_file_path, "w") - pickle.dump(self.prefs_user, prefs_file) - prefs_file.close() - self.log.info("preferences saved to {}".format(prefs_user_file_path)) - self.log.info("preferences contents:\n" + pformat(self.prefs_user)) - except: - self.log.info("unable to save preferences to {}".format(prefs_user_file_path)) + with save_preferences_file(self, user_pref_path, "prefs_user"): + self.log.info( + "User - preferences contents:\n{}".format( + pformat(self.prefs_user) + )) - try: - prefs_file = open(prefs_global_file_path, "w") - pickle.dump(self.prefs_global, prefs_file) - prefs_file.close() - self.log.info("preferences saved to {}".format(prefs_global_file_path)) - self.log.info("preferences contents:\n" + pformat(self.prefs_global)) - except: - self.log.info("unable to save preferences to {}".format(prefs_global_file_path)) + with save_preferences_file(self, glob_pref_path, "prefs_global"): + self.log.info( + "Global - preferences contents:\n{}".format( + pformat(self.prefs_global) + )) return True @@ -263,6 +244,7 @@ def maintain_current_timeline(to_timeline, from_timeline=None): >>> print(get_current_timeline().GetName()) timeline1 """ + # todo: this is still Resolve's implementation project = get_current_project() working_timeline = from_timeline or project.GetCurrentTimeline() @@ -306,5 +288,5 @@ def rescan_hooks(): import flame try: flame.execute_shortcut('Rescan Python Hooks') - except: + except Exception: pass diff --git a/openpype/hosts/flame/api/menu.py b/openpype/hosts/flame/api/menu.py index 65d1535beb..184881c6a7 100644 --- a/openpype/hosts/flame/api/menu.py +++ b/openpype/hosts/flame/api/menu.py @@ -70,7 +70,7 @@ class _FlameMenuApp(object): self.log.info('Rescan Python Hooks') -class FlameMenuProjectconnect(_FlameMenuApp): +class FlameMenuProjectConnect(_FlameMenuApp): # flameMenuProjectconnect app takes care of the preferences dialog as well diff --git a/openpype/hosts/flame/api/utils.py b/openpype/hosts/flame/api/utils.py index 489b51e37c..bd321e194c 100644 --- a/openpype/hosts/flame/api/utils.py +++ b/openpype/hosts/flame/api/utils.py @@ -1,5 +1,3 @@ -#! python3 - """ Resolve's tools for setting environment """ @@ -9,7 +7,6 @@ import shutil from openpype.api import Logger log = Logger().get_logger(__name__) - def _sync_utility_scripts(env=None): """ Synchronizing basic utlility scripts for resolve. @@ -20,8 +17,7 @@ def _sync_utility_scripts(env=None): """ from .. import HOST_DIR - if not env: - env = os.environ + env = env or os.environ # initiate inputs scripts = {} @@ -41,7 +37,8 @@ def _sync_utility_scripts(env=None): # 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())) + log.warning("Path is not a valid dir: `{_dirpath}`".format( + **locals())) continue fsd_paths.append(_dirpath) @@ -82,8 +79,7 @@ def _sync_utility_scripts(env=None): def setup(env=None): """ Wrapper installer started from pype.hooks.resolve.FlamePrelaunch() """ - if not env: - env = os.environ + env = env or os.environ # synchronize resolve utility scripts _sync_utility_scripts(env) diff --git a/openpype/hosts/flame/hooks/pre_flame_setup.py b/openpype/hosts/flame/hooks/pre_flame_setup.py index aec9a15e30..368a70f395 100644 --- a/openpype/hosts/flame/hooks/pre_flame_setup.py +++ b/openpype/hosts/flame/hooks/pre_flame_setup.py @@ -1,6 +1,7 @@ import os import json import tempfile +import contextlib from openpype.lib import ( PreLaunchHook, get_openpype_username) from openpype.hosts import flame as opflame @@ -76,41 +77,56 @@ class FlamePrelaunch(PreLaunchHook): # 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(dumped_script_data) + temporary_json_file.write(data) temporary_json_file.close() temporary_json_filepath = temporary_json_file.name.replace( "\\", "/" ) - # Prepare subprocess arguments - args = [ - self.flame_python_exe, - self.wtc_script_path, - temporary_json_filepath - ] - self.log.info("Executing: {}".format(" ".join(args))) + yield temporary_json_filepath - 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(temporary_json_filepath).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") + 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) - - return app_args diff --git a/openpype/hosts/flame/utility_scripts/flame_hook.py b/openpype/hosts/flame/utility_scripts/flame_hook.py index b46109a609..2233b97d32 100644 --- a/openpype/hosts/flame/utility_scripts/flame_hook.py +++ b/openpype/hosts/flame/utility_scripts/flame_hook.py @@ -55,7 +55,7 @@ atexit.register(cleanup) def load_apps(): - opflame.apps.append(opflame.FlameMenuProjectconnect(opflame.app_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") @@ -121,7 +121,7 @@ def get_main_menu_custom_ui_actions(): # install openpype and the host openpype_install() - return _build_app_menu("FlameMenuProjectconnect") + return _build_app_menu("FlameMenuProjectConnect") def get_timeline_custom_ui_actions(): From f6076af2ad5e513eb1fa7f6fd22c00d1e3161810 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 26 Oct 2021 16:26:34 +0200 Subject: [PATCH 143/161] Hound and review comments --- openpype/hosts/flame/api/menu.py | 31 ++++++++++++++++++---------- openpype/hosts/flame/api/pipeline.py | 7 ------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/flame/api/menu.py b/openpype/hosts/flame/api/menu.py index 184881c6a7..b4f1728acf 100644 --- a/openpype/hosts/flame/api/menu.py +++ b/openpype/hosts/flame/api/menu.py @@ -1,19 +1,28 @@ import os -import sys -from Qt import QtWidgets, QtCore -from pprint import pprint, pformat +from Qt import QtWidgets from copy import deepcopy -from .lib import rescan_hooks 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'} + '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' + } } @@ -31,7 +40,7 @@ class _FlameMenuApp(object): try: import flame self.flame = flame - except: + except ImportError: self.flame = None self.flame_project_name = flame.project.current_project.name @@ -62,7 +71,7 @@ class _FlameMenuApp(object): try: import flame self.flame = flame - except: + except ImportError: self.flame = None if self.flame: @@ -130,7 +139,7 @@ class FlameMenuProjectConnect(_FlameMenuApp): try: import flame self.flame = flame - except: + except ImportError: self.flame = None if self.flame: @@ -191,7 +200,7 @@ class FlameMenuTimeline(_FlameMenuApp): try: import flame self.flame = flame - except: + except ImportError: self.flame = None if self.flame: diff --git a/openpype/hosts/flame/api/pipeline.py b/openpype/hosts/flame/api/pipeline.py index 297ab0e44c..85b9f7e24a 100644 --- a/openpype/hosts/flame/api/pipeline.py +++ b/openpype/hosts/flame/api/pipeline.py @@ -1,23 +1,16 @@ """ Basic avalon integration """ -import os import contextlib -from collections import OrderedDict -from avalon.tools import workfiles from avalon import api as avalon -from avalon import schema -from avalon.pipeline import AVALON_CONTAINER_ID from pyblish import api as pyblish from openpype.api import Logger -from . import lib AVALON_CONTAINERS = "AVALON_CONTAINERS" log = Logger().get_logger(__name__) - def install(): from .. import ( PUBLISH_PATH, From 97a405b5a119ba7822c062cb69111ba43e4a0342 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 26 Oct 2021 20:13:12 +0200 Subject: [PATCH 144/161] Hound suggestions --- openpype/hosts/flame/api/plugin.py | 10 ---------- openpype/hosts/flame/utility_scripts/flame_hook.py | 11 ++++------- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/flame/api/plugin.py b/openpype/hosts/flame/api/plugin.py index ac86c7c224..2a28a20a75 100644 --- a/openpype/hosts/flame/api/plugin.py +++ b/openpype/hosts/flame/api/plugin.py @@ -1,13 +1,3 @@ -import re -import uuid -from avalon import api -import openpype.api as pype -from openpype.hosts import resolve -from avalon.vendor import qargparse -from . import lib - -from Qt import QtWidgets, QtCore - # Creator plugin functions # Publishing plugin functions # Loader plugin functions diff --git a/openpype/hosts/flame/utility_scripts/flame_hook.py b/openpype/hosts/flame/utility_scripts/flame_hook.py index 2233b97d32..f482126624 100644 --- a/openpype/hosts/flame/utility_scripts/flame_hook.py +++ b/openpype/hosts/flame/utility_scripts/flame_hook.py @@ -1,6 +1,6 @@ import sys -from Qt import QtWidgets, QtCore -from pprint import pprint, pformat +from Qt import QtWidgets +from pprint import pformat import atexit import openpype import avalon @@ -81,11 +81,7 @@ except ImportError: def rescan_hooks(): - import flame - try: - flame.execute_shortcut('Rescan Python Hooks') - except: - pass + flame.execute_shortcut('Rescan Python Hooks') def _build_app_menu(app_name): @@ -112,6 +108,7 @@ def _build_app_menu(app_name): return menu + def project_saved(project_name, save_time, is_auto_save): if opflame.app_framework: opflame.app_framework.save_prefs() From deada2422e39182151285cfc03f032ea298842ad Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 26 Oct 2021 20:36:04 +0200 Subject: [PATCH 145/161] flame hook adding docstrings --- .../hosts/flame/utility_scripts/flame_hook.py | 77 ++++++++++++++++--- 1 file changed, 68 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/flame_hook.py b/openpype/hosts/flame/utility_scripts/flame_hook.py index f482126624..bce668a389 100644 --- a/openpype/hosts/flame/utility_scripts/flame_hook.py +++ b/openpype/hosts/flame/utility_scripts/flame_hook.py @@ -1,3 +1,4 @@ +from __future__ import print_function import sys from Qt import QtWidgets from pprint import pformat @@ -11,23 +12,32 @@ flh._project = None def openpype_install(): + """Registering OpenPype in context + """ openpype.install() avalon.api.install(opflame) - print("<<<<<<<<<<< Avalon registred hosts: {} >>>>>>>>>>>>>>>".format( + print("Avalon registred hosts: {}".format( avalon.api.registered_host())) # Exception handler -def exeption_handler(exctype, value, tb): +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, tb))) + pformat(traceback.format_exception(exctype, value, _traceback))) mbox.setStyleSheet('QLabel{min-width: 800px;}') mbox.exec_() - sys.__excepthook__(exctype, value, tb) + sys.__excepthook__(exctype, value, _traceback) # add exception handler into sys module @@ -36,12 +46,14 @@ 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( + 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)) + print('`{}` removing : {}'.format(__file__, app.name)) del app opflame.apps = [] @@ -55,24 +67,44 @@ 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(">> flame_hook.py: {} initializing".format( + 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 app_initialized(parent=None) @@ -85,7 +117,17 @@ def rescan_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: @@ -94,8 +136,6 @@ def _build_app_menu(app_name): if app: menu.append(app.build_menu()) - print(">>_> `{}` was build: {}".format(app_name, pformat(menu))) - if opflame.app_framework: menu_auto_refresh = opflame.app_framework.prefs_global.get( 'menu_auto_refresh', {}) @@ -109,12 +149,26 @@ def _build_app_menu(app_name): 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() @@ -122,6 +176,11 @@ def get_main_menu_custom_ui_actions(): def get_timeline_custom_ui_actions(): + """Hook to create submenu in timeline + + Returns: + list: menu object + """ # install openpype and the host openpype_install() From a2330d218c0e069d2ec4f3074c0a3e9bc2b415fa Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 26 Oct 2021 20:56:47 +0200 Subject: [PATCH 146/161] Hound suggestions --- openpype/hosts/flame/scripts/wiretap_com.py | 41 +++++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/flame/scripts/wiretap_com.py b/openpype/hosts/flame/scripts/wiretap_com.py index a5925d0546..d8dc1884cf 100644 --- a/openpype/hosts/flame/scripts/wiretap_com.py +++ b/openpype/hosts/flame/scripts/wiretap_com.py @@ -10,20 +10,26 @@ import xml.dom.minidom as minidom from copy import deepcopy import datetime -# Todo: this has to be replaced with somehting more dynamic -flame_python_path = "/opt/Autodesk/flame_2021/python" -flame_exe_path = "/opt/Autodesk/flame_2021/bin/flame.app/Contents/MacOS/startApp" +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) + sys.path.append(flame_python_path) + + from libwiretapPythonClientAPI import ( + WireTapClientInit, + WireTapClientUninit, + WireTapNodeHandle, + WireTapServerHandle, + WireTapInt, + WireTapStr + ) -from libwiretapPythonClientAPI import ( - WireTapClientInit, - WireTapClientUninit, - WireTapNodeHandle, - WireTapServerHandle, - WireTapInt, - WireTapStr -) class WireTapCom(object): """ @@ -231,7 +237,8 @@ class WireTapCom(object): if not get_children_name: raise AttributeError( - "Unable to get child name: {}".format(child_obj.lastError()) + "Unable to get child name: {}".format( + child_obj.lastError()) ) volumes.append(node_name.c_str()) @@ -256,11 +263,12 @@ class WireTapCom(object): 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 + # 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 + 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) @@ -314,7 +322,8 @@ class WireTapCom(object): if not get_children_name: raise AttributeError( - "Unable to get child name: {}".format(child_obj.lastError()) + "Unable to get child name: {}".format( + child_obj.lastError()) ) usernames.append(node_name.c_str()) From 3b67b7d4849c5910eceaa0f521a7fb92d43634b3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 26 Oct 2021 20:57:11 +0200 Subject: [PATCH 147/161] flame hook: renaming to something more appropriate --- .../utility_scripts/{flame_hook.py => openpype_in_flame.py} | 3 +++ 1 file changed, 3 insertions(+) rename openpype/hosts/flame/utility_scripts/{flame_hook.py => openpype_in_flame.py} (99%) diff --git a/openpype/hosts/flame/utility_scripts/flame_hook.py b/openpype/hosts/flame/utility_scripts/openpype_in_flame.py similarity index 99% rename from openpype/hosts/flame/utility_scripts/flame_hook.py rename to openpype/hosts/flame/utility_scripts/openpype_in_flame.py index bce668a389..afbd44afc9 100644 --- a/openpype/hosts/flame/utility_scripts/flame_hook.py +++ b/openpype/hosts/flame/utility_scripts/openpype_in_flame.py @@ -105,6 +105,7 @@ 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 app_initialized(parent=None) @@ -151,6 +152,8 @@ def _build_app_menu(app_name): """ Flame hooks are starting here """ + + def project_saved(project_name, save_time, is_auto_save): """Hook to activate when project is saved From 65d860d3f8cc54d86ca8c9f5acb63a68571866a8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 26 Oct 2021 21:06:55 +0200 Subject: [PATCH 148/161] Flame: custom script dirs variable changed to plural --- openpype/hosts/flame/api/utils.py | 10 ++++++---- .../defaults/system_settings/applications.json | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/flame/api/utils.py b/openpype/hosts/flame/api/utils.py index bd321e194c..daebea32a7 100644 --- a/openpype/hosts/flame/api/utils.py +++ b/openpype/hosts/flame/api/utils.py @@ -1,5 +1,5 @@ """ -Resolve's tools for setting environment +Flame utils for syncing scripts """ import os @@ -7,8 +7,9 @@ import shutil from openpype.api import Logger log = Logger().get_logger(__name__) + def _sync_utility_scripts(env=None): - """ Synchronizing basic utlility scripts for resolve. + """ 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 @@ -21,7 +22,7 @@ def _sync_utility_scripts(env=None): # initiate inputs scripts = {} - fsd_env = env.get("FLAME_SCRIPT_DIR", "") + fsd_env = env.get("FLAME_SCRIPT_DIRS", "") flame_shared_dir = "/opt/Autodesk/shared/python" fsd_paths = [os.path.join( @@ -77,7 +78,8 @@ def _sync_utility_scripts(env=None): def setup(env=None): - """ Wrapper installer started from pype.hooks.resolve.FlamePrelaunch() + """ Wrapper installer started from + `flame/hooks/pre_flame_setup.py` """ env = env or os.environ diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 8aa7f3c1a3..2866673f4b 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -103,7 +103,7 @@ "icon": "{}/app_icons/flame.png", "host_name": "flame", "environment": { - "FLAME_SCRIPT_DIR": { + "FLAME_SCRIPT_DIRS": { "windows": "", "darvin": "", "linux": "" From 25557e7be8a7058f727acc4cdb89bb48a897b234 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 26 Oct 2021 21:07:06 +0200 Subject: [PATCH 149/161] hound suggestion --- openpype/hosts/flame/api/workio.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/flame/api/workio.py b/openpype/hosts/flame/api/workio.py index 00fcdb9405..d2e2408798 100644 --- a/openpype/hosts/flame/api/workio.py +++ b/openpype/hosts/flame/api/workio.py @@ -2,10 +2,10 @@ import os from openpype.api import Logger -from .. import ( - get_project_manager, - get_current_project -) +# from .. import ( +# get_project_manager, +# get_current_project +# ) log = Logger().get_logger(__name__) From d01ffd9373a0cc2f5196804da9e53f4c660e80ec Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 26 Oct 2021 21:24:11 +0200 Subject: [PATCH 150/161] flame: fixing contextmanager for save and load preferences file --- openpype/hosts/flame/api/lib.py | 48 +++++++++++---------------------- 1 file changed, 16 insertions(+), 32 deletions(-) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index a58b67d54a..2b3396c420 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -10,37 +10,15 @@ log = Logger().get_logger(__name__) @contextlib.contextmanager -def load_preferences_file(klass, filepath, attribute): +def io_preferences_file(klass, filepath, write=False): try: - with open(filepath, "r") as prefs_file: - setattr(klass, attribute, pickle.load(prefs_file)) - - yield + flag = "w" if write else "r" + yield open(filepath, flag) except IOError: - klass.log.info("Unable to load preferences from {}".format( + klass.log.info("Unable to work with preferences `{}`".format( filepath)) - finally: - klass.log.info("Preferences loaded from {}".format(filepath)) - - -@contextlib.contextmanager -def save_preferences_file(klass, filepath, attribute): - try: - with open(filepath, "w") as prefs_file: - attr = getattr(klass, attribute) - pickle.dump(attr, prefs_file) - - yield - - except IOError: - klass.log.info("Unable to save preferences to {}".format( - filepath)) - - finally: - klass.log.info("Preferences saved to {}".format(filepath)) - class FlameAppFramework(object): # flameAppFramework class takes care of preferences @@ -170,19 +148,22 @@ class FlameAppFramework(object): (proj_pref_path, user_pref_path, glob_pref_path) = self.get_pref_file_paths() - with load_preferences_file(self, proj_pref_path, "prefs"): + 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 load_preferences_file(self, user_pref_path, "prefs_user"): + 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 load_preferences_file(self, glob_pref_path, "prefs_global"): + 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) @@ -204,19 +185,22 @@ class FlameAppFramework(object): (proj_pref_path, user_pref_path, glob_pref_path) = self.get_pref_file_paths() - with save_preferences_file(self, proj_pref_path, "prefs"): + 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 save_preferences_file(self, user_pref_path, "prefs_user"): + 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 save_preferences_file(self, glob_pref_path, "prefs_global"): + 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) From 1d8acaa45dbd01d2dff4c014958ccfd1fc96a877 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 26 Oct 2021 21:36:40 +0200 Subject: [PATCH 151/161] Flame debugging `io_preferences_file` --- openpype/hosts/flame/api/lib.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index 2b3396c420..48331dcbc2 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -15,9 +15,9 @@ def io_preferences_file(klass, filepath, write=False): flag = "w" if write else "r" yield open(filepath, flag) - except IOError: - klass.log.info("Unable to work with preferences `{}`".format( - filepath)) + except IOError as _error: + klass.log.info("Unable to work with preferences `{}`: {}".format( + filepath, _error)) class FlameAppFramework(object): From 211fc22e090253d0be7b971dccd9a87f369f485d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 26 Oct 2021 21:36:57 +0200 Subject: [PATCH 152/161] Flame: cleaning Resolve mentioning --- openpype/hosts/flame/api/pipeline.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/flame/api/pipeline.py b/openpype/hosts/flame/api/pipeline.py index 85b9f7e24a..26dfe7c032 100644 --- a/openpype/hosts/flame/api/pipeline.py +++ b/openpype/hosts/flame/api/pipeline.py @@ -36,7 +36,7 @@ def install(): pyblish.register_host("flame") pyblish.register_plugin_path(PUBLISH_PATH) - log.info("Registering DaVinci Resovle plug-ins..") + log.info("Registering Flame plug-ins..") avalon.register_plugin_path(avalon.Loader, LOAD_PATH) avalon.register_plugin_path(avalon.Creator, CREATE_PATH) @@ -129,13 +129,13 @@ def on_pyblish_instance_toggled(instance, old_value, new_value): log.info("instance toggle: {}, old_value: {}, new_value:{} ".format( instance, old_value, new_value)) - from openpype.hosts.resolve import ( - set_publish_attribute - ) + # 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) + # # 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): From 86c528dd4a836f444d28f2a63fe84314660eec6f Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 27 Oct 2021 03:40:31 +0000 Subject: [PATCH 153/161] [Automated] Bump version --- CHANGELOG.md | 8 ++++---- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eca2a8b423..68409c4db8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [3.5.1-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.6.0-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.5.0...HEAD) @@ -8,11 +8,13 @@ - 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) @@ -103,7 +105,7 @@ - Settings for Nuke IncrementScriptVersion [\#2039](https://github.com/pypeclub/OpenPype/pull/2039) - 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) -- TimersManager: Removed interface of timers manager [\#2024](https://github.com/pypeclub/OpenPype/pull/2024) +- WebserverModule: Removed interface of webserver module [\#2028](https://github.com/pypeclub/OpenPype/pull/2028) **🐛 Bug fixes** @@ -125,8 +127,6 @@ - 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) -- WebserverModule: Removed interface of webserver module [\#2028](https://github.com/pypeclub/OpenPype/pull/2028) -- Console interpreter: Handle invalid sizes on initialization [\#2022](https://github.com/pypeclub/OpenPype/pull/2022) **🐛 Bug fixes** diff --git a/openpype/version.py b/openpype/version.py index 49b61c755e..6eb58f6fcc 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.5.1-nightly.1" +__version__ = "3.6.0-nightly.1" diff --git a/pyproject.toml b/pyproject.toml index c49ecabdab..1a112d2071 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.5.1-nightly.1" # OpenPype +version = "3.6.0-nightly.1" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From e8b1467800fcda662e83fce6a987d374b00ecaf1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 27 Oct 2021 12:28:53 +0200 Subject: [PATCH 154/161] PYPE-1901 - currently only image family is created in PS --- .../schemas/projects_schema/schema_project_photoshop.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 cd457ee21d..cec3f58460 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json @@ -81,7 +81,12 @@ { "key": "family", "label": "Resulting family", - "type": "text" + "type": "enum", + "enum_items": [ + { + "image": "image" + } + ] }, { "type": "text", From 82f1cb9a0f5716d8e6547eb95b4b42a52daba39d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Oct 2021 14:42:03 +0200 Subject: [PATCH 155/161] flame settings fixing darvin to darwin --- openpype/hosts/flame/api/utils.py | 2 +- .../defaults/system_settings/applications.json | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/flame/api/utils.py b/openpype/hosts/flame/api/utils.py index daebea32a7..4ae7157812 100644 --- a/openpype/hosts/flame/api/utils.py +++ b/openpype/hosts/flame/api/utils.py @@ -31,7 +31,7 @@ def _sync_utility_scripts(env=None): )] # collect script dirs - log.info("FLAME_SCRIPT_DIR: `{fsd_env}`".format(**locals())) + 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 diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 2866673f4b..79711f3067 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -105,7 +105,7 @@ "environment": { "FLAME_SCRIPT_DIRS": { "windows": "", - "darvin": "", + "darwin": "", "linux": "" } }, @@ -656,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": [ @@ -722,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": [ From c04a86ddbff933d6604672307265b221d9c68e30 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Oct 2021 14:54:17 +0200 Subject: [PATCH 156/161] Flame: adding filter to sync utility script --- openpype/hosts/flame/api/utils.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/hosts/flame/api/utils.py b/openpype/hosts/flame/api/utils.py index 4ae7157812..3a36b30784 100644 --- a/openpype/hosts/flame/api/utils.py +++ b/openpype/hosts/flame/api/utils.py @@ -47,12 +47,20 @@ def _sync_utility_scripts(env=None): 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("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 s in os.listdir(flame_shared_dir): + # skip all scripts and folders which are not maintained + if s not in remove_black_list: + continue + path = os.path.join(flame_shared_dir, s) log.info("Removing `{path}`...".format(**locals())) if os.path.isdir(path): From 432fa380f9ac15f6c7fb1d1a6dba99d9da1838dd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Oct 2021 15:38:06 +0200 Subject: [PATCH 157/161] Flame: adding exception for not maintained files during sync --- openpype/hosts/flame/api/utils.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/flame/api/utils.py b/openpype/hosts/flame/api/utils.py index 3a36b30784..0a3ee68815 100644 --- a/openpype/hosts/flame/api/utils.py +++ b/openpype/hosts/flame/api/utils.py @@ -51,17 +51,28 @@ def _sync_utility_scripts(env=None): 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 s in os.listdir(flame_shared_dir): + for _itm in os.listdir(flame_shared_dir): + skip = False + # skip all scripts and folders which are not maintained - if s not in remove_black_list: + 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, s) + path = os.path.join(flame_shared_dir, _itm) log.info("Removing `{path}`...".format(**locals())) if os.path.isdir(path): shutil.rmtree(path, onerror=None) From 40142effb6f353367110db4b82a24a6c6bb3ff11 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Oct 2021 15:38:17 +0200 Subject: [PATCH 158/161] Flame: missing import --- openpype/hosts/flame/utility_scripts/openpype_in_flame.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/flame/utility_scripts/openpype_in_flame.py b/openpype/hosts/flame/utility_scripts/openpype_in_flame.py index afbd44afc9..50ccbb521c 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_in_flame.py +++ b/openpype/hosts/flame/utility_scripts/openpype_in_flame.py @@ -114,6 +114,7 @@ except ImportError: def rescan_hooks(): + import flame flame.execute_shortcut('Rescan Python Hooks') From 1f5b8132edb7370afa7fa022ea51a4183ecb85eb Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Oct 2021 15:44:49 +0200 Subject: [PATCH 159/161] hound suggestions --- openpype/hosts/flame/api/utils.py | 6 +++--- openpype/hosts/flame/utility_scripts/openpype_in_flame.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/flame/api/utils.py b/openpype/hosts/flame/api/utils.py index 0a3ee68815..a750046362 100644 --- a/openpype/hosts/flame/api/utils.py +++ b/openpype/hosts/flame/api/utils.py @@ -59,15 +59,15 @@ def _sync_utility_scripts(env=None): 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 diff --git a/openpype/hosts/flame/utility_scripts/openpype_in_flame.py b/openpype/hosts/flame/utility_scripts/openpype_in_flame.py index 50ccbb521c..c5fa881f3c 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_in_flame.py +++ b/openpype/hosts/flame/utility_scripts/openpype_in_flame.py @@ -107,14 +107,14 @@ all menu objects as apps. """ try: - import flame + import flame # noqa app_initialized(parent=None) except ImportError: print("!!!! not able to import flame module !!!!") def rescan_hooks(): - import flame + import flame # noqa flame.execute_shortcut('Rescan Python Hooks') @@ -143,7 +143,7 @@ def _build_app_menu(app_name): 'menu_auto_refresh', {}) if menu_auto_refresh.get('timeline_menu', True): try: - import flame + import flame # noqa flame.schedule_idle_event(rescan_hooks) except ImportError: print("!-!!! not able to import flame module !!!!") From 6bbdd3c6b66b2c130c173e7adfdb8798738233e3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 29 Oct 2021 15:22:54 +0200 Subject: [PATCH 160/161] make representaions key optional --- .../standalonepublisher/plugins/publish/validate_sources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_sources.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_sources.py index da424cfb45..8baebcfc82 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_sources.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_sources.py @@ -22,7 +22,7 @@ class ValidateSources(pyblish.api.InstancePlugin): def process(self, instance): self.log.info("instance {}".format(instance.data)) - for repr in instance.data["representations"]: + for repr in instance.data.get("representations") or []: files = [] if isinstance(repr["files"], str): files.append(repr["files"]) From 1ca9edac7cdb59c9f4edbe3612bb08d8ee074f22 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 29 Oct 2021 15:23:05 +0200 Subject: [PATCH 161/161] changed 'repr' variable to 'repre' --- .../plugins/publish/validate_sources.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_sources.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_sources.py index 8baebcfc82..eec675e97f 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_sources.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_sources.py @@ -22,15 +22,15 @@ class ValidateSources(pyblish.api.InstancePlugin): def process(self, instance): self.log.info("instance {}".format(instance.data)) - for repr in instance.data.get("representations") or []: + for repre in instance.data.get("representations") or []: files = [] - if isinstance(repr["files"], str): - files.append(repr["files"]) + if isinstance(repre["files"], str): + files.append(repre["files"]) else: - files = list(repr["files"]) + files = list(repre["files"]) for file_name in files: - source_file = os.path.join(repr["stagingDir"], + source_file = os.path.join(repre["stagingDir"], file_name) if not os.path.exists(source_file):