From 14b5ac4c251f5cb3b9a263f7f0b03bc0567ca45f Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 18 Aug 2022 14:01:54 +0300 Subject: [PATCH 01/96] 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 65c2638f3dd047d5168b256dd848a9a0c89c5ec4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 11 Oct 2022 09:43:18 +0200 Subject: [PATCH 02/96] Fusion: Implement backwards compatibility (pre Fusion 17.4) --- openpype/hosts/fusion/api/pipeline.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index b22ee5328f..b6092f7c1b 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -44,11 +44,26 @@ INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") class FusionLogHandler(logging.Handler): # Keep a reference to fusion's Print function (Remote Object) - _print = getattr(sys.modules["__main__"], "fusion").Print + _print = None + + @property + def print(self): + if self._print is not None: + # Use cached + return self._print + + _print = getattr(sys.modules["__main__"], "fusion").Print + if _print is None: + # Backwards compatibility: Print method on Fusion instance was + # added around Fusion 17.4 and wasn't available on PyRemote Object + # before + _print = get_current_comp().Print + self._print = _print + return _print def emit(self, record): entry = self.format(record) - self._print(entry) + self.print(entry) def install(): From bacbff0262e8306ef16ccf3d1e826e3e1cb2728f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 17 Oct 2022 15:19:48 +0200 Subject: [PATCH 03/96] create context has callbacks for reset preparation and finalization --- openpype/pipeline/create/context.py | 15 +++++++++++++++ openpype/tools/publisher/control.py | 4 ++++ 2 files changed, 19 insertions(+) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index c1cf4dab44..1f3c32f0a7 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -860,6 +860,9 @@ class CreateContext: All changes will be lost if were not saved explicitely. """ + + self.reset_preparation() + self.reset_avalon_context() self.reset_plugins(discover_publish_plugins) self.reset_context_data() @@ -868,6 +871,18 @@ class CreateContext: self.reset_instances() self.execute_autocreators() + self.reset_finalization() + + def reset_preparation(self): + """Prepare attributes that must be prepared/cleaned before reset.""" + + pass + + def reset_finalization(self): + """Cleanup of attributes after reset.""" + + pass + def reset_avalon_context(self): """Give ability to reset avalon context. diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index b4c89f221f..19e28cca4b 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -552,6 +552,8 @@ class PublisherController: self.save_changes() + self.create_context.reset_preparation() + # Reset avalon context self.create_context.reset_avalon_context() @@ -560,6 +562,8 @@ class PublisherController: self._reset_publish() self._reset_instances() + self.create_context.reset_finalization() + self.emit_card_message("Refreshed..") def _reset_plugins(self): From e0bb8c0469d50273ad041280b786380d98de080c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 17 Oct 2022 15:20:06 +0200 Subject: [PATCH 04/96] context can handle shared data for collection phase --- openpype/pipeline/create/context.py | 68 ++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 1f3c32f0a7..02398818d9 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -27,6 +27,11 @@ from .creator_plugins import ( UpdateData = collections.namedtuple("UpdateData", ["instance", "changes"]) +class UnavailableSharedData(Exception): + """Shared data are not available at the moment when are accessed.""" + pass + + class ImmutableKeyError(TypeError): """Accessed key is immutable so does not allow changes or removements.""" @@ -809,6 +814,9 @@ class CreateContext: self._bulk_counter = 0 self._bulk_instances_to_process = [] + # Shared data across creators during collection phase + self._collection_shared_data = None + # Trigger reset if was enabled if reset: self.reset(discover_publish_plugins) @@ -877,11 +885,15 @@ class CreateContext: """Prepare attributes that must be prepared/cleaned before reset.""" pass + # Give ability to store shared data for collection phase + self._collection_shared_data = {} def reset_finalization(self): """Cleanup of attributes after reset.""" pass + # Stop access to collection shared data + self._collection_shared_data = None def reset_avalon_context(self): """Give ability to reset avalon context. @@ -991,7 +1003,8 @@ class CreateContext: and creator_class.host_name != self.host_name ): self.log.info(( - "Creator's host name is not supported for current host {}" + "Creator's host name \"{}\"" + " is not supported for current host \"{}\"" ).format(creator_class.host_name, self.host_name)) continue @@ -1266,3 +1279,56 @@ class CreateContext: if not plugin.__instanceEnabled__: plugins.append(plugin) return plugins + + def _validate_collection_shared_data(self): + if self._collection_shared_data is None: + raise UnavailableSharedData( + "Accessed Collection shared data out of collection phase" + ) + + def has_collection_shared_data(self, key): + """Check if collection shared data are set. + + Args: + key (str): Key under which are shared data stored. + + Retruns: + bool: Key is already set. + + Raises: + UnavailableSharedData: When called out of collection phase. + """ + + self._validate_collection_shared_data() + return key in self._collection_shared_data + + def get_collection_shared_data(self, key, default=None): + """Receive shared data during collection phase. + + Args: + key (str): Key under which are shared data stored. + default (Any): Default value if key is not set. + + Returns: + Any: Value stored under the key. + + Raises: + UnavailableSharedData: When called out of collection phase. + """ + + self._validate_collection_shared_data() + return self._collection_shared_data.get(key, default) + + def set_collection_shared_data(self, key, value): + """Store a value under collection shared data. + + Args: + key (str): Key under which will shared data be stored. + value (Any): Value to store. + + Raises: + UnavailableSharedData: When called out of collection phase. + """ + + self._validate_collection_shared_data() + self._collection_shared_data[key] = value From 2ed383c4768571436df3f2b3b2245d55ccdfdc6b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 17 Oct 2022 15:20:24 +0200 Subject: [PATCH 05/96] added wrappers for access to shared data in create plugins --- openpype/pipeline/create/creator_plugins.py | 36 +++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 05ba8902aa..761054fbd5 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -6,6 +6,7 @@ from abc import ( abstractmethod, abstractproperty ) + import six from openpype.settings import get_system_settings, get_project_settings @@ -323,6 +324,41 @@ class BaseCreator: return self.instance_attr_defs + def has_collection_shared_data(self, key): + """Check if collection shared data are set. + + Args: + key (str): Key under which are shared data stored. + + Retruns: + bool: Key is already set. + """ + + return self.create_context.has_collection_shared_data(key) + + def get_collection_shared_data(self, key, default=None): + """Receive shared data during collection phase. + + Args: + key (str): Key under which are shared data stored. + default (Any): Default value if key is not set. + + Returns: + Any: Value stored under the key. + """ + + return self.create_context.get_collection_shared_data(key, default) + + def set_collection_shared_data(self, key, value): + """Store a value under collection shared data. + + Args: + key (str): Key under which will shared data be stored. + value (Any): Value to store. + """ + + return self.create_context.set_collection_shared_data(key, value) + class Creator(BaseCreator): """Creator that has more information for artist to show in UI. From 3f01d008c59205136d18675068f4242def604b2f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 17 Oct 2022 15:26:38 +0200 Subject: [PATCH 06/96] added recommendation --- openpype/pipeline/create/context.py | 3 +++ openpype/pipeline/create/creator_plugins.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 02398818d9..613eaa2865 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1322,6 +1322,9 @@ class CreateContext: def set_collection_shared_data(self, key, value): """Store a value under collection shared data. + It is highly recommended to use very specific keys as creators may + clash each other if simple keys are used. + Args: key (str): Key under which will shared data be stored. value (Any): Value to store. diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 761054fbd5..343a416872 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -352,6 +352,9 @@ class BaseCreator: def set_collection_shared_data(self, key, value): """Store a value under collection shared data. + It is highly recommended to use very specific keys as creators may + clash each other if simple keys are used. + Args: key (str): Key under which will shared data be stored. value (Any): Value to store. From 0aefb39acb85b4f8dc0de3904b3613c3762d8c2c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 17 Oct 2022 15:31:07 +0200 Subject: [PATCH 07/96] cache instances in shared data in tray publisher --- openpype/hosts/traypublisher/api/plugin.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index 89c25389cb..1e592e786d 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -17,11 +17,27 @@ from openpype.lib.transcoding import IMAGE_EXTENSIONS, VIDEO_EXTENSIONS REVIEW_EXTENSIONS = IMAGE_EXTENSIONS + VIDEO_EXTENSIONS +def _cache_and_get_instances(creator): + """Cache instances in shared data. + + Args: + creator (Creator): Plugin which would like to get instances from host. + + Returns: + List[Dict[str, Any]]: Cached instances list from host implementation. + """ + + shared_key = "openpype.traypublisher.instances" + if not creator.has_collection_shared_data(shared_key): + creator.set_collection_shared_data(shared_key, list_instances()) + return creator.get_collection_shared_data(shared_key) + + class HiddenTrayPublishCreator(HiddenCreator): host_name = "traypublisher" def collect_instances(self): - for instance_data in list_instances(): + for instance_data in _cache_and_get_instances(): creator_id = instance_data.get("creator_identifier") if creator_id == self.identifier: instance = CreatedInstance.from_existing( @@ -58,7 +74,7 @@ class TrayPublishCreator(Creator): host_name = "traypublisher" def collect_instances(self): - for instance_data in list_instances(): + for instance_data in _cache_and_get_instances(): creator_id = instance_data.get("creator_identifier") if creator_id == self.identifier: instance = CreatedInstance.from_existing( From aa6de1cfeba7338212c4043a3674fe50f6ecab90 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 17 Oct 2022 16:51:15 +0200 Subject: [PATCH 08/96] renamed 'has_collection_shared_data' to 'collection_shared_data_contains' --- openpype/hosts/traypublisher/api/plugin.py | 2 +- openpype/pipeline/create/context.py | 2 +- openpype/pipeline/create/creator_plugins.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index 1e592e786d..0f519e3c32 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -28,7 +28,7 @@ def _cache_and_get_instances(creator): """ shared_key = "openpype.traypublisher.instances" - if not creator.has_collection_shared_data(shared_key): + if not creator.collection_shared_data_contains(shared_key): creator.set_collection_shared_data(shared_key, list_instances()) return creator.get_collection_shared_data(shared_key) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 613eaa2865..298eacecb5 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1286,7 +1286,7 @@ class CreateContext: "Accessed Collection shared data out of collection phase" ) - def has_collection_shared_data(self, key): + def collection_shared_data_contains(self, key): """Check if collection shared data are set. Args: diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 343a416872..e5018c395e 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -324,7 +324,7 @@ class BaseCreator: return self.instance_attr_defs - def has_collection_shared_data(self, key): + def collection_shared_data_contains(self, key): """Check if collection shared data are set. Args: @@ -334,7 +334,7 @@ class BaseCreator: bool: Key is already set. """ - return self.create_context.has_collection_shared_data(key) + return self.create_context.collection_shared_data_contains(key) def get_collection_shared_data(self, key, default=None): """Receive shared data during collection phase. From ba2cb2d11d7dbc0384265071a9d464b68a8813f8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 17 Oct 2022 17:00:44 +0200 Subject: [PATCH 09/96] add information about shared data to documentation --- website/docs/dev_publishing.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/website/docs/dev_publishing.md b/website/docs/dev_publishing.md index 7a6082a517..5f30f7f9c8 100644 --- a/website/docs/dev_publishing.md +++ b/website/docs/dev_publishing.md @@ -47,10 +47,14 @@ Context discovers creator and publish plugins. Trigger collections of existing i Creator plugins can call **creator_adds_instance** or **creator_removed_instance** to add/remove instances but these methods are not meant to be called directly out of the creator. The reason is that it is the creator's responsibility to remove metadata or decide if it should remove the instance. -#### Required functions in host implementation -Host implementation **must** implement **get_context_data** and **update_context_data**. These two functions are needed to store metadata that are not related to any instance but are needed for Creating and publishing process. Right now only data about enabled/disabled optional publish plugins is stored there. When data is not stored and loaded properly, reset of publishing will cause that they will be set to default value. Context data also parsed to json string similarly as instance data. +During reset are re-cached Creator plugins, re-collected instances, refreshed host context and more. Object of `CreateContext` supply shared data during the reset. They can be used by creators to share same data needed during collection phase or during creation for autocreators. -There are also few optional functions. For UI purposes it is possible to implement **get_context_title** which can return a string shown in UI as a title. Output string may contain html tags. It is recommended to return context path (it will be created function this purposes) in this order `"{project name}/{asset hierarchy}/{asset name}/{task name}"`. +#### Required functions in host implementation +It is recommended to use `HostBase` class (`from openpype.host import HostBase`) as base for host implementation with combination of `IPublishHost` interface (`from openpype.host import IPublishHost`). These abstract classes should guide you to fill missing attributes and methods. + +To sum them and in case host implementation is inheriting `HostBase` the implementation **must** implement **get_context_data** and **update_context_data**. These two functions are needed to store metadata that are not related to any instance but are needed for Creating and publishing process. Right now only data about enabled/disabled optional publish plugins is stored there. When data is not stored and loaded properly, reset of publishing will cause that they will be set to default value. Context data also parsed to json string similarly as instance data. + +There are also few optional functions. For UI purposes it is possible to implement **get_context_title** which can return a string shown in UI as a title. Output string may contain html tags. It is recommended to return context path (it will be created function this purposes) in this order `"{project name}/{asset hierarchy}/{asset name}/{task name}"` (this is default implementation in `HostBase`). Another optional function is **get_current_context**. This function is handy in hosts where it is possible to open multiple workfiles in one process so using global context variables is not relevant because artists can switch between opened workfiles without being acknowledged. When a function is not implemented or won't return the right keys the global context is used. ```json @@ -68,6 +72,12 @@ Main responsibility of create plugin is to create, update, collect and remove in #### *BaseCreator* Base implementation of creator plugin. It is not recommended to use this class as base for production plugins but rather use one of **HiddenCreator**, **AutoCreator** and **Creator** variants. +**Access to shared data** +Functions to work with "Collection shared data" can be used during reset phase of `CreateContext`. Creators can cache there data that are common for them. For example list of nodes in scene. Methods are implemented on `CreateContext` but their usage is primarily for Create plugins as nothing else should use it. +- **`collection_shared_data_contains`** - Check if shared data already has set a key. +- **`get_collection_shared_data`** - Receive value of shared data by a key. +- **`set_collection_shared_data`** - Set or update value of shared data key. + **Abstractions** - **`family`** (class attr) - Tells what kind of instance will be created. ```python From cad97d6d1dc98d9d2d46dec060801fa4645e6053 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 18 Oct 2022 15:18:24 +0200 Subject: [PATCH 10/96] simplify api by giving access to 'collection_shared_data' property --- openpype/hosts/traypublisher/api/plugin.py | 6 +- openpype/pipeline/create/context.py | 65 ++++----------------- openpype/pipeline/create/creator_plugins.py | 41 +++---------- 3 files changed, 23 insertions(+), 89 deletions(-) diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index 0f519e3c32..2cb5a8729f 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -28,9 +28,9 @@ def _cache_and_get_instances(creator): """ shared_key = "openpype.traypublisher.instances" - if not creator.collection_shared_data_contains(shared_key): - creator.set_collection_shared_data(shared_key, list_instances()) - return creator.get_collection_shared_data(shared_key) + if shared_key not in creator.collection_shared_data: + creator.collection_shared_data[shared_key] = list_instances() + return creator.collection_shared_data[shared_key] class HiddenTrayPublishCreator(HiddenCreator): diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 298eacecb5..c5c9a14f33 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -884,14 +884,12 @@ class CreateContext: def reset_preparation(self): """Prepare attributes that must be prepared/cleaned before reset.""" - pass # Give ability to store shared data for collection phase self._collection_shared_data = {} def reset_finalization(self): """Cleanup of attributes after reset.""" - pass # Stop access to collection shared data self._collection_shared_data = None @@ -1280,58 +1278,19 @@ class CreateContext: plugins.append(plugin) return plugins - def _validate_collection_shared_data(self): + @property + def collection_shared_data(self): + """Access to shared data that can be used during creator's collection. + + Retruns: + Dict[str, Any]: Shared data. + + Raises: + UnavailableSharedData: When called out of collection phase. + """ + if self._collection_shared_data is None: raise UnavailableSharedData( "Accessed Collection shared data out of collection phase" ) - - def collection_shared_data_contains(self, key): - """Check if collection shared data are set. - - Args: - key (str): Key under which are shared data stored. - - Retruns: - bool: Key is already set. - - Raises: - UnavailableSharedData: When called out of collection phase. - """ - - self._validate_collection_shared_data() - return key in self._collection_shared_data - - def get_collection_shared_data(self, key, default=None): - """Receive shared data during collection phase. - - Args: - key (str): Key under which are shared data stored. - default (Any): Default value if key is not set. - - Returns: - Any: Value stored under the key. - - Raises: - UnavailableSharedData: When called out of collection phase. - """ - - self._validate_collection_shared_data() - return self._collection_shared_data.get(key, default) - - def set_collection_shared_data(self, key, value): - """Store a value under collection shared data. - - It is highly recommended to use very specific keys as creators may - clash each other if simple keys are used. - - Args: - key (str): Key under which will shared data be stored. - value (Any): Value to store. - - Raises: - UnavailableSharedData: When called out of collection phase. - """ - - self._validate_collection_shared_data() - self._collection_shared_data[key] = value + return self._collection_shared_data diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index e5018c395e..97ee94c449 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -324,43 +324,18 @@ class BaseCreator: return self.instance_attr_defs - def collection_shared_data_contains(self, key): - """Check if collection shared data are set. - - Args: - key (str): Key under which are shared data stored. + @property + def collection_shared_data(self): + """Access to shared data that can be used during creator's collection. Retruns: - bool: Key is already set. + Dict[str, Any]: Shared data. + + Raises: + UnavailableSharedData: When called out of collection phase. """ - return self.create_context.collection_shared_data_contains(key) - - def get_collection_shared_data(self, key, default=None): - """Receive shared data during collection phase. - - Args: - key (str): Key under which are shared data stored. - default (Any): Default value if key is not set. - - Returns: - Any: Value stored under the key. - """ - - return self.create_context.get_collection_shared_data(key, default) - - def set_collection_shared_data(self, key, value): - """Store a value under collection shared data. - - It is highly recommended to use very specific keys as creators may - clash each other if simple keys are used. - - Args: - key (str): Key under which will shared data be stored. - value (Any): Value to store. - """ - - return self.create_context.set_collection_shared_data(key, value) + return self.create_context.collection_shared_data class Creator(BaseCreator): From 7571d62709eb5c9ecd08a07062e818f32eb4ab0c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 18 Oct 2022 15:22:50 +0200 Subject: [PATCH 11/96] Updated documentation --- website/docs/dev_publishing.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/website/docs/dev_publishing.md b/website/docs/dev_publishing.md index 5f30f7f9c8..135f6cd985 100644 --- a/website/docs/dev_publishing.md +++ b/website/docs/dev_publishing.md @@ -73,10 +73,7 @@ Main responsibility of create plugin is to create, update, collect and remove in Base implementation of creator plugin. It is not recommended to use this class as base for production plugins but rather use one of **HiddenCreator**, **AutoCreator** and **Creator** variants. **Access to shared data** -Functions to work with "Collection shared data" can be used during reset phase of `CreateContext`. Creators can cache there data that are common for them. For example list of nodes in scene. Methods are implemented on `CreateContext` but their usage is primarily for Create plugins as nothing else should use it. -- **`collection_shared_data_contains`** - Check if shared data already has set a key. -- **`get_collection_shared_data`** - Receive value of shared data by a key. -- **`set_collection_shared_data`** - Set or update value of shared data key. +Functions to work with "Collection shared data" can be used during reset phase of `CreateContext`. Creators can cache there data that are common for them. For example list of nodes in scene. Methods are implemented on `CreateContext` but their usage is primarily for Create plugins as nothing else should use it. Each creator can access `collection_shared_data` attribute which is a dictionary where shared data can be stored. **Abstractions** - **`family`** (class attr) - Tells what kind of instance will be created. From c9fc2547a9b44c6b108633504e64b6dfc5b9d603 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 19 Oct 2022 11:56:55 +0200 Subject: [PATCH 12/96] replace imports from openpype.api --- openpype/hosts/hiero/api/pipeline.py | 1 - .../hiero/plugins/publish/integrate_version_up_workfile.py | 5 +++-- openpype/hosts/nuke/api/pipeline.py | 1 - openpype/hosts/nuke/plugins/publish/precollect_workfile.py | 5 +++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/hiero/api/pipeline.py b/openpype/hosts/hiero/api/pipeline.py index dacfd338bb..ea61dc4785 100644 --- a/openpype/hosts/hiero/api/pipeline.py +++ b/openpype/hosts/hiero/api/pipeline.py @@ -251,7 +251,6 @@ def reload_config(): import importlib for module in ( - "openpype.api", "openpype.hosts.hiero.lib", "openpype.hosts.hiero.menu", "openpype.hosts.hiero.tags" diff --git a/openpype/hosts/hiero/plugins/publish/integrate_version_up_workfile.py b/openpype/hosts/hiero/plugins/publish/integrate_version_up_workfile.py index 934e7112fa..6ccbe955f2 100644 --- a/openpype/hosts/hiero/plugins/publish/integrate_version_up_workfile.py +++ b/openpype/hosts/hiero/plugins/publish/integrate_version_up_workfile.py @@ -1,5 +1,6 @@ from pyblish import api -import openpype.api as pype + +from openpype.lib import version_up class IntegrateVersionUpWorkfile(api.ContextPlugin): @@ -15,7 +16,7 @@ class IntegrateVersionUpWorkfile(api.ContextPlugin): def process(self, context): project = context.data["activeProject"] path = context.data.get("currentFile") - new_path = pype.version_up(path) + new_path = version_up(path) if project: project.saveAs(new_path) diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index 7db420f6af..c343c635fa 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -66,7 +66,6 @@ def reload_config(): """ for module in ( - "openpype.api", "openpype.hosts.nuke.api.actions", "openpype.hosts.nuke.api.menu", "openpype.hosts.nuke.api.plugin", diff --git a/openpype/hosts/nuke/plugins/publish/precollect_workfile.py b/openpype/hosts/nuke/plugins/publish/precollect_workfile.py index 822f405a6f..316c651b66 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_workfile.py @@ -3,7 +3,8 @@ import os import nuke import pyblish.api -import openpype.api as pype + +from openpype.lib import get_version_from_path from openpype.hosts.nuke.api.lib import ( add_publish_knob, get_avalon_knob_data @@ -74,7 +75,7 @@ class CollectWorkfile(pyblish.api.ContextPlugin): "fps": root['fps'].value(), "currentFile": current_file, - "version": int(pype.get_version_from_path(current_file)), + "version": int(get_version_from_path(current_file)), "host": pyblish.api.current_host(), "hostVersion": nuke.NUKE_VERSION_STRING From 84d03eac53ac4287a7e660fbd863f8923bb23b17 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 19 Oct 2022 17:49:37 +0200 Subject: [PATCH 13/96] removed unused imports and change imports from openpype.api --- .../hosts/maya/plugins/create/create_render.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 2b2c978d3c..a3e1272652 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -13,22 +13,14 @@ from openpype.settings import ( get_system_settings, get_project_settings, ) +from openpype.lib import requests_get +from openpype.modules import ModulesManager +from openpype.pipeline import legacy_io from openpype.hosts.maya.api import ( lib, lib_rendersettings, plugin ) -from openpype.lib import requests_get -from openpype.api import ( - get_system_settings, - get_project_settings) -from openpype.modules import ModulesManager -from openpype.pipeline import legacy_io -from openpype.pipeline import ( - CreatorError, - legacy_io, -) -from openpype.pipeline.context_tools import get_current_project_asset class CreateRender(plugin.Creator): From 321512bb0115ee38d085da753a2bb6b7e4e2a2ce Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 19 Oct 2022 21:19:31 +0200 Subject: [PATCH 14/96] 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 15/96] 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 16/96] 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 17/96] 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 18/96] 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 a23dfcea7909331b6db1d7b6301a7658aface47d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 Oct 2022 23:19:51 +0200 Subject: [PATCH 19/96] implemented main function for publish report viewer --- .../tools/publisher/publish_report_viewer/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openpype/tools/publisher/publish_report_viewer/__init__.py b/openpype/tools/publisher/publish_report_viewer/__init__.py index ce1cc3729c..bf77a6d30b 100644 --- a/openpype/tools/publisher/publish_report_viewer/__init__.py +++ b/openpype/tools/publisher/publish_report_viewer/__init__.py @@ -1,3 +1,5 @@ +from Qt import QtWidgets + from .report_items import ( PublishReport ) @@ -16,4 +18,13 @@ __all__ = ( "PublishReportViewerWidget", "PublishReportViewerWindow", + + "main", ) + + +def main(): + app = QtWidgets.QApplication([]) + window = PublishReportViewerWindow() + window.show() + return app.exec_() From a7747eb1acf121c692d17fa2ae86010e1ea4ba47 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 Oct 2022 23:20:04 +0200 Subject: [PATCH 20/96] added launch function for publish report viewer --- openpype/cli.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/cli.py b/openpype/cli.py index 398d1a94c0..d24cd4a872 100644 --- a/openpype/cli.py +++ b/openpype/cli.py @@ -277,6 +277,13 @@ def projectmanager(): PypeCommands().launch_project_manager() +@main.command(context_settings={"ignore_unknown_options": True}) +def publish_report_viewer(): + from openpype.tools.publisher.publish_report_viewer import main + + sys.exit(main()) + + @main.command() @click.argument("output_path") @click.option("--project", help="Define project context") From 8fb9dd41d1dfd353ac906f6febcb2922202ed6c5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 Oct 2022 23:20:27 +0200 Subject: [PATCH 21/96] added ps1 script to launch publish report viewer --- tools/run_publish_report_viewer.ps1 | 40 +++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tools/run_publish_report_viewer.ps1 diff --git a/tools/run_publish_report_viewer.ps1 b/tools/run_publish_report_viewer.ps1 new file mode 100644 index 0000000000..3ff40e64e3 --- /dev/null +++ b/tools/run_publish_report_viewer.ps1 @@ -0,0 +1,40 @@ +<# +.SYNOPSIS + Helper script OpenPype Tray. + +.DESCRIPTION + + +.EXAMPLE + +PS> .\run_tray.ps1 + +#> +$current_dir = Get-Location +$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent +$openpype_root = (Get-Item $script_dir).parent.FullName + +# Install PSWriteColor to support colorized output to terminal +$env:PSModulePath = $env:PSModulePath + ";$($openpype_root)\tools\modules\powershell" + +$env:_INSIDE_OPENPYPE_TOOL = "1" + +# make sure Poetry is in PATH +if (-not (Test-Path 'env:POETRY_HOME')) { + $env:POETRY_HOME = "$openpype_root\.poetry" +} +$env:PATH = "$($env:PATH);$($env:POETRY_HOME)\bin" + +Set-Location -Path $openpype_root + +Write-Color -Text ">>> ", "Reading Poetry ... " -Color Green, Gray -NoNewline +if (-not (Test-Path -PathType Container -Path "$($env:POETRY_HOME)\bin")) { + Write-Color -Text "NOT FOUND" -Color Yellow + Write-Color -Text "*** ", "We need to install Poetry create virtual env first ..." -Color Yellow, Gray + & "$openpype_root\tools\create_env.ps1" +} else { + Write-Color -Text "OK" -Color Green +} + +& "$($env:POETRY_HOME)\bin\poetry" run python "$($openpype_root)\start.py" publish-report-viewer --debug +Set-Location -Path $current_dir From e35e3e0299622bd600d59574857f27ad700890dd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 20 Oct 2022 12:49:39 +0200 Subject: [PATCH 22/96] fix python 2 compatibility of ffmpeg and oiio tools discovery --- openpype/lib/vendor_bin_utils.py | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/openpype/lib/vendor_bin_utils.py b/openpype/lib/vendor_bin_utils.py index eb7987c8a1..099f9a34ba 100644 --- a/openpype/lib/vendor_bin_utils.py +++ b/openpype/lib/vendor_bin_utils.py @@ -195,6 +195,28 @@ def find_tool_in_custom_paths(paths, tool, validation_func=None): return None +def _check_args_returncode(args): + try: + # Python 2 compatibility where DEVNULL is not available + if hasattr(subprocess, "DEVNULL"): + proc = subprocess.Popen( + args, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + proc.wait() + else: + with open(os.devnull, "w") as devnull: + proc = subprocess.Popen( + args, stdout=devnull, stderr=devnull, + ) + proc.wait() + + except Exception: + return False + return proc.returncode == 0 + + def _oiio_executable_validation(filepath): """Validate oiio tool executable if can be executed. @@ -223,18 +245,7 @@ def _oiio_executable_validation(filepath): if not filepath: return False - try: - proc = subprocess.Popen( - [filepath, "--help"], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - proc.wait() - return proc.returncode == 0 - - except Exception: - pass - return False + return _check_args_returncode([filepath, "--help"]) def get_oiio_tools_path(tool="oiiotool"): @@ -302,18 +313,7 @@ def _ffmpeg_executable_validation(filepath): if not filepath: return False - try: - proc = subprocess.Popen( - [filepath, "-version"], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - proc.wait() - return proc.returncode == 0 - - except Exception: - pass - return False + return _check_args_returncode([filepath, "-version"]) def get_ffmpeg_tool_path(tool="ffmpeg"): From 5d476f8f8b8136e20c88a6b5229c74fec2f2b1d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Thu, 20 Oct 2022 12:53:25 +0200 Subject: [PATCH 23/96] :recycle: remove redundant Creator stub this caused empty item in creator dialog --- openpype/hosts/unreal/api/__init__.py | 7 ++----- openpype/hosts/unreal/api/plugin.py | 11 +---------- openpype/hosts/unreal/plugins/create/create_camera.py | 4 ++-- openpype/hosts/unreal/plugins/create/create_layout.py | 4 ++-- openpype/hosts/unreal/plugins/create/create_look.py | 3 ++- openpype/hosts/unreal/plugins/create/create_render.py | 4 ++-- .../unreal/plugins/create/create_staticmeshfbx.py | 4 ++-- 7 files changed, 13 insertions(+), 24 deletions(-) diff --git a/openpype/hosts/unreal/api/__init__.py b/openpype/hosts/unreal/api/__init__.py index 870982f5f9..3f96d8ac6f 100644 --- a/openpype/hosts/unreal/api/__init__.py +++ b/openpype/hosts/unreal/api/__init__.py @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- """Unreal Editor OpenPype host API.""" -from .plugin import ( - Loader, - Creator -) +from .plugin import Loader + from .pipeline import ( install, uninstall, @@ -25,7 +23,6 @@ from .pipeline import ( __all__ = [ "install", "uninstall", - "Creator", "Loader", "ls", "publish", diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index d8d2f2420d..6fc00cb71c 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -1,16 +1,7 @@ # -*- coding: utf-8 -*- from abc import ABC -from openpype.pipeline import ( - LegacyCreator, - LoaderPlugin, -) - - -class Creator(LegacyCreator): - """This serves as skeleton for future OpenPype specific functionality""" - defaults = ['Main'] - maintain_selection = False +from openpype.pipeline import LoaderPlugin class Loader(LoaderPlugin, ABC): diff --git a/openpype/hosts/unreal/plugins/create/create_camera.py b/openpype/hosts/unreal/plugins/create/create_camera.py index 2842900834..bf1489d688 100644 --- a/openpype/hosts/unreal/plugins/create/create_camera.py +++ b/openpype/hosts/unreal/plugins/create/create_camera.py @@ -2,11 +2,11 @@ import unreal from unreal import EditorAssetLibrary as eal from unreal import EditorLevelLibrary as ell -from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api.pipeline import instantiate +from openpype.pipeline import LegacyCreator -class CreateCamera(plugin.Creator): +class CreateCamera(LegacyCreator): """Layout output for character rigs""" name = "layoutMain" diff --git a/openpype/hosts/unreal/plugins/create/create_layout.py b/openpype/hosts/unreal/plugins/create/create_layout.py index 5fef08ce2a..c1067b00d9 100644 --- a/openpype/hosts/unreal/plugins/create/create_layout.py +++ b/openpype/hosts/unreal/plugins/create/create_layout.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- from unreal import EditorLevelLibrary -from openpype.hosts.unreal.api import plugin +from openpype.pipeline import LegacyCreator from openpype.hosts.unreal.api.pipeline import instantiate -class CreateLayout(plugin.Creator): +class CreateLayout(LegacyCreator): """Layout output for character rigs.""" name = "layoutMain" diff --git a/openpype/hosts/unreal/plugins/create/create_look.py b/openpype/hosts/unreal/plugins/create/create_look.py index 12f6b70ae6..4abf3f6095 100644 --- a/openpype/hosts/unreal/plugins/create/create_look.py +++ b/openpype/hosts/unreal/plugins/create/create_look.py @@ -2,9 +2,10 @@ """Create look in Unreal.""" import unreal # noqa from openpype.hosts.unreal.api import pipeline, plugin +from openpype.pipeline import LegacyCreator -class CreateLook(plugin.Creator): +class CreateLook(LegacyCreator): """Shader connections defining shape look.""" name = "unrealLook" diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index 950799cc10..a85d17421b 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -1,10 +1,10 @@ import unreal from openpype.hosts.unreal.api import pipeline -from openpype.hosts.unreal.api.plugin import Creator +from openpype.pipeline import LegacyCreator -class CreateRender(Creator): +class CreateRender(LegacyCreator): """Create instance for sequence for rendering""" name = "unrealRender" diff --git a/openpype/hosts/unreal/plugins/create/create_staticmeshfbx.py b/openpype/hosts/unreal/plugins/create/create_staticmeshfbx.py index 601c2fae06..45d517d27d 100644 --- a/openpype/hosts/unreal/plugins/create/create_staticmeshfbx.py +++ b/openpype/hosts/unreal/plugins/create/create_staticmeshfbx.py @@ -1,13 +1,13 @@ # -*- coding: utf-8 -*- """Create Static Meshes as FBX geometry.""" import unreal # noqa -from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api.pipeline import ( instantiate, ) +from openpype.pipeline import LegacyCreator -class CreateStaticMeshFBX(plugin.Creator): +class CreateStaticMeshFBX(LegacyCreator): """Static FBX geometry.""" name = "unrealStaticMeshMain" From 05c87821941bf2dd51f50453fb5d2864a7419092 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 20 Oct 2022 14:32:43 +0200 Subject: [PATCH 24/96] raise exception on collect/save/remove operations of creator --- openpype/pipeline/create/context.py | 85 +++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 5 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 4ec6d7bdad..6bc70f33ea 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -62,6 +62,22 @@ class HostMissRequiredMethod(Exception): super(HostMissRequiredMethod, self).__init__(msg) +class CreatorOperationFailed(Exception): + pass + + +class CreatorsCollectionFailed(CreatorOperationFailed): + pass + + +class CreatorsSaveFailed(CreatorOperationFailed): + pass + + +class CreatorsRemoveFailed(CreatorOperationFailed): + pass + + class InstanceMember: """Representation of instance member. @@ -1221,8 +1237,26 @@ class CreateContext: self._instances_by_id = {} # Collect instances + failed_creators = [] for creator in self.creators.values(): - creator.collect_instances() + try: + creator.collect_instances() + except: + failed_creators.append(creator) + self.log.warning( + "Collection of instances for creator {} ({}) failed".format( + creator.label, creator.identifier), + exc_info=True + ) + + if failed_creators: + joined_creators = ", ".join( + [creator.label for creator in failed_creators] + ) + + raise CreatorsCollectionFailed( + "Failed to collect instances of creators {}".format(joined_creators) + ) def execute_autocreators(self): """Execute discovered AutoCreator plugins. @@ -1315,16 +1349,35 @@ class CreateContext: identifier = instance.creator_identifier instances_by_identifier[identifier].append(instance) - for identifier, cretor_instances in instances_by_identifier.items(): + failed_creators = [] + for identifier, creator_instances in instances_by_identifier.items(): update_list = [] - for instance in cretor_instances: + for instance in creator_instances: instance_changes = instance.changes() if instance_changes: update_list.append(UpdateData(instance, instance_changes)) creator = self.creators[identifier] if update_list: - creator.update_instances(update_list) + try: + creator.update_instances(update_list) + + except: + failed_creators.append(creator) + self.log.warning( + "Instances update of creator {} ({}) failed".format( + creator.label, creator.identifier), + exc_info=True + ) + + if failed_creators: + joined_creators = ", ".join( + [creator.label for creator in failed_creators] + ) + + raise CreatorsSaveFailed( + "Failed save changes of creators {}".format(joined_creators) + ) def remove_instances(self, instances): """Remove instances from context. @@ -1333,14 +1386,36 @@ class CreateContext: instances(list): Instances that should be removed from context. """ + instances_by_identifier = collections.defaultdict(list) for instance in instances: identifier = instance.creator_identifier instances_by_identifier[identifier].append(instance) + failed_creators = [] for identifier, creator_instances in instances_by_identifier.items(): creator = self.creators.get(identifier) - creator.remove_instances(creator_instances) + try: + creator.remove_instances(creator_instances) + except: + failed_creators.append(creator) + self.log.warning( + "Instances removement of creator {} ({}) failed".format( + creator.label, creator.identifier), + exc_info=True + ) + + if failed_creators: + joined_creators = ", ".join( + [creator.label for creator in failed_creators] + ) + + raise CreatorsRemoveFailed( + "Failed to remove instances of creators {}".format( + joined_creators + ) + ) + def _get_publish_plugins_with_attr_for_family(self, family): """Publish plugin attributes for passed family. From 3f54214a1113bac1ab5b53e8a0f17078eba1ce6a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 20 Oct 2022 14:43:32 +0200 Subject: [PATCH 25/96] don't autotrigger save on controller reset --- openpype/tools/publisher/control.py | 3 --- openpype/tools/publisher/window.py | 41 ++++++++++++++++++----------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index a340f8c1d2..1a15a71040 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1662,11 +1662,8 @@ class PublisherController(BasePublisherController): def reset(self): """Reset everything related to creation and publishing.""" - # Stop publishing self.stop_publish() - self.save_changes() - self.host_is_valid = self._create_context.host_is_valid # Reset avalon context diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 39075d2489..7559b4a641 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -36,7 +36,7 @@ class PublisherWindow(QtWidgets.QDialog): footer_border = 8 publish_footer_spacer = 2 - def __init__(self, parent=None, controller=None, reset_on_show=None): + def __init__(self, parent=None, controller=None, reset_on_first_show=None): super(PublisherWindow, self).__init__(parent) self.setWindowTitle("OpenPype publisher") @@ -44,8 +44,8 @@ class PublisherWindow(QtWidgets.QDialog): icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) self.setWindowIcon(icon) - if reset_on_show is None: - reset_on_show = True + if reset_on_first_show is None: + reset_on_first_show = True if parent is None: on_top_flag = QtCore.Qt.WindowStaysOnTopHint @@ -298,7 +298,8 @@ class PublisherWindow(QtWidgets.QDialog): self._controller = controller self._first_show = True - self._reset_on_show = reset_on_show + self._reset_on_first_show = reset_on_first_show + self._reset_on_show = True self._restart_timer = None self._publish_frame_visible = None @@ -314,6 +315,18 @@ class PublisherWindow(QtWidgets.QDialog): self._first_show = False self._on_first_show() + if not self._reset_on_show: + return + + self._reset_on_show = False + # Detach showing - give OS chance to draw the window + timer = QtCore.QTimer() + timer.setSingleShot(True) + timer.setInterval(1) + timer.timeout.connect(self._on_show_restart_timer) + self._restart_timer = timer + timer.start() + def resizeEvent(self, event): super(PublisherWindow, self).resizeEvent(event) self._update_publish_frame_rect() @@ -324,16 +337,7 @@ class PublisherWindow(QtWidgets.QDialog): def _on_first_show(self): self.resize(self.default_width, self.default_height) self.setStyleSheet(style.load_stylesheet()) - if not self._reset_on_show: - return - - # Detach showing - give OS chance to draw the window - timer = QtCore.QTimer() - timer.setSingleShot(True) - timer.setInterval(1) - timer.timeout.connect(self._on_show_restart_timer) - self._restart_timer = timer - timer.start() + self._reset_on_show = self._reset_on_first_show def _on_show_restart_timer(self): """Callback for '_restart_timer' timer.""" @@ -342,9 +346,13 @@ class PublisherWindow(QtWidgets.QDialog): self.reset() def closeEvent(self, event): - self._controller.save_changes() + self.save_changes() + self._reset_on_show = True super(PublisherWindow, self).closeEvent(event) + def save_changes(self): + self._controller.save_changes() + def reset(self): self._controller.reset() @@ -436,7 +444,8 @@ class PublisherWindow(QtWidgets.QDialog): self._update_publish_frame_rect() def _on_reset_clicked(self): - self._controller.reset() + self.save_changes() + self.reset() def _on_stop_clicked(self): self._controller.stop_publish() From 2d92aed06e530e581229dc96c73ce562ba15ab70 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 20 Oct 2022 15:08:36 +0200 Subject: [PATCH 26/96] handle failed collect,update and remove instances --- openpype/tools/publisher/control.py | 41 ++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 1a15a71040..2346825734 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -31,6 +31,9 @@ from openpype.pipeline.create import ( HiddenCreator, Creator, ) +from openpype.pipeline.create.context import ( + CreatorsOperationFailed, +) # Define constant for plugin orders offset PLUGIN_ORDER_OFFSET = 0.5 @@ -1708,8 +1711,18 @@ class PublisherController(BasePublisherController): self._create_context.reset_context_data() with self._create_context.bulk_instances_collection(): - self._create_context.reset_instances() - self._create_context.execute_autocreators() + try: + self._create_context.reset_instances() + self._create_context.execute_autocreators() + + except CreatorsOperationFailed as exc: + self._emit_event( + "instances.collection.failed", + { + "title": "Instance collection failed", + "message": str(exc) + } + ) self._resetting_instances = False @@ -1845,8 +1858,19 @@ class PublisherController(BasePublisherController): def save_changes(self): """Save changes happened during creation.""" - if self._create_context.host_is_valid: + if not self._create_context.host_is_valid: + return + + try: self._create_context.save_changes() + except CreatorsOperationFailed as exc: + self._emit_event( + "instances.save.failed", + { + "title": "Save failed", + "message": str(exc) + } + ) def remove_instances(self, instance_ids): """Remove instances based on instance ids. @@ -1869,7 +1893,16 @@ class PublisherController(BasePublisherController): instances_by_id[instance_id] for instance_id in instance_ids ] - self._create_context.remove_instances(instances) + try: + self._create_context.remove_instances(instances) + except CreatorsOperationFailed as exc: + self._emit_event( + "instances.remove.failed", + { + "title": "Remove failed", + "message": str(exc) + } + ) def _on_create_instance_change(self): self._emit_event("instances.refresh.finished") From f22e3c3a99697f312d35b769a25126effab5c65d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 20 Oct 2022 15:08:44 +0200 Subject: [PATCH 27/96] show dialogs when creator fails --- openpype/tools/publisher/window.py | 65 ++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 7559b4a641..a7f2ec2ce6 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -1,3 +1,4 @@ +import collections from Qt import QtWidgets, QtCore, QtGui from openpype import ( @@ -222,6 +223,10 @@ class PublisherWindow(QtWidgets.QDialog): # Floating publish frame publish_frame = PublishFrame(controller, self.footer_border, self) + dialog_message_timer = QtCore.QTimer() + dialog_message_timer.setInterval(100) + dialog_message_timer.timeout.connect(self._on_dialog_message_timeout) + help_btn.clicked.connect(self._on_help_click) tabs_widget.tab_changed.connect(self._on_tab_change) overview_widget.active_changed.connect( @@ -259,6 +264,15 @@ class PublisherWindow(QtWidgets.QDialog): controller.event_system.add_callback( "show.card.message", self._on_overlay_message ) + controller.event_system.add_callback( + "instances.collection.failed", self._instance_collection_failed + ) + controller.event_system.add_callback( + "instances.save.failed", self._instance_save_failed + ) + controller.event_system.add_callback( + "instances.remove.failed", self._instance_remove_failed + ) # Store extra header widget for TrayPublisher # - can be used to add additional widgets to header between context @@ -303,6 +317,9 @@ class PublisherWindow(QtWidgets.QDialog): self._restart_timer = None self._publish_frame_visible = None + self._dialog_messages_to_show = collections.deque() + self._dialog_message_timer = dialog_message_timer + self._set_publish_visibility(False) @property @@ -578,3 +595,51 @@ class PublisherWindow(QtWidgets.QDialog): self._publish_frame.move( 0, window_size.height() - height ) + + def add_message_dialog(self, message, title): + self._dialog_messages_to_show.append((message, title)) + self._dialog_message_timer.start() + + def _on_dialog_message_timeout(self): + if not self._dialog_messages_to_show: + self._dialog_message_timer.stop() + return + + item = self._dialog_messages_to_show.popleft() + message, title = item + dialog = MessageDialog(message, title) + dialog.exec_() + + def _instance_collection_failed(self, event): + self.add_message_dialog(event["message"], event["title"]) + + def _instance_save_failed(self, event): + self.add_message_dialog(event["message"], event["title"]) + + def _instance_remove_failed(self, event): + self.add_message_dialog(event["message"], event["title"]) + + +class MessageDialog(QtWidgets.QDialog): + def __init__(self, message, title, parent=None): + super(MessageDialog, self).__init__(parent) + + self.setWindowTitle(title or "Something happend") + + message_widget = QtWidgets.QLabel(message, self) + + btns_widget = QtWidgets.QWidget(self) + submit_btn = QtWidgets.QPushButton("OK", btns_widget) + + btns_layout = QtWidgets.QHBoxLayout(btns_widget) + btns_layout.setContentsMargins(0, 0, 0, 0) + btns_layout.addStretch(1) + btns_layout.addWidget(submit_btn) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(message_widget, 1) + layout.addWidget(btns_widget, 0) + + def showEvent(self, event): + super(MessageDialog, self).showEvent(event) + self.resize(400, 300) From 34dcbcbf1ec987e77e7390071207b36c36aab284 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 20 Oct 2022 15:17:05 +0200 Subject: [PATCH 28/96] renamed 'CreatorOperationFailed' to 'CreatorsOperationFailed' --- openpype/pipeline/create/context.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 6bc70f33ea..fb53b95a92 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -62,19 +62,19 @@ class HostMissRequiredMethod(Exception): super(HostMissRequiredMethod, self).__init__(msg) -class CreatorOperationFailed(Exception): +class CreatorsOperationFailed(Exception): pass -class CreatorsCollectionFailed(CreatorOperationFailed): +class CreatorsCollectionFailed(CreatorsOperationFailed): pass -class CreatorsSaveFailed(CreatorOperationFailed): +class CreatorsSaveFailed(CreatorsOperationFailed): pass -class CreatorsRemoveFailed(CreatorOperationFailed): +class CreatorsRemoveFailed(CreatorsOperationFailed): pass From 65ec73a53a8589e4d3ddff7df2d579e4236cf823 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 20 Oct 2022 15:57:21 +0200 Subject: [PATCH 29/96] fix page change --- openpype/tools/publisher/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index a7f2ec2ce6..2199981519 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -498,7 +498,7 @@ class PublisherWindow(QtWidgets.QDialog): self._update_publish_details_widget() if ( not self._tabs_widget.is_current_tab("create") - or not self._tabs_widget.is_current_tab("publish") + and not self._tabs_widget.is_current_tab("publish") ): self._tabs_widget.set_current_tab("publish") From d04231cc6b112457b5e440b3ef6bd7e5f0bd475d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 20 Oct 2022 15:57:30 +0200 Subject: [PATCH 30/96] fix context label before publishing start --- openpype/tools/publisher/control.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 2346825734..d402ab2434 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -302,8 +302,11 @@ class PublishReport: } def _extract_context_data(self, context): + context_label = "Context" + if context is not None: + context_label = context.data.get("label") return { - "label": context.data.get("label") + "label": context_label } def _extract_instance_data(self, instance, exists): From ab40ab6201a32142b702f7ba1ad1545631c34171 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 20 Oct 2022 16:19:02 +0200 Subject: [PATCH 31/96] change init arg back --- openpype/tools/publisher/window.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 2199981519..0e514bd2f2 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -37,7 +37,7 @@ class PublisherWindow(QtWidgets.QDialog): footer_border = 8 publish_footer_spacer = 2 - def __init__(self, parent=None, controller=None, reset_on_first_show=None): + def __init__(self, parent=None, controller=None, reset_on_show=None): super(PublisherWindow, self).__init__(parent) self.setWindowTitle("OpenPype publisher") @@ -45,8 +45,8 @@ class PublisherWindow(QtWidgets.QDialog): icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) self.setWindowIcon(icon) - if reset_on_first_show is None: - reset_on_first_show = True + if reset_on_show is None: + reset_on_show = True if parent is None: on_top_flag = QtCore.Qt.WindowStaysOnTopHint @@ -312,7 +312,9 @@ class PublisherWindow(QtWidgets.QDialog): self._controller = controller self._first_show = True - self._reset_on_first_show = reset_on_first_show + # This is a little bit confusing but 'reset_on_first_show' is too long + # forin init + self._reset_on_first_show = reset_on_show self._reset_on_show = True self._restart_timer = None self._publish_frame_visible = None From 0c899e601b22c6c5c9a8f084d4a7a9ad71b8104c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 20 Oct 2022 17:10:09 +0200 Subject: [PATCH 32/96] fix attributes --- openpype/tools/publisher/control.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index bf1564597f..d2d01e7921 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1669,7 +1669,7 @@ class PublisherController(BasePublisherController): self.host_is_valid = self._create_context.host_is_valid - self.create_context.reset_preparation() + self._create_context.reset_preparation() # Reset avalon context self._create_context.reset_avalon_context() @@ -1681,7 +1681,7 @@ class PublisherController(BasePublisherController): self._reset_publish() self._reset_instances() - self.create_context.reset_finalization() + self._create_context.reset_finalization() self._emit_event("controller.reset.finished") From 2e9572aaebfac811a69a2a13b2eb2af8e094c097 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 20 Oct 2022 17:21:43 +0200 Subject: [PATCH 33/96] use float numbers for animation --- openpype/tools/publisher/widgets/overview_widget.py | 7 ++++--- openpype/tools/publisher/widgets/publish_frame.py | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/openpype/tools/publisher/widgets/overview_widget.py b/openpype/tools/publisher/widgets/overview_widget.py index 5bd3017c2a..4cf8ae0eed 100644 --- a/openpype/tools/publisher/widgets/overview_widget.py +++ b/openpype/tools/publisher/widgets/overview_widget.py @@ -93,8 +93,8 @@ class OverviewWidget(QtWidgets.QFrame): main_layout.addWidget(subset_content_widget, 1) change_anim = QtCore.QVariantAnimation() - change_anim.setStartValue(0) - change_anim.setEndValue(self.anim_end_value) + change_anim.setStartValue(float(0)) + change_anim.setEndValue(float(self.anim_end_value)) change_anim.setDuration(self.anim_duration) change_anim.setEasingCurve(QtCore.QEasingCurve.InOutQuad) @@ -264,9 +264,10 @@ class OverviewWidget(QtWidgets.QFrame): + (self._subset_content_layout.spacing() * 2) ) ) - subset_attrs_width = int(float(width) / self.anim_end_value) * value + subset_attrs_width = int((float(width) / self.anim_end_value) * value) if subset_attrs_width > width: subset_attrs_width = width + create_width = width - subset_attrs_width self._create_widget.setMinimumWidth(create_width) diff --git a/openpype/tools/publisher/widgets/publish_frame.py b/openpype/tools/publisher/widgets/publish_frame.py index e6333a104f..00597451a9 100644 --- a/openpype/tools/publisher/widgets/publish_frame.py +++ b/openpype/tools/publisher/widgets/publish_frame.py @@ -248,13 +248,13 @@ class PublishFrame(QtWidgets.QWidget): hint = self._top_content_widget.minimumSizeHint() end = hint.height() - self._shrunk_anim.setStartValue(start) - self._shrunk_anim.setEndValue(end) + self._shrunk_anim.setStartValue(float(start)) + self._shrunk_anim.setEndValue(float(end)) if not anim_is_running: self._shrunk_anim.start() def _on_shrunk_anim(self, value): - diff = self._top_content_widget.height() - value + diff = self._top_content_widget.height() - int(value) if not self._top_content_widget.isVisible(): diff -= self._content_layout.spacing() From 8049bda095ca1290443e1ebcabb1d9a9a54caeca Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 20 Oct 2022 18:38:00 +0200 Subject: [PATCH 34/96] fix message box --- openpype/tools/publisher/window.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 0e514bd2f2..4f0b81fa85 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -609,8 +609,9 @@ class PublisherWindow(QtWidgets.QDialog): item = self._dialog_messages_to_show.popleft() message, title = item - dialog = MessageDialog(message, title) + dialog = MessageDialog(message, title, self) dialog.exec_() + dialog.deleteLater() def _instance_collection_failed(self, event): self.add_message_dialog(event["message"], event["title"]) @@ -629,6 +630,7 @@ class MessageDialog(QtWidgets.QDialog): self.setWindowTitle(title or "Something happend") message_widget = QtWidgets.QLabel(message, self) + message_widget.setWordWrap(True) btns_widget = QtWidgets.QWidget(self) submit_btn = QtWidgets.QPushButton("OK", btns_widget) @@ -639,9 +641,15 @@ class MessageDialog(QtWidgets.QDialog): btns_layout.addWidget(submit_btn) layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(message_widget, 1) + layout.addWidget(message_widget, 0) + layout.addStretch(1) layout.addWidget(btns_widget, 0) + submit_btn.clicked.connect(self._on_submit_click) + + def _on_submit_click(self): + self.close() + def showEvent(self, event): super(MessageDialog, self).showEvent(event) - self.resize(400, 300) + self.resize(400, 200) From 29a50cc280c13a3be9d6c9971d91e494ac8711d7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 20 Oct 2022 21:23:46 +0200 Subject: [PATCH 35/96] 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 8e3b2ab933c11ef37678da0b784b971c861afdf6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 10:26:39 +0200 Subject: [PATCH 36/96] raise specific exceptions when creators fail to run their methods --- openpype/pipeline/create/context.py | 258 ++++++++++++++++++++++------ 1 file changed, 204 insertions(+), 54 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index fb53b95a92..dfa9049601 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1,6 +1,8 @@ import os +import sys import copy import logging +import traceback import collections import inspect from uuid import uuid4 @@ -22,6 +24,7 @@ from .creator_plugins import ( Creator, AutoCreator, discover_creator_plugins, + CreatorError, ) UpdateData = collections.namedtuple("UpdateData", ["instance", "changes"]) @@ -63,19 +66,76 @@ class HostMissRequiredMethod(Exception): class CreatorsOperationFailed(Exception): - pass + """Raised when a creator process crashes in 'CreateContext'. + + The exception contains information about the creator and error. The data + are prepared using 'prepare_failed_creator_operation_info' and can be + serialized using json. + + Usage is for UI purposes which may not have access to exceptions directly + and would not have ability to catch exceptions 'per creator'. + + Args: + msg (str): General error message. + failed_info (list[dict[str, Any]]): List of failed creators with + exception message and optionally formatted traceback. + """ + + def __init__(self, msg, failed_info): + super(CreatorsOperationFailed, self).__init__(msg) + self.failed_info = failed_info class CreatorsCollectionFailed(CreatorsOperationFailed): - pass + def __init__(self, failed_info): + msg = "Failed to collect instances" + super(CreatorsCollectionFailed, self).__init__( + msg, failed_info + ) class CreatorsSaveFailed(CreatorsOperationFailed): - pass + def __init__(self, failed_info): + msg = "Failed update instance changes" + super(CreatorsSaveFailed, self).__init__( + msg, failed_info + ) class CreatorsRemoveFailed(CreatorsOperationFailed): - pass + def __init__(self, failed_info): + msg = "Failed to remove instances" + super(CreatorsRemoveFailed, self).__init__( + msg, failed_info + ) + + +class CreatorsCreateFailed(CreatorsOperationFailed): + def __init__(self, failed_info): + msg = "Faled to create instances" + super(CreatorsCreateFailed, self).__init__( + msg, failed_info + ) + + +def prepare_failed_creator_operation_info( + identifier, label, exc_info, add_traceback=True +): + formatted_traceback = None + exc_type, exc_value, exc_traceback = exc_info + error_msg = str(exc_value) + + if add_traceback: + formatted_traceback = "".join(traceback.format_exception( + exc_type, exc_value, exc_traceback + )) + + return { + "creator_identifier": identifier, + "creator_label": label, + "message": error_msg, + "traceback": formatted_traceback + } class InstanceMember: @@ -1202,7 +1262,67 @@ class CreateContext: with self.bulk_instances_collection(): self._bulk_instances_to_process.append(instance) + def create(self, identifier, *args, **kwargs): + """Wrapper for creators to trigger created. + + Different types of creators may expect different arguments thus the + hints for args are blind. + + Args: + identifier (str): Creator's identifier. + *args (Tuple[Any]): Arguments for create method. + **kwargs (Dict[Any, Any]): Keyword argument for create method. + """ + + creator = self.creators.get(identifier) + label = getattr(creator, "label", None) + failed = False + add_traceback = False + try: + # Fake CreatorError (Could be maybe specific exception?) + if creator is None: + raise CreatorError( + "Creator {} was not found".format(identifier) + ) + + creator.create(*args, **kwargs) + + except CreatorError: + failed = True + exc_info = sys.exc_info() + + except: + failed = True + add_traceback = True + exc_info = sys.exc_info() + + if not failed: + return + + self.log.warning( + ( + "Failed to run Creator with identifier \"{}\"." + ).format(identifier), + exc_info=add_traceback + ) + + raise CreatorsCreateFailed([ + prepare_failed_creator_operation_info( + identifier, label, exc_info, add_traceback + ) + ]) + def creator_removed_instance(self, instance): + """When creator removes instance context should be acknowledged. + + If creator removes instance conext should know about it to avoid + possible issues in the session. + + Args: + instance (CreatedInstance): Object of instance which was removed + from scene metadata. + """ + self._instances_by_id.pop(instance.id, None) @contextmanager @@ -1237,42 +1357,72 @@ class CreateContext: self._instances_by_id = {} # Collect instances - failed_creators = [] + failed_info = [] for creator in self.creators.values(): + label = creator.label try: creator.collect_instances() except: - failed_creators.append(creator) + exc_info = sys.exc_info() + identifier = creator.identifier self.log.warning( - "Collection of instances for creator {} ({}) failed".format( - creator.label, creator.identifier), + ( + "Collection of instances for creator {} failed" + ).format(identifier), exc_info=True ) + failed_info.append( + prepare_failed_creator_operation_info( + identifier, label, exc_info + ) + ) - if failed_creators: - joined_creators = ", ".join( - [creator.label for creator in failed_creators] - ) - - raise CreatorsCollectionFailed( - "Failed to collect instances of creators {}".format(joined_creators) - ) + if failed_info: + raise CreatorsCollectionFailed(failed_info) def execute_autocreators(self): """Execute discovered AutoCreator plugins. Reset instances if any autocreator executed properly. """ + + failed_info = [] for identifier, creator in self.autocreators.items(): + label = creator.label + failed = False + add_traceback = False try: creator.create() - except Exception: - # TODO raise report exception if any crashed - msg = ( - "Failed to run AutoCreator with identifier \"{}\" ({})." - ).format(identifier, inspect.getfile(creator.__class__)) - self.log.warning(msg, exc_info=True) + except CreatorError: + failed = True + exc_info = sys.exc_info() + + # Use bare except because some hosts raise their exceptions that + # do not inherit from python's `BaseException` + except: + failed = True + add_traceback = True + exc_info = sys.exc_info() + + if not failed: + continue + + failed_info.append( + prepare_failed_creator_operation_info( + identifier, label, exc_info, add_traceback + ) + ) + + self.log.warning( + ( + "Failed to run AutoCreator with identifier \"{}\"." + ).format(identifier), + exc_info=exc_info + ) + + if failed_info: + raise CreatorsCreateFailed(failed_info) def validate_instances_context(self, instances=None): """Validate 'asset' and 'task' instance context.""" @@ -1349,7 +1499,7 @@ class CreateContext: identifier = instance.creator_identifier instances_by_identifier[identifier].append(instance) - failed_creators = [] + failed_info = [] for identifier, creator_instances in instances_by_identifier.items(): update_list = [] for instance in creator_instances: @@ -1358,26 +1508,29 @@ class CreateContext: update_list.append(UpdateData(instance, instance_changes)) creator = self.creators[identifier] - if update_list: - try: - creator.update_instances(update_list) + if not update_list: + continue - except: - failed_creators.append(creator) - self.log.warning( - "Instances update of creator {} ({}) failed".format( - creator.label, creator.identifier), - exc_info=True + label = creator.label + try: + creator.update_instances(update_list) + + except: + exc_info = sys.exc_info() + self.log.warning( + "Instances update of creator \"{}\" failed".format( + identifier), + exc_info=True + ) + + failed_info.append( + prepare_failed_creator_operation_info( + identifier, label, exc_info ) + ) - if failed_creators: - joined_creators = ", ".join( - [creator.label for creator in failed_creators] - ) - - raise CreatorsSaveFailed( - "Failed save changes of creators {}".format(joined_creators) - ) + if failed_info: + raise CreatorsSaveFailed(failed_info) def remove_instances(self, instances): """Remove instances from context. @@ -1392,30 +1545,27 @@ class CreateContext: identifier = instance.creator_identifier instances_by_identifier[identifier].append(instance) - failed_creators = [] + failed_info = [] for identifier, creator_instances in instances_by_identifier.items(): creator = self.creators.get(identifier) + label = creator.label try: creator.remove_instances(creator_instances) except: - failed_creators.append(creator) + exc_info = sys.exc_info() self.log.warning( - "Instances removement of creator {} ({}) failed".format( - creator.label, creator.identifier), + "Instances removement of creator \"{}\" failed".format( + identifier), exc_info=True ) - - if failed_creators: - joined_creators = ", ".join( - [creator.label for creator in failed_creators] - ) - - raise CreatorsRemoveFailed( - "Failed to remove instances of creators {}".format( - joined_creators + failed_info.append( + prepare_failed_creator_operation_info( + identifier, label, exc_info + ) ) - ) + if failed_info: + raise CreatorsRemoveFailed(failed_info) def _get_publish_plugins_with_attr_for_family(self, family): """Publish plugin attributes for passed family. From 91fa300d997abe7f483965c1d342949d6460e61e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 10:57:45 +0200 Subject: [PATCH 37/96] implementaed separator widget --- openpype/tools/utils/widgets.py | 54 +++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py index c8133b3359..ca65182124 100644 --- a/openpype/tools/utils/widgets.py +++ b/openpype/tools/utils/widgets.py @@ -448,3 +448,57 @@ class OptionDialog(QtWidgets.QDialog): def parse(self): return self._options.copy() + + +class SeparatorWidget(QtWidgets.QFrame): + """Prepared widget that can be used as separator with predefined color. + + Args: + size (int): Size of separator (width or height). + orientation (Qt.Horizontal|Qt.Vertical): Orintation of widget. + parent (QtWidgets.QWidget): Parent widget. + """ + + def __init__(self, size=2, orientation=QtCore.Qt.Horizontal, parent=None): + super(SeparatorWidget, self).__init__(parent) + + self.setObjectName("Separator") + + maximum_width = self.maximumWidth() + maximum_height = self.maximumHeight() + + self._size = None + self._orientation = orientation + self._maximum_width = maximum_width + self._maximum_height = maximum_height + self.set_size(size) + + def set_size(self, size): + if size == self._size: + return + if self._orientation == QtCore.Qt.Vertical: + self.setMinimumWidth(size) + self.setMaximumWidth(size) + else: + self.setMinimumHeight(size) + self.setMaximumHeight(size) + + self._size = size + + def set_orientation(self, orientation): + if self._orientation == orientation: + return + + # Reset min/max sizes in opossite direction + if self._orientation == QtCore.Qt.Vertical: + self.setMinimumHeight(0) + self.setMaximumHeight(self._maximum_height) + else: + self.setMinimumWidth(0) + self.setMaximumWidth(self._maximum_width) + + self._orientation = orientation + + size = self._size + self._size = None + self.set_size(size) From 20dd53a830b6745a89b4203c681e8eea54b079cc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 10:58:08 +0200 Subject: [PATCH 38/96] use separator widget in error dialog --- openpype/tools/utils/error_dialog.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/openpype/tools/utils/error_dialog.py b/openpype/tools/utils/error_dialog.py index f7b12bb69f..30cba56416 100644 --- a/openpype/tools/utils/error_dialog.py +++ b/openpype/tools/utils/error_dialog.py @@ -1,6 +1,6 @@ from Qt import QtWidgets, QtCore -from .widgets import ClickableFrame, ExpandBtn +from .widgets import ClickableFrame, ExpandBtn, SeparatorWidget def convert_text_for_html(text): @@ -139,12 +139,10 @@ class ErrorMessageBox(QtWidgets.QDialog): mime_data ) - def _create_line(self): - line = QtWidgets.QFrame(self) - line.setObjectName("Separator") - line.setMinimumHeight(2) - line.setMaximumHeight(2) - return line + def _create_line(self, parent=None): + if parent is None: + parent = self + return SeparatorWidget(2, parent=parent) def _create_traceback_widget(self, traceback_text, parent=None): if parent is None: From 9ae99eb4badd874b1181697179fc9747963a2c58 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 10:58:34 +0200 Subject: [PATCH 39/96] change function name 'convert_text_for_html' to 'escape_text_for_html' --- openpype/tools/utils/error_dialog.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/tools/utils/error_dialog.py b/openpype/tools/utils/error_dialog.py index 30cba56416..d4ca91848c 100644 --- a/openpype/tools/utils/error_dialog.py +++ b/openpype/tools/utils/error_dialog.py @@ -3,7 +3,7 @@ from Qt import QtWidgets, QtCore from .widgets import ClickableFrame, ExpandBtn, SeparatorWidget -def convert_text_for_html(text): +def escape_text_for_html(text): return ( text .replace("<", "<") @@ -19,7 +19,7 @@ class TracebackWidget(QtWidgets.QWidget): # Modify text to match html # - add more replacements when needed - tb_text = convert_text_for_html(tb_text) + tb_text = escape_text_for_html(tb_text) expand_btn = ExpandBtn(self) clickable_frame = ClickableFrame(self) @@ -110,7 +110,7 @@ class ErrorMessageBox(QtWidgets.QDialog): @staticmethod def convert_text_for_html(text): - return convert_text_for_html(text) + return escape_text_for_html(text) def _create_top_widget(self, parent_widget): label_widget = QtWidgets.QLabel(parent_widget) From ad28ea8121dc62f7dbe708ef3f2bbbe21d111810 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 10:58:55 +0200 Subject: [PATCH 40/96] top widget is optional --- openpype/tools/utils/error_dialog.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/tools/utils/error_dialog.py b/openpype/tools/utils/error_dialog.py index d4ca91848c..9c9015c00f 100644 --- a/openpype/tools/utils/error_dialog.py +++ b/openpype/tools/utils/error_dialog.py @@ -91,11 +91,12 @@ class ErrorMessageBox(QtWidgets.QDialog): footer_layout.addWidget(ok_btn, 0) bottom_line = self._create_line() - body_layout = QtWidgets.QVBoxLayout(self) - body_layout.addWidget(top_widget, 0) - body_layout.addWidget(content_scroll, 1) - body_layout.addWidget(bottom_line, 0) - body_layout.addLayout(footer_layout, 0) + main_layout = QtWidgets.QVBoxLayout(self) + if top_widget is not None: + main_layout.addWidget(top_widget, 0) + main_layout.addWidget(content_scroll, 1) + main_layout.addWidget(bottom_line, 0) + main_layout.addWidget(footer_widget, 0) copy_report_btn.clicked.connect(self._on_copy_report) ok_btn.clicked.connect(self._on_ok_clicked) From 280e4fe0ca3d32ae506969a249034bfd720976f5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 11:00:59 +0200 Subject: [PATCH 41/96] change separator of report copy --- openpype/tools/utils/error_dialog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/tools/utils/error_dialog.py b/openpype/tools/utils/error_dialog.py index 9c9015c00f..6973fda8c4 100644 --- a/openpype/tools/utils/error_dialog.py +++ b/openpype/tools/utils/error_dialog.py @@ -132,7 +132,8 @@ class ErrorMessageBox(QtWidgets.QDialog): self.close() def _on_copy_report(self): - report_text = (10 * "*").join(self._report_data) + sep = "\n{}\n".format(10 * "*") + report_text = sep.join(self._report_data) mime_data = QtCore.QMimeData() mime_data.setText(report_text) From 239d2f90bfd729f1a6fe58883b6fd992e9242de2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 11:01:41 +0200 Subject: [PATCH 42/96] footer layout is under widget and store the widget to self --- openpype/tools/utils/error_dialog.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/tools/utils/error_dialog.py b/openpype/tools/utils/error_dialog.py index 6973fda8c4..5fe49a53af 100644 --- a/openpype/tools/utils/error_dialog.py +++ b/openpype/tools/utils/error_dialog.py @@ -85,7 +85,9 @@ class ErrorMessageBox(QtWidgets.QDialog): copy_report_btn = QtWidgets.QPushButton("Copy report", self) ok_btn = QtWidgets.QPushButton("OK", self) - footer_layout = QtWidgets.QHBoxLayout() + footer_widget = QtWidgets.QWidget(self) + footer_layout = QtWidgets.QHBoxLayout(footer_widget) + footer_layout.setContentsMargins(0, 0, 0, 0) footer_layout.addWidget(copy_report_btn, 0) footer_layout.addStretch(1) footer_layout.addWidget(ok_btn, 0) @@ -107,6 +109,8 @@ class ErrorMessageBox(QtWidgets.QDialog): if not report_data: copy_report_btn.setVisible(False) + self._content_scroll = content_scroll + self._footer_widget = footer_widget self._report_data = report_data @staticmethod From f4b123d65a5a10e871dfdd2adf1fe6c92a2a6727 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 11:01:52 +0200 Subject: [PATCH 43/96] added separator widget to public widgets --- openpype/tools/utils/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py index 5ccc1b40b3..019ea16391 100644 --- a/openpype/tools/utils/__init__.py +++ b/openpype/tools/utils/__init__.py @@ -7,6 +7,7 @@ from .widgets import ( ExpandBtn, PixmapLabel, IconButton, + SeparatorWidget, ) from .views import DeselectableTreeView from .error_dialog import ErrorMessageBox @@ -37,6 +38,7 @@ __all__ = ( "ExpandBtn", "PixmapLabel", "IconButton", + "SeparatorWidget", "DeselectableTreeView", From 36a9aa2da61d583e9ddd8209b92614f751a42bbf Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 11:02:57 +0200 Subject: [PATCH 44/96] modified error dialog to show more information by creator --- openpype/tools/publisher/window.py | 153 +++++++++++++++++++++-------- 1 file changed, 114 insertions(+), 39 deletions(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 4f0b81fa85..1bbc0d0cf4 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -6,6 +6,7 @@ from openpype import ( style ) from openpype.tools.utils import ( + ErrorMessageBox, PlaceholderLineEdit, MessageOverlayObject, PixmapLabel, @@ -223,9 +224,11 @@ class PublisherWindow(QtWidgets.QDialog): # Floating publish frame publish_frame = PublishFrame(controller, self.footer_border, self) - dialog_message_timer = QtCore.QTimer() - dialog_message_timer.setInterval(100) - dialog_message_timer.timeout.connect(self._on_dialog_message_timeout) + creators_dialog_message_timer = QtCore.QTimer() + creators_dialog_message_timer.setInterval(100) + creators_dialog_message_timer.timeout.connect( + self._on_creators_message_timeout + ) help_btn.clicked.connect(self._on_help_click) tabs_widget.tab_changed.connect(self._on_tab_change) @@ -273,6 +276,9 @@ class PublisherWindow(QtWidgets.QDialog): controller.event_system.add_callback( "instances.remove.failed", self._instance_remove_failed ) + controller.event_system.add_callback( + "instances.create.failed", self._instance_create_failed + ) # Store extra header widget for TrayPublisher # - can be used to add additional widgets to header between context @@ -319,8 +325,8 @@ class PublisherWindow(QtWidgets.QDialog): self._restart_timer = None self._publish_frame_visible = None - self._dialog_messages_to_show = collections.deque() - self._dialog_message_timer = dialog_message_timer + self._creators_messages_to_show = collections.deque() + self._creators_dialog_message_timer = creators_dialog_message_timer self._set_publish_visibility(False) @@ -598,58 +604,127 @@ class PublisherWindow(QtWidgets.QDialog): 0, window_size.height() - height ) - def add_message_dialog(self, message, title): - self._dialog_messages_to_show.append((message, title)) - self._dialog_message_timer.start() + def add_message_dialog(self, title, failed_info): + self._creators_messages_to_show.append((title, failed_info)) + self._creators_dialog_message_timer.start() - def _on_dialog_message_timeout(self): - if not self._dialog_messages_to_show: - self._dialog_message_timer.stop() + def _on_creators_message_timeout(self): + if not self._creators_messages_to_show: + self._creators_dialog_message_timer.stop() return - item = self._dialog_messages_to_show.popleft() - message, title = item - dialog = MessageDialog(message, title, self) + item = self._creators_messages_to_show.popleft() + title, failed_info = item + dialog = CreatorsErrorMessageBox(title, failed_info, self) dialog.exec_() dialog.deleteLater() def _instance_collection_failed(self, event): - self.add_message_dialog(event["message"], event["title"]) + self.add_message_dialog(event["title"], event["failed_info"]) def _instance_save_failed(self, event): - self.add_message_dialog(event["message"], event["title"]) + self.add_message_dialog(event["title"], event["failed_info"]) def _instance_remove_failed(self, event): - self.add_message_dialog(event["message"], event["title"]) + self.add_message_dialog(event["title"], event["failed_info"]) + + def _instance_create_failed(self, event): + self.add_message_dialog(event["title"], event["failed_info"]) -class MessageDialog(QtWidgets.QDialog): - def __init__(self, message, title, parent=None): - super(MessageDialog, self).__init__(parent) +class CreatorsErrorMessageBox(ErrorMessageBox): + def __init__(self, error_title, failed_info, parent): + self._failed_info = failed_info + self._info_with_id = [ + {"id": idx, "info": info} + for idx, info in enumerate(failed_info) + ] + self._widgets_by_id = {} + self._tabs_widget = None + self._stack_layout = None - self.setWindowTitle(title or "Something happend") + super(CreatorsErrorMessageBox, self).__init__(error_title, parent) - message_widget = QtWidgets.QLabel(message, self) - message_widget.setWordWrap(True) + layout = self.layout() + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) - btns_widget = QtWidgets.QWidget(self) - submit_btn = QtWidgets.QPushButton("OK", btns_widget) + footer_layout = self._footer_widget.layout() + footer_layout.setContentsMargins(5, 5, 5, 5) - btns_layout = QtWidgets.QHBoxLayout(btns_widget) - btns_layout.setContentsMargins(0, 0, 0, 0) - btns_layout.addStretch(1) - btns_layout.addWidget(submit_btn) + def _create_top_widget(self, parent_widget): + return None - layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(message_widget, 0) - layout.addStretch(1) - layout.addWidget(btns_widget, 0) + 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) + else: + report_message += " {}".format(creator_identifier) - submit_btn.clicked.connect(self._on_submit_click) + report_message += "\n\nError: {}".format(info["message"]) + formatted_traceback = info["traceback"] + if formatted_traceback: + report_message += "\n\n{}".format(formatted_traceback) + output.append(report_message) + return output - def _on_submit_click(self): - self.close() + def _create_content(self, content_layout): + tabs_widget = PublisherTabsWidget(self) - def showEvent(self, event): - super(MessageDialog, self).showEvent(event) - self.resize(400, 200) + stack_widget = QtWidgets.QFrame(self._content_widget) + stack_layout = QtWidgets.QStackedLayout(stack_widget) + + first = True + for item in self._info_with_id: + 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 + + msg_widget = QtWidgets.QWidget(stack_widget) + msg_layout = QtWidgets.QVBoxLayout(msg_widget) + + exc_msg_template = "{}" + message_label_widget = QtWidgets.QLabel(msg_widget) + message_label_widget.setText( + exc_msg_template.format(self.convert_text_for_html(message)) + ) + msg_layout.addWidget(message_label_widget, 0) + + if formatted_traceback: + line_widget = self._create_line(msg_widget) + tb_widget = self._create_traceback_widget(formatted_traceback) + msg_layout.addWidget(line_widget, 0) + msg_layout.addWidget(tb_widget, 0) + + msg_layout.addStretch(1) + + tabs_widget.add_tab(creator_label, item_id) + stack_layout.addWidget(msg_widget) + if first: + first = False + stack_layout.setCurrentWidget(msg_widget) + + self._widgets_by_id[item_id] = msg_widget + + content_layout.addWidget(tabs_widget, 0) + content_layout.addWidget(stack_widget, 1) + + tabs_widget.tab_changed.connect(self._on_tab_change) + + self._tabs_widget = tabs_widget + self._stack_layout = stack_layout + + def _on_tab_change(self, identifier): + widget = self._widgets_by_id[identifier] + self._stack_layout.setCurrentWidget(widget) From 098903260c79be64c58a07eb7616e24c04c41de9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 11:03:15 +0200 Subject: [PATCH 45/96] controller is triggering event on creator operation fail --- openpype/tools/publisher/control.py | 45 +++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index d402ab2434..280d2cf0a0 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1107,6 +1107,8 @@ class AbstractPublisherController(object): options (Dict[str, Any]): Data from pre-create attributes. """ + pass + def save_changes(self): """Save changes in create context.""" @@ -1716,14 +1718,24 @@ class PublisherController(BasePublisherController): with self._create_context.bulk_instances_collection(): try: self._create_context.reset_instances() - self._create_context.execute_autocreators() - except CreatorsOperationFailed as exc: self._emit_event( "instances.collection.failed", { "title": "Instance collection failed", - "message": str(exc) + "failed_info": exc.failed_info + } + ) + + try: + self._create_context.execute_autocreators() + + except CreatorsOperationFailed as exc: + self._emit_event( + "instances.create.failed", + { + "title": "AutoCreation failed", + "failed_info": exc.failed_info } ) @@ -1854,10 +1866,24 @@ class PublisherController(BasePublisherController): self, creator_identifier, subset_name, instance_data, options ): """Trigger creation and refresh of instances in UI.""" - creator = self._creators[creator_identifier] - creator.create(subset_name, instance_data, options) + + success = True + try: + self._create_context.create( + creator_identifier, subset_name, instance_data, options + ) + except CreatorsOperationFailed as exc: + success = False + self._emit_event( + "instances.create.failed", + { + "title": "Creation failed", + "failed_info": exc.failed_info + } + ) self._on_create_instance_change() + return success def save_changes(self): """Save changes happened during creation.""" @@ -1866,12 +1892,13 @@ class PublisherController(BasePublisherController): try: self._create_context.save_changes() + except CreatorsOperationFailed as exc: self._emit_event( "instances.save.failed", { - "title": "Save failed", - "message": str(exc) + "title": "Instances save failed", + "failed_info": exc.failed_info } ) @@ -1902,8 +1929,8 @@ class PublisherController(BasePublisherController): self._emit_event( "instances.remove.failed", { - "title": "Remove failed", - "message": str(exc) + "title": "Instance removement failed", + "failed_info": exc.failed_info } ) From accab0ca5f17996ff0db8e178dae1782eec31c4a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 11:03:30 +0200 Subject: [PATCH 46/96] create widget does not handle failed creation on it's own --- .../tools/publisher/widgets/create_widget.py | 118 ++---------------- 1 file changed, 7 insertions(+), 111 deletions(-) diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py index 10cf39675e..910b2adfc7 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -9,7 +9,6 @@ from openpype.pipeline.create import ( SUBSET_NAME_ALLOWED_SYMBOLS, TaskNotSetError, ) -from openpype.tools.utils import ErrorMessageBox from .widgets import ( IconValuePixmapLabel, @@ -35,79 +34,6 @@ class VariantInputsWidget(QtWidgets.QWidget): self.resized.emit() -class CreateErrorMessageBox(ErrorMessageBox): - def __init__( - self, - creator_label, - subset_name, - asset_name, - exc_msg, - formatted_traceback, - parent - ): - self._creator_label = creator_label - self._subset_name = subset_name - self._asset_name = asset_name - self._exc_msg = exc_msg - self._formatted_traceback = formatted_traceback - super(CreateErrorMessageBox, self).__init__("Creation failed", parent) - - def _create_top_widget(self, parent_widget): - label_widget = QtWidgets.QLabel(parent_widget) - label_widget.setText( - "Failed to create" - ) - return label_widget - - def _get_report_data(self): - report_message = ( - "{creator}: Failed to create Subset: \"{subset}\"" - " in Asset: \"{asset}\"" - "\n\nError: {message}" - ).format( - creator=self._creator_label, - subset=self._subset_name, - asset=self._asset_name, - message=self._exc_msg, - ) - if self._formatted_traceback: - report_message += "\n\n{}".format(self._formatted_traceback) - return [report_message] - - def _create_content(self, content_layout): - item_name_template = ( - "Creator: {}
" - "Subset: {}
" - "Asset: {}
" - ) - exc_msg_template = "{}" - - line = self._create_line() - content_layout.addWidget(line) - - item_name_widget = QtWidgets.QLabel(self) - item_name_widget.setText( - item_name_template.format( - self._creator_label, self._subset_name, self._asset_name - ) - ) - content_layout.addWidget(item_name_widget) - - message_label_widget = QtWidgets.QLabel(self) - message_label_widget.setText( - exc_msg_template.format(self.convert_text_for_html(self._exc_msg)) - ) - content_layout.addWidget(message_label_widget) - - if self._formatted_traceback: - line_widget = self._create_line() - tb_widget = self._create_traceback_widget( - self._formatted_traceback - ) - content_layout.addWidget(line_widget) - content_layout.addWidget(tb_widget) - - # TODO add creator identifier/label to details class CreatorShortDescWidget(QtWidgets.QWidget): def __init__(self, parent=None): @@ -178,8 +104,6 @@ class CreateWidget(QtWidgets.QWidget): self._prereq_available = False - self._message_dialog = None - name_pattern = "^[{}]*$".format(SUBSET_NAME_ALLOWED_SYMBOLS) self._name_pattern = name_pattern self._compiled_name_pattern = re.compile(name_pattern) @@ -769,7 +693,6 @@ class CreateWidget(QtWidgets.QWidget): return index = indexes[0] - creator_label = index.data(QtCore.Qt.DisplayRole) creator_identifier = index.data(CREATOR_IDENTIFIER_ROLE) family = index.data(FAMILY_ROLE) variant = self.variant_input.text() @@ -792,40 +715,13 @@ class CreateWidget(QtWidgets.QWidget): "family": family } - error_msg = None - formatted_traceback = None - try: - self._controller.create( - creator_identifier, - subset_name, - instance_data, - pre_create_data - ) + success = self._controller.create( + creator_identifier, + subset_name, + instance_data, + pre_create_data + ) - except CreatorError as exc: - error_msg = str(exc) - - # Use bare except because some hosts raise their exceptions that - # do not inherit from python's `BaseException` - except: - exc_type, exc_value, exc_traceback = sys.exc_info() - formatted_traceback = "".join(traceback.format_exception( - exc_type, exc_value, exc_traceback - )) - error_msg = str(exc_value) - - if error_msg is None: + if success: self._set_creator(self._selected_creator) self._controller.emit_card_message("Creation finished...") - else: - box = CreateErrorMessageBox( - creator_label, - subset_name, - asset_name, - error_msg, - formatted_traceback, - parent=self - ) - box.show() - # Store dialog so is not garbage collected before is shown - self._message_dialog = box From bda1bb3f292c05004546870ddd39d4c4df8bd384 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 11:46:02 +0200 Subject: [PATCH 47/96] unify the error handling in create context --- openpype/pipeline/create/context.py | 123 +++++++++++++++++----------- 1 file changed, 76 insertions(+), 47 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index dfa9049601..2dfdfc142f 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -123,8 +123,6 @@ def prepare_failed_creator_operation_info( ): formatted_traceback = None exc_type, exc_value, exc_traceback = exc_info - error_msg = str(exc_value) - if add_traceback: formatted_traceback = "".join(traceback.format_exception( exc_type, exc_value, exc_traceback @@ -133,7 +131,7 @@ def prepare_failed_creator_operation_info( return { "creator_identifier": identifier, "creator_label": label, - "message": error_msg, + "message": str(exc_value), "traceback": formatted_traceback } @@ -1274,10 +1272,12 @@ class CreateContext: **kwargs (Dict[Any, Any]): Keyword argument for create method. """ + error_message = "Failed to run Creator with identifier \"{}\". {}" creator = self.creators.get(identifier) label = getattr(creator, "label", None) failed = False add_traceback = False + exc_info = None try: # Fake CreatorError (Could be maybe specific exception?) if creator is None: @@ -1290,27 +1290,23 @@ class CreateContext: except CreatorError: failed = True exc_info = sys.exc_info() + self.log.warning(error_message.format(identifier, exc_info[1])) except: failed = True add_traceback = True exc_info = sys.exc_info() - - if not failed: - return - - self.log.warning( - ( - "Failed to run Creator with identifier \"{}\"." - ).format(identifier), - exc_info=add_traceback - ) - - raise CreatorsCreateFailed([ - prepare_failed_creator_operation_info( - identifier, label, exc_info, add_traceback + self.log.warning( + error_message.format(identifier, ""), + exc_info=True ) - ]) + + if failed: + raise CreatorsCreateFailed([ + prepare_failed_creator_operation_info( + identifier, label, exc_info, add_traceback + ) + ]) def creator_removed_instance(self, instance): """When creator removes instance context should be acknowledged. @@ -1357,23 +1353,35 @@ class CreateContext: self._instances_by_id = {} # Collect instances + error_message = "Collection of instances for creator {} failed. {}" failed_info = [] for creator in self.creators.values(): label = creator.label + identifier = creator.identifier + failed = False + add_traceback = False + exc_info = None try: creator.collect_instances() - except: + + except CreatorError: + failed = True + exc_info = sys.exc_info() + self.log.warning(error_message.format(identifier, exc_info[1])) + + except: + failed = True + add_traceback = True exc_info = sys.exc_info() - identifier = creator.identifier self.log.warning( - ( - "Collection of instances for creator {} failed" - ).format(identifier), + error_message.format(identifier, ""), exc_info=True ) + + if failed: failed_info.append( prepare_failed_creator_operation_info( - identifier, label, exc_info + identifier, label, exc_info, add_traceback ) ) @@ -1386,6 +1394,7 @@ class CreateContext: Reset instances if any autocreator executed properly. """ + error_message = "Failed to run AutoCreator with identifier \"{}\". {}" failed_info = [] for identifier, creator in self.autocreators.items(): label = creator.label @@ -1397,6 +1406,7 @@ class CreateContext: except CreatorError: failed = True exc_info = sys.exc_info() + self.log.warning(error_message.format(identifier, exc_info[1])) # Use bare except because some hosts raise their exceptions that # do not inherit from python's `BaseException` @@ -1404,22 +1414,17 @@ class CreateContext: failed = True add_traceback = True exc_info = sys.exc_info() - - if not failed: - continue - - failed_info.append( - prepare_failed_creator_operation_info( - identifier, label, exc_info, add_traceback + self.log.warning( + error_message.format(identifier, ""), + exc_info=True ) - ) - self.log.warning( - ( - "Failed to run AutoCreator with identifier \"{}\"." - ).format(identifier), - exc_info=exc_info - ) + if failed: + failed_info.append( + prepare_failed_creator_operation_info( + identifier, label, exc_info, add_traceback + ) + ) if failed_info: raise CreatorsCreateFailed(failed_info) @@ -1499,6 +1504,7 @@ class CreateContext: identifier = instance.creator_identifier instances_by_identifier[identifier].append(instance) + error_message = "Instances update of creator \"{}\" failed. {}" failed_info = [] for identifier, creator_instances in instances_by_identifier.items(): update_list = [] @@ -1512,20 +1518,28 @@ class CreateContext: continue label = creator.label + failed = False + add_traceback = False + exc_info = None try: creator.update_instances(update_list) + except CreatorError: + failed = True + exc_info = sys.exc_info() + self.log.warning(error_message.format(identifier, exc_info[1])) + except: + failed = True + add_traceback = True exc_info = sys.exc_info() self.log.warning( - "Instances update of creator \"{}\" failed".format( - identifier), - exc_info=True - ) + error_message.format(identifier, ""), exc_info=True) + if failed: failed_info.append( prepare_failed_creator_operation_info( - identifier, label, exc_info + identifier, label, exc_info, add_traceback ) ) @@ -1545,22 +1559,37 @@ class CreateContext: identifier = instance.creator_identifier instances_by_identifier[identifier].append(instance) + error_message = "Instances removement of creator \"{}\" failed. {}" failed_info = [] for identifier, creator_instances in instances_by_identifier.items(): creator = self.creators.get(identifier) label = creator.label + failed = False + add_traceback = False + exc_info = None try: creator.remove_instances(creator_instances) - except: + + except CreatorError: + failed = True exc_info = sys.exc_info() self.log.warning( - "Instances removement of creator \"{}\" failed".format( - identifier), + error_message.format(identifier, exc_info[1]) + ) + + except: + failed = True + add_traceback = True + exc_info = sys.exc_info() + self.log.warning( + error_message.format(identifier, ""), exc_info=True ) + + if failed: failed_info.append( prepare_failed_creator_operation_info( - identifier, label, exc_info + identifier, label, exc_info, add_traceback ) ) From 1f2e54c2c385af56c5d61d2976e8db434d9cb4ec Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 11:46:21 +0200 Subject: [PATCH 48/96] fix tab switch --- openpype/tools/publisher/window.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 1bbc0d0cf4..b6bd506c18 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -636,7 +636,8 @@ class CreatorsErrorMessageBox(ErrorMessageBox): def __init__(self, error_title, failed_info, parent): self._failed_info = failed_info self._info_with_id = [ - {"id": idx, "info": info} + # Id must be string when used in tab widget + {"id": str(idx), "info": info} for idx, info in enumerate(failed_info) ] self._widgets_by_id = {} @@ -725,6 +726,6 @@ class CreatorsErrorMessageBox(ErrorMessageBox): self._tabs_widget = tabs_widget self._stack_layout = stack_layout - def _on_tab_change(self, identifier): + def _on_tab_change(self, old_identifier, identifier): widget = self._widgets_by_id[identifier] self._stack_layout.setCurrentWidget(widget) From d29cd8edcdd678370b18ae57815942c82d7e6611 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 21 Oct 2022 14:32:16 +0200 Subject: [PATCH 49/96] workflows: adding milestone creator and assigner --- .github/workflows/milestone_assign.yml | 28 ++++++++++++ .github/workflows/milestone_create.yml | 62 ++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 .github/workflows/milestone_assign.yml create mode 100644 .github/workflows/milestone_create.yml diff --git a/.github/workflows/milestone_assign.yml b/.github/workflows/milestone_assign.yml new file mode 100644 index 0000000000..b41886816b --- /dev/null +++ b/.github/workflows/milestone_assign.yml @@ -0,0 +1,28 @@ +name: Milestone - assign to PRs + +on: + pull_request_target: + types: [opened, reopened, edited] + +jobs: + run_if_release: + if: startsWith(github.base_ref, 'release/') + runs-on: ubuntu-latest + steps: + - name: 'Assign Milestone [next-minor]' + if: github.event.pull_request.milestone == null + uses: zoispag/action-assign-milestone@v1 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + milestone: 'next-minor' + + run_if_develop: + if: ${{ github.base_ref == 'develop' }} + runs-on: ubuntu-latest + steps: + - name: 'Assign Milestone [next-patch]' + if: github.event.pull_request.milestone == null + uses: zoispag/action-assign-milestone@v1 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + milestone: 'next-patch' \ No newline at end of file diff --git a/.github/workflows/milestone_create.yml b/.github/workflows/milestone_create.yml new file mode 100644 index 0000000000..b56ca81dc1 --- /dev/null +++ b/.github/workflows/milestone_create.yml @@ -0,0 +1,62 @@ +name: Milestone - create default + +on: + milestone: + types: [closed, edited] + +jobs: + generate-next-patch: + runs-on: ubuntu-latest + steps: + - name: 'Get Milestones' + uses: "WyriHaximus/github-action-get-milestones@master" + id: milestones + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + + - run: printf "name=number::%s" $(printenv MILESTONES | jq --arg MILESTONE $(printenv MILESTONE) '.[] | select(.title == $MILESTONE) | .number') + id: querymilestone + env: + MILESTONES: ${{ steps.milestones.outputs.milestones }} + MILESTONE: "next-patch" + + - name: Read output + run: | + echo "${{ steps.querymilestone.outputs.number }}" + + - name: 'Create `next-patch` milestone' + if: steps.querymilestone.outputs.number == '' + id: createmilestone + uses: "WyriHaximus/github-action-create-milestone@v1" + with: + title: 'next-patch' + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + + generate-next-minor: + runs-on: ubuntu-latest + steps: + - name: 'Get Milestones' + uses: "WyriHaximus/github-action-get-milestones@master" + id: milestones + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + + - run: printf "name=number::%s" $(printenv MILESTONES | jq --arg MILESTONE $(printenv MILESTONE) '.[] | select(.title == $MILESTONE) | .number') + id: querymilestone + env: + MILESTONES: ${{ steps.milestones.outputs.milestones }} + MILESTONE: "next-minor" + + - name: Read output + run: | + echo "${{ steps.querymilestone.outputs.number }}" + + - name: 'Create `next-minor` milestone' + if: steps.querymilestone.outputs.number == '' + id: createmilestone + uses: "WyriHaximus/github-action-create-milestone@v1" + with: + title: 'next-minor' + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" \ No newline at end of file From 260573506b56d83d73ea785b335aa9134d652d96 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 14:56:09 +0200 Subject: [PATCH 50/96] 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 51/96] 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 52/96] 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 53/96] 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 3de3d303895cfe4bb92aaf373613d9d54871b432 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 16:47:06 +0200 Subject: [PATCH 54/96] pass creator to cache function --- openpype/hosts/traypublisher/api/plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index 2cb5a8729f..555041d389 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -37,7 +37,7 @@ class HiddenTrayPublishCreator(HiddenCreator): host_name = "traypublisher" def collect_instances(self): - for instance_data in _cache_and_get_instances(): + for instance_data in _cache_and_get_instances(self): creator_id = instance_data.get("creator_identifier") if creator_id == self.identifier: instance = CreatedInstance.from_existing( @@ -74,7 +74,7 @@ class TrayPublishCreator(Creator): host_name = "traypublisher" def collect_instances(self): - for instance_data in _cache_and_get_instances(): + for instance_data in _cache_and_get_instances(self): creator_id = instance_data.get("creator_identifier") if creator_id == self.identifier: instance = CreatedInstance.from_existing( From 24ebd76bd90ef5705434b6ff26c34f294ce96dc5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 17:55:56 +0200 Subject: [PATCH 55/96] 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 56/96] 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 57/96] 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 58/96] 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 59/96] 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 60/96] 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 61/96] 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 62/96] [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 63/96] 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 64/96] 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 65/96] =?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 66/96] [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 67/96] 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 68/96] 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 69/96] 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 70/96] 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 71/96] [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 72/96] [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 73/96] 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 74/96] 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 75/96] 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 76/96] 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 77/96] 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 78/96] 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 79/96] 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 80/96] 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 81/96] 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 82/96] 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 83/96] 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 84/96] 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 85/96] 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 86/96] 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 87/96] 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 88/96] 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 89/96] 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 From 72287eb3d375d6c999c961296ecb1cf3b34a6761 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 25 Oct 2022 10:23:15 +0200 Subject: [PATCH 90/96] validate source streams before otio burnins super is called --- openpype/scripts/otio_burnin.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py index 4c3a5de2ec..3520d8668c 100644 --- a/openpype/scripts/otio_burnin.py +++ b/openpype/scripts/otio_burnin.py @@ -113,11 +113,20 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins): if not ffprobe_data: ffprobe_data = _get_ffprobe_data(source) + # Validate 'streams' before calling super to raise more specific + # error + source_streams = ffprobe_data.get("streams") + if not source_streams: + raise ValueError(( + "Input file \"{}\" does not contain any streams" + " with image/video content." + ).format(source)) + self.ffprobe_data = ffprobe_data self.first_frame = first_frame self.input_args = [] - super().__init__(source, ffprobe_data["streams"]) + super().__init__(source, source_streams) if options_init: self.options_init.update(options_init) From 438922ccb0e7bd12c559e61c07f782c6d3537929 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 25 Oct 2022 10:31:53 +0200 Subject: [PATCH 91/96] ffprobe run as list of args instead of string --- openpype/scripts/otio_burnin.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py index 3520d8668c..7223e8d4de 100644 --- a/openpype/scripts/otio_burnin.py +++ b/openpype/scripts/otio_burnin.py @@ -22,10 +22,6 @@ FFMPEG = ( '"{}"%(input_args)s -i "%(input)s" %(filters)s %(args)s%(output)s' ).format(ffmpeg_path) -FFPROBE = ( - '"{}" -v quiet -print_format json -show_format -show_streams "%(source)s"' -).format(ffprobe_path) - DRAWTEXT = ( "drawtext=fontfile='%(font)s':text=\\'%(text)s\\':" "x=%(x)s:y=%(y)s:fontcolor=%(color)s@%(opacity).1f:fontsize=%(size)d" @@ -48,8 +44,15 @@ def _get_ffprobe_data(source): :param str source: source media file :rtype: [{}, ...] """ - command = FFPROBE % {'source': source} - proc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE) + command = [ + ffprobe_path, + "-v", "quiet", + "-print_format", "json", + "-show_format", + "-show_streams", + source + ] + proc = subprocess.Popen(command, stdout=subprocess.PIPE) out = proc.communicate()[0] if proc.returncode != 0: raise RuntimeError("Failed to run: %s" % command) From 6f642ab34c09c617b0a0a10adc6d1821b901f337 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 25 Oct 2022 16:56:28 +0200 Subject: [PATCH 92/96] trigger reset of controller when conversion finishes --- openpype/pipeline/create/context.py | 3 ++- openpype/tools/publisher/control.py | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index c87803c5c4..52a1729233 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1823,7 +1823,8 @@ class CreateContext: def run_convertors(self, convertor_identifiers): """Run convertor plugins by idenfitifiers. - Conversion is skipped if convertor is not available. + Conversion is skipped if convertor is not available. It is recommended + to trigger reset after conversion to reload instances. Args: convertor_identifiers (Iterator[str]): Identifiers of convertors diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 18d1a5b083..e05cffe20e 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1906,6 +1906,8 @@ class PublisherController(BasePublisherController): ) def trigger_convertor_items(self, convertor_identifiers): + self.save_changes() + success = True try: self._create_context.run_convertors(convertor_identifiers) @@ -1924,7 +1926,8 @@ class PublisherController(BasePublisherController): self.emit_card_message("Conversion finished") else: self.emit_card_message("Conversion failed", CardMessageTypes.error) - self._on_create_instance_change() + + self.reset() def create( self, creator_identifier, subset_name, instance_data, options @@ -1972,7 +1975,6 @@ class PublisherController(BasePublisherController): Args: instance_ids (List[str]): List of instance ids to remove. """ - # TODO expect instance ids instead of instances # QUESTION Expect that instances are really removed? In that case save # reset is not required and save changes too. self.save_changes() From 698fe8379ea78901418f4cc4f1d6f8fc941c40ae Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 25 Oct 2022 16:57:33 +0200 Subject: [PATCH 93/96] added logger to convertor --- openpype/pipeline/create/creator_plugins.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 584e082221..c69abb8861 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -61,9 +61,23 @@ class SubsetConvertorPlugin(object): create_context """ + _log = None + def __init__(self, create_context): self._create_context = create_context + @property + def log(self): + """Logger of the plugin. + + Returns: + logging.Logger: Logger with name of the plugin. + """ + + if self._log is None: + self._log = Logger.get_logger(self.__class__.__name__) + return self._log + @abstractproperty def identifier(self): """Converted identifier. From 9b74287bb2d746da8f128a707d7e80785888d571 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 26 Oct 2022 03:58:16 +0000 Subject: [PATCH 94/96] [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 b1e4227030..bf36fc4b10 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.14.5" +__version__ = "3.14.6-nightly.1" From 3eda8aa64bb2f975a5577344fc065aec5130e725 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 26 Oct 2022 11:46:52 +0200 Subject: [PATCH 95/96] add milestone worlfow to main --- .github/workflows/milestone_assign.yml | 28 ++++++++++++ .github/workflows/milestone_create.yml | 62 ++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 .github/workflows/milestone_assign.yml create mode 100644 .github/workflows/milestone_create.yml diff --git a/.github/workflows/milestone_assign.yml b/.github/workflows/milestone_assign.yml new file mode 100644 index 0000000000..b41886816b --- /dev/null +++ b/.github/workflows/milestone_assign.yml @@ -0,0 +1,28 @@ +name: Milestone - assign to PRs + +on: + pull_request_target: + types: [opened, reopened, edited] + +jobs: + run_if_release: + if: startsWith(github.base_ref, 'release/') + runs-on: ubuntu-latest + steps: + - name: 'Assign Milestone [next-minor]' + if: github.event.pull_request.milestone == null + uses: zoispag/action-assign-milestone@v1 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + milestone: 'next-minor' + + run_if_develop: + if: ${{ github.base_ref == 'develop' }} + runs-on: ubuntu-latest + steps: + - name: 'Assign Milestone [next-patch]' + if: github.event.pull_request.milestone == null + uses: zoispag/action-assign-milestone@v1 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + milestone: 'next-patch' \ No newline at end of file diff --git a/.github/workflows/milestone_create.yml b/.github/workflows/milestone_create.yml new file mode 100644 index 0000000000..b56ca81dc1 --- /dev/null +++ b/.github/workflows/milestone_create.yml @@ -0,0 +1,62 @@ +name: Milestone - create default + +on: + milestone: + types: [closed, edited] + +jobs: + generate-next-patch: + runs-on: ubuntu-latest + steps: + - name: 'Get Milestones' + uses: "WyriHaximus/github-action-get-milestones@master" + id: milestones + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + + - run: printf "name=number::%s" $(printenv MILESTONES | jq --arg MILESTONE $(printenv MILESTONE) '.[] | select(.title == $MILESTONE) | .number') + id: querymilestone + env: + MILESTONES: ${{ steps.milestones.outputs.milestones }} + MILESTONE: "next-patch" + + - name: Read output + run: | + echo "${{ steps.querymilestone.outputs.number }}" + + - name: 'Create `next-patch` milestone' + if: steps.querymilestone.outputs.number == '' + id: createmilestone + uses: "WyriHaximus/github-action-create-milestone@v1" + with: + title: 'next-patch' + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + + generate-next-minor: + runs-on: ubuntu-latest + steps: + - name: 'Get Milestones' + uses: "WyriHaximus/github-action-get-milestones@master" + id: milestones + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + + - run: printf "name=number::%s" $(printenv MILESTONES | jq --arg MILESTONE $(printenv MILESTONE) '.[] | select(.title == $MILESTONE) | .number') + id: querymilestone + env: + MILESTONES: ${{ steps.milestones.outputs.milestones }} + MILESTONE: "next-minor" + + - name: Read output + run: | + echo "${{ steps.querymilestone.outputs.number }}" + + - name: 'Create `next-minor` milestone' + if: steps.querymilestone.outputs.number == '' + id: createmilestone + uses: "WyriHaximus/github-action-create-milestone@v1" + with: + title: 'next-minor' + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" \ No newline at end of file From 3ce9bd26ffea646dd3373af039f1bba8fd0c18fa Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 26 Oct 2022 11:53:32 +0200 Subject: [PATCH 96/96] adding synchronized to workflow --- .github/workflows/milestone_assign.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/milestone_assign.yml b/.github/workflows/milestone_assign.yml index b41886816b..c5a231e59e 100644 --- a/.github/workflows/milestone_assign.yml +++ b/.github/workflows/milestone_assign.yml @@ -2,7 +2,7 @@ name: Milestone - assign to PRs on: pull_request_target: - types: [opened, reopened, edited] + types: [opened, reopened, edited, synchronize] jobs: run_if_release: