From c238a9cbc15d516b1d2b662eb6b67aec5d341afa Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 24 Aug 2022 10:51:12 +0100 Subject: [PATCH 001/121] Fix maya extractor for instance_name --- openpype/hosts/maya/plugins/publish/extract_layout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_layout.py b/openpype/hosts/maya/plugins/publish/extract_layout.py index 991217684a..92ca6c883f 100644 --- a/openpype/hosts/maya/plugins/publish/extract_layout.py +++ b/openpype/hosts/maya/plugins/publish/extract_layout.py @@ -56,7 +56,7 @@ class ExtractLayout(openpype.api.Extractor): json_element = { "family": family, - "instance_name": cmds.getAttr(f"{container}.name"), + "instance_name": cmds.getAttr(f"{container}.namespace"), "representation": str(representation_id), "version": str(version_id) } From e3de88e4fe54f0e483d16841730883a6762a7f85 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 24 Aug 2022 10:52:22 +0100 Subject: [PATCH 002/121] Implemented loader for layouts for existing scenes --- .../plugins/load/load_layout_existing.py | 403 ++++++++++++++++++ 1 file changed, 403 insertions(+) create mode 100644 openpype/hosts/unreal/plugins/load/load_layout_existing.py diff --git a/openpype/hosts/unreal/plugins/load/load_layout_existing.py b/openpype/hosts/unreal/plugins/load/load_layout_existing.py new file mode 100644 index 0000000000..297e8d1a4c --- /dev/null +++ b/openpype/hosts/unreal/plugins/load/load_layout_existing.py @@ -0,0 +1,403 @@ +import json +from pathlib import Path + +import unreal +from unreal import EditorLevelLibrary + +from bson.objectid import ObjectId + +from openpype import pipeline +from openpype.pipeline import ( + discover_loader_plugins, + loaders_from_representation, + load_container, + AVALON_CONTAINER_ID, + legacy_io, +) +from openpype.hosts.unreal.api import plugin +from openpype.hosts.unreal.api import pipeline as upipeline + + +class ExistingLayoutLoader(plugin.Loader): + """ + Load Layout for an existing scene, and match the existing assets. + """ + + families = ["layout"] + representations = ["json"] + + label = "Load Layout on Existing Scene" + icon = "code-fork" + color = "orange" + ASSET_ROOT = "/Game/OpenPype" + + @staticmethod + def _create_container( + asset_name, asset_dir, asset, representation, parent, family + ): + container_name = f"{asset_name}_CON" + + container = None + if not unreal.EditorAssetLibrary.does_asset_exist( + f"{asset_dir}/{container_name}" + ): + container = upipeline.create_container(container_name, asset_dir) + else: + ar = unreal.AssetRegistryHelpers.get_asset_registry() + obj = ar.get_asset_by_object_path( + f"{asset_dir}/{container_name}.{container_name}") + container = obj.get_asset() + + data = { + "schema": "openpype:container-2.0", + "id": AVALON_CONTAINER_ID, + "asset": asset, + "namespace": asset_dir, + "container_name": container_name, + "asset_name": asset_name, + # "loader": str(self.__class__.__name__), + "representation": representation, + "parent": parent, + "family": family + } + + upipeline.imprint( + "{}/{}".format(asset_dir, container_name), data) + + return container.get_path_name() + + @staticmethod + def _get_current_level(): + ue_version = unreal.SystemLibrary.get_engine_version().split('.') + ue_major = ue_version[0] + + if ue_major == '4': + return EditorLevelLibrary.get_editor_world() + elif ue_major == '5': + return unreal.LevelEditorSubsystem().get_current_level() + + raise NotImplementedError( + f"Unreal version {ue_major} not supported") + + @staticmethod + def _transform_from_basis(transform, basis, conversion): + """Transform a transform from a basis to a new basis.""" + # Get the basis matrix + basis_matrix = unreal.Matrix( + basis[0], + basis[1], + basis[2], + basis[3] + ) + transform_matrix = unreal.Matrix( + transform[0], + transform[1], + transform[2], + transform[3] + ) + + new_transform = ( + basis_matrix.get_inverse() * transform_matrix * basis_matrix) + + return conversion.inverse() * new_transform.transform() + + def _get_transform(self, ext, import_data, lasset): + conversion = unreal.Matrix.IDENTITY.transform() + + # Check for the conversion settings. We cannot access + # the alembic conversion settings, so we assume that + # the maya ones have been applied. + if ext == '.fbx': + loc = import_data.import_translation + rot = import_data.import_rotation.to_vector() + scale = import_data.import_scale + conversion = unreal.Transform( + location=[loc.x, loc.y, loc.z], + rotation=[rot.x, rot.y, rot.z], + scale=[scale, scale, scale] + ) + elif ext == '.abc': + # This is the standard conversion settings for + # alembic files from Maya. + conversion = unreal.Transform( + location=[0.0, 0.0, 0.0], + rotation=[0.0, 0.0, 0.0], + scale=[1.0, -1.0, 1.0] + ) + + transform = self._transform_from_basis( + lasset.get('transform_matrix'), + lasset.get('basis'), + conversion + ) + return transform + + @staticmethod + def _get_fbx_loader(loaders, family): + name = "" + if family == 'rig': + name = "SkeletalMeshFBXLoader" + elif family == 'model': + name = "StaticMeshFBXLoader" + elif family == 'camera': + name = "CameraLoader" + + if name == "": + return None + + for loader in loaders: + if loader.__name__ == name: + return loader + + return None + + @staticmethod + def _get_abc_loader(loaders, family): + name = "" + if family == 'rig': + name = "SkeletalMeshAlembicLoader" + elif family == 'model': + name = "StaticMeshAlembicLoader" + + if name == "": + return None + + for loader in loaders: + if loader.__name__ == name: + return loader + + return None + + def _load_asset(self, representation, version, instance_name, family): + valid_formats = ['fbx', 'abc'] + + repr_data = legacy_io.find_one({ + "type": "representation", + "parent": ObjectId(version), + "name": {"$in": valid_formats} + }) + repr_format = repr_data.get('name') + + all_loaders = discover_loader_plugins() + loaders = loaders_from_representation( + all_loaders, representation) + + loader = None + + if repr_format == 'fbx': + loader = self._get_fbx_loader(loaders, family) + elif repr_format == 'abc': + loader = self._get_abc_loader(loaders, family) + + if not loader: + raise AssertionError(f"No valid loader found for {representation}") + + assets = load_container( + loader, + representation, + namespace=instance_name + ) + + return assets + + def load(self, context, name, namespace, options): + print("Loading Layout and Match Assets") + + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + asset = context.get('asset').get('name') + container_name = f"{asset}_{name}_CON" + + actors = EditorLevelLibrary.get_all_level_actors() + + with open(self.fname, "r") as fp: + data = json.load(fp) + + layout_data = [] + + # Get all the representations in the JSON from the database. + for element in data: + if element.get('representation'): + layout_data.append(( + pipeline.legacy_io.find_one({ + "_id": ObjectId(element.get('representation')) + }), + element + )) + + containers = [] + actors_matched = [] + + for (repr_data, lasset) in layout_data: + if not repr_data: + raise AssertionError("Representation not found") + if not (repr_data.get('data') or repr_data.get('data').get('path')): + raise AssertionError("Representation does not have path") + if not repr_data.get('context'): + raise AssertionError("Representation does not have context") + + # For every actor in the scene, check if it has a representation in + # those we got from the JSON. If so, create a container for it. + # Otherwise, remove it from the scene. + found = False + + for actor in actors: + if not actor.get_class().get_name() == 'StaticMeshActor': + continue + if actor in actors_matched: + continue + + # Get the original path of the file from which the asset has + # been imported. + actor.set_actor_label(lasset.get('instance_name')) + smc = actor.get_editor_property('static_mesh_component') + mesh = smc.get_editor_property('static_mesh') + import_data = mesh.get_editor_property('asset_import_data') + filename = import_data.get_first_filename() + path = Path(filename) + + if not path.name in repr_data.get('data').get('path'): + continue + + asset_name = path.with_suffix('').name + mesh_path = Path(mesh.get_path_name()).parent.as_posix() + + # Create the container for the asset. + asset = repr_data.get('context').get('asset') + subset = repr_data.get('context').get('subset') + container = self._create_container( + f"{asset}_{subset}", mesh_path, asset, + repr_data.get('_id'), repr_data.get('parent'), + repr_data.get('context').get('family') + ) + containers.append(container) + + # Set the transform for the actor. + transform = self._get_transform( + path.suffix, import_data, lasset) + actor.set_actor_transform(transform, False, True) + + actors_matched.append(actor) + found = True + break + + # If an actor has not been found for this representation, + # we check if it has been loaded already by checking all the + # loaded containers. If so, we add it to the scene. Otherwise, + # we load it. + if found: + continue + + all_containers = upipeline.ls() + + loaded = False + + for container in all_containers: + repr = container.get('representation') + + if not repr == str(repr_data.get('_id')): + continue + + asset_dir = container.get('namespace') + + filter = unreal.ARFilter( + class_names=["StaticMesh"], + package_paths=[asset_dir], + recursive_paths=False) + assets = ar.get_assets(filter) + + for asset in assets: + obj = asset.get_asset() + actor = EditorLevelLibrary.spawn_actor_from_object( + obj, unreal.Vector(0.0, 0.0, 0.0) + ) + + actor.set_actor_label(lasset.get('instance_name')) + smc = actor.get_editor_property( + 'static_mesh_component') + mesh = smc.get_editor_property('static_mesh') + import_data = mesh.get_editor_property( + 'asset_import_data') + filename = import_data.get_first_filename() + path = Path(filename) + + transform = self._get_transform( + path.suffix, import_data, lasset) + + actor.set_actor_transform(transform, False, True) + + loaded = True + break + + # If the asset has not been loaded yet, we load it. + if loaded: + continue + + assets = self._load_asset( + lasset.get('representation'), + lasset.get('version'), + lasset.get('instance_name'), + lasset.get('family') + ) + + for asset in assets: + obj = ar.get_asset_by_object_path(asset).get_asset() + if not obj.get_class().get_name() == 'StaticMesh': + continue + actor = EditorLevelLibrary.spawn_actor_from_object( + obj, unreal.Vector(0.0, 0.0, 0.0) + ) + + actor.set_actor_label(lasset.get('instance_name')) + smc = actor.get_editor_property('static_mesh_component') + mesh = smc.get_editor_property('static_mesh') + import_data = mesh.get_editor_property('asset_import_data') + filename = import_data.get_first_filename() + path = Path(filename) + + transform = self._transform_from_basis( + lasset.get('transform_matrix'), + lasset.get('basis'), + unreal.Matrix.IDENTITY.transform() + ) + + actor.set_actor_transform(transform, False, True) + + break + + # Check if an actor was not matched to a representation. + # If so, remove it from the scene. + for actor in actors: + if not actor.get_class().get_name() == 'StaticMeshActor': + continue + if actor not in actors_matched: + EditorLevelLibrary.destroy_actor(actor) + + curr_level = self._get_current_level() + + if not curr_level: + return + + curr_level_path = Path( + curr_level.get_outer().get_path_name()).parent.as_posix() + + if not unreal.EditorAssetLibrary.does_asset_exist( + f"{curr_level_path}/{container_name}" + ): + upipeline.create_container( + container=container_name, path=curr_level_path) + + data = { + "schema": "openpype:container-2.0", + "id": AVALON_CONTAINER_ID, + "asset": asset, + "namespace": curr_level_path, + "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"], + "loaded_assets": containers + } + upipeline.imprint(f"{curr_level_path}/{container_name}", data) From b3cd5e1ea060a533d983d7cfb4b231f240430226 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 24 Aug 2022 11:46:18 +0100 Subject: [PATCH 003/121] Hound fixes --- openpype/hosts/unreal/plugins/load/load_layout_existing.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_layout_existing.py b/openpype/hosts/unreal/plugins/load/load_layout_existing.py index 297e8d1a4c..c20af950d9 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout_existing.py +++ b/openpype/hosts/unreal/plugins/load/load_layout_existing.py @@ -231,7 +231,8 @@ class ExistingLayoutLoader(plugin.Loader): for (repr_data, lasset) in layout_data: if not repr_data: raise AssertionError("Representation not found") - if not (repr_data.get('data') or repr_data.get('data').get('path')): + if not (repr_data.get('data') or + repr_data.get('data').get('path')): raise AssertionError("Representation does not have path") if not repr_data.get('context'): raise AssertionError("Representation does not have context") @@ -256,7 +257,7 @@ class ExistingLayoutLoader(plugin.Loader): filename = import_data.get_first_filename() path = Path(filename) - if not path.name in repr_data.get('data').get('path'): + if path.name not in repr_data.get('data').get('path'): continue asset_name = path.with_suffix('').name From 2f0f9508d4d36a157d4a4a55ed63c7408ae3c7f8 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 26 Aug 2022 12:05:18 +0100 Subject: [PATCH 004/121] Implemented update --- .../plugins/load/load_alembic_staticmesh.py | 26 +++-- .../plugins/load/load_layout_existing.py | 103 ++++++++++-------- 2 files changed, 75 insertions(+), 54 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py b/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py index 50e498dbb0..a5b9cbd1fc 100644 --- a/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py +++ b/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py @@ -20,15 +20,11 @@ class StaticMeshAlembicLoader(plugin.Loader): icon = "cube" color = "orange" - def get_task(self, filename, asset_dir, asset_name, replace): + @staticmethod + def get_task(filename, asset_dir, asset_name, replace, default_conversion): task = unreal.AssetImportTask() options = unreal.AbcImportSettings() sm_settings = unreal.AbcStaticMeshSettings() - conversion_settings = unreal.AbcConversionSettings( - preset=unreal.AbcConversionPreset.CUSTOM, - flip_u=False, flip_v=False, - rotation=[0.0, 0.0, 0.0], - scale=[1.0, 1.0, 1.0]) task.set_editor_property('filename', filename) task.set_editor_property('destination_path', asset_dir) @@ -44,13 +40,20 @@ class StaticMeshAlembicLoader(plugin.Loader): sm_settings.set_editor_property('merge_meshes', True) + if not default_conversion: + conversion_settings = unreal.AbcConversionSettings( + preset=unreal.AbcConversionPreset.CUSTOM, + flip_u=False, flip_v=False, + rotation=[0.0, 0.0, 0.0], + scale=[1.0, 1.0, 1.0]) + options.conversion_settings = conversion_settings + options.static_mesh_settings = sm_settings - options.conversion_settings = conversion_settings task.options = options return task - def load(self, context, name, namespace, data): + def load(self, context, name, namespace, options): """Load and containerise representation into Content Browser. This is two step process. First, import FBX to temporary path and @@ -82,6 +85,10 @@ class StaticMeshAlembicLoader(plugin.Loader): asset_name = "{}".format(name) version = context.get('version').get('name') + default_conversion = False + if options.get("default_conversion"): + default_conversion = options.get("default_conversion") + tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( f"{root}/{asset}/{name}_v{version:03d}", suffix="") @@ -91,7 +98,8 @@ class StaticMeshAlembicLoader(plugin.Loader): if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): unreal.EditorAssetLibrary.make_directory(asset_dir) - task = self.get_task(self.fname, asset_dir, asset_name, False) + task = self.get_task( + self.fname, asset_dir, asset_name, False, default_conversion) unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 diff --git a/openpype/hosts/unreal/plugins/load/load_layout_existing.py b/openpype/hosts/unreal/plugins/load/load_layout_existing.py index c20af950d9..8cd1950f7e 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout_existing.py +++ b/openpype/hosts/unreal/plugins/load/load_layout_existing.py @@ -11,6 +11,7 @@ from openpype.pipeline import ( discover_loader_plugins, loaders_from_representation, load_container, + get_representation_path, AVALON_CONTAINER_ID, legacy_io, ) @@ -132,6 +133,23 @@ class ExistingLayoutLoader(plugin.Loader): ) return transform + def _spawn_actor(self, obj, lasset): + actor = EditorLevelLibrary.spawn_actor_from_object( + obj, unreal.Vector(0.0, 0.0, 0.0) + ) + + actor.set_actor_label(lasset.get('instance_name')) + smc = actor.get_editor_property('static_mesh_component') + mesh = smc.get_editor_property('static_mesh') + import_data = mesh.get_editor_property('asset_import_data') + filename = import_data.get_first_filename() + path = Path(filename) + + transform = self._get_transform( + path.suffix, import_data, lasset) + + actor.set_actor_transform(transform, False, True) + @staticmethod def _get_fbx_loader(loaders, family): name = "" @@ -192,25 +210,29 @@ class ExistingLayoutLoader(plugin.Loader): if not loader: raise AssertionError(f"No valid loader found for {representation}") + # This option is necessary to avoid importing the assets with a + # different conversion compared to the other assets. For ABC files, + # it is in fact impossible to access the conversion settings. So, + # we must assume that the Maya conversion settings have been applied. + options = { + "default_conversion": True + } + assets = load_container( loader, representation, - namespace=instance_name + namespace=instance_name, + options=options ) return assets - def load(self, context, name, namespace, options): - print("Loading Layout and Match Assets") - + def _process(self, lib_path): ar = unreal.AssetRegistryHelpers.get_asset_registry() - asset = context.get('asset').get('name') - container_name = f"{asset}_{name}_CON" - actors = EditorLevelLibrary.get_all_level_actors() - with open(self.fname, "r") as fp: + with open(lib_path, "r") as fp: data = json.load(fp) layout_data = [] @@ -260,7 +282,6 @@ class ExistingLayoutLoader(plugin.Loader): if path.name not in repr_data.get('data').get('path'): continue - asset_name = path.with_suffix('').name mesh_path = Path(mesh.get_path_name()).parent.as_posix() # Create the container for the asset. @@ -309,23 +330,7 @@ class ExistingLayoutLoader(plugin.Loader): for asset in assets: obj = asset.get_asset() - actor = EditorLevelLibrary.spawn_actor_from_object( - obj, unreal.Vector(0.0, 0.0, 0.0) - ) - - actor.set_actor_label(lasset.get('instance_name')) - smc = actor.get_editor_property( - 'static_mesh_component') - mesh = smc.get_editor_property('static_mesh') - import_data = mesh.get_editor_property( - 'asset_import_data') - filename = import_data.get_first_filename() - path = Path(filename) - - transform = self._get_transform( - path.suffix, import_data, lasset) - - actor.set_actor_transform(transform, False, True) + self._spawn_actor(obj, lasset) loaded = True break @@ -345,24 +350,7 @@ class ExistingLayoutLoader(plugin.Loader): obj = ar.get_asset_by_object_path(asset).get_asset() if not obj.get_class().get_name() == 'StaticMesh': continue - actor = EditorLevelLibrary.spawn_actor_from_object( - obj, unreal.Vector(0.0, 0.0, 0.0) - ) - - actor.set_actor_label(lasset.get('instance_name')) - smc = actor.get_editor_property('static_mesh_component') - mesh = smc.get_editor_property('static_mesh') - import_data = mesh.get_editor_property('asset_import_data') - filename = import_data.get_first_filename() - path = Path(filename) - - transform = self._transform_from_basis( - lasset.get('transform_matrix'), - lasset.get('basis'), - unreal.Matrix.IDENTITY.transform() - ) - - actor.set_actor_transform(transform, False, True) + self._spawn_actor(obj, lasset) break @@ -374,10 +362,21 @@ class ExistingLayoutLoader(plugin.Loader): if actor not in actors_matched: EditorLevelLibrary.destroy_actor(actor) + return containers + + def load(self, context, name, namespace, options): + print("Loading Layout and Match Assets") + + asset = context.get('asset').get('name') + asset_name = f"{asset}_{name}" if asset else name + container_name = f"{asset}_{name}_CON" + curr_level = self._get_current_level() if not curr_level: - return + raise AssertionError("Current level not saved") + + containers = self._process(self.fname) curr_level_path = Path( curr_level.get_outer().get_path_name()).parent.as_posix() @@ -402,3 +401,17 @@ class ExistingLayoutLoader(plugin.Loader): "loaded_assets": containers } upipeline.imprint(f"{curr_level_path}/{container_name}", data) + + def update(self, container, representation): + asset_dir = container.get('namespace') + + source_path = get_representation_path(representation) + containers = self._process(source_path) + + data = { + "representation": str(representation["_id"]), + "parent": str(representation["parent"]), + "loaded_assets": containers + } + upipeline.imprint( + "{}/{}".format(asset_dir, container.get('container_name')), data) From 41e673c3ee45c85a7c71e5b0a0bb465ae95a9b83 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 9 Sep 2022 00:20:47 +0200 Subject: [PATCH 005/121] Cleanup comment --- openpype/hosts/maya/api/lib.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 58e160cb2f..5e449b324e 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2633,8 +2633,6 @@ def load_capture_preset(data=None): scene = capture.parse_active_scene() options['sound'] = scene['sound'] - # options['display_options'] = temp_options - return options From ed11baf0fdaa8c35764bdee8b04215e58aca1e23 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 9 Sep 2022 00:29:16 +0200 Subject: [PATCH 006/121] Include camera options --- openpype/hosts/maya/api/lib.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 5e449b324e..c8369bac13 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2629,6 +2629,13 @@ def load_capture_preset(data=None): options['viewport_options'] = temp_options options['viewport2_options'] = temp_options2 + # CAMERA OPTIONS + id = 'Camera Options' + camera_options = {} + for key, value in preset[id].items(): + camera_options[key] = value + options['camera_options'] = camera_options + # use active sound track scene = capture.parse_active_scene() options['sound'] = scene['sound'] From d0d80b0b90648b1633a11c5980c0c66e3a3cff7f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 13 Sep 2022 14:57:05 +0200 Subject: [PATCH 007/121] Fix typo `camera_option` -> `camera_options` - Also use `setdefault` to ensure its added into the preset when key wasn't there originally --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 871adda0c3..6010319f40 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -79,8 +79,10 @@ class ExtractPlayblast(openpype.api.Extractor): preset['height'] = asset_height preset['start_frame'] = start preset['end_frame'] = end - camera_option = preset.get("camera_option", {}) - camera_option["depthOfField"] = cmds.getAttr( + + # Enforce persisting camera depth of field + camera_options = preset.setdefault("camera_options", {}) + camera_options["depthOfField"] = cmds.getAttr( "{0}.depthOfField".format(camera)) stagingdir = self.staging_dir(instance) From 3fff6647389d28b47fe4048a5a048d3fb420da91 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 15 Sep 2022 16:13:01 +0200 Subject: [PATCH 008/121] Refactor `load_capture_preset` --- openpype/hosts/maya/api/lib.py | 204 +++++++++++---------------------- 1 file changed, 64 insertions(+), 140 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 97035ad3f2..3561c3f53f 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2462,28 +2462,16 @@ def load_capture_preset(data=None): import capture preset = data - options = dict() - # CODEC - id = 'Codec' - for key in preset[id]: - options[str(key)] = preset[id][key] - - # GENERIC - id = 'Generic' - for key in preset[id]: - options[str(key)] = preset[id][key] - - # RESOLUTION - id = 'Resolution' - options['height'] = preset[id]['height'] - options['width'] = preset[id]['width'] + # Straight key-value match from settings to capture arguments + for settings_key in ["Codec", "Generic", "Resolution"]: + for key, value in preset[settings_key].items(): + options[key] = value # DISPLAY OPTIONS - id = 'Display Options' disp_options = {} - for key in preset[id]: + for key in preset['Display Options']: if key.startswith('background'): disp_options[key] = preset['Display Options'][key] if len(disp_options[key]) == 4: @@ -2497,142 +2485,78 @@ def load_capture_preset(data=None): options['display_options'] = disp_options # VIEWPORT OPTIONS - temp_options = {} - id = 'Renderer' - for key in preset[id]: - temp_options[str(key)] = preset[id][key] + viewport_options = {} + viewport2_options = {} - temp_options2 = {} - id = 'Viewport Options' - for key in preset[id]: + for key, value in preset['Renderer'].items(): + viewport_options[key] = value + + # Viewport Options has a mixture of Viewport2 Options and Viewport Options + # to pass along to capture. So we'll need to differentiate between the two + VIEWPORT2_OPTIONS = { + "textureMaxResolution", + "renderDepthOfField", + "ssaoEnable", + "ssaoSamples", + "ssaoAmount", + "ssaoRadius", + "ssaoFilterRadius", + "hwFogStart", + "hwFogEnd", + "hwFogAlpha", + "hwFogFalloff", + "hwFogColorR", + "hwFogColorG", + "hwFogColorB", + "hwFogDensity", + "motionBlurEnable", + "motionBlurSampleCount", + "motionBlurShutterOpenFraction", + "lineAAEnable" + } + for key, value in preset['Viewport Options'].items(): + + # There are some keys we want to ignore + if key in {"override_viewport_options", "high_quality"}: + continue + + # First handle special cases where we do value conversion to + # separate option values if key == 'textureMaxResolution': - if preset[id][key] > 0: - temp_options2['textureMaxResolution'] = preset[id][key] - temp_options2['enableTextureMaxRes'] = True - temp_options2['textureMaxResMode'] = 1 + viewport2_options['textureMaxResolution'] = value + if value > 0: + viewport2_options['enableTextureMaxRes'] = True + viewport2_options['textureMaxResMode'] = 1 else: - temp_options2['textureMaxResolution'] = preset[id][key] - temp_options2['enableTextureMaxRes'] = False - temp_options2['textureMaxResMode'] = 0 + viewport2_options['enableTextureMaxRes'] = False + viewport2_options['textureMaxResMode'] = 0 - if key == 'multiSample': - if preset[id][key] > 0: - temp_options2['multiSampleEnable'] = True - temp_options2['multiSampleCount'] = preset[id][key] - else: - temp_options2['multiSampleEnable'] = False - temp_options2['multiSampleCount'] = preset[id][key] + elif key == 'multiSample': + viewport2_options['multiSampleEnable'] = value > 0 + viewport2_options['multiSampleCount'] = value - if key == 'renderDepthOfField': - temp_options2['renderDepthOfField'] = preset[id][key] + elif key == 'alphaCut': + viewport2_options['transparencyAlgorithm'] = 5 + viewport2_options['transparencyQuality'] = 1 - if key == 'ssaoEnable': - if preset[id][key] is True: - temp_options2['ssaoEnable'] = True - else: - temp_options2['ssaoEnable'] = False + elif key == 'hwFogFalloff': + # Settings enum value string to integer + viewport2_options['hwFogFalloff'] = int(value) - if key == 'ssaoSamples': - temp_options2['ssaoSamples'] = preset[id][key] - - if key == 'ssaoAmount': - temp_options2['ssaoAmount'] = preset[id][key] - - if key == 'ssaoRadius': - temp_options2['ssaoRadius'] = preset[id][key] - - if key == 'hwFogDensity': - temp_options2['hwFogDensity'] = preset[id][key] - - if key == 'ssaoFilterRadius': - temp_options2['ssaoFilterRadius'] = preset[id][key] - - if key == 'alphaCut': - temp_options2['transparencyAlgorithm'] = 5 - temp_options2['transparencyQuality'] = 1 - - if key == 'headsUpDisplay': - temp_options['headsUpDisplay'] = True - - if key == 'fogging': - temp_options['fogging'] = preset[id][key] or False - - if key == 'hwFogStart': - temp_options2['hwFogStart'] = preset[id][key] - - if key == 'hwFogEnd': - temp_options2['hwFogEnd'] = preset[id][key] - - if key == 'hwFogAlpha': - temp_options2['hwFogAlpha'] = preset[id][key] - - if key == 'hwFogFalloff': - temp_options2['hwFogFalloff'] = int(preset[id][key]) - - if key == 'hwFogColorR': - temp_options2['hwFogColorR'] = preset[id][key] - - if key == 'hwFogColorG': - temp_options2['hwFogColorG'] = preset[id][key] - - if key == 'hwFogColorB': - temp_options2['hwFogColorB'] = preset[id][key] - - if key == 'motionBlurEnable': - if preset[id][key] is True: - temp_options2['motionBlurEnable'] = True - else: - temp_options2['motionBlurEnable'] = False - - if key == 'motionBlurSampleCount': - temp_options2['motionBlurSampleCount'] = preset[id][key] - - if key == 'motionBlurShutterOpenFraction': - temp_options2['motionBlurShutterOpenFraction'] = preset[id][key] - - if key == 'lineAAEnable': - if preset[id][key] is True: - temp_options2['lineAAEnable'] = True - else: - temp_options2['lineAAEnable'] = False + # Then handle Viewport 2.0 Options + elif key in VIEWPORT2_OPTIONS: + viewport2_options[key] = value + # Then assume remainder is Viewport Options else: - temp_options[str(key)] = preset[id][key] + viewport_options[key] = value - for key in ['override_viewport_options', - 'high_quality', - 'alphaCut', - 'gpuCacheDisplayFilter', - 'multiSample', - 'ssaoEnable', - 'ssaoSamples', - 'ssaoAmount', - 'ssaoFilterRadius', - 'ssaoRadius', - 'hwFogStart', - 'hwFogEnd', - 'hwFogAlpha', - 'hwFogFalloff', - 'hwFogColorR', - 'hwFogColorG', - 'hwFogColorB', - 'hwFogDensity', - 'textureMaxResolution', - 'motionBlurEnable', - 'motionBlurSampleCount', - 'motionBlurShutterOpenFraction', - 'lineAAEnable', - 'renderDepthOfField' - ]: - temp_options.pop(key, None) - - options['viewport_options'] = temp_options - options['viewport2_options'] = temp_options2 + options['viewport_options'] = viewport_options + options['viewport2_options'] = viewport2_options # CAMERA OPTIONS - id = 'Camera Options' camera_options = {} - for key, value in preset[id].items(): + for key, value in preset['Camera Options'].items(): camera_options[key] = value options['camera_options'] = camera_options From 2560fc0081292b7862367101e895a7d3526045da Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 15 Sep 2022 16:29:16 +0200 Subject: [PATCH 009/121] Simplify logic where we're taking values directly --- openpype/hosts/maya/api/lib.py | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 3561c3f53f..4d6f599d2b 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2460,14 +2460,20 @@ def bake_to_world_space(nodes, def load_capture_preset(data=None): import capture - preset = data + options = dict() + viewport_options = dict() + viewport2_options = dict() + camera_options = dict() # Straight key-value match from settings to capture arguments - for settings_key in ["Codec", "Generic", "Resolution"]: - for key, value in preset[settings_key].items(): - options[key] = value + options.update(preset["Codec"]) + options.update(preset["Generic"]) + options.update(preset["Resolution"]) + + camera_options.update(preset['Camera Options']) + viewport_options.update(preset["Renderer"]) # DISPLAY OPTIONS disp_options = {} @@ -2484,13 +2490,6 @@ def load_capture_preset(data=None): options['display_options'] = disp_options - # VIEWPORT OPTIONS - viewport_options = {} - viewport2_options = {} - - for key, value in preset['Renderer'].items(): - viewport_options[key] = value - # Viewport Options has a mixture of Viewport2 Options and Viewport Options # to pass along to capture. So we'll need to differentiate between the two VIEWPORT2_OPTIONS = { @@ -2553,11 +2552,6 @@ def load_capture_preset(data=None): options['viewport_options'] = viewport_options options['viewport2_options'] = viewport2_options - - # CAMERA OPTIONS - camera_options = {} - for key, value in preset['Camera Options'].items(): - camera_options[key] = value options['camera_options'] = camera_options # use active sound track From f99b31d13a7bafbfe3763978db9a10a1024f8d85 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 15 Sep 2022 16:30:32 +0200 Subject: [PATCH 010/121] Don't remap input argument `preset` isn't necessarily more explicit than `data` and actually adds to confusion because it makes it feel like it's an actual capture preset but instead it converts OpenPype preset data into capture preset --- openpype/hosts/maya/api/lib.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 4d6f599d2b..7220f53e66 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2460,7 +2460,6 @@ def bake_to_world_space(nodes, def load_capture_preset(data=None): import capture - preset = data options = dict() viewport_options = dict() @@ -2468,18 +2467,18 @@ def load_capture_preset(data=None): camera_options = dict() # Straight key-value match from settings to capture arguments - options.update(preset["Codec"]) - options.update(preset["Generic"]) - options.update(preset["Resolution"]) + options.update(data["Codec"]) + options.update(data["Generic"]) + options.update(data["Resolution"]) - camera_options.update(preset['Camera Options']) - viewport_options.update(preset["Renderer"]) + camera_options.update(data['Camera Options']) + viewport_options.update(data["Renderer"]) # DISPLAY OPTIONS disp_options = {} - for key in preset['Display Options']: + for key in data['Display Options']: if key.startswith('background'): - disp_options[key] = preset['Display Options'][key] + disp_options[key] = data['Display Options'][key] if len(disp_options[key]) == 4: disp_options[key][0] = (float(disp_options[key][0])/255) disp_options[key][1] = (float(disp_options[key][1])/255) @@ -2513,7 +2512,7 @@ def load_capture_preset(data=None): "motionBlurShutterOpenFraction", "lineAAEnable" } - for key, value in preset['Viewport Options'].items(): + for key, value in data['Viewport Options'].items(): # There are some keys we want to ignore if key in {"override_viewport_options", "high_quality"}: From 1132d8d4f8ffa4b224308b5099424e9984d0822c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 15 Sep 2022 16:34:11 +0200 Subject: [PATCH 011/121] Add docstring --- openpype/hosts/maya/api/lib.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 7220f53e66..dec698062a 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2459,6 +2459,19 @@ def bake_to_world_space(nodes, def load_capture_preset(data=None): + """Convert OpenPype Extract Playblast settings to `capture` arguments + + Input data is the settings from: + `project_settings/maya/publish/ExtractPlayblast/capture_preset` + + Args: + data (dict): Capture preset settings from OpenPype settings + + Returns: + dict: `capture.capture` compatible keyword arguments + + """ + import capture options = dict() From 598ec6e2dcb5664407945d081ae15adad6b4d8c6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 15 Sep 2022 16:57:42 +0200 Subject: [PATCH 012/121] Improve readability of color conversion logic --- openpype/hosts/maya/api/lib.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index dec698062a..5b436a018d 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2489,14 +2489,17 @@ def load_capture_preset(data=None): # DISPLAY OPTIONS disp_options = {} - for key in data['Display Options']: + for key, value in data['Display Options'].items(): if key.startswith('background'): - disp_options[key] = data['Display Options'][key] - if len(disp_options[key]) == 4: - disp_options[key][0] = (float(disp_options[key][0])/255) - disp_options[key][1] = (float(disp_options[key][1])/255) - disp_options[key][2] = (float(disp_options[key][2])/255) - disp_options[key].pop() + # Convert background, backgroundTop, backgroundBottom colors + if len(value) == 4: + # Ignore alpha + convert RGB to float + value = [ + float(value[0]) / 255, + float(value[1]) / 255, + float(value[2]) / 255 + ] + disp_options[key] = value else: disp_options['displayGradient'] = True From ccab10b0d3fecc4e32711e5f1a783de433b54ce8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 16 Sep 2022 14:43:13 +0200 Subject: [PATCH 013/121] Do not enforce maya/ folder --- .../publish/validate_rendersettings.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py index 08ecc0d149..4a67cb73e4 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py +++ b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py @@ -16,10 +16,10 @@ from openpype.hosts.maya.api import lib class ValidateRenderSettings(pyblish.api.InstancePlugin): """Validates the global render settings - * File Name Prefix must start with: `maya/` + * File Name Prefix must start with: `` all other token are customizable but sane values for Arnold are: - `maya///_` + `//_` token is supported also, useful for multiple renderable cameras per render layer. @@ -58,12 +58,12 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): } ImagePrefixTokens = { - 'mentalray': 'maya///{aov_separator}', # noqa: E501 - 'arnold': 'maya///{aov_separator}', # noqa: E501 - 'redshift': 'maya///', - 'vray': 'maya///', + 'mentalray': '//{aov_separator}', # noqa: E501 + 'arnold': '//{aov_separator}', # noqa: E501 + 'redshift': '//', + 'vray': '//', 'renderman': '{aov_separator}..', - 'mayahardware2': 'maya///', + 'mayahardware2': '//', } _aov_chars = { @@ -74,7 +74,7 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): redshift_AOV_prefix = "/{aov_separator}" # noqa: E501 - renderman_dir_prefix = "maya//" + renderman_dir_prefix = "/" R_AOV_TOKEN = re.compile( r'%a||', re.IGNORECASE) @@ -84,8 +84,8 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): R_SCENE_TOKEN = re.compile(r'%s|', re.IGNORECASE) DEFAULT_PADDING = 4 - VRAY_PREFIX = "maya///" - DEFAULT_PREFIX = "maya///_" + VRAY_PREFIX = "//" + DEFAULT_PREFIX = "//_" def process(self, instance): @@ -116,7 +116,7 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): prefix = prefix.replace( "{aov_separator}", instance.data.get("aovSeparator", "_")) - required_prefix = "maya/" + required_prefix = "" default_prefix = cls.ImagePrefixTokens[renderer] if not anim_override: From 59cfca7508f493bb7afec85ded74bd8abd44e2e0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 16 Sep 2022 14:44:19 +0200 Subject: [PATCH 014/121] Do not enforce maya/ folder --- openpype/hosts/maya/api/lib_rendersettings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 7cd2193086..21dd7f00c8 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -28,7 +28,7 @@ class RenderSettings(object): _image_prefixes = { 'vray': get_current_project_settings()["maya"]["RenderSettings"]["vray_renderer"]["image_prefix"], # noqa 'arnold': get_current_project_settings()["maya"]["RenderSettings"]["arnold_renderer"]["image_prefix"], # noqa - 'renderman': 'maya///{aov_separator}', + 'renderman': '//{aov_separator}', 'redshift': get_current_project_settings()["maya"]["RenderSettings"]["redshift_renderer"]["image_prefix"] # noqa } From 47d7b3044bff4827356ddde4f4c257ac87a9287c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 16 Sep 2022 14:46:45 +0200 Subject: [PATCH 015/121] Remove maya/ from file prefixes in setting defaults --- openpype/settings/defaults/project_settings/maya.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 8643297f02..716e45a6e2 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -34,12 +34,12 @@ }, "RenderSettings": { "apply_render_settings": true, - "default_render_image_folder": "renders", + "default_render_image_folder": "renders/maya", "enable_all_lights": false, "aov_separator": "underscore", "reset_current_frame": false, "arnold_renderer": { - "image_prefix": "maya///_", + "image_prefix": "//_", "image_format": "exr", "multilayer_exr": true, "tiled": true, @@ -47,14 +47,14 @@ "additional_options": [] }, "vray_renderer": { - "image_prefix": "maya///", + "image_prefix": "//", "engine": "1", "image_format": "png", "aov_list": [], "additional_options": [] }, "redshift_renderer": { - "image_prefix": "maya///", + "image_prefix": "//", "primary_gi_engine": "0", "secondary_gi_engine": "0", "image_format": "iff", From ea5ae50982cc867f24c253e8c5dcb1df1612d30f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 16 Sep 2022 14:47:28 +0200 Subject: [PATCH 016/121] Add maya/ folder by default into "images" file rule in workspace --- openpype/settings/defaults/project_settings/maya.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 716e45a6e2..a62d356162 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -1,5 +1,5 @@ { - "mel_workspace": "workspace -fr \"shaders\" \"renderData/shaders\";\nworkspace -fr \"images\" \"renders\";\nworkspace -fr \"particles\" \"particles\";\nworkspace -fr \"mayaAscii\" \"\";\nworkspace -fr \"mayaBinary\" \"\";\nworkspace -fr \"scene\" \"\";\nworkspace -fr \"alembicCache\" \"cache/alembic\";\nworkspace -fr \"renderData\" \"renderData\";\nworkspace -fr \"sourceImages\" \"sourceimages\";\nworkspace -fr \"fileCache\" \"cache/nCache\";\n", + "mel_workspace": "workspace -fr \"shaders\" \"renderData/shaders\";\nworkspace -fr \"images\" \"renders/maya\";\nworkspace -fr \"particles\" \"particles\";\nworkspace -fr \"mayaAscii\" \"\";\nworkspace -fr \"mayaBinary\" \"\";\nworkspace -fr \"scene\" \"\";\nworkspace -fr \"alembicCache\" \"cache/alembic\";\nworkspace -fr \"renderData\" \"renderData\";\nworkspace -fr \"sourceImages\" \"sourceimages\";\nworkspace -fr \"fileCache\" \"cache/nCache\";\n", "ext_mapping": { "model": "ma", "mayaAscii": "ma", From b4b62ce7ba793dcd812ea6cc40adcdb79b687aac Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 16 Sep 2022 14:48:56 +0200 Subject: [PATCH 017/121] Fix remainder of hardcoded maya/ folders in file prefixes --- openpype/hosts/maya/api/lib_renderproducts.py | 2 +- openpype/hosts/maya/plugins/publish/submit_maya_muster.py | 2 +- .../modules/deadline/plugins/publish/submit_maya_deadline.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index 1e883ea43f..1ab771cfe6 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -80,7 +80,7 @@ IMAGE_PREFIXES = { "mayahardware2": "defaultRenderGlobals.imageFilePrefix" } -RENDERMAN_IMAGE_DIR = "maya//" +RENDERMAN_IMAGE_DIR = "/" def has_tokens(string, tokens): diff --git a/openpype/hosts/maya/plugins/publish/submit_maya_muster.py b/openpype/hosts/maya/plugins/publish/submit_maya_muster.py index c4250a20bd..01008b7756 100644 --- a/openpype/hosts/maya/plugins/publish/submit_maya_muster.py +++ b/openpype/hosts/maya/plugins/publish/submit_maya_muster.py @@ -118,7 +118,7 @@ def preview_fname(folder, scene, layer, padding, ext): """ # Following hardcoded "/_/" - output = "maya/{scene}/{layer}/{layer}.{number}.{ext}".format( + output = "{scene}/{layer}/{layer}.{number}.{ext}".format( scene=scene, layer=layer, number="#" * padding, diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index 44f2b5b2b4..3e3e5c5b16 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -729,10 +729,10 @@ def _format_tiles( Example:: Image prefix is: - `maya///_` + `//_` Result for tile 0 for 4x4 will be: - `maya///_tile_1x1_4x4__` + `//_tile_1x1_4x4__` Calculating coordinates is tricky as in Job they are defined as top, left, bottom, right with zero being in top-left corner. But Assembler From 8742acaef94ab0a2695e4637d66fccd783742bea Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 21 Sep 2022 23:56:10 +0200 Subject: [PATCH 018/121] Match logic of global thumbnail extractor to avoid overwriting source files --- .../maya/plugins/publish/extract_thumbnail.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 854301ea48..712159c2be 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -1,5 +1,6 @@ import os import glob +import tempfile import capture @@ -81,9 +82,17 @@ class ExtractThumbnail(publish.Extractor): elif asset_width and asset_height: preset['width'] = asset_width preset['height'] = asset_height - stagingDir = self.staging_dir(instance) + + # Create temp directory for thumbnail + # - this is to avoid "override" of source file + dst_staging = tempfile.mkdtemp(prefix="pyblish_tmp_") + self.log.debug( + "Create temp directory {} for thumbnail".format(dst_staging) + ) + # Store new staging to cleanup paths + instance.context.data["cleanupFullPaths"].append(dst_staging) filename = "{0}".format(instance.name) - path = os.path.join(stagingDir, filename) + path = os.path.join(dst_staging, filename) self.log.info("Outputting images to %s" % path) @@ -137,7 +146,7 @@ class ExtractThumbnail(publish.Extractor): 'name': 'thumbnail', 'ext': 'jpg', 'files': thumbnail, - "stagingDir": stagingDir, + "stagingDir": dst_staging, "thumbnail": True } instance.data["representations"].append(representation) From e97b6ce01f511b1cf240cb8640b871de3d79dc4e Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 23 Sep 2022 16:18:08 +0100 Subject: [PATCH 019/121] Fixed path resolving not finding the workfile in certain conditions In case the workfile only contains the project name, the workfile is not found because while the regex matches, the match doesn't have any group, and so it throws an exception. --- openpype/pipeline/workfile/path_resolving.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/pipeline/workfile/path_resolving.py b/openpype/pipeline/workfile/path_resolving.py index ed1d1d793e..4e4d3ca1c0 100644 --- a/openpype/pipeline/workfile/path_resolving.py +++ b/openpype/pipeline/workfile/path_resolving.py @@ -265,6 +265,10 @@ def get_last_workfile_with_version( if not match: continue + if not match.groups(): + output_filenames.append(filename) + continue + file_version = int(match.group(1)) if version is None or file_version > version: output_filenames[:] = [] From 548a37e4e1baf7f8cd9c4af61d0172caeec0f3ba Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 23 Sep 2022 16:18:52 +0100 Subject: [PATCH 020/121] Added setting to remove unmatched assets --- openpype/settings/defaults/project_settings/unreal.json | 1 + .../schemas/projects_schema/schema_project_unreal.json | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/openpype/settings/defaults/project_settings/unreal.json b/openpype/settings/defaults/project_settings/unreal.json index c5f5cdf719..391e2415a5 100644 --- a/openpype/settings/defaults/project_settings/unreal.json +++ b/openpype/settings/defaults/project_settings/unreal.json @@ -1,5 +1,6 @@ { "level_sequences_for_layouts": false, + "delete_unmatched_assets": false, "project_setup": { "dev_mode": true } diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json index d26b5c1ccf..09e5791ac4 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json @@ -10,6 +10,11 @@ "key": "level_sequences_for_layouts", "label": "Generate level sequences when loading layouts" }, + { + "type": "boolean", + "key": "delete_unmatched_assets", + "label": "Delete assets that are not matched" + }, { "type": "dict", "collapsible": true, From b42fb6aedb10934f5836d89e1734e32669671350 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 23 Sep 2022 16:24:21 +0100 Subject: [PATCH 021/121] Fixed problem with transformations from FBX files --- .../plugins/load/load_layout_existing.py | 52 ++++++++----------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_layout_existing.py b/openpype/hosts/unreal/plugins/load/load_layout_existing.py index 8cd1950f7e..9ab27d0cef 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout_existing.py +++ b/openpype/hosts/unreal/plugins/load/load_layout_existing.py @@ -80,30 +80,22 @@ class ExistingLayoutLoader(plugin.Loader): raise NotImplementedError( f"Unreal version {ue_major} not supported") - @staticmethod - def _transform_from_basis(transform, basis, conversion): - """Transform a transform from a basis to a new basis.""" - # Get the basis matrix - basis_matrix = unreal.Matrix( - basis[0], - basis[1], - basis[2], - basis[3] - ) - transform_matrix = unreal.Matrix( - transform[0], - transform[1], - transform[2], - transform[3] - ) - - new_transform = ( - basis_matrix.get_inverse() * transform_matrix * basis_matrix) - - return conversion.inverse() * new_transform.transform() - def _get_transform(self, ext, import_data, lasset): conversion = unreal.Matrix.IDENTITY.transform() + fbx_tuning = unreal.Matrix.IDENTITY.transform() + + basis = unreal.Matrix( + lasset.get('basis')[0], + lasset.get('basis')[1], + lasset.get('basis')[2], + lasset.get('basis')[3] + ).transform() + transform = unreal.Matrix( + lasset.get('transform_matrix')[0], + lasset.get('transform_matrix')[1], + lasset.get('transform_matrix')[2], + lasset.get('transform_matrix')[3] + ).transform() # Check for the conversion settings. We cannot access # the alembic conversion settings, so we assume that @@ -111,11 +103,15 @@ class ExistingLayoutLoader(plugin.Loader): if ext == '.fbx': loc = import_data.import_translation rot = import_data.import_rotation.to_vector() - scale = import_data.import_scale + scale = import_data.import_uniform_scale conversion = unreal.Transform( location=[loc.x, loc.y, loc.z], rotation=[rot.x, rot.y, rot.z], - scale=[scale, scale, scale] + scale=[-scale, scale, scale] + ) + fbx_tuning = unreal.Transform( + rotation=[180.0, 0.0, 90.0], + scale=[1.0, 1.0, 1.0] ) elif ext == '.abc': # This is the standard conversion settings for @@ -126,12 +122,8 @@ class ExistingLayoutLoader(plugin.Loader): scale=[1.0, -1.0, 1.0] ) - transform = self._transform_from_basis( - lasset.get('transform_matrix'), - lasset.get('basis'), - conversion - ) - return transform + new_transform = (basis.inverse() * transform * basis) + return fbx_tuning * conversion.inverse() * new_transform def _spawn_actor(self, obj, lasset): actor = EditorLevelLibrary.spawn_actor_from_object( From a3eb15387108938f8536ed4b336e66c893e595aa Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 23 Sep 2022 16:34:05 +0100 Subject: [PATCH 022/121] Checks settings to determine if deleting or not unmatched assets --- .../plugins/load/load_layout_existing.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_layout_existing.py b/openpype/hosts/unreal/plugins/load/load_layout_existing.py index 9ab27d0cef..3ce99f8ef6 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout_existing.py +++ b/openpype/hosts/unreal/plugins/load/load_layout_existing.py @@ -15,6 +15,7 @@ from openpype.pipeline import ( AVALON_CONTAINER_ID, legacy_io, ) +from openpype.api import get_current_project_settings from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api import pipeline as upipeline @@ -147,7 +148,7 @@ class ExistingLayoutLoader(plugin.Loader): name = "" if family == 'rig': name = "SkeletalMeshFBXLoader" - elif family == 'model': + elif family == 'model' or family == 'staticMesh': name = "StaticMeshFBXLoader" elif family == 'camera': name = "CameraLoader" @@ -200,7 +201,8 @@ class ExistingLayoutLoader(plugin.Loader): loader = self._get_abc_loader(loaders, family) if not loader: - raise AssertionError(f"No valid loader found for {representation}") + self.log.error(f"No valid loader found for {representation}") + return [] # This option is necessary to avoid importing the assets with a # different conversion compared to the other assets. For ABC files, @@ -220,6 +222,9 @@ class ExistingLayoutLoader(plugin.Loader): return assets def _process(self, lib_path): + data = get_current_project_settings() + delete_unmatched = data["unreal"]["delete_unmatched_assets"] + ar = unreal.AssetRegistryHelpers.get_asset_registry() actors = EditorLevelLibrary.get_all_level_actors() @@ -264,16 +269,18 @@ class ExistingLayoutLoader(plugin.Loader): # Get the original path of the file from which the asset has # been imported. - actor.set_actor_label(lasset.get('instance_name')) smc = actor.get_editor_property('static_mesh_component') mesh = smc.get_editor_property('static_mesh') import_data = mesh.get_editor_property('asset_import_data') filename = import_data.get_first_filename() path = Path(filename) - if path.name not in repr_data.get('data').get('path'): + if (not path.name or + path.name not in repr_data.get('data').get('path')): continue + actor.set_actor_label(lasset.get('instance_name')) + mesh_path = Path(mesh.get_path_name()).parent.as_posix() # Create the container for the asset. @@ -352,7 +359,9 @@ class ExistingLayoutLoader(plugin.Loader): if not actor.get_class().get_name() == 'StaticMeshActor': continue if actor not in actors_matched: - EditorLevelLibrary.destroy_actor(actor) + self.log.warning(f"Actor {actor.get_name()} not matched.") + if delete_unmatched: + EditorLevelLibrary.destroy_actor(actor) return containers From 073a38726e3450409a9fd2bc7e6d789583c379e3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Sep 2022 15:00:49 +0200 Subject: [PATCH 023/121] footer widget is not part of subset widget --- openpype/tools/publisher/window.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 2a0e6e940a..3b504655d9 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -142,6 +142,9 @@ class PublisherWindow(QtWidgets.QDialog): subset_content_layout.addWidget(subset_attributes_wrap, 7) # Footer + footer_widget = QtWidgets.QWidget(self) + footer_bottom_widget = QtWidgets.QWidget(footer_widget) + comment_input = PlaceholderLineEdit(subset_frame) comment_input.setObjectName("PublishCommentInput") comment_input.setPlaceholderText( @@ -153,13 +156,17 @@ class PublisherWindow(QtWidgets.QDialog): validate_btn = ValidateBtn(subset_frame) publish_btn = PublishBtn(subset_frame) - footer_layout = QtWidgets.QHBoxLayout() - footer_layout.setContentsMargins(0, 0, 0, 0) - footer_layout.addWidget(comment_input, 1) - footer_layout.addWidget(reset_btn, 0) - footer_layout.addWidget(stop_btn, 0) - footer_layout.addWidget(validate_btn, 0) - footer_layout.addWidget(publish_btn, 0) + footer_bottom_layout = QtWidgets.QHBoxLayout(footer_bottom_widget) + footer_bottom_layout.setContentsMargins(0, 0, 0, 0) + footer_bottom_layout.addStretch(1) + footer_bottom_layout.addWidget(reset_btn, 0) + footer_bottom_layout.addWidget(stop_btn, 0) + footer_bottom_layout.addWidget(validate_btn, 0) + footer_bottom_layout.addWidget(publish_btn, 0) + + footer_layout = QtWidgets.QVBoxLayout(footer_widget) + footer_layout.addWidget(comment_input, 0) + footer_layout.addWidget(footer_bottom_widget, 0) # Subset frame layout subset_layout = QtWidgets.QVBoxLayout(subset_frame) @@ -167,10 +174,9 @@ class PublisherWindow(QtWidgets.QDialog): marings.setLeft(marings.left() * 2) marings.setRight(marings.right() * 2) marings.setTop(marings.top() * 2) - marings.setBottom(marings.bottom() * 2) + marings.setBottom(0) subset_layout.setContentsMargins(marings) subset_layout.addWidget(subset_content_widget, 1) - subset_layout.addLayout(footer_layout, 0) # Create publish frame publish_frame = PublishFrame(controller, content_stacked_widget) @@ -192,6 +198,7 @@ class PublisherWindow(QtWidgets.QDialog): main_layout.addWidget(header_widget, 0) main_layout.addWidget(line_widget, 0) main_layout.addWidget(content_stacked_widget, 1) + main_layout.addWidget(footer_widget, 0) creator_window = CreateDialog(controller, parent=self) From 46ea4561f31cc96537e3820c858385baa7e1b30b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Sep 2022 15:49:31 +0200 Subject: [PATCH 024/121] implemented tabs widget --- openpype/style/data.json | 1 + openpype/style/style.css | 23 +++++ openpype/tools/publisher/widgets/__init__.py | 6 ++ .../tools/publisher/widgets/tabs_widget.py | 83 +++++++++++++++++++ 4 files changed, 113 insertions(+) create mode 100644 openpype/tools/publisher/widgets/tabs_widget.py diff --git a/openpype/style/data.json b/openpype/style/data.json index adda49de23..b75aa98508 100644 --- a/openpype/style/data.json +++ b/openpype/style/data.json @@ -91,6 +91,7 @@ "error": "#AA5050", "success": "#458056", "warning": "#ffc671", + "tab-bg": "#16191d", "list-view-group": { "bg": "#434a56", "bg-hover": "rgba(168, 175, 189, 0.3)", diff --git a/openpype/style/style.css b/openpype/style/style.css index 72d12a9230..ab23dd621f 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -856,6 +856,29 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { } /* New Create/Publish UI */ +PublisherTabsWidget { + background: {color:publisher:tab-bg}; +} + +PublisherTabBtn { + border-radius: 0px; + background: {color:bg-inputs}; + font-size: 9pt; + font-weight: regular; + padding: 0.5em 1em 0.5em 1em; +} + +PublisherTabBtn:hover { + background: {color:bg-buttons}; +} + +PublisherTabBtn[active="1"] { + background: {color:bg}; +} +PublisherTabBtn[active="1"]:hover { + background: {color:bg}; +} + #CreatorDetailedDescription { padding-left: 5px; padding-right: 5px; diff --git a/openpype/tools/publisher/widgets/__init__.py b/openpype/tools/publisher/widgets/__init__.py index 55afc349ff..a09e1353ec 100644 --- a/openpype/tools/publisher/widgets/__init__.py +++ b/openpype/tools/publisher/widgets/__init__.py @@ -33,6 +33,10 @@ from .list_view_widgets import ( InstanceListView ) +from .tabs_widget import ( + PublisherTabsWidget +) + __all__ = ( "get_icon_path", @@ -57,4 +61,6 @@ __all__ = ( "InstanceCardView", "InstanceListView", + + "PublisherTabsWidget", ) diff --git a/openpype/tools/publisher/widgets/tabs_widget.py b/openpype/tools/publisher/widgets/tabs_widget.py new file mode 100644 index 0000000000..0e92a6fd8d --- /dev/null +++ b/openpype/tools/publisher/widgets/tabs_widget.py @@ -0,0 +1,83 @@ +from Qt import QtWidgets, QtCore +from openpype.tools.utils import set_style_property + + +class PublisherTabBtn(QtWidgets.QPushButton): + tab_clicked = QtCore.Signal(str) + + def __init__(self, identifier, label, parent): + super(PublisherTabBtn, self).__init__(label, parent) + self._identifier = identifier + self._active = False + + self.clicked.connect(self._on_click) + + def _on_click(self): + self.tab_clicked.emit(self.identifier) + + @property + def identifier(self): + return self._identifier + + def activate(self): + if self._active: + return + self._active = True + set_style_property(self, "active", "1") + + def deactivate(self): + if not self._active: + return + self._active = False + set_style_property(self, "active", "") + + +class PublisherTabsWidget(QtWidgets.QFrame): + tab_changed = QtCore.Signal(str, str) + + def __init__(self, parent=None): + super(PublisherTabsWidget, self).__init__(parent) + + btns_widget = QtWidgets.QWidget(self) + btns_layout = QtWidgets.QHBoxLayout(btns_widget) + btns_layout.setContentsMargins(0, 0, 0, 0) + btns_layout.setSpacing(0) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(btns_widget, 0) + layout.addStretch(1) + + self._btns_layout = btns_layout + + self._current_button = None + self._buttons_by_identifier = {} + + def add_tab(self, label, identifier): + button = PublisherTabBtn(identifier, label, self) + button.tab_clicked.connect(self._on_tab_click) + self._btns_layout.addWidget(button, 0) + self._buttons_by_identifier[identifier] = button + + if self._current_button is None: + self.set_current_tab(identifier) + + def set_current_tab(self, identifier): + if identifier == self._current_button: + return + + new_btn = self._buttons_by_identifier.get(identifier) + if new_btn is None: + return + + old_identifier = self._current_button + old_btn = self._buttons_by_identifier.get(old_identifier) + self._current_button = identifier + + if old_btn is not None: + old_btn.deactivate() + new_btn.activate() + self.tab_changed.emit(old_identifier, identifier) + + def _on_tab_click(self, identifier): + self.set_current_tab(identifier) From ae62357d9873fec68e55fbfc545c8b9a61409667 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Sep 2022 16:00:22 +0200 Subject: [PATCH 025/121] added tab to main window --- openpype/tools/publisher/window.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 3b504655d9..d02fe704ee 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -15,6 +15,8 @@ from .widgets import ( SubsetAttributesWidget, InstanceCardView, InstanceListView, + PublisherTabsWidget, + CreateDialog, StopBtn, @@ -78,9 +80,11 @@ class PublisherWindow(QtWidgets.QDialog): header_layout.addWidget(icon_label, 0) header_layout.addWidget(context_label, 1) - line_widget = QtWidgets.QWidget(self) - line_widget.setObjectName("Separator") - line_widget.setMinimumHeight(2) + tabs_widget = PublisherTabsWidget(self) + tabs_widget.add_tab("Create", "create") + tabs_widget.add_tab("Publish", "publish") + tabs_widget.add_tab("Report", "report") + tabs_widget.add_tab("Details", "details") # Content content_stacked_widget = QtWidgets.QWidget(self) @@ -196,12 +200,14 @@ class PublisherWindow(QtWidgets.QDialog): main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setSpacing(0) main_layout.addWidget(header_widget, 0) - main_layout.addWidget(line_widget, 0) + main_layout.addWidget(tabs_widget, 0) main_layout.addWidget(content_stacked_widget, 1) main_layout.addWidget(footer_widget, 0) creator_window = CreateDialog(controller, parent=self) + tabs_widget.tab_changed.connect(self._on_tab_change) + create_btn.clicked.connect(self._on_create_clicked) delete_btn.clicked.connect(self._on_delete_clicked) change_view_btn.clicked.connect(self._on_change_view_clicked) @@ -318,6 +324,9 @@ class PublisherWindow(QtWidgets.QDialog): self._on_subset_change() + def _on_tab_change(self, prev_tab, new_tab): + print(prev_tab, new_tab) + def _on_create_clicked(self): self.creator_window.show() From 7a0ce610b1472332719ec19e3ca98badbf8ab8c6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Sep 2022 16:41:12 +0200 Subject: [PATCH 026/121] moved overview widget from window --- openpype/tools/publisher/widgets/__init__.py | 43 +-- .../publisher/widgets/overview_widget.py | 246 +++++++++++++ openpype/tools/publisher/window.py | 340 ++++-------------- openpype/tools/traypublisher/window.py | 2 +- 4 files changed, 328 insertions(+), 303 deletions(-) create mode 100644 openpype/tools/publisher/widgets/overview_widget.py diff --git a/openpype/tools/publisher/widgets/__init__.py b/openpype/tools/publisher/widgets/__init__.py index a09e1353ec..869f7adf9b 100644 --- a/openpype/tools/publisher/widgets/__init__.py +++ b/openpype/tools/publisher/widgets/__init__.py @@ -3,64 +3,31 @@ from .icons import ( get_pixmap, get_icon ) -from .border_label_widget import ( - BorderedLabelWidget -) from .widgets import ( - SubsetAttributesWidget, - StopBtn, ResetBtn, ValidateBtn, PublishBtn, - - CreateInstanceBtn, - RemoveInstanceBtn, - ChangeViewBtn ) -from .publish_widget import ( - PublishFrame -) -from .create_dialog import ( - CreateDialog -) - -from .card_view_widgets import ( - InstanceCardView -) - -from .list_view_widgets import ( - InstanceListView -) - -from .tabs_widget import ( - PublisherTabsWidget -) - +from .publish_widget import PublishFrame +from .create_dialog import CreateDialog +from .tabs_widget import PublisherTabsWidget +from .overview_widget import CreateOverviewWidget __all__ = ( "get_icon_path", "get_pixmap", "get_icon", - "SubsetAttributesWidget", - "BorderedLabelWidget", - "StopBtn", "ResetBtn", "ValidateBtn", "PublishBtn", - "CreateInstanceBtn", - "RemoveInstanceBtn", - "ChangeViewBtn", - "PublishFrame", "CreateDialog", - "InstanceCardView", - "InstanceListView", - "PublisherTabsWidget", + "CreateOverviewWidget", ) diff --git a/openpype/tools/publisher/widgets/overview_widget.py b/openpype/tools/publisher/widgets/overview_widget.py new file mode 100644 index 0000000000..abdd98ff7c --- /dev/null +++ b/openpype/tools/publisher/widgets/overview_widget.py @@ -0,0 +1,246 @@ +from Qt import QtWidgets, QtCore + +from .border_label_widget import BorderedLabelWidget + +from .card_view_widgets import InstanceCardView +from .list_view_widgets import InstanceListView +from .widgets import ( + SubsetAttributesWidget, + CreateInstanceBtn, + RemoveInstanceBtn, + ChangeViewBtn +) + + +class CreateOverviewWidget(QtWidgets.QFrame): + active_changed = QtCore.Signal() + instance_context_changed = QtCore.Signal() + create_requested = QtCore.Signal() + + def __init__(self, controller, parent): + super(CreateOverviewWidget, self).__init__(parent) + + self._controller = controller + self._refreshing_instances = False + + subset_views_widget = BorderedLabelWidget( + "Subsets to publish", self + ) + + subset_view_cards = InstanceCardView(controller, subset_views_widget) + subset_list_view = InstanceListView(controller, subset_views_widget) + + subset_views_layout = QtWidgets.QStackedLayout() + subset_views_layout.addWidget(subset_view_cards) + subset_views_layout.addWidget(subset_list_view) + + # Buttons at the bottom of subset view + create_btn = CreateInstanceBtn(self) + delete_btn = RemoveInstanceBtn(self) + change_view_btn = ChangeViewBtn(self) + + # Subset details widget + subset_attributes_wrap = BorderedLabelWidget( + "Publish options", self + ) + subset_attributes_widget = SubsetAttributesWidget( + controller, subset_attributes_wrap + ) + subset_attributes_wrap.set_center_widget(subset_attributes_widget) + + # Layout of buttons at the bottom of subset view + subset_view_btns_layout = QtWidgets.QHBoxLayout() + subset_view_btns_layout.setContentsMargins(0, 5, 0, 0) + subset_view_btns_layout.addWidget(create_btn) + subset_view_btns_layout.addSpacing(5) + subset_view_btns_layout.addWidget(delete_btn) + subset_view_btns_layout.addStretch(1) + subset_view_btns_layout.addWidget(change_view_btn) + + # Layout of view and buttons + # - widget 'subset_view_widget' is necessary + # - only layout won't be resized automatically to minimum size hint + # on child resize request! + subset_view_widget = QtWidgets.QWidget(subset_views_widget) + subset_view_layout = QtWidgets.QVBoxLayout(subset_view_widget) + subset_view_layout.setContentsMargins(0, 0, 0, 0) + subset_view_layout.addLayout(subset_views_layout, 1) + subset_view_layout.addLayout(subset_view_btns_layout, 0) + + subset_views_widget.set_center_widget(subset_view_widget) + + # Whole subset layout with attributes and details + subset_content_widget = QtWidgets.QWidget(self) + subset_content_layout = QtWidgets.QHBoxLayout(subset_content_widget) + subset_content_layout.setContentsMargins(0, 0, 0, 0) + subset_content_layout.addWidget(subset_views_widget, 3) + subset_content_layout.addWidget(subset_attributes_wrap, 7) + + # Subset frame layout + main_layout = QtWidgets.QVBoxLayout(self) + marings = main_layout.contentsMargins() + marings.setLeft(marings.left() * 2) + marings.setRight(marings.right() * 2) + marings.setTop(marings.top() * 2) + marings.setBottom(0) + main_layout.setContentsMargins(marings) + main_layout.addWidget(subset_content_widget, 1) + + create_btn.clicked.connect(self._on_create_clicked) + delete_btn.clicked.connect(self._on_delete_clicked) + change_view_btn.clicked.connect(self._on_change_view_clicked) + + # Selection changed + subset_list_view.selection_changed.connect( + self._on_subset_change + ) + subset_view_cards.selection_changed.connect( + self._on_subset_change + ) + # Active instances changed + subset_list_view.active_changed.connect( + self._on_active_changed + ) + subset_view_cards.active_changed.connect( + self._on_active_changed + ) + # Instance context has changed + subset_attributes_widget.instance_context_changed.connect( + self._on_instance_context_change + ) + + controller.add_publish_reset_callback(self._on_publish_reset) + controller.add_instances_refresh_callback(self._on_instances_refresh) + + self.subset_content_widget = subset_content_widget + + self.subset_view_cards = subset_view_cards + self.subset_list_view = subset_list_view + self.subset_views_layout = subset_views_layout + + self.delete_btn = delete_btn + + self.subset_attributes_widget = subset_attributes_widget + + def _on_create_clicked(self): + """Pass signal to parent widget which should care about changing state. + + We don't change anything here until the parent will care about it. + """ + + self.create_requested.emit() + + def _on_delete_clicked(self): + instances, _ = self.get_selected_items() + + # Ask user if he really wants to remove instances + dialog = QtWidgets.QMessageBox(self) + dialog.setIcon(QtWidgets.QMessageBox.Question) + dialog.setWindowTitle("Are you sure?") + if len(instances) > 1: + msg = ( + "Do you really want to remove {} instances?" + ).format(len(instances)) + else: + msg = ( + "Do you really want to remove the instance?" + ) + dialog.setText(msg) + dialog.setStandardButtons( + QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel + ) + dialog.setDefaultButton(QtWidgets.QMessageBox.Ok) + dialog.setEscapeButton(QtWidgets.QMessageBox.Cancel) + dialog.exec_() + # Skip if OK was not clicked + if dialog.result() == QtWidgets.QMessageBox.Ok: + self._controller.remove_instances(instances) + + def _on_change_view_clicked(self): + self._change_view_type() + + def _on_subset_change(self, *_args): + # Ignore changes if in middle of refreshing + if self._refreshing_instances: + return + + instances, context_selected = self.get_selected_items() + + # Disable delete button if nothing is selected + self.delete_btn.setEnabled(len(instances) > 0) + + self.subset_attributes_widget.set_current_instances( + instances, context_selected + ) + + def _on_active_changed(self): + if self._refreshing_instances: + return + self.active_changed.emit() + + def _on_instance_context_change(self): + current_idx = self.subset_views_layout.currentIndex() + for idx in range(self.subset_views_layout.count()): + if idx == current_idx: + continue + widget = self.subset_views_layout.widget(idx) + if widget.refreshed: + widget.set_refreshed(False) + + current_widget = self.subset_views_layout.widget(current_idx) + current_widget.refresh_instance_states() + + self.instance_context_changed.emit() + + def get_selected_items(self): + view = self.subset_views_layout.currentWidget() + return view.get_selected_items() + + def _change_view_type(self): + idx = self.subset_views_layout.currentIndex() + new_idx = (idx + 1) % self.subset_views_layout.count() + self.subset_views_layout.setCurrentIndex(new_idx) + + new_view = self.subset_views_layout.currentWidget() + if not new_view.refreshed: + new_view.refresh() + new_view.set_refreshed(True) + else: + new_view.refresh_instance_states() + + self._on_subset_change() + + def _refresh_instances(self): + if self._refreshing_instances: + return + + self._refreshing_instances = True + + for idx in range(self.subset_views_layout.count()): + widget = self.subset_views_layout.widget(idx) + widget.set_refreshed(False) + + view = self.subset_views_layout.currentWidget() + view.refresh() + view.set_refreshed(True) + + self._refreshing_instances = False + + # Force to change instance and refresh details + self._on_subset_change() + + def _on_publish_reset(self): + """Context in controller has been refreshed.""" + + self.subset_content_widget.setEnabled(self._controller.host_is_valid) + + def _on_instances_refresh(self): + """Controller refreshed instances.""" + + self._refresh_instances() + + # Give a change to process Resize Request + QtWidgets.QApplication.processEvents() + # Trigger update geometry of + widget = self.subset_views_layout.currentWidget() + widget.updateGeometry() diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index d02fe704ee..8df9f9bbf5 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -10,11 +10,9 @@ from openpype.tools.utils import ( ) from .control import PublisherController from .widgets import ( - BorderedLabelWidget, + CreateOverviewWidget, PublishFrame, - SubsetAttributesWidget, - InstanceCardView, - InstanceListView, + PublisherTabsWidget, CreateDialog, @@ -23,10 +21,6 @@ from .widgets import ( ResetBtn, ValidateBtn, PublishBtn, - - CreateInstanceBtn, - RemoveInstanceBtn, - ChangeViewBtn ) @@ -62,7 +56,6 @@ class PublisherWindow(QtWidgets.QDialog): self._reset_on_show = reset_on_show self._first_show = True - self._refreshing_instances = False controller = PublisherController() @@ -78,8 +71,10 @@ class PublisherWindow(QtWidgets.QDialog): header_layout.setContentsMargins(15, 15, 15, 15) header_layout.setSpacing(15) header_layout.addWidget(icon_label, 0) - header_layout.addWidget(context_label, 1) + header_layout.addWidget(context_label, 0) + header_layout.addStretch(1) + # Tabs widget under header tabs_widget = PublisherTabsWidget(self) tabs_widget.add_tab("Create", "create") tabs_widget.add_tab("Publish", "publish") @@ -89,76 +84,24 @@ class PublisherWindow(QtWidgets.QDialog): # Content content_stacked_widget = QtWidgets.QWidget(self) - # Subset widget - subset_frame = QtWidgets.QFrame(content_stacked_widget) - - subset_views_widget = BorderedLabelWidget( - "Subsets to publish", subset_frame + create_overview_widget = CreateOverviewWidget( + controller, content_stacked_widget ) - subset_view_cards = InstanceCardView(controller, subset_views_widget) - subset_list_view = InstanceListView(controller, subset_views_widget) - - subset_views_layout = QtWidgets.QStackedLayout() - subset_views_layout.addWidget(subset_view_cards) - subset_views_layout.addWidget(subset_list_view) - - # Buttons at the bottom of subset view - create_btn = CreateInstanceBtn(subset_frame) - delete_btn = RemoveInstanceBtn(subset_frame) - change_view_btn = ChangeViewBtn(subset_frame) - - # Subset details widget - subset_attributes_wrap = BorderedLabelWidget( - "Publish options", subset_frame - ) - subset_attributes_widget = SubsetAttributesWidget( - controller, subset_attributes_wrap - ) - subset_attributes_wrap.set_center_widget(subset_attributes_widget) - - # Layout of buttons at the bottom of subset view - subset_view_btns_layout = QtWidgets.QHBoxLayout() - subset_view_btns_layout.setContentsMargins(0, 5, 0, 0) - subset_view_btns_layout.addWidget(create_btn) - subset_view_btns_layout.addSpacing(5) - subset_view_btns_layout.addWidget(delete_btn) - subset_view_btns_layout.addStretch(1) - subset_view_btns_layout.addWidget(change_view_btn) - - # Layout of view and buttons - # - widget 'subset_view_widget' is necessary - # - only layout won't be resized automatically to minimum size hint - # on child resize request! - subset_view_widget = QtWidgets.QWidget(subset_views_widget) - subset_view_layout = QtWidgets.QVBoxLayout(subset_view_widget) - subset_view_layout.setContentsMargins(0, 0, 0, 0) - subset_view_layout.addLayout(subset_views_layout, 1) - subset_view_layout.addLayout(subset_view_btns_layout, 0) - - subset_views_widget.set_center_widget(subset_view_widget) - - # Whole subset layout with attributes and details - subset_content_widget = QtWidgets.QWidget(subset_frame) - subset_content_layout = QtWidgets.QHBoxLayout(subset_content_widget) - subset_content_layout.setContentsMargins(0, 0, 0, 0) - subset_content_layout.addWidget(subset_views_widget, 3) - subset_content_layout.addWidget(subset_attributes_wrap, 7) - # Footer footer_widget = QtWidgets.QWidget(self) footer_bottom_widget = QtWidgets.QWidget(footer_widget) - comment_input = PlaceholderLineEdit(subset_frame) + comment_input = PlaceholderLineEdit(footer_widget) comment_input.setObjectName("PublishCommentInput") comment_input.setPlaceholderText( "Attach a comment to your publish" ) - reset_btn = ResetBtn(subset_frame) - stop_btn = StopBtn(subset_frame) - validate_btn = ValidateBtn(subset_frame) - publish_btn = PublishBtn(subset_frame) + reset_btn = ResetBtn(footer_widget) + stop_btn = StopBtn(footer_widget) + validate_btn = ValidateBtn(footer_widget) + publish_btn = PublishBtn(footer_widget) footer_bottom_layout = QtWidgets.QHBoxLayout(footer_bottom_widget) footer_bottom_layout.setContentsMargins(0, 0, 0, 0) @@ -172,16 +115,6 @@ class PublisherWindow(QtWidgets.QDialog): footer_layout.addWidget(comment_input, 0) footer_layout.addWidget(footer_bottom_widget, 0) - # Subset frame layout - subset_layout = QtWidgets.QVBoxLayout(subset_frame) - marings = subset_layout.contentsMargins() - marings.setLeft(marings.left() * 2) - marings.setRight(marings.right() * 2) - marings.setTop(marings.top() * 2) - marings.setBottom(0) - subset_layout.setContentsMargins(marings) - subset_layout.addWidget(subset_content_widget, 1) - # Create publish frame publish_frame = PublishFrame(controller, content_stacked_widget) @@ -192,7 +125,7 @@ class PublisherWindow(QtWidgets.QDialog): content_stacked_layout.setStackingMode( QtWidgets.QStackedLayout.StackAll ) - content_stacked_layout.addWidget(subset_frame) + content_stacked_layout.addWidget(create_overview_widget) content_stacked_layout.addWidget(publish_frame) # Add main frame to this window @@ -207,37 +140,22 @@ class PublisherWindow(QtWidgets.QDialog): creator_window = CreateDialog(controller, parent=self) tabs_widget.tab_changed.connect(self._on_tab_change) - - create_btn.clicked.connect(self._on_create_clicked) - delete_btn.clicked.connect(self._on_delete_clicked) - change_view_btn.clicked.connect(self._on_change_view_clicked) + create_overview_widget.active_changed.connect( + self._on_context_or_active_change + ) + create_overview_widget.instance_context_changed.connect( + self._on_context_or_active_change + ) + create_overview_widget.create_requested.connect( + self._on_create_request + ) reset_btn.clicked.connect(self._on_reset_clicked) stop_btn.clicked.connect(self._on_stop_clicked) validate_btn.clicked.connect(self._on_validate_clicked) publish_btn.clicked.connect(self._on_publish_clicked) - # Selection changed - subset_list_view.selection_changed.connect( - self._on_subset_change - ) - subset_view_cards.selection_changed.connect( - self._on_subset_change - ) - # Active instances changed - subset_list_view.active_changed.connect( - self._on_active_changed - ) - subset_view_cards.active_changed.connect( - self._on_active_changed - ) - # Instance context has changed - subset_attributes_widget.instance_context_changed.connect( - self._on_instance_context_change - ) - controller.add_instances_refresh_callback(self._on_instances_refresh) - controller.add_publish_reset_callback(self._on_publish_reset) controller.add_publish_started_callback(self._on_publish_start) controller.add_publish_validated_callback(self._on_publish_validated) @@ -246,22 +164,15 @@ class PublisherWindow(QtWidgets.QDialog): # Store header for TrayPublisher self._header_layout = header_layout + self._tabs_widget = tabs_widget + self._content_stacked_widget = content_stacked_widget self.content_stacked_layout = content_stacked_layout + self._create_overview_widget = create_overview_widget self.publish_frame = publish_frame - self.subset_frame = subset_frame - self.subset_content_widget = subset_content_widget self.context_label = context_label - self.subset_view_cards = subset_view_cards - self.subset_list_view = subset_list_view - self.subset_views_layout = subset_views_layout - - self.delete_btn = delete_btn - - self.subset_attributes_widget = subset_attributes_widget - self.comment_input = comment_input self.stop_btn = stop_btn @@ -269,10 +180,14 @@ class PublisherWindow(QtWidgets.QDialog): self.validate_btn = validate_btn self.publish_btn = publish_btn - self.controller = controller + self._controller = controller self.creator_window = creator_window + @property + def controller(self): + return self._controller + def showEvent(self, event): super(PublisherWindow, self).showEvent(event) if self._first_show: @@ -283,88 +198,33 @@ class PublisherWindow(QtWidgets.QDialog): self.reset() def closeEvent(self, event): - self.controller.save_changes() + self._controller.save_changes() super(PublisherWindow, self).closeEvent(event) def reset(self): - self.controller.reset() + self._controller.reset() def set_context_label(self, label): self.context_label.setText(label) - def get_selected_items(self): - view = self.subset_views_layout.currentWidget() - return view.get_selected_items() - - def _on_instance_context_change(self): - current_idx = self.subset_views_layout.currentIndex() - for idx in range(self.subset_views_layout.count()): - if idx == current_idx: - continue - widget = self.subset_views_layout.widget(idx) - if widget.refreshed: - widget.set_refreshed(False) - - current_widget = self.subset_views_layout.widget(current_idx) - current_widget.refresh_instance_states() - - self._validate_create_instances() - - def _change_view_type(self): - idx = self.subset_views_layout.currentIndex() - new_idx = (idx + 1) % self.subset_views_layout.count() - self.subset_views_layout.setCurrentIndex(new_idx) - - new_view = self.subset_views_layout.currentWidget() - if not new_view.refreshed: - new_view.refresh() - new_view.set_refreshed(True) - else: - new_view.refresh_instance_states() - - self._on_subset_change() - def _on_tab_change(self, prev_tab, new_tab): print(prev_tab, new_tab) - def _on_create_clicked(self): - self.creator_window.show() + def _on_context_or_active_change(self): + self._validate_create_instances() - def _on_delete_clicked(self): - instances, _ = self.get_selected_items() + def _on_create_request(self): + self._go_to_create_tab() - # Ask user if he really wants to remove instances - dialog = QtWidgets.QMessageBox(self) - dialog.setIcon(QtWidgets.QMessageBox.Question) - dialog.setWindowTitle("Are you sure?") - if len(instances) > 1: - msg = ( - "Do you really want to remove {} instances?" - ).format(len(instances)) - else: - msg = ( - "Do you really want to remove the instance?" - ) - dialog.setText(msg) - dialog.setStandardButtons( - QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel - ) - dialog.setDefaultButton(QtWidgets.QMessageBox.Ok) - dialog.setEscapeButton(QtWidgets.QMessageBox.Cancel) - dialog.exec_() - # Skip if OK was not clicked - if dialog.result() == QtWidgets.QMessageBox.Ok: - self.controller.remove_instances(instances) - - def _on_change_view_clicked(self): - self._change_view_type() + def _go_to_create_tab(self): + self._tabs_widget.set_current_tab("create") def _set_publish_visibility(self, visible): if visible: widget = self.publish_frame publish_frame_visible = True else: - widget = self.subset_frame + widget = self._create_overview_widget publish_frame_visible = False self.content_stacked_layout.setCurrentWidget(widget) self._set_publish_frame_visible(publish_frame_visible) @@ -381,79 +241,27 @@ class PublisherWindow(QtWidgets.QDialog): self.creator_window.close() def _on_reset_clicked(self): - self.controller.reset() + self._controller.reset() def _on_stop_clicked(self): - self.controller.stop_publish() + self._controller.stop_publish() def _set_publish_comment(self): - if self.controller.publish_comment_is_set: + if self._controller.publish_comment_is_set: return comment = self.comment_input.text() - self.controller.set_comment(comment) + self._controller.set_comment(comment) def _on_validate_clicked(self): self._set_publish_comment() self._set_publish_visibility(True) - self.controller.validate() + self._controller.validate() def _on_publish_clicked(self): self._set_publish_comment() self._set_publish_visibility(True) - self.controller.publish() - - def _refresh_instances(self): - if self._refreshing_instances: - return - - self._refreshing_instances = True - - for idx in range(self.subset_views_layout.count()): - widget = self.subset_views_layout.widget(idx) - widget.set_refreshed(False) - - view = self.subset_views_layout.currentWidget() - view.refresh() - view.set_refreshed(True) - - self._refreshing_instances = False - - # Force to change instance and refresh details - self._on_subset_change() - - def _on_instances_refresh(self): - self._refresh_instances() - - self._validate_create_instances() - - context_title = self.controller.get_context_title() - self.set_context_label(context_title) - - # Give a change to process Resize Request - QtWidgets.QApplication.processEvents() - # Trigger update geometry of - widget = self.subset_views_layout.currentWidget() - widget.updateGeometry() - - def _on_subset_change(self, *_args): - # Ignore changes if in middle of refreshing - if self._refreshing_instances: - return - - instances, context_selected = self.get_selected_items() - - # Disable delete button if nothing is selected - self.delete_btn.setEnabled(len(instances) > 0) - - self.subset_attributes_widget.set_current_instances( - instances, context_selected - ) - - def _on_active_changed(self): - if self._refreshing_instances: - return - self._validate_create_instances() + self._controller.publish() def _set_footer_enabled(self, enabled): self.comment_input.setEnabled(enabled) @@ -467,30 +275,9 @@ class PublisherWindow(QtWidgets.QDialog): self.validate_btn.setEnabled(enabled) self.publish_btn.setEnabled(enabled) - def _validate_create_instances(self): - if not self.controller.host_is_valid: - self._set_footer_enabled(True) - return - - all_valid = None - for instance in self.controller.instances: - if not instance["active"]: - continue - - if not instance.has_valid_context: - all_valid = False - break - - if all_valid is None: - all_valid = True - - self._set_footer_enabled(bool(all_valid)) - def _on_publish_reset(self): self._set_publish_visibility(False) - self.subset_content_widget.setEnabled(self.controller.host_is_valid) - self._set_footer_enabled(False) def _on_publish_start(self): @@ -505,19 +292,44 @@ class PublisherWindow(QtWidgets.QDialog): def _on_publish_stop(self): self.reset_btn.setEnabled(True) self.stop_btn.setEnabled(False) - validate_enabled = not self.controller.publish_has_crashed - publish_enabled = not self.controller.publish_has_crashed + validate_enabled = not self._controller.publish_has_crashed + publish_enabled = not self._controller.publish_has_crashed if validate_enabled: - validate_enabled = not self.controller.publish_has_validated + validate_enabled = not self._controller.publish_has_validated if publish_enabled: if ( - self.controller.publish_has_validated - and self.controller.publish_has_validation_errors + self._controller.publish_has_validated + and self._controller.publish_has_validation_errors ): publish_enabled = False else: - publish_enabled = not self.controller.publish_has_finished + publish_enabled = not self._controller.publish_has_finished self.validate_btn.setEnabled(validate_enabled) self.publish_btn.setEnabled(publish_enabled) + + def _validate_create_instances(self): + if not self._controller.host_is_valid: + self._set_footer_enabled(True) + return + + all_valid = None + for instance in self._controller.instances: + if not instance["active"]: + continue + + if not instance.has_valid_context: + all_valid = False + break + + if all_valid is None: + all_valid = True + + self._set_footer_enabled(bool(all_valid)) + + def _on_instances_refresh(self): + self._validate_create_instances() + + context_title = self.controller.get_context_title() + self.set_context_label(context_title) diff --git a/openpype/tools/traypublisher/window.py b/openpype/tools/traypublisher/window.py index 930c27ca9c..128c0fef11 100644 --- a/openpype/tools/traypublisher/window.py +++ b/openpype/tools/traypublisher/window.py @@ -244,7 +244,7 @@ class TrayPublishWindow(PublisherWindow): self.reset() if not self.controller.instances: - self._on_create_clicked() + self._go_to_create_tab() def _on_tray_publish_save(self): self.controller.save_changes() From 05372493a3fcb21903ddc12a310ec258b6c9e20c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Sep 2022 17:45:35 +0200 Subject: [PATCH 027/121] added create widget to overview widget --- .../tools/publisher/widgets/assets_widget.py | 10 +- .../tools/publisher/widgets/create_widget.py | 986 ++++++++++++++++++ .../publisher/widgets/overview_widget.py | 74 +- .../tools/publisher/widgets/tasks_widget.py | 6 +- 4 files changed, 1045 insertions(+), 31 deletions(-) create mode 100644 openpype/tools/publisher/widgets/create_widget.py diff --git a/openpype/tools/publisher/widgets/assets_widget.py b/openpype/tools/publisher/widgets/assets_widget.py index 46fdcc6526..7a77c9e898 100644 --- a/openpype/tools/publisher/widgets/assets_widget.py +++ b/openpype/tools/publisher/widgets/assets_widget.py @@ -13,13 +13,13 @@ from openpype.tools.utils.assets_widget import ( ) -class CreateDialogAssetsWidget(SingleSelectAssetsWidget): +class CreateWidgetAssetsWidget(SingleSelectAssetsWidget): current_context_required = QtCore.Signal() header_height_changed = QtCore.Signal(int) def __init__(self, controller, parent): self._controller = controller - super(CreateDialogAssetsWidget, self).__init__(None, parent) + super(CreateWidgetAssetsWidget, self).__init__(None, parent) self.set_refresh_btn_visibility(False) self.set_current_asset_btn_visibility(False) @@ -42,11 +42,11 @@ class CreateDialogAssetsWidget(SingleSelectAssetsWidget): self.header_height_changed.emit(height) def resizeEvent(self, event): - super(CreateDialogAssetsWidget, self).resizeEvent(event) + super(CreateWidgetAssetsWidget, self).resizeEvent(event) self._check_header_height() def showEvent(self, event): - super(CreateDialogAssetsWidget, self).showEvent(event) + super(CreateWidgetAssetsWidget, self).showEvent(event) self._check_header_height() def _on_current_asset_click(self): @@ -63,7 +63,7 @@ class CreateDialogAssetsWidget(SingleSelectAssetsWidget): self.select_asset(self._last_selection) def _select_indexes(self, *args, **kwargs): - super(CreateDialogAssetsWidget, self)._select_indexes(*args, **kwargs) + super(CreateWidgetAssetsWidget, self)._select_indexes(*args, **kwargs) if self._enabled: return self._last_selection = self.get_selected_asset_id() diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py new file mode 100644 index 0000000000..a0b3db0409 --- /dev/null +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -0,0 +1,986 @@ +import sys +import re +import traceback +import copy + +import qtawesome +try: + import commonmark +except Exception: + commonmark = None +from Qt import QtWidgets, QtCore, QtGui + +from openpype.client import get_asset_by_name, get_subsets +from openpype.pipeline.create import ( + CreatorError, + SUBSET_NAME_ALLOWED_SYMBOLS, + TaskNotSetError, +) +from openpype.tools.utils import ( + ErrorMessageBox, + MessageOverlayObject, + ClickableFrame, +) + +from .widgets import IconValuePixmapLabel +from .assets_widget import CreateWidgetAssetsWidget +from .tasks_widget import CreateWidgetTasksWidget +from .precreate_widget import PreCreateWidget +from ..constants import ( + VARIANT_TOOLTIP, + CREATOR_IDENTIFIER_ROLE, + FAMILY_ROLE +) + +SEPARATORS = ("---separator---", "---") + + +class VariantInputsWidget(QtWidgets.QWidget): + resized = QtCore.Signal() + + def resizeEvent(self, event): + super(VariantInputsWidget, self).resizeEvent(event) + self.resized.emit() + + +class CreateErrorMessageBox(ErrorMessageBox): + def __init__( + self, + creator_label, + subset_name, + asset_name, + exc_msg, + formatted_traceback, + parent + ): + self._creator_label = creator_label + self._subset_name = subset_name + self._asset_name = asset_name + self._exc_msg = exc_msg + self._formatted_traceback = formatted_traceback + super(CreateErrorMessageBox, self).__init__("Creation failed", parent) + + def _create_top_widget(self, parent_widget): + label_widget = QtWidgets.QLabel(parent_widget) + label_widget.setText( + "Failed to create" + ) + return label_widget + + def _get_report_data(self): + report_message = ( + "{creator}: Failed to create Subset: \"{subset}\"" + " in Asset: \"{asset}\"" + "\n\nError: {message}" + ).format( + creator=self._creator_label, + subset=self._subset_name, + asset=self._asset_name, + message=self._exc_msg, + ) + if self._formatted_traceback: + report_message += "\n\n{}".format(self._formatted_traceback) + return [report_message] + + def _create_content(self, content_layout): + item_name_template = ( + "Creator: {}
" + "Subset: {}
" + "Asset: {}
" + ) + exc_msg_template = "{}" + + line = self._create_line() + content_layout.addWidget(line) + + item_name_widget = QtWidgets.QLabel(self) + item_name_widget.setText( + item_name_template.format( + self._creator_label, self._subset_name, self._asset_name + ) + ) + content_layout.addWidget(item_name_widget) + + message_label_widget = QtWidgets.QLabel(self) + message_label_widget.setText( + exc_msg_template.format(self.convert_text_for_html(self._exc_msg)) + ) + content_layout.addWidget(message_label_widget) + + if self._formatted_traceback: + line_widget = self._create_line() + tb_widget = self._create_traceback_widget( + self._formatted_traceback + ) + content_layout.addWidget(line_widget) + content_layout.addWidget(tb_widget) + + +# TODO add creator identifier/label to details +class CreatorShortDescWidget(QtWidgets.QWidget): + height_changed = QtCore.Signal(int) + + def __init__(self, parent=None): + super(CreatorShortDescWidget, self).__init__(parent=parent) + + # --- Short description widget --- + icon_widget = IconValuePixmapLabel(None, self) + icon_widget.setObjectName("FamilyIconLabel") + + # --- Short description inputs --- + short_desc_input_widget = QtWidgets.QWidget(self) + + family_label = QtWidgets.QLabel(short_desc_input_widget) + family_label.setAlignment( + QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft + ) + + description_label = QtWidgets.QLabel(short_desc_input_widget) + description_label.setAlignment( + QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft + ) + + short_desc_input_layout = QtWidgets.QVBoxLayout( + short_desc_input_widget + ) + short_desc_input_layout.setSpacing(0) + short_desc_input_layout.addWidget(family_label) + short_desc_input_layout.addWidget(description_label) + # -------------------------------- + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(icon_widget, 0) + layout.addWidget(short_desc_input_widget, 1) + # -------------------------------- + + self._icon_widget = icon_widget + self._family_label = family_label + self._description_label = description_label + + self._last_height = None + + def _check_height_change(self): + height = self.height() + if height != self._last_height: + self._last_height = height + self.height_changed.emit(height) + + def showEvent(self, event): + super(CreatorShortDescWidget, self).showEvent(event) + self._check_height_change() + + def resizeEvent(self, event): + super(CreatorShortDescWidget, self).resizeEvent(event) + self._check_height_change() + + def set_plugin(self, plugin=None): + if not plugin: + self._icon_widget.set_icon_def(None) + self._family_label.setText("") + self._description_label.setText("") + return + + plugin_icon = plugin.get_icon() + description = plugin.get_description() or "" + + self._icon_widget.set_icon_def(plugin_icon) + self._family_label.setText("{}".format(plugin.family)) + self._family_label.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) + self._description_label.setText(description) + + +class HelpButton(ClickableFrame): + resized = QtCore.Signal(int) + question_mark_icon_name = "fa.question" + help_icon_name = "fa.question-circle" + hide_icon_name = "fa.angle-left" + + def __init__(self, *args, **kwargs): + super(HelpButton, self).__init__(*args, **kwargs) + self.setObjectName("CreateDialogHelpButton") + + question_mark_label = QtWidgets.QLabel(self) + help_widget = QtWidgets.QWidget(self) + + help_question = QtWidgets.QLabel(help_widget) + help_label = QtWidgets.QLabel("Help", help_widget) + hide_icon = QtWidgets.QLabel(help_widget) + + help_layout = QtWidgets.QHBoxLayout(help_widget) + help_layout.setContentsMargins(0, 0, 5, 0) + help_layout.addWidget(help_question, 0) + help_layout.addWidget(help_label, 0) + help_layout.addStretch(1) + help_layout.addWidget(hide_icon, 0) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + layout.addWidget(question_mark_label, 0) + layout.addWidget(help_widget, 1) + + help_widget.setVisible(False) + + self._question_mark_label = question_mark_label + self._help_widget = help_widget + self._help_question = help_question + self._hide_icon = hide_icon + + self._expanded = None + self.set_expanded() + + def set_expanded(self, expanded=None): + if self._expanded is expanded: + if expanded is not None: + return + expanded = False + self._expanded = expanded + self._help_widget.setVisible(expanded) + self._update_content() + + def _update_content(self): + width = self.get_icon_width() + if self._expanded: + question_mark_pix = QtGui.QPixmap(width, width) + question_mark_pix.fill(QtCore.Qt.transparent) + + else: + question_mark_icon = qtawesome.icon( + self.question_mark_icon_name, color=QtCore.Qt.white + ) + question_mark_pix = question_mark_icon.pixmap(width, width) + + hide_icon = qtawesome.icon( + self.hide_icon_name, color=QtCore.Qt.white + ) + help_question_icon = qtawesome.icon( + self.help_icon_name, color=QtCore.Qt.white + ) + self._question_mark_label.setPixmap(question_mark_pix) + self._question_mark_label.setMaximumWidth(width) + self._hide_icon.setPixmap(hide_icon.pixmap(width, width)) + self._help_question.setPixmap(help_question_icon.pixmap(width, width)) + + def get_icon_width(self): + metrics = self.fontMetrics() + return metrics.height() + + def set_pos_and_size(self, pos_x, pos_y, width, height): + update_icon = self.height() != height + self.move(pos_x, pos_y) + self.resize(width, height) + + if update_icon: + self._update_content() + self.updateGeometry() + + def showEvent(self, event): + super(HelpButton, self).showEvent(event) + self.resized.emit(self.height()) + + def resizeEvent(self, event): + super(HelpButton, self).resizeEvent(event) + self.resized.emit(self.height()) + + +class CreateWidget(QtWidgets.QWidget): + def __init__(self, controller, parent=None): + super(CreateWidget, self).__init__(parent) + + self.setWindowTitle("Create new instance") + + self.controller = controller + + self._asset_name = self.dbcon.Session.get("AVALON_ASSET") + self._task_name = self.dbcon.Session.get("AVALON_TASK") + + self._asset_doc = None + self._subset_names = None + self._selected_creator = None + + self._prereq_available = False + + self._message_dialog = None + + name_pattern = "^[{}]*$".format(SUBSET_NAME_ALLOWED_SYMBOLS) + self._name_pattern = name_pattern + self._compiled_name_pattern = re.compile(name_pattern) + + overlay_object = MessageOverlayObject(self) + + context_widget = QtWidgets.QWidget(self) + + assets_widget = CreateWidgetAssetsWidget(controller, context_widget) + tasks_widget = CreateWidgetTasksWidget(controller, context_widget) + + context_layout = QtWidgets.QVBoxLayout(context_widget) + context_layout.setContentsMargins(0, 0, 0, 0) + context_layout.setSpacing(0) + context_layout.addWidget(assets_widget, 2) + context_layout.addWidget(tasks_widget, 1) + + # --- Creators view --- + creators_header_widget = QtWidgets.QWidget(self) + header_label_widget = QtWidgets.QLabel( + "Choose family:", creators_header_widget + ) + creators_header_layout = QtWidgets.QHBoxLayout(creators_header_widget) + creators_header_layout.setContentsMargins(0, 0, 0, 0) + creators_header_layout.addWidget(header_label_widget, 1) + + creators_view = QtWidgets.QListView(self) + creators_model = QtGui.QStandardItemModel() + creators_sort_model = QtCore.QSortFilterProxyModel() + creators_sort_model.setSourceModel(creators_model) + creators_view.setModel(creators_sort_model) + + variant_widget = VariantInputsWidget(self) + + variant_input = QtWidgets.QLineEdit(variant_widget) + variant_input.setObjectName("VariantInput") + variant_input.setToolTip(VARIANT_TOOLTIP) + + variant_hints_btn = QtWidgets.QToolButton(variant_widget) + variant_hints_btn.setArrowType(QtCore.Qt.DownArrow) + variant_hints_btn.setIconSize(QtCore.QSize(12, 12)) + + variant_hints_menu = QtWidgets.QMenu(variant_widget) + variant_hints_group = QtWidgets.QActionGroup(variant_hints_menu) + + variant_layout = QtWidgets.QHBoxLayout(variant_widget) + variant_layout.setContentsMargins(0, 0, 0, 0) + variant_layout.setSpacing(0) + variant_layout.addWidget(variant_input, 1) + variant_layout.addWidget(variant_hints_btn, 0, QtCore.Qt.AlignVCenter) + + subset_name_input = QtWidgets.QLineEdit(self) + subset_name_input.setEnabled(False) + + form_layout = QtWidgets.QFormLayout() + form_layout.addRow("Variant:", variant_widget) + form_layout.addRow("Subset:", subset_name_input) + + mid_widget = QtWidgets.QWidget(self) + mid_layout = QtWidgets.QVBoxLayout(mid_widget) + mid_layout.setContentsMargins(0, 0, 0, 0) + mid_layout.addWidget(creators_header_widget, 0) + mid_layout.addWidget(creators_view, 1) + mid_layout.addLayout(form_layout, 0) + # ------------ + + # --- Creator short info and attr defs --- + creator_attrs_widget = QtWidgets.QWidget(self) + + creator_short_desc_widget = CreatorShortDescWidget( + creator_attrs_widget + ) + + attr_separator_widget = QtWidgets.QWidget(self) + attr_separator_widget.setObjectName("Separator") + attr_separator_widget.setMinimumHeight(1) + attr_separator_widget.setMaximumHeight(1) + + # Precreate attributes widget + pre_create_widget = PreCreateWidget(creator_attrs_widget) + + # Create button + create_btn_wrapper = QtWidgets.QWidget(creator_attrs_widget) + create_btn = QtWidgets.QPushButton("Create", create_btn_wrapper) + create_btn.setEnabled(False) + + create_btn_wrap_layout = QtWidgets.QHBoxLayout(create_btn_wrapper) + create_btn_wrap_layout.setContentsMargins(0, 0, 0, 0) + create_btn_wrap_layout.addStretch(1) + create_btn_wrap_layout.addWidget(create_btn, 0) + + creator_attrs_layout = QtWidgets.QVBoxLayout(creator_attrs_widget) + creator_attrs_layout.setContentsMargins(0, 0, 0, 0) + creator_attrs_layout.addWidget(creator_short_desc_widget, 0) + creator_attrs_layout.addWidget(attr_separator_widget, 0) + creator_attrs_layout.addWidget(pre_create_widget, 1) + creator_attrs_layout.addWidget(create_btn_wrapper, 0) + # ------------------------------------- + + # --- Detailed information about creator --- + # Detailed description of creator + detail_description_widget = QtWidgets.QWidget(self) + + detail_placoholder_widget = QtWidgets.QWidget( + detail_description_widget + ) + detail_placoholder_widget.setAttribute( + QtCore.Qt.WA_TranslucentBackground + ) + + detail_description_input = QtWidgets.QTextEdit( + detail_description_widget + ) + detail_description_input.setObjectName("CreatorDetailedDescription") + detail_description_input.setTextInteractionFlags( + QtCore.Qt.TextBrowserInteraction + ) + + detail_description_layout = QtWidgets.QVBoxLayout( + detail_description_widget + ) + detail_description_layout.setContentsMargins(0, 0, 0, 0) + detail_description_layout.setSpacing(0) + detail_description_layout.addWidget(detail_placoholder_widget, 0) + detail_description_layout.addWidget(detail_description_input, 1) + + detail_description_widget.setVisible(False) + + # ------------------------------------------- + splitter_widget = QtWidgets.QSplitter(self) + splitter_widget.addWidget(context_widget) + splitter_widget.addWidget(mid_widget) + splitter_widget.addWidget(creator_attrs_widget) + splitter_widget.addWidget(detail_description_widget) + splitter_widget.setStretchFactor(0, 1) + splitter_widget.setStretchFactor(1, 1) + splitter_widget.setStretchFactor(2, 1) + splitter_widget.setStretchFactor(3, 1) + + layout = QtWidgets.QHBoxLayout(self) + layout.addWidget(splitter_widget, 1) + + prereq_timer = QtCore.QTimer() + prereq_timer.setInterval(50) + prereq_timer.setSingleShot(True) + + prereq_timer.timeout.connect(self._invalidate_prereq) + + assets_widget.header_height_changed.connect( + self._on_asset_filter_height_change + ) + + create_btn.clicked.connect(self._on_create) + variant_widget.resized.connect(self._on_variant_widget_resize) + variant_input.returnPressed.connect(self._on_create) + variant_input.textChanged.connect(self._on_variant_change) + creators_view.selectionModel().currentChanged.connect( + self._on_creator_item_change + ) + variant_hints_btn.clicked.connect(self._on_variant_btn_click) + variant_hints_menu.triggered.connect(self._on_variant_action) + assets_widget.selection_changed.connect(self._on_asset_change) + assets_widget.current_context_required.connect( + self._on_current_session_context_request + ) + tasks_widget.task_changed.connect(self._on_task_change) + creator_short_desc_widget.height_changed.connect( + self._on_description_height_change + ) + + controller.add_plugins_refresh_callback(self._on_plugins_refresh) + + self._overlay_object = overlay_object + + self._splitter_widget = splitter_widget + + self._context_widget = context_widget + self._assets_widget = assets_widget + self._tasks_widget = tasks_widget + + self.subset_name_input = subset_name_input + + self.variant_input = variant_input + self.variant_hints_btn = variant_hints_btn + self.variant_hints_menu = variant_hints_menu + self.variant_hints_group = variant_hints_group + + self._creators_header_widget = creators_header_widget + self._creators_model = creators_model + self._creators_sort_model = creators_sort_model + self._creators_view = creators_view + self._create_btn = create_btn + + self._creator_short_desc_widget = creator_short_desc_widget + self._pre_create_widget = pre_create_widget + self._attr_separator_widget = attr_separator_widget + + self._detail_placoholder_widget = detail_placoholder_widget + self._detail_description_widget = detail_description_widget + self._detail_description_input = detail_description_input + + self._prereq_timer = prereq_timer + self._first_show = True + + def _emit_message(self, message): + self._overlay_object.add_message(message) + + def _context_change_is_enabled(self): + return self._context_widget.isEnabled() + + def _get_asset_name(self): + asset_name = None + if self._context_change_is_enabled(): + asset_name = self._assets_widget.get_selected_asset_name() + + if asset_name is None: + asset_name = self._asset_name + return asset_name + + def _get_task_name(self): + task_name = None + if self._context_change_is_enabled(): + # Don't use selection of task if asset is not set + asset_name = self._assets_widget.get_selected_asset_name() + if asset_name: + task_name = self._tasks_widget.get_selected_task_name() + + if not task_name: + task_name = self._task_name + return task_name + + @property + def dbcon(self): + return self.controller.dbcon + + def _set_context_enabled(self, enabled): + self._assets_widget.set_enabled(enabled) + self._tasks_widget.set_enabled(enabled) + check_prereq = self._context_widget.isEnabled() != enabled + self._context_widget.setEnabled(enabled) + if check_prereq: + self._invalidate_prereq() + + def refresh(self): + # Get context before refresh to keep selection of asset and + # task widgets + asset_name = self._get_asset_name() + task_name = self._get_task_name() + + self._prereq_available = False + + # Disable context widget so refresh of asset will use context asset + # name + self._set_context_enabled(False) + + self._assets_widget.refresh() + + # Refresh data before update of creators + self._refresh_asset() + # Then refresh creators which may trigger callbacks using refreshed + # data + self._refresh_creators() + + self._assets_widget.set_current_asset_name(self._asset_name) + self._assets_widget.select_asset_by_name(asset_name) + self._tasks_widget.set_asset_name(asset_name) + self._tasks_widget.select_task_name(task_name) + + self._invalidate_prereq_deffered() + + def _invalidate_prereq_deffered(self): + self._prereq_timer.start() + + def _on_asset_filter_height_change(self, height): + self._creators_header_widget.setMinimumHeight(height) + self._creators_header_widget.setMaximumHeight(height) + + def _invalidate_prereq(self): + prereq_available = True + creator_btn_tooltips = [] + + available_creators = self._creators_model.rowCount() > 0 + if available_creators != self._creators_view.isEnabled(): + self._creators_view.setEnabled(available_creators) + + if not available_creators: + prereq_available = False + creator_btn_tooltips.append("Creator is not selected") + + if self._context_change_is_enabled() and self._asset_doc is None: + # QUESTION how to handle invalid asset? + prereq_available = False + creator_btn_tooltips.append("Context is not selected") + + if prereq_available != self._prereq_available: + self._prereq_available = prereq_available + + self._create_btn.setEnabled(prereq_available) + + self.variant_input.setEnabled(prereq_available) + self.variant_hints_btn.setEnabled(prereq_available) + + tooltip = "" + if creator_btn_tooltips: + tooltip = "\n".join(creator_btn_tooltips) + self._create_btn.setToolTip(tooltip) + + self._on_variant_change() + + def _refresh_asset(self): + asset_name = self._get_asset_name() + + # Skip if asset did not change + if self._asset_doc and self._asset_doc["name"] == asset_name: + return + + # Make sure `_asset_doc` and `_subset_names` variables are reset + self._asset_doc = None + self._subset_names = None + if asset_name is None: + return + + project_name = self.dbcon.active_project() + asset_doc = get_asset_by_name(project_name, asset_name) + self._asset_doc = asset_doc + + if asset_doc: + asset_id = asset_doc["_id"] + subset_docs = get_subsets( + project_name, asset_ids=[asset_id], fields=["name"] + ) + self._subset_names = { + subset_doc["name"] + for subset_doc in subset_docs + } + + if not asset_doc: + self.subset_name_input.setText("< Asset is not set >") + + def _refresh_creators(self): + # Refresh creators and add their families to list + existing_items = {} + old_creators = set() + for row in range(self._creators_model.rowCount()): + item = self._creators_model.item(row, 0) + identifier = item.data(CREATOR_IDENTIFIER_ROLE) + existing_items[identifier] = item + old_creators.add(identifier) + + # Add new families + new_creators = set() + for identifier, creator in self.controller.manual_creators.items(): + # TODO add details about creator + new_creators.add(identifier) + if identifier in existing_items: + item = existing_items[identifier] + else: + item = QtGui.QStandardItem() + item.setFlags( + QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + ) + self._creators_model.appendRow(item) + + label = creator.label or identifier + item.setData(label, QtCore.Qt.DisplayRole) + item.setData(identifier, CREATOR_IDENTIFIER_ROLE) + item.setData(creator.family, FAMILY_ROLE) + + # Remove families that are no more available + for identifier in (old_creators - new_creators): + item = existing_items[identifier] + self._creators_model.takeRow(item.row()) + + if self._creators_model.rowCount() < 1: + return + + self._creators_sort_model.sort(0) + # Make sure there is a selection + indexes = self._creators_view.selectedIndexes() + if not indexes: + index = self._creators_sort_model.index(0, 0) + self._creators_view.setCurrentIndex(index) + else: + index = indexes[0] + + identifier = index.data(CREATOR_IDENTIFIER_ROLE) + + self._set_creator_by_identifier(identifier) + + def _on_plugins_refresh(self): + # Trigger refresh only if is visible + self.refresh() + + def _on_asset_change(self): + self._refresh_asset() + + asset_name = self._assets_widget.get_selected_asset_name() + self._tasks_widget.set_asset_name(asset_name) + if self._context_change_is_enabled(): + self._invalidate_prereq_deffered() + + def _on_task_change(self): + if self._context_change_is_enabled(): + self._invalidate_prereq_deffered() + + def _on_current_session_context_request(self): + self._assets_widget.set_current_session_asset() + if self._task_name: + self._tasks_widget.select_task_name(self._task_name) + + def _on_description_height_change(self): + # Use separator's 'y' position as height + height = self._attr_separator_widget.y() + self._detail_placoholder_widget.setMinimumHeight(height) + self._detail_placoholder_widget.setMaximumHeight(height) + + def _on_creator_item_change(self, new_index, _old_index): + identifier = None + if new_index.isValid(): + identifier = new_index.data(CREATOR_IDENTIFIER_ROLE) + self._set_creator_by_identifier(identifier) + + def _set_creator_detailed_text(self, creator): + if not creator: + self._detail_description_input.setPlainText("") + return + detailed_description = creator.get_detail_description() or "" + if commonmark: + html = commonmark.commonmark(detailed_description) + self._detail_description_input.setHtml(html) + else: + self._detail_description_input.setMarkdown(detailed_description) + + def _set_creator_by_identifier(self, identifier): + creator = self.controller.manual_creators.get(identifier) + self._set_creator(creator) + + def _set_creator(self, creator): + self._creator_short_desc_widget.set_plugin(creator) + self._set_creator_detailed_text(creator) + self._pre_create_widget.set_plugin(creator) + + self._selected_creator = creator + + if not creator: + self._set_context_enabled(False) + return + + if ( + creator.create_allow_context_change + != self._context_change_is_enabled() + ): + self._set_context_enabled(creator.create_allow_context_change) + self._refresh_asset() + + default_variants = creator.get_default_variants() + if not default_variants: + default_variants = ["Main"] + + default_variant = creator.get_default_variant() + if not default_variant: + default_variant = default_variants[0] + + for action in tuple(self.variant_hints_menu.actions()): + self.variant_hints_menu.removeAction(action) + action.deleteLater() + + for variant in default_variants: + if variant in SEPARATORS: + self.variant_hints_menu.addSeparator() + elif variant: + self.variant_hints_menu.addAction(variant) + + variant_text = default_variant or "Main" + # Make sure subset name is updated to new plugin + if variant_text == self.variant_input.text(): + self._on_variant_change() + else: + self.variant_input.setText(variant_text) + + def _on_variant_widget_resize(self): + self.variant_hints_btn.setFixedHeight(self.variant_input.height()) + + def _on_variant_btn_click(self): + pos = self.variant_hints_btn.rect().bottomLeft() + point = self.variant_hints_btn.mapToGlobal(pos) + self.variant_hints_menu.popup(point) + + def _on_variant_action(self, action): + value = action.text() + if self.variant_input.text() != value: + self.variant_input.setText(value) + + def _on_variant_change(self, variant_value=None): + if not self._prereq_available: + return + + # This should probably never happen? + if not self._selected_creator: + if self.subset_name_input.text(): + self.subset_name_input.setText("") + return + + if variant_value is None: + variant_value = self.variant_input.text() + + if not self._compiled_name_pattern.match(variant_value): + self._create_btn.setEnabled(False) + self._set_variant_state_property("invalid") + self.subset_name_input.setText("< Invalid variant >") + return + + if not self._context_change_is_enabled(): + self._create_btn.setEnabled(True) + self._set_variant_state_property("") + self.subset_name_input.setText("< Valid variant >") + return + + project_name = self.controller.project_name + task_name = self._get_task_name() + + asset_doc = copy.deepcopy(self._asset_doc) + # Calculate subset name with Creator plugin + try: + subset_name = self._selected_creator.get_subset_name( + variant_value, task_name, asset_doc, project_name + ) + except TaskNotSetError: + self._create_btn.setEnabled(False) + self._set_variant_state_property("invalid") + self.subset_name_input.setText("< Missing task >") + return + + self.subset_name_input.setText(subset_name) + + self._create_btn.setEnabled(True) + self._validate_subset_name(subset_name, variant_value) + + def _validate_subset_name(self, subset_name, variant_value): + # Get all subsets of the current asset + if self._subset_names: + existing_subset_names = set(self._subset_names) + else: + existing_subset_names = set() + existing_subset_names_low = set( + _name.lower() + for _name in existing_subset_names + ) + + # Replace + compare_regex = re.compile(re.sub( + variant_value, "(.+)", subset_name, flags=re.IGNORECASE + )) + variant_hints = set() + if variant_value: + for _name in existing_subset_names: + _result = compare_regex.search(_name) + if _result: + variant_hints |= set(_result.groups()) + + # Remove previous hints from menu + for action in tuple(self.variant_hints_group.actions()): + self.variant_hints_group.removeAction(action) + self.variant_hints_menu.removeAction(action) + action.deleteLater() + + # Add separator if there are hints and menu already has actions + if variant_hints and self.variant_hints_menu.actions(): + self.variant_hints_menu.addSeparator() + + # Add hints to actions + for variant_hint in variant_hints: + action = self.variant_hints_menu.addAction(variant_hint) + self.variant_hints_group.addAction(action) + + # Indicate subset existence + if not variant_value: + property_value = "empty" + + elif subset_name.lower() in existing_subset_names_low: + # validate existence of subset name with lowered text + # - "renderMain" vs. "rendermain" mean same path item for + # windows + property_value = "exists" + else: + property_value = "new" + + self._set_variant_state_property(property_value) + + variant_is_valid = variant_value.strip() != "" + if variant_is_valid != self._create_btn.isEnabled(): + self._create_btn.setEnabled(variant_is_valid) + + def _set_variant_state_property(self, state): + current_value = self.variant_input.property("state") + if current_value != state: + self.variant_input.setProperty("state", state) + self.variant_input.style().polish(self.variant_input) + + def _on_first_show(self): + width = self.width() + part = int(width / 7) + self._splitter_widget.setSizes( + [part * 2, part * 2, width - (part * 4)] + ) + + def showEvent(self, event): + super(CreateWidget, self).showEvent(event) + if self._first_show: + self._first_show = False + self._on_first_show() + + def _on_create(self): + indexes = self._creators_view.selectedIndexes() + if not indexes or len(indexes) > 1: + return + + if not self._create_btn.isEnabled(): + return + + index = indexes[0] + creator_label = index.data(QtCore.Qt.DisplayRole) + creator_identifier = index.data(CREATOR_IDENTIFIER_ROLE) + family = index.data(FAMILY_ROLE) + variant = self.variant_input.text() + # Care about subset name only if context change is enabled + subset_name = None + asset_name = None + task_name = None + if self._context_change_is_enabled(): + subset_name = self.subset_name_input.text() + asset_name = self._get_asset_name() + task_name = self._get_task_name() + + pre_create_data = self._pre_create_widget.current_value() + # Where to define these data? + # - what data show be stored? + instance_data = { + "asset": asset_name, + "task": task_name, + "variant": variant, + "family": family + } + + error_msg = None + formatted_traceback = None + try: + self.controller.create( + creator_identifier, + subset_name, + instance_data, + pre_create_data + ) + + except CreatorError as exc: + error_msg = str(exc) + + # Use bare except because some hosts raise their exceptions that + # do not inherit from python's `BaseException` + except: + exc_type, exc_value, exc_traceback = sys.exc_info() + formatted_traceback = "".join(traceback.format_exception( + exc_type, exc_value, exc_traceback + )) + error_msg = str(exc_value) + + if error_msg is None: + self._set_creator(self._selected_creator) + self._emit_message("Creation finished...") + else: + box = CreateErrorMessageBox( + creator_label, + subset_name, + asset_name, + error_msg, + formatted_traceback, + parent=self + ) + box.show() + # Store dialog so is not garbage collected before is shown + self._message_dialog = box diff --git a/openpype/tools/publisher/widgets/overview_widget.py b/openpype/tools/publisher/widgets/overview_widget.py index abdd98ff7c..ddc976d458 100644 --- a/openpype/tools/publisher/widgets/overview_widget.py +++ b/openpype/tools/publisher/widgets/overview_widget.py @@ -10,6 +10,7 @@ from .widgets import ( RemoveInstanceBtn, ChangeViewBtn ) +from .create_widget import CreateWidget class CreateOverviewWidget(QtWidgets.QFrame): @@ -20,9 +21,13 @@ class CreateOverviewWidget(QtWidgets.QFrame): def __init__(self, controller, parent): super(CreateOverviewWidget, self).__init__(parent) - self._controller = controller self._refreshing_instances = False + self._controller = controller + create_widget = CreateWidget(controller, self) + + # --- Created Subsets/Instances --- + # Common widget for creation and overview subset_views_widget = BorderedLabelWidget( "Subsets to publish", self ) @@ -39,6 +44,7 @@ class CreateOverviewWidget(QtWidgets.QFrame): delete_btn = RemoveInstanceBtn(self) change_view_btn = ChangeViewBtn(self) + # --- Overview --- # Subset details widget subset_attributes_wrap = BorderedLabelWidget( "Publish options", self @@ -73,6 +79,7 @@ class CreateOverviewWidget(QtWidgets.QFrame): subset_content_widget = QtWidgets.QWidget(self) subset_content_layout = QtWidgets.QHBoxLayout(subset_content_widget) subset_content_layout.setContentsMargins(0, 0, 0, 0) + subset_content_layout.addWidget(create_widget, 7) subset_content_layout.addWidget(subset_views_widget, 3) subset_content_layout.addWidget(subset_attributes_wrap, 7) @@ -86,6 +93,7 @@ class CreateOverviewWidget(QtWidgets.QFrame): main_layout.setContentsMargins(marings) main_layout.addWidget(subset_content_widget, 1) + # --- Calbacks for instances/subsets view --- create_btn.clicked.connect(self._on_create_clicked) delete_btn.clicked.connect(self._on_delete_clicked) change_view_btn.clicked.connect(self._on_change_view_clicked) @@ -109,18 +117,38 @@ class CreateOverviewWidget(QtWidgets.QFrame): self._on_instance_context_change ) + # --- Controller callbacks --- controller.add_publish_reset_callback(self._on_publish_reset) controller.add_instances_refresh_callback(self._on_instances_refresh) - self.subset_content_widget = subset_content_widget + self._subset_content_widget = subset_content_widget - self.subset_view_cards = subset_view_cards - self.subset_list_view = subset_list_view - self.subset_views_layout = subset_views_layout + self._subset_view_cards = subset_view_cards + self._subset_list_view = subset_list_view + self._subset_views_layout = subset_views_layout - self.delete_btn = delete_btn + self._delete_btn = delete_btn - self.subset_attributes_widget = subset_attributes_widget + self._subset_attributes_widget = subset_attributes_widget + self._create_widget = create_widget + self._subset_attributes_wrap = subset_attributes_wrap + + # Start in create mode + self._current_state = "create" + subset_attributes_wrap.setVisible(False) + + def set_state(self, old_state, new_state): + if new_state == self._current_state: + return + + self._current_state = new_state + + self._create_widget.setVisible( + self._current_state == "create" + ) + self._subset_attributes_wrap.setVisible( + self._current_state == "publish" + ) def _on_create_clicked(self): """Pass signal to parent widget which should care about changing state. @@ -167,9 +195,9 @@ class CreateOverviewWidget(QtWidgets.QFrame): instances, context_selected = self.get_selected_items() # Disable delete button if nothing is selected - self.delete_btn.setEnabled(len(instances) > 0) + self._delete_btn.setEnabled(len(instances) > 0) - self.subset_attributes_widget.set_current_instances( + self._subset_attributes_widget.set_current_instances( instances, context_selected ) @@ -179,29 +207,29 @@ class CreateOverviewWidget(QtWidgets.QFrame): self.active_changed.emit() def _on_instance_context_change(self): - current_idx = self.subset_views_layout.currentIndex() - for idx in range(self.subset_views_layout.count()): + current_idx = self._subset_views_layout.currentIndex() + for idx in range(self._subset_views_layout.count()): if idx == current_idx: continue - widget = self.subset_views_layout.widget(idx) + widget = self._subset_views_layout.widget(idx) if widget.refreshed: widget.set_refreshed(False) - current_widget = self.subset_views_layout.widget(current_idx) + current_widget = self._subset_views_layout.widget(current_idx) current_widget.refresh_instance_states() self.instance_context_changed.emit() def get_selected_items(self): - view = self.subset_views_layout.currentWidget() + view = self._subset_views_layout.currentWidget() return view.get_selected_items() def _change_view_type(self): - idx = self.subset_views_layout.currentIndex() - new_idx = (idx + 1) % self.subset_views_layout.count() - self.subset_views_layout.setCurrentIndex(new_idx) + idx = self._subset_views_layout.currentIndex() + new_idx = (idx + 1) % self._subset_views_layout.count() + self._subset_views_layout.setCurrentIndex(new_idx) - new_view = self.subset_views_layout.currentWidget() + new_view = self._subset_views_layout.currentWidget() if not new_view.refreshed: new_view.refresh() new_view.set_refreshed(True) @@ -216,11 +244,11 @@ class CreateOverviewWidget(QtWidgets.QFrame): self._refreshing_instances = True - for idx in range(self.subset_views_layout.count()): - widget = self.subset_views_layout.widget(idx) + for idx in range(self._subset_views_layout.count()): + widget = self._subset_views_layout.widget(idx) widget.set_refreshed(False) - view = self.subset_views_layout.currentWidget() + view = self._subset_views_layout.currentWidget() view.refresh() view.set_refreshed(True) @@ -232,7 +260,7 @@ class CreateOverviewWidget(QtWidgets.QFrame): def _on_publish_reset(self): """Context in controller has been refreshed.""" - self.subset_content_widget.setEnabled(self._controller.host_is_valid) + self._subset_content_widget.setEnabled(self._controller.host_is_valid) def _on_instances_refresh(self): """Controller refreshed instances.""" @@ -242,5 +270,5 @@ class CreateOverviewWidget(QtWidgets.QFrame): # Give a change to process Resize Request QtWidgets.QApplication.processEvents() # Trigger update geometry of - widget = self.subset_views_layout.currentWidget() + widget = self._subset_views_layout.currentWidget() widget.updateGeometry() diff --git a/openpype/tools/publisher/widgets/tasks_widget.py b/openpype/tools/publisher/widgets/tasks_widget.py index aa239f6334..f31fffb9ea 100644 --- a/openpype/tools/publisher/widgets/tasks_widget.py +++ b/openpype/tools/publisher/widgets/tasks_widget.py @@ -141,10 +141,10 @@ class TasksModel(QtGui.QStandardItemModel): return super(TasksModel, self).headerData(section, orientation, role) -class CreateDialogTasksWidget(TasksWidget): +class CreateWidgetTasksWidget(TasksWidget): def __init__(self, controller, parent): self._controller = controller - super(CreateDialogTasksWidget, self).__init__(None, parent) + super(CreateWidgetTasksWidget, self).__init__(None, parent) self._enabled = None @@ -164,7 +164,7 @@ class CreateDialogTasksWidget(TasksWidget): self.task_changed.emit() def select_task_name(self, task_name): - super(CreateDialogTasksWidget, self).select_task_name(task_name) + super(CreateWidgetTasksWidget, self).select_task_name(task_name) if not self._enabled: current = self.get_selected_task_name() if current: From b6312fe3692850dc1a821e9c79d83425078d5ba7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Sep 2022 17:45:43 +0200 Subject: [PATCH 028/121] changed style of disable button --- openpype/style/style.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/style/style.css b/openpype/style/style.css index ab23dd621f..1d112fa575 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -868,6 +868,10 @@ PublisherTabBtn { padding: 0.5em 1em 0.5em 1em; } +PublisherTabBtn:disabled { + background: {color:bg-inputs}; +} + PublisherTabBtn:hover { background: {color:bg-buttons}; } From 22dab5ddefb2908d3eefcbd8a46cbb77a204ed4c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Sep 2022 17:46:20 +0200 Subject: [PATCH 029/121] removed created dialog --- openpype/tools/publisher/widgets/__init__.py | 3 - .../tools/publisher/widgets/create_dialog.py | 1222 ----------------- .../tools/publisher/widgets/tabs_widget.py | 9 + openpype/tools/publisher/window.py | 36 +- 4 files changed, 22 insertions(+), 1248 deletions(-) delete mode 100644 openpype/tools/publisher/widgets/create_dialog.py diff --git a/openpype/tools/publisher/widgets/__init__.py b/openpype/tools/publisher/widgets/__init__.py index 869f7adf9b..1d0ed0633b 100644 --- a/openpype/tools/publisher/widgets/__init__.py +++ b/openpype/tools/publisher/widgets/__init__.py @@ -10,7 +10,6 @@ from .widgets import ( PublishBtn, ) from .publish_widget import PublishFrame -from .create_dialog import CreateDialog from .tabs_widget import PublisherTabsWidget from .overview_widget import CreateOverviewWidget @@ -26,8 +25,6 @@ __all__ = ( "PublishFrame", - "CreateDialog", - "PublisherTabsWidget", "CreateOverviewWidget", ) diff --git a/openpype/tools/publisher/widgets/create_dialog.py b/openpype/tools/publisher/widgets/create_dialog.py deleted file mode 100644 index 173df7d5c8..0000000000 --- a/openpype/tools/publisher/widgets/create_dialog.py +++ /dev/null @@ -1,1222 +0,0 @@ -import sys -import re -import traceback -import copy - -import qtawesome -try: - import commonmark -except Exception: - commonmark = None -from Qt import QtWidgets, QtCore, QtGui - -from openpype.client import get_asset_by_name, get_subsets -from openpype.pipeline.create import ( - CreatorError, - SUBSET_NAME_ALLOWED_SYMBOLS, - TaskNotSetError, -) -from openpype.tools.utils import ( - ErrorMessageBox, - MessageOverlayObject, - ClickableFrame, -) - -from .widgets import IconValuePixmapLabel -from .assets_widget import CreateDialogAssetsWidget -from .tasks_widget import CreateDialogTasksWidget -from .precreate_widget import PreCreateWidget -from ..constants import ( - VARIANT_TOOLTIP, - CREATOR_IDENTIFIER_ROLE, - FAMILY_ROLE -) - -SEPARATORS = ("---separator---", "---") - - -class VariantInputsWidget(QtWidgets.QWidget): - resized = QtCore.Signal() - - def resizeEvent(self, event): - super(VariantInputsWidget, self).resizeEvent(event) - self.resized.emit() - - -class CreateErrorMessageBox(ErrorMessageBox): - def __init__( - self, - creator_label, - subset_name, - asset_name, - exc_msg, - formatted_traceback, - parent - ): - self._creator_label = creator_label - self._subset_name = subset_name - self._asset_name = asset_name - self._exc_msg = exc_msg - self._formatted_traceback = formatted_traceback - super(CreateErrorMessageBox, self).__init__("Creation failed", parent) - - def _create_top_widget(self, parent_widget): - label_widget = QtWidgets.QLabel(parent_widget) - label_widget.setText( - "Failed to create" - ) - return label_widget - - def _get_report_data(self): - report_message = ( - "{creator}: Failed to create Subset: \"{subset}\"" - " in Asset: \"{asset}\"" - "\n\nError: {message}" - ).format( - creator=self._creator_label, - subset=self._subset_name, - asset=self._asset_name, - message=self._exc_msg, - ) - if self._formatted_traceback: - report_message += "\n\n{}".format(self._formatted_traceback) - return [report_message] - - def _create_content(self, content_layout): - item_name_template = ( - "Creator: {}
" - "Subset: {}
" - "Asset: {}
" - ) - exc_msg_template = "{}" - - line = self._create_line() - content_layout.addWidget(line) - - item_name_widget = QtWidgets.QLabel(self) - item_name_widget.setText( - item_name_template.format( - self._creator_label, self._subset_name, self._asset_name - ) - ) - content_layout.addWidget(item_name_widget) - - message_label_widget = QtWidgets.QLabel(self) - message_label_widget.setText( - exc_msg_template.format(self.convert_text_for_html(self._exc_msg)) - ) - content_layout.addWidget(message_label_widget) - - if self._formatted_traceback: - line_widget = self._create_line() - tb_widget = self._create_traceback_widget( - self._formatted_traceback - ) - content_layout.addWidget(line_widget) - content_layout.addWidget(tb_widget) - - -# TODO add creator identifier/label to details -class CreatorShortDescWidget(QtWidgets.QWidget): - height_changed = QtCore.Signal(int) - - def __init__(self, parent=None): - super(CreatorShortDescWidget, self).__init__(parent=parent) - - # --- Short description widget --- - icon_widget = IconValuePixmapLabel(None, self) - icon_widget.setObjectName("FamilyIconLabel") - - # --- Short description inputs --- - short_desc_input_widget = QtWidgets.QWidget(self) - - family_label = QtWidgets.QLabel(short_desc_input_widget) - family_label.setAlignment( - QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft - ) - - description_label = QtWidgets.QLabel(short_desc_input_widget) - description_label.setAlignment( - QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft - ) - - short_desc_input_layout = QtWidgets.QVBoxLayout( - short_desc_input_widget - ) - short_desc_input_layout.setSpacing(0) - short_desc_input_layout.addWidget(family_label) - short_desc_input_layout.addWidget(description_label) - # -------------------------------- - - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(icon_widget, 0) - layout.addWidget(short_desc_input_widget, 1) - # -------------------------------- - - self._icon_widget = icon_widget - self._family_label = family_label - self._description_label = description_label - - self._last_height = None - - def _check_height_change(self): - height = self.height() - if height != self._last_height: - self._last_height = height - self.height_changed.emit(height) - - def showEvent(self, event): - super(CreatorShortDescWidget, self).showEvent(event) - self._check_height_change() - - def resizeEvent(self, event): - super(CreatorShortDescWidget, self).resizeEvent(event) - self._check_height_change() - - def set_plugin(self, plugin=None): - if not plugin: - self._icon_widget.set_icon_def(None) - self._family_label.setText("") - self._description_label.setText("") - return - - plugin_icon = plugin.get_icon() - description = plugin.get_description() or "" - - self._icon_widget.set_icon_def(plugin_icon) - self._family_label.setText("{}".format(plugin.family)) - self._family_label.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) - self._description_label.setText(description) - - -class HelpButton(ClickableFrame): - resized = QtCore.Signal(int) - question_mark_icon_name = "fa.question" - help_icon_name = "fa.question-circle" - hide_icon_name = "fa.angle-left" - - def __init__(self, *args, **kwargs): - super(HelpButton, self).__init__(*args, **kwargs) - self.setObjectName("CreateDialogHelpButton") - - question_mark_label = QtWidgets.QLabel(self) - help_widget = QtWidgets.QWidget(self) - - help_question = QtWidgets.QLabel(help_widget) - help_label = QtWidgets.QLabel("Help", help_widget) - hide_icon = QtWidgets.QLabel(help_widget) - - help_layout = QtWidgets.QHBoxLayout(help_widget) - help_layout.setContentsMargins(0, 0, 5, 0) - help_layout.addWidget(help_question, 0) - help_layout.addWidget(help_label, 0) - help_layout.addStretch(1) - help_layout.addWidget(hide_icon, 0) - - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - layout.addWidget(question_mark_label, 0) - layout.addWidget(help_widget, 1) - - help_widget.setVisible(False) - - self._question_mark_label = question_mark_label - self._help_widget = help_widget - self._help_question = help_question - self._hide_icon = hide_icon - - self._expanded = None - self.set_expanded() - - def set_expanded(self, expanded=None): - if self._expanded is expanded: - if expanded is not None: - return - expanded = False - self._expanded = expanded - self._help_widget.setVisible(expanded) - self._update_content() - - def _update_content(self): - width = self.get_icon_width() - if self._expanded: - question_mark_pix = QtGui.QPixmap(width, width) - question_mark_pix.fill(QtCore.Qt.transparent) - - else: - question_mark_icon = qtawesome.icon( - self.question_mark_icon_name, color=QtCore.Qt.white - ) - question_mark_pix = question_mark_icon.pixmap(width, width) - - hide_icon = qtawesome.icon( - self.hide_icon_name, color=QtCore.Qt.white - ) - help_question_icon = qtawesome.icon( - self.help_icon_name, color=QtCore.Qt.white - ) - self._question_mark_label.setPixmap(question_mark_pix) - self._question_mark_label.setMaximumWidth(width) - self._hide_icon.setPixmap(hide_icon.pixmap(width, width)) - self._help_question.setPixmap(help_question_icon.pixmap(width, width)) - - def get_icon_width(self): - metrics = self.fontMetrics() - return metrics.height() - - def set_pos_and_size(self, pos_x, pos_y, width, height): - update_icon = self.height() != height - self.move(pos_x, pos_y) - self.resize(width, height) - - if update_icon: - self._update_content() - self.updateGeometry() - - def showEvent(self, event): - super(HelpButton, self).showEvent(event) - self.resized.emit(self.height()) - - def resizeEvent(self, event): - super(HelpButton, self).resizeEvent(event) - self.resized.emit(self.height()) - - -class CreateDialog(QtWidgets.QDialog): - default_size = (1000, 560) - - def __init__( - self, controller, asset_name=None, task_name=None, parent=None - ): - super(CreateDialog, self).__init__(parent) - - self.setWindowTitle("Create new instance") - - self.controller = controller - - if asset_name is None: - asset_name = self.dbcon.Session.get("AVALON_ASSET") - - if task_name is None: - task_name = self.dbcon.Session.get("AVALON_TASK") - - self._asset_name = asset_name - self._task_name = task_name - - self._last_pos = None - self._asset_doc = None - self._subset_names = None - self._selected_creator = None - - self._prereq_available = False - - self._message_dialog = None - - name_pattern = "^[{}]*$".format(SUBSET_NAME_ALLOWED_SYMBOLS) - self._name_pattern = name_pattern - self._compiled_name_pattern = re.compile(name_pattern) - - overlay_object = MessageOverlayObject(self) - - context_widget = QtWidgets.QWidget(self) - - assets_widget = CreateDialogAssetsWidget(controller, context_widget) - tasks_widget = CreateDialogTasksWidget(controller, context_widget) - - context_layout = QtWidgets.QVBoxLayout(context_widget) - context_layout.setContentsMargins(0, 0, 0, 0) - context_layout.setSpacing(0) - context_layout.addWidget(assets_widget, 2) - context_layout.addWidget(tasks_widget, 1) - - # --- Creators view --- - creators_header_widget = QtWidgets.QWidget(self) - header_label_widget = QtWidgets.QLabel( - "Choose family:", creators_header_widget - ) - creators_header_layout = QtWidgets.QHBoxLayout(creators_header_widget) - creators_header_layout.setContentsMargins(0, 0, 0, 0) - creators_header_layout.addWidget(header_label_widget, 1) - - creators_view = QtWidgets.QListView(self) - creators_model = QtGui.QStandardItemModel() - creators_sort_model = QtCore.QSortFilterProxyModel() - creators_sort_model.setSourceModel(creators_model) - creators_view.setModel(creators_sort_model) - - variant_widget = VariantInputsWidget(self) - - variant_input = QtWidgets.QLineEdit(variant_widget) - variant_input.setObjectName("VariantInput") - variant_input.setToolTip(VARIANT_TOOLTIP) - - variant_hints_btn = QtWidgets.QToolButton(variant_widget) - variant_hints_btn.setArrowType(QtCore.Qt.DownArrow) - variant_hints_btn.setIconSize(QtCore.QSize(12, 12)) - - variant_hints_menu = QtWidgets.QMenu(variant_widget) - variant_hints_group = QtWidgets.QActionGroup(variant_hints_menu) - - variant_layout = QtWidgets.QHBoxLayout(variant_widget) - variant_layout.setContentsMargins(0, 0, 0, 0) - variant_layout.setSpacing(0) - variant_layout.addWidget(variant_input, 1) - variant_layout.addWidget(variant_hints_btn, 0, QtCore.Qt.AlignVCenter) - - subset_name_input = QtWidgets.QLineEdit(self) - subset_name_input.setEnabled(False) - - form_layout = QtWidgets.QFormLayout() - form_layout.addRow("Variant:", variant_widget) - form_layout.addRow("Subset:", subset_name_input) - - mid_widget = QtWidgets.QWidget(self) - mid_layout = QtWidgets.QVBoxLayout(mid_widget) - mid_layout.setContentsMargins(0, 0, 0, 0) - mid_layout.addWidget(creators_header_widget, 0) - mid_layout.addWidget(creators_view, 1) - mid_layout.addLayout(form_layout, 0) - # ------------ - - # --- Creator short info and attr defs --- - creator_attrs_widget = QtWidgets.QWidget(self) - - creator_short_desc_widget = CreatorShortDescWidget( - creator_attrs_widget - ) - - attr_separator_widget = QtWidgets.QWidget(self) - attr_separator_widget.setObjectName("Separator") - attr_separator_widget.setMinimumHeight(1) - attr_separator_widget.setMaximumHeight(1) - - # Precreate attributes widget - pre_create_widget = PreCreateWidget(creator_attrs_widget) - - # Create button - create_btn_wrapper = QtWidgets.QWidget(creator_attrs_widget) - create_btn = QtWidgets.QPushButton("Create", create_btn_wrapper) - create_btn.setEnabled(False) - - create_btn_wrap_layout = QtWidgets.QHBoxLayout(create_btn_wrapper) - create_btn_wrap_layout.setContentsMargins(0, 0, 0, 0) - create_btn_wrap_layout.addStretch(1) - create_btn_wrap_layout.addWidget(create_btn, 0) - - creator_attrs_layout = QtWidgets.QVBoxLayout(creator_attrs_widget) - creator_attrs_layout.setContentsMargins(0, 0, 0, 0) - creator_attrs_layout.addWidget(creator_short_desc_widget, 0) - creator_attrs_layout.addWidget(attr_separator_widget, 0) - creator_attrs_layout.addWidget(pre_create_widget, 1) - creator_attrs_layout.addWidget(create_btn_wrapper, 0) - # ------------------------------------- - - # --- Detailed information about creator --- - # Detailed description of creator - detail_description_widget = QtWidgets.QWidget(self) - - detail_placoholder_widget = QtWidgets.QWidget( - detail_description_widget - ) - detail_placoholder_widget.setAttribute( - QtCore.Qt.WA_TranslucentBackground - ) - - detail_description_input = QtWidgets.QTextEdit( - detail_description_widget - ) - detail_description_input.setObjectName("CreatorDetailedDescription") - detail_description_input.setTextInteractionFlags( - QtCore.Qt.TextBrowserInteraction - ) - - detail_description_layout = QtWidgets.QVBoxLayout( - detail_description_widget - ) - detail_description_layout.setContentsMargins(0, 0, 0, 0) - detail_description_layout.setSpacing(0) - detail_description_layout.addWidget(detail_placoholder_widget, 0) - detail_description_layout.addWidget(detail_description_input, 1) - - detail_description_widget.setVisible(False) - - # ------------------------------------------- - splitter_widget = QtWidgets.QSplitter(self) - splitter_widget.addWidget(context_widget) - splitter_widget.addWidget(mid_widget) - splitter_widget.addWidget(creator_attrs_widget) - splitter_widget.addWidget(detail_description_widget) - splitter_widget.setStretchFactor(0, 1) - splitter_widget.setStretchFactor(1, 1) - splitter_widget.setStretchFactor(2, 1) - splitter_widget.setStretchFactor(3, 1) - - layout = QtWidgets.QHBoxLayout(self) - layout.addWidget(splitter_widget, 1) - - # Floating help button - # - Create this button as last to be fully visible - help_btn = HelpButton(self) - - prereq_timer = QtCore.QTimer() - prereq_timer.setInterval(50) - prereq_timer.setSingleShot(True) - - desc_width_anim_timer = QtCore.QTimer() - desc_width_anim_timer.setInterval(10) - - prereq_timer.timeout.connect(self._invalidate_prereq) - - desc_width_anim_timer.timeout.connect(self._on_desc_animation) - - help_btn.clicked.connect(self._on_help_btn) - help_btn.resized.connect(self._on_help_btn_resize) - - assets_widget.header_height_changed.connect( - self._on_asset_filter_height_change - ) - - create_btn.clicked.connect(self._on_create) - variant_widget.resized.connect(self._on_variant_widget_resize) - variant_input.returnPressed.connect(self._on_create) - variant_input.textChanged.connect(self._on_variant_change) - creators_view.selectionModel().currentChanged.connect( - self._on_creator_item_change - ) - variant_hints_btn.clicked.connect(self._on_variant_btn_click) - variant_hints_menu.triggered.connect(self._on_variant_action) - assets_widget.selection_changed.connect(self._on_asset_change) - assets_widget.current_context_required.connect( - self._on_current_session_context_request - ) - tasks_widget.task_changed.connect(self._on_task_change) - creator_short_desc_widget.height_changed.connect( - self._on_description_height_change - ) - splitter_widget.splitterMoved.connect(self._on_splitter_move) - - controller.add_plugins_refresh_callback(self._on_plugins_refresh) - - self._overlay_object = overlay_object - - self._splitter_widget = splitter_widget - - self._context_widget = context_widget - self._assets_widget = assets_widget - self._tasks_widget = tasks_widget - - self.subset_name_input = subset_name_input - - self.variant_input = variant_input - self.variant_hints_btn = variant_hints_btn - self.variant_hints_menu = variant_hints_menu - self.variant_hints_group = variant_hints_group - - self._creators_header_widget = creators_header_widget - self._creators_model = creators_model - self._creators_sort_model = creators_sort_model - self._creators_view = creators_view - self._create_btn = create_btn - - self._creator_short_desc_widget = creator_short_desc_widget - self._pre_create_widget = pre_create_widget - self._attr_separator_widget = attr_separator_widget - - self._detail_placoholder_widget = detail_placoholder_widget - self._detail_description_widget = detail_description_widget - self._detail_description_input = detail_description_input - self._help_btn = help_btn - - self._prereq_timer = prereq_timer - self._first_show = True - - # Description animation - self._description_size_policy = detail_description_widget.sizePolicy() - self._desc_width_anim_timer = desc_width_anim_timer - self._desc_widget_step = 0 - self._last_description_width = None - self._last_full_width = 0 - self._expected_description_width = 0 - self._last_desc_max_width = None - self._other_widgets_widths = [] - - def _emit_message(self, message): - self._overlay_object.add_message(message) - - def _context_change_is_enabled(self): - return self._context_widget.isEnabled() - - def _get_asset_name(self): - asset_name = None - if self._context_change_is_enabled(): - asset_name = self._assets_widget.get_selected_asset_name() - - if asset_name is None: - asset_name = self._asset_name - return asset_name - - def _get_task_name(self): - task_name = None - if self._context_change_is_enabled(): - # Don't use selection of task if asset is not set - asset_name = self._assets_widget.get_selected_asset_name() - if asset_name: - task_name = self._tasks_widget.get_selected_task_name() - - if not task_name: - task_name = self._task_name - return task_name - - @property - def dbcon(self): - return self.controller.dbcon - - def _set_context_enabled(self, enabled): - self._assets_widget.set_enabled(enabled) - self._tasks_widget.set_enabled(enabled) - check_prereq = self._context_widget.isEnabled() != enabled - self._context_widget.setEnabled(enabled) - if check_prereq: - self._invalidate_prereq() - - def refresh(self): - # Get context before refresh to keep selection of asset and - # task widgets - asset_name = self._get_asset_name() - task_name = self._get_task_name() - - self._prereq_available = False - - # Disable context widget so refresh of asset will use context asset - # name - self._set_context_enabled(False) - - self._assets_widget.refresh() - - # Refresh data before update of creators - self._refresh_asset() - # Then refresh creators which may trigger callbacks using refreshed - # data - self._refresh_creators() - - self._assets_widget.set_current_asset_name(self._asset_name) - self._assets_widget.select_asset_by_name(asset_name) - self._tasks_widget.set_asset_name(asset_name) - self._tasks_widget.select_task_name(task_name) - - self._invalidate_prereq_deffered() - - def _invalidate_prereq_deffered(self): - self._prereq_timer.start() - - def _on_asset_filter_height_change(self, height): - self._creators_header_widget.setMinimumHeight(height) - self._creators_header_widget.setMaximumHeight(height) - - def _invalidate_prereq(self): - prereq_available = True - creator_btn_tooltips = [] - - available_creators = self._creators_model.rowCount() > 0 - if available_creators != self._creators_view.isEnabled(): - self._creators_view.setEnabled(available_creators) - - if not available_creators: - prereq_available = False - creator_btn_tooltips.append("Creator is not selected") - - if self._context_change_is_enabled() and self._asset_doc is None: - # QUESTION how to handle invalid asset? - prereq_available = False - creator_btn_tooltips.append("Context is not selected") - - if prereq_available != self._prereq_available: - self._prereq_available = prereq_available - - self._create_btn.setEnabled(prereq_available) - - self.variant_input.setEnabled(prereq_available) - self.variant_hints_btn.setEnabled(prereq_available) - - tooltip = "" - if creator_btn_tooltips: - tooltip = "\n".join(creator_btn_tooltips) - self._create_btn.setToolTip(tooltip) - - self._on_variant_change() - - def _refresh_asset(self): - asset_name = self._get_asset_name() - - # Skip if asset did not change - if self._asset_doc and self._asset_doc["name"] == asset_name: - return - - # Make sure `_asset_doc` and `_subset_names` variables are reset - self._asset_doc = None - self._subset_names = None - if asset_name is None: - return - - project_name = self.dbcon.active_project() - asset_doc = get_asset_by_name(project_name, asset_name) - self._asset_doc = asset_doc - - if asset_doc: - asset_id = asset_doc["_id"] - subset_docs = get_subsets( - project_name, asset_ids=[asset_id], fields=["name"] - ) - self._subset_names = { - subset_doc["name"] - for subset_doc in subset_docs - } - - if not asset_doc: - self.subset_name_input.setText("< Asset is not set >") - - def _refresh_creators(self): - # Refresh creators and add their families to list - existing_items = {} - old_creators = set() - for row in range(self._creators_model.rowCount()): - item = self._creators_model.item(row, 0) - identifier = item.data(CREATOR_IDENTIFIER_ROLE) - existing_items[identifier] = item - old_creators.add(identifier) - - # Add new families - new_creators = set() - for identifier, creator in self.controller.manual_creators.items(): - # TODO add details about creator - new_creators.add(identifier) - if identifier in existing_items: - item = existing_items[identifier] - else: - item = QtGui.QStandardItem() - item.setFlags( - QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable - ) - self._creators_model.appendRow(item) - - label = creator.label or identifier - item.setData(label, QtCore.Qt.DisplayRole) - item.setData(identifier, CREATOR_IDENTIFIER_ROLE) - item.setData(creator.family, FAMILY_ROLE) - - # Remove families that are no more available - for identifier in (old_creators - new_creators): - item = existing_items[identifier] - self._creators_model.takeRow(item.row()) - - if self._creators_model.rowCount() < 1: - return - - self._creators_sort_model.sort(0) - # Make sure there is a selection - indexes = self._creators_view.selectedIndexes() - if not indexes: - index = self._creators_sort_model.index(0, 0) - self._creators_view.setCurrentIndex(index) - else: - index = indexes[0] - - identifier = index.data(CREATOR_IDENTIFIER_ROLE) - - self._set_creator_by_identifier(identifier) - - def _on_plugins_refresh(self): - # Trigger refresh only if is visible - if self.isVisible(): - self.refresh() - - def _on_asset_change(self): - self._refresh_asset() - - asset_name = self._assets_widget.get_selected_asset_name() - self._tasks_widget.set_asset_name(asset_name) - if self._context_change_is_enabled(): - self._invalidate_prereq_deffered() - - def _on_task_change(self): - if self._context_change_is_enabled(): - self._invalidate_prereq_deffered() - - def _on_current_session_context_request(self): - self._assets_widget.set_current_session_asset() - if self._task_name: - self._tasks_widget.select_task_name(self._task_name) - - def _on_description_height_change(self): - # Use separator's 'y' position as height - height = self._attr_separator_widget.y() - self._detail_placoholder_widget.setMinimumHeight(height) - self._detail_placoholder_widget.setMaximumHeight(height) - - def _on_creator_item_change(self, new_index, _old_index): - identifier = None - if new_index.isValid(): - identifier = new_index.data(CREATOR_IDENTIFIER_ROLE) - self._set_creator_by_identifier(identifier) - - def _update_help_btn(self): - short_desc_rect = self._creator_short_desc_widget.rect() - - # point = short_desc_rect.topRight() - point = short_desc_rect.center() - mapped_point = self._creator_short_desc_widget.mapTo(self, point) - # pos_y = mapped_point.y() - center_pos_y = mapped_point.y() - icon_width = self._help_btn.get_icon_width() - - _height = int(icon_width * 2.5) - height = min(_height, short_desc_rect.height()) - pos_y = center_pos_y - int(height / 2) - - pos_x = self.width() - icon_width - if self._detail_placoholder_widget.isVisible(): - pos_x -= ( - self._detail_placoholder_widget.width() - + self._splitter_widget.handle(3).width() - ) - - width = self.width() - pos_x - - self._help_btn.set_pos_and_size( - max(0, pos_x), max(0, pos_y), - width, height - ) - - def _on_help_btn_resize(self, height): - if self._creator_short_desc_widget.height() != height: - self._update_help_btn() - - def _on_splitter_move(self, *args): - self._update_help_btn() - - def _on_help_btn(self): - if self._desc_width_anim_timer.isActive(): - return - - final_size = self.size() - cur_sizes = self._splitter_widget.sizes() - - if self._desc_widget_step == 0: - now_visible = self._detail_description_widget.isVisible() - else: - now_visible = self._desc_widget_step > 0 - - sizes = [] - for idx, value in enumerate(cur_sizes): - if idx < 3: - sizes.append(value) - - self._last_full_width = final_size.width() - self._other_widgets_widths = list(sizes) - - if now_visible: - cur_desc_width = self._detail_description_widget.width() - if cur_desc_width < 1: - cur_desc_width = 2 - step_size = int(cur_desc_width / 5) - if step_size < 1: - step_size = 1 - - step_size *= -1 - expected_width = 0 - desc_width = cur_desc_width - 1 - width = final_size.width() - 1 - min_max = desc_width - self._last_description_width = cur_desc_width - - else: - self._detail_description_widget.setVisible(True) - handle = self._splitter_widget.handle(3) - desc_width = handle.sizeHint().width() - if self._last_description_width: - expected_width = self._last_description_width - else: - hint = self._detail_description_widget.sizeHint() - expected_width = hint.width() - - width = final_size.width() + desc_width - step_size = int(expected_width / 5) - if step_size < 1: - step_size = 1 - min_max = 0 - - if self._last_desc_max_width is None: - self._last_desc_max_width = ( - self._detail_description_widget.maximumWidth() - ) - self._detail_description_widget.setMinimumWidth(min_max) - self._detail_description_widget.setMaximumWidth(min_max) - self._expected_description_width = expected_width - self._desc_widget_step = step_size - - self._desc_width_anim_timer.start() - - sizes.append(desc_width) - - final_size.setWidth(width) - - self._splitter_widget.setSizes(sizes) - self.resize(final_size) - - self._help_btn.set_expanded(not now_visible) - - def _on_desc_animation(self): - current_width = self._detail_description_widget.width() - - desc_width = None - last_step = False - growing = self._desc_widget_step > 0 - - # Growing - if growing: - if current_width < self._expected_description_width: - desc_width = current_width + self._desc_widget_step - if desc_width >= self._expected_description_width: - desc_width = self._expected_description_width - last_step = True - - # Decreasing - elif self._desc_widget_step < 0: - if current_width > self._expected_description_width: - desc_width = current_width + self._desc_widget_step - if desc_width <= self._expected_description_width: - desc_width = self._expected_description_width - last_step = True - - if desc_width is None: - self._desc_widget_step = 0 - self._desc_width_anim_timer.stop() - return - - if last_step and not growing: - self._detail_description_widget.setVisible(False) - QtWidgets.QApplication.processEvents() - - width = self._last_full_width - handle_width = self._splitter_widget.handle(3).width() - if growing: - width += (handle_width + desc_width) - else: - width -= self._last_description_width - if last_step: - width -= handle_width - else: - width += desc_width - - if not last_step or growing: - self._detail_description_widget.setMaximumWidth(desc_width) - self._detail_description_widget.setMinimumWidth(desc_width) - - window_size = self.size() - window_size.setWidth(width) - self.resize(window_size) - if not last_step: - return - - self._desc_widget_step = 0 - self._desc_width_anim_timer.stop() - - if not growing: - return - - self._detail_description_widget.setMinimumWidth(0) - self._detail_description_widget.setMaximumWidth( - self._last_desc_max_width - ) - self._detail_description_widget.setSizePolicy( - self._description_size_policy - ) - - sizes = list(self._other_widgets_widths) - sizes.append(desc_width) - self._splitter_widget.setSizes(sizes) - - def _set_creator_detailed_text(self, creator): - if not creator: - self._detail_description_input.setPlainText("") - return - detailed_description = creator.get_detail_description() or "" - if commonmark: - html = commonmark.commonmark(detailed_description) - self._detail_description_input.setHtml(html) - else: - self._detail_description_input.setMarkdown(detailed_description) - - def _set_creator_by_identifier(self, identifier): - creator = self.controller.manual_creators.get(identifier) - self._set_creator(creator) - - def _set_creator(self, creator): - self._creator_short_desc_widget.set_plugin(creator) - self._set_creator_detailed_text(creator) - self._pre_create_widget.set_plugin(creator) - - self._selected_creator = creator - - if not creator: - self._set_context_enabled(False) - return - - if ( - creator.create_allow_context_change - != self._context_change_is_enabled() - ): - self._set_context_enabled(creator.create_allow_context_change) - self._refresh_asset() - - default_variants = creator.get_default_variants() - if not default_variants: - default_variants = ["Main"] - - default_variant = creator.get_default_variant() - if not default_variant: - default_variant = default_variants[0] - - for action in tuple(self.variant_hints_menu.actions()): - self.variant_hints_menu.removeAction(action) - action.deleteLater() - - for variant in default_variants: - if variant in SEPARATORS: - self.variant_hints_menu.addSeparator() - elif variant: - self.variant_hints_menu.addAction(variant) - - variant_text = default_variant or "Main" - # Make sure subset name is updated to new plugin - if variant_text == self.variant_input.text(): - self._on_variant_change() - else: - self.variant_input.setText(variant_text) - - def _on_variant_widget_resize(self): - self.variant_hints_btn.setFixedHeight(self.variant_input.height()) - - def _on_variant_btn_click(self): - pos = self.variant_hints_btn.rect().bottomLeft() - point = self.variant_hints_btn.mapToGlobal(pos) - self.variant_hints_menu.popup(point) - - def _on_variant_action(self, action): - value = action.text() - if self.variant_input.text() != value: - self.variant_input.setText(value) - - def _on_variant_change(self, variant_value=None): - if not self._prereq_available: - return - - # This should probably never happen? - if not self._selected_creator: - if self.subset_name_input.text(): - self.subset_name_input.setText("") - return - - if variant_value is None: - variant_value = self.variant_input.text() - - if not self._compiled_name_pattern.match(variant_value): - self._create_btn.setEnabled(False) - self._set_variant_state_property("invalid") - self.subset_name_input.setText("< Invalid variant >") - return - - if not self._context_change_is_enabled(): - self._create_btn.setEnabled(True) - self._set_variant_state_property("") - self.subset_name_input.setText("< Valid variant >") - return - - project_name = self.controller.project_name - task_name = self._get_task_name() - - asset_doc = copy.deepcopy(self._asset_doc) - # Calculate subset name with Creator plugin - try: - subset_name = self._selected_creator.get_subset_name( - variant_value, task_name, asset_doc, project_name - ) - except TaskNotSetError: - self._create_btn.setEnabled(False) - self._set_variant_state_property("invalid") - self.subset_name_input.setText("< Missing task >") - return - - self.subset_name_input.setText(subset_name) - - self._create_btn.setEnabled(True) - self._validate_subset_name(subset_name, variant_value) - - def _validate_subset_name(self, subset_name, variant_value): - # Get all subsets of the current asset - if self._subset_names: - existing_subset_names = set(self._subset_names) - else: - existing_subset_names = set() - existing_subset_names_low = set( - _name.lower() - for _name in existing_subset_names - ) - - # Replace - compare_regex = re.compile(re.sub( - variant_value, "(.+)", subset_name, flags=re.IGNORECASE - )) - variant_hints = set() - if variant_value: - for _name in existing_subset_names: - _result = compare_regex.search(_name) - if _result: - variant_hints |= set(_result.groups()) - - # Remove previous hints from menu - for action in tuple(self.variant_hints_group.actions()): - self.variant_hints_group.removeAction(action) - self.variant_hints_menu.removeAction(action) - action.deleteLater() - - # Add separator if there are hints and menu already has actions - if variant_hints and self.variant_hints_menu.actions(): - self.variant_hints_menu.addSeparator() - - # Add hints to actions - for variant_hint in variant_hints: - action = self.variant_hints_menu.addAction(variant_hint) - self.variant_hints_group.addAction(action) - - # Indicate subset existence - if not variant_value: - property_value = "empty" - - elif subset_name.lower() in existing_subset_names_low: - # validate existence of subset name with lowered text - # - "renderMain" vs. "rendermain" mean same path item for - # windows - property_value = "exists" - else: - property_value = "new" - - self._set_variant_state_property(property_value) - - variant_is_valid = variant_value.strip() != "" - if variant_is_valid != self._create_btn.isEnabled(): - self._create_btn.setEnabled(variant_is_valid) - - def _set_variant_state_property(self, state): - current_value = self.variant_input.property("state") - if current_value != state: - self.variant_input.setProperty("state", state) - self.variant_input.style().polish(self.variant_input) - - def _on_first_show(self): - center = self.rect().center() - - width, height = self.default_size - self.resize(width, height) - part = int(width / 7) - self._splitter_widget.setSizes( - [part * 2, part * 2, width - (part * 4)] - ) - - new_pos = self.mapToGlobal(center) - new_pos.setX(new_pos.x() - int(self.width() / 2)) - new_pos.setY(new_pos.y() - int(self.height() / 2)) - self.move(new_pos) - - def moveEvent(self, event): - super(CreateDialog, self).moveEvent(event) - self._last_pos = self.pos() - - def showEvent(self, event): - super(CreateDialog, self).showEvent(event) - if self._first_show: - self._first_show = False - self._on_first_show() - - if self._last_pos is not None: - self.move(self._last_pos) - - self._update_help_btn() - - self.refresh() - - def resizeEvent(self, event): - super(CreateDialog, self).resizeEvent(event) - self._update_help_btn() - - def _on_create(self): - indexes = self._creators_view.selectedIndexes() - if not indexes or len(indexes) > 1: - return - - if not self._create_btn.isEnabled(): - return - - index = indexes[0] - creator_label = index.data(QtCore.Qt.DisplayRole) - creator_identifier = index.data(CREATOR_IDENTIFIER_ROLE) - family = index.data(FAMILY_ROLE) - variant = self.variant_input.text() - # Care about subset name only if context change is enabled - subset_name = None - asset_name = None - task_name = None - if self._context_change_is_enabled(): - subset_name = self.subset_name_input.text() - asset_name = self._get_asset_name() - task_name = self._get_task_name() - - pre_create_data = self._pre_create_widget.current_value() - # Where to define these data? - # - what data show be stored? - instance_data = { - "asset": asset_name, - "task": task_name, - "variant": variant, - "family": family - } - - error_msg = None - formatted_traceback = None - try: - self.controller.create( - creator_identifier, - subset_name, - instance_data, - pre_create_data - ) - - except CreatorError as exc: - error_msg = str(exc) - - # Use bare except because some hosts raise their exceptions that - # do not inherit from python's `BaseException` - except: - exc_type, exc_value, exc_traceback = sys.exc_info() - formatted_traceback = "".join(traceback.format_exception( - exc_type, exc_value, exc_traceback - )) - error_msg = str(exc_value) - - if error_msg is None: - self._set_creator(self._selected_creator) - self._emit_message("Creation finished...") - else: - box = CreateErrorMessageBox( - creator_label, - subset_name, - asset_name, - error_msg, - formatted_traceback, - parent=self - ) - box.show() - # Store dialog so is not garbage collected before is shown - self._message_dialog = box diff --git a/openpype/tools/publisher/widgets/tabs_widget.py b/openpype/tools/publisher/widgets/tabs_widget.py index 0e92a6fd8d..bf3ef9eec0 100644 --- a/openpype/tools/publisher/widgets/tabs_widget.py +++ b/openpype/tools/publisher/widgets/tabs_widget.py @@ -53,6 +53,11 @@ class PublisherTabsWidget(QtWidgets.QFrame): self._current_button = None self._buttons_by_identifier = {} + def is_current_tab(self, identifier): + if isinstance(identifier, PublisherTabBtn): + identifier = identifier.identifier + return self._current_button == identifier + def add_tab(self, label, identifier): button = PublisherTabBtn(identifier, label, self) button.tab_clicked.connect(self._on_tab_click) @@ -61,8 +66,12 @@ class PublisherTabsWidget(QtWidgets.QFrame): if self._current_button is None: self.set_current_tab(identifier) + return button def set_current_tab(self, identifier): + if isinstance(identifier, PublisherTabBtn): + identifier = identifier.identifier + if identifier == self._current_button: return diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 8df9f9bbf5..5fd558a1b5 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -15,8 +15,6 @@ from .widgets import ( PublisherTabsWidget, - CreateDialog, - StopBtn, ResetBtn, ValidateBtn, @@ -76,7 +74,7 @@ class PublisherWindow(QtWidgets.QDialog): # Tabs widget under header tabs_widget = PublisherTabsWidget(self) - tabs_widget.add_tab("Create", "create") + create_tab = tabs_widget.add_tab("Create", "create") tabs_widget.add_tab("Publish", "publish") tabs_widget.add_tab("Report", "report") tabs_widget.add_tab("Details", "details") @@ -137,8 +135,6 @@ class PublisherWindow(QtWidgets.QDialog): main_layout.addWidget(content_stacked_widget, 1) main_layout.addWidget(footer_widget, 0) - creator_window = CreateDialog(controller, parent=self) - tabs_widget.tab_changed.connect(self._on_tab_change) create_overview_widget.active_changed.connect( self._on_context_or_active_change @@ -165,6 +161,7 @@ class PublisherWindow(QtWidgets.QDialog): self._header_layout = header_layout self._tabs_widget = tabs_widget + self._create_tab = create_tab self._content_stacked_widget = content_stacked_widget self.content_stacked_layout = content_stacked_layout @@ -182,8 +179,6 @@ class PublisherWindow(QtWidgets.QDialog): self._controller = controller - self.creator_window = creator_window - @property def controller(self): return self._controller @@ -208,7 +203,10 @@ class PublisherWindow(QtWidgets.QDialog): self.context_label.setText(label) def _on_tab_change(self, prev_tab, new_tab): - print(prev_tab, new_tab) + if new_tab in ("create", "publish"): + self._create_overview_widget.set_state(prev_tab, new_tab) + + # TODO handle rest of conditions def _on_context_or_active_change(self): self._validate_create_instances() @@ -222,23 +220,9 @@ class PublisherWindow(QtWidgets.QDialog): def _set_publish_visibility(self, visible): if visible: widget = self.publish_frame - publish_frame_visible = True else: widget = self._create_overview_widget - publish_frame_visible = False self.content_stacked_layout.setCurrentWidget(widget) - self._set_publish_frame_visible(publish_frame_visible) - - def _set_publish_frame_visible(self, publish_frame_visible): - """Publish frame visibility has changed. - - Also used in TrayPublisher to be able handle start/end of publish - widget overlay. - """ - - # Hide creator dialog if visible - if publish_frame_visible and self.creator_window.isVisible(): - self.creator_window.close() def _on_reset_clicked(self): self._controller.reset() @@ -264,7 +248,6 @@ class PublisherWindow(QtWidgets.QDialog): self._controller.publish() def _set_footer_enabled(self, enabled): - self.comment_input.setEnabled(enabled) self.reset_btn.setEnabled(True) if enabled: self.stop_btn.setEnabled(False) @@ -276,6 +259,8 @@ class PublisherWindow(QtWidgets.QDialog): self.publish_btn.setEnabled(enabled) def _on_publish_reset(self): + self._create_tab.setEnabled(True) + self.comment_input.setVisible(True) self._set_publish_visibility(False) self._set_footer_enabled(False) @@ -286,6 +271,11 @@ class PublisherWindow(QtWidgets.QDialog): self.validate_btn.setEnabled(False) self.publish_btn.setEnabled(False) + self.comment_input.setVisible(False) + self._create_tab.setEnabled(False) + if self._tabs_widget.is_current_tab(self._create_tab): + self._tabs_widget.set_current_tab("publish") + def _on_publish_validated(self): self.validate_btn.setEnabled(False) From 88ace97c941def6b9189cc4bc9e8bd6792d173bf Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Sep 2022 17:57:16 +0200 Subject: [PATCH 030/121] fixed project selection in tray publisher --- openpype/tools/traypublisher/window.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/openpype/tools/traypublisher/window.py b/openpype/tools/traypublisher/window.py index 128c0fef11..97edb9ab06 100644 --- a/openpype/tools/traypublisher/window.py +++ b/openpype/tools/traypublisher/window.py @@ -137,11 +137,14 @@ class StandaloneOverlayWidget(QtWidgets.QFrame): src_index = self._projects_model.find_project(project_name) if src_index is not None: index = self._projects_proxy.mapFromSource(src_index) - if index: - mode = ( - QtCore.QItemSelectionModel.Select - | QtCore.QItemSelectionModel.Rows) - self._projects_view.selectionModel().select(index, mode) + + if index is not None: + selection_model = self._projects_view.selectionModel() + selection_model.select( + index, + QtCore.QItemSelectionModel.SelectCurrent + ) + self._projects_view.setCurrentIndex(index) self._cancel_btn.setVisible(self._project_name is not None) super(StandaloneOverlayWidget, self).showEvent(event) @@ -239,15 +242,15 @@ class TrayPublishWindow(PublisherWindow): def _on_project_select(self, project_name): # TODO register project specific plugin paths - self.controller.save_changes() - self.controller.reset_project_data_cache() + self._controller.save_changes() + self._controller.reset_project_data_cache() self.reset() - if not self.controller.instances: + if not self._controller.instances: self._go_to_create_tab() def _on_tray_publish_save(self): - self.controller.save_changes() + self._controller.save_changes() print("NOT YET IMPLEMENTED") From 5857c01442dc81daa71e71f78725a682c450f27e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Sep 2022 18:20:08 +0200 Subject: [PATCH 031/121] changed PublishReportViewerWidget to frame --- openpype/tools/publisher/publish_report_viewer/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/publisher/publish_report_viewer/widgets.py b/openpype/tools/publisher/publish_report_viewer/widgets.py index 61eb814a56..dc82448495 100644 --- a/openpype/tools/publisher/publish_report_viewer/widgets.py +++ b/openpype/tools/publisher/publish_report_viewer/widgets.py @@ -331,7 +331,7 @@ class DetailsPopup(QtWidgets.QDialog): self.closed.emit() -class PublishReportViewerWidget(QtWidgets.QWidget): +class PublishReportViewerWidget(QtWidgets.QFrame): def __init__(self, parent=None): super(PublishReportViewerWidget, self).__init__(parent) From 55e4aa3c76d397e59c909021e4eb642f6e73821a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Sep 2022 18:20:33 +0200 Subject: [PATCH 032/121] added details to window --- openpype/tools/publisher/widgets/__init__.py | 1 + .../tools/publisher/widgets/create_widget.py | 1 + .../publisher/widgets/overview_widget.py | 9 +- openpype/tools/publisher/window.py | 128 +++++++++++------- 4 files changed, 86 insertions(+), 53 deletions(-) diff --git a/openpype/tools/publisher/widgets/__init__.py b/openpype/tools/publisher/widgets/__init__.py index 1d0ed0633b..f8e3c4b19b 100644 --- a/openpype/tools/publisher/widgets/__init__.py +++ b/openpype/tools/publisher/widgets/__init__.py @@ -13,6 +13,7 @@ from .publish_widget import PublishFrame from .tabs_widget import PublisherTabsWidget from .overview_widget import CreateOverviewWidget + __all__ = ( "get_icon_path", "get_pixmap", diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py index a0b3db0409..733dbf18ca 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -443,6 +443,7 @@ class CreateWidget(QtWidgets.QWidget): splitter_widget.setStretchFactor(3, 1) layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(splitter_widget, 1) prereq_timer = QtCore.QTimer() diff --git a/openpype/tools/publisher/widgets/overview_widget.py b/openpype/tools/publisher/widgets/overview_widget.py index ddc976d458..7afe02116f 100644 --- a/openpype/tools/publisher/widgets/overview_widget.py +++ b/openpype/tools/publisher/widgets/overview_widget.py @@ -85,12 +85,7 @@ class CreateOverviewWidget(QtWidgets.QFrame): # Subset frame layout main_layout = QtWidgets.QVBoxLayout(self) - marings = main_layout.contentsMargins() - marings.setLeft(marings.left() * 2) - marings.setRight(marings.right() * 2) - marings.setTop(marings.top() * 2) - marings.setBottom(0) - main_layout.setContentsMargins(marings) + main_layout.setContentsMargins(0, 0, 0, 0) main_layout.addWidget(subset_content_widget, 1) # --- Calbacks for instances/subsets view --- @@ -137,7 +132,7 @@ class CreateOverviewWidget(QtWidgets.QFrame): self._current_state = "create" subset_attributes_wrap.setVisible(False) - def set_state(self, old_state, new_state): + def set_state(self, new_state, animate): if new_state == self._current_state: return diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 5fd558a1b5..4aa02ff2d5 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -8,6 +8,7 @@ from openpype.tools.utils import ( PlaceholderLineEdit, PixmapLabel ) +from .publish_report_viewer import PublishReportViewerWidget from .control import PublisherController from .widgets import ( CreateOverviewWidget, @@ -79,13 +80,6 @@ class PublisherWindow(QtWidgets.QDialog): tabs_widget.add_tab("Report", "report") tabs_widget.add_tab("Details", "details") - # Content - content_stacked_widget = QtWidgets.QWidget(self) - - create_overview_widget = CreateOverviewWidget( - controller, content_stacked_widget - ) - # Footer footer_widget = QtWidgets.QWidget(self) footer_bottom_widget = QtWidgets.QWidget(footer_widget) @@ -113,17 +107,45 @@ class PublisherWindow(QtWidgets.QDialog): footer_layout.addWidget(comment_input, 0) footer_layout.addWidget(footer_bottom_widget, 0) + # Content + # - wrap stacked widget under one more widget to be able propagate + # margins (QStackedLayout can't have margins) + content_widget = QtWidgets.QWidget(self) + + content_stacked_widget = QtWidgets.QWidget(content_widget) + + content_layout = QtWidgets.QVBoxLayout(content_widget) + marings = content_layout.contentsMargins() + marings.setLeft(marings.left() * 2) + marings.setRight(marings.right() * 2) + marings.setTop(marings.top() * 2) + marings.setBottom(0) + content_layout.setContentsMargins(marings) + content_layout.addWidget(content_stacked_widget, 1) + + # Overview - create and attributes part + overview_widget = CreateOverviewWidget( + controller, content_stacked_widget + ) + + # Details - Publish details + publish_details_widget = PublishReportViewerWidget( + content_stacked_widget + ) + # Create publish frame publish_frame = PublishFrame(controller, content_stacked_widget) content_stacked_layout = QtWidgets.QStackedLayout( content_stacked_widget ) + content_stacked_layout.setContentsMargins(0, 0, 0, 0) content_stacked_layout.setStackingMode( QtWidgets.QStackedLayout.StackAll ) - content_stacked_layout.addWidget(create_overview_widget) + content_stacked_layout.addWidget(overview_widget) + content_stacked_layout.addWidget(publish_details_widget) content_stacked_layout.addWidget(publish_frame) # Add main frame to this window @@ -132,17 +154,17 @@ class PublisherWindow(QtWidgets.QDialog): main_layout.setSpacing(0) main_layout.addWidget(header_widget, 0) main_layout.addWidget(tabs_widget, 0) - main_layout.addWidget(content_stacked_widget, 1) + main_layout.addWidget(content_widget, 1) main_layout.addWidget(footer_widget, 0) tabs_widget.tab_changed.connect(self._on_tab_change) - create_overview_widget.active_changed.connect( + overview_widget.active_changed.connect( self._on_context_or_active_change ) - create_overview_widget.instance_context_changed.connect( + overview_widget.instance_context_changed.connect( self._on_context_or_active_change ) - create_overview_widget.create_requested.connect( + overview_widget.create_requested.connect( self._on_create_request ) @@ -164,18 +186,20 @@ class PublisherWindow(QtWidgets.QDialog): self._create_tab = create_tab self._content_stacked_widget = content_stacked_widget - self.content_stacked_layout = content_stacked_layout - self._create_overview_widget = create_overview_widget - self.publish_frame = publish_frame + self._content_stacked_layout = content_stacked_layout - self.context_label = context_label + self._overview_widget = overview_widget + self._publish_details_widget = publish_details_widget + self._publish_frame = publish_frame - self.comment_input = comment_input + self._context_label = context_label - self.stop_btn = stop_btn - self.reset_btn = reset_btn - self.validate_btn = validate_btn - self.publish_btn = publish_btn + self._comment_input = comment_input + + self._stop_btn = stop_btn + self._reset_btn = reset_btn + self._validate_btn = validate_btn + self._publish_btn = publish_btn self._controller = controller @@ -200,11 +224,23 @@ class PublisherWindow(QtWidgets.QDialog): self._controller.reset() def set_context_label(self, label): - self.context_label.setText(label) + self._context_label.setText(label) - def _on_tab_change(self, prev_tab, new_tab): + def _on_tab_change(self, old_tab, new_tab): if new_tab in ("create", "publish"): - self._create_overview_widget.set_state(prev_tab, new_tab) + animate = True + if old_tab not in ("create", "publish"): + animate = False + self._content_stacked_layout.setCurrentWidget( + self._overview_widget + ) + self._overview_widget.set_state(new_tab, animate) + + elif new_tab == "details": + self._content_stacked_layout.setCurrentWidget( + self._publish_details_widget + ) + # TODO handle rest of conditions @@ -219,10 +255,10 @@ class PublisherWindow(QtWidgets.QDialog): def _set_publish_visibility(self, visible): if visible: - widget = self.publish_frame + widget = self._publish_frame else: - widget = self._create_overview_widget - self.content_stacked_layout.setCurrentWidget(widget) + widget = self._overview_widget + self._content_stacked_layout.setCurrentWidget(widget) def _on_reset_clicked(self): self._controller.reset() @@ -234,7 +270,7 @@ class PublisherWindow(QtWidgets.QDialog): if self._controller.publish_comment_is_set: return - comment = self.comment_input.text() + comment = self._comment_input.text() self._controller.set_comment(comment) def _on_validate_clicked(self): @@ -248,40 +284,40 @@ class PublisherWindow(QtWidgets.QDialog): self._controller.publish() def _set_footer_enabled(self, enabled): - self.reset_btn.setEnabled(True) + self._reset_btn.setEnabled(True) if enabled: - self.stop_btn.setEnabled(False) - self.validate_btn.setEnabled(True) - self.publish_btn.setEnabled(True) + self._stop_btn.setEnabled(False) + self._validate_btn.setEnabled(True) + self._publish_btn.setEnabled(True) else: - self.stop_btn.setEnabled(enabled) - self.validate_btn.setEnabled(enabled) - self.publish_btn.setEnabled(enabled) + self._stop_btn.setEnabled(enabled) + self._validate_btn.setEnabled(enabled) + self._publish_btn.setEnabled(enabled) def _on_publish_reset(self): self._create_tab.setEnabled(True) - self.comment_input.setVisible(True) + self._comment_input.setVisible(True) self._set_publish_visibility(False) self._set_footer_enabled(False) def _on_publish_start(self): - self.reset_btn.setEnabled(False) - self.stop_btn.setEnabled(True) - self.validate_btn.setEnabled(False) - self.publish_btn.setEnabled(False) + self._reset_btn.setEnabled(False) + self._stop_btn.setEnabled(True) + self._validate_btn.setEnabled(False) + self._publish_btn.setEnabled(False) - self.comment_input.setVisible(False) + self._comment_input.setVisible(False) self._create_tab.setEnabled(False) if self._tabs_widget.is_current_tab(self._create_tab): self._tabs_widget.set_current_tab("publish") def _on_publish_validated(self): - self.validate_btn.setEnabled(False) + self._validate_btn.setEnabled(False) def _on_publish_stop(self): - self.reset_btn.setEnabled(True) - self.stop_btn.setEnabled(False) + self._reset_btn.setEnabled(True) + self._stop_btn.setEnabled(False) validate_enabled = not self._controller.publish_has_crashed publish_enabled = not self._controller.publish_has_crashed if validate_enabled: @@ -296,8 +332,8 @@ class PublisherWindow(QtWidgets.QDialog): else: publish_enabled = not self._controller.publish_has_finished - self.validate_btn.setEnabled(validate_enabled) - self.publish_btn.setEnabled(publish_enabled) + self._validate_btn.setEnabled(validate_enabled) + self._publish_btn.setEnabled(publish_enabled) def _validate_create_instances(self): if not self._controller.host_is_valid: From bd9a987f7ba9de82a75f3d6b8e3d0cfbcd1b872c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Sep 2022 18:33:22 +0200 Subject: [PATCH 033/121] update details in certain situations --- openpype/tools/publisher/widgets/tabs_widget.py | 15 +++++++++------ openpype/tools/publisher/window.py | 16 ++++++++++++++-- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/openpype/tools/publisher/widgets/tabs_widget.py b/openpype/tools/publisher/widgets/tabs_widget.py index bf3ef9eec0..84638a002c 100644 --- a/openpype/tools/publisher/widgets/tabs_widget.py +++ b/openpype/tools/publisher/widgets/tabs_widget.py @@ -50,13 +50,13 @@ class PublisherTabsWidget(QtWidgets.QFrame): self._btns_layout = btns_layout - self._current_button = None + self._current_identifier = None self._buttons_by_identifier = {} def is_current_tab(self, identifier): if isinstance(identifier, PublisherTabBtn): identifier = identifier.identifier - return self._current_button == identifier + return self._current_identifier == identifier def add_tab(self, label, identifier): button = PublisherTabBtn(identifier, label, self) @@ -64,7 +64,7 @@ class PublisherTabsWidget(QtWidgets.QFrame): self._btns_layout.addWidget(button, 0) self._buttons_by_identifier[identifier] = button - if self._current_button is None: + if self._current_identifier is None: self.set_current_tab(identifier) return button @@ -72,21 +72,24 @@ class PublisherTabsWidget(QtWidgets.QFrame): if isinstance(identifier, PublisherTabBtn): identifier = identifier.identifier - if identifier == self._current_button: + if identifier == self._current_identifier: return new_btn = self._buttons_by_identifier.get(identifier) if new_btn is None: return - old_identifier = self._current_button + old_identifier = self._current_identifier old_btn = self._buttons_by_identifier.get(old_identifier) - self._current_button = identifier + self._current_identifier = identifier if old_btn is not None: old_btn.deactivate() new_btn.activate() self.tab_changed.emit(old_identifier, identifier) + def current_tab(self): + return self._current_identifier + def _on_tab_click(self, identifier): self.set_current_tab(identifier) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 4aa02ff2d5..95c639a56c 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -226,7 +226,17 @@ class PublisherWindow(QtWidgets.QDialog): def set_context_label(self, label): self._context_label.setText(label) + def _update_publish_details_widget(self, force=False): + if not force and self._tabs_widget.current_tab() != "details": + return + + report_data = self.controller.get_publish_report() + self._publish_details_widget.set_report_data(report_data) + def _on_tab_change(self, old_tab, new_tab): + if old_tab == "details": + self._publish_details_widget.close_details_popup() + if new_tab in ("create", "publish"): animate = True if old_tab not in ("create", "publish"): @@ -240,7 +250,7 @@ class PublisherWindow(QtWidgets.QDialog): self._content_stacked_layout.setCurrentWidget( self._publish_details_widget ) - + self._update_publish_details_widget() # TODO handle rest of conditions @@ -298,8 +308,8 @@ class PublisherWindow(QtWidgets.QDialog): self._create_tab.setEnabled(True) self._comment_input.setVisible(True) self._set_publish_visibility(False) - self._set_footer_enabled(False) + self._update_publish_details_widget() def _on_publish_start(self): self._reset_btn.setEnabled(False) @@ -334,6 +344,7 @@ class PublisherWindow(QtWidgets.QDialog): self._validate_btn.setEnabled(validate_enabled) self._publish_btn.setEnabled(publish_enabled) + self._update_publish_details_widget() def _validate_create_instances(self): if not self._controller.host_is_valid: @@ -359,3 +370,4 @@ class PublisherWindow(QtWidgets.QDialog): context_title = self.controller.get_context_title() self.set_context_label(context_title) + self._update_publish_details_widget() From acb167c0d4b48afce082f6d76dad2ecf264b0096 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Sep 2022 18:45:48 +0200 Subject: [PATCH 034/121] added report page to window --- openpype/tools/publisher/widgets/__init__.py | 6 ++++-- .../publisher/widgets/overview_widget.py | 4 ++-- .../publisher/widgets/validations_widget.py | 5 ++--- openpype/tools/publisher/window.py | 19 ++++++++++++++++--- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/openpype/tools/publisher/widgets/__init__.py b/openpype/tools/publisher/widgets/__init__.py index f8e3c4b19b..81bb77ce89 100644 --- a/openpype/tools/publisher/widgets/__init__.py +++ b/openpype/tools/publisher/widgets/__init__.py @@ -11,7 +11,8 @@ from .widgets import ( ) from .publish_widget import PublishFrame from .tabs_widget import PublisherTabsWidget -from .overview_widget import CreateOverviewWidget +from .overview_widget import OverviewWidget +from .validations_widget import ValidationsWidget __all__ = ( @@ -27,5 +28,6 @@ __all__ = ( "PublishFrame", "PublisherTabsWidget", - "CreateOverviewWidget", + "OverviewWidget", + "ValidationsWidget", ) diff --git a/openpype/tools/publisher/widgets/overview_widget.py b/openpype/tools/publisher/widgets/overview_widget.py index 7afe02116f..90527234a7 100644 --- a/openpype/tools/publisher/widgets/overview_widget.py +++ b/openpype/tools/publisher/widgets/overview_widget.py @@ -13,13 +13,13 @@ from .widgets import ( from .create_widget import CreateWidget -class CreateOverviewWidget(QtWidgets.QFrame): +class OverviewWidget(QtWidgets.QFrame): active_changed = QtCore.Signal() instance_context_changed = QtCore.Signal() create_requested = QtCore.Signal() def __init__(self, controller, parent): - super(CreateOverviewWidget, self).__init__(parent) + super(OverviewWidget, self).__init__(parent) self._refreshing_instances = False self._controller = controller diff --git a/openpype/tools/publisher/widgets/validations_widget.py b/openpype/tools/publisher/widgets/validations_widget.py index e7ab4ecf5a..b70cd81878 100644 --- a/openpype/tools/publisher/widgets/validations_widget.py +++ b/openpype/tools/publisher/widgets/validations_widget.py @@ -400,7 +400,7 @@ class VerticallScrollArea(QtWidgets.QScrollArea): return super(VerticallScrollArea, self).eventFilter(obj, event) -class ValidationsWidget(QtWidgets.QWidget): +class ValidationsWidget(QtWidgets.QFrame): """Widgets showing validation error. This widget is shown if validation error/s happened during validation part. @@ -418,11 +418,10 @@ class ValidationsWidget(QtWidgets.QWidget): │ Publish buttons │ └───────────────────────────────┘ """ + def __init__(self, controller, parent): super(ValidationsWidget, self).__init__(parent) - self.setAttribute(QtCore.Qt.WA_TranslucentBackground) - errors_scroll = VerticallScrollArea(self) errors_scroll.setWidgetResizable(True) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 95c639a56c..568735f4ae 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -11,7 +11,8 @@ from openpype.tools.utils import ( from .publish_report_viewer import PublishReportViewerWidget from .control import PublisherController from .widgets import ( - CreateOverviewWidget, + OverviewWidget, + ValidationsWidget, PublishFrame, PublisherTabsWidget, @@ -124,10 +125,12 @@ class PublisherWindow(QtWidgets.QDialog): content_layout.addWidget(content_stacked_widget, 1) # Overview - create and attributes part - overview_widget = CreateOverviewWidget( + overview_widget = OverviewWidget( controller, content_stacked_widget ) + report_widget = ValidationsWidget(controller, parent) + # Details - Publish details publish_details_widget = PublishReportViewerWidget( content_stacked_widget @@ -145,6 +148,7 @@ class PublisherWindow(QtWidgets.QDialog): QtWidgets.QStackedLayout.StackAll ) content_stacked_layout.addWidget(overview_widget) + content_stacked_layout.addWidget(report_widget) content_stacked_layout.addWidget(publish_details_widget) content_stacked_layout.addWidget(publish_frame) @@ -189,6 +193,7 @@ class PublisherWindow(QtWidgets.QDialog): self._content_stacked_layout = content_stacked_layout self._overview_widget = overview_widget + self._report_widget = report_widget self._publish_details_widget = publish_details_widget self._publish_frame = publish_frame @@ -252,7 +257,10 @@ class PublisherWindow(QtWidgets.QDialog): ) self._update_publish_details_widget() - # TODO handle rest of conditions + elif new_tab == "report": + self._content_stacked_layout.setCurrentWidget( + self._report_widget + ) def _on_context_or_active_change(self): self._validate_create_instances() @@ -319,6 +327,8 @@ class PublisherWindow(QtWidgets.QDialog): self._comment_input.setVisible(False) self._create_tab.setEnabled(False) + + self._report_widget.clear() if self._tabs_widget.is_current_tab(self._create_tab): self._tabs_widget.set_current_tab("publish") @@ -346,6 +356,9 @@ class PublisherWindow(QtWidgets.QDialog): self._publish_btn.setEnabled(publish_enabled) self._update_publish_details_widget() + validation_errors = self._controller.get_validation_errors() + self._report_widget.set_errors(validation_errors) + def _validate_create_instances(self): if not self._controller.host_is_valid: self._set_footer_enabled(True) From 3f4d59b54667b9c0fbda928173793ea31e599e9c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Sep 2022 18:45:58 +0200 Subject: [PATCH 035/121] removed report and validation from publish frame --- .../tools/publisher/widgets/publish_widget.py | 41 ++----------------- 1 file changed, 3 insertions(+), 38 deletions(-) diff --git a/openpype/tools/publisher/widgets/publish_widget.py b/openpype/tools/publisher/widgets/publish_widget.py index b32b5381d1..ea23f9c42c 100644 --- a/openpype/tools/publisher/widgets/publish_widget.py +++ b/openpype/tools/publisher/widgets/publish_widget.py @@ -6,8 +6,6 @@ from Qt import QtWidgets, QtCore, QtGui from openpype.pipeline import KnownPublishError -from .validations_widget import ValidationsWidget -from ..publish_report_viewer import PublishReportViewerWidget from .widgets import ( StopBtn, ResetBtn, @@ -101,9 +99,6 @@ class PublishFrame(QtWidgets.QFrame): self.setObjectName("PublishFrame") - # Widget showing validation errors. Their details and action callbacks. - validation_errors_widget = ValidationsWidget(controller, self) - # Bottom part of widget where process and callback buttons are showed # - QFrame used to be able set background using stylesheets easily # and not override all children widgets style @@ -203,17 +198,15 @@ class PublishFrame(QtWidgets.QFrame): publish_widget = QtWidgets.QWidget(self) publish_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) publish_layout = QtWidgets.QVBoxLayout(publish_widget) - publish_layout.addWidget(validation_errors_widget, 1) + publish_layout.addStretch(1) publish_layout.addWidget(info_frame, 0) details_widget = QtWidgets.QWidget(self) - report_view = PublishReportViewerWidget(details_widget) close_report_btn = QtWidgets.QPushButton(details_widget) close_report_icon = self._get_report_close_icon() close_report_btn.setIcon(close_report_icon) details_layout = QtWidgets.QVBoxLayout(details_widget) - details_layout.addWidget(report_view) details_layout.addWidget(close_report_btn) main_layout = QtWidgets.QStackedLayout(self) @@ -224,8 +217,6 @@ class PublishFrame(QtWidgets.QFrame): main_layout.setCurrentWidget(publish_widget) - show_details_btn.clicked.connect(self._on_show_details) - copy_report_btn.clicked.connect(self._on_copy_report) export_report_btn.clicked.connect(self._on_export_report) @@ -234,8 +225,6 @@ class PublishFrame(QtWidgets.QFrame): validate_btn.clicked.connect(self._on_validate_clicked) publish_btn.clicked.connect(self._on_publish_clicked) - close_report_btn.clicked.connect(self._on_close_report_clicked) - controller.add_publish_reset_callback(self._on_publish_reset) controller.add_publish_started_callback(self._on_publish_start) controller.add_publish_validated_callback(self._on_publish_validated) @@ -249,8 +238,6 @@ class PublishFrame(QtWidgets.QFrame): self._info_frame = info_frame self._publish_widget = publish_widget - self._validation_errors_widget = validation_errors_widget - self._main_layout = main_layout self._main_label = main_label @@ -269,7 +256,6 @@ class PublishFrame(QtWidgets.QFrame): self._publish_btn = publish_btn self._details_widget = details_widget - self._report_view = report_view def _get_report_close_icon(self): size = 100 @@ -314,8 +300,6 @@ class PublishFrame(QtWidgets.QFrame): self._progress_widget.setMaximum(self.controller.publish_max_progress) def _on_publish_start(self): - self._validation_errors_widget.clear() - self._set_success_property(-1) self._change_bg_property() self._set_progress_visibility(True) @@ -388,7 +372,7 @@ class PublishFrame(QtWidgets.QFrame): elif validation_errors: self._set_progress_visibility(False) self._change_bg_property(1) - self._set_validation_errors(validation_errors) + self._set_validation_errors() elif self.controller.publish_has_finished: self._set_finished() @@ -421,14 +405,12 @@ class PublishFrame(QtWidgets.QFrame): self._message_label_bottom.setText("") self._set_success_property(0) - def _set_validation_errors(self, validation_errors): + def _set_validation_errors(self): self._main_label.setText("Your publish didn't pass studio validations") self._message_label_top.setText("") self._message_label_bottom.setText("Check results above please") self._set_success_property(2) - self._validation_errors_widget.set_errors(validation_errors) - def _set_finished(self): self._main_label.setText("Finished") self._message_label_top.setText("") @@ -489,23 +471,6 @@ class PublishFrame(QtWidgets.QFrame): with open(full_path, "w") as file_stream: json.dump(logs, file_stream) - def _on_show_details(self): - self._change_bg_property(2) - self._main_layout.setCurrentWidget(self._details_widget) - report_data = self.controller.get_publish_report() - self._report_view.set_report_data(report_data) - - def _on_close_report_clicked(self): - self._report_view.close_details_popup() - if self.controller.get_publish_crash_error(): - self._change_bg_property() - - elif self.controller.get_validation_errors(): - self._change_bg_property(1) - else: - self._change_bg_property(2) - self._main_layout.setCurrentWidget(self._publish_widget) - def _on_reset_clicked(self): self.controller.reset() From 0c86e157f98544a5f57b80b0b59450f51ed5a49a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 29 Sep 2022 12:37:55 +0200 Subject: [PATCH 036/121] disable attributes after publish start --- openpype/tools/publisher/widgets/overview_widget.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/tools/publisher/widgets/overview_widget.py b/openpype/tools/publisher/widgets/overview_widget.py index 90527234a7..99b9d88007 100644 --- a/openpype/tools/publisher/widgets/overview_widget.py +++ b/openpype/tools/publisher/widgets/overview_widget.py @@ -38,6 +38,7 @@ class OverviewWidget(QtWidgets.QFrame): subset_views_layout = QtWidgets.QStackedLayout() subset_views_layout.addWidget(subset_view_cards) subset_views_layout.addWidget(subset_list_view) + subset_views_layout.setCurrentWidget(subset_view_cards) # Buttons at the bottom of subset view create_btn = CreateInstanceBtn(self) @@ -113,6 +114,7 @@ class OverviewWidget(QtWidgets.QFrame): ) # --- Controller callbacks --- + controller.add_publish_started_callback(self._on_publish_start) controller.add_publish_reset_callback(self._on_publish_reset) controller.add_instances_refresh_callback(self._on_instances_refresh) @@ -252,9 +254,15 @@ class OverviewWidget(QtWidgets.QFrame): # Force to change instance and refresh details self._on_subset_change() + def _on_publish_start(self): + """Publish started.""" + + self._subset_attributes_wrap.setEnabled(False) + def _on_publish_reset(self): """Context in controller has been refreshed.""" + self._subset_attributes_wrap.setEnabled(True) self._subset_content_widget.setEnabled(self._controller.host_is_valid) def _on_instances_refresh(self): From a2a686cfb86156b4bb12ed8256e572867a3e3a53 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 29 Sep 2022 12:38:50 +0200 Subject: [PATCH 037/121] added base of validation labels showed in different stages of publishing --- .../publisher/widgets/validations_widget.py | 136 ++++++++++++++++-- 1 file changed, 124 insertions(+), 12 deletions(-) diff --git a/openpype/tools/publisher/widgets/validations_widget.py b/openpype/tools/publisher/widgets/validations_widget.py index b70cd81878..b21cb3bb1b 100644 --- a/openpype/tools/publisher/widgets/validations_widget.py +++ b/openpype/tools/publisher/widgets/validations_widget.py @@ -414,15 +414,76 @@ class ValidationsWidget(QtWidgets.QFrame): │ │ Error detail │ │ │ │ │ │ │ │ │ │ - ├──────┴────────────────┴───────┤ - │ Publish buttons │ - └───────────────────────────────┘ + └──────┴────────────────┴───────┘ """ def __init__(self, controller, parent): super(ValidationsWidget, self).__init__(parent) - errors_scroll = VerticallScrollArea(self) + # Before publishing + before_publish_widget = QtWidgets.QWidget(self) + before_publish_label = QtWidgets.QLabel( + "Nothing to report until you run publish", + before_publish_widget + ) + before_publish_label.setAlignment(QtCore.Qt.AlignCenter) + before_publish_layout = QtWidgets.QHBoxLayout(before_publish_widget) + before_publish_layout.setContentsMargins(0, 0, 0, 0) + before_publish_layout.addWidget( + before_publish_label, 1, QtCore.Qt.AlignCenter + ) + + # After success publishing + publish_started_widget = QtWidgets.QWidget(self) + publish_started_label = QtWidgets.QLabel( + "Publishing run smoothly", + publish_started_widget + ) + publish_started_label.setAlignment(QtCore.Qt.AlignCenter) + publish_started_layout = QtWidgets.QHBoxLayout( + publish_started_widget + ) + publish_started_layout.setContentsMargins(0, 0, 0, 0) + publish_started_layout.addWidget( + publish_started_label, 1, QtCore.Qt.AlignCenter + ) + + # After success publishing + publish_stop_ok_widget = QtWidgets.QWidget(self) + publish_stop_ok_label = QtWidgets.QLabel( + "Publishing finished successfully", + publish_stop_ok_widget + ) + publish_stop_ok_label.setAlignment(QtCore.Qt.AlignCenter) + publish_stop_ok_layout = QtWidgets.QHBoxLayout( + publish_stop_ok_widget + ) + publish_stop_ok_layout.setContentsMargins(0, 0, 0, 0) + publish_stop_ok_layout.addWidget( + publish_stop_ok_label, 1, QtCore.Qt.AlignCenter + ) + + # After failed publishing (not with validation error) + publish_stop_fail_widget = QtWidgets.QWidget(self) + publish_stop_fail_label = QtWidgets.QLabel( + "This is not your fault", + publish_stop_fail_widget + ) + publish_stop_fail_label.setAlignment(QtCore.Qt.AlignCenter) + publish_stop_fail_layout = QtWidgets.QHBoxLayout( + publish_stop_fail_widget + ) + publish_stop_fail_layout.setContentsMargins(0, 0, 0, 0) + publish_stop_fail_layout.addWidget( + publish_stop_fail_label, 1, QtCore.Qt.AlignCenter + ) + + # Validation errors + validations_widget = QtWidgets.QWidget(self) + + content_widget = QtWidgets.QWidget(validations_widget) + + errors_scroll = VerticallScrollArea(content_widget) errors_scroll.setWidgetResizable(True) errors_widget = QtWidgets.QWidget(errors_scroll) @@ -432,35 +493,58 @@ class ValidationsWidget(QtWidgets.QFrame): errors_scroll.setWidget(errors_widget) - error_details_frame = QtWidgets.QFrame(self) + error_details_frame = QtWidgets.QFrame(content_widget) error_details_input = QtWidgets.QTextEdit(error_details_frame) error_details_input.setObjectName("InfoText") error_details_input.setTextInteractionFlags( QtCore.Qt.TextBrowserInteraction ) - actions_widget = ValidateActionsWidget(controller, self) + actions_widget = ValidateActionsWidget(controller, content_widget) actions_widget.setMinimumWidth(140) error_details_layout = QtWidgets.QHBoxLayout(error_details_frame) error_details_layout.addWidget(error_details_input, 1) error_details_layout.addWidget(actions_widget, 0) - content_layout = QtWidgets.QHBoxLayout() + content_layout = QtWidgets.QHBoxLayout(content_widget) content_layout.setSpacing(0) content_layout.setContentsMargins(0, 0, 0, 0) content_layout.addWidget(errors_scroll, 0) content_layout.addWidget(error_details_frame, 1) - top_label = QtWidgets.QLabel("Publish validation report", self) + top_label = QtWidgets.QLabel( + "Publish validation report", content_widget + ) top_label.setObjectName("PublishInfoMainLabel") top_label.setAlignment(QtCore.Qt.AlignCenter) - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(top_label) - layout.addLayout(content_layout) + validation_layout = QtWidgets.QVBoxLayout(validations_widget) + validation_layout.setContentsMargins(0, 0, 0, 0) + validation_layout.addWidget(top_label, 0) + validation_layout.addWidget(content_widget, 1) + + main_layout = QtWidgets.QStackedLayout(self) + main_layout.addWidget(before_publish_widget) + main_layout.addWidget(publish_started_widget) + main_layout.addWidget(publish_stop_ok_widget) + main_layout.addWidget(publish_stop_fail_widget) + main_layout.addWidget(validations_widget) + + main_layout.setCurrentWidget(before_publish_widget) + + controller.add_publish_started_callback(self._on_publish_start) + controller.add_publish_reset_callback(self._on_publish_reset) + controller.add_publish_stopped_callback(self._on_publish_stop) + + self._main_layout = main_layout + + self._before_publish_widget = before_publish_widget + self._publish_started_widget = publish_started_widget + self._publish_stop_ok_widget = publish_stop_ok_widget + self._publish_stop_fail_widget = publish_stop_fail_widget + self._validations_widget = validations_widget self._top_label = top_label self._errors_widget = errors_widget @@ -473,6 +557,8 @@ class ValidationsWidget(QtWidgets.QFrame): self._error_info = {} self._previous_select = None + self._controller = controller + def clear(self): """Delete all dynamic widgets and hide all wrappers.""" self._title_widgets = {} @@ -536,6 +622,32 @@ class ValidationsWidget(QtWidgets.QFrame): self.updateGeometry() + def _set_current_widget(self, widget): + self._main_layout.setCurrentWidget(widget) + + def _on_publish_start(self): + self._set_current_widget(self._publish_started_widget) + + def _on_publish_reset(self): + self._set_current_widget(self._before_publish_widget) + + def _on_publish_stop(self): + if self._controller.publish_has_crashed: + self._set_current_widget(self._publish_stop_fail_widget) + return + + if self._controller.publish_has_validation_errors: + validation_errors = self._controller.get_validation_errors() + self._set_current_widget(self._validations_widget) + self.set_errors(validation_errors) + return + + if self._contoller.publish_has_finished: + self._set_current_widget(self._publish_stop_ok_widget) + return + + self._set_current_widget(self._publish_started_widget) + def _on_select(self, index): if self._previous_select: if self._previous_select.index == index: From e725246c31acb948c0075aff4c33a4cac7ebe921 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 29 Sep 2022 13:49:39 +0200 Subject: [PATCH 038/121] publish frame is showed in a different way --- openpype/style/style.css | 10 - .../tools/publisher/widgets/publish_widget.py | 188 ++++++------------ openpype/tools/publisher/widgets/widgets.py | 43 ++-- openpype/tools/publisher/window.py | 141 ++++++++++--- 4 files changed, 194 insertions(+), 188 deletions(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index 1d112fa575..9dbc6b2adc 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -971,16 +971,6 @@ VariantInputsWidget QToolButton { color: {color:publisher:error}; } -#PublishFrame { - background: rgba(0, 0, 0, 127); -} -#PublishFrame[state="1"] { - background: rgb(22, 25, 29); -} -#PublishFrame[state="2"] { - background: {color:bg}; -} - #PublishInfoFrame { background: {color:bg}; border: 2px solid black; diff --git a/openpype/tools/publisher/widgets/publish_widget.py b/openpype/tools/publisher/widgets/publish_widget.py index ea23f9c42c..8a8c25e1b6 100644 --- a/openpype/tools/publisher/widgets/publish_widget.py +++ b/openpype/tools/publisher/widgets/publish_widget.py @@ -2,7 +2,7 @@ import os import json import time -from Qt import QtWidgets, QtCore, QtGui +from Qt import QtWidgets, QtCore from openpype.pipeline import KnownPublishError @@ -11,9 +11,7 @@ from .widgets import ( ResetBtn, ValidateBtn, PublishBtn, - CopyPublishReportBtn, - SavePublishReportBtn, - ShowPublishReportBtn + PublishReportBtn, ) @@ -66,7 +64,7 @@ class ActionsButton(QtWidgets.QToolButton): self._set_action(action) -class PublishFrame(QtWidgets.QFrame): +class PublishFrame(QtWidgets.QWidget): """Frame showed during publishing. Shows all information related to publishing. Contains validation error @@ -77,65 +75,49 @@ class PublishFrame(QtWidgets.QFrame): only when publishing process is stopped and must be manually triggered to change into that layer. - +------------------------------------------------------------------------+ - | | - | | - | | - | < Validation error widget > | - | | - | | - | | - | | +------------------------------------------------------------------------+ | < Main label > | | < Label top > | | (#### 10% ) | | | - | Report: