From 14b5ac4c251f5cb3b9a263f7f0b03bc0567ca45f Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 18 Aug 2022 14:01:54 +0300 Subject: [PATCH 01/46] Add `extract_obj.py` and `obj.py` --- openpype/hosts/maya/api/obj.py | 0 .../hosts/maya/plugins/publish/extract_obj.py | 62 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 openpype/hosts/maya/api/obj.py create mode 100644 openpype/hosts/maya/plugins/publish/extract_obj.py diff --git a/openpype/hosts/maya/api/obj.py b/openpype/hosts/maya/api/obj.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/hosts/maya/plugins/publish/extract_obj.py b/openpype/hosts/maya/plugins/publish/extract_obj.py new file mode 100644 index 0000000000..7c915a80d8 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/extract_obj.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +import os + +from maya import cmds +import maya.mel as mel +import pyblish.api +import openpype.api +from openpype.hosts.maya.api.lib import maintained_selection + +from openpype.hosts.maya.api import obj + + +class ExtractObj(openpype.api.Extractor): + """Extract OBJ from Maya. + + This extracts reproducible OBJ exports ignoring any of the settings + set on the local machine in the OBJ export options window. + + """ + order = pyblish.api.ExtractorOrder + label = "Extract OBJ" + families = ["obj"] + + def process(self, instance): + obj_exporter = obj.OBJExtractor(log=self.log) + + # Define output path + + staging_dir = self.staging_dir(instance) + filename = "{0}.fbx".format(instance.name) + path = os.path.join(staging_dir, filename) + + # The export requires forward slashes because we need to + # format it into a string in a mel expression + path = path.replace('\\', '/') + + self.log.info("Extracting OBJ to: {0}".format(path)) + + members = instance.data["setMembners"] + self.log.info("Members: {0}".format(members)) + self.log.info("Instance: {0}".format(instance[:])) + + obj_exporter.set_options_from_instance(instance) + + # Export + with maintained_selection(): + obj_exporter.export(members, path) + cmds.select(members, r=1, noExpand=True) + mel.eval('file -force -options "{0};{1};{2};{3};{4}" -typ "OBJexport" -pr -es "{5}";'.format(grp_flag, ptgrp_flag, mats_flag, smooth_flag, normals_flag, path)) # noqa + + if "representation" not in instance.data: + instance.data["representation"] = [] + + representation = { + 'name':'obj', + 'ext':'obx', + 'files': filename, + "stagingDir": staging_dir, + } + instance.data["representations"].append(representation) + + self.log.info("Extract OBJ successful to: {0}".format(path)) From 321512bb0115ee38d085da753a2bb6b7e4e2a2ce Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 19 Oct 2022 21:19:31 +0200 Subject: [PATCH 02/46] nuke: adding viewer and display exctractor --- openpype/hosts/nuke/api/lib.py | 44 ++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 1aea04d889..2691b7447a 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2930,3 +2930,47 @@ def get_nodes_by_names(names): nuke.toNode(name) for name in names ] + + +def get_viewer_config_from_string(input_string): + """Convert string to display and viewer string + + Args: + input_string (str): string with viewer + + Raises: + IndexError: if more then one slash in input string + IndexError: if missing closing bracket + + Returns: + tuple[str]: display, viewer + """ + display = None + viewer = input_string + # check if () or / or \ in name + if "/" in viewer: + split = viewer.split("/") + + # rise if more then one column + if len(split) > 2: + raise IndexError(( + "Viewer Input string is not correct. " + "more then two `/` slashes! {}" + ).format(input_string)) + + viewer = split[1] + display = split[0] + elif "(" in viewer: + pattern = r"([\w\d\s]+).*[(](.*)[)]" + result = re.findall(pattern, viewer) + try: + result = result.pop() + display = str(result[1]).rstrip() + viewer = str(result[0]).rstrip() + except IndexError: + raise IndexError(( + "Viewer Input string is not correct. " + "Missing bracket! {}" + ).format(input_string)) + + return (display, viewer) From babd9898d2ac5da414d5f758533e4fcd3096024c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 19 Oct 2022 21:21:05 +0200 Subject: [PATCH 03/46] nuke: implementing display and viewer assignment --- openpype/hosts/nuke/api/plugin.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 91bb90ff99..9330309f64 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -19,7 +19,8 @@ from .lib import ( add_publish_knob, get_nuke_imageio_settings, set_node_knobs_from_settings, - get_view_process_node + get_view_process_node, + get_viewer_config_from_string ) @@ -312,7 +313,8 @@ class ExporterReviewLut(ExporterReview): dag_node.setInput(0, self.previous_node) self._temp_nodes.append(dag_node) self.previous_node = dag_node - self.log.debug("OCIODisplay... `{}`".format(self._temp_nodes)) + self.log.debug( + "OCIODisplay... `{}`".format(self._temp_nodes)) # GenerateLUT gen_lut_node = nuke.createNode("GenerateLUT") @@ -491,7 +493,15 @@ class ExporterReviewMov(ExporterReview): if not self.viewer_lut_raw: # OCIODisplay dag_node = nuke.createNode("OCIODisplay") - dag_node["view"].setValue(str(baking_view_profile)) + + display, viewer = get_viewer_config_from_string( + str(baking_view_profile) + ) + if display: + dag_node["display"].setValue(display) + + # assign viewer + dag_node["view"].setValue(viewer) # connect dag_node.setInput(0, self.previous_node) From 3cdad1e9677c5320951f090c9b7674863c11ee4c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 19 Oct 2022 21:37:13 +0200 Subject: [PATCH 04/46] Nuke: add custom tags inputs to settings also implement custom tags to exctractor --- openpype/hosts/nuke/api/plugin.py | 24 ++++++++++++++++--- .../defaults/project_settings/nuke.json | 2 +- .../schemas/schema_nuke_publish.json | 4 ++-- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 9330309f64..5981a8b386 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -191,7 +191,20 @@ class ExporterReview(object): if "#" in self.fhead: self.fhead = self.fhead.replace("#", "")[:-1] - def get_representation_data(self, tags=None, range=False): + def get_representation_data( + self, tags=None, range=False, + custom_tags=None + ): + """ Add representation data to self.data + + Args: + tags (list[str], optional): list of defined tags. + Defaults to None. + range (bool, optional): flag for adding ranges. + Defaults to False. + custom_tags (list[str], optional): user inputed custom tags. + Defaults to None. + """ add_tags = tags or [] repre = { "name": self.name, @@ -201,6 +214,9 @@ class ExporterReview(object): "tags": [self.name.replace("_", "-")] + add_tags } + if custom_tags: + repre["custom_tags"] = custom_tags + if range: repre.update({ "frameStart": self.first_frame, @@ -417,6 +433,7 @@ class ExporterReviewMov(ExporterReview): return path def generate_mov(self, farm=False, **kwargs): + add_tags = [] self.publish_on_farm = farm read_raw = kwargs["read_raw"] reformat_node_add = kwargs["reformat_node_add"] @@ -435,10 +452,10 @@ class ExporterReviewMov(ExporterReview): self.log.debug(">> baking_view_profile `{}`".format( baking_view_profile)) - add_tags = kwargs.get("add_tags", []) + add_custom_tags = kwargs.get("add_custom_tags", []) self.log.info( - "__ add_tags: `{0}`".format(add_tags)) + "__ add_custom_tags: `{0}`".format(add_custom_tags)) subset = self.instance.data["subset"] self._temp_nodes[subset] = [] @@ -552,6 +569,7 @@ class ExporterReviewMov(ExporterReview): # ---------- generate representation data self.get_representation_data( tags=["review", "delete"] + add_tags, + custom_tags=add_custom_tags, range=True ) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index e5cbacbda7..57a09086ca 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -434,7 +434,7 @@ } ], "extension": "mov", - "add_tags": [] + "add_custom_tags": [] } } }, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json index e5827a92c4..c91d3c0e3d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json @@ -296,8 +296,8 @@ "label": "Write node file type" }, { - "key": "add_tags", - "label": "Add additional tags to representations", + "key": "add_custom_tags", + "label": "Add custom tags", "type": "list", "object_type": "text" } From 463f83a201519592f20fa54a45a329bdcd58b146 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 19 Oct 2022 22:17:40 +0200 Subject: [PATCH 05/46] global: adding filtering custom tags to settings --- openpype/settings/defaults/project_settings/global.json | 3 ++- .../projects_schema/schemas/schema_global_publish.json | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 1b7dc7a41a..b128564bc2 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -78,7 +78,8 @@ "review", "ftrack" ], - "subsets": [] + "subsets": [], + "custom_tags": [] }, "overscan_crop": "", "overscan_color": [ diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 773dea1229..51fc8dedf3 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -295,6 +295,15 @@ "label": "Subsets", "type": "list", "object_type": "text" + }, + { + "type": "separator" + }, + { + "key": "custom_tags", + "label": "Custom Tags", + "type": "list", + "object_type": "text" } ] }, From 9f05131c17849e076b41e96cd0e0ccba1abfa8f0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 19 Oct 2022 22:19:12 +0200 Subject: [PATCH 06/46] global: implementing filtering by custom tags --- openpype/plugins/publish/extract_review.py | 28 +++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 27117510b2..cf8d6429fa 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -1619,6 +1619,24 @@ class ExtractReview(pyblish.api.InstancePlugin): return self.profile_exclusion(matching_profiles) + def custom_tags_filter_validation( + self, repr_custom_tags, output_custom_tags_filter + ): + """Determines if entered custom tags intersect with custom tags filters. + + All cutsom tags values are lowered to avoid unexpected results. + """ + repr_custom_tags = repr_custom_tags or [] + valid = False + for tag in output_custom_tags_filter: + if tag in repr_custom_tags: + valid = True + break + + if valid: + return True + return False + def families_filter_validation(self, families, output_families_filter): """Determines if entered families intersect with families filters. @@ -1656,7 +1674,9 @@ class ExtractReview(pyblish.api.InstancePlugin): return True return False - def filter_output_defs(self, profile, subset_name, families): + def filter_output_defs( + self, profile, subset_name, families, custom_tags=None + ): """Return outputs matching input instance families. Output definitions without families filter are marked as valid. @@ -1689,6 +1709,12 @@ class ExtractReview(pyblish.api.InstancePlugin): if not self.families_filter_validation(families, families_filters): continue + custom_tags_filters = output_filters.get("custom_tags") + if custom_tags and not self.custom_tags_filter_validation( + custom_tags, custom_tags_filters + ): + continue + # Subsets name filters subset_filters = [ subset_filter From 29a50cc280c13a3be9d6c9971d91e494ac8711d7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 20 Oct 2022 21:23:46 +0200 Subject: [PATCH 07/46] global: exctract review custom tag filtering fix --- openpype/plugins/publish/extract_review.py | 95 +++++++++++----------- 1 file changed, 46 insertions(+), 49 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index cf8d6429fa..431ddcc3b4 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -128,6 +128,7 @@ class ExtractReview(pyblish.api.InstancePlugin): for repre in instance.data["representations"]: repre_name = str(repre.get("name")) tags = repre.get("tags") or [] + custom_tags = repre.get("custom_tags") if "review" not in tags: self.log.debug(( "Repre: {} - Didn't found \"review\" in tags. Skipping" @@ -158,15 +159,18 @@ class ExtractReview(pyblish.api.InstancePlugin): ) continue - # Filter output definition by representation tags (optional) - outputs = self.filter_outputs_by_tags(profile_outputs, tags) + # Filter output definition by representation's + # custom tags (optional) + outputs = self.filter_outputs_by_custom_tags( + profile_outputs, custom_tags) if not outputs: self.log.info(( "Skipped representation. All output definitions from" " selected profile does not match to representation's" - " tags. \"{}\"" + " custom tags. \"{}\"" ).format(str(tags))) continue + outputs_per_representations.append((repre, outputs)) return outputs_per_representations @@ -1619,24 +1623,6 @@ class ExtractReview(pyblish.api.InstancePlugin): return self.profile_exclusion(matching_profiles) - def custom_tags_filter_validation( - self, repr_custom_tags, output_custom_tags_filter - ): - """Determines if entered custom tags intersect with custom tags filters. - - All cutsom tags values are lowered to avoid unexpected results. - """ - repr_custom_tags = repr_custom_tags or [] - valid = False - for tag in output_custom_tags_filter: - if tag in repr_custom_tags: - valid = True - break - - if valid: - return True - return False - def families_filter_validation(self, families, output_families_filter): """Determines if entered families intersect with families filters. @@ -1675,7 +1661,7 @@ class ExtractReview(pyblish.api.InstancePlugin): return False def filter_output_defs( - self, profile, subset_name, families, custom_tags=None + self, profile, subset_name, families ): """Return outputs matching input instance families. @@ -1684,6 +1670,7 @@ class ExtractReview(pyblish.api.InstancePlugin): Args: profile (dict): Profile from presets matching current context. families (list): All families of current instance. + subset_name (str): name of subset Returns: list: Containg all output definitions matching entered families. @@ -1709,12 +1696,6 @@ class ExtractReview(pyblish.api.InstancePlugin): if not self.families_filter_validation(families, families_filters): continue - custom_tags_filters = output_filters.get("custom_tags") - if custom_tags and not self.custom_tags_filter_validation( - custom_tags, custom_tags_filters - ): - continue - # Subsets name filters subset_filters = [ subset_filter @@ -1737,39 +1718,55 @@ class ExtractReview(pyblish.api.InstancePlugin): return filtered_outputs - def filter_outputs_by_tags(self, outputs, tags): - """Filter output definitions by entered representation tags. + def filter_outputs_by_custom_tags(self, outputs, custom_tags): + """Filter output definitions by entered representation custom_tags. - Output definitions without tags filter are marked as valid. + Output definitions without custom_tags filter are marked as invalid, + only in case representation is having any custom_tags defined. Args: outputs (list): Contain list of output definitions from presets. - tags (list): Tags of processed representation. + custom_tags (list): Custom Tags of processed representation. Returns: list: Containg all output definitions matching entered tags. """ filtered_outputs = [] - repre_tags_low = [tag.lower() for tag in tags] + repre_c_tags_low = [tag.lower() for tag in (custom_tags or [])] for output_def in outputs: - valid = True - output_filters = output_def.get("filter") - if output_filters: - # Check tag filters - tag_filters = output_filters.get("tags") - if tag_filters: - tag_filters_low = [tag.lower() for tag in tag_filters] - valid = False - for tag in repre_tags_low: - if tag in tag_filters_low: - valid = True - break + valid = False + tag_filters = output_def.get("filter", {}).get("custom_tags") - if not valid: - continue + if ( + # if any of tag filter is empty, skip + custom_tags and not tag_filters + or not custom_tags and tag_filters + ): + continue + elif not custom_tags and not tag_filters: + valid = True - if valid: - filtered_outputs.append(output_def) + # lower all filter tags + tag_filters_low = [tag.lower() for tag in tag_filters] + + self.log.debug("__ tag_filters: {}".format(tag_filters)) + self.log.debug("__ repre_c_tags_low: {}".format( + repre_c_tags_low)) + + # check if any repre tag is not in filter tags + for tag in repre_c_tags_low: + if tag in tag_filters_low: + valid = True + break + + if not valid: + continue + + filtered_outputs.append(output_def) + + self.log.debug("__ filtered_outputs: {}".format( + [_o["filename_suffix"] for _o in filtered_outputs] + )) return filtered_outputs From 260573506b56d83d73ea785b335aa9134d652d96 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 14:56:09 +0200 Subject: [PATCH 08/46] Created simple item representing conversion requirement --- openpype/pipeline/create/context.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 3e09ff287d..918bc66cb0 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -852,6 +852,29 @@ class CreatedInstance: self[key] = new_value +class LegacyInstancesItem(object): + """Item representing convertor for legacy instances. + + Args: + identifier (str): Identifier of convertor. + label (str): Label which will be shown in UI. + """ + + def __init__(self, identifier, label): + self.identifier = identifier + self.label = label + + def to_data(self): + return { + "identifier": self.identifier, + "label": self.label + } + + @classmethod + def from_data(cls, data): + return cls(data["identifier"], data["label"]) + + class CreateContext: """Context of instance creation. From 8e99d9128a622956299e9dfdd5e22f22460e63d5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 14:56:56 +0200 Subject: [PATCH 09/46] implemented basic of convertor --- openpype/pipeline/create/creator_plugins.py | 90 +++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 97ee94c449..62562e4428 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -33,6 +33,96 @@ class CreatorError(Exception): super(CreatorError, self).__init__(message) +@six.add_metaclass(ABCMeta) +class LegacyInstanceConvertor(object): + """Helper for conversion of instances created using legacy creators. + + Conversion from legacy creators would mean to loose legacy instances, + convert them automatically or write a script which must user run. All of + these solutions are workign but will happen without asking or user must + know about them. This plugin can be used to show legacy instances in + Publisher and give user ability to run conversion script. + + Convertor logic should be very simple. Method 'find_instances' is to + look for legacy instances in scene a possibly call + pre-implemented 'add_legacy_item'. + + User will have ability to trigger conversion which is executed by calling + 'convert' which should call 'remove_legacy_item' when is done. + + It does make sense to add only one or none legacy item to create context + for convertor as it's not possible to choose which instace are converted + and which are not. + + Convertor can use 'collection_shared_data' property like creators. Also + can store any information to it's object for conversion purposes. + + Args: + create_context + """ + + def __init__(self, create_context): + self._create_context = create_context + + @abstractproperty + def identifier(self): + """Converted identifier. + + Returns: + str: Converted identifier unique for all converters in host. + """ + + pass + + @abstractmethod + def find_instances(self): + """Look for legacy instances in the scene. + + Should call 'add_legacy_item' if there is at least one item. + """ + + pass + + @abstractmethod + def convert(self): + """Conversion code.""" + + pass + + @property + def create_context(self): + """Quick access to create context.""" + + return self._create_context + + @property + def collection_shared_data(self): + """Access to shared data that can be used during 'find_instances'. + + Retruns: + Dict[str, Any]: Shared data. + + Raises: + UnavailableSharedData: When called out of collection phase. + """ + + return self._create_context.collection_shared_data + + def add_legacy_item(self, label): + """Add item to CreateContext. + + Args: + label (str): Label of item which will show in UI. + """ + + self._create_context.add_legacy_item(self.identifier, label) + + def remove_legacy_item(self): + """Remove legacy item from create context when conversion finished.""" + + self._create_context.remove_legacy_item(self.identifier) + + @six.add_metaclass(ABCMeta) class BaseCreator: """Plugin that create and modify instance data before publishing process. From 971e4a23bd67fb4ec214bed4b39f32e9f0943715 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 15:19:40 +0200 Subject: [PATCH 10/46] split reset of plugins to more methods --- openpype/pipeline/create/context.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 918bc66cb0..565fdbdf89 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1074,6 +1074,11 @@ class CreateContext: Reloads creators from preregistered paths and can load publish plugins if it's enabled on context. """ + + self._reset_publish_plugins(discover_publish_plugins) + self._reset_creator_plugins() + + def _reset_publish_plugins(self, discover_publish_plugins): import pyblish.logic from openpype.pipeline import OpenPypePyblishPluginMixin @@ -1115,6 +1120,7 @@ class CreateContext: self.publish_plugins = plugins_by_targets self.plugins_with_defs = plugins_with_defs + def _reset_creator_plugins(self): # Prepare settings system_settings = get_system_settings() project_settings = get_project_settings(self.project_name) From cff9990c6fc59ee5d142ce14db206951a5620fdf Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 15:20:07 +0200 Subject: [PATCH 11/46] added logic to discover convertors and find legacy items --- openpype/pipeline/create/context.py | 51 +++++++++++++++++++++ openpype/pipeline/create/creator_plugins.py | 12 +++++ 2 files changed, 63 insertions(+) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 565fdbdf89..783b599aef 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -22,6 +22,7 @@ from .creator_plugins import ( Creator, AutoCreator, discover_creator_plugins, + discover_legacy_convertor_plugins, ) UpdateData = collections.namedtuple("UpdateData", ["instance", "changes"]) @@ -940,6 +941,9 @@ class CreateContext: # Manual creators self.manual_creators = {} + self.legacy_convertors = {} + self.legacy_items_by_id = {} + self.publish_discover_result = None self.publish_plugins_mismatch_targets = [] self.publish_plugins = [] @@ -1020,6 +1024,7 @@ class CreateContext: with self.bulk_instances_collection(): self.reset_instances() + self.find_legacy_items() self.execute_autocreators() self.reset_finalization() @@ -1077,6 +1082,7 @@ class CreateContext: self._reset_publish_plugins(discover_publish_plugins) self._reset_creator_plugins() + self._reset_legacy_convertor_plugins() def _reset_publish_plugins(self, discover_publish_plugins): import pyblish.logic @@ -1172,6 +1178,29 @@ class CreateContext: self.creators = creators + def _reset_legacy_convertor_plugins(self): + legacy_convertors = {} + for convertor_class in discover_legacy_convertor_plugins(): + if inspect.isabstract(convertor_class): + self.log.info( + "Skipping abstract Creator {}".format(str(convertor_class)) + ) + continue + + convertor_identifier = convertor_class.identifier + if convertor_identifier in legacy_convertors: + self.log.warning(( + "Duplicated Converter identifier. " + "Using first and skipping following" + )) + continue + + legacy_convertors[convertor_identifier] = ( + convertor_identifier(self) + ) + + self.legacy_convertors = legacy_convertors + def reset_context_data(self): """Reload context data using host implementation. @@ -1243,6 +1272,14 @@ class CreateContext: def creator_removed_instance(self, instance): self._instances_by_id.pop(instance.id, None) + def add_legacy_item(self, convertor_identifier, label): + self.legacy_items_by_id[convertor_identifier] = ( + LegacyInstancesItem(convertor_identifier, label) + ) + + def remove_legacy_item(self, convertor_identifier): + self.legacy_items_by_id.pop(convertor_identifier, None) + @contextmanager def bulk_instances_collection(self): """Validate context of instances in bulk. @@ -1278,6 +1315,20 @@ class CreateContext: for creator in self.creators.values(): creator.collect_instances() + def find_legacy_items(self): + self.legacy_items_by_id = {} + + for convertor in self.legacy_convertors.values(): + try: + convertor.find_instances() + except: + self.log.warning( + "Failed to find instances of convertor \"{}\"".format( + convertor.identifier + ), + exc_info=True + ) + def execute_autocreators(self): """Execute discovered AutoCreator plugins. diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 62562e4428..ff9326693e 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -559,6 +559,10 @@ def discover_creator_plugins(): return discover(BaseCreator) +def discover_legacy_convertor_plugins(): + return discover(LegacyInstanceConvertor) + + def discover_legacy_creator_plugins(): from openpype.lib import Logger @@ -616,6 +620,9 @@ def register_creator_plugin(plugin): elif issubclass(plugin, LegacyCreator): register_plugin(LegacyCreator, plugin) + elif issubclass(plugin, LegacyInstanceConvertor): + register_plugin(LegacyInstanceConvertor, plugin) + def deregister_creator_plugin(plugin): if issubclass(plugin, BaseCreator): @@ -624,12 +631,17 @@ def deregister_creator_plugin(plugin): elif issubclass(plugin, LegacyCreator): deregister_plugin(LegacyCreator, plugin) + elif issubclass(plugin, LegacyInstanceConvertor): + deregister_plugin(LegacyInstanceConvertor, plugin) + def register_creator_plugin_path(path): register_plugin_path(BaseCreator, path) register_plugin_path(LegacyCreator, path) + register_plugin_path(LegacyInstanceConvertor, path) def deregister_creator_plugin_path(path): deregister_plugin_path(BaseCreator, path) deregister_plugin_path(LegacyCreator, path) + deregister_plugin_path(LegacyInstanceConvertor, path) From 24ebd76bd90ef5705434b6ff26c34f294ce96dc5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 17:55:56 +0200 Subject: [PATCH 12/46] fix convertor creation --- openpype/pipeline/create/context.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 783b599aef..5f39d7a0d0 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1195,9 +1195,7 @@ class CreateContext: )) continue - legacy_convertors[convertor_identifier] = ( - convertor_identifier(self) - ) + legacy_convertors[convertor_identifier] = convertor_class(self) self.legacy_convertors = legacy_convertors From 3bdaf89a791a88e0a8fed5f3938aad697b7d08d2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 17:56:03 +0200 Subject: [PATCH 13/46] added id to legacy item --- openpype/pipeline/create/context.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 5f39d7a0d0..e0c5e49e40 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -862,18 +862,26 @@ class LegacyInstancesItem(object): """ def __init__(self, identifier, label): + self._id = str(uuid4()) self.identifier = identifier self.label = label + @property + def id(self): + return self._id + def to_data(self): return { + "id": self.id, "identifier": self.identifier, "label": self.label } @classmethod def from_data(cls, data): - return cls(data["identifier"], data["label"]) + obj = cls(data["identifier"], data["label"]) + obj._id = data["id"] + return obj class CreateContext: From e484df219d6e9cf8031a6f1268575cc2060b75d1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 19:28:12 +0200 Subject: [PATCH 14/46] Define constant for context group --- openpype/tools/publisher/constants.py | 3 +++ .../tools/publisher/widgets/card_view_widgets.py | 15 ++++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/openpype/tools/publisher/constants.py b/openpype/tools/publisher/constants.py index dc44aade45..866792aa32 100644 --- a/openpype/tools/publisher/constants.py +++ b/openpype/tools/publisher/constants.py @@ -3,6 +3,9 @@ from Qt import QtCore # ID of context item in instance view CONTEXT_ID = "context" CONTEXT_LABEL = "Options" +# Not showed anywhere - used as identifier +CONTEXT_GROUP = "__ContextGroup__" + # Allowed symbols for subset name (and variant) # - characters, numbers, unsercore and dash diff --git a/openpype/tools/publisher/widgets/card_view_widgets.py b/openpype/tools/publisher/widgets/card_view_widgets.py index 5daf8059b0..55e2249496 100644 --- a/openpype/tools/publisher/widgets/card_view_widgets.py +++ b/openpype/tools/publisher/widgets/card_view_widgets.py @@ -37,7 +37,8 @@ from .widgets import ( ) from ..constants import ( CONTEXT_ID, - CONTEXT_LABEL + CONTEXT_LABEL, + CONTEXT_GROUP, ) @@ -284,7 +285,7 @@ class ContextCardWidget(CardWidget): super(ContextCardWidget, self).__init__(parent) self._id = CONTEXT_ID - self._group_identifier = "" + self._group_identifier = CONTEXT_GROUP icon_widget = PublishPixmapLabel(None, self) icon_widget.setObjectName("FamilyIconLabel") @@ -595,7 +596,7 @@ class InstanceCardView(AbstractInstanceView): instances_by_group[group_name] ) - ordered_group_names = [""] + ordered_group_names = [CONTEXT_GROUP] for idx in range(self._content_layout.count()): if idx > 0: item = self._content_layout.itemAt(idx) @@ -749,7 +750,7 @@ class InstanceCardView(AbstractInstanceView): # If start group is not set then use context item group name if start_group is None: - start_group = "" + start_group = CONTEXT_GROUP # If start instance id is not filled then use context id (similar to # group) @@ -777,7 +778,7 @@ class InstanceCardView(AbstractInstanceView): # Go through ordered groups (from top to bottom) and change selection for name in self._ordered_groups: # Prepare sorted instance widgets - if name == "": + if name == CONTEXT_GROUP: sorted_widgets = [self._context_widget] else: group_widget = self._widgets_by_group[name] @@ -916,13 +917,13 @@ class InstanceCardView(AbstractInstanceView): selected_groups = [] selected_instances = [] if context_selected: - selected_groups.append("") + selected_groups.append(CONTEXT_GROUP) selected_instances.append(CONTEXT_ID) self._context_widget.set_selected(context_selected) for group_name in self._ordered_groups: - if group_name == "": + if group_name == CONTEXT_GROUP: continue group_widget = self._widgets_by_group[group_name] From 3a6bc00a5344c1e0a2124e5a62bda8bfa4d96a2d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 19:30:42 +0200 Subject: [PATCH 15/46] controller has access to convertor items --- openpype/tools/publisher/control.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index d2d01e7921..9abc53675d 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1234,6 +1234,14 @@ class AbstractPublisherController(object): pass + @abstractproperty + def legacy_items(self): + pass + + @abstractmethod + def convert_legacy_items(self, convertor_identifiers): + pass + @abstractmethod def set_comment(self, comment): """Set comment on pyblish context. @@ -1598,6 +1606,10 @@ class PublisherController(BasePublisherController): """Current instances in create context.""" return self._create_context.instances_by_id + @property + def legacy_items(self): + return self._create_context.legacy_items_by_id + @property def _creators(self): """All creators loaded in create context.""" @@ -1716,6 +1728,7 @@ class PublisherController(BasePublisherController): self._create_context.reset_context_data() with self._create_context.bulk_instances_collection(): self._create_context.reset_instances() + self._create_context.find_legacy_items() self._create_context.execute_autocreators() self._resetting_instances = False @@ -1841,6 +1854,12 @@ class PublisherController(BasePublisherController): variant, task_name, asset_doc, project_name, instance=instance ) + def convert_legacy_items(self, convertor_identifiers): + for convertor_identifier in convertor_identifiers: + self._create_context.run_convertor(convertor_identifier) + self._on_create_instance_change() + self.emit_card_message("Conversion finished") + def create( self, creator_identifier, subset_name, instance_data, options ): From b8e5e5e75f7ce5c85c702c757a65b2f6d9ed5e56 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 19:31:31 +0200 Subject: [PATCH 16/46] create context has function to run convertor --- openpype/pipeline/create/context.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index e0c5e49e40..250193f511 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1500,3 +1500,8 @@ class CreateContext: "Accessed Collection shared data out of collection phase" ) return self._collection_shared_data + + def run_convertor(self, convertor_identifier): + convertor = self.legacy_convertors.get(convertor_identifier) + if convertor is not None: + convertor.convert() From e19268c4a1606cff38ab018556bc63a261624578 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 19:31:57 +0200 Subject: [PATCH 17/46] implemented basic implementation of converter --- openpype/style/data.json | 5 +- openpype/style/style.css | 12 + openpype/tools/publisher/constants.py | 3 + .../publisher/widgets/card_view_widgets.py | 292 ++++++++++++---- .../publisher/widgets/list_view_widgets.py | 312 +++++++++++++----- .../publisher/widgets/overview_widget.py | 23 +- openpype/tools/publisher/widgets/widgets.py | 60 +++- 7 files changed, 538 insertions(+), 169 deletions(-) diff --git a/openpype/style/data.json b/openpype/style/data.json index fef69071ed..44c0d51999 100644 --- a/openpype/style/data.json +++ b/openpype/style/data.json @@ -100,7 +100,10 @@ "bg-expander": "#2C313A", "bg-expander-hover": "#2d6c9f", "bg-expander-selected-hover": "#3784c5" - } + }, + "bg-legacy": "rgb(17, 17, 17)", + "bg-legacy-hover": "rgb(41, 41, 41)", + "bg-legacy-selected": "rgba(42, 123, 174, .4)" }, "settings": { "invalid-light": "#C93636", diff --git a/openpype/style/style.css b/openpype/style/style.css index a6818a5792..983f2c886f 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -965,6 +965,18 @@ VariantInputsWidget QToolButton { background: {color:bg-view-selection}; } +#CardViewLegacyItemWidget { + background: {color:publisher:bg-legacy}; + border-radius: 0.2em; + +} +#CardViewLegacyItemWidget:hover { + background: {color:publisher:bg-legacy-hover}; +} +#CardViewLegacyItemWidget[state="selected"] { + background: {color:publisher:bg-legacy-selected}; +} + #ListViewSubsetName[state="invalid"] { color: {color:publisher:error}; } diff --git a/openpype/tools/publisher/constants.py b/openpype/tools/publisher/constants.py index 866792aa32..3c192bf8a3 100644 --- a/openpype/tools/publisher/constants.py +++ b/openpype/tools/publisher/constants.py @@ -6,6 +6,7 @@ CONTEXT_LABEL = "Options" # Not showed anywhere - used as identifier CONTEXT_GROUP = "__ContextGroup__" +LEGACY_ITEM_GROUP = "Legacy instances" # Allowed symbols for subset name (and variant) # - characters, numbers, unsercore and dash @@ -20,6 +21,8 @@ SORT_VALUE_ROLE = QtCore.Qt.UserRole + 2 IS_GROUP_ROLE = QtCore.Qt.UserRole + 3 CREATOR_IDENTIFIER_ROLE = QtCore.Qt.UserRole + 4 FAMILY_ROLE = QtCore.Qt.UserRole + 5 +GROUP_ROLE = QtCore.Qt.UserRole + 6 +LEGACY_CONVERTER_IDENTIFIER = QtCore.Qt.UserRole + 7 __all__ = ( diff --git a/openpype/tools/publisher/widgets/card_view_widgets.py b/openpype/tools/publisher/widgets/card_view_widgets.py index 55e2249496..58a7bbc509 100644 --- a/openpype/tools/publisher/widgets/card_view_widgets.py +++ b/openpype/tools/publisher/widgets/card_view_widgets.py @@ -39,6 +39,7 @@ from ..constants import ( CONTEXT_ID, CONTEXT_LABEL, CONTEXT_GROUP, + LEGACY_ITEM_GROUP, ) @@ -58,15 +59,12 @@ class SelectionTypes: extend_to = SelectionType("extend_to") -class GroupWidget(QtWidgets.QWidget): - """Widget wrapping instances under group.""" - +class BaseGroupWidget(QtWidgets.QWidget): selected = QtCore.Signal(str, str, SelectionType) - active_changed = QtCore.Signal() removed_selected = QtCore.Signal() - def __init__(self, group_name, group_icons, parent): - super(GroupWidget, self).__init__(parent) + def __init__(self, group_name, parent): + super(BaseGroupWidget, self).__init__(parent) label_widget = QtWidgets.QLabel(group_name, self) @@ -87,10 +85,9 @@ class GroupWidget(QtWidgets.QWidget): layout.addLayout(label_layout, 0) self._group = group_name - self._group_icons = group_icons self._widgets_by_id = {} - self._ordered_instance_ids = [] + self._ordered_item_ids = [] self._label_widget = label_widget self._content_layout = layout @@ -105,7 +102,12 @@ class GroupWidget(QtWidgets.QWidget): return self._group - def get_selected_instance_ids(self): + def get_widget_by_item_id(self, item_id): + """Get instance widget by it's id.""" + + return self._widgets_by_id.get(item_id) + + def get_selected_item_ids(self): """Selected instance ids. Returns: @@ -140,13 +142,80 @@ class GroupWidget(QtWidgets.QWidget): return [ self._widgets_by_id[instance_id] - for instance_id in self._ordered_instance_ids + for instance_id in self._ordered_item_ids ] - def get_widget_by_instance_id(self, instance_id): - """Get instance widget by it's id.""" + def _remove_all_except(self, item_ids): + item_ids = set(item_ids) + # Remove instance widgets that are not in passed instances + for item_id in tuple(self._widgets_by_id.keys()): + if item_id in item_ids: + continue - return self._widgets_by_id.get(instance_id) + widget = self._widgets_by_id.pop(item_id) + if widget.is_selected: + self.removed_selected.emit() + + widget.setVisible(False) + self._content_layout.removeWidget(widget) + widget.deleteLater() + + def _update_ordered_item_ids(self): + ordered_item_ids = [] + for idx in range(self._content_layout.count()): + if idx > 0: + item = self._content_layout.itemAt(idx) + widget = item.widget() + if widget is not None: + ordered_item_ids.append(widget.id) + + self._ordered_item_ids = ordered_item_ids + + def _on_widget_selection(self, instance_id, group_id, selection_type): + self.selected.emit(instance_id, group_id, selection_type) + + +class LegacyItemsGroupWidget(BaseGroupWidget): + def update_items(self, items_by_id): + items_by_label = collections.defaultdict(list) + for item_id, item in items_by_id.items(): + items_by_label[item.label].append(item) + + # Remove instance widgets that are not in passed instances + self._remove_all_except(items_by_id.keys()) + + # Sort instances by subset name + sorted_labels = list(sorted(items_by_label.keys())) + + # Add new instances to widget + widget_idx = 1 + for label in sorted_labels: + for item in items_by_label[label]: + if item.id in self._widgets_by_id: + widget = self._widgets_by_id[item.id] + widget.update_item(item) + else: + widget = LegacyItemCardWidget(item, self) + widget.selected.connect(self._on_widget_selection) + self._widgets_by_id[item.id] = widget + self._content_layout.insertWidget(widget_idx, widget) + widget_idx += 1 + + self._update_ordered_item_ids() + + +class InstanceGroupWidget(BaseGroupWidget): + """Widget wrapping instances under group.""" + + active_changed = QtCore.Signal() + + def __init__(self, group_icons, *args, **kwargs): + super(InstanceGroupWidget, self).__init__(*args, **kwargs) + + self._group_icons = group_icons + + def update_icons(self, group_icons): + self._group_icons = group_icons def update_instance_values(self): """Trigger update on instance widgets.""" @@ -154,14 +223,6 @@ class GroupWidget(QtWidgets.QWidget): for widget in self._widgets_by_id.values(): widget.update_instance_values() - def confirm_remove_instance_id(self, instance_id): - """Delete widget by instance id.""" - - widget = self._widgets_by_id.pop(instance_id) - widget.setVisible(False) - self._content_layout.removeWidget(widget) - widget.deleteLater() - def update_instances(self, instances): """Update instances for the group. @@ -179,17 +240,7 @@ class GroupWidget(QtWidgets.QWidget): instances_by_subset_name[subset_name].append(instance) # Remove instance widgets that are not in passed instances - for instance_id in tuple(self._widgets_by_id.keys()): - if instance_id in instances_by_id: - continue - - widget = self._widgets_by_id.pop(instance_id) - if widget.is_selected: - self.removed_selected.emit() - - widget.setVisible(False) - self._content_layout.removeWidget(widget) - widget.deleteLater() + self._remove_all_except(instances_by_id.keys()) # Sort instances by subset name sorted_subset_names = list(sorted(instances_by_subset_name.keys())) @@ -212,18 +263,7 @@ class GroupWidget(QtWidgets.QWidget): self._content_layout.insertWidget(widget_idx, widget) widget_idx += 1 - ordered_instance_ids = [] - for idx in range(self._content_layout.count()): - if idx > 0: - item = self._content_layout.itemAt(idx) - widget = item.widget() - if widget is not None: - ordered_instance_ids.append(widget.id) - - self._ordered_instance_ids = ordered_instance_ids - - def _on_widget_selection(self, instance_id, group_id, selection_type): - self.selected.emit(instance_id, group_id, selection_type) + self._update_ordered_item_ids() class CardWidget(BaseClickableFrame): @@ -305,6 +345,41 @@ class ContextCardWidget(CardWidget): self._label_widget = label_widget +class LegacyItemCardWidget(CardWidget): + """Card for global context. + + Is not visually under group widget and is always at the top of card view. + """ + + def __init__(self, item, parent): + super(LegacyItemCardWidget, self).__init__(parent) + self.setObjectName("CardViewLegacyItemWidget") + + self._id = item.id + self.identifier = item.identifier + self._group_identifier = LEGACY_ITEM_GROUP + + icon_widget = PublishPixmapLabel(None, self) + icon_widget.setObjectName("FamilyIconLabel") + + label_widget = QtWidgets.QLabel(item.label, self) + + icon_layout = QtWidgets.QHBoxLayout() + icon_layout.setContentsMargins(5, 5, 5, 5) + icon_layout.addWidget(icon_widget) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 5, 10, 5) + layout.addLayout(icon_layout, 0) + layout.addWidget(label_widget, 1) + + self._icon_widget = icon_widget + self._label_widget = label_widget + + def update_instance_values(self): + pass + + class InstanceCardWidget(CardWidget): """Card widget representing instance.""" @@ -482,6 +557,7 @@ class InstanceCardView(AbstractInstanceView): self._content_widget = content_widget self._context_widget = None + self._legacy_items_group = None self._widgets_by_group = {} self._ordered_groups = [] @@ -514,6 +590,9 @@ class InstanceCardView(AbstractInstanceView): ): output.append(self._context_widget) + if self._legacy_items_group is not None: + output.extend(self._legacy_items_group.get_selected_widgets()) + for group_widget in self._widgets_by_group.values(): for widget in group_widget.get_selected_widgets(): output.append(widget) @@ -527,23 +606,19 @@ class InstanceCardView(AbstractInstanceView): ): output.append(CONTEXT_ID) + if self._legacy_items_group is not None: + output.extend(self._legacy_items_group.get_selected_item_ids()) + for group_widget in self._widgets_by_group.values(): - output.extend(group_widget.get_selected_instance_ids()) + output.extend(group_widget.get_selected_item_ids()) return output def refresh(self): """Refresh instances in view based on CreatedContext.""" - # Create context item if is not already existing - # - this must be as first thing to do as context item should be at the - # top - if self._context_widget is None: - widget = ContextCardWidget(self._content_widget) - widget.selected.connect(self._on_widget_selection) - self._context_widget = widget + self._make_sure_context_widget_exists() - self.selection_changed.emit() - self._content_layout.insertWidget(0, widget) + self._update_legacy_items_group() # Prepare instances by group and identifiers by group instances_by_group = collections.defaultdict(list) @@ -574,17 +649,21 @@ class InstanceCardView(AbstractInstanceView): # Keep track of widget indexes # - we start with 1 because Context item as at the top widget_idx = 1 + if self._legacy_items_group is not None: + widget_idx += 1 + for group_name in sorted_group_names: + group_icons = { + idenfier: self._controller.get_creator_icon(idenfier) + for idenfier in identifiers_by_group[group_name] + } if group_name in self._widgets_by_group: group_widget = self._widgets_by_group[group_name] - else: - group_icons = { - idenfier: self._controller.get_creator_icon(idenfier) - for idenfier in identifiers_by_group[group_name] - } + group_widget.update_icons(group_icons) - group_widget = GroupWidget( - group_name, group_icons, self._content_widget + else: + group_widget = InstanceGroupWidget( + group_icons, group_name, self._content_widget ) group_widget.active_changed.connect(self._on_active_changed) group_widget.selected.connect(self._on_widget_selection) @@ -596,6 +675,9 @@ class InstanceCardView(AbstractInstanceView): instances_by_group[group_name] ) + self._update_ordered_group_nameS() + + def _update_ordered_group_nameS(self): ordered_group_names = [CONTEXT_GROUP] for idx in range(self._content_layout.count()): if idx > 0: @@ -606,6 +688,43 @@ class InstanceCardView(AbstractInstanceView): self._ordered_groups = ordered_group_names + def _make_sure_context_widget_exists(self): + # Create context item if is not already existing + # - this must be as first thing to do as context item should be at the + # top + if self._context_widget is not None: + return + + widget = ContextCardWidget(self._content_widget) + widget.selected.connect(self._on_widget_selection) + + self._context_widget = widget + + self.selection_changed.emit() + self._content_layout.insertWidget(0, widget) + + def _update_legacy_items_group(self): + legacy_items = self._controller.legacy_items + if not legacy_items and self._legacy_items_group is None: + return + + if not legacy_items: + self._legacy_items_group.setVisible(False) + self._content_layout.removeWidget(self._legacy_items_group) + self._legacy_items_group.deleteLater() + self._legacy_items_group = None + return + + if self._legacy_items_group is None: + group_widget = LegacyItemsGroupWidget( + LEGACY_ITEM_GROUP, self._content_widget + ) + group_widget.selected.connect(self._on_widget_selection) + self._content_layout.insertWidget(1, group_widget) + self._legacy_items_group = group_widget + + self._legacy_items_group.update_items(legacy_items) + def refresh_instance_states(self): """Trigger update of instances on group widgets.""" for widget in self._widgets_by_group.values(): @@ -622,9 +741,13 @@ class InstanceCardView(AbstractInstanceView): """ if instance_id == CONTEXT_ID: new_widget = self._context_widget + else: - group_widget = self._widgets_by_group[group_name] - new_widget = group_widget.get_widget_by_instance_id(instance_id) + if group_name == LEGACY_ITEM_GROUP: + group_widget = self._legacy_items_group + else: + group_widget = self._widgets_by_group[group_name] + new_widget = group_widget.get_widget_by_item_id(instance_id) if selection_type is SelectionTypes.clear: self._select_item_clear(instance_id, group_name, new_widget) @@ -669,7 +792,10 @@ class InstanceCardView(AbstractInstanceView): if instance_id == CONTEXT_ID: remove_group = True else: - group_widget = self._widgets_by_group[group_name] + if group_name == LEGACY_ITEM_GROUP: + group_widget = self._legacy_items_group + else: + group_widget = self._widgets_by_group[group_name] if not group_widget.get_selected_widgets(): remove_group = True @@ -781,7 +907,10 @@ class InstanceCardView(AbstractInstanceView): if name == CONTEXT_GROUP: sorted_widgets = [self._context_widget] else: - group_widget = self._widgets_by_group[name] + if name == LEGACY_ITEM_GROUP: + group_widget = self._legacy_items_group + else: + group_widget = self._widgets_by_group[name] sorted_widgets = group_widget.get_ordered_widgets() # Change selection based on explicit selection if start group @@ -893,6 +1022,8 @@ class InstanceCardView(AbstractInstanceView): def get_selected_items(self): """Get selected instance ids and context.""" + + convertor_identifiers = [] instances = [] selected_widgets = self._get_selected_widgets() @@ -900,17 +1031,27 @@ class InstanceCardView(AbstractInstanceView): for widget in selected_widgets: if widget is self._context_widget: context_selected = True - else: + + elif isinstance(widget, InstanceCardWidget): instances.append(widget.id) - return instances, context_selected + elif isinstance(widget, LegacyItemCardWidget): + convertor_identifiers.append(widget.identifier) - def set_selected_items(self, instance_ids, context_selected): + return instances, context_selected, convertor_identifiers + + def set_selected_items( + self, instance_ids, context_selected, convertor_identifiers + ): s_instance_ids = set(instance_ids) - cur_ids, cur_context = self.get_selected_items() + s_convertor_identifiers = set(convertor_identifiers) + cur_ids, cur_context, cur_convertor_identifiers = ( + self.get_selected_items() + ) if ( set(cur_ids) == s_instance_ids and cur_context == context_selected + and set(cur_convertor_identifiers) == s_convertor_identifiers ): return @@ -926,11 +1067,20 @@ class InstanceCardView(AbstractInstanceView): if group_name == CONTEXT_GROUP: continue - group_widget = self._widgets_by_group[group_name] + legacy_group = group_name == LEGACY_ITEM_GROUP + if legacy_group: + group_widget = self._legacy_items_group + else: + group_widget = self._widgets_by_group[group_name] + group_selected = False for widget in group_widget.get_ordered_widgets(): select = False - if widget.id in s_instance_ids: + if legacy_group: + is_in = widget.identifier in s_convertor_identifiers + else: + is_in = widget.id in s_instance_ids + if is_in: selected_instances.append(widget.id) group_selected = True select = True diff --git a/openpype/tools/publisher/widgets/list_view_widgets.py b/openpype/tools/publisher/widgets/list_view_widgets.py index c329ca0e8c..df07470f1d 100644 --- a/openpype/tools/publisher/widgets/list_view_widgets.py +++ b/openpype/tools/publisher/widgets/list_view_widgets.py @@ -35,7 +35,10 @@ from ..constants import ( SORT_VALUE_ROLE, IS_GROUP_ROLE, CONTEXT_ID, - CONTEXT_LABEL + CONTEXT_LABEL, + GROUP_ROLE, + LEGACY_CONVERTER_IDENTIFIER, + LEGACY_ITEM_GROUP, ) @@ -330,6 +333,9 @@ class InstanceTreeView(QtWidgets.QTreeView): """Ids of selected instances.""" instance_ids = set() for index in self.selectionModel().selectedIndexes(): + if index.data(LEGACY_CONVERTER_IDENTIFIER) is not None: + continue + instance_id = index.data(INSTANCE_ID_ROLE) if instance_id is not None: instance_ids.add(instance_id) @@ -439,26 +445,36 @@ class InstanceListView(AbstractInstanceView): self._group_items = {} self._group_widgets = {} self._widgets_by_id = {} + # Group by instance id for handling of active state self._group_by_instance_id = {} self._context_item = None self._context_widget = None + self._legacy_group_item = None + self._legacy_group_widget = None + self._legacy_widgets_by_id = {} + self._legacy_items_by_id = {} + self._instance_view = instance_view self._instance_delegate = instance_delegate self._instance_model = instance_model self._proxy_model = proxy_model def _on_expand(self, index): - group_name = index.data(SORT_VALUE_ROLE) - group_widget = self._group_widgets.get(group_name) - if group_widget: - group_widget.set_expanded(True) + self._update_widget_expand_state(index, True) def _on_collapse(self, index): - group_name = index.data(SORT_VALUE_ROLE) - group_widget = self._group_widgets.get(group_name) + self._update_widget_expand_state(index, False) + + def _update_widget_expand_state(self, index, expanded): + group_name = index.data(GROUP_ROLE) + if group_name == LEGACY_ITEM_GROUP: + group_widget = self._legacy_group_widget + else: + group_widget = self._group_widgets.get(group_name) + if group_widget: - group_widget.set_expanded(False) + group_widget.set_expanded(expanded) def _on_toggle_request(self, toggle): selected_instance_ids = self._instance_view.get_selected_instance_ids() @@ -517,6 +533,16 @@ class InstanceListView(AbstractInstanceView): def refresh(self): """Refresh instances in the view.""" + # Sort view at the end of refresh + # - is turned off until any change in view happens + sort_at_the_end = False + # Create or use already existing context item + # - context widget does not change so we don't have to update anything + if self._make_sure_context_item_exists(): + sort_at_the_end = True + + self._update_legacy_items_group() + # Prepare instances by their groups instances_by_group_name = collections.defaultdict(list) group_names = set() @@ -525,75 +551,12 @@ class InstanceListView(AbstractInstanceView): group_names.add(group_label) instances_by_group_name[group_label].append(instance) - # Sort view at the end of refresh - # - is turned off until any change in view happens - sort_at_the_end = False - - # Access to root item of main model - root_item = self._instance_model.invisibleRootItem() - - # Create or use already existing context item - # - context widget does not change so we don't have to update anything - context_item = None - if self._context_item is None: - sort_at_the_end = True - context_item = QtGui.QStandardItem() - context_item.setData(0, SORT_VALUE_ROLE) - context_item.setData(CONTEXT_ID, INSTANCE_ID_ROLE) - - root_item.appendRow(context_item) - - index = self._instance_model.index( - context_item.row(), context_item.column() - ) - proxy_index = self._proxy_model.mapFromSource(index) - widget = ListContextWidget(self._instance_view) - self._instance_view.setIndexWidget(proxy_index, widget) - - self._context_widget = widget - self._context_item = context_item - # Create new groups based on prepared `instances_by_group_name` - new_group_items = [] - for group_name in group_names: - if group_name in self._group_items: - continue - - group_item = QtGui.QStandardItem() - group_item.setData(group_name, SORT_VALUE_ROLE) - group_item.setData(True, IS_GROUP_ROLE) - group_item.setFlags(QtCore.Qt.ItemIsEnabled) - self._group_items[group_name] = group_item - new_group_items.append(group_item) - - # Add new group items to root item if there are any - if new_group_items: - # Trigger sort at the end + if self._make_sure_groups_exists(group_names): sort_at_the_end = True - root_item.appendRows(new_group_items) - - # Create widget for each new group item and store it for future usage - for group_item in new_group_items: - index = self._instance_model.index( - group_item.row(), group_item.column() - ) - proxy_index = self._proxy_model.mapFromSource(index) - group_name = group_item.data(SORT_VALUE_ROLE) - widget = InstanceListGroupWidget(group_name, self._instance_view) - widget.expand_changed.connect(self._on_group_expand_request) - widget.toggle_requested.connect(self._on_group_toggle_request) - self._group_widgets[group_name] = widget - self._instance_view.setIndexWidget(proxy_index, widget) # Remove groups that are not available anymore - for group_name in tuple(self._group_items.keys()): - if group_name in group_names: - continue - - group_item = self._group_items.pop(group_name) - root_item.removeRow(group_item.row()) - widget = self._group_widgets.pop(group_name) - widget.deleteLater() + self._remove_groups_except(group_names) # Store which groups should be expanded at the end expand_groups = set() @@ -652,6 +615,7 @@ class InstanceListView(AbstractInstanceView): # Create new item and store it as new item = QtGui.QStandardItem() item.setData(instance["subset"], SORT_VALUE_ROLE) + item.setData(instance["subset"], GROUP_ROLE) item.setData(instance_id, INSTANCE_ID_ROLE) new_items.append(item) new_items_with_instance.append((item, instance)) @@ -717,13 +681,147 @@ class InstanceListView(AbstractInstanceView): self._instance_view.expand(proxy_index) + def _make_sure_context_item_exists(self): + if self._context_item is not None: + return False + + root_item = self._instance_model.invisibleRootItem() + context_item = QtGui.QStandardItem() + context_item.setData(0, SORT_VALUE_ROLE) + context_item.setData(CONTEXT_ID, INSTANCE_ID_ROLE) + + root_item.appendRow(context_item) + + index = self._instance_model.index( + context_item.row(), context_item.column() + ) + proxy_index = self._proxy_model.mapFromSource(index) + widget = ListContextWidget(self._instance_view) + self._instance_view.setIndexWidget(proxy_index, widget) + + self._context_widget = widget + self._context_item = context_item + return True + + def _update_legacy_items_group(self): + created_new_items = False + legacy_items_by_id = self._controller.legacy_items + group_item = self._legacy_group_item + if not legacy_items_by_id and group_item is None: + return created_new_items + + root_item = self._instance_model.invisibleRootItem() + if not legacy_items_by_id: + root_item.removeRow(group_item.row()) + self._legacy_group_widget.deleteLater() + self._legacy_group_widget = None + return created_new_items + + if group_item is None: + created_new_items = True + group_item = QtGui.QStandardItem() + group_item.setData(LEGACY_ITEM_GROUP, GROUP_ROLE) + group_item.setData(1, SORT_VALUE_ROLE) + group_item.setData(True, IS_GROUP_ROLE) + group_item.setFlags(QtCore.Qt.ItemIsEnabled) + + root_item.appendRow(group_item) + + index = self._instance_model.index( + group_item.row(), group_item.column() + ) + proxy_index = self._proxy_model.mapFromSource(index) + widget = InstanceListGroupWidget( + LEGACY_ITEM_GROUP, self._instance_view + ) + widget.toggle_checkbox.setVisible(False) + widget.expand_changed.connect(self._on_legacy_group_expand_request) + self._instance_view.setIndexWidget(proxy_index, widget) + + self._legacy_group_item = group_item + self._legacy_group_widget = widget + + for row in reversed(range(group_item.rowCount())): + child_item = group_item.child(row) + child_identifier = child_item.data(LEGACY_CONVERTER_IDENTIFIER) + if child_identifier not in legacy_items_by_id: + group_item.removeRows(row, 1) + + new_items = [] + for identifier, convertor_item in legacy_items_by_id.items(): + item = self._legacy_items_by_id.get(identifier) + if item is None: + created_new_items = True + item = QtGui.QStandardItem(convertor_item.label) + new_items.append(item) + item.setData(convertor_item.id, INSTANCE_ID_ROLE) + item.setData(convertor_item.label, SORT_VALUE_ROLE) + item.setData(LEGACY_ITEM_GROUP, GROUP_ROLE) + item.setData( + convertor_item.identifier, LEGACY_CONVERTER_IDENTIFIER + ) + + if new_items: + group_item.appendRows(new_items) + + return created_new_items + + def _make_sure_groups_exists(self, group_names): + new_group_items = [] + for group_name in group_names: + if group_name in self._group_items: + continue + + group_item = QtGui.QStandardItem() + group_item.setData(group_name, GROUP_ROLE) + group_item.setData(group_name, SORT_VALUE_ROLE) + group_item.setData(True, IS_GROUP_ROLE) + group_item.setFlags(QtCore.Qt.ItemIsEnabled) + self._group_items[group_name] = group_item + new_group_items.append(group_item) + + # Add new group items to root item if there are any + if not new_group_items: + return False + + # Access to root item of main model + root_item = self._instance_model.invisibleRootItem() + root_item.appendRows(new_group_items) + + # Create widget for each new group item and store it for future usage + for group_item in new_group_items: + index = self._instance_model.index( + group_item.row(), group_item.column() + ) + proxy_index = self._proxy_model.mapFromSource(index) + group_name = group_item.data(GROUP_ROLE) + widget = InstanceListGroupWidget(group_name, self._instance_view) + widget.expand_changed.connect(self._on_group_expand_request) + widget.toggle_requested.connect(self._on_group_toggle_request) + self._group_widgets[group_name] = widget + self._instance_view.setIndexWidget(proxy_index, widget) + + return True + + def _remove_groups_except(self, group_names): + # Remove groups that are not available anymore + root_item = self._instance_model.invisibleRootItem() + for group_name in tuple(self._group_items.keys()): + if group_name in group_names: + continue + + group_item = self._group_items.pop(group_name) + root_item.removeRow(group_item.row()) + widget = self._group_widgets.pop(group_name) + widget.deleteLater() + def refresh_instance_states(self): """Trigger update of all instances.""" for widget in self._widgets_by_id.values(): widget.update_instance_values() def _on_active_changed(self, changed_instance_id, new_value): - selected_instance_ids, _ = self.get_selected_items() + selected_instance_ids, _, _ = self.get_selected_items() selected_ids = set() found = False @@ -774,6 +872,16 @@ class InstanceListView(AbstractInstanceView): proxy_index = self._proxy_model.mapFromSource(group_index) self._instance_view.setExpanded(proxy_index, expanded) + def _on_legacy_group_expand_request(self, _, expanded): + group_item = self._legacy_group_item + if not group_item: + return + group_index = self._instance_model.index( + group_item.row(), group_item.column() + ) + proxy_index = self._proxy_model.mapFromSource(group_index) + self._instance_view.setExpanded(proxy_index, expanded) + def _on_group_toggle_request(self, group_name, state): if state == QtCore.Qt.PartiallyChecked: return @@ -807,10 +915,17 @@ class InstanceListView(AbstractInstanceView): tuple: Selected instance ids and boolean if context is selected. """ + instance_ids = [] + convertor_identifiers = [] context_selected = False for index in self._instance_view.selectionModel().selectedIndexes(): + convertor_identifier = index.data(LEGACY_CONVERTER_IDENTIFIER) + if convertor_identifier is not None: + convertor_identifiers.append(convertor_identifier) + continue + instance_id = index.data(INSTANCE_ID_ROLE) if not context_selected and instance_id == CONTEXT_ID: context_selected = True @@ -818,14 +933,20 @@ class InstanceListView(AbstractInstanceView): elif instance_id is not None: instance_ids.append(instance_id) - return instance_ids, context_selected + return instance_ids, context_selected, convertor_identifiers - def set_selected_items(self, instance_ids, context_selected): + def set_selected_items( + self, instance_ids, context_selected, convertor_identifiers + ): s_instance_ids = set(instance_ids) - cur_ids, cur_context = self.get_selected_items() + s_convertor_identifiers = set(convertor_identifiers) + cur_ids, cur_context, cur_convertor_identifiers = ( + self.get_selected_items() + ) if ( set(cur_ids) == s_instance_ids and cur_context == context_selected + and set(cur_convertor_identifiers) == s_convertor_identifiers ): return @@ -851,20 +972,35 @@ class InstanceListView(AbstractInstanceView): (item.child(row), list(new_parent_items)) ) - instance_id = item.data(INSTANCE_ID_ROLE) - if not instance_id: + convertor_identifier = item.data(LEGACY_CONVERTER_IDENTIFIER) + + select = False + expand_parent = True + if convertor_identifier is not None: + if convertor_identifier in s_convertor_identifiers: + select = True + else: + instance_id = item.data(INSTANCE_ID_ROLE) + if instance_id == CONTEXT_ID: + if context_selected: + select = True + expand_parent = False + + elif instance_id in s_instance_ids: + select = True + + if not select: continue - if instance_id in s_instance_ids: - select_indexes.append(item.index()) - for parent_item in parent_items: - index = parent_item.index() - proxy_index = proxy_model.mapFromSource(index) - if not view.isExpanded(proxy_index): - view.expand(proxy_index) + select_indexes.append(item.index()) + if not expand_parent: + continue - elif context_selected and instance_id == CONTEXT_ID: - select_indexes.append(item.index()) + for parent_item in parent_items: + index = parent_item.index() + proxy_index = proxy_model.mapFromSource(index) + if not view.isExpanded(proxy_index): + view.expand(proxy_index) selection_model = view.selectionModel() if not select_indexes: diff --git a/openpype/tools/publisher/widgets/overview_widget.py b/openpype/tools/publisher/widgets/overview_widget.py index 5bd3017c2a..e208786fc7 100644 --- a/openpype/tools/publisher/widgets/overview_widget.py +++ b/openpype/tools/publisher/widgets/overview_widget.py @@ -124,6 +124,9 @@ class OverviewWidget(QtWidgets.QFrame): subset_attributes_widget.instance_context_changed.connect( self._on_instance_context_change ) + subset_attributes_widget.convert_requested.connect( + self._on_convert_requested + ) # --- Controller callbacks --- controller.event_system.add_callback( @@ -201,7 +204,7 @@ class OverviewWidget(QtWidgets.QFrame): self.create_requested.emit() def _on_delete_clicked(self): - instance_ids, _ = self.get_selected_items() + instance_ids, _, _ = self.get_selected_items() # Ask user if he really wants to remove instances dialog = QtWidgets.QMessageBox(self) @@ -235,7 +238,9 @@ class OverviewWidget(QtWidgets.QFrame): if self._refreshing_instances: return - instance_ids, context_selected = self.get_selected_items() + instance_ids, context_selected, convertor_identifiers = ( + self.get_selected_items() + ) # Disable delete button if nothing is selected self._delete_btn.setEnabled(len(instance_ids) > 0) @@ -246,7 +251,7 @@ class OverviewWidget(QtWidgets.QFrame): for instance_id in instance_ids ] self._subset_attributes_widget.set_current_instances( - instances, context_selected + instances, context_selected, convertor_identifiers ) def _on_active_changed(self): @@ -314,6 +319,10 @@ class OverviewWidget(QtWidgets.QFrame): self.instance_context_changed.emit() + def _on_convert_requested(self): + _, _, convertor_identifiers = self.get_selected_items() + self._controller.convert_legacy_items(convertor_identifiers) + def get_selected_items(self): view = self._subset_views_layout.currentWidget() return view.get_selected_items() @@ -331,8 +340,12 @@ class OverviewWidget(QtWidgets.QFrame): else: new_view.refresh_instance_states() - instance_ids, context_selected = old_view.get_selected_items() - new_view.set_selected_items(instance_ids, context_selected) + instance_ids, context_selected, convertor_identifiers = ( + old_view.get_selected_items() + ) + new_view.set_selected_items( + instance_ids, context_selected, convertor_identifiers + ) self._subset_views_layout.setCurrentIndex(new_idx) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index ddbe1eb6b7..b01fed25a5 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1461,6 +1461,7 @@ class SubsetAttributesWidget(QtWidgets.QWidget): └───────────────────────────────┘ """ instance_context_changed = QtCore.Signal() + convert_requested = QtCore.Signal() def __init__(self, controller, parent): super(SubsetAttributesWidget, self).__init__(parent) @@ -1479,9 +1480,48 @@ class SubsetAttributesWidget(QtWidgets.QWidget): # BOTTOM PART bottom_widget = QtWidgets.QWidget(self) - creator_attrs_widget = CreatorAttrsWidget( - controller, bottom_widget + + # Wrap Creator attributes to widget to be able add convert button + creator_widget = QtWidgets.QWidget(bottom_widget) + + # Convert button widget (with layout to handle stretch) + convert_widget = QtWidgets.QWidget(creator_widget) + convert_label = QtWidgets.QLabel( + ( + "Found instances created with legacy creators." + "\nDo you with to convert them?" + ), + creator_widget ) + convert_label.setWordWrap(True) + convert_label.setAlignment(QtCore.Qt.AlignCenter) + + convert_btn = QtWidgets.QPushButton( + "Convert legacy instances", convert_widget + ) + convert_separator = QtWidgets.QFrame(convert_widget) + convert_separator.setObjectName("Separator") + convert_separator.setMinimumHeight(2) + convert_separator.setMaximumHeight(2) + + convert_layout = QtWidgets.QGridLayout(convert_widget) + convert_layout.setContentsMargins(0, 0, 0, 0) + convert_layout.addWidget(convert_label, 0, 0, 1, 3) + convert_layout.addWidget(convert_btn, 1, 1) + convert_layout.addWidget(convert_separator, 2, 0, 1, 3) + convert_layout.setColumnStretch(0, 1) + convert_layout.setColumnStretch(1, 0) + convert_layout.setColumnStretch(2, 1) + + # Creator attributes widget + creator_attrs_widget = CreatorAttrsWidget( + controller, creator_widget + ) + creator_layout = QtWidgets.QVBoxLayout(creator_widget) + creator_layout.setContentsMargins(0, 0, 0, 0) + creator_layout.addWidget(convert_widget, 0) + creator_layout.addWidget(creator_attrs_widget, 1) + publish_attrs_widget = PublishPluginAttrsWidget( controller, bottom_widget ) @@ -1492,7 +1532,7 @@ class SubsetAttributesWidget(QtWidgets.QWidget): bottom_layout = QtWidgets.QHBoxLayout(bottom_widget) bottom_layout.setContentsMargins(0, 0, 0, 0) - bottom_layout.addWidget(creator_attrs_widget, 1) + bottom_layout.addWidget(creator_widget, 1) bottom_layout.addWidget(bottom_separator, 0) bottom_layout.addWidget(publish_attrs_widget, 1) @@ -1505,6 +1545,7 @@ class SubsetAttributesWidget(QtWidgets.QWidget): layout.addWidget(top_bottom, 0) layout.addWidget(bottom_widget, 1) + self._convertor_identifiers = None self._current_instances = None self._context_selected = False self._all_instances_valid = True @@ -1512,9 +1553,12 @@ class SubsetAttributesWidget(QtWidgets.QWidget): global_attrs_widget.instance_context_changed.connect( self._on_instance_context_changed ) + convert_btn.clicked.connect(self._on_convert_click) self._controller = controller + self._convert_widget = convert_widget + self.global_attrs_widget = global_attrs_widget self.creator_attrs_widget = creator_attrs_widget @@ -1537,7 +1581,12 @@ class SubsetAttributesWidget(QtWidgets.QWidget): self.instance_context_changed.emit() - def set_current_instances(self, instances, context_selected): + def _on_convert_click(self): + self.convert_requested.emit() + + def set_current_instances( + self, instances, context_selected, convertor_identifiers + ): """Change currently selected items. Args: @@ -1551,10 +1600,13 @@ class SubsetAttributesWidget(QtWidgets.QWidget): all_valid = False break + s_convertor_identifiers = set(convertor_identifiers) + self._convertor_identifiers = s_convertor_identifiers self._current_instances = instances self._context_selected = context_selected self._all_instances_valid = all_valid + self._convert_widget.setVisible(len(s_convertor_identifiers) > 0) self.global_attrs_widget.set_current_instances(instances) self.creator_attrs_widget.set_current_instances(instances) self.publish_attrs_widget.set_current_instances( From 45c944816c42d2593b61fc18f78ca321e6b3d120 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 19:45:17 +0200 Subject: [PATCH 18/46] removed unused variable --- openpype/tools/publisher/widgets/card_view_widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/card_view_widgets.py b/openpype/tools/publisher/widgets/card_view_widgets.py index 58a7bbc509..96802087ee 100644 --- a/openpype/tools/publisher/widgets/card_view_widgets.py +++ b/openpype/tools/publisher/widgets/card_view_widgets.py @@ -178,7 +178,7 @@ class BaseGroupWidget(QtWidgets.QWidget): class LegacyItemsGroupWidget(BaseGroupWidget): def update_items(self, items_by_id): items_by_label = collections.defaultdict(list) - for item_id, item in items_by_id.items(): + for item in items_by_id.values(): items_by_label[item.label].append(item) # Remove instance widgets that are not in passed instances From ba621ee54a9f7bc318dd3701ec80b3ee18354f55 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 22 Oct 2022 04:02:46 +0000 Subject: [PATCH 19/46] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index fd3606e9f2..cda0a98ef3 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.14.4" +__version__ = "3.14.5-nightly.1" From d0d8c8958ce81c17f72d1717822699c28a6ba04c Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 24 Oct 2022 11:29:58 +0200 Subject: [PATCH 20/46] fix obj extractor --- openpype/hosts/maya/plugins/load/actions.py | 2 +- .../hosts/maya/plugins/publish/extract_obj.py | 51 ++++++++++++------- .../defaults/project_settings/maya.json | 24 +++++---- .../schemas/schema_maya_publish.json | 19 +++++++ 4 files changed, 67 insertions(+), 29 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/actions.py b/openpype/hosts/maya/plugins/load/actions.py index 253dae1e43..eca1b27f34 100644 --- a/openpype/hosts/maya/plugins/load/actions.py +++ b/openpype/hosts/maya/plugins/load/actions.py @@ -90,7 +90,7 @@ class ImportMayaLoader(load.LoaderPlugin): so you could also use it as a new base. """ - representations = ["ma", "mb"] + representations = ["ma", "mb", "obj"] families = ["*"] label = "Import" diff --git a/openpype/hosts/maya/plugins/publish/extract_obj.py b/openpype/hosts/maya/plugins/publish/extract_obj.py index 7c915a80d8..59f11a4aa9 100644 --- a/openpype/hosts/maya/plugins/publish/extract_obj.py +++ b/openpype/hosts/maya/plugins/publish/extract_obj.py @@ -2,15 +2,12 @@ import os from maya import cmds -import maya.mel as mel +# import maya.mel as mel import pyblish.api -import openpype.api -from openpype.hosts.maya.api.lib import maintained_selection +from openpype.pipeline import publish +from openpype.hosts.maya.api import lib -from openpype.hosts.maya.api import obj - - -class ExtractObj(openpype.api.Extractor): +class ExtractObj(publish.Extractor): """Extract OBJ from Maya. This extracts reproducible OBJ exports ignoring any of the settings @@ -18,42 +15,60 @@ class ExtractObj(openpype.api.Extractor): """ order = pyblish.api.ExtractorOrder + hosts = ["maya"] label = "Extract OBJ" - families = ["obj"] + families = ["model"] def process(self, instance): - obj_exporter = obj.OBJExtractor(log=self.log) # Define output path staging_dir = self.staging_dir(instance) - filename = "{0}.fbx".format(instance.name) + filename = "{0}.obj".format(instance.name) path = os.path.join(staging_dir, filename) # The export requires forward slashes because we need to # format it into a string in a mel expression - path = path.replace('\\', '/') self.log.info("Extracting OBJ to: {0}".format(path)) - members = instance.data["setMembners"] + members = instance.data("setMembers") + members = cmds.ls(members, + dag=True, + shapes=True, + type=("mesh", "nurbsCurve"), + noIntermediate=True, + long=True) self.log.info("Members: {0}".format(members)) self.log.info("Instance: {0}".format(instance[:])) - obj_exporter.set_options_from_instance(instance) + if not cmds.pluginInfo('objExport', query=True, loaded=True): + cmds.loadPlugin('objExport') # Export - with maintained_selection(): - obj_exporter.export(members, path) - cmds.select(members, r=1, noExpand=True) - mel.eval('file -force -options "{0};{1};{2};{3};{4}" -typ "OBJexport" -pr -es "{5}";'.format(grp_flag, ptgrp_flag, mats_flag, smooth_flag, normals_flag, path)) # noqa + with lib.no_display_layers(instance): + with lib.displaySmoothness(members, + divisionsU=0, + divisionsV=0, + pointsWire=4, + pointsShaded=1, + polygonObject=1): + with lib.shader(members, + shadingEngine="initialShadingGroup"): + with lib.maintained_selection(): + cmds.select(members, noExpand=True) + cmds.file(path, + exportSelected=True, + type='OBJexport', + preserveReferences=True, + force=True) if "representation" not in instance.data: instance.data["representation"] = [] representation = { 'name':'obj', - 'ext':'obx', + 'ext':'obj', 'files': filename, "stagingDir": staging_dir, } diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 86815b8fc4..b0bef4943b 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -131,6 +131,16 @@ "Main" ] }, + "CreateModel": { + "enabled": true, + "write_color_sets": false, + "write_face_sets": false, + "defaults": [ + "Main", + "Proxy", + "Sculpt" + ] + }, "CreatePointCache": { "enabled": true, "write_color_sets": false, @@ -187,16 +197,6 @@ "Main" ] }, - "CreateModel": { - "enabled": true, - "write_color_sets": false, - "write_face_sets": false, - "defaults": [ - "Main", - "Proxy", - "Sculpt" - ] - }, "CreateRenderSetup": { "enabled": true, "defaults": [ @@ -577,6 +577,10 @@ "vrayproxy" ] }, + "ExtractObj": { + "enabled": true, + "optional": true + }, "ValidateRigContents": { "enabled": false, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index 53247f6bd4..ab8c6b885e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -657,6 +657,25 @@ "object_type": "text" } ] + }, + { + "type": "dict", + "collapsible": true, + "key": "ExtractObj", + "label": "Extract OBJ", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + } + ] } ] }, From 96af8158796dad310feae7f96f74677e4311710f Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 24 Oct 2022 11:36:43 +0200 Subject: [PATCH 21/46] turn off OBJ by default --- 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 b0bef4943b..988c0e777a 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -578,7 +578,7 @@ ] }, "ExtractObj": { - "enabled": true, + "enabled": false, "optional": true }, "ValidateRigContents": { From f0a394bfd9e749f3bde00fd4d43ba5921192fe7e Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 24 Oct 2022 11:37:02 +0200 Subject: [PATCH 22/46] =?UTF-8?q?=F0=9F=90=95=E2=80=8D=F0=9F=A6=BA=20shut?= =?UTF-8?q?=20up=20hound?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hosts/maya/plugins/publish/extract_obj.py | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_obj.py b/openpype/hosts/maya/plugins/publish/extract_obj.py index 59f11a4aa9..edfe0b9439 100644 --- a/openpype/hosts/maya/plugins/publish/extract_obj.py +++ b/openpype/hosts/maya/plugins/publish/extract_obj.py @@ -7,6 +7,7 @@ import pyblish.api from openpype.pipeline import publish from openpype.hosts.maya.api import lib + class ExtractObj(publish.Extractor): """Extract OBJ from Maya. @@ -34,11 +35,11 @@ class ExtractObj(publish.Extractor): members = instance.data("setMembers") members = cmds.ls(members, - dag=True, - shapes=True, - type=("mesh", "nurbsCurve"), - noIntermediate=True, - long=True) + dag=True, + shapes=True, + type=("mesh", "nurbsCurve"), + noIntermediate=True, + long=True) self.log.info("Members: {0}".format(members)) self.log.info("Instance: {0}".format(instance[:])) @@ -58,17 +59,17 @@ class ExtractObj(publish.Extractor): with lib.maintained_selection(): cmds.select(members, noExpand=True) cmds.file(path, - exportSelected=True, - type='OBJexport', - preserveReferences=True, - force=True) + exportSelected=True, + type='OBJexport', + preserveReferences=True, + force=True) if "representation" not in instance.data: instance.data["representation"] = [] representation = { - 'name':'obj', - 'ext':'obj', + 'name': 'obj', + 'ext': 'obj', 'files': filename, "stagingDir": staging_dir, } From aefb6163ee35e428facb1a8044c33a6cfdc3b372 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Mon, 24 Oct 2022 09:40:21 +0000 Subject: [PATCH 23/46] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index cda0a98ef3..079822029e 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.14.5-nightly.1" +__version__ = "3.14.5-nightly.2" From 5f3312af04155a0d34d2a7a4ccd23a1e3c8eee1d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 24 Oct 2022 13:38:42 +0200 Subject: [PATCH 24/46] change log update --- CHANGELOG.md | 53 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4f1dcf314..5464c390ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,40 @@ # Changelog -## [3.14.4](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.14.5](https://github.com/pypeclub/OpenPype/tree/HEAD) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.3...HEAD) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.4...HEAD) + +**🚀 Enhancements** + +- Maya: add OBJ extractor to model family [\#4021](https://github.com/pypeclub/OpenPype/pull/4021) +- Publish report viewer tool [\#4010](https://github.com/pypeclub/OpenPype/pull/4010) +- Nuke | Global: adding custom tags representation filtering [\#4009](https://github.com/pypeclub/OpenPype/pull/4009) +- Publisher: Create context has shared data for collection phase [\#3995](https://github.com/pypeclub/OpenPype/pull/3995) +- Resolve: updating to v18 compatibility [\#3986](https://github.com/pypeclub/OpenPype/pull/3986) + +**🐛 Bug fixes** + +- TrayPublisher: Fix missing argument [\#4019](https://github.com/pypeclub/OpenPype/pull/4019) +- General: Fix python 2 compatibility of ffmpeg and oiio tools discovery [\#4011](https://github.com/pypeclub/OpenPype/pull/4011) + +**🔀 Refactored code** + +- Maya: Removed unused imports [\#4008](https://github.com/pypeclub/OpenPype/pull/4008) +- Unreal: Fix import of moved function [\#4007](https://github.com/pypeclub/OpenPype/pull/4007) +- Houdini: Change import of RepairAction [\#4005](https://github.com/pypeclub/OpenPype/pull/4005) +- Nuke/Hiero: Refactor openpype.api imports [\#4000](https://github.com/pypeclub/OpenPype/pull/4000) +- TVPaint: Defined with HostBase [\#3994](https://github.com/pypeclub/OpenPype/pull/3994) + +**Merged pull requests:** + +- Unreal: Remove redundant Creator stub [\#4012](https://github.com/pypeclub/OpenPype/pull/4012) +- Unreal: add `uproject` extension to Unreal project template [\#4004](https://github.com/pypeclub/OpenPype/pull/4004) +- Unreal: fix order of includes [\#4002](https://github.com/pypeclub/OpenPype/pull/4002) +- Fusion: Implement backwards compatibility \(+/- Fusion 17.2\) [\#3958](https://github.com/pypeclub/OpenPype/pull/3958) + +## [3.14.4](https://github.com/pypeclub/OpenPype/tree/3.14.4) (2022-10-19) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.3...3.14.4) **🆕 New features** @@ -27,7 +59,6 @@ - Maya: Moved plugin from global to maya [\#3939](https://github.com/pypeclub/OpenPype/pull/3939) - Publisher: Create dialog is part of main window [\#3936](https://github.com/pypeclub/OpenPype/pull/3936) - Fusion: Implement Alembic and FBX mesh loader [\#3927](https://github.com/pypeclub/OpenPype/pull/3927) -- Maya: Remove hardcoded requirement for maya/ start for image file prefix [\#3873](https://github.com/pypeclub/OpenPype/pull/3873) **🐛 Bug fixes** @@ -71,14 +102,6 @@ **🚀 Enhancements** - Publisher: Enhancement proposals [\#3897](https://github.com/pypeclub/OpenPype/pull/3897) -- Maya: better logging in Maketx [\#3886](https://github.com/pypeclub/OpenPype/pull/3886) -- Photoshop: review can be turned off [\#3885](https://github.com/pypeclub/OpenPype/pull/3885) -- TrayPublisher: added persisting of last selected project [\#3871](https://github.com/pypeclub/OpenPype/pull/3871) -- TrayPublisher: added text filter on project name to Tray Publisher [\#3867](https://github.com/pypeclub/OpenPype/pull/3867) -- Github issues adding `running version` section [\#3864](https://github.com/pypeclub/OpenPype/pull/3864) -- Publisher: Increase size of main window [\#3862](https://github.com/pypeclub/OpenPype/pull/3862) -- Flame: make migratable projects after creation [\#3860](https://github.com/pypeclub/OpenPype/pull/3860) -- Photoshop: synchronize image version with workfile [\#3854](https://github.com/pypeclub/OpenPype/pull/3854) **🐛 Bug fixes** @@ -86,12 +109,6 @@ - Flame: loading multilayer exr to batch/reel is working [\#3901](https://github.com/pypeclub/OpenPype/pull/3901) - Hiero: Fix inventory check on launch [\#3895](https://github.com/pypeclub/OpenPype/pull/3895) - WebPublisher: Fix import after refactor [\#3891](https://github.com/pypeclub/OpenPype/pull/3891) -- TVPaint: Fix renaming of rendered files [\#3882](https://github.com/pypeclub/OpenPype/pull/3882) -- Publisher: Nice checkbox visible in Python 2 [\#3877](https://github.com/pypeclub/OpenPype/pull/3877) -- Settings: Add missing default settings [\#3870](https://github.com/pypeclub/OpenPype/pull/3870) -- General: Copy of workfile does not use 'copy' function but 'copyfile' [\#3869](https://github.com/pypeclub/OpenPype/pull/3869) -- Tray Publisher: skip plugin if otioTimeline is missing [\#3856](https://github.com/pypeclub/OpenPype/pull/3856) -- Flame: retimed attributes are integrated with settings [\#3855](https://github.com/pypeclub/OpenPype/pull/3855) **🔀 Refactored code** @@ -105,8 +122,6 @@ **Merged pull requests:** - Maya: Fix Scene Inventory possibly starting off-screen due to maya preferences [\#3923](https://github.com/pypeclub/OpenPype/pull/3923) -- Maya: RenderSettings set default image format for V-Ray+Redshift to exr [\#3879](https://github.com/pypeclub/OpenPype/pull/3879) -- Remove lockfile during publish [\#3874](https://github.com/pypeclub/OpenPype/pull/3874) ## [3.14.2](https://github.com/pypeclub/OpenPype/tree/3.14.2) (2022-09-12) From 74ebf90046a442f905966a4fdd77f419b208f6e8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 24 Oct 2022 13:51:20 +0200 Subject: [PATCH 25/46] Removed submodule vendor/configs/OpenColorIO-Configs --- vendor/configs/OpenColorIO-Configs | 1 - 1 file changed, 1 deletion(-) delete mode 160000 vendor/configs/OpenColorIO-Configs diff --git a/vendor/configs/OpenColorIO-Configs b/vendor/configs/OpenColorIO-Configs deleted file mode 160000 index 0bb079c08b..0000000000 --- a/vendor/configs/OpenColorIO-Configs +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0bb079c08be410030669cbf5f19ff869b88af953 From 0ccbcbce93f789572bbdb424bdfdf02940d40abb Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 24 Oct 2022 13:54:55 +0200 Subject: [PATCH 26/46] Removed submodule vendor/configs/OpenColorIO-Configs --- vendor/configs/OpenColorIO-Configs | 1 - 1 file changed, 1 deletion(-) delete mode 160000 vendor/configs/OpenColorIO-Configs diff --git a/vendor/configs/OpenColorIO-Configs b/vendor/configs/OpenColorIO-Configs deleted file mode 160000 index 0bb079c08b..0000000000 --- a/vendor/configs/OpenColorIO-Configs +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0bb079c08be410030669cbf5f19ff869b88af953 From 883f035361cee56d4e297271d178be20b40f2557 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 14:08:24 +0200 Subject: [PATCH 27/46] extract review does not crash with old settings overrides --- openpype/plugins/publish/extract_review.py | 41 ++++++++++------------ 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 431ddcc3b4..5e8f85ab86 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -1731,38 +1731,33 @@ class ExtractReview(pyblish.api.InstancePlugin): Returns: list: Containg all output definitions matching entered tags. """ + filtered_outputs = [] repre_c_tags_low = [tag.lower() for tag in (custom_tags or [])] for output_def in outputs: - valid = False tag_filters = output_def.get("filter", {}).get("custom_tags") - if ( - # if any of tag filter is empty, skip - custom_tags and not tag_filters - or not custom_tags and tag_filters - ): - continue - elif not custom_tags and not tag_filters: + if not custom_tags and not tag_filters: + # Definition is valid if both tags are empty valid = True - # lower all filter tags - tag_filters_low = [tag.lower() for tag in tag_filters] + elif not custom_tags or not tag_filters: + # Invalid if one is empty + valid = False - self.log.debug("__ tag_filters: {}".format(tag_filters)) - self.log.debug("__ repre_c_tags_low: {}".format( - repre_c_tags_low)) + else: + # Check if output definition tags are in representation tags + valid = False + # lower all filter tags + tag_filters_low = [tag.lower() for tag in tag_filters] + # check if any repre tag is not in filter tags + for tag in repre_c_tags_low: + if tag in tag_filters_low: + valid = True + break - # check if any repre tag is not in filter tags - for tag in repre_c_tags_low: - if tag in tag_filters_low: - valid = True - break - - if not valid: - continue - - filtered_outputs.append(output_def) + if valid: + filtered_outputs.append(output_def) self.log.debug("__ filtered_outputs: {}".format( [_o["filename_suffix"] for _o in filtered_outputs] From 32d1e572d7f869b596597cdc340f787f16858b92 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Mon, 24 Oct 2022 12:52:58 +0000 Subject: [PATCH 28/46] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 079822029e..d1ba207aa3 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.14.5-nightly.2" +__version__ = "3.14.5-nightly.3" From 2e818a44041e806bc503594c211ad04bd3b4a29d Mon Sep 17 00:00:00 2001 From: OpenPype Date: Mon, 24 Oct 2022 13:11:22 +0000 Subject: [PATCH 29/46] [Automated] Release --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index d1ba207aa3..b1e4227030 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.14.5-nightly.3" +__version__ = "3.14.5" From 245c5e9afb81f231bcc884f5c503ae6c812421b8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 17:16:45 +0200 Subject: [PATCH 30/46] changed label of legacy group --- openpype/tools/publisher/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/publisher/constants.py b/openpype/tools/publisher/constants.py index 3c192bf8a3..e5969160c1 100644 --- a/openpype/tools/publisher/constants.py +++ b/openpype/tools/publisher/constants.py @@ -6,7 +6,7 @@ CONTEXT_LABEL = "Options" # Not showed anywhere - used as identifier CONTEXT_GROUP = "__ContextGroup__" -LEGACY_ITEM_GROUP = "Legacy instances" +LEGACY_ITEM_GROUP = "Incompatible subsets" # Allowed symbols for subset name (and variant) # - characters, numbers, unsercore and dash From 080deda3167c2b40fcd10582b6c4e99498cf2ff1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 17:18:06 +0200 Subject: [PATCH 31/46] fix list view update --- openpype/tools/publisher/widgets/list_view_widgets.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/list_view_widgets.py b/openpype/tools/publisher/widgets/list_view_widgets.py index df07470f1d..53951e3cba 100644 --- a/openpype/tools/publisher/widgets/list_view_widgets.py +++ b/openpype/tools/publisher/widgets/list_view_widgets.py @@ -452,7 +452,6 @@ class InstanceListView(AbstractInstanceView): self._legacy_group_item = None self._legacy_group_widget = None - self._legacy_widgets_by_id = {} self._legacy_items_by_id = {} self._instance_view = instance_view @@ -715,6 +714,7 @@ class InstanceListView(AbstractInstanceView): root_item.removeRow(group_item.row()) self._legacy_group_widget.deleteLater() self._legacy_group_widget = None + self._legacy_items_by_id = {} return created_new_items if group_item is None: @@ -745,6 +745,7 @@ class InstanceListView(AbstractInstanceView): child_item = group_item.child(row) child_identifier = child_item.data(LEGACY_CONVERTER_IDENTIFIER) if child_identifier not in legacy_items_by_id: + self._legacy_items_by_id.pop(child_identifier, None) group_item.removeRows(row, 1) new_items = [] @@ -760,6 +761,7 @@ class InstanceListView(AbstractInstanceView): item.setData( convertor_item.identifier, LEGACY_CONVERTER_IDENTIFIER ) + self._legacy_items_by_id[identifier] = item if new_items: group_item.appendRows(new_items) From 2787351f03aa7eb7c0220a8f60ba85e4b6a91166 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 17:18:23 +0200 Subject: [PATCH 32/46] change labels of the message for user --- openpype/tools/publisher/widgets/widgets.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index b01fed25a5..ec63509dfa 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1486,18 +1486,22 @@ class SubsetAttributesWidget(QtWidgets.QWidget): # Convert button widget (with layout to handle stretch) convert_widget = QtWidgets.QWidget(creator_widget) - convert_label = QtWidgets.QLabel( + convert_label = QtWidgets.QLabel(creator_widget) + # Set the label text with 'setText' to apply html + convert_label.setText( ( - "Found instances created with legacy creators." - "\nDo you with to convert them?" - ), - creator_widget + "Found old publishable subsets" + " incompatible with new publisher." + "

Press the update subsets button" + " to automatically update them" + " to be able to publish again." + ) ) convert_label.setWordWrap(True) convert_label.setAlignment(QtCore.Qt.AlignCenter) convert_btn = QtWidgets.QPushButton( - "Convert legacy instances", convert_widget + "Update subsets", convert_widget ) convert_separator = QtWidgets.QFrame(convert_widget) convert_separator.setObjectName("Separator") From e94cd00ad7adc36adf48f9c05a752e94611778d3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 17:18:31 +0200 Subject: [PATCH 33/46] change separator size --- openpype/tools/publisher/widgets/widgets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index ec63509dfa..e091e76fab 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1505,8 +1505,8 @@ class SubsetAttributesWidget(QtWidgets.QWidget): ) convert_separator = QtWidgets.QFrame(convert_widget) convert_separator.setObjectName("Separator") - convert_separator.setMinimumHeight(2) - convert_separator.setMaximumHeight(2) + convert_separator.setMinimumHeight(1) + convert_separator.setMaximumHeight(1) convert_layout = QtWidgets.QGridLayout(convert_widget) convert_layout.setContentsMargins(0, 0, 0, 0) From a98085704ff5a2ebaa205b715bf72024bea0e6bd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 17:21:39 +0200 Subject: [PATCH 34/46] added some padding and spacing --- openpype/tools/publisher/widgets/widgets.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index e091e76fab..d4c2623790 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1509,7 +1509,8 @@ class SubsetAttributesWidget(QtWidgets.QWidget): convert_separator.setMaximumHeight(1) convert_layout = QtWidgets.QGridLayout(convert_widget) - convert_layout.setContentsMargins(0, 0, 0, 0) + convert_layout.setContentsMargins(5, 0, 5, 0) + convert_layout.setVerticalSpacing(10) convert_layout.addWidget(convert_label, 0, 0, 1, 3) convert_layout.addWidget(convert_btn, 1, 1) convert_layout.addWidget(convert_separator, 2, 0, 1, 3) From 271a0056bcd988a2371124879e86805cc379cbca Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 17:31:38 +0200 Subject: [PATCH 35/46] change the item look --- openpype/style/data.json | 5 +---- openpype/style/style.css | 12 ------------ .../tools/publisher/widgets/card_view_widgets.py | 5 ++--- 3 files changed, 3 insertions(+), 19 deletions(-) diff --git a/openpype/style/data.json b/openpype/style/data.json index 44c0d51999..fef69071ed 100644 --- a/openpype/style/data.json +++ b/openpype/style/data.json @@ -100,10 +100,7 @@ "bg-expander": "#2C313A", "bg-expander-hover": "#2d6c9f", "bg-expander-selected-hover": "#3784c5" - }, - "bg-legacy": "rgb(17, 17, 17)", - "bg-legacy-hover": "rgb(41, 41, 41)", - "bg-legacy-selected": "rgba(42, 123, 174, .4)" + } }, "settings": { "invalid-light": "#C93636", diff --git a/openpype/style/style.css b/openpype/style/style.css index 983f2c886f..a6818a5792 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -965,18 +965,6 @@ VariantInputsWidget QToolButton { background: {color:bg-view-selection}; } -#CardViewLegacyItemWidget { - background: {color:publisher:bg-legacy}; - border-radius: 0.2em; - -} -#CardViewLegacyItemWidget:hover { - background: {color:publisher:bg-legacy-hover}; -} -#CardViewLegacyItemWidget[state="selected"] { - background: {color:publisher:bg-legacy-selected}; -} - #ListViewSubsetName[state="invalid"] { color: {color:publisher:error}; } diff --git a/openpype/tools/publisher/widgets/card_view_widgets.py b/openpype/tools/publisher/widgets/card_view_widgets.py index 96802087ee..95fa8cd5d2 100644 --- a/openpype/tools/publisher/widgets/card_view_widgets.py +++ b/openpype/tools/publisher/widgets/card_view_widgets.py @@ -353,19 +353,18 @@ class LegacyItemCardWidget(CardWidget): def __init__(self, item, parent): super(LegacyItemCardWidget, self).__init__(parent) - self.setObjectName("CardViewLegacyItemWidget") self._id = item.id self.identifier = item.identifier self._group_identifier = LEGACY_ITEM_GROUP - icon_widget = PublishPixmapLabel(None, self) + icon_widget = IconValuePixmapLabel("fa.magic", self) icon_widget.setObjectName("FamilyIconLabel") label_widget = QtWidgets.QLabel(item.label, self) icon_layout = QtWidgets.QHBoxLayout() - icon_layout.setContentsMargins(5, 5, 5, 5) + icon_layout.setContentsMargins(10, 5, 5, 5) icon_layout.addWidget(icon_widget) layout = QtWidgets.QHBoxLayout(self) From 7afb2b2e9fea0ca8cc4fd3d48c16069d052c50df Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 17:56:27 +0200 Subject: [PATCH 36/46] change variable to use convertor instead of legacy --- openpype/tools/publisher/constants.py | 4 +- openpype/tools/publisher/control.py | 4 +- .../publisher/widgets/card_view_widgets.py | 74 +++++++++---------- .../publisher/widgets/list_view_widgets.py | 70 +++++++++--------- 4 files changed, 77 insertions(+), 75 deletions(-) diff --git a/openpype/tools/publisher/constants.py b/openpype/tools/publisher/constants.py index e5969160c1..8bea69c812 100644 --- a/openpype/tools/publisher/constants.py +++ b/openpype/tools/publisher/constants.py @@ -6,7 +6,7 @@ CONTEXT_LABEL = "Options" # Not showed anywhere - used as identifier CONTEXT_GROUP = "__ContextGroup__" -LEGACY_ITEM_GROUP = "Incompatible subsets" +CONVERTOR_ITEM_GROUP = "Incompatible subsets" # Allowed symbols for subset name (and variant) # - characters, numbers, unsercore and dash @@ -22,7 +22,7 @@ IS_GROUP_ROLE = QtCore.Qt.UserRole + 3 CREATOR_IDENTIFIER_ROLE = QtCore.Qt.UserRole + 4 FAMILY_ROLE = QtCore.Qt.UserRole + 5 GROUP_ROLE = QtCore.Qt.UserRole + 6 -LEGACY_CONVERTER_IDENTIFIER = QtCore.Qt.UserRole + 7 +CONVERTER_IDENTIFIER_ROLE = QtCore.Qt.UserRole + 7 __all__ = ( diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 9abc53675d..b867bddc9d 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1235,7 +1235,7 @@ class AbstractPublisherController(object): pass @abstractproperty - def legacy_items(self): + def convertor_items(self): pass @abstractmethod @@ -1607,7 +1607,7 @@ class PublisherController(BasePublisherController): return self._create_context.instances_by_id @property - def legacy_items(self): + def convertor_items(self): return self._create_context.legacy_items_by_id @property diff --git a/openpype/tools/publisher/widgets/card_view_widgets.py b/openpype/tools/publisher/widgets/card_view_widgets.py index 95fa8cd5d2..9fd2bf0824 100644 --- a/openpype/tools/publisher/widgets/card_view_widgets.py +++ b/openpype/tools/publisher/widgets/card_view_widgets.py @@ -39,7 +39,7 @@ from ..constants import ( CONTEXT_ID, CONTEXT_LABEL, CONTEXT_GROUP, - LEGACY_ITEM_GROUP, + CONVERTOR_ITEM_GROUP, ) @@ -175,7 +175,7 @@ class BaseGroupWidget(QtWidgets.QWidget): self.selected.emit(instance_id, group_id, selection_type) -class LegacyItemsGroupWidget(BaseGroupWidget): +class ConvertorItemsGroupWidget(BaseGroupWidget): def update_items(self, items_by_id): items_by_label = collections.defaultdict(list) for item in items_by_id.values(): @@ -195,7 +195,7 @@ class LegacyItemsGroupWidget(BaseGroupWidget): widget = self._widgets_by_id[item.id] widget.update_item(item) else: - widget = LegacyItemCardWidget(item, self) + widget = ConvertorItemCardWidget(item, self) widget.selected.connect(self._on_widget_selection) self._widgets_by_id[item.id] = widget self._content_layout.insertWidget(widget_idx, widget) @@ -345,18 +345,18 @@ class ContextCardWidget(CardWidget): self._label_widget = label_widget -class LegacyItemCardWidget(CardWidget): +class ConvertorItemCardWidget(CardWidget): """Card for global context. Is not visually under group widget and is always at the top of card view. """ def __init__(self, item, parent): - super(LegacyItemCardWidget, self).__init__(parent) + super(ConvertorItemCardWidget, self).__init__(parent) self._id = item.id self.identifier = item.identifier - self._group_identifier = LEGACY_ITEM_GROUP + self._group_identifier = CONVERTOR_ITEM_GROUP icon_widget = IconValuePixmapLabel("fa.magic", self) icon_widget.setObjectName("FamilyIconLabel") @@ -556,7 +556,7 @@ class InstanceCardView(AbstractInstanceView): self._content_widget = content_widget self._context_widget = None - self._legacy_items_group = None + self._convertor_items_group = None self._widgets_by_group = {} self._ordered_groups = [] @@ -589,8 +589,8 @@ class InstanceCardView(AbstractInstanceView): ): output.append(self._context_widget) - if self._legacy_items_group is not None: - output.extend(self._legacy_items_group.get_selected_widgets()) + if self._convertor_items_group is not None: + output.extend(self._convertor_items_group.get_selected_widgets()) for group_widget in self._widgets_by_group.values(): for widget in group_widget.get_selected_widgets(): @@ -605,8 +605,8 @@ class InstanceCardView(AbstractInstanceView): ): output.append(CONTEXT_ID) - if self._legacy_items_group is not None: - output.extend(self._legacy_items_group.get_selected_item_ids()) + if self._convertor_items_group is not None: + output.extend(self._convertor_items_group.get_selected_item_ids()) for group_widget in self._widgets_by_group.values(): output.extend(group_widget.get_selected_item_ids()) @@ -617,7 +617,7 @@ class InstanceCardView(AbstractInstanceView): self._make_sure_context_widget_exists() - self._update_legacy_items_group() + self._update_convertor_items_group() # Prepare instances by group and identifiers by group instances_by_group = collections.defaultdict(list) @@ -648,7 +648,7 @@ class InstanceCardView(AbstractInstanceView): # Keep track of widget indexes # - we start with 1 because Context item as at the top widget_idx = 1 - if self._legacy_items_group is not None: + if self._convertor_items_group is not None: widget_idx += 1 for group_name in sorted_group_names: @@ -702,27 +702,27 @@ class InstanceCardView(AbstractInstanceView): self.selection_changed.emit() self._content_layout.insertWidget(0, widget) - def _update_legacy_items_group(self): - legacy_items = self._controller.legacy_items - if not legacy_items and self._legacy_items_group is None: + def _update_convertor_items_group(self): + convertor_items = self._controller.convertor_items + if not convertor_items and self._convertor_items_group is None: return - if not legacy_items: - self._legacy_items_group.setVisible(False) - self._content_layout.removeWidget(self._legacy_items_group) - self._legacy_items_group.deleteLater() - self._legacy_items_group = None + if not convertor_items: + self._convertor_items_group.setVisible(False) + self._content_layout.removeWidget(self._convertor_items_group) + self._convertor_items_group.deleteLater() + self._convertor_items_group = None return - if self._legacy_items_group is None: - group_widget = LegacyItemsGroupWidget( - LEGACY_ITEM_GROUP, self._content_widget + if self._convertor_items_group is None: + group_widget = ConvertorItemsGroupWidget( + CONVERTOR_ITEM_GROUP, self._content_widget ) group_widget.selected.connect(self._on_widget_selection) self._content_layout.insertWidget(1, group_widget) - self._legacy_items_group = group_widget + self._convertor_items_group = group_widget - self._legacy_items_group.update_items(legacy_items) + self._convertor_items_group.update_items(convertor_items) def refresh_instance_states(self): """Trigger update of instances on group widgets.""" @@ -742,8 +742,8 @@ class InstanceCardView(AbstractInstanceView): new_widget = self._context_widget else: - if group_name == LEGACY_ITEM_GROUP: - group_widget = self._legacy_items_group + if group_name == CONVERTOR_ITEM_GROUP: + group_widget = self._convertor_items_group else: group_widget = self._widgets_by_group[group_name] new_widget = group_widget.get_widget_by_item_id(instance_id) @@ -791,8 +791,8 @@ class InstanceCardView(AbstractInstanceView): if instance_id == CONTEXT_ID: remove_group = True else: - if group_name == LEGACY_ITEM_GROUP: - group_widget = self._legacy_items_group + if group_name == CONVERTOR_ITEM_GROUP: + group_widget = self._convertor_items_group else: group_widget = self._widgets_by_group[group_name] if not group_widget.get_selected_widgets(): @@ -906,8 +906,8 @@ class InstanceCardView(AbstractInstanceView): if name == CONTEXT_GROUP: sorted_widgets = [self._context_widget] else: - if name == LEGACY_ITEM_GROUP: - group_widget = self._legacy_items_group + if name == CONVERTOR_ITEM_GROUP: + group_widget = self._convertor_items_group else: group_widget = self._widgets_by_group[name] sorted_widgets = group_widget.get_ordered_widgets() @@ -1034,7 +1034,7 @@ class InstanceCardView(AbstractInstanceView): elif isinstance(widget, InstanceCardWidget): instances.append(widget.id) - elif isinstance(widget, LegacyItemCardWidget): + elif isinstance(widget, ConvertorItemCardWidget): convertor_identifiers.append(widget.identifier) return instances, context_selected, convertor_identifiers @@ -1066,16 +1066,16 @@ class InstanceCardView(AbstractInstanceView): if group_name == CONTEXT_GROUP: continue - legacy_group = group_name == LEGACY_ITEM_GROUP - if legacy_group: - group_widget = self._legacy_items_group + is_convertor_group = group_name == CONVERTOR_ITEM_GROUP + if is_convertor_group: + group_widget = self._convertor_items_group else: group_widget = self._widgets_by_group[group_name] group_selected = False for widget in group_widget.get_ordered_widgets(): select = False - if legacy_group: + if is_convertor_group: is_in = widget.identifier in s_convertor_identifiers else: is_in = widget.id in s_instance_ids diff --git a/openpype/tools/publisher/widgets/list_view_widgets.py b/openpype/tools/publisher/widgets/list_view_widgets.py index 53951e3cba..32d84862f0 100644 --- a/openpype/tools/publisher/widgets/list_view_widgets.py +++ b/openpype/tools/publisher/widgets/list_view_widgets.py @@ -37,8 +37,8 @@ from ..constants import ( CONTEXT_ID, CONTEXT_LABEL, GROUP_ROLE, - LEGACY_CONVERTER_IDENTIFIER, - LEGACY_ITEM_GROUP, + CONVERTER_IDENTIFIER_ROLE, + CONVERTOR_ITEM_GROUP, ) @@ -333,7 +333,7 @@ class InstanceTreeView(QtWidgets.QTreeView): """Ids of selected instances.""" instance_ids = set() for index in self.selectionModel().selectedIndexes(): - if index.data(LEGACY_CONVERTER_IDENTIFIER) is not None: + if index.data(CONVERTER_IDENTIFIER_ROLE) is not None: continue instance_id = index.data(INSTANCE_ID_ROLE) @@ -450,9 +450,9 @@ class InstanceListView(AbstractInstanceView): self._context_item = None self._context_widget = None - self._legacy_group_item = None - self._legacy_group_widget = None - self._legacy_items_by_id = {} + self._convertor_group_item = None + self._convertor_group_widget = None + self._convertor_items_by_id = {} self._instance_view = instance_view self._instance_delegate = instance_delegate @@ -467,8 +467,8 @@ class InstanceListView(AbstractInstanceView): def _update_widget_expand_state(self, index, expanded): group_name = index.data(GROUP_ROLE) - if group_name == LEGACY_ITEM_GROUP: - group_widget = self._legacy_group_widget + if group_name == CONVERTOR_ITEM_GROUP: + group_widget = self._convertor_group_widget else: group_widget = self._group_widgets.get(group_name) @@ -540,7 +540,7 @@ class InstanceListView(AbstractInstanceView): if self._make_sure_context_item_exists(): sort_at_the_end = True - self._update_legacy_items_group() + self._update_convertor_items_group() # Prepare instances by their groups instances_by_group_name = collections.defaultdict(list) @@ -702,25 +702,25 @@ class InstanceListView(AbstractInstanceView): self._context_item = context_item return True - def _update_legacy_items_group(self): + def _update_convertor_items_group(self): created_new_items = False - legacy_items_by_id = self._controller.legacy_items - group_item = self._legacy_group_item - if not legacy_items_by_id and group_item is None: + convertor_items_by_id = self._controller.convertor_items + group_item = self._convertor_group_item + if not convertor_items_by_id and group_item is None: return created_new_items root_item = self._instance_model.invisibleRootItem() - if not legacy_items_by_id: + if not convertor_items_by_id: root_item.removeRow(group_item.row()) - self._legacy_group_widget.deleteLater() - self._legacy_group_widget = None - self._legacy_items_by_id = {} + self._convertor_group_widget.deleteLater() + self._convertor_group_widget = None + self._convertor_items_by_id = {} return created_new_items if group_item is None: created_new_items = True group_item = QtGui.QStandardItem() - group_item.setData(LEGACY_ITEM_GROUP, GROUP_ROLE) + group_item.setData(CONVERTOR_ITEM_GROUP, GROUP_ROLE) group_item.setData(1, SORT_VALUE_ROLE) group_item.setData(True, IS_GROUP_ROLE) group_item.setFlags(QtCore.Qt.ItemIsEnabled) @@ -732,36 +732,38 @@ class InstanceListView(AbstractInstanceView): ) proxy_index = self._proxy_model.mapFromSource(index) widget = InstanceListGroupWidget( - LEGACY_ITEM_GROUP, self._instance_view + CONVERTOR_ITEM_GROUP, self._instance_view ) widget.toggle_checkbox.setVisible(False) - widget.expand_changed.connect(self._on_legacy_group_expand_request) + widget.expand_changed.connect( + self._on_convertor_group_expand_request + ) self._instance_view.setIndexWidget(proxy_index, widget) - self._legacy_group_item = group_item - self._legacy_group_widget = widget + self._convertor_group_item = group_item + self._convertor_group_widget = widget for row in reversed(range(group_item.rowCount())): child_item = group_item.child(row) - child_identifier = child_item.data(LEGACY_CONVERTER_IDENTIFIER) - if child_identifier not in legacy_items_by_id: - self._legacy_items_by_id.pop(child_identifier, None) + child_identifier = child_item.data(CONVERTER_IDENTIFIER_ROLE) + if child_identifier not in convertor_items_by_id: + self._convertor_items_by_id.pop(child_identifier, None) group_item.removeRows(row, 1) new_items = [] - for identifier, convertor_item in legacy_items_by_id.items(): - item = self._legacy_items_by_id.get(identifier) + for identifier, convertor_item in convertor_items_by_id.items(): + item = self._convertor_items_by_id.get(identifier) if item is None: created_new_items = True item = QtGui.QStandardItem(convertor_item.label) new_items.append(item) item.setData(convertor_item.id, INSTANCE_ID_ROLE) item.setData(convertor_item.label, SORT_VALUE_ROLE) - item.setData(LEGACY_ITEM_GROUP, GROUP_ROLE) + item.setData(CONVERTOR_ITEM_GROUP, GROUP_ROLE) item.setData( - convertor_item.identifier, LEGACY_CONVERTER_IDENTIFIER + convertor_item.identifier, CONVERTER_IDENTIFIER_ROLE ) - self._legacy_items_by_id[identifier] = item + self._convertor_items_by_id[identifier] = item if new_items: group_item.appendRows(new_items) @@ -874,8 +876,8 @@ class InstanceListView(AbstractInstanceView): proxy_index = self._proxy_model.mapFromSource(group_index) self._instance_view.setExpanded(proxy_index, expanded) - def _on_legacy_group_expand_request(self, _, expanded): - group_item = self._legacy_group_item + def _on_convertor_group_expand_request(self, _, expanded): + group_item = self._convertor_group_item if not group_item: return group_index = self._instance_model.index( @@ -923,7 +925,7 @@ class InstanceListView(AbstractInstanceView): context_selected = False for index in self._instance_view.selectionModel().selectedIndexes(): - convertor_identifier = index.data(LEGACY_CONVERTER_IDENTIFIER) + convertor_identifier = index.data(CONVERTER_IDENTIFIER_ROLE) if convertor_identifier is not None: convertor_identifiers.append(convertor_identifier) continue @@ -974,7 +976,7 @@ class InstanceListView(AbstractInstanceView): (item.child(row), list(new_parent_items)) ) - convertor_identifier = item.data(LEGACY_CONVERTER_IDENTIFIER) + convertor_identifier = item.data(CONVERTER_IDENTIFIER_ROLE) select = False expand_parent = True From be54ff4d27978079855c99e5f8f9f1d188742b53 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 18:00:26 +0200 Subject: [PATCH 37/46] rename 'convert_legacy_items' to 'trigger_convertor_items' --- openpype/tools/publisher/control.py | 4 ++-- openpype/tools/publisher/widgets/overview_widget.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index b867bddc9d..245d328be4 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1239,7 +1239,7 @@ class AbstractPublisherController(object): pass @abstractmethod - def convert_legacy_items(self, convertor_identifiers): + def trigger_convertor_items(self, convertor_identifiers): pass @abstractmethod @@ -1854,7 +1854,7 @@ class PublisherController(BasePublisherController): variant, task_name, asset_doc, project_name, instance=instance ) - def convert_legacy_items(self, convertor_identifiers): + def trigger_convertor_items(self, convertor_identifiers): for convertor_identifier in convertor_identifiers: self._create_context.run_convertor(convertor_identifier) self._on_create_instance_change() diff --git a/openpype/tools/publisher/widgets/overview_widget.py b/openpype/tools/publisher/widgets/overview_widget.py index e208786fc7..7c1755b3eb 100644 --- a/openpype/tools/publisher/widgets/overview_widget.py +++ b/openpype/tools/publisher/widgets/overview_widget.py @@ -321,7 +321,7 @@ class OverviewWidget(QtWidgets.QFrame): def _on_convert_requested(self): _, _, convertor_identifiers = self.get_selected_items() - self._controller.convert_legacy_items(convertor_identifiers) + self._controller.trigger_convertor_items(convertor_identifiers) def get_selected_items(self): view = self._subset_views_layout.currentWidget() From 81f7aa5525e52f229cf4ec340f8a125358d0afeb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 18:15:24 +0200 Subject: [PATCH 38/46] get rid of 'legacy' from variables --- openpype/pipeline/create/context.py | 44 ++++++++++----------- openpype/pipeline/create/creator_plugins.py | 33 ++++++++-------- openpype/tools/publisher/control.py | 4 +- 3 files changed, 41 insertions(+), 40 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 250193f511..56d7447a0b 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -22,7 +22,7 @@ from .creator_plugins import ( Creator, AutoCreator, discover_creator_plugins, - discover_legacy_convertor_plugins, + discover_convertor_plugins, ) UpdateData = collections.namedtuple("UpdateData", ["instance", "changes"]) @@ -853,8 +853,8 @@ class CreatedInstance: self[key] = new_value -class LegacyInstancesItem(object): - """Item representing convertor for legacy instances. +class ConvertorItem(object): + """Item representing convertor plugin. Args: identifier (str): Identifier of convertor. @@ -949,8 +949,8 @@ class CreateContext: # Manual creators self.manual_creators = {} - self.legacy_convertors = {} - self.legacy_items_by_id = {} + self.convertors_plugins = {} + self.convertor_items_by_id = {} self.publish_discover_result = None self.publish_plugins_mismatch_targets = [] @@ -1032,7 +1032,7 @@ class CreateContext: with self.bulk_instances_collection(): self.reset_instances() - self.find_legacy_items() + self.find_convertor_items() self.execute_autocreators() self.reset_finalization() @@ -1090,7 +1090,7 @@ class CreateContext: self._reset_publish_plugins(discover_publish_plugins) self._reset_creator_plugins() - self._reset_legacy_convertor_plugins() + self._reset_convertor_plugins() def _reset_publish_plugins(self, discover_publish_plugins): import pyblish.logic @@ -1186,9 +1186,9 @@ class CreateContext: self.creators = creators - def _reset_legacy_convertor_plugins(self): - legacy_convertors = {} - for convertor_class in discover_legacy_convertor_plugins(): + def _reset_convertor_plugins(self): + convertors_plugins = {} + for convertor_class in discover_convertor_plugins(): if inspect.isabstract(convertor_class): self.log.info( "Skipping abstract Creator {}".format(str(convertor_class)) @@ -1196,16 +1196,16 @@ class CreateContext: continue convertor_identifier = convertor_class.identifier - if convertor_identifier in legacy_convertors: + if convertor_identifier in convertors_plugins: self.log.warning(( "Duplicated Converter identifier. " "Using first and skipping following" )) continue - legacy_convertors[convertor_identifier] = convertor_class(self) + convertors_plugins[convertor_identifier] = convertor_class(self) - self.legacy_convertors = legacy_convertors + self.convertors_plugins = convertors_plugins def reset_context_data(self): """Reload context data using host implementation. @@ -1278,13 +1278,13 @@ class CreateContext: def creator_removed_instance(self, instance): self._instances_by_id.pop(instance.id, None) - def add_legacy_item(self, convertor_identifier, label): - self.legacy_items_by_id[convertor_identifier] = ( - LegacyInstancesItem(convertor_identifier, label) + def add_convertor_item(self, convertor_identifier, label): + self.convertor_items_by_id[convertor_identifier] = ConvertorItem( + convertor_identifier, label ) - def remove_legacy_item(self, convertor_identifier): - self.legacy_items_by_id.pop(convertor_identifier, None) + def remove_convertor_item(self, convertor_identifier): + self.convertor_items_by_id.pop(convertor_identifier, None) @contextmanager def bulk_instances_collection(self): @@ -1321,10 +1321,10 @@ class CreateContext: for creator in self.creators.values(): creator.collect_instances() - def find_legacy_items(self): - self.legacy_items_by_id = {} + def find_convertor_items(self): + self.convertor_items_by_id = {} - for convertor in self.legacy_convertors.values(): + for convertor in self.convertors_plugins.values(): try: convertor.find_instances() except: @@ -1502,6 +1502,6 @@ class CreateContext: return self._collection_shared_data def run_convertor(self, convertor_identifier): - convertor = self.legacy_convertors.get(convertor_identifier) + convertor = self.convertors_plugins.get(convertor_identifier) if convertor is not None: convertor.convert() diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index ff9326693e..2e7d8709a2 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -34,7 +34,7 @@ class CreatorError(Exception): @six.add_metaclass(ABCMeta) -class LegacyInstanceConvertor(object): +class LegacySubsetConvertor(object): """Helper for conversion of instances created using legacy creators. Conversion from legacy creators would mean to loose legacy instances, @@ -45,10 +45,10 @@ class LegacyInstanceConvertor(object): Convertor logic should be very simple. Method 'find_instances' is to look for legacy instances in scene a possibly call - pre-implemented 'add_legacy_item'. + pre-implemented 'add_convertor_item'. User will have ability to trigger conversion which is executed by calling - 'convert' which should call 'remove_legacy_item' when is done. + 'convert' which should call 'remove_convertor_item' when is done. It does make sense to add only one or none legacy item to create context for convertor as it's not possible to choose which instace are converted @@ -78,7 +78,8 @@ class LegacyInstanceConvertor(object): def find_instances(self): """Look for legacy instances in the scene. - Should call 'add_legacy_item' if there is at least one item. + Should call 'add_convertor_item' if there is at least one instance to + convert. """ pass @@ -108,19 +109,19 @@ class LegacyInstanceConvertor(object): return self._create_context.collection_shared_data - def add_legacy_item(self, label): + def add_convertor_item(self, label): """Add item to CreateContext. Args: label (str): Label of item which will show in UI. """ - self._create_context.add_legacy_item(self.identifier, label) + self._create_context.add_convertor_item(self.identifier, label) - def remove_legacy_item(self): + def remove_convertor_item(self): """Remove legacy item from create context when conversion finished.""" - self._create_context.remove_legacy_item(self.identifier) + self._create_context.remove_convertor_item(self.identifier) @six.add_metaclass(ABCMeta) @@ -559,8 +560,8 @@ def discover_creator_plugins(): return discover(BaseCreator) -def discover_legacy_convertor_plugins(): - return discover(LegacyInstanceConvertor) +def discover_convertor_plugins(): + return discover(LegacySubsetConvertor) def discover_legacy_creator_plugins(): @@ -620,8 +621,8 @@ def register_creator_plugin(plugin): elif issubclass(plugin, LegacyCreator): register_plugin(LegacyCreator, plugin) - elif issubclass(plugin, LegacyInstanceConvertor): - register_plugin(LegacyInstanceConvertor, plugin) + elif issubclass(plugin, LegacySubsetConvertor): + register_plugin(LegacySubsetConvertor, plugin) def deregister_creator_plugin(plugin): @@ -631,17 +632,17 @@ def deregister_creator_plugin(plugin): elif issubclass(plugin, LegacyCreator): deregister_plugin(LegacyCreator, plugin) - elif issubclass(plugin, LegacyInstanceConvertor): - deregister_plugin(LegacyInstanceConvertor, plugin) + elif issubclass(plugin, LegacySubsetConvertor): + deregister_plugin(LegacySubsetConvertor, plugin) def register_creator_plugin_path(path): register_plugin_path(BaseCreator, path) register_plugin_path(LegacyCreator, path) - register_plugin_path(LegacyInstanceConvertor, path) + register_plugin_path(LegacySubsetConvertor, path) def deregister_creator_plugin_path(path): deregister_plugin_path(BaseCreator, path) deregister_plugin_path(LegacyCreator, path) - deregister_plugin_path(LegacyInstanceConvertor, path) + deregister_plugin_path(LegacySubsetConvertor, path) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 245d328be4..107ddbbb93 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1608,7 +1608,7 @@ class PublisherController(BasePublisherController): @property def convertor_items(self): - return self._create_context.legacy_items_by_id + return self._create_context.convertor_items_by_id @property def _creators(self): @@ -1728,7 +1728,7 @@ class PublisherController(BasePublisherController): self._create_context.reset_context_data() with self._create_context.bulk_instances_collection(): self._create_context.reset_instances() - self._create_context.find_legacy_items() + self._create_context.find_convertor_items() self._create_context.execute_autocreators() self._resetting_instances = False From 4f70a58d5c7e9c604c1d6dabbeb80c4b74ab83b3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 18:17:27 +0200 Subject: [PATCH 39/46] renamed 'LegacySubsetConvertor' to 'SubsetConvertorPlugin' --- openpype/pipeline/create/creator_plugins.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 2e7d8709a2..584e082221 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -34,7 +34,7 @@ class CreatorError(Exception): @six.add_metaclass(ABCMeta) -class LegacySubsetConvertor(object): +class SubsetConvertorPlugin(object): """Helper for conversion of instances created using legacy creators. Conversion from legacy creators would mean to loose legacy instances, @@ -561,7 +561,7 @@ def discover_creator_plugins(): def discover_convertor_plugins(): - return discover(LegacySubsetConvertor) + return discover(SubsetConvertorPlugin) def discover_legacy_creator_plugins(): @@ -621,8 +621,8 @@ def register_creator_plugin(plugin): elif issubclass(plugin, LegacyCreator): register_plugin(LegacyCreator, plugin) - elif issubclass(plugin, LegacySubsetConvertor): - register_plugin(LegacySubsetConvertor, plugin) + elif issubclass(plugin, SubsetConvertorPlugin): + register_plugin(SubsetConvertorPlugin, plugin) def deregister_creator_plugin(plugin): @@ -632,17 +632,17 @@ def deregister_creator_plugin(plugin): elif issubclass(plugin, LegacyCreator): deregister_plugin(LegacyCreator, plugin) - elif issubclass(plugin, LegacySubsetConvertor): - deregister_plugin(LegacySubsetConvertor, plugin) + elif issubclass(plugin, SubsetConvertorPlugin): + deregister_plugin(SubsetConvertorPlugin, plugin) def register_creator_plugin_path(path): register_plugin_path(BaseCreator, path) register_plugin_path(LegacyCreator, path) - register_plugin_path(LegacySubsetConvertor, path) + register_plugin_path(SubsetConvertorPlugin, path) def deregister_creator_plugin_path(path): deregister_plugin_path(BaseCreator, path) deregister_plugin_path(LegacyCreator, path) - deregister_plugin_path(LegacySubsetConvertor, path) + deregister_plugin_path(SubsetConvertorPlugin, path) From 87671bcfd6905e7e1bf729c6aa0fef42f47d6d9c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 19:16:54 +0200 Subject: [PATCH 40/46] added style for errored card message --- openpype/style/data.json | 4 +++- openpype/style/style.css | 21 +++++++++++---------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/openpype/style/data.json b/openpype/style/data.json index fef69071ed..146af84663 100644 --- a/openpype/style/data.json +++ b/openpype/style/data.json @@ -64,7 +64,9 @@ "overlay-messages": { "close-btn": "#D3D8DE", "bg-success": "#458056", - "bg-success-hover": "#55a066" + "bg-success-hover": "#55a066", + "bg-error": "#AD2E2E", + "bg-error-hover": "#C93636" }, "tab-widget": { "bg": "#21252B", diff --git a/openpype/style/style.css b/openpype/style/style.css index a6818a5792..9919973b06 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -688,22 +688,23 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { } /* Messages overlay */ -#OverlayMessageWidget { +OverlayMessageWidget { border-radius: 0.2em; - background: {color:bg-buttons}; -} - -#OverlayMessageWidget:hover { - background: {color:bg-button-hover}; -} -#OverlayMessageWidget { background: {color:overlay-messages:bg-success}; } -#OverlayMessageWidget:hover { + +OverlayMessageWidget:hover { background: {color:overlay-messages:bg-success-hover}; } -#OverlayMessageWidget QWidget { +OverlayMessageWidget[type="error"] { + background: {color:overlay-messages:bg-error}; +} +OverlayMessageWidget[type="error"]:hover { + background: {color:overlay-messages:bg-error-hover}; +} + +OverlayMessageWidget QWidget { background: transparent; } From 0fd54454192ffec16170b1cca574825955f7397f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 19:25:35 +0200 Subject: [PATCH 41/46] wrap convertor callbacks by custom exceptions --- openpype/pipeline/create/context.py | 94 +++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index b6dce4c03d..c87803c5c4 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -71,6 +71,41 @@ class HostMissRequiredMethod(Exception): super(HostMissRequiredMethod, self).__init__(msg) +class ConvertorsOperationFailed(Exception): + def __init__(self, msg, failed_info): + super(ConvertorsOperationFailed, self).__init__(msg) + self.failed_info = failed_info + + +class ConvertorsFindFailed(ConvertorsOperationFailed): + def __init__(self, failed_info): + msg = "Failed to find incompatible subsets" + super(ConvertorsFindFailed, self).__init__( + msg, failed_info + ) + + +class ConvertorsConversionFailed(ConvertorsOperationFailed): + def __init__(self, failed_info): + msg = "Failed to convert incompatible subsets" + super(ConvertorsConversionFailed, self).__init__( + msg, failed_info + ) + + +def prepare_failed_convertor_operation_info(identifier, exc_info): + exc_type, exc_value, exc_traceback = exc_info + formatted_traceback = "".join(traceback.format_exception( + exc_type, exc_value, exc_traceback + )) + + return { + "convertor_identifier": identifier, + "message": str(exc_value), + "traceback": formatted_traceback + } + + class CreatorsOperationFailed(Exception): """Raised when a creator process crashes in 'CreateContext'. @@ -1486,12 +1521,26 @@ class CreateContext: raise CreatorsCollectionFailed(failed_info) def find_convertor_items(self): + """Go through convertor plugins to look for items to convert. + + Raises: + ConvertorsFindFailed: When one or more convertors fails during + finding. + """ + self.convertor_items_by_id = {} + failed_info = [] for convertor in self.convertors_plugins.values(): try: convertor.find_instances() + except: + failed_info.append( + prepare_failed_convertor_operation_info( + convertor.identifier, sys.exc_info() + ) + ) self.log.warning( "Failed to find instances of convertor \"{}\"".format( convertor.identifier @@ -1499,6 +1548,9 @@ class CreateContext: exc_info=True ) + if failed_info: + raise ConvertorsFindFailed(failed_info) + def execute_autocreators(self): """Execute discovered AutoCreator plugins. @@ -1756,6 +1808,48 @@ class CreateContext: return self._collection_shared_data def run_convertor(self, convertor_identifier): + """Run convertor plugin by it's idenfitifier. + + Conversion is skipped if convertor is not available. + + Args: + convertor_identifier (str): Identifier of convertor. + """ + convertor = self.convertors_plugins.get(convertor_identifier) if convertor is not None: convertor.convert() + + def run_convertors(self, convertor_identifiers): + """Run convertor plugins by idenfitifiers. + + Conversion is skipped if convertor is not available. + + Args: + convertor_identifiers (Iterator[str]): Identifiers of convertors + to run. + + Raises: + ConvertorsConversionFailed: When one or more convertors fails. + """ + + failed_info = [] + for convertor_identifier in convertor_identifiers: + try: + self.run_convertor(convertor_identifier) + + except: + failed_info.append( + prepare_failed_convertor_operation_info( + convertor_identifier, sys.exc_info() + ) + ) + self.log.warning( + "Failed to convert instances of convertor \"{}\"".format( + convertor_identifier + ), + exc_info=True + ) + + if failed_info: + raise ConvertorsConversionFailed(failed_info) From 9774c507f20623697dbeae1de747ca99d990fded Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 19:26:27 +0200 Subject: [PATCH 42/46] Error message box is less creator's specific --- openpype/tools/publisher/window.py | 105 ++++++++++++++++------------- 1 file changed, 57 insertions(+), 48 deletions(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index b6bd506c18..58c73f4821 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -1,4 +1,5 @@ import collections +import copy from Qt import QtWidgets, QtCore, QtGui from openpype import ( @@ -224,10 +225,10 @@ class PublisherWindow(QtWidgets.QDialog): # Floating publish frame publish_frame = PublishFrame(controller, self.footer_border, self) - creators_dialog_message_timer = QtCore.QTimer() - creators_dialog_message_timer.setInterval(100) - creators_dialog_message_timer.timeout.connect( - self._on_creators_message_timeout + errors_dialog_message_timer = QtCore.QTimer() + errors_dialog_message_timer.setInterval(100) + errors_dialog_message_timer.timeout.connect( + self._on_errors_message_timeout ) help_btn.clicked.connect(self._on_help_click) @@ -268,16 +269,16 @@ class PublisherWindow(QtWidgets.QDialog): "show.card.message", self._on_overlay_message ) controller.event_system.add_callback( - "instances.collection.failed", self._instance_collection_failed + "instances.collection.failed", self._on_creator_error ) controller.event_system.add_callback( - "instances.save.failed", self._instance_save_failed + "instances.save.failed", self._on_creator_error ) controller.event_system.add_callback( - "instances.remove.failed", self._instance_remove_failed + "instances.remove.failed", self._on_creator_error ) controller.event_system.add_callback( - "instances.create.failed", self._instance_create_failed + "instances.create.failed", self._on_creator_error ) # Store extra header widget for TrayPublisher @@ -325,8 +326,8 @@ class PublisherWindow(QtWidgets.QDialog): self._restart_timer = None self._publish_frame_visible = None - self._creators_messages_to_show = collections.deque() - self._creators_dialog_message_timer = creators_dialog_message_timer + self._error_messages_to_show = collections.deque() + self._errors_dialog_message_timer = errors_dialog_message_timer self._set_publish_visibility(False) @@ -357,7 +358,10 @@ class PublisherWindow(QtWidgets.QDialog): self._update_publish_frame_rect() def _on_overlay_message(self, event): - self._overlay_object.add_message(event["message"]) + self._overlay_object.add_message( + event["message"], + event.get("message_type") + ) def _on_first_show(self): self.resize(self.default_width, self.default_height) @@ -604,37 +608,39 @@ class PublisherWindow(QtWidgets.QDialog): 0, window_size.height() - height ) - def add_message_dialog(self, title, failed_info): - self._creators_messages_to_show.append((title, failed_info)) - self._creators_dialog_message_timer.start() + def add_error_message_dialog(self, title, failed_info, message_start=None): + self._error_messages_to_show.append( + (title, failed_info, message_start) + ) + self._errors_dialog_message_timer.start() - def _on_creators_message_timeout(self): - if not self._creators_messages_to_show: - self._creators_dialog_message_timer.stop() + def _on_errors_message_timeout(self): + if not self._error_messages_to_show: + self._errors_dialog_message_timer.stop() return - item = self._creators_messages_to_show.popleft() - title, failed_info = item - dialog = CreatorsErrorMessageBox(title, failed_info, self) + item = self._error_messages_to_show.popleft() + title, failed_info, message_start = item + dialog = ErrorsMessageBox( + title, failed_info, message_start, self + ) dialog.exec_() dialog.deleteLater() - def _instance_collection_failed(self, event): - self.add_message_dialog(event["title"], event["failed_info"]) - - def _instance_save_failed(self, event): - self.add_message_dialog(event["title"], event["failed_info"]) - - def _instance_remove_failed(self, event): - self.add_message_dialog(event["title"], event["failed_info"]) - - def _instance_create_failed(self, event): - self.add_message_dialog(event["title"], event["failed_info"]) + def _on_creator_error(self, event): + new_failed_info = [] + for item in event["failed_info"]: + new_item = copy.deepcopy(item) + new_item["label"] = new_item.pop("creator_label") + new_item["identifier"] = new_item.pop("creator_identifier") + new_failed_info.append(new_item) + self.add_error_message_dialog(event["title"], new_failed_info, "Creator:") -class CreatorsErrorMessageBox(ErrorMessageBox): - def __init__(self, error_title, failed_info, parent): +class ErrorsMessageBox(ErrorMessageBox): + def __init__(self, error_title, failed_info, message_start, parent): self._failed_info = failed_info + self._message_start = message_start self._info_with_id = [ # Id must be string when used in tab widget {"id": str(idx), "info": info} @@ -644,7 +650,7 @@ class CreatorsErrorMessageBox(ErrorMessageBox): self._tabs_widget = None self._stack_layout = None - super(CreatorsErrorMessageBox, self).__init__(error_title, parent) + super(ErrorsMessageBox, self).__init__(error_title, parent) layout = self.layout() layout.setContentsMargins(0, 0, 0, 0) @@ -659,17 +665,21 @@ class CreatorsErrorMessageBox(ErrorMessageBox): def _get_report_data(self): output = [] for info in self._failed_info: - creator_label = info["creator_label"] - creator_identifier = info["creator_identifier"] - report_message = "Creator:" - if creator_label: - report_message += " {} ({})".format( - creator_label, creator_identifier) + item_label = info.get("label") + item_identifier = info["identifier"] + if item_label: + report_message = "{} ({})".format( + item_label, item_identifier) else: - report_message += " {}".format(creator_identifier) + report_message = "{}".format(item_identifier) + + if self._message_start: + report_message = "{} {}".format( + self._message_start, report_message + ) report_message += "\n\nError: {}".format(info["message"]) - formatted_traceback = info["traceback"] + formatted_traceback = info.get("traceback") if formatted_traceback: report_message += "\n\n{}".format(formatted_traceback) output.append(report_message) @@ -686,11 +696,10 @@ class CreatorsErrorMessageBox(ErrorMessageBox): item_id = item["id"] info = item["info"] message = info["message"] - formatted_traceback = info["traceback"] - creator_label = info["creator_label"] - creator_identifier = info["creator_identifier"] - if not creator_label: - creator_label = creator_identifier + formatted_traceback = info.get("traceback") + item_label = info.get("label") + if not item_label: + item_label = info["identifier"] msg_widget = QtWidgets.QWidget(stack_widget) msg_layout = QtWidgets.QVBoxLayout(msg_widget) @@ -710,7 +719,7 @@ class CreatorsErrorMessageBox(ErrorMessageBox): msg_layout.addStretch(1) - tabs_widget.add_tab(creator_label, item_id) + tabs_widget.add_tab(item_label, item_id) stack_layout.addWidget(msg_widget) if first: first = False From 3ab3582b0a260bb9008a4138e8c3edc1c8f67ac1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 19:26:48 +0200 Subject: [PATCH 43/46] prepare to handle convertor errors --- openpype/tools/publisher/window.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 58c73f4821..a3387043b8 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -280,6 +280,12 @@ class PublisherWindow(QtWidgets.QDialog): controller.event_system.add_callback( "instances.create.failed", self._on_creator_error ) + controller.event_system.add_callback( + "convertors.convert.failed", self._on_convertor_error + ) + controller.event_system.add_callback( + "convertors.find.failed", self._on_convertor_error + ) # Store extra header widget for TrayPublisher # - can be used to add additional widgets to header between context @@ -636,6 +642,16 @@ class PublisherWindow(QtWidgets.QDialog): new_failed_info.append(new_item) self.add_error_message_dialog(event["title"], new_failed_info, "Creator:") + def _on_convertor_error(self, event): + new_failed_info = [] + for item in event["failed_info"]: + new_item = copy.deepcopy(item) + new_item["identifier"] = new_item.pop("convertor_identifier") + new_failed_info.append(new_item) + self.add_error_message_dialog( + event["title"], new_failed_info, "Convertor:" + ) + class ErrorsMessageBox(ErrorMessageBox): def __init__(self, error_title, failed_info, message_start, parent): From f9a75ea240e1c1c9c5e9213dbbb32d4cbf354067 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 19:27:21 +0200 Subject: [PATCH 44/46] handle ConvertorsOperationFailed in controller --- openpype/tools/publisher/control.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 482227e708..7cfc89f59e 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -33,6 +33,7 @@ from openpype.pipeline.create import ( ) from openpype.pipeline.create.context import ( CreatorsOperationFailed, + ConvertorsOperationFailed, ) # Define constant for plugin orders offset @@ -1743,7 +1744,16 @@ class PublisherController(BasePublisherController): } ) - self._create_context.find_convertor_items() + try: + self._create_context.find_convertor_items() + except ConvertorsOperationFailed as exc: + self._emit_event( + "convertors.find.failed", + { + "title": "Collection of unsupported subset failed", + "failed_info": exc.failed_info + } + ) try: self._create_context.execute_autocreators() @@ -1881,8 +1891,19 @@ class PublisherController(BasePublisherController): ) def trigger_convertor_items(self, convertor_identifiers): - for convertor_identifier in convertor_identifiers: - self._create_context.run_convertor(convertor_identifier) + success = True + try: + self._create_context.run_convertors(convertor_identifiers) + + except ConvertorsOperationFailed as exc: + success = False + self._emit_event( + "convertors.convert.failed", + { + "title": "Conversion failed", + "failed_info": exc.failed_info + } + ) self._on_create_instance_change() self.emit_card_message("Conversion finished") From 22a1191ab1a4e8ce516aef216e18f0f5a0817c68 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 19:27:34 +0200 Subject: [PATCH 45/46] emit card message can accept message types --- openpype/tools/publisher/control.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 7cfc89f59e..d4dddb75d5 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1264,7 +1264,7 @@ class AbstractPublisherController(object): pass @abstractmethod - def emit_card_message(self, message): + def emit_card_message(self, message, message_type=None): """Emit a card message which can have a lifetime. This is for UI purposes. Method can be extended to more arguments @@ -1771,8 +1771,14 @@ class PublisherController(BasePublisherController): self._on_create_instance_change() - def emit_card_message(self, message): - self._emit_event("show.card.message", {"message": message}) + def emit_card_message(self, message, message_type=None): + self._emit_event( + "show.card.message", + { + "message": message, + "message_type": message_type + } + ) def get_creator_attribute_definitions(self, instances): """Collect creator attribute definitions for multuple instances. From 12a272a8eec1c63bc2aece3c5a9acbb56cee0867 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 19:32:06 +0200 Subject: [PATCH 46/46] added different types of card messages --- openpype/tools/publisher/control.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index d4dddb75d5..18d1a5b083 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -40,6 +40,11 @@ from openpype.pipeline.create.context import ( PLUGIN_ORDER_OFFSET = 0.5 +class CardMessageTypes: + standard = None + error = "error" + + class MainThreadItem: """Callback with args and kwargs.""" @@ -1264,7 +1269,9 @@ class AbstractPublisherController(object): pass @abstractmethod - def emit_card_message(self, message, message_type=None): + def emit_card_message( + self, message, message_type=CardMessageTypes.standard + ): """Emit a card message which can have a lifetime. This is for UI purposes. Method can be extended to more arguments @@ -1771,7 +1778,9 @@ class PublisherController(BasePublisherController): self._on_create_instance_change() - def emit_card_message(self, message, message_type=None): + def emit_card_message( + self, message, message_type=CardMessageTypes.standard + ): self._emit_event( "show.card.message", { @@ -1910,8 +1919,12 @@ class PublisherController(BasePublisherController): "failed_info": exc.failed_info } ) + + if success: + self.emit_card_message("Conversion finished") + else: + self.emit_card_message("Conversion failed", CardMessageTypes.error) self._on_create_instance_change() - self.emit_card_message("Conversion finished") def create( self, creator_identifier, subset_name, instance_data, options