From 8f04c3351b51508245ad50780e511ac20aff2b90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 5 Oct 2022 12:12:53 +0200 Subject: [PATCH 01/90] Fix: 2 fixes, nb_frames and Shot type error --- .../modules/kitsu/utils/update_op_with_zou.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 10e80b3c89..4cdc53497b 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -115,7 +115,8 @@ def update_op_assets( item_data["frameStart"] = frame_in # Frames duration, fallback on 0 try: - frames_duration = int(item_data.pop("nb_frames", 0)) + # NOTE nb_frames is stored directly in item because of zou's legacy design + frames_duration = int(item.get("nb_frames", 0)) except (TypeError, ValueError): frames_duration = 0 # Frame out, fallback on frame_in + duration or project's value or 1001 @@ -170,7 +171,7 @@ def update_op_assets( # Substitute item type for general classification (assets or shots) if item_type in ["Asset", "AssetType"]: entity_root_asset_name = "Assets" - elif item_type in ["Episode", "Sequence"]: + elif item_type in ["Episode", "Sequence", "Shot"]: entity_root_asset_name = "Shots" # Root parent folder if exist @@ -276,11 +277,13 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: match_res = re.match(r"(\d+)x(\d+)", project["resolution"]) if match_res: - project_data['resolutionWidth'] = int(match_res.group(1)) - project_data['resolutionHeight'] = int(match_res.group(2)) + project_data["resolutionWidth"] = int(match_res.group(1)) + project_data["resolutionHeight"] = int(match_res.group(2)) else: - log.warning(f"\'{project['resolution']}\' does not match the expected" - " format for the resolution, for example: 1920x1080") + log.warning( + f"'{project['resolution']}' does not match the expected" + " format for the resolution, for example: 1920x1080" + ) return UpdateOne( {"_id": project_doc["_id"]}, From 0337403182459fc7babc7386242d61fc3ac261f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 5 Oct 2022 12:17:19 +0200 Subject: [PATCH 02/90] line length --- openpype/modules/kitsu/utils/update_op_with_zou.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 4cdc53497b..a0cacc11cb 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -115,7 +115,8 @@ def update_op_assets( item_data["frameStart"] = frame_in # Frames duration, fallback on 0 try: - # NOTE nb_frames is stored directly in item because of zou's legacy design + # NOTE nb_frames is stored directly in item because + # of zou's legacy design frames_duration = int(item.get("nb_frames", 0)) except (TypeError, ValueError): frames_duration = 0 From 4aec32af91334021d3f943f6886343b5d3490ef9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 5 Oct 2022 12:30:20 +0200 Subject: [PATCH 03/90] line length --- openpype/modules/kitsu/utils/update_op_with_zou.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index a0cacc11cb..2d14b38bc4 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -115,8 +115,8 @@ def update_op_assets( item_data["frameStart"] = frame_in # Frames duration, fallback on 0 try: - # NOTE nb_frames is stored directly in item because - # of zou's legacy design + # NOTE nb_frames is stored directly in item + # because of zou's legacy design frames_duration = int(item.get("nb_frames", 0)) except (TypeError, ValueError): frames_duration = 0 From ff2453c70d04bf38762736d9f7168d159e91379c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 5 Oct 2022 17:22:04 +0200 Subject: [PATCH 04/90] PublisherController can be imported without import of Qt --- openpype/tools/publisher/__init__.py | 7 -- openpype/tools/publisher/control.py | 75 +++------------------- openpype/tools/publisher/control_qt.py | 88 ++++++++++++++++++++++++++ openpype/tools/publisher/window.py | 7 +- 4 files changed, 102 insertions(+), 75 deletions(-) create mode 100644 openpype/tools/publisher/control_qt.py diff --git a/openpype/tools/publisher/__init__.py b/openpype/tools/publisher/__init__.py index a7b597eece..e69de29bb2 100644 --- a/openpype/tools/publisher/__init__.py +++ b/openpype/tools/publisher/__init__.py @@ -1,7 +0,0 @@ -from .app import show -from .window import PublisherWindow - -__all__ = ( - "show", - "PublisherWindow" -) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 481fb5981b..af0556afc5 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -14,14 +14,13 @@ from openpype.pipeline import ( ) from openpype.pipeline.create import CreateContext -from Qt import QtCore - # Define constant for plugin orders offset PLUGIN_ORDER_OFFSET = 0.5 class MainThreadItem: """Callback with args and kwargs.""" + def __init__(self, callback, *args, **kwargs): self.callback = callback self.args = args @@ -31,64 +30,9 @@ class MainThreadItem: self.callback(*self.args, **self.kwargs) -class MainThreadProcess(QtCore.QObject): - """Qt based main thread process executor. - - Has timer which controls each 50ms if there is new item to process. - - This approach gives ability to update UI meanwhile plugin is in progress. - """ - - count_timeout = 2 - - def __init__(self): - super(MainThreadProcess, self).__init__() - self._items_to_process = collections.deque() - - timer = QtCore.QTimer() - timer.setInterval(0) - - timer.timeout.connect(self._execute) - - self._timer = timer - self._switch_counter = self.count_timeout - - def process(self, func, *args, **kwargs): - item = MainThreadItem(func, *args, **kwargs) - self.add_item(item) - - def add_item(self, item): - self._items_to_process.append(item) - - def _execute(self): - if not self._items_to_process: - return - - if self._switch_counter > 0: - self._switch_counter -= 1 - return - - self._switch_counter = self.count_timeout - - item = self._items_to_process.popleft() - item.process() - - def start(self): - if not self._timer.isActive(): - self._timer.start() - - def stop(self): - if self._timer.isActive(): - self._timer.stop() - - def clear(self): - if self._timer.isActive(): - self._timer.stop() - self._items_to_process = collections.deque() - - class AssetDocsCache: """Cache asset documents for creation part.""" + projection = { "_id": True, "name": True, @@ -133,6 +77,7 @@ class PublishReport: Report keeps current state of publishing and currently processed plugin. """ + def __init__(self, controller): self.controller = controller self._publish_discover_result = None @@ -341,7 +286,7 @@ class PublishReport: return output -class PublisherController: +class PublisherController(object): """Middleware between UI, CreateContext and publish Context. Handle both creation and publishing parts. @@ -394,8 +339,6 @@ class PublisherController: pyblish.api.ValidatorOrder + PLUGIN_ORDER_OFFSET ) - # Qt based main thread processor - self._main_thread_processor = MainThreadProcess() # Plugin iterator self._main_thread_iter = None @@ -744,7 +687,7 @@ class PublisherController: self._publish_up_validation = False self._publish_finished = False self._publish_comment_is_set = False - self._main_thread_processor.clear() + self._main_thread_iter = self._publish_iterator() self._publish_context = pyblish.api.Context() # Make sure "comment" is set on publish context @@ -792,13 +735,12 @@ class PublisherController: self._publish_is_running = True self._emit_event("publish.process.started") - self._main_thread_processor.start() + self._publish_next_process() def _stop_publish(self): """Stop or pause publishing.""" self._publish_is_running = False - self._main_thread_processor.stop() self._emit_event("publish.process.stopped") @@ -837,7 +779,10 @@ class PublisherController: else: item = next(self._main_thread_iter) - self._main_thread_processor.add_item(item) + self._process_main_thread_item(item) + + def _process_main_thread_item(self, item): + item() def _publish_iterator(self): """Main logic center of publishing. diff --git a/openpype/tools/publisher/control_qt.py b/openpype/tools/publisher/control_qt.py new file mode 100644 index 0000000000..add7c4c7e4 --- /dev/null +++ b/openpype/tools/publisher/control_qt.py @@ -0,0 +1,88 @@ +import collections + +from Qt import QtCore + +from .control import MainThreadItem, PublisherController + + +class MainThreadProcess(QtCore.QObject): + """Qt based main thread process executor. + + Has timer which controls each 50ms if there is new item to process. + + This approach gives ability to update UI meanwhile plugin is in progress. + """ + + count_timeout = 2 + + def __init__(self): + super(MainThreadProcess, self).__init__() + self._items_to_process = collections.deque() + + timer = QtCore.QTimer() + timer.setInterval(0) + + timer.timeout.connect(self._execute) + + self._timer = timer + self._switch_counter = self.count_timeout + + def process(self, func, *args, **kwargs): + item = MainThreadItem(func, *args, **kwargs) + self.add_item(item) + + def add_item(self, item): + self._items_to_process.append(item) + + def _execute(self): + if not self._items_to_process: + return + + if self._switch_counter > 0: + self._switch_counter -= 1 + return + + self._switch_counter = self.count_timeout + + item = self._items_to_process.popleft() + item.process() + + def start(self): + if not self._timer.isActive(): + self._timer.start() + + def stop(self): + if self._timer.isActive(): + self._timer.stop() + + def clear(self): + if self._timer.isActive(): + self._timer.stop() + self._items_to_process = collections.deque() + + +class QtPublisherController(PublisherController): + def __init__(self, *args, **kwargs): + self._main_thread_processor = MainThreadProcess() + + super(QtPublisherController, self).__init__(*args, **kwargs) + + self._event_system.add_callback( + "publish.process.started", self._qt_on_publish_start + ) + self._event_system.add_callback( + "publish.process.stopped", self._qt_on_publish_stop + ) + + def _reset_publish(self): + super(QtPublisherController, self)._reset_publish() + self._main_thread_processor.clear() + + def _process_main_thread_item(self, item): + self._main_thread_processor.add_item(item) + + def _qt_on_publish_start(self): + self._main_thread_processor.start() + + def _qt_on_publish_stop(self): + self._main_thread_processor.stop() diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index aa5f08eed4..699cf6f1f9 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -11,7 +11,7 @@ from openpype.tools.utils import ( ) from .publish_report_viewer import PublishReportViewerWidget -from .control import PublisherController +from .control_qt import QtPublisherController from .widgets import ( OverviewWidget, ValidationsWidget, @@ -34,7 +34,7 @@ class PublisherWindow(QtWidgets.QDialog): default_width = 1300 default_height = 800 - def __init__(self, parent=None, reset_on_show=None): + def __init__(self, parent=None, controller=None, reset_on_show=None): super(PublisherWindow, self).__init__(parent) self.setWindowTitle("OpenPype publisher") @@ -59,7 +59,8 @@ class PublisherWindow(QtWidgets.QDialog): | on_top_flag ) - controller = PublisherController() + if controller is None: + controller = QtPublisherController() help_dialog = HelpDialog(controller, self) From a00dafb4b6cfa9fa5c1035ac320fbb4c429a45e3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 5 Oct 2022 17:30:47 +0200 Subject: [PATCH 05/90] change few attributes to private --- openpype/tools/publisher/control.py | 63 ++++++++++++++++------------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index af0556afc5..9abcc620a8 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -296,16 +296,17 @@ class PublisherController(object): headless (bool): Headless publishing. ATM not implemented or used. """ + _log = None + def __init__(self, dbcon=None, headless=False): - self.log = logging.getLogger("PublisherController") - self.host = registered_host() - self.headless = headless + self._host = registered_host() + self._headless = headless # Inner event system of controller self._event_system = EventSystem() - self.create_context = CreateContext( - self.host, dbcon, headless=headless, reset=False + self._create_context = CreateContext( + self._host, dbcon, headless=headless, reset=False ) # pyblish.api.Context @@ -349,6 +350,12 @@ class PublisherController(object): # Cacher of avalon documents self._asset_docs_cache = AssetDocsCache(self) + @property + def log(self): + if self._log is None: + self._log = logging.getLogger("PublisherController") + return self._log + @property def project_name(self): """Current project context defined by host. @@ -357,7 +364,7 @@ class PublisherController(object): str: Project name. """ - return self.host.get_current_context()["project_name"] + return self._host.get_current_context()["project_name"] @property def current_asset_name(self): @@ -367,7 +374,7 @@ class PublisherController(object): Union[str, None]: Asset name or None if asset is not set. """ - return self.host.get_current_context()["asset_name"] + return self._host.get_current_context()["asset_name"] @property def current_task_name(self): @@ -377,37 +384,37 @@ class PublisherController(object): Union[str, None]: Task name or None if task is not set. """ - return self.host.get_current_context()["task_name"] + return self._host.get_current_context()["task_name"] @property def instances(self): """Current instances in create context.""" - return self.create_context.instances + return self._create_context.instances @property def creators(self): """All creators loaded in create context.""" - return self.create_context.creators + return self._create_context.creators @property def manual_creators(self): """Creators that can be shown in create dialog.""" - return self.create_context.manual_creators + return self._create_context.manual_creators @property def host_is_valid(self): """Host is valid for creation.""" - return self.create_context.host_is_valid + return self._create_context.host_is_valid @property def publish_plugins(self): """Publish plugins.""" - return self.create_context.publish_plugins + return self._create_context.publish_plugins @property def plugins_with_defs(self): """Publish plugins with possible attribute definitions.""" - return self.create_context.plugins_with_defs + return self._create_context.plugins_with_defs @property def event_system(self): @@ -445,8 +452,8 @@ class PublisherController(object): def get_context_title(self): """Get context title for artist shown at the top of main window.""" context_title = None - if hasattr(self.host, "get_context_title"): - context_title = self.host.get_context_title() + if hasattr(self._host, "get_context_title"): + context_title = self._host.get_context_title() if context_title is None: context_title = os.environ.get("AVALON_APP_NAME") @@ -486,7 +493,7 @@ class PublisherController(object): self.save_changes() # Reset avalon context - self.create_context.reset_avalon_context() + self._create_context.reset_avalon_context() self._reset_plugins() # Publish part must be reset after plugins @@ -502,7 +509,7 @@ class PublisherController(object): self._resetting_plugins = True - self.create_context.reset_plugins() + self._create_context.reset_plugins() self._resetting_plugins = False @@ -515,10 +522,10 @@ class PublisherController(object): self._resetting_instances = True - self.create_context.reset_context_data() - with self.create_context.bulk_instances_collection(): - self.create_context.reset_instances() - self.create_context.execute_autocreators() + self._create_context.reset_context_data() + with self._create_context.bulk_instances_collection(): + self._create_context.reset_instances() + self._create_context.execute_autocreators() self._resetting_instances = False @@ -567,7 +574,7 @@ class PublisherController(object): """ _tmp_items = [] if include_context: - _tmp_items.append(self.create_context) + _tmp_items.append(self._create_context) for instance in instances: _tmp_items.append(instance) @@ -626,8 +633,8 @@ class PublisherController(object): def save_changes(self): """Save changes happened during creation.""" - if self.create_context.host_is_valid: - self.create_context.save_changes() + if self._create_context.host_is_valid: + self._create_context.save_changes() def remove_instances(self, instances): """""" @@ -635,7 +642,7 @@ class PublisherController(object): # reset is not required and save changes too. self.save_changes() - self.create_context.remove_instances(instances) + self._create_context.remove_instances(instances) self._emit_event("instances.refresh.finished") @@ -696,9 +703,9 @@ class PublisherController(object): # - must not be used for changing CreatedInstances during publishing! # QUESTION # - pop the key after first collector using it would be safest option? - self._publish_context.data["create_context"] = self.create_context + self._publish_context.data["create_context"] = self._create_context - self._publish_report.reset(self._publish_context, self.create_context) + self._publish_report.reset(self._publish_context, self._create_context) self._publish_validation_errors = [] self._publish_current_plugin_validation_errors = None self._publish_error = None From 92cd6b60dfb750e562a1aba61020b4b5c077d083 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 6 Oct 2022 13:47:22 +0200 Subject: [PATCH 06/90] added abstract controller for UI --- openpype/tools/publisher/control.py | 236 +++++++++++++++++++++++++++- 1 file changed, 235 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 9abcc620a8..09f6555d69 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -3,7 +3,9 @@ import copy import logging import traceback import collections +from abc import ABCMeta, abstractmethod, abstractproperty +import six import pyblish.api from openpype.client import get_assets @@ -286,7 +288,239 @@ class PublishReport: return output -class PublisherController(object): + + +@six.add_metaclass(ABCMeta) +class AbstractPublisherController(object): + """Publisher tool controller. + + Define what must be implemented to be able use Publisher functionality. + + Goal is to have "data driven" controller that can be used to control UI + running in different process. That lead to some "" + """ + + _log = None + _event_system = None + + @property + def log(self): + """Controller's logger object. + + Returns: + logging.Logger: Logger object that can be used for logging. + """ + + if self._log is None: + self._log = logging.getLogget(self.__class__.__name__) + return self._log + + @property + def event_system(self): + """Inner event system for publisher controller. + + Event system is autocreated. + + Known topics: + "show.detailed.help" - Detailed help requested (UI related). + "show.card.message" - Show card message request (UI related). + "instances.refresh.finished" - Instances are refreshed. + "plugins.refresh.finished" - Plugins refreshed. + "publish.reset.finished" - Controller reset finished. + "publish.process.started" - Publishing started. Can be started from + paused state. + "publish.process.validated" - Publishing passed validation. + "publish.process.stopped" - Publishing stopped/paused process. + "publish.process.plugin.changed" - Plugin state has changed. + "publish.process.instance.changed" - Instance state has changed. + + Returns: + EventSystem: Event system which can trigger callbacks for topics. + """ + + if self._event_system is None: + self._event_system = EventSystem() + return self._event_system + + @abstractproperty + def project_name(self): + """Current context project name. + + Returns: + str: Name of project. + """ + + pass + + @abstractproperty + def current_asset_name(self): + """Current context asset name. + + Returns: + Union[str, None]: Name of asset. + """ + + pass + + @abstractproperty + def current_task_name(self): + """Current context task name. + + Returns: + Union[str, None]: Name of task. + """ + + pass + + @abstractproperty + def instances(self): + """Collected/created instances. + + Returns: + List[CreatedInstance]: List of created instances. + """ + + pass + + @abstractmethod + def get_manual_creators_base_info(self): + """Creators that can be selected and triggered by artist. + + Returns: + List[CreatorBaseInfo]: Base information about creator plugin. + """ + + pass + + @abstractmethod + def get_context_title(self): + """Get context title for artist shown at the top of main window. + + Returns: + Union[str, None]: Context title for window or None. In case of None + a warning is displayed (not nice for artists). + """ + + pass + + @abstractmethod + def get_asset_docs(self): + pass + + @abstractmethod + def get_asset_hierarchy(self): + pass + + @abstractmethod + def get_task_names_by_asset_names(self, asset_names): + pass + + @abstractmethod + def reset(self): + pass + + @abstractmethod + def emit_card_message(self, message): + pass + + @abstractmethod + def get_creator_attribute_definitions(self, instances): + pass + + @abstractmethod + def get_publish_attribute_definitions(self, instances, include_context): + pass + + @abstractmethod + def get_icon_for_family(self, family): + pass + + @abstractmethod + def create( + self, creator_identifier, subset_name, instance_data, options + ): + pass + + def save_changes(self): + """Save changes happened during creation.""" + + pass + + def remove_instances(self, instances): + """Remove list of instances.""" + + pass + + @abstractproperty + def publish_has_finished(self): + pass + + @abstractproperty + def publish_is_running(self): + pass + + @abstractproperty + def publish_has_validated(self): + pass + + @abstractproperty + def publish_has_crashed(self): + pass + + @abstractproperty + def publish_has_validation_errors(self): + pass + + @abstractproperty + def publish_max_progress(self): + pass + + @abstractproperty + def publish_progress(self): + pass + + @abstractproperty + def publish_comment_is_set(self): + pass + + @abstractmethod + def get_publish_crash_error(self): + pass + + @abstractmethod + def get_publish_report(self): + pass + + @abstractmethod + def get_validation_errors(self): + pass + + @abstractmethod + def set_comment(self, comment): + pass + + @abstractmethod + def publish(self): + pass + + @abstractmethod + def validate(self): + pass + + @abstractmethod + def stop_publish(self): + pass + + @abstractmethod + def run_action(self, plugin, action): + pass + + @abstractmethod + def reset_project_data_cache(self): + pass + + +class PublisherController(AbstractPublisherController): """Middleware between UI, CreateContext and publish Context. Handle both creation and publishing parts. From 6397db6e7956703de0776a90cb090d6f70bcabd7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 6 Oct 2022 15:14:11 +0200 Subject: [PATCH 07/90] removed 'plugins_with_defs' attribute --- openpype/tools/publisher/control.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 09f6555d69..a5a7539369 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -645,11 +645,6 @@ class PublisherController(AbstractPublisherController): """Publish plugins.""" return self._create_context.publish_plugins - @property - def plugins_with_defs(self): - """Publish plugins with possible attribute definitions.""" - return self._create_context.plugins_with_defs - @property def event_system(self): """Inner event system for publisher controller. @@ -838,7 +833,7 @@ class PublisherController(AbstractPublisherController): attr_values.append((item, value)) output = [] - for plugin in self.plugins_with_defs: + for plugin in self._create_context.plugins_with_defs: plugin_name = plugin.__name__ if plugin_name not in all_defs_by_plugin_name: continue From 80103e60e8ffb431ab0696ce5e396096f5d0faeb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 6 Oct 2022 15:14:59 +0200 Subject: [PATCH 08/90] changed 'creators' attribute to '_creators' --- openpype/tools/publisher/control.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index a5a7539369..c2816757d4 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -626,8 +626,9 @@ class PublisherController(AbstractPublisherController): return self._create_context.instances @property - def creators(self): + def _creators(self): """All creators loaded in create context.""" + return self._create_context.creators @property @@ -846,7 +847,7 @@ class PublisherController(AbstractPublisherController): def get_icon_for_family(self, family): """TODO rename to get creator icon.""" - creator = self.creators.get(family) + creator = self._creators.get(family) if creator is not None: return creator.get_icon() return None @@ -855,7 +856,7 @@ class PublisherController(AbstractPublisherController): self, creator_identifier, subset_name, instance_data, options ): """Trigger creation and refresh of instances in UI.""" - creator = self.creators[creator_identifier] + creator = self._creators[creator_identifier] creator.create(subset_name, instance_data, options) self._emit_event("instances.refresh.finished") From 71cca8e74288284135484183ae24647acdfa5dea Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 6 Oct 2022 15:15:46 +0200 Subject: [PATCH 09/90] changed 'publish_plugins' attribute to '_publish_plugins' --- openpype/tools/publisher/control.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index c2816757d4..6a73989ae8 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -642,7 +642,7 @@ class PublisherController(AbstractPublisherController): return self._create_context.host_is_valid @property - def publish_plugins(self): + def _publish_plugins(self): """Publish plugins.""" return self._create_context.publish_plugins @@ -681,6 +681,7 @@ class PublisherController(AbstractPublisherController): def get_context_title(self): """Get context title for artist shown at the top of main window.""" + context_title = None if hasattr(self._host, "get_context_title"): context_title = self._host.get_context_title() @@ -913,7 +914,7 @@ class PublisherController(AbstractPublisherController): return self._publish_error def get_publish_report(self): - return self._publish_report.get_report(self.publish_plugins) + return self._publish_report.get_report(self._publish_plugins) def get_validation_errors(self): return self._publish_validation_errors @@ -940,7 +941,7 @@ class PublisherController(AbstractPublisherController): self._publish_current_plugin_validation_errors = None self._publish_error = None - self._publish_max_progress = len(self.publish_plugins) + self._publish_max_progress = len(self._publish_plugins) self._publish_progress = 0 self._emit_event("publish.reset.finished") @@ -1034,7 +1035,7 @@ class PublisherController(AbstractPublisherController): QUESTION: Does validate button still make sense? """ - for idx, plugin in enumerate(self.publish_plugins): + for idx, plugin in enumerate(self._publish_plugins): self._publish_progress = idx # Reset current plugin validations error From c232e812396cd00681b6badd9a49f63931d96b44 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 6 Oct 2022 15:17:11 +0200 Subject: [PATCH 10/90] removed doubled event system --- openpype/tools/publisher/control.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 6a73989ae8..57098f8734 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -536,9 +536,6 @@ class PublisherController(AbstractPublisherController): self._host = registered_host() self._headless = headless - # Inner event system of controller - self._event_system = EventSystem() - self._create_context = CreateContext( self._host, dbcon, headless=headless, reset=False ) @@ -646,29 +643,6 @@ class PublisherController(AbstractPublisherController): """Publish plugins.""" return self._create_context.publish_plugins - @property - def event_system(self): - """Inner event system for publisher controller. - - Known topics: - "show.detailed.help" - Detailed help requested (UI related). - "show.card.message" - Show card message request (UI related). - "instances.refresh.finished" - Instances are refreshed. - "plugins.refresh.finished" - Plugins refreshed. - "publish.reset.finished" - Controller reset finished. - "publish.process.started" - Publishing started. Can be started from - paused state. - "publish.process.validated" - Publishing passed validation. - "publish.process.stopped" - Publishing stopped/paused process. - "publish.process.plugin.changed" - Plugin state has changed. - "publish.process.instance.changed" - Instance state has changed. - - Returns: - EventSystem: Event system which can trigger callbacks for topics. - """ - - return self._event_system - def _emit_event(self, topic, data=None): if data is None: data = {} From 5b75511a6064c14c7532edddf50266d638616526 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 6 Oct 2022 18:53:24 +0200 Subject: [PATCH 11/90] traypublisher has it's controller --- openpype/tools/traypublisher/window.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/openpype/tools/traypublisher/window.py b/openpype/tools/traypublisher/window.py index b1ff3c7383..be9f12e269 100644 --- a/openpype/tools/traypublisher/window.py +++ b/openpype/tools/traypublisher/window.py @@ -15,6 +15,7 @@ import appdirs from openpype.lib import JSONSettingRegistry from openpype.pipeline import install_host from openpype.hosts.traypublisher.api import TrayPublisherHost +from openpype.tools.publisher.control_qt import QtPublisherController from openpype.tools.publisher.window import PublisherWindow from openpype.tools.utils import PlaceholderLineEdit from openpype.tools.utils.constants import PROJECT_NAME_ROLE @@ -24,6 +25,12 @@ from openpype.tools.utils.models import ( ) +class TrayPublisherController(QtPublisherController): + @property + def host(self): + return self._host + + class TrayPublisherRegistry(JSONSettingRegistry): """Class handling OpenPype general settings registry. @@ -179,7 +186,10 @@ class StandaloneOverlayWidget(QtWidgets.QFrame): class TrayPublishWindow(PublisherWindow): def __init__(self, *args, **kwargs): - super(TrayPublishWindow, self).__init__(reset_on_show=False) + controller = TrayPublisherController() + super(TrayPublishWindow, self).__init__( + controller=controller, reset_on_show=False + ) flags = self.windowFlags() # Disable always on top hint From 054b87bd687d41f2bc9e7fa7387c389c10da3112 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 11:20:10 +0200 Subject: [PATCH 12/90] fix event system access in qt controller --- openpype/tools/publisher/control_qt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/tools/publisher/control_qt.py b/openpype/tools/publisher/control_qt.py index add7c4c7e4..8515a7a843 100644 --- a/openpype/tools/publisher/control_qt.py +++ b/openpype/tools/publisher/control_qt.py @@ -67,10 +67,10 @@ class QtPublisherController(PublisherController): super(QtPublisherController, self).__init__(*args, **kwargs) - self._event_system.add_callback( + self.event_system.add_callback( "publish.process.started", self._qt_on_publish_start ) - self._event_system.add_callback( + self.event_system.add_callback( "publish.process.stopped", self._qt_on_publish_stop ) From f13d2bc9653726dce4c3db4375b9c288d7e79bb6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 11:20:46 +0200 Subject: [PATCH 13/90] implemented helper publish plugins proxy to handle actions for plugins --- openpype/tools/publisher/control.py | 89 +++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 57098f8734..c084cba381 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -288,6 +288,95 @@ class PublishReport: return output +class PublishPluginsProxy: + """Wrapper around publish plugin. + + Prepare mapping for publish plugins and actions. Also can create + serializable data for plugin actions so UI don't have to have access to + them. + + This object is created in process where publishing is actually running. + + Notes: + Actions have id but single action can be used on multiple plugins so + to run an action is needed combination of plugin and action. + + Args: + plugins [List[pyblish.api.Plugin]]: Discovered plugins that will be + processed. + """ + + def __init__(self, plugins): + plugins_by_id = {} + actions_by_id = {} + action_ids_by_plugin_id = {} + for plugin in plugins: + plugin_id = plugin.id + plugins_by_id[plugin_id] = plugin + + action_ids = set() + action_ids_by_plugin_id[plugin_id] = action_ids + + actions = getattr(plugin, "actions", None) or [] + for action in actions: + action_id = action.id + action_ids.add(action_id) + actions_by_id[action_id] = action + + self._plugins_by_id = plugins_by_id + self._actions_by_id = actions_by_id + self._action_ids_by_plugin_id = action_ids_by_plugin_id + + def get_action(self, action_id): + return self._actions_by_id[action_id] + + def get_plugin(self, plugin_id): + return self._plugins_by_id[plugin_id] + + def get_plugin_id(self, plugin): + """Get id of plugin based on plugin object. + + It's used for validation errors report. + + Args: + plugin (pyblish.api.Plugin): Publish plugin for which id should be + returned. + + Returns: + str: Plugin id. + """ + + return plugin.id + + def get_plugin_action_items(self, plugin_id): + """Get plugin action items for plugin by it's id. + + Args: + plugin_id (str): Publish plugin id. + + Returns: + List[PublishPluginActionItem]: Items with information about publish + plugin actions. + """ + + return [ + self._create_action_item(self._actions_by_id[action_id], plugin_id) + for action_id in self._action_ids_by_plugin_id[plugin_id] + ] + + def _create_action_item(self, action, plugin_id): + label = action.label or action.__name__ + icon = getattr(action, "icon", None) + return PublishPluginActionItem( + action.id, + plugin_id, + action.active, + action.on, + label, + icon + ) + + @six.add_metaclass(ABCMeta) From a3d16def9b42328cfe89a03dbed9dc8ea1a51f9b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 11:23:01 +0200 Subject: [PATCH 14/90] created objects for controller <-> ui communiction related to plugin actions and validation errors --- openpype/tools/publisher/control.py | 236 ++++++++++++++++++++++++++++ 1 file changed, 236 insertions(+) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index c084cba381..484d90fc16 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -377,6 +377,242 @@ class PublishPluginsProxy: ) +class PublishPluginActionItem: + """Representation of publish plugin action. + + Data driven object which is used as proxy for controller and UI. + + Args: + action_id (str): Action id. + plugin_id (str): Plugin id. + active (bool): Action is active. + on_filter (str): Actions have 'on' attribte which define when can be + action triggered (e.g. 'all', 'failed', ...). + label (str): Action's label. + icon (Union[str, None]) Action's icon. + """ + + def __init__(self, action_id, plugin_id, active, on_filter, label, icon): + self.action_id = action_id + self.plugin_id = plugin_id + self.active = active + self.on_filter = on_filter + self.label = label + self.icon = icon + + def to_data(self): + """Serialize object to dictionary. + + Returns: + Dict[str, Union[str,bool,None]]: Serialized object. + """ + + return { + "action_id": self.action_id, + "plugin_id": self.plugin_id, + "active": self.active, + "on_filter": self.on_filter, + "label": self.label, + "icon": self.icon + } + + @classmethod + def from_data(cls, data): + """Create object from data. + + Args: + data (Dict[str, Union[str,bool,None]]): Data used to recreate + object. + + Returns: + PublishPluginActionItem: Object created using data. + """ + + return cls(**data) + + +class ValidationErrorItem: + """Data driven validation error item. + + Prepared data container with information about validation error and it's + source plugin. + + Can be converted to raw data and recreated should be used for controller + and UI connection. + + Args: + instance_id (str): Id of pyblish instance to which is validation error + connected. + instance_label (str): Prepared instance label. + plugin_id (str): Id of pyblish Plugin which triggered the validation + error. Id is generated using 'PublishPluginsProxy'. + """ + + def __init__( + self, + instance_id, + instance_label, + plugin_id, + context_validation, + title, + description, + detail, + ): + self.instance_id = instance_id + self.instance_label = instance_label + self.plugin_id = plugin_id + self.context_validation = context_validation + self.title = title + self.description = description + self.detail = detail + + def to_data(self): + """Serialize object to dictionary. + + Returns: + Dict[str, Union[str, bool, None]]: Serialized object data. + """ + + return { + "instance_id": self.instance_id, + "instance_label": self.instance_label, + "plugin_id": self.plugin_id, + "context_validation": self.context_validation, + "title": self.title, + "description": self.description, + "detail": self.detail, + } + + @classmethod + def from_result(cls, plugin_id, error, instance): + """Create new object based on resukt from controller. + + Returns: + ValidationErrorItem: New object with filled data. + """ + + instance_label = None + instance_id = None + if instance is not None: + instance_label = ( + instance.data.get("label") or instance.data.get("name") + ) + instance_id = instance.id + + return cls( + instance_id, + instance_label, + plugin_id, + instance is None, + error.title, + error.description, + error.detail, + ) + + @classmethod + def from_data(cls, data): + return cls(**data) + + +class PublishValidationErrorsReport: + """Publish validation errors report that can be parsed to raw data. + + Args: + error_items (List[ValidationErrorItem]): List of validation errors. + plugin_action_items (Dict[str, PublishPluginActionItem]): Action items + by plugin id. + """ + + def __init__(self, error_items, plugin_action_items): + self._error_items = error_items + self._plugin_action_items = plugin_action_items + + def __iter__(self): + for item in self._error_items: + yield item + + def group_items_by_title(self): + """Group errors by plugin and their titles. + + Items are grouped by plugin and title -> same title from different + plugin is different item. Items are ordered by plugin order. + + Returns: + List[Dict[str, Any]]: List where each item title, instance + information related to title and possible plugin actions. + """ + + ordered_plugin_ids = [] + error_items_by_plugin_id = collections.defaultdict(list) + for error_item in self._error_items: + plugin_id = error_item.plugin_id + if plugin_id not in ordered_plugin_ids: + ordered_plugin_ids.append(plugin_id) + error_items_by_plugin_id[plugin_id].append(error_item) + + grouped_error_items = [] + for plugin_id in ordered_plugin_ids: + plugin_action_items = self._plugin_action_items[plugin_id] + error_items = error_items_by_plugin_id[plugin_id] + + titles = [] + error_items_by_title = collections.defaultdict(list) + for error_item in error_items: + title = error_item.title + if title not in titles: + titles.append(error_item.title) + error_items_by_title[title].append(error_item) + + for title in titles: + grouped_error_items.append({ + "plugin_action_items": list(plugin_action_items), + "error_items": error_items_by_title[title], + "title": title + }) + return grouped_error_items + + def to_data(self): + """Serialize object to dictionary. + + Returns: + Dict[str, Any]: Serialized data. + """ + + return { + "error_items": [ + item.to_data() + for item in self._error_items + ], + "plugin_action_items": { + plugin_id: [ + action_item.to_data() + for action_item in action_items + ] + for plugin_id, action_items in self._plugin_action_items.items() + } + } + + @classmethod + def from_data(cls, data): + """Recreate object from data. + + Args: + data (dict[str, Any]): Data to recreate object. Can be created + using 'to_data' method. + + Returns: + PublishValidationErrorsReport: New object based on data. + """ + + error_items = [ + ValidationErrorItem.from_data(error_item) + for error_item in data["error_items"] + ] + plugin_action_items = [ + PublishPluginActionItem.from_data(action_item) + for action_item in data["plugin_action_items"] + ] + return cls(error_items, plugin_action_items) @six.add_metaclass(ABCMeta) From a63854f2656a04e4f1643112e86e2d7a48dcc657 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 11:24:54 +0200 Subject: [PATCH 15/90] Created object to gather validation errors during publish processing --- openpype/tools/publisher/control.py | 65 +++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 484d90fc16..c28d7ab3c9 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -615,6 +615,71 @@ class PublishValidationErrorsReport: return cls(error_items, plugin_action_items) +class PublishValidationErrors: + """Object to keep track about validation errors by plugin.""" + + def __init__(self): + self._plugins_proxy = None + self._error_items = [] + self._plugin_action_items = {} + + def __bool__(self): + return self.has_errors + + @property + def has_errors(self): + """At least one error was added.""" + + return bool(self._error_items) + + def reset(self, plugins_proxy): + """Reset object to default state. + + Args: + plugins_proxy (PublishPluginsProxy): Proxy which store plugins, + actions by ids and create mapping of action ids by plugin ids. + """ + + self._plugins_proxy = plugins_proxy + self._error_items = [] + self._plugin_action_items = {} + + def create_report(self): + """Create report based on currently existing errors. + + Returns: + PublishValidationErrorsReport: Validation error report with all + error information and publish plugin action items. + """ + + return PublishValidationErrorsReport( + self._error_items, self._plugin_action_items + ) + + def add_error(self, plugin, error, instance): + """Add error from pyblish result. + + Args: + plugin (pyblish.api.Plugin): Plugin which triggered error. + error (ValidationException): Validation error. + instance (Union[pyblish.api.Instance, None]): Instance on which was + error raised or None if was raised on context. + """ + + # Make sure the cached report is cleared + plugin_id = self._plugins_proxy.get_plugin_id(plugin) + self._error_items.append( + ValidationErrorItem.from_result(plugin_id, error, instance) + ) + if plugin_id in self._plugin_action_items: + return + + plugin_actions = self._plugins_proxy.get_plugin_action_items( + plugin_id + ) + self._plugin_action_items[plugin_id] = plugin_actions + + @six.add_metaclass(ABCMeta) class AbstractPublisherController(object): """Publisher tool controller. From d90838b630a47b6055600b2b4175e14b6cba62f5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 11:25:41 +0200 Subject: [PATCH 16/90] removed unused 'get_manual_creators_base_info' --- openpype/tools/publisher/control.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index c28d7ab3c9..bd4b6a738e 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -772,16 +772,6 @@ class AbstractPublisherController(object): pass - @abstractmethod - def get_manual_creators_base_info(self): - """Creators that can be selected and triggered by artist. - - Returns: - List[CreatorBaseInfo]: Base information about creator plugin. - """ - - pass - @abstractmethod def get_context_title(self): """Get context title for artist shown at the top of main window. From e53efc7aba85bbbe12d46a3c35351a2f16bf6d59 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 11:26:38 +0200 Subject: [PATCH 17/90] create plugins proxy in controller --- openpype/tools/publisher/control.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index bd4b6a738e..4fbf20492d 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -920,6 +920,8 @@ class PublisherController(AbstractPublisherController): self._host, dbcon, headless=headless, reset=False ) + self._publish_plugins_proxy = None + # pyblish.api.Context self._publish_context = None # Pyblish report @@ -1290,6 +1292,10 @@ class PublisherController(AbstractPublisherController): # - pop the key after first collector using it would be safest option? self._publish_context.data["create_context"] = self._create_context + self._publish_plugins_proxy = PublishPluginsProxy( + self._publish_plugins + ) + self._publish_report.reset(self._publish_context, self._create_context) self._publish_validation_errors = [] self._publish_current_plugin_validation_errors = None From 8996e27df1a4fbf92973c58973cc9881cc81e655 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 11:27:51 +0200 Subject: [PATCH 18/90] changed how validation errors are collected and worked with in UI --- openpype/tools/publisher/control.py | 27 +-- .../publisher/widgets/validations_widget.py | 192 ++++++++++-------- 2 files changed, 119 insertions(+), 100 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 4fbf20492d..3df8da62cb 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -927,9 +927,7 @@ class PublisherController(AbstractPublisherController): # Pyblish report self._publish_report = PublishReport(self) # Store exceptions of validation error - self._publish_validation_errors = [] - # Currently processing plugin errors - self._publish_current_plugin_validation_errors = None + self._publish_validation_errors = PublishValidationErrors() # Any other exception that happened during publishing self._publish_error = None # Publishing is in progress @@ -1273,7 +1271,7 @@ class PublisherController(AbstractPublisherController): return self._publish_report.get_report(self._publish_plugins) def get_validation_errors(self): - return self._publish_validation_errors + return self._publish_validation_errors.create_report() def _reset_publish(self): self._publish_is_running = False @@ -1297,8 +1295,7 @@ class PublisherController(AbstractPublisherController): ) self._publish_report.reset(self._publish_context, self._create_context) - self._publish_validation_errors = [] - self._publish_current_plugin_validation_errors = None + self._publish_validation_errors.reset(self._publish_plugins_proxy) self._publish_error = None self._publish_max_progress = len(self._publish_plugins) @@ -1488,19 +1485,11 @@ class PublisherController(AbstractPublisherController): yield MainThreadItem(self.stop_publish) def _add_validation_error(self, result): - if self._publish_current_plugin_validation_errors is None: - self._publish_current_plugin_validation_errors = { - "plugin": result["plugin"], - "errors": [] - } - self._publish_validation_errors.append( - self._publish_current_plugin_validation_errors - ) - - self._publish_current_plugin_validation_errors["errors"].append({ - "exception": result["error"], - "instance": result["instance"] - }) + self._publish_validation_errors.add_error( + result["plugin"], + result["error"], + result["instance"] + ) def _process_and_continue(self, plugin, instance): result = pyblish.plugin.process( diff --git a/openpype/tools/publisher/widgets/validations_widget.py b/openpype/tools/publisher/widgets/validations_widget.py index fd9410df98..48b7370eee 100644 --- a/openpype/tools/publisher/widgets/validations_widget.py +++ b/openpype/tools/publisher/widgets/validations_widget.py @@ -50,6 +50,7 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget): Has toggle button to show/hide instances on which validation error happened if there is a list (Valdation error may happen on context). """ + selected = QtCore.Signal(int) instance_changed = QtCore.Signal(int) @@ -75,33 +76,33 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget): title_frame_layout.addWidget(toggle_instance_btn, 0) instances_model = QtGui.QStandardItemModel() - error_info = error_info["error_info"] help_text_by_instance_id = {} - context_validation = False - if ( - not error_info - or (len(error_info) == 1 and error_info[0][0] is None) - ): - context_validation = True - toggle_instance_btn.setArrowType(QtCore.Qt.NoArrow) - description = self._prepare_description(error_info[0][1]) - help_text_by_instance_id[None] = description - else: - items = [] - for instance, exception in error_info: - label = instance.data.get("label") or instance.data.get("name") - item = QtGui.QStandardItem(label) - item.setFlags( - QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable - ) - item.setData(label, QtCore.Qt.ToolTipRole) - item.setData(instance.id, INSTANCE_ID_ROLE) - items.append(item) - description = self._prepare_description(exception) - help_text_by_instance_id[instance.id] = description - instances_model.invisibleRootItem().appendRows(items) + items = [] + context_validation = False + for error_item in error_info["error_items"]: + context_validation = error_item.context_validation + if context_validation: + toggle_instance_btn.setArrowType(QtCore.Qt.NoArrow) + description = self._prepare_description(error_item) + help_text_by_instance_id[None] = description + continue + + label = error_item.instance_label + item = QtGui.QStandardItem(label) + item.setFlags( + QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + ) + item.setData(label, QtCore.Qt.ToolTipRole) + item.setData(error_item.instance_id, INSTANCE_ID_ROLE) + items.append(item) + description = self._prepare_description(error_item) + help_text_by_instance_id[error_item.instance_id] = description + + if items: + root_item = instances_model.invisibleRootItem() + root_item.appendRows(items) instances_view = ValidationErrorInstanceList(self) instances_view.setModel(instances_model) @@ -162,9 +163,19 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget): def minimumSizeHint(self): return self.sizeHint() - def _prepare_description(self, exception): - dsc = exception.description - detail = exception.detail + def _prepare_description(self, error_item): + """Prepare description text for detail intput. + + Args: + error_item (ValidationErrorItem): Item which hold information about + validation error. + + Returns: + str: Prepared detailed description. + """ + + dsc = error_item.description + detail = error_item.detail if detail: dsc += "

{}".format(detail) @@ -192,32 +203,51 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget): @property def is_selected(self): - """Is widget marked a selected""" + """Is widget marked a selected. + + Returns: + bool: Item is selected or not. + """ + return self._selected @property def index(self): - """Widget's index set by parent.""" + """Widget's index set by parent. + + Returns: + int: Index of widget. + """ + return self._index def set_index(self, index): - """Set index of widget (called by parent).""" + """Set index of widget (called by parent). + + Args: + int: New index of widget. + """ + self._index = index def _change_style_property(self, selected): """Change style of widget based on selection.""" + value = "1" if selected else "" self._title_frame.setProperty("selected", value) self._title_frame.style().polish(self._title_frame) def set_selected(self, selected=None): """Change selected state of widget.""" + if selected is None: selected = not self._selected + # Clear instance view selection on deselect if not selected: self._instances_view.clearSelection() + # Skip if has same value if selected == self._selected: return @@ -255,18 +285,23 @@ class ActionButton(BaseClickableFrame): """Plugin's action callback button. Action may have label or icon or both. - """ - action_clicked = QtCore.Signal(str) - def __init__(self, action, parent): + Args: + plugin_action_item (PublishPluginActionItem): Action item that can be + triggered by it's id. + """ + + action_clicked = QtCore.Signal(str, str) + + def __init__(self, plugin_action_item, parent): super(ActionButton, self).__init__(parent) self.setObjectName("ValidationActionButton") - self.action = action + self.plugin_action_item = plugin_action_item - action_label = action.label or action.__name__ - action_icon = getattr(action, "icon", None) + action_label = plugin_action_item.label + action_icon = plugin_action_item.icon label_widget = QtWidgets.QLabel(action_label, self) icon_label = None if action_icon: @@ -284,7 +319,10 @@ class ActionButton(BaseClickableFrame): ) def _mouse_release_callback(self): - self.action_clicked.emit(self.action.id) + self.action_clicked.emit( + self.plugin_action_item.plugin_id, + self.plugin_action_item.action_id + ) class ValidateActionsWidget(QtWidgets.QFrame): @@ -292,6 +330,7 @@ class ValidateActionsWidget(QtWidgets.QFrame): Change actions based on selected validation error. """ + def __init__(self, controller, parent): super(ValidateActionsWidget, self).__init__(parent) @@ -304,10 +343,9 @@ class ValidateActionsWidget(QtWidgets.QFrame): layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(content_widget) - self.controller = controller + self._controller = controller self._content_widget = content_widget self._content_layout = content_layout - self._plugin = None self._actions_mapping = {} def clear(self): @@ -320,28 +358,34 @@ class ValidateActionsWidget(QtWidgets.QFrame): widget.deleteLater() self._actions_mapping = {} - def set_plugin(self, plugin): + def set_error_item(self, error_item): """Set selected plugin and show it's actions. Clears current actions from widget and recreate them from the plugin. + + Args: + Dict[str, Any]: Object holding error items, title and possible + actions to run. """ + self.clear() - self._plugin = plugin - if not plugin: + + if not error_item: self.setVisible(False) return - actions = getattr(plugin, "actions", []) - for action in actions: - if not action.active: + plugin_action_items = error_item["plugin_action_items"] + for plugin_action_item in plugin_action_items: + if not plugin_action_item.active: continue - if action.on not in ("failed", "all"): + if plugin_action_item.on_filter not in ("failed", "all"): continue - self._actions_mapping[action.id] = action + action_id = plugin_action_item.action_id + self._actions_mapping[action_id] = plugin_action_item - action_btn = ActionButton(action, self._content_widget) + action_btn = ActionButton(plugin_action_item, self._content_widget) action_btn.action_clicked.connect(self._on_action_click) self._content_layout.addWidget(action_btn) @@ -351,9 +395,8 @@ class ValidateActionsWidget(QtWidgets.QFrame): else: self.setVisible(False) - def _on_action_click(self, action_id): - action = self._actions_mapping[action_id] - self.controller.run_action(self._plugin, action) + def _on_action_click(self, plugin_id, action_id): + self._controller.run_action(plugin_id, action_id) class VerticallScrollArea(QtWidgets.QScrollArea): @@ -365,6 +408,7 @@ class VerticallScrollArea(QtWidgets.QScrollArea): Resize if deferred by 100ms because at the moment of resize are not yet propagated sizes and visibility of scroll bars. """ + def __init__(self, *args, **kwargs): super(VerticallScrollArea, self).__init__(*args, **kwargs) @@ -576,45 +620,31 @@ class ValidationsWidget(QtWidgets.QFrame): self._errors_widget.setVisible(False) self._actions_widget.setVisible(False) - def set_errors(self, errors): - """Set errors into context and created titles.""" + def _set_errors(self, validation_error_report): + """Set errors into context and created titles. + + Args: + validation_error_report (PublishValidationErrorsReport): Report + with information about validation errors and publish plugin + actions. + """ + self.clear() - if not errors: + if not validation_error_report: return self._top_label.setVisible(True) self._error_details_frame.setVisible(True) self._errors_widget.setVisible(True) - errors_by_title = [] - for plugin_info in errors: - titles = [] - error_info_by_title = {} - - for error_info in plugin_info["errors"]: - exception = error_info["exception"] - title = exception.title - if title not in titles: - titles.append(title) - error_info_by_title[title] = [] - error_info_by_title[title].append( - (error_info["instance"], exception) - ) - - for title in titles: - errors_by_title.append({ - "plugin": plugin_info["plugin"], - "error_info": error_info_by_title[title], - "title": title - }) - - for idx, item in enumerate(errors_by_title): - widget = ValidationErrorTitleWidget(idx, item, self) + grouped_error_items = validation_error_report.group_items_by_title() + for idx, error_info in enumerate(grouped_error_items): + widget = ValidationErrorTitleWidget(idx, error_info, self) widget.selected.connect(self._on_select) widget.instance_changed.connect(self._on_instance_change) self._errors_layout.addWidget(widget) self._title_widgets[idx] = widget - self._error_info[idx] = item + self._error_info[idx] = error_info self._errors_layout.addStretch(1) @@ -640,7 +670,7 @@ class ValidationsWidget(QtWidgets.QFrame): if self._controller.publish_has_validation_errors: validation_errors = self._controller.get_validation_errors() self._set_current_widget(self._validations_widget) - self.set_errors(validation_errors) + self._set_errors(validation_errors) return if self._contoller.publish_has_finished: @@ -659,7 +689,7 @@ class ValidationsWidget(QtWidgets.QFrame): error_item = self._error_info[index] - self._actions_widget.set_plugin(error_item["plugin"]) + self._actions_widget.set_error_item(error_item) self._update_description() From 2a34a5f9780e9423a409230dc1751b057c53a2fa Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 11:28:51 +0200 Subject: [PATCH 19/90] renamed 'get_icon_for_family' to 'get_creator_icon' --- openpype/tools/publisher/control.py | 15 ++++++++++++--- .../tools/publisher/widgets/card_view_widgets.py | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 3df8da62cb..f870f5d9e3 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -812,7 +812,16 @@ class AbstractPublisherController(object): pass @abstractmethod - def get_icon_for_family(self, family): + def get_creator_icon(self, identifier): + """Receive creator's icon by identifier. + + Args: + identifier (str): Creator's identifier. + + Returns: + Union[str, None]: Creator's icon string. + """ + pass @abstractmethod @@ -1200,9 +1209,9 @@ class PublisherController(AbstractPublisherController): )) return output - def get_icon_for_family(self, family): + def get_creator_icon(self, identifier): """TODO rename to get creator icon.""" - creator = self._creators.get(family) + creator = self._creators.get(identifier) if creator is not None: return creator.get_icon() return None diff --git a/openpype/tools/publisher/widgets/card_view_widgets.py b/openpype/tools/publisher/widgets/card_view_widgets.py index fa391f4ba0..4bd2cf25ae 100644 --- a/openpype/tools/publisher/widgets/card_view_widgets.py +++ b/openpype/tools/publisher/widgets/card_view_widgets.py @@ -469,7 +469,7 @@ class InstanceCardView(AbstractInstanceView): group_widget = self._widgets_by_group[group_name] else: group_icons = { - idenfier: self.controller.get_icon_for_family(idenfier) + idenfier: self.controller.get_creator_icon(idenfier) for idenfier in identifiers_by_group[group_name] } From 562852875ed60a310a4f29052cbbe66356bda466 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 11:29:08 +0200 Subject: [PATCH 20/90] fix action trigger --- 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 f870f5d9e3..643efa8645 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1354,8 +1354,11 @@ class PublisherController(AbstractPublisherController): if self._publish_is_running: self._stop_publish() - def run_action(self, plugin, action): + def run_action(self, plugin_id, action_id): # TODO handle result in UI + plugin = self._publish_plugins_proxy.get_plugin(plugin_id) + action = self._publish_plugins_proxy.get_action(action_id) + result = pyblish.plugin.process( plugin, self._publish_context, None, action.id ) From 35562e4abb2c41962e701012bcb13ff7ad1e0067 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 11:29:36 +0200 Subject: [PATCH 21/90] remove unused variable reset --- openpype/tools/publisher/control.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 643efa8645..7aaaccd8d8 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1407,9 +1407,6 @@ class PublisherController(AbstractPublisherController): for idx, plugin in enumerate(self._publish_plugins): self._publish_progress = idx - # Reset current plugin validations error - self._publish_current_plugin_validation_errors = None - # Check if plugin is over validation order if not self._publish_validated: self._publish_validated = ( From e6042d9889cb52675f642a9deee1664ed0f7057b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 11:29:54 +0200 Subject: [PATCH 22/90] fix event system access --- openpype/tools/publisher/control.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 7aaaccd8d8..a42657dd9a 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1025,6 +1025,7 @@ class PublisherController(AbstractPublisherController): @property def host_is_valid(self): """Host is valid for creation.""" + return self._create_context.host_is_valid @property @@ -1035,7 +1036,7 @@ class PublisherController(AbstractPublisherController): def _emit_event(self, topic, data=None): if data is None: data = {} - self._event_system.emit(topic, data, "controller") + self.event_system.emit(topic, data, "controller") # --- Publish specific callbacks --- def get_asset_docs(self): From a06f629a08b8cfaa50fa35745d7805fd8737eff9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 11:30:32 +0200 Subject: [PATCH 23/90] added some docstrings --- openpype/tools/publisher/control.py | 123 +++++++++++++++++++++++++--- 1 file changed, 112 insertions(+), 11 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index a42657dd9a..d4b624e959 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -797,10 +797,12 @@ class AbstractPublisherController(object): @abstractmethod def reset(self): - pass + """Reset whole controller. + + This should reset create context, publish context and all variables + that are related to it. + """ - @abstractmethod - def emit_card_message(self, message): pass @abstractmethod @@ -828,52 +830,113 @@ class AbstractPublisherController(object): def create( self, creator_identifier, subset_name, instance_data, options ): - pass + """Trigger creation by creator identifier. + + Should also trigger refresh of instanes. + + Args: + creator_identifier (str): Identifier of Creator plugin. + subset_name (str): Calculated subset name. + instance_data (Dict[str, Any]): Base instance data with variant, + asset name and task name. + options (Dict[str, Any]): Data from pre-create attributes. + """ def save_changes(self): - """Save changes happened during creation.""" + """Save changes in create context.""" pass def remove_instances(self, instances): - """Remove list of instances.""" + """Remove list of instances from create context.""" pass @abstractproperty def publish_has_finished(self): + """Has publishing finished. + + Returns: + bool: If publishing finished and all plugins were iterated. + """ + pass @abstractproperty def publish_is_running(self): + """Publishing is running right now. + + Returns: + bool: If publishing is in progress. + """ + pass @abstractproperty def publish_has_validated(self): + """Publish validation passed. + + Returns: + bool: If publishing passed last possible validation order. + """ + pass @abstractproperty def publish_has_crashed(self): + """Publishing crashed for any reason. + + Returns: + bool: Publishing crashed. + """ + pass @abstractproperty def publish_has_validation_errors(self): + """During validation happened at least one validation error. + + Returns: + bool: Validation error was raised during validation. + """ + pass @abstractproperty def publish_max_progress(self): + """Get maximum possible progress number. + + Returns: + int: Number that can be used as 100% of publish progress bar. + """ + pass @abstractproperty def publish_progress(self): + """Current progress number. + + Returns: + int: Current progress value which is between 0 and + 'publish_max_progress'. + """ + pass @abstractproperty def publish_comment_is_set(self): + """Publish comment was at least once set. + + Publish comment can be set only once when publish is started for a + first time. This helpt to idetify if 'set_comment' should be called or + not. + """ + pass @abstractmethod def get_publish_crash_error(self): + pass @abstractmethod @@ -884,30 +947,68 @@ class AbstractPublisherController(object): def get_validation_errors(self): pass - @abstractmethod - def set_comment(self, comment): - pass - @abstractmethod def publish(self): + """Trigger publishing without any order limitations.""" + pass @abstractmethod def validate(self): + """Trigger publishing which will stop after validation order.""" + pass @abstractmethod def stop_publish(self): + """Stop publishing can be also used to pause publishing. + + Pause of publishing is possible only if all plugins successfully + finished. + """ + pass @abstractmethod - def run_action(self, plugin, action): + def run_action(self, plugin_id, action_id): + """Trigger pyblish action on a plugin. + + Args: + plugin_id (str): Id of publish plugin. + action_id (str): Id of publish action. + """ + pass @abstractmethod def reset_project_data_cache(self): pass + @abstractmethod + def set_comment(self, comment): + """Set comment on pyblish context. + + Set "comment" key on current pyblish.api.Context data. + + Args: + comment (str): Artist's comment. + """ + + pass + + @abstractmethod + def emit_card_message(self, message): + """Emit a card message which can have a lifetime. + + This is for UI purposes. Method can be extended to more arguments + in future e.g. different message timeout or type (color). + + Args: + message (str): Message that will be showed. + """ + + pass + class PublisherController(AbstractPublisherController): """Middleware between UI, CreateContext and publish Context. From 200107245a79c75bb5ce3329a04b31e3690241f0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 11:33:51 +0200 Subject: [PATCH 24/90] controller is private for all widgets --- .../publisher/widgets/card_view_widgets.py | 7 ++-- .../publisher/widgets/list_view_widgets.py | 6 +-- .../tools/publisher/widgets/publish_frame.py | 42 +++++++++---------- openpype/tools/publisher/widgets/widgets.py | 16 +++---- 4 files changed, 36 insertions(+), 35 deletions(-) diff --git a/openpype/tools/publisher/widgets/card_view_widgets.py b/openpype/tools/publisher/widgets/card_view_widgets.py index 4bd2cf25ae..06fa49320e 100644 --- a/openpype/tools/publisher/widgets/card_view_widgets.py +++ b/openpype/tools/publisher/widgets/card_view_widgets.py @@ -351,10 +351,11 @@ class InstanceCardView(AbstractInstanceView): Wrapper of all widgets in card view. """ + def __init__(self, controller, parent): super(InstanceCardView, self).__init__(parent) - self.controller = controller + self._controller = controller scroll_area = QtWidgets.QScrollArea(self) scroll_area.setWidgetResizable(True) @@ -440,7 +441,7 @@ class InstanceCardView(AbstractInstanceView): # Prepare instances by group and identifiers by group instances_by_group = collections.defaultdict(list) identifiers_by_group = collections.defaultdict(set) - for instance in self.controller.instances: + for instance in self._controller.instances: group_name = instance.group_label instances_by_group[group_name].append(instance) identifiers_by_group[group_name].add( @@ -469,7 +470,7 @@ class InstanceCardView(AbstractInstanceView): group_widget = self._widgets_by_group[group_name] else: group_icons = { - idenfier: self.controller.get_creator_icon(idenfier) + idenfier: self._controller.get_creator_icon(idenfier) for idenfier in identifiers_by_group[group_name] } diff --git a/openpype/tools/publisher/widgets/list_view_widgets.py b/openpype/tools/publisher/widgets/list_view_widgets.py index a701181e5b..8438e17167 100644 --- a/openpype/tools/publisher/widgets/list_view_widgets.py +++ b/openpype/tools/publisher/widgets/list_view_widgets.py @@ -409,7 +409,7 @@ class InstanceListView(AbstractInstanceView): def __init__(self, controller, parent): super(InstanceListView, self).__init__(parent) - self.controller = controller + self._controller = controller instance_view = InstanceTreeView(self) instance_delegate = ListItemDelegate(instance_view) @@ -520,7 +520,7 @@ class InstanceListView(AbstractInstanceView): # Prepare instances by their groups instances_by_group_name = collections.defaultdict(list) group_names = set() - for instance in self.controller.instances: + for instance in self._controller.instances: group_label = instance.group_label group_names.add(group_label) instances_by_group_name[group_label].append(instance) @@ -771,7 +771,7 @@ class InstanceListView(AbstractInstanceView): context_selected = False instances_by_id = { instance.id: instance - for instance in self.controller.instances + for instance in self._controller.instances } for index in self._instance_view.selectionModel().selectedIndexes(): diff --git a/openpype/tools/publisher/widgets/publish_frame.py b/openpype/tools/publisher/widgets/publish_frame.py index 4e5f02f2da..b49f005640 100644 --- a/openpype/tools/publisher/widgets/publish_frame.py +++ b/openpype/tools/publisher/widgets/publish_frame.py @@ -185,7 +185,7 @@ class PublishFrame(QtWidgets.QWidget): self._shrunk_anim = shrunk_anim - self.controller = controller + self._controller = controller self._content_frame = content_frame self._content_layout = content_layout @@ -309,8 +309,8 @@ class PublishFrame(QtWidgets.QWidget): self._validate_btn.setEnabled(True) self._publish_btn.setEnabled(True) - self._progress_bar.setValue(self.controller.publish_progress) - self._progress_bar.setMaximum(self.controller.publish_max_progress) + self._progress_bar.setValue(self._controller.publish_progress) + self._progress_bar.setMaximum(self._controller.publish_max_progress) def _on_publish_start(self): self._set_success_property(-1) @@ -334,34 +334,34 @@ class PublishFrame(QtWidgets.QWidget): def _on_plugin_change(self, event): """Change plugin label when instance is going to be processed.""" - self._progress_bar.setValue(self.controller.publish_progress) + self._progress_bar.setValue(self._controller.publish_progress) self._plugin_label.setText(event["plugin_label"]) QtWidgets.QApplication.processEvents() def _on_publish_stop(self): - self._progress_bar.setValue(self.controller.publish_progress) + self._progress_bar.setValue(self._controller.publish_progress) self._reset_btn.setEnabled(True) self._stop_btn.setEnabled(False) - validate_enabled = not self.controller.publish_has_crashed - publish_enabled = not self.controller.publish_has_crashed + validate_enabled = not self._controller.publish_has_crashed + publish_enabled = not self._controller.publish_has_crashed if validate_enabled: - validate_enabled = not self.controller.publish_has_validated + validate_enabled = not self._controller.publish_has_validated if publish_enabled: if ( - self.controller.publish_has_validated - and self.controller.publish_has_validation_errors + self._controller.publish_has_validated + and self._controller.publish_has_validation_errors ): publish_enabled = False else: - publish_enabled = not self.controller.publish_has_finished + publish_enabled = not self._controller.publish_has_finished self._validate_btn.setEnabled(validate_enabled) self._publish_btn.setEnabled(publish_enabled) - error = self.controller.get_publish_crash_error() - validation_errors = self.controller.get_validation_errors() + error = self._controller.get_publish_crash_error() + validation_errors = self._controller.get_validation_errors() if error: self._set_error(error) @@ -369,7 +369,7 @@ class PublishFrame(QtWidgets.QWidget): self._set_progress_visibility(False) self._set_validation_errors() - elif self.controller.publish_has_finished: + elif self._controller.publish_has_finished: self._set_finished() else: @@ -377,7 +377,7 @@ class PublishFrame(QtWidgets.QWidget): def _set_stopped(self): main_label = "Publish paused" - if self.controller.publish_has_validated: + if self._controller.publish_has_validated: main_label += " - Validation passed" self._set_main_label(main_label) @@ -440,7 +440,7 @@ class PublishFrame(QtWidgets.QWidget): widget.style().polish(widget) def _copy_report(self): - logs = self.controller.get_publish_report() + logs = self._controller.get_publish_report() logs_string = json.dumps(logs, indent=4) mime_data = QtCore.QMimeData() @@ -463,7 +463,7 @@ class PublishFrame(QtWidgets.QWidget): if not ext or not new_filepath: return - logs = self.controller.get_publish_report() + logs = self._controller.get_publish_report() full_path = new_filepath + ext dir_path = os.path.dirname(full_path) if not os.path.exists(dir_path): @@ -483,13 +483,13 @@ class PublishFrame(QtWidgets.QWidget): self.details_page_requested.emit() def _on_reset_clicked(self): - self.controller.reset() + self._controller.reset() def _on_stop_clicked(self): - self.controller.stop_publish() + self._controller.stop_publish() def _on_validate_clicked(self): - self.controller.validate() + self._controller.validate() def _on_publish_clicked(self): - self.controller.publish() + self._controller.publish() diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index d5e55b88f9..903ce70f01 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -994,7 +994,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): def __init__(self, controller, parent): super(GlobalAttrsWidget, self).__init__(parent) - self.controller = controller + self._controller = controller self._current_instances = [] variant_input = VariantInputWidget(self) @@ -1068,7 +1068,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): else: asset_names.add(asset_name) - for asset_doc in self.controller.get_asset_docs(): + for asset_doc in self._controller.get_asset_docs(): _asset_name = asset_doc["name"] if _asset_name in asset_names: asset_names.remove(_asset_name) @@ -1077,7 +1077,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): if not asset_names: break - project_name = self.controller.project_name + project_name = self._controller.project_name subset_names = set() invalid_tasks = False for instance in self._current_instances: @@ -1245,7 +1245,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget): self._main_layout = main_layout - self.controller = controller + self._controller = controller self._scroll_area = scroll_area self._attr_def_id_to_instances = {} @@ -1274,7 +1274,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget): self._attr_def_id_to_instances = {} self._attr_def_id_to_attr_def = {} - result = self.controller.get_creator_attribute_definitions( + result = self._controller.get_creator_attribute_definitions( instances ) @@ -1366,7 +1366,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): self._main_layout = main_layout - self.controller = controller + self._controller = controller self._scroll_area = scroll_area self._attr_def_id_to_instances = {} @@ -1398,7 +1398,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): self._attr_def_id_to_attr_def = {} self._attr_def_id_to_plugin_name = {} - result = self.controller.get_publish_attribute_definitions( + result = self._controller.get_publish_attribute_definitions( instances, context_selected ) @@ -1513,7 +1513,7 @@ class SubsetAttributesWidget(QtWidgets.QWidget): self._on_instance_context_changed ) - self.controller = controller + self._controller = controller self.global_attrs_widget = global_attrs_widget From 5cfd5db5d7d323133fb1d79a6a1da0e2effc4c49 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 11:53:10 +0200 Subject: [PATCH 25/90] added missing abstract property 'host_is_valid' --- openpype/tools/publisher/control.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index d4b624e959..1725961aac 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -687,7 +687,8 @@ class AbstractPublisherController(object): Define what must be implemented to be able use Publisher functionality. Goal is to have "data driven" controller that can be used to control UI - running in different process. That lead to some "" + running in different process. That lead to some disadvantages like UI can't + access objects directly but by using wrappers that can be serialized. """ _log = None @@ -762,6 +763,19 @@ class AbstractPublisherController(object): pass + @abstractproperty + def host_is_valid(self): + """Host is valid for creation part. + + Host must have implemented certain functionality to be able create + in Publisher tool. + + Returns: + bool: Host can handle creation of instances. + """ + + pass + @abstractproperty def instances(self): """Collected/created instances. From 56449218344d23d6f5bb4e23c849dbd5ba1ac93a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 12:47:10 +0200 Subject: [PATCH 26/90] store asset documents by name --- openpype/tools/publisher/control.py | 39 ++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 1725961aac..2da26622eb 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -45,25 +45,34 @@ class AssetDocsCache: def __init__(self, controller): self._controller = controller self._asset_docs = None + # TODO use asset ids instead self._task_names_by_asset_name = {} + self._asset_docs_by_name = {} def reset(self): self._asset_docs = None self._task_names_by_asset_name = {} + self._asset_docs_by_name = {} def _query(self): - if self._asset_docs is None: - project_name = self._controller.project_name - asset_docs = get_assets( - project_name, fields=self.projection.keys() - ) - task_names_by_asset_name = {} - for asset_doc in asset_docs: - asset_name = asset_doc["name"] - asset_tasks = asset_doc.get("data", {}).get("tasks") or {} - task_names_by_asset_name[asset_name] = list(asset_tasks.keys()) - self._asset_docs = asset_docs - self._task_names_by_asset_name = task_names_by_asset_name + if self._asset_docs is not None: + return + + project_name = self._controller.project_name + asset_docs = get_assets( + project_name, fields=self.projection.keys() + ) + asset_docs_by_name = {} + task_names_by_asset_name = {} + for asset_doc in asset_docs: + asset_name = asset_doc["name"] + asset_tasks = asset_doc.get("data", {}).get("tasks") or {} + task_names_by_asset_name[asset_name] = list(asset_tasks.keys()) + asset_docs_by_name[asset_name] = asset_doc + + self._asset_docs = asset_docs + self._asset_docs_by_name = asset_docs_by_name + self._task_names_by_asset_name = task_names_by_asset_name def get_asset_docs(self): self._query() @@ -73,6 +82,12 @@ class AssetDocsCache: self._query() return copy.deepcopy(self._task_names_by_asset_name) + def get_asset_by_name(self, asset_name): + asset_doc = self._asset_docs_by_name.get(asset_name) + if asset_doc is None: + return None + return copy.deepcopy(asset_doc) + class PublishReport: """Report for single publishing process. From 2ab0ad9d4466c8d518726266115e0910fd53a0bf Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 12:47:44 +0200 Subject: [PATCH 27/90] added ability to get and query full asset document --- openpype/tools/publisher/control.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 2da26622eb..8abe62e4b1 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -8,7 +8,10 @@ from abc import ABCMeta, abstractmethod, abstractproperty import six import pyblish.api -from openpype.client import get_assets +from openpype.client import ( + get_assets, + get_asset_by_id, +) from openpype.lib.events import EventSystem from openpype.pipeline import ( PublishValidationError, @@ -48,6 +51,7 @@ class AssetDocsCache: # TODO use asset ids instead self._task_names_by_asset_name = {} self._asset_docs_by_name = {} + self._full_asset_docs_by_name = {} def reset(self): self._asset_docs = None @@ -88,6 +92,15 @@ class AssetDocsCache: return None return copy.deepcopy(asset_doc) + def get_full_asset_by_name(self, asset_name): + self._query() + if asset_name not in self._full_asset_docs_by_name: + asset_doc = self._asset_docs_by_name.get(asset_name) + project_name = self._controller.project_name + full_asset_doc = get_asset_by_id(project_name, asset_doc["_id"]) + self._full_asset_docs_by_name[asset_name] = full_asset_doc + return copy.deepcopy(self._full_asset_docs_by_name[asset_name]) + class PublishReport: """Report for single publishing process. From 626cb387934956d2c1eea08a535dd83223a876cd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 12:48:34 +0200 Subject: [PATCH 28/90] added ability to get existing subsets for passet asset name via controller --- openpype/tools/publisher/control.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 8abe62e4b1..89619f70f7 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -11,6 +11,7 @@ import pyblish.api from openpype.client import ( get_assets, get_asset_by_id, + get_subsets, ) from openpype.lib.events import EventSystem from openpype.pipeline import ( @@ -837,6 +838,10 @@ class AbstractPublisherController(object): def get_task_names_by_asset_names(self, asset_names): pass + @abstractmethod + def get_existing_subset_names(self, asset_name): + pass + @abstractmethod def reset(self): """Reset whole controller. @@ -1223,6 +1228,21 @@ class PublisherController(AbstractPublisherController): ) return result + def get_existing_subset_names(self, asset_name): + project_name = self.project_name + asset_doc = self._asset_docs_cache.get_asset_by_name(asset_name) + if not asset_doc: + return None + + asset_id = asset_doc["_id"] + subset_docs = get_subsets( + project_name, asset_ids=[asset_id], fields=["name"] + ) + return { + subset_doc["name"] + for subset_doc in subset_docs + } + def reset(self): """Reset everything related to creation and publishing.""" # Stop publishing From ac61407a4fe0d517b78cad30f2abffe51c378546 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 12:49:25 +0200 Subject: [PATCH 29/90] controller can handle get subset name based on creator identifier --- openpype/tools/publisher/control.py | 52 +++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 89619f70f7..444cdbc914 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -873,6 +873,29 @@ class AbstractPublisherController(object): pass + @abstractmethod + def get_subset_name( + self, + creator_identifier, + variant, + task_name, + asset_name, + instance_id=None + ): + """Get subset name based on passed data. + + Args: + creator_identifier (str): Identifier of creator which should be + responsible for subset name creation. + variant (str): Variant value from user's input. + task_name (str): Name of task for which is instance created. + asset_name (str): Name of asset for which is instance created. + instance_id (Union[str, None]): Existing instance id when subset + name is updated. + """ + + pass + @abstractmethod def create( self, creator_identifier, subset_name, instance_data, options @@ -1380,6 +1403,35 @@ class PublisherController(AbstractPublisherController): return creator.get_icon() return None + def get_subset_name( + self, + creator_identifier, + variant, + task_name, + asset_name, + instance_id=None + ): + """Get subset name based on passed data. + + Args: + creator_identifier (str): Identifier of creator which should be + responsible for subset name creation. + variant (str): Variant value from user's input. + task_name (str): Name of task for which is instance created. + asset_name (str): Name of asset for which is instance created. + instance_id (Union[str, None]): Existing instance id when subset + name is updated. + """ + + creator = self._creators[creator_identifier] + project_name = self.project_name + print(asset_name) + asset_doc = self._asset_docs_cache.get_full_asset_by_name(asset_name) + + return creator.get_subset_name( + variant, task_name, asset_doc, project_name + ) + def create( self, creator_identifier, subset_name, instance_data, options ): From 72dccf24a2fd887a86221bfc12ae815447bde7f6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 12:50:03 +0200 Subject: [PATCH 30/90] create widget does not call 'get_subset_name' on creator but via controller --- .../tools/publisher/widgets/create_widget.py | 38 +++++++------------ 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py index 4c9fa63d24..39fdeae30f 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -174,7 +174,7 @@ class CreateWidget(QtWidgets.QWidget): self._controller = controller - self._asset_doc = None + self._asset_name = None self._subset_names = None self._selected_creator = None @@ -380,7 +380,7 @@ class CreateWidget(QtWidgets.QWidget): if asset_name is None: asset_name = self.current_asset_name - return asset_name + return asset_name or None def _get_task_name(self): task_name = None @@ -444,7 +444,7 @@ class CreateWidget(QtWidgets.QWidget): prereq_available = False creator_btn_tooltips.append("Creator is not selected") - if self._context_change_is_enabled() and self._asset_doc is None: + if self._context_change_is_enabled() and self._asset_name is None: # QUESTION how to handle invalid asset? prereq_available = False creator_btn_tooltips.append("Context is not selected") @@ -468,30 +468,19 @@ class CreateWidget(QtWidgets.QWidget): asset_name = self._get_asset_name() # Skip if asset did not change - if self._asset_doc and self._asset_doc["name"] == asset_name: + if self._asset_name and self._asset_name == asset_name: return - # Make sure `_asset_doc` and `_subset_names` variables are reset - self._asset_doc = None + # Make sure `_asset_name` and `_subset_names` variables are reset + self._asset_name = asset_name self._subset_names = None if asset_name is None: return - project_name = self._controller.project_name - asset_doc = get_asset_by_name(project_name, asset_name) - self._asset_doc = asset_doc + subset_names = self._controller.get_existing_subset_names(asset_name) - if asset_doc: - asset_id = asset_doc["_id"] - subset_docs = get_subsets( - project_name, asset_ids=[asset_id], fields=["name"] - ) - self._subset_names = { - subset_doc["name"] - for subset_doc in subset_docs - } - - if not asset_doc: + self._subset_names = subset_names + if subset_names is None: self.subset_name_input.setText("< Asset is not set >") def _refresh_creators(self): @@ -670,14 +659,13 @@ class CreateWidget(QtWidgets.QWidget): self.subset_name_input.setText("< Valid variant >") return - project_name = self._controller.project_name + asset_name = self._get_asset_name() task_name = self._get_task_name() - - asset_doc = copy.deepcopy(self._asset_doc) + creator_idenfier = self._selected_creator.identifier # Calculate subset name with Creator plugin try: - subset_name = self._selected_creator.get_subset_name( - variant_value, task_name, asset_doc, project_name + subset_name = self._controller.get_subset_name( + creator_idenfier, variant_value, task_name, asset_name ) except TaskNotSetError: self._create_btn.setEnabled(False) From 9ce236a9de4747102ccab076fd52f763cf5051d2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 15:52:57 +0200 Subject: [PATCH 31/90] Added creator item for warpping creator plugins --- openpype/tools/publisher/control.py | 103 +++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 444cdbc914..047b34d550 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -18,7 +18,12 @@ from openpype.pipeline import ( PublishValidationError, registered_host, ) -from openpype.pipeline.create import CreateContext +from openpype.pipeline.create import ( + CreateContext, + AutoCreator, + HiddenCreator, + Creator, +) # Define constant for plugin orders offset PLUGIN_ORDER_OFFSET = 0.5 @@ -709,6 +714,102 @@ class PublishValidationErrors: self._plugin_action_items[plugin_id] = plugin_actions +class CreatorType: + def __init__(self, name): + self.name = name + + def __str__(self): + return self.name + + def __eq__(self, other): + return self.name == str(other) + + +class CreatorTypes: + base = CreatorType("base") + auto = CreatorType("auto") + hidden = CreatorType("hidden") + artist = CreatorType("artist") + + +class CreatorItem: + """Wrapper around Creator plugin. + + Object can be serialized and recreated. + """ + + def __init__( + self, + identifier, + creator_type, + family, + label, + group_label, + icon, + instance_attributes_defs, + description, + detailed_description, + default_variant, + default_variants, + create_allow_context_change, + pre_create_attributes_defs + ): + self.identifier = identifier + self.creator_type = creator_type + self.family = family + self.label = label + self.icon = icon + self.description = description + self.detailed_description = detailed_description + self.default_variant = default_variant + self.default_variants = default_variants + self.create_allow_context_change = create_allow_context_change + self.instance_attributes_defs = instance_attributes_defs + self.pre_create_attributes_defs = pre_create_attributes_defs + + @classmethod + def from_creator(cls, creator): + if isinstance(creator, AutoCreator): + creator_type = CreatorTypes.auto + elif isinstance(creator, HiddenCreator): + creator_type = CreatorTypes.hidden + elif isinstance(creator, Creator): + creator_type = CreatorTypes.artist + else: + creator_type = CreatorTypes.base + + description = None + detail_description = None + default_variant = None + default_variants = None + pre_create_attr_defs = None + create_allow_context_change = None + if creator_type is CreatorTypes.artist: + description = creator.get_description() + detail_description = creator.get_detail_description() + default_variant = creator.get_default_variant() + default_variants = creator.get_default_variants() + pre_create_attr_defs = creator.get_pre_create_attr_defs() + create_allow_context_change = creator.create_allow_context_change + + identifier = creator.identifier + return cls( + identifier, + creator_type, + creator.family, + creator.label or identifier, + creator.get_group_label(), + creator.get_icon(), + creator.get_instance_attr_defs(), + description, + detail_description, + default_variant, + default_variants, + create_allow_context_change, + pre_create_attr_defs + ) + + @six.add_metaclass(ABCMeta) class AbstractPublisherController(object): """Publisher tool controller. From 447d15694a6eaa08c6470b0cc8329c6d94951803 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 15:53:26 +0200 Subject: [PATCH 32/90] use creator items instead of creators directly --- openpype/tools/publisher/control.py | 22 ++++--- .../tools/publisher/widgets/create_widget.py | 59 +++++++++++-------- .../publisher/widgets/precreate_widget.py | 6 +- 3 files changed, 50 insertions(+), 37 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 047b34d550..a8b9290811 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1107,7 +1107,6 @@ class AbstractPublisherController(object): @abstractmethod def get_publish_crash_error(self): - pass @abstractmethod @@ -1201,6 +1200,8 @@ class PublisherController(AbstractPublisherController): self._host, dbcon, headless=headless, reset=False ) + self._creator_items = {} + self._publish_plugins_proxy = None # pyblish.api.Context @@ -1290,9 +1291,10 @@ class PublisherController(AbstractPublisherController): return self._create_context.creators @property - def manual_creators(self): + def creator_items(self): """Creators that can be shown in create dialog.""" - return self._create_context.manual_creators + + return self._creator_items @property def host_is_valid(self): @@ -1393,6 +1395,12 @@ class PublisherController(AbstractPublisherController): self._create_context.reset_plugins() + creator_items = { + identifier: CreatorItem.from_creator(creator) + for identifier, creator in self._create_context.creators.items() + } + self._creator_items = creator_items + self._resetting_plugins = False self._emit_event("plugins.refresh.finished") @@ -1498,10 +1506,9 @@ class PublisherController(AbstractPublisherController): return output def get_creator_icon(self, identifier): - """TODO rename to get creator icon.""" - creator = self._creators.get(identifier) - if creator is not None: - return creator.get_icon() + creator_item = self._creator_items.get(identifier) + if creator_item is not None: + return creator_item.icon return None def get_subset_name( @@ -1526,7 +1533,6 @@ class PublisherController(AbstractPublisherController): creator = self._creators[creator_identifier] project_name = self.project_name - print(asset_name) asset_doc = self._asset_docs_cache.get_full_asset_by_name(asset_name) return creator.get_subset_name( diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py index 39fdeae30f..10cf39675e 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -1,11 +1,9 @@ import sys import re import traceback -import copy from Qt import QtWidgets, QtCore, QtGui -from openpype.client import get_asset_by_name, get_subsets from openpype.pipeline.create import ( CreatorError, SUBSET_NAME_ALLOWED_SYMBOLS, @@ -150,18 +148,18 @@ class CreatorShortDescWidget(QtWidgets.QWidget): self._family_label = family_label self._description_label = description_label - def set_plugin(self, plugin=None): - if not plugin: + def set_creator_item(self, creator_item=None): + if not creator_item: self._icon_widget.set_icon_def(None) self._family_label.setText("") self._description_label.setText("") return - plugin_icon = plugin.get_icon() - description = plugin.get_description() or "" + plugin_icon = creator_item.icon + description = creator_item.description or "" self._icon_widget.set_icon_def(plugin_icon) - self._family_label.setText("{}".format(plugin.family)) + self._family_label.setText("{}".format(creator_item.family)) self._family_label.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) self._description_label.setText(description) @@ -495,7 +493,10 @@ class CreateWidget(QtWidgets.QWidget): # Add new families new_creators = set() - for identifier, creator in self._controller.manual_creators.items(): + for identifier, creator_item in self._controller.creator_items.items(): + if creator_item.creator_type != "artist": + continue + # TODO add details about creator new_creators.add(identifier) if identifier in existing_items: @@ -507,10 +508,9 @@ class CreateWidget(QtWidgets.QWidget): ) self._creators_model.appendRow(item) - label = creator.label or identifier - item.setData(label, QtCore.Qt.DisplayRole) + item.setData(creator_item.label, QtCore.Qt.DisplayRole) item.setData(identifier, CREATOR_IDENTIFIER_ROLE) - item.setData(creator.family, FAMILY_ROLE) + item.setData(creator_item.family, FAMILY_ROLE) # Remove families that are no more available for identifier in (old_creators - new_creators): @@ -561,11 +561,11 @@ class CreateWidget(QtWidgets.QWidget): identifier = new_index.data(CREATOR_IDENTIFIER_ROLE) self._set_creator_by_identifier(identifier) - def _set_creator_detailed_text(self, creator): + def _set_creator_detailed_text(self, creator_item): # TODO implement description = "" - if creator is not None: - description = creator.get_detail_description() or description + if creator_item is not None: + description = creator_item.detailed_description or description self._controller.event_system.emit( "show.detailed.help", { @@ -575,32 +575,39 @@ class CreateWidget(QtWidgets.QWidget): ) def _set_creator_by_identifier(self, identifier): - creator = self._controller.manual_creators.get(identifier) - self._set_creator(creator) + creator_item = self._controller.creator_items.get(identifier) + self._set_creator(creator_item) - def _set_creator(self, creator): - self._creator_short_desc_widget.set_plugin(creator) - self._set_creator_detailed_text(creator) - self._pre_create_widget.set_plugin(creator) + def _set_creator(self, creator_item): + """Set current creator item. - self._selected_creator = creator + Args: + creator_item (CreatorItem): Item representing creator that can be + triggered by artist. + """ - if not creator: + self._creator_short_desc_widget.set_creator_item(creator_item) + self._set_creator_detailed_text(creator_item) + self._pre_create_widget.set_creator_item(creator_item) + + self._selected_creator = creator_item + + if not creator_item: self._set_context_enabled(False) return if ( - creator.create_allow_context_change + creator_item.create_allow_context_change != self._context_change_is_enabled() ): - self._set_context_enabled(creator.create_allow_context_change) + self._set_context_enabled(creator_item.create_allow_context_change) self._refresh_asset() - default_variants = creator.get_default_variants() + default_variants = creator_item.default_variants if not default_variants: default_variants = ["Main"] - default_variant = creator.get_default_variant() + default_variant = creator_item.default_variant if not default_variant: default_variant = default_variants[0] diff --git a/openpype/tools/publisher/widgets/precreate_widget.py b/openpype/tools/publisher/widgets/precreate_widget.py index eaadfe890b..ef34c9bcb5 100644 --- a/openpype/tools/publisher/widgets/precreate_widget.py +++ b/openpype/tools/publisher/widgets/precreate_widget.py @@ -58,12 +58,12 @@ class PreCreateWidget(QtWidgets.QWidget): def current_value(self): return self._attributes_widget.current_value() - def set_plugin(self, creator): + def set_creator_item(self, creator_item): attr_defs = [] creator_selected = False - if creator is not None: + if creator_item is not None: creator_selected = True - attr_defs = creator.get_pre_create_attr_defs() + attr_defs = creator_item.pre_create_attributes_defs self._attributes_widget.set_attr_defs(attr_defs) From 06e1cf0b0ffd5f74da6bea47e1bc82c83623d844 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 16:34:27 +0200 Subject: [PATCH 33/90] attribute definitions now have types --- openpype/lib/attribute_definitions.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/openpype/lib/attribute_definitions.py b/openpype/lib/attribute_definitions.py index 37446f01f8..0ce4c7866f 100644 --- a/openpype/lib/attribute_definitions.py +++ b/openpype/lib/attribute_definitions.py @@ -3,7 +3,7 @@ import re import collections import uuid import json -from abc import ABCMeta, abstractmethod +from abc import ABCMeta, abstractmethod, abstractproperty import six import clique @@ -115,6 +115,16 @@ class AbtractAttrDef: return False return self.key == other.key + @abstractproperty + def type(self): + """Attribute definition type also used as identifier of class. + + Returns: + str: Type of attribute definition. + """ + + pass + @abstractmethod def convert_value(self, value): """Convert value to a valid one. @@ -141,10 +151,12 @@ class UIDef(AbtractAttrDef): class UISeparatorDef(UIDef): - pass + type = "separator" class UILabelDef(UIDef): + type = "label" + def __init__(self, label): super(UILabelDef, self).__init__(label=label) @@ -160,6 +172,8 @@ class UnknownDef(AbtractAttrDef): have known definition of type. """ + type = "unknown" + def __init__(self, key, default=None, **kwargs): kwargs["default"] = default super(UnknownDef, self).__init__(key, **kwargs) @@ -181,6 +195,7 @@ class NumberDef(AbtractAttrDef): default(int, float): Default value for conversion. """ + type = "number" def __init__( self, key, minimum=None, maximum=None, decimals=None, default=None, **kwargs @@ -301,6 +316,8 @@ class EnumDef(AbtractAttrDef): default: Default value. Must be one key(value) from passed items. """ + type = "enum" + def __init__(self, key, items, default=None, **kwargs): if not items: raise ValueError(( @@ -343,6 +360,8 @@ class BoolDef(AbtractAttrDef): default(bool): Default value. Set to `False` if not defined. """ + type = "bool" + def __init__(self, key, default=None, **kwargs): if default is None: default = False @@ -585,6 +604,7 @@ class FileDef(AbtractAttrDef): default(str, List[str]): Default value. """ + type = "path" def __init__( self, key, single_item=True, folders=None, extensions=None, allow_sequences=True, extensions_label=None, default=None, **kwargs From bc39b992709ee75da2bf6dbc6f679b0a84b8f5f0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 16:34:50 +0200 Subject: [PATCH 34/90] attribute definitions can be serialized and deserialized --- openpype/lib/attribute_definitions.py | 61 +++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/openpype/lib/attribute_definitions.py b/openpype/lib/attribute_definitions.py index 0ce4c7866f..a721aa09b8 100644 --- a/openpype/lib/attribute_definitions.py +++ b/openpype/lib/attribute_definitions.py @@ -90,6 +90,8 @@ class AbtractAttrDef: next to value input or ahead. """ + type_attributes = [] + is_value_def = True def __init__( @@ -135,6 +137,35 @@ class AbtractAttrDef: pass + def serialize(self): + """Serialize object to data so it's possible to recreate it. + + Returns: + Dict[str, Any]: Serialized object that can be passed to + 'deserialize' method. + """ + + data = { + "type": self.type, + "key": self.key, + "label": self.label, + "tooltip": self.tooltip, + "default": self.default, + "is_label_horizontal": self.is_label_horizontal + } + for attr in self.type_attributes: + data[attr] = getattr(self, attr) + return data + + @classmethod + def deserialize(cls, data): + """Recreate object from data. + + Data can be received using 'serialize' method. + """ + + return cls(**data) + # ----------------------------------------- # UI attribute definitoins won't hold value @@ -196,6 +227,12 @@ class NumberDef(AbtractAttrDef): """ type = "number" + type_attributes = [ + "minimum", + "maximum", + "decimals" + ] + def __init__( self, key, minimum=None, maximum=None, decimals=None, default=None, **kwargs @@ -267,6 +304,12 @@ class TextDef(AbtractAttrDef): default(str, None): Default value. Empty string used when not defined. """ + type = "text" + type_attributes = [ + "multiline", + "placeholder", + ] + def __init__( self, key, multiline=None, regex=None, placeholder=None, default=None, **kwargs @@ -305,6 +348,11 @@ class TextDef(AbtractAttrDef): return value return self.default + def serialize(self): + data = super(TextDef, self).serialize() + data["regex"] = self.regex.pattern + return data + class EnumDef(AbtractAttrDef): """Enumeration of single item from items. @@ -352,6 +400,11 @@ class EnumDef(AbtractAttrDef): return value return self.default + def serialize(self): + data = super(TextDef, self).serialize() + data["items"] = list(self.items) + return data + class BoolDef(AbtractAttrDef): """Boolean representation. @@ -605,6 +658,14 @@ class FileDef(AbtractAttrDef): """ type = "path" + type_attributes = [ + "single_item", + "folders", + "extensions", + "allow_sequences", + "extensions_label", + ] + def __init__( self, key, single_item=True, folders=None, extensions=None, allow_sequences=True, extensions_label=None, default=None, **kwargs From ac406106308bbff34b2e6a94d9d133159d23a853 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 16:35:07 +0200 Subject: [PATCH 35/90] added helper functions to serialize and deserialize attribute definitions --- openpype/lib/attribute_definitions.py | 90 +++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/openpype/lib/attribute_definitions.py b/openpype/lib/attribute_definitions.py index a721aa09b8..bb0b07948f 100644 --- a/openpype/lib/attribute_definitions.py +++ b/openpype/lib/attribute_definitions.py @@ -8,6 +8,28 @@ from abc import ABCMeta, abstractmethod, abstractproperty import six import clique +# Global variable which store attribude definitions by type +# - default types are registered on import +_attr_defs_by_type = {} + + +def register_attr_def_class(cls): + """Register attribute definition. + + Currently are registered definitions used to deserialize data to objects. + + Attrs: + cls (AbtractAttrDef): Non-abstract class to be registered with unique + 'type' attribute. + + Raises: + KeyError: When type was already registered. + """ + + if cls.type in _attr_defs_by_type: + raise KeyError("Type \"{}\" was already registered".format(cls.type)) + _attr_defs_by_type[cls.type] = cls + def get_attributes_keys(attribute_definitions): """Collect keys from list of attribute definitions. @@ -756,3 +778,71 @@ class FileDef(AbtractAttrDef): if self.single_item: return FileDefItem.create_empty_item().to_dict() return [] + + +def serialize_attr_def(attr_def): + """Serialize attribute definition to data. + + Args: + attr_def (AbtractAttrDef): Attribute definition to serialize. + + Returns: + Dict[str, Any]: Serialized data. + """ + + return attr_def.serialize() + + +def serialize_attr_defs(attr_defs): + """Serialize attribute definitions to data. + + Args: + attr_defs (List[AbtractAttrDef]): Attribute definitions to serialize. + + Returns: + List[Dict[str, Any]]: Serialized data. + """ + + return [ + serialize_attr_def(attr_def) + for attr_def in attr_defs + ] + + +def deserialize_attr_def(attr_def_data): + """Deserialize attribute definition from data. + + Args: + attr_def (Dict[str, Any]): Attribute definition data to deserialize. + """ + + attr_type = attr_def_data.pop("type") + cls = _attr_defs_by_type[attr_type] + return cls.deserialize(attr_def_data) + + +def deserialize_attr_defs(attr_defs_data): + """Deserialize attribute definitions. + + Args: + List[Dict[str, Any]]: List of attribute definitions. + """ + + return [ + deserialize_attr_def(attr_def_data) + for attr_def_data in attr_defs_data + ] + + +# Register attribute definitions +for _attr_class in ( + UISeparatorDef, + UILabelDef, + UnknownDef, + NumberDef, + TextDef, + EnumDef, + BoolDef, + FileDef +): + register_attr_def_class(_attr_class) From 409ec104055779bab17c78da7d344c012dbf517f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 16:47:13 +0200 Subject: [PATCH 36/90] added serialization and deserialization of CreatorItem --- openpype/tools/publisher/control.py | 66 ++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index a8b9290811..f96782b08d 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -14,6 +14,10 @@ from openpype.client import ( get_subsets, ) from openpype.lib.events import EventSystem +from openpype.lib.attribute_definitions import ( + serialize_attr_defs, + deserialize_attr_defs, +) from openpype.pipeline import ( PublishValidationError, registered_host, @@ -731,6 +735,18 @@ class CreatorTypes: hidden = CreatorType("hidden") artist = CreatorType("artist") + @classmethod + def from_str(cls, value): + for creator_type in ( + cls.base, + cls.auto, + cls.hidden, + cls.artist + ): + if value == creator_type: + return creator_type + raise ValueError("Unknown type \"{}\"".format(str(value))) + class CreatorItem: """Wrapper around Creator plugin. @@ -758,6 +774,7 @@ class CreatorItem: self.creator_type = creator_type self.family = family self.label = label + self.group_label = group_label self.icon = icon self.description = description self.detailed_description = detailed_description @@ -809,6 +826,52 @@ class CreatorItem: pre_create_attr_defs ) + def to_data(self): + instance_attributes_defs = None + if self.instance_attributes_defs is not None: + instance_attributes_defs = serialize_attr_defs( + self.instance_attributes_defs + ) + + pre_create_attributes_defs = None + if self.pre_create_attributes_defs is not None: + instance_attributes_defs = serialize_attr_defs( + self.pre_create_attributes_defs + ) + + return { + "identifier": self.identifier, + "creator_type": str(self.creator_type), + "family": self.family, + "label": self.label, + "group_label": self.group_label, + "icon": self.icon, + "description": self.description, + "detailed_description": self.detailed_description, + "default_variant": self.default_variant, + "default_variants": self.default_variants, + "create_allow_context_change": self.create_allow_context_change, + "instance_attributes_defs": instance_attributes_defs, + "pre_create_attributes_defs": pre_create_attributes_defs, + } + + @classmethod + def from_data(cls, data): + instance_attributes_defs = data["instance_attributes_defs"] + if instance_attributes_defs is not None: + data["instance_attributes_defs"] = deserialize_attr_defs( + instance_attributes_defs + ) + + pre_create_attributes_defs = data["pre_create_attributes_defs"] + if pre_create_attributes_defs is not None: + data["pre_create_attributes_defs"] = deserialize_attr_defs( + pre_create_attributes_defs + ) + + data["creator_type"] = CreatorTypes.from_str(data["creator_type"]) + return cls(**data) + @six.add_metaclass(ABCMeta) class AbstractPublisherController(object): @@ -1395,11 +1458,10 @@ class PublisherController(AbstractPublisherController): self._create_context.reset_plugins() - creator_items = { + self._creator_items = { identifier: CreatorItem.from_creator(creator) for identifier, creator in self._create_context.creators.items() } - self._creator_items = creator_items self._resetting_plugins = False From 12fee4ec4ff0985d28c74b40070e40aa13f25238 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 18:40:23 +0200 Subject: [PATCH 37/90] create context provides instances by id --- openpype/pipeline/create/context.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index a7e43cb2f2..87768606e6 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -780,6 +780,10 @@ class CreateContext: def instances(self): return self._instances_by_id.values() + @property + def instances_by_id(self): + return self._instances_by_id + @property def publish_attributes(self): """Access to global publish attributes.""" From 8f83ff878f45a01a3689da4e31ca63db5c97a67d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 18:40:41 +0200 Subject: [PATCH 38/90] prepared some methods for instance remote processing --- openpype/pipeline/create/context.py | 116 ++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 87768606e6..804e3955e5 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -197,6 +197,16 @@ class AttributeValues: def changes(self): return self.calculate_changes(self._data, self._origin_data) + def apply_changes(self, changes): + for key, item in changes.items(): + old_value, new_value = item + if new_value is None: + if key in self: + self.pop(key) + + elif self.get(key) != new_value: + self[key] = new_value + class CreatorAttributeValues(AttributeValues): """Creator specific attribute values of an instance. @@ -327,6 +337,21 @@ class PublishAttributes: changes[key] = (value, None) return changes + def apply_changes(self, changes): + for key, item in changes.items(): + if isinstance(item, dict): + self._data[key].apply_changes(item) + continue + + old_value, new_value = item + if new_value is not None: + raise ValueError( + "Unexpected type \"{}\" expected None".format( + str(type(new_value)) + ) + ) + self.pop(key) + def set_publish_plugins(self, attr_plugins): """Set publish plugins attribute definitions.""" @@ -693,6 +718,97 @@ class CreatedInstance: if member not in self._members: self._members.append(member) + def serialize_for_remote(self): + return { + "data": self.data_to_store(), + "orig_data": copy.deepcopy(self._orig_data) + } + + @classmethod + def deserialize_on_remote(cls, serialized_data, creator_items): + """Convert instance data to CreatedInstance. + + This is fake instance in remote process e.g. in UI process. The creator + is not a full creator and should not be used for calling methods when + instance is created from this method (matters on implementation). + + Args: + serialized_data (Dict[str, Any]): Serialized data for remote + recreating. Should contain 'data' and 'orig_data'. + creator_items (Dict[str, Any]): Mapping of creator identifier and + objects that behave like a creator for most of attribute + access. + """ + + instance_data = copy.deepcopy(serialized_data["data"]) + creator_identifier = instance_data["creator_identifier"] + creator_item = creator_items[creator_identifier] + + family = instance_data.get("family", None) + if family is None: + family = creator_item.family + subset_name = instance_data.get("subset", None) + + obj = cls( + family, subset_name, instance_data, creator_item, new=False + ) + obj._orig_data = serialized_data["orig_data"] + + return obj + + def remote_changes(self): + """Prepare serializable changes on remote side. + + Returns: + Dict[str, Any]: Prepared changes that can be send to client side. + """ + + return { + "changes": self.changes(), + "asset_is_valid": self._asset_is_valid, + "task_is_valid": self._task_is_valid, + } + + def update_from_remote(self, remote_changes): + """Apply changes from remote side on client side. + + Args: + remote_changes (Dict[str, Any]): Changes created on remote side. + """ + + self._asset_is_valid = remote_changes["asset_is_valid"] + self._task_is_valid = remote_changes["task_is_valid"] + + changes = remote_changes["changes"] + creator_attributes = changes.pop("creator_attributes", None) or {} + publish_attributes = changes.pop("publish_attributes", None) or {} + if changes: + self.apply_changes(changes) + + if creator_attributes: + self.creator_attributes.apply_changes(creator_attributes) + + if publish_attributes: + self.publish_attributes.apply_changes(publish_attributes) + + def apply_changes(self, changes): + """Apply changes created via 'changes'. + + Args: + Dict[str, Tuple[Any, Any]]: Instance changes to apply. Same values + are kept untouched. + """ + + for key, item in changes.items(): + old_value, new_value = item + if new_value is None: + if key in self: + self.pop(key) + else: + current_value = self.get(key) + if current_value != new_value: + self[key] = new_value + class CreateContext: """Context of instance creation. From b5a4420f0a8fa78b26988ab1e7e18d7150a04799 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 18:41:31 +0200 Subject: [PATCH 39/90] instances returns instances by id --- openpype/tools/publisher/control.py | 2 +- openpype/tools/publisher/widgets/card_view_widgets.py | 2 +- openpype/tools/publisher/widgets/list_view_widgets.py | 7 ++----- openpype/tools/publisher/window.py | 2 +- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index f96782b08d..6765c75992 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1345,7 +1345,7 @@ class PublisherController(AbstractPublisherController): @property def instances(self): """Current instances in create context.""" - return self._create_context.instances + return self._create_context.instances_by_id @property def _creators(self): diff --git a/openpype/tools/publisher/widgets/card_view_widgets.py b/openpype/tools/publisher/widgets/card_view_widgets.py index 06fa49320e..2be37ea44c 100644 --- a/openpype/tools/publisher/widgets/card_view_widgets.py +++ b/openpype/tools/publisher/widgets/card_view_widgets.py @@ -441,7 +441,7 @@ class InstanceCardView(AbstractInstanceView): # Prepare instances by group and identifiers by group instances_by_group = collections.defaultdict(list) identifiers_by_group = collections.defaultdict(set) - for instance in self._controller.instances: + for instance in self._controller.instances.values(): group_name = instance.group_label instances_by_group[group_name].append(instance) identifiers_by_group[group_name].add( diff --git a/openpype/tools/publisher/widgets/list_view_widgets.py b/openpype/tools/publisher/widgets/list_view_widgets.py index 8438e17167..17b50b764a 100644 --- a/openpype/tools/publisher/widgets/list_view_widgets.py +++ b/openpype/tools/publisher/widgets/list_view_widgets.py @@ -520,7 +520,7 @@ class InstanceListView(AbstractInstanceView): # Prepare instances by their groups instances_by_group_name = collections.defaultdict(list) group_names = set() - for instance in self._controller.instances: + for instance in self._controller.instances.values(): group_label = instance.group_label group_names.add(group_label) instances_by_group_name[group_label].append(instance) @@ -769,10 +769,7 @@ class InstanceListView(AbstractInstanceView): """ instances = [] context_selected = False - instances_by_id = { - instance.id: instance - for instance in self._controller.instances - } + instances_by_id = self._controller.instances for index in self._instance_view.selectionModel().selectedIndexes(): instance_id = index.data(INSTANCE_ID_ROLE) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 699cf6f1f9..bc2e42f051 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -523,7 +523,7 @@ class PublisherWindow(QtWidgets.QDialog): return all_valid = None - for instance in self._controller.instances: + for instance in self._controller.instances.values(): if not instance["active"]: continue From 56cea034aba692180f59a68814b900d5e127d8da Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 18:41:46 +0200 Subject: [PATCH 40/90] don't call same property more then once --- 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 bc2e42f051..3b3e27660d 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -496,8 +496,9 @@ class PublisherWindow(QtWidgets.QDialog): self._set_publish_overlay_visibility(False) self._reset_btn.setEnabled(True) self._stop_btn.setEnabled(False) - validate_enabled = not self._controller.publish_has_crashed - publish_enabled = not self._controller.publish_has_crashed + publish_has_crashed = self._controller.publish_has_crashed + validate_enabled = not publish_has_crashed + publish_enabled = not publish_has_crashed if validate_enabled: validate_enabled = not self._controller.publish_has_validated if publish_enabled: From d71f201f65d453a8dacb98330e0f1fab39276d8a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 18:42:26 +0200 Subject: [PATCH 41/90] removed 'reset_project_data_cache' used in traypublisher --- openpype/tools/publisher/control.py | 7 ------- openpype/tools/traypublisher/window.py | 3 +++ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 6765c75992..4482aea5ec 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1213,10 +1213,6 @@ class AbstractPublisherController(object): pass - @abstractmethod - def reset_project_data_cache(self): - pass - @abstractmethod def set_comment(self, comment): """Set comment on pyblish context. @@ -1905,9 +1901,6 @@ class PublisherController(AbstractPublisherController): self._publish_next_process() - def reset_project_data_cache(self): - self._asset_docs_cache.reset() - def collect_families_from_instances(instances, only_active=False): """Collect all families for passed publish instances. diff --git a/openpype/tools/traypublisher/window.py b/openpype/tools/traypublisher/window.py index be9f12e269..dfe06d149d 100644 --- a/openpype/tools/traypublisher/window.py +++ b/openpype/tools/traypublisher/window.py @@ -30,6 +30,9 @@ class TrayPublisherController(QtPublisherController): def host(self): return self._host + def reset_project_data_cache(self): + self._asset_docs_cache.reset() + class TrayPublisherRegistry(JSONSettingRegistry): """Class handling OpenPype general settings registry. From 05344514d320c2cacba1a4a826f86b9910372839 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 18:42:45 +0200 Subject: [PATCH 42/90] reset assets cache on controller reset --- openpype/tools/publisher/control.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 4482aea5ec..a2dd88e4fb 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1438,6 +1438,8 @@ class PublisherController(AbstractPublisherController): # Reset avalon context self._create_context.reset_avalon_context() + self._asset_docs_cache.reset() + self._reset_plugins() # Publish part must be reset after plugins self._reset_publish() From 92f28271c5ba7d3769b453b11bf60a9b14d49e0c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 18:43:15 +0200 Subject: [PATCH 43/90] mimic creator methods --- openpype/tools/publisher/control.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index a2dd88e4fb..9f62eed54a 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -784,6 +784,12 @@ class CreatorItem: self.instance_attributes_defs = instance_attributes_defs self.pre_create_attributes_defs = pre_create_attributes_defs + def get_instance_attr_defs(self): + return self.instance_attributes_defs + + def get_group_label(self): + return self.group_label + @classmethod def from_creator(cls, creator): if isinstance(creator, AutoCreator): From ae717d4151a34f09f0cf6b7a641bca37d22757da Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 18:43:45 +0200 Subject: [PATCH 44/90] use creator item to get attribute definitions instead of instance --- openpype/tools/publisher/control.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 9f62eed54a..389382b96e 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1500,7 +1500,9 @@ class PublisherController(AbstractPublisherController): output = [] _attr_defs = {} for instance in instances: - for attr_def in instance.creator_attribute_defs: + creator_identifier = instance.creator_identifier + creator_item = self._creator_items[creator_identifier] + for attr_def in creator_item.instance_attributes_defs: found_idx = None for idx, _attr_def in _attr_defs.items(): if attr_def == _attr_def: From 098bcce75193e5e46adbe29ba1d9771ab0ab2f59 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 18:44:31 +0200 Subject: [PATCH 45/90] added some helper functions for easy overriding to avoid duplicity --- openpype/tools/publisher/control.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 389382b96e..b08486654c 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1089,6 +1089,7 @@ class AbstractPublisherController(object): def remove_instances(self, instances): """Remove list of instances from create context.""" + # TODO expect instance ids pass @@ -1485,7 +1486,7 @@ class PublisherController(AbstractPublisherController): self._resetting_instances = False - self._emit_event("instances.refresh.finished") + self._on_create_instance_change() def emit_card_message(self, message): self._emit_event("show.card.message", {"message": message}) @@ -1494,9 +1495,10 @@ class PublisherController(AbstractPublisherController): """Collect creator attribute definitions for multuple instances. Args: - instances(list): List of created instances for + instances(List[CreatedInstance]): List of created instances for which should be attribute definitions returned. """ + output = [] _attr_defs = {} for instance in instances: @@ -1530,6 +1532,7 @@ class PublisherController(AbstractPublisherController): which should be attribute definitions returned. include_context(bool): Add context specific attribute definitions. """ + _tmp_items = [] if include_context: _tmp_items.append(self._create_context) @@ -1614,7 +1617,7 @@ class PublisherController(AbstractPublisherController): creator = self._creators[creator_identifier] creator.create(subset_name, instance_data, options) - self._emit_event("instances.refresh.finished") + self._on_create_instance_change() def save_changes(self): """Save changes happened during creation.""" @@ -1623,12 +1626,19 @@ class PublisherController(AbstractPublisherController): def remove_instances(self, instances): """""" + # 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() + self._remove_instances_from_context(instances) + + self._on_create_instance_change() + + def _remove_instances_from_context(self, instances): self._create_context.remove_instances(instances) + def _on_create_instance_change(self): self._emit_event("instances.refresh.finished") # --- Publish specific implementations --- From 7e53b0354a37de52ef46d7011275b99e832e4e18 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 7 Oct 2022 18:44:51 +0200 Subject: [PATCH 46/90] prepared base class of remote qt controller --- openpype/tools/publisher/control_qt.py | 311 +++++++++++++++++++++++++ 1 file changed, 311 insertions(+) diff --git a/openpype/tools/publisher/control_qt.py b/openpype/tools/publisher/control_qt.py index 8515a7a843..c7099caf98 100644 --- a/openpype/tools/publisher/control_qt.py +++ b/openpype/tools/publisher/control_qt.py @@ -2,6 +2,8 @@ import collections from Qt import QtCore +from openpype.pipeline.create import CreatedInstance + from .control import MainThreadItem, PublisherController @@ -86,3 +88,312 @@ class QtPublisherController(PublisherController): def _qt_on_publish_stop(self): self._main_thread_processor.stop() + + +class QtRemotePublishController(QtPublisherController): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self._created_instances = {} + + def _on_create_instance_change(self): + # TODO somehow get serialized instances from client + serialized_instances = [] + + created_instances = {} + for serialized_data in serialized_instances: + item = CreatedInstance.deserialize_on_remote( + serialized_data, + self._creator_items + ) + created_instances[item.id] = item + + self._created_instances = created_instances + self._emit_event("instances.refresh.finished") + + @property + def project_name(self): + """Current context project name. + + Returns: + str: Name of project. + """ + + pass + + @property + def current_asset_name(self): + """Current context asset name. + + Returns: + Union[str, None]: Name of asset. + """ + + pass + + @property + def current_task_name(self): + """Current context task name. + + Returns: + Union[str, None]: Name of task. + """ + + pass + + @property + def host_is_valid(self): + """Host is valid for creation part. + + Host must have implemented certain functionality to be able create + in Publisher tool. + + Returns: + bool: Host can handle creation of instances. + """ + + pass + + @property + def instances(self): + """Collected/created instances. + + Returns: + List[CreatedInstance]: List of created instances. + """ + + return self._created_instances + + def get_context_title(self): + """Get context title for artist shown at the top of main window. + + Returns: + Union[str, None]: Context title for window or None. In case of None + a warning is displayed (not nice for artists). + """ + + pass + + def get_asset_docs(self): + pass + + def get_asset_hierarchy(self): + pass + + def get_task_names_by_asset_names(self, asset_names): + pass + + def get_existing_subset_names(self, asset_name): + pass + + def reset(self): + """Reset whole controller. + + This should reset create context, publish context and all variables + that are related to it. + """ + + pass + + def get_publish_attribute_definitions(self, instances, include_context): + pass + + def get_subset_name( + self, + creator_identifier, + variant, + task_name, + asset_name, + instance_id=None + ): + """Get subset name based on passed data. + + Args: + creator_identifier (str): Identifier of creator which should be + responsible for subset name creation. + variant (str): Variant value from user's input. + task_name (str): Name of task for which is instance created. + asset_name (str): Name of asset for which is instance created. + instance_id (Union[str, None]): Existing instance id when subset + name is updated. + """ + + pass + + def create( + self, creator_identifier, subset_name, instance_data, options + ): + """Trigger creation by creator identifier. + + Should also trigger refresh of instanes. + + Args: + creator_identifier (str): Identifier of Creator plugin. + subset_name (str): Calculated subset name. + instance_data (Dict[str, Any]): Base instance data with variant, + asset name and task name. + options (Dict[str, Any]): Data from pre-create attributes. + """ + + pass + + def save_changes(self): + """Save changes happened during creation.""" + + created_instance_changes = {} + for instance_id, instance in self._created_instances.items(): + created_instance_changes[instance_id] = ( + instance.remote_changes() + ) + + # TODO trigger save changes + self._trigger("save_changes", created_instance_changes) + + def remove_instances(self, instances): + """Remove list of instances from create context.""" + # TODO add Args: + + pass + + @property + def publish_has_finished(self): + """Has publishing finished. + + Returns: + bool: If publishing finished and all plugins were iterated. + """ + + pass + + @property + def publish_is_running(self): + """Publishing is running right now. + + Returns: + bool: If publishing is in progress. + """ + + pass + + @property + def publish_has_validated(self): + """Publish validation passed. + + Returns: + bool: If publishing passed last possible validation order. + """ + + pass + + @property + def publish_has_crashed(self): + """Publishing crashed for any reason. + + Returns: + bool: Publishing crashed. + """ + + pass + + @property + def publish_has_validation_errors(self): + """During validation happened at least one validation error. + + Returns: + bool: Validation error was raised during validation. + """ + + pass + + @property + def publish_max_progress(self): + """Get maximum possible progress number. + + Returns: + int: Number that can be used as 100% of publish progress bar. + """ + + pass + + @property + def publish_progress(self): + """Current progress number. + + Returns: + int: Current progress value which is between 0 and + 'publish_max_progress'. + """ + + pass + + @property + def publish_comment_is_set(self): + """Publish comment was at least once set. + + Publish comment can be set only once when publish is started for a + first time. This helpt to idetify if 'set_comment' should be called or + not. + """ + + pass + + def get_publish_crash_error(self): + pass + + def get_publish_report(self): + pass + + def get_validation_errors(self): + pass + + def publish(self): + """Trigger publishing without any order limitations.""" + + pass + + def validate(self): + """Trigger publishing which will stop after validation order.""" + + pass + + def stop_publish(self): + """Stop publishing can be also used to pause publishing. + + Pause of publishing is possible only if all plugins successfully + finished. + """ + + pass + + def run_action(self, plugin_id, action_id): + """Trigger pyblish action on a plugin. + + Args: + plugin_id (str): Id of publish plugin. + action_id (str): Id of publish action. + """ + + pass + + def set_comment(self, comment): + """Set comment on pyblish context. + + Set "comment" key on current pyblish.api.Context data. + + Args: + comment (str): Artist's comment. + """ + + pass + + def emit_card_message(self, message): + """Emit a card message which can have a lifetime. + + This is for UI purposes. Method can be extended to more arguments + in future e.g. different message timeout or type (color). + + Args: + message (str): Message that will be showed. + """ + + pass From 811e7853e5c031d23fa51dabbc07cc0caf3bc2f1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 10 Oct 2022 14:16:53 +0200 Subject: [PATCH 47/90] added ability to serailize and deserialize event to data --- openpype/lib/events.py | 49 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/openpype/lib/events.py b/openpype/lib/events.py index 301d62e2a6..747761fb3e 100644 --- a/openpype/lib/events.py +++ b/openpype/lib/events.py @@ -1,6 +1,7 @@ """Events holding data about specific event.""" import os import re +import copy import inspect import logging import weakref @@ -207,6 +208,12 @@ class Event(object): @property def source(self): + """Event's source used for triggering callbacks. + + Returns: + Union[str, None]: Source string or None. Source is optional. + """ + return self._source @property @@ -215,6 +222,12 @@ class Event(object): @property def topic(self): + """Event's topic used for triggering callbacks. + + Returns: + str: Topic string. + """ + return self._topic def emit(self): @@ -227,6 +240,42 @@ class Event(object): ) self._event_system.emit_event(self) + def to_data(self): + """Convert Event object to data. + + Returns: + Dict[str, Any]: Event data. + """ + + return { + "id": self.id, + "topic": self.topic, + "source": self.source, + "data": copy.deepcopy(self.data) + } + + @classmethod + def from_data(cls, event_data, event_system=None): + """Create event from data. + + Args: + event_data (Dict[str, Any]): Event data with defined keys. Can be + created using 'to_data' method. + event_system (EventSystem): System to which the event belongs. + + Returns: + Event: Event with attributes from passed data. + """ + + obj = cls( + event_data["topic"], + event_data["data"], + event_data["source"], + event_system + ) + obj._id = event_data["id"] + return obj + class EventSystem(object): """Encapsulate event handling into an object. From d1f3c8e18e7fe4c87a07007918715e1b368937a2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 10 Oct 2022 14:33:11 +0200 Subject: [PATCH 48/90] added properties with getters and setters --- openpype/tools/publisher/control.py | 128 +++++++++++++++++++++------- 1 file changed, 95 insertions(+), 33 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index b08486654c..9ca9924f39 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1278,12 +1278,15 @@ class PublisherController(AbstractPublisherController): self._publish_validation_errors = PublishValidationErrors() # Any other exception that happened during publishing self._publish_error = None + self._publish_error_msg = None # Publishing is in progress self._publish_is_running = False # Publishing is over validation order - self._publish_validated = False + self._publish_has_validated = False # Publishing should stop at validation stage self._publish_up_validation = False + self._publish_has_validation_errors = False + self._publish_has_crashed = False # All publish plugins are processed self._publish_finished = False self._publish_max_progress = 0 @@ -1642,41 +1645,100 @@ class PublisherController(AbstractPublisherController): self._emit_event("instances.refresh.finished") # --- Publish specific implementations --- - @property - def publish_has_finished(self): - return self._publish_finished - - @property - def publish_is_running(self): - return self._publish_is_running - - @property - def publish_has_validated(self): - return self._publish_validated - - @property - def publish_has_crashed(self): - return bool(self._publish_error) - - @property - def publish_has_validation_errors(self): - return bool(self._publish_validation_errors) - - @property - def publish_max_progress(self): - return self._publish_max_progress - - @property - def publish_progress(self): - return self._publish_progress - - @property - def publish_comment_is_set(self): - return self._publish_comment_is_set - def get_publish_crash_error(self): return self._publish_error + def _get_publish_has_finished(self): + return self._publish_finished + + def _set_publish_has_finished(self, value): + if self._publish_finished != value: + self._publish_finished = value + + def _get_publish_is_running(self): + return self._publish_is_running + + def _set_publish_is_running(self, value): + if self._publish_is_running != value: + self._publish_is_running = value + self._emit_event("publish.is_running.changed", {"value": value}) + + def _get_publish_has_validated(self): + return self._publish_has_validated + + def _set_publish_has_validated(self, value): + if self._publish_has_validated != value: + self._publish_has_validated = value + self._emit_event("publish.has_validated.changed", {"value": value}) + + def _get_publish_has_crashed(self): + return self._publish_has_crashed + + def _set_publish_has_crashed(self, value): + if self._publish_has_crashed != value: + self._publish_has_crashed = value + self._emit_event("publish.has_crashed.changed", {"value": value}) + + def _get_publish_has_validation_errors(self): + return self._publish_has_validation_errors + + def _set_publish_has_validation_errors(self, value): + if self._publish_has_validation_errors != value: + self._publish_has_validation_errors = value + self._emit_event( + "publish.has_validation_errors.changed", + {"value": value} + ) + + def _get_publish_max_progress(self): + return self._publish_max_progress + + def _set_publish_max_progress(self, value): + if self._publish_max_progress != value: + self._publish_max_progress = value + self._emit_event("publish.max_progress.changed", {"value": value}) + + def _get_publish_progress(self): + return self._publish_progress + + def _set_publish_progress(self, value): + if self._publish_progress != value: + self._publish_progress = value + self._emit_event("publish.progress.changed", {"value": value}) + + def _get_publish_error_msg(self): + return self._publish_error_msg + + def _set_publish_error_msg(self, value): + if self._publish_error_msg != value: + self._publish_error_msg = value + self._emit_event("publish.publish_error.changed", {"value": value}) + + publish_has_finished = property( + _get_publish_has_finished, _set_publish_has_finished + ) + publish_is_running = property( + _get_publish_is_running, _set_publish_is_running + ) + publish_has_validated = property( + _get_publish_has_validated, _set_publish_has_validated + ) + publish_has_crashed = property( + _get_publish_has_crashed, _set_publish_has_crashed + ) + publish_has_validation_errors = property( + _get_publish_has_validation_errors, _set_publish_has_validation_errors + ) + publish_max_progress = property( + _get_publish_max_progress, _set_publish_max_progress + ) + publish_progress = property( + _get_publish_progress, _set_publish_progress + ) + publish_error_msg = property( + _get_publish_error_msg, _set_publish_error_msg + ) + def get_publish_report(self): return self._publish_report.get_report(self._publish_plugins) From c907383f88f3db6fd7eaef76321bcab11069a958 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 10 Oct 2022 14:34:23 +0200 Subject: [PATCH 49/90] use events to handle controller changes --- openpype/tools/publisher/control.py | 110 +++++++++++------- .../tools/publisher/widgets/publish_frame.py | 33 +++--- openpype/tools/publisher/window.py | 13 +-- 3 files changed, 86 insertions(+), 70 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 9ca9924f39..b4fc7cb91a 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -20,6 +20,7 @@ from openpype.lib.attribute_definitions import ( ) from openpype.pipeline import ( PublishValidationError, + KnownPublishError, registered_host, ) from openpype.pipeline.create import ( @@ -909,7 +910,7 @@ class AbstractPublisherController(object): def event_system(self): """Inner event system for publisher controller. - Event system is autocreated. + Is used for communication with UI. Event system is autocreated. Known topics: "show.detailed.help" - Detailed help requested (UI related). @@ -919,10 +920,20 @@ class AbstractPublisherController(object): "publish.reset.finished" - Controller reset finished. "publish.process.started" - Publishing started. Can be started from paused state. - "publish.process.validated" - Publishing passed validation. "publish.process.stopped" - Publishing stopped/paused process. "publish.process.plugin.changed" - Plugin state has changed. "publish.process.instance.changed" - Instance state has changed. + "publish.has_validated.changed" - Attr 'publish_has_validated' + changed. + "publish.is_running.changed" - Attr 'publish_is_running' changed. + "publish.has_validated.changed" - Attr 'has_validated' changed. + "publish.has_crashed.changed" - Attr 'publish_has_crashed' changed. + "publish.publish_error.changed" - Attr 'publish_error' + "publish.has_validation_errors.changed" - Attr + 'has_validation_errors' changed. + "publish.max_progress.changed" - Attr 'publish_max_progress' + changed. + "publish.progress.changed" - Attr 'publish_progress' changed. Returns: EventSystem: Event system which can trigger callbacks for topics. @@ -1158,27 +1169,22 @@ class AbstractPublisherController(object): """Current progress number. Returns: - int: Current progress value which is between 0 and - 'publish_max_progress'. + int: Current progress value from 0 to 'publish_max_progress'. """ pass @abstractproperty - def publish_comment_is_set(self): - """Publish comment was at least once set. + def publish_error_msg(self): + """Current error message which cause fail of publishing. - Publish comment can be set only once when publish is started for a - first time. This helpt to idetify if 'set_comment' should be called or - not. + Returns: + Union[str, None]: Message which will be showed to artist or + None. """ pass - @abstractmethod - def get_publish_crash_error(self): - pass - @abstractmethod def get_publish_report(self): pass @@ -1277,7 +1283,6 @@ class PublisherController(AbstractPublisherController): # Store exceptions of validation error self._publish_validation_errors = PublishValidationErrors() # Any other exception that happened during publishing - self._publish_error = None self._publish_error_msg = None # Publishing is in progress self._publish_is_running = False @@ -1645,9 +1650,6 @@ class PublisherController(AbstractPublisherController): self._emit_event("instances.refresh.finished") # --- Publish specific implementations --- - def get_publish_crash_error(self): - return self._publish_error - def _get_publish_has_finished(self): return self._publish_finished @@ -1746,10 +1748,13 @@ class PublisherController(AbstractPublisherController): return self._publish_validation_errors.create_report() def _reset_publish(self): - self._publish_is_running = False - self._publish_validated = False + self.publish_is_running = False + self.publish_has_validated = False + self.publish_has_crashed = False + self.publish_has_validation_errors = False + self.publish_finished = False + self._publish_up_validation = False - self._publish_finished = False self._publish_comment_is_set = False self._main_thread_iter = self._publish_iterator() @@ -1768,16 +1773,25 @@ class PublisherController(AbstractPublisherController): self._publish_report.reset(self._publish_context, self._create_context) self._publish_validation_errors.reset(self._publish_plugins_proxy) - self._publish_error = None - self._publish_max_progress = len(self._publish_plugins) - self._publish_progress = 0 + self.publish_error_msg = None + + self.publish_max_progress = len(self._publish_plugins) + self.publish_progress = 0 self._emit_event("publish.reset.finished") def set_comment(self, comment): - self._publish_context.data["comment"] = comment - self._publish_comment_is_set = True + """Set comment from ui to pyblish context. + + This should be called always before publishing is started but should + happen only once on first publish start thus variable + '_publish_comment_is_set' is used to keep track about the information. + """ + + if not self._publish_comment_is_set: + self._publish_context.data["comment"] = comment + self._publish_comment_is_set = True def publish(self): """Run publishing.""" @@ -1786,20 +1800,20 @@ class PublisherController(AbstractPublisherController): def validate(self): """Run publishing and stop after Validation.""" - if self._publish_validated: + if self.publish_has_validated: return self._publish_up_validation = True self._start_publish() def _start_publish(self): """Start or continue in publishing.""" - if self._publish_is_running: + if self.publish_is_running: return # Make sure changes are saved self.save_changes() - self._publish_is_running = True + self.publish_is_running = True self._emit_event("publish.process.started") @@ -1807,14 +1821,14 @@ class PublisherController(AbstractPublisherController): def _stop_publish(self): """Stop or pause publishing.""" - self._publish_is_running = False + self.publish_is_running = False self._emit_event("publish.process.stopped") def stop_publish(self): """Stop publishing process (any reason).""" - if self._publish_is_running: + if self.publish_is_running: self._stop_publish() def run_action(self, plugin_id, action_id): @@ -1835,14 +1849,14 @@ class PublisherController(AbstractPublisherController): # There are validation errors and validation is passed # - can't do any progree if ( - self._publish_validated - and self._publish_validation_errors + self.publish_has_validated + and self.publish_has_validation_errors ): item = MainThreadItem(self.stop_publish) # Any unexpected error happened # - everything should stop - elif self._publish_error: + elif self.publish_has_crashed: item = MainThreadItem(self.stop_publish) # Everything is ok so try to get new processing item @@ -1871,23 +1885,20 @@ class PublisherController(AbstractPublisherController): self._publish_progress = idx # Check if plugin is over validation order - if not self._publish_validated: - self._publish_validated = ( + if not self.publish_has_validated: + self.publish_has_validated = ( plugin.order >= self._validation_order ) - # Trigger callbacks when validation stage is passed - if self._publish_validated: - self._emit_event("publish.process.validated") # Stop if plugin is over validation order and process # should process up to validation. - if self._publish_up_validation and self._publish_validated: + if self._publish_up_validation and self.publish_has_validated: yield MainThreadItem(self.stop_publish) # Stop if validation is over and validation errors happened if ( - self._publish_validated - and self._publish_validation_errors + self.publish_has_validated + and self.publish_has_validation_errors ): yield MainThreadItem(self.stop_publish) @@ -1952,11 +1963,12 @@ class PublisherController(AbstractPublisherController): self._publish_report.set_plugin_skipped() # Cleanup of publishing process - self._publish_finished = True - self._publish_progress = self._publish_max_progress + self.publish_finished = True + self.publish_progress = self._publish_max_progress yield MainThreadItem(self.stop_publish) def _add_validation_error(self, result): + self.publish_has_validation_errors = False self._publish_validation_errors.add_error( result["plugin"], result["error"], @@ -1974,12 +1986,20 @@ class PublisherController(AbstractPublisherController): if exception: if ( isinstance(exception, PublishValidationError) - and not self._publish_validated + and not self.publish_has_validated ): self._add_validation_error(result) else: - self._publish_error = exception + if isinstance(exception, KnownPublishError): + msg = str(exception) + else: + msg = ( + "Something went wrong. Send report" + " to your supervisor or OpenPype." + ) + self.publish_error_msg = msg + self.publish_has_crashed = False self._publish_next_process() diff --git a/openpype/tools/publisher/widgets/publish_frame.py b/openpype/tools/publisher/widgets/publish_frame.py index b49f005640..8fd783a3c4 100644 --- a/openpype/tools/publisher/widgets/publish_frame.py +++ b/openpype/tools/publisher/widgets/publish_frame.py @@ -4,8 +4,6 @@ import time from Qt import QtWidgets, QtCore -from openpype.pipeline import KnownPublishError - from .widgets import ( StopBtn, ResetBtn, @@ -170,7 +168,7 @@ class PublishFrame(QtWidgets.QWidget): "publish.process.started", self._on_publish_start ) controller.event_system.add_callback( - "publish.process.validated", self._on_publish_validated + "publish.has_validated.changed", self._on_publish_validated_change ) controller.event_system.add_callback( "publish.process.stopped", self._on_publish_stop @@ -322,8 +320,9 @@ class PublishFrame(QtWidgets.QWidget): self._validate_btn.setEnabled(False) self._publish_btn.setEnabled(False) - def _on_publish_validated(self): - self._validate_btn.setEnabled(False) + def _on_publish_validated_change(self, event): + if event["value"]: + self._validate_btn.setEnabled(False) def _on_instance_change(self, event): """Change instance label when instance is going to be processed.""" @@ -360,10 +359,10 @@ class PublishFrame(QtWidgets.QWidget): self._validate_btn.setEnabled(validate_enabled) self._publish_btn.setEnabled(publish_enabled) - error = self._controller.get_publish_crash_error() + error_msg = self._controller.publish_error_msg validation_errors = self._controller.get_validation_errors() - if error: - self._set_error(error) + if error_msg: + self._set_error_msg(error_msg) elif validation_errors: self._set_progress_visibility(False) @@ -387,16 +386,16 @@ class PublishFrame(QtWidgets.QWidget): self._set_success_property(-1) - def _set_error(self, error): + def _set_error_msg(self, error_msg): + """Show error message to artist. + + Args: + error_msg (str): Message which is showed to artist. + """ + self._set_main_label("Error happened") - if isinstance(error, KnownPublishError): - msg = str(error) - else: - msg = ( - "Something went wrong. Send report" - " to your supervisor or OpenPype." - ) - self._message_label_top.setText(msg) + + self._message_label_top.setText(error_msg) self._set_success_property(0) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 3b3e27660d..e2beb480bd 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -248,7 +248,7 @@ class PublisherWindow(QtWidgets.QDialog): "publish.process.started", self._on_publish_start ) controller.event_system.add_callback( - "publish.process.validated", self._on_publish_validated + "publish.has_validated.changed", self._on_publish_validated_change ) controller.event_system.add_callback( "publish.process.stopped", self._on_publish_stop @@ -439,11 +439,7 @@ class PublisherWindow(QtWidgets.QDialog): self._controller.stop_publish() def _set_publish_comment(self): - if self._controller.publish_comment_is_set: - return - - comment = self._comment_input.text() - self._controller.set_comment(comment) + self._controller.set_comment(self._comment_input.text()) def _on_validate_clicked(self): self._set_publish_comment() @@ -489,8 +485,9 @@ class PublisherWindow(QtWidgets.QDialog): if self._tabs_widget.is_current_tab(self._create_tab): self._tabs_widget.set_current_tab("publish") - def _on_publish_validated(self): - self._validate_btn.setEnabled(False) + def _on_publish_validated_change(self, event): + if event["value"]: + self._validate_btn.setEnabled(False) def _on_publish_stop(self): self._set_publish_overlay_visibility(False) From 0f514aa5528efc44938a4eec671692d41329daed Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 10 Oct 2022 14:38:52 +0200 Subject: [PATCH 50/90] mark methods that should be abstract in remote controller --- openpype/tools/publisher/control_qt.py | 112 ++++--------------------- 1 file changed, 18 insertions(+), 94 deletions(-) diff --git a/openpype/tools/publisher/control_qt.py b/openpype/tools/publisher/control_qt.py index c7099caf98..8f0f304f9a 100644 --- a/openpype/tools/publisher/control_qt.py +++ b/openpype/tools/publisher/control_qt.py @@ -111,7 +111,7 @@ class QtRemotePublishController(QtPublisherController): self._created_instances = created_instances self._emit_event("instances.refresh.finished") - @property + @abstractproperty def project_name(self): """Current context project name. @@ -121,7 +121,7 @@ class QtRemotePublishController(QtPublisherController): pass - @property + @abstractproperty def current_asset_name(self): """Current context asset name. @@ -131,7 +131,7 @@ class QtRemotePublishController(QtPublisherController): pass - @property + @abstractproperty def current_task_name(self): """Current context task name. @@ -141,7 +141,7 @@ class QtRemotePublishController(QtPublisherController): pass - @property + @abstractproperty def host_is_valid(self): """Host is valid for creation part. @@ -186,6 +186,7 @@ class QtRemotePublishController(QtPublisherController): def get_existing_subset_names(self, asset_name): pass + @abstractmethod def reset(self): """Reset whole controller. @@ -195,9 +196,7 @@ class QtRemotePublishController(QtPublisherController): pass - def get_publish_attribute_definitions(self, instances, include_context): - pass - + @abstractmethod def get_subset_name( self, creator_identifier, @@ -220,6 +219,7 @@ class QtRemotePublishController(QtPublisherController): pass + @abstractmethod def create( self, creator_identifier, subset_name, instance_data, options ): @@ -237,6 +237,7 @@ class QtRemotePublishController(QtPublisherController): pass + @abstractmethod def save_changes(self): """Save changes happened during creation.""" @@ -246,116 +247,36 @@ class QtRemotePublishController(QtPublisherController): instance.remote_changes() ) - # TODO trigger save changes - self._trigger("save_changes", created_instance_changes) + # Send 'created_instance_changes' value to client + @abstractmethod def remove_instances(self, instances): """Remove list of instances from create context.""" # TODO add Args: pass - @property - def publish_has_finished(self): - """Has publishing finished. - - Returns: - bool: If publishing finished and all plugins were iterated. - """ - - pass - - @property - def publish_is_running(self): - """Publishing is running right now. - - Returns: - bool: If publishing is in progress. - """ - - pass - - @property - def publish_has_validated(self): - """Publish validation passed. - - Returns: - bool: If publishing passed last possible validation order. - """ - - pass - - @property - def publish_has_crashed(self): - """Publishing crashed for any reason. - - Returns: - bool: Publishing crashed. - """ - - pass - - @property - def publish_has_validation_errors(self): - """During validation happened at least one validation error. - - Returns: - bool: Validation error was raised during validation. - """ - - pass - - @property - def publish_max_progress(self): - """Get maximum possible progress number. - - Returns: - int: Number that can be used as 100% of publish progress bar. - """ - - pass - - @property - def publish_progress(self): - """Current progress number. - - Returns: - int: Current progress value which is between 0 and - 'publish_max_progress'. - """ - - pass - - @property - def publish_comment_is_set(self): - """Publish comment was at least once set. - - Publish comment can be set only once when publish is started for a - first time. This helpt to idetify if 'set_comment' should be called or - not. - """ - - pass - - def get_publish_crash_error(self): - pass - + @abstractmethod def get_publish_report(self): pass + @abstractmethod def get_validation_errors(self): pass + @abstractmethod def publish(self): """Trigger publishing without any order limitations.""" pass + @abstractmethod def validate(self): """Trigger publishing which will stop after validation order.""" pass + @abstractmethod def stop_publish(self): """Stop publishing can be also used to pause publishing. @@ -365,6 +286,7 @@ class QtRemotePublishController(QtPublisherController): pass + @abstractmethod def run_action(self, plugin_id, action_id): """Trigger pyblish action on a plugin. @@ -375,6 +297,7 @@ class QtRemotePublishController(QtPublisherController): pass + @abstractmethod def set_comment(self, comment): """Set comment on pyblish context. @@ -386,6 +309,7 @@ class QtRemotePublishController(QtPublisherController): pass + @abstractmethod def emit_card_message(self, message): """Emit a card message which can have a lifetime. From ebb6a17d9793b7aee94e01cb4ebe572bab26ecea Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 10 Oct 2022 14:43:57 +0200 Subject: [PATCH 51/90] trigger event on finished attribute change --- openpype/tools/publisher/control.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index b4fc7cb91a..dd7e90ea5f 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1656,6 +1656,7 @@ class PublisherController(AbstractPublisherController): def _set_publish_has_finished(self, value): if self._publish_finished != value: self._publish_finished = value + self._emit_event("publish.finished.changed", {"value": value}) def _get_publish_is_running(self): return self._publish_is_running From 8ffdbf0dcfc70d0bf2741cdce7464864e82f0051 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 10 Oct 2022 14:44:51 +0200 Subject: [PATCH 52/90] instances are removed by ids --- openpype/tools/publisher/control.py | 13 +++++++++---- openpype/tools/publisher/control_qt.py | 2 +- openpype/tools/publisher/widgets/overview_widget.py | 6 +++++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index dd7e90ea5f..0981f48dbe 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1098,7 +1098,7 @@ class AbstractPublisherController(object): pass - def remove_instances(self, instances): + def remove_instances(self, instance_ids): """Remove list of instances from create context.""" # TODO expect instance ids @@ -1632,18 +1632,23 @@ class PublisherController(AbstractPublisherController): if self._create_context.host_is_valid: self._create_context.save_changes() - def remove_instances(self, instances): + def remove_instances(self, instance_ids): """""" # 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() - self._remove_instances_from_context(instances) + self._remove_instances_from_context(instance_ids) self._on_create_instance_change() - def _remove_instances_from_context(self, instances): + def _remove_instances_from_context(self, instance_ids): + instances_by_id = self._create_context.instances_by_id + instances = [ + instances_by_id[instance_id] + for instance_id in instance_ids + ] self._create_context.remove_instances(instances) def _on_create_instance_change(self): diff --git a/openpype/tools/publisher/control_qt.py b/openpype/tools/publisher/control_qt.py index 8f0f304f9a..69809bcfe8 100644 --- a/openpype/tools/publisher/control_qt.py +++ b/openpype/tools/publisher/control_qt.py @@ -250,7 +250,7 @@ class QtRemotePublishController(QtPublisherController): # Send 'created_instance_changes' value to client @abstractmethod - def remove_instances(self, instances): + def remove_instances(self, instance_ids): """Remove list of instances from create context.""" # TODO add Args: diff --git a/openpype/tools/publisher/widgets/overview_widget.py b/openpype/tools/publisher/widgets/overview_widget.py index 08c2ce0513..3c67e6298e 100644 --- a/openpype/tools/publisher/widgets/overview_widget.py +++ b/openpype/tools/publisher/widgets/overview_widget.py @@ -224,7 +224,11 @@ class OverviewWidget(QtWidgets.QFrame): dialog.exec_() # Skip if OK was not clicked if dialog.result() == QtWidgets.QMessageBox.Ok: - self._controller.remove_instances(instances) + instance_ids = { + instance.id + for instance in instances + } + self._controller.remove_instances(instance_ids) def _on_change_view_clicked(self): self._change_view_type() From 91b66812dbb9adc00a42b634608291d934a7e30b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 10 Oct 2022 14:45:16 +0200 Subject: [PATCH 53/90] added some basic implementation of client event handling --- openpype/tools/publisher/control_qt.py | 71 ++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 3 deletions(-) diff --git a/openpype/tools/publisher/control_qt.py b/openpype/tools/publisher/control_qt.py index 69809bcfe8..5638ea554a 100644 --- a/openpype/tools/publisher/control_qt.py +++ b/openpype/tools/publisher/control_qt.py @@ -1,7 +1,9 @@ import collections +from ABC import abstractmethod, abstractproperty from Qt import QtCore +from openpype.lib.events import Event from openpype.pipeline.create import CreatedInstance from .control import MainThreadItem, PublisherController @@ -90,15 +92,29 @@ class QtPublisherController(PublisherController): self._main_thread_processor.stop() -class QtRemotePublishController(QtPublisherController): +class QtRemotePublishController(PublisherController): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._created_instances = {} + self._main_thread_processor = MainThreadProcess() + self._main_thread_processor.start() + + @abstractmethod + def _get_serialized_instances(self): + """Receive serialized instances from client process. + + Returns: + List[Dict[str, Any]]: Serialized instances. + """ + + pass + + def _process_main_thread_item(self, item): + self._main_thread_processor.add_item(item) def _on_create_instance_change(self): - # TODO somehow get serialized instances from client - serialized_instances = [] + serialized_instances = self._get_serialized_instances() created_instances = {} for serialized_data in serialized_instances: @@ -111,6 +127,55 @@ class QtRemotePublishController(QtPublisherController): self._created_instances = created_instances self._emit_event("instances.refresh.finished") + def remote_events_handler(self, event_data): + event = Event.from_data(event_data) + + # Topics that cause "replication" of controller changes + if event.topic == "publish.max_progress.changed": + self.publish_max_progress = event["value"] + return + + if event.topic == "publish.progress.changed": + self.publish_progress = event["value"] + return + + if event.topic == "publish.has_validated.changed": + self.publish_has_validated = event["value"] + return + + if event.topic == "publish.is_running.changed": + self.publish_is_running = event["value"] + return + + if event.topic == "publish.publish_error.changed": + self.publish_error_msg = event["value"] + return + + if event.topic == "publish.has_crashed.changed": + self.publish_has_crashed = event["value"] + return + + if event.topic == "publish.has_validation_errors.changed": + self.publish_has_validation_errors = event["value"] + return + + if event.topic == "publish.finished.changed": + self.publish_finished = event["value"] + return + + # Topics that can be just passed by because are not affecting + # controller itself + # - "show.card.message" + # - "show.detailed.help" + # - "publish.reset.finished" + # - "instances.refresh.finished" + # - "plugins.refresh.finished" + # - "publish.process.started" + # - "publish.process.stopped" + # - "publish.process.plugin.changed" + # - "publish.process.instance.changed" + self.event_system.emit_event(event) + @abstractproperty def project_name(self): """Current context project name. From ac3326d29690183206eed23520a6fad48e0982de Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 10 Oct 2022 15:08:43 +0200 Subject: [PATCH 54/90] fix import --- openpype/tools/publisher/control_qt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/publisher/control_qt.py b/openpype/tools/publisher/control_qt.py index 5638ea554a..10f576a3f3 100644 --- a/openpype/tools/publisher/control_qt.py +++ b/openpype/tools/publisher/control_qt.py @@ -1,5 +1,5 @@ import collections -from ABC import abstractmethod, abstractproperty +from abc import abstractmethod, abstractproperty from Qt import QtCore From 187411ef8bab8241662add0c57f81d538f3b008c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 10 Oct 2022 15:10:18 +0200 Subject: [PATCH 55/90] added BaseController to handle base attributes --- openpype/tools/publisher/control.py | 319 +++++++++++++++------------- 1 file changed, 172 insertions(+), 147 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 0981f48dbe..f2f6d07cd6 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -917,7 +917,8 @@ class AbstractPublisherController(object): "show.card.message" - Show card message request (UI related). "instances.refresh.finished" - Instances are refreshed. "plugins.refresh.finished" - Plugins refreshed. - "publish.reset.finished" - Controller reset finished. + "publish.reset.finished" - Publish context reset finished. + "controller.reset.finished" - Controller reset finished. "publish.process.started" - Publishing started. Can be started from paused state. "publish.process.stopped" - Publishing stopped/paused process. @@ -934,6 +935,8 @@ class AbstractPublisherController(object): "publish.max_progress.changed" - Attr 'publish_max_progress' changed. "publish.progress.changed" - Attr 'publish_progress' changed. + "publish.host_is_valid.changed" - Attr 'host_is_valid' changed. + "publish.finished.changed" - Attr 'publish_finished' changed. Returns: EventSystem: Event system which can trigger callbacks for topics. @@ -943,6 +946,11 @@ class AbstractPublisherController(object): self._event_system = EventSystem() return self._event_system + def _emit_event(self, topic, data=None): + if data is None: + data = {} + self.event_system.emit(topic, data, "controller") + @abstractproperty def project_name(self): """Current context project name. @@ -1252,7 +1260,156 @@ class AbstractPublisherController(object): pass -class PublisherController(AbstractPublisherController): +class BasePublishController(AbstractPublisherController): + def __init__(self): + # Controller must implement it's update + self._creator_items = {} + + self._host_is_valid = False + + # Any other exception that happened during publishing + self._publish_error_msg = None + # Publishing is in progress + self._publish_is_running = False + # Publishing is over validation order + self._publish_has_validated = False + + self._publish_has_validation_errors = False + self._publish_has_crashed = False + # All publish plugins are processed + self._publish_finished = False + self._publish_max_progress = 0 + self._publish_progress = 0 + + @property + def creator_items(self): + """Creators that can be shown in create dialog.""" + + return self._creator_items + + def get_creator_icon(self, identifier): + creator_item = self._creator_items.get(identifier) + if creator_item is not None: + return creator_item.icon + return None + + def _get_host_is_valid(self): + return self._host_is_valid + + def _set_host_is_valid(self, value): + if self._host_is_valid != value: + self._host_is_valid = value + self._emit_event("publish.host_is_valid.changed", {"value": value}) + + def _get_publish_has_finished(self): + return self._publish_finished + + def _set_publish_has_finished(self, value): + if self._publish_finished != value: + self._publish_finished = value + self._emit_event("publish.finished.changed", {"value": value}) + + def _get_publish_is_running(self): + return self._publish_is_running + + def _set_publish_is_running(self, value): + if self._publish_is_running != value: + self._publish_is_running = value + self._emit_event("publish.is_running.changed", {"value": value}) + + def _get_publish_has_validated(self): + return self._publish_has_validated + + def _set_publish_has_validated(self, value): + if self._publish_has_validated != value: + self._publish_has_validated = value + self._emit_event("publish.has_validated.changed", {"value": value}) + + def _get_publish_has_crashed(self): + return self._publish_has_crashed + + def _set_publish_has_crashed(self, value): + if self._publish_has_crashed != value: + self._publish_has_crashed = value + self._emit_event("publish.has_crashed.changed", {"value": value}) + + def _get_publish_has_validation_errors(self): + return self._publish_has_validation_errors + + def _set_publish_has_validation_errors(self, value): + if self._publish_has_validation_errors != value: + self._publish_has_validation_errors = value + self._emit_event( + "publish.has_validation_errors.changed", + {"value": value} + ) + + def _get_publish_max_progress(self): + return self._publish_max_progress + + def _set_publish_max_progress(self, value): + if self._publish_max_progress != value: + self._publish_max_progress = value + self._emit_event("publish.max_progress.changed", {"value": value}) + + def _get_publish_progress(self): + return self._publish_progress + + def _set_publish_progress(self, value): + if self._publish_progress != value: + self._publish_progress = value + self._emit_event("publish.progress.changed", {"value": value}) + + def _get_publish_error_msg(self): + return self._publish_error_msg + + def _set_publish_error_msg(self, value): + if self._publish_error_msg != value: + self._publish_error_msg = value + self._emit_event("publish.publish_error.changed", {"value": value}) + + host_is_valid = property( + _get_host_is_valid, _set_host_is_valid + ) + publish_has_finished = property( + _get_publish_has_finished, _set_publish_has_finished + ) + publish_is_running = property( + _get_publish_is_running, _set_publish_is_running + ) + publish_has_validated = property( + _get_publish_has_validated, _set_publish_has_validated + ) + publish_has_crashed = property( + _get_publish_has_crashed, _set_publish_has_crashed + ) + publish_has_validation_errors = property( + _get_publish_has_validation_errors, _set_publish_has_validation_errors + ) + publish_max_progress = property( + _get_publish_max_progress, _set_publish_max_progress + ) + publish_progress = property( + _get_publish_progress, _set_publish_progress + ) + publish_error_msg = property( + _get_publish_error_msg, _set_publish_error_msg + ) + + def _reset_attributes(self): + """Reset most of attributes that can be reset.""" + + self.publish_is_running = False + self.publish_has_validated = False + self.publish_has_crashed = False + self.publish_has_validation_errors = False + self.publish_finished = False + + self.publish_error_msg = None + self.publish_progress = 0 + + +class PublisherController(BasePublishController): """Middleware between UI, CreateContext and publish Context. Handle both creation and publishing parts. @@ -1265,6 +1422,8 @@ class PublisherController(AbstractPublisherController): _log = None def __init__(self, dbcon=None, headless=False): + super(PublisherController, self).__init__() + self._host = registered_host() self._headless = headless @@ -1272,8 +1431,6 @@ class PublisherController(AbstractPublisherController): self._host, dbcon, headless=headless, reset=False ) - self._creator_items = {} - self._publish_plugins_proxy = None # pyblish.api.Context @@ -1282,20 +1439,9 @@ class PublisherController(AbstractPublisherController): self._publish_report = PublishReport(self) # Store exceptions of validation error self._publish_validation_errors = PublishValidationErrors() - # Any other exception that happened during publishing - self._publish_error_msg = None - # Publishing is in progress - self._publish_is_running = False - # Publishing is over validation order - self._publish_has_validated = False + # Publishing should stop at validation stage self._publish_up_validation = False - self._publish_has_validation_errors = False - self._publish_has_crashed = False - # All publish plugins are processed - self._publish_finished = False - self._publish_max_progress = 0 - self._publish_progress = 0 # This information is not much important for controller but for widget # which can change (and set) the comment. self._publish_comment_is_set = False @@ -1317,12 +1463,6 @@ class PublisherController(AbstractPublisherController): # Cacher of avalon documents self._asset_docs_cache = AssetDocsCache(self) - @property - def log(self): - if self._log is None: - self._log = logging.getLogger("PublisherController") - return self._log - @property def project_name(self): """Current project context defined by host. @@ -1364,28 +1504,11 @@ class PublisherController(AbstractPublisherController): return self._create_context.creators - @property - def creator_items(self): - """Creators that can be shown in create dialog.""" - - return self._creator_items - - @property - def host_is_valid(self): - """Host is valid for creation.""" - - return self._create_context.host_is_valid - @property def _publish_plugins(self): """Publish plugins.""" return self._create_context.publish_plugins - def _emit_event(self, topic, data=None): - if data is None: - data = {} - self.event_system.emit(topic, data, "controller") - # --- Publish specific callbacks --- def get_asset_docs(self): """Get asset documents from cache for whole project.""" @@ -1450,6 +1573,8 @@ class PublisherController(AbstractPublisherController): self.save_changes() + self.host_is_valid = self._create_context.host_is_valid + # Reset avalon context self._create_context.reset_avalon_context() @@ -1460,6 +1585,8 @@ class PublisherController(AbstractPublisherController): self._reset_publish() self._reset_instances() + self._emit_event("controller.reset.finished") + self.emit_card_message("Refreshed..") def _reset_plugins(self): @@ -1584,12 +1711,6 @@ class PublisherController(AbstractPublisherController): )) return output - def get_creator_icon(self, identifier): - creator_item = self._creator_items.get(identifier) - if creator_item is not None: - return creator_item.icon - return None - def get_subset_name( self, creator_identifier, @@ -1633,7 +1754,11 @@ class PublisherController(AbstractPublisherController): self._create_context.save_changes() def remove_instances(self, instance_ids): - """""" + """Remove instances based on instance ids. + + 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. @@ -1654,99 +1779,6 @@ class PublisherController(AbstractPublisherController): def _on_create_instance_change(self): self._emit_event("instances.refresh.finished") - # --- Publish specific implementations --- - def _get_publish_has_finished(self): - return self._publish_finished - - def _set_publish_has_finished(self, value): - if self._publish_finished != value: - self._publish_finished = value - self._emit_event("publish.finished.changed", {"value": value}) - - def _get_publish_is_running(self): - return self._publish_is_running - - def _set_publish_is_running(self, value): - if self._publish_is_running != value: - self._publish_is_running = value - self._emit_event("publish.is_running.changed", {"value": value}) - - def _get_publish_has_validated(self): - return self._publish_has_validated - - def _set_publish_has_validated(self, value): - if self._publish_has_validated != value: - self._publish_has_validated = value - self._emit_event("publish.has_validated.changed", {"value": value}) - - def _get_publish_has_crashed(self): - return self._publish_has_crashed - - def _set_publish_has_crashed(self, value): - if self._publish_has_crashed != value: - self._publish_has_crashed = value - self._emit_event("publish.has_crashed.changed", {"value": value}) - - def _get_publish_has_validation_errors(self): - return self._publish_has_validation_errors - - def _set_publish_has_validation_errors(self, value): - if self._publish_has_validation_errors != value: - self._publish_has_validation_errors = value - self._emit_event( - "publish.has_validation_errors.changed", - {"value": value} - ) - - def _get_publish_max_progress(self): - return self._publish_max_progress - - def _set_publish_max_progress(self, value): - if self._publish_max_progress != value: - self._publish_max_progress = value - self._emit_event("publish.max_progress.changed", {"value": value}) - - def _get_publish_progress(self): - return self._publish_progress - - def _set_publish_progress(self, value): - if self._publish_progress != value: - self._publish_progress = value - self._emit_event("publish.progress.changed", {"value": value}) - - def _get_publish_error_msg(self): - return self._publish_error_msg - - def _set_publish_error_msg(self, value): - if self._publish_error_msg != value: - self._publish_error_msg = value - self._emit_event("publish.publish_error.changed", {"value": value}) - - publish_has_finished = property( - _get_publish_has_finished, _set_publish_has_finished - ) - publish_is_running = property( - _get_publish_is_running, _set_publish_is_running - ) - publish_has_validated = property( - _get_publish_has_validated, _set_publish_has_validated - ) - publish_has_crashed = property( - _get_publish_has_crashed, _set_publish_has_crashed - ) - publish_has_validation_errors = property( - _get_publish_has_validation_errors, _set_publish_has_validation_errors - ) - publish_max_progress = property( - _get_publish_max_progress, _set_publish_max_progress - ) - publish_progress = property( - _get_publish_progress, _set_publish_progress - ) - publish_error_msg = property( - _get_publish_error_msg, _set_publish_error_msg - ) - def get_publish_report(self): return self._publish_report.get_report(self._publish_plugins) @@ -1754,11 +1786,7 @@ class PublisherController(AbstractPublisherController): return self._publish_validation_errors.create_report() def _reset_publish(self): - self.publish_is_running = False - self.publish_has_validated = False - self.publish_has_crashed = False - self.publish_has_validation_errors = False - self.publish_finished = False + self._reset_attributes() self._publish_up_validation = False self._publish_comment_is_set = False @@ -1780,10 +1808,7 @@ class PublisherController(AbstractPublisherController): self._publish_report.reset(self._publish_context, self._create_context) self._publish_validation_errors.reset(self._publish_plugins_proxy) - self.publish_error_msg = None - self.publish_max_progress = len(self._publish_plugins) - self.publish_progress = 0 self._emit_event("publish.reset.finished") From 16aff5224fd86552e84744dc6201d30c2e14863e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 10 Oct 2022 15:24:40 +0200 Subject: [PATCH 56/90] fix attribute changes --- openpype/tools/publisher/control.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index f2f6d07cd6..014efd5c01 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1995,11 +1995,11 @@ class PublisherController(BasePublishController): # Cleanup of publishing process self.publish_finished = True - self.publish_progress = self._publish_max_progress + self.publish_progress = self.publish_max_progress yield MainThreadItem(self.stop_publish) def _add_validation_error(self, result): - self.publish_has_validation_errors = False + self.publish_has_validation_errors = True self._publish_validation_errors.add_error( result["plugin"], result["error"], @@ -2030,7 +2030,7 @@ class PublisherController(BasePublishController): " to your supervisor or OpenPype." ) self.publish_error_msg = msg - self.publish_has_crashed = False + self.publish_has_crashed = True self._publish_next_process() From f9155bd6429933e8df407f9673d603bae5e71e6f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 10 Oct 2022 15:28:45 +0200 Subject: [PATCH 57/90] implemented base controller --- openpype/tools/publisher/control.py | 181 ++++++++++++++++++---------- 1 file changed, 115 insertions(+), 66 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 014efd5c01..32a5d62fb5 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -891,10 +891,7 @@ class AbstractPublisherController(object): access objects directly but by using wrappers that can be serialized. """ - _log = None - _event_system = None - - @property + @abstractproperty def log(self): """Controller's logger object. @@ -902,54 +899,13 @@ class AbstractPublisherController(object): logging.Logger: Logger object that can be used for logging. """ - if self._log is None: - self._log = logging.getLogget(self.__class__.__name__) - return self._log + pass - @property + @abstractproperty def event_system(self): - """Inner event system for publisher controller. + """Inner event system for publisher controller.""" - Is used for communication with UI. Event system is autocreated. - - Known topics: - "show.detailed.help" - Detailed help requested (UI related). - "show.card.message" - Show card message request (UI related). - "instances.refresh.finished" - Instances are refreshed. - "plugins.refresh.finished" - Plugins refreshed. - "publish.reset.finished" - Publish context reset finished. - "controller.reset.finished" - Controller reset finished. - "publish.process.started" - Publishing started. Can be started from - paused state. - "publish.process.stopped" - Publishing stopped/paused process. - "publish.process.plugin.changed" - Plugin state has changed. - "publish.process.instance.changed" - Instance state has changed. - "publish.has_validated.changed" - Attr 'publish_has_validated' - changed. - "publish.is_running.changed" - Attr 'publish_is_running' changed. - "publish.has_validated.changed" - Attr 'has_validated' changed. - "publish.has_crashed.changed" - Attr 'publish_has_crashed' changed. - "publish.publish_error.changed" - Attr 'publish_error' - "publish.has_validation_errors.changed" - Attr - 'has_validation_errors' changed. - "publish.max_progress.changed" - Attr 'publish_max_progress' - changed. - "publish.progress.changed" - Attr 'publish_progress' changed. - "publish.host_is_valid.changed" - Attr 'host_is_valid' changed. - "publish.finished.changed" - Attr 'publish_finished' changed. - - Returns: - EventSystem: Event system which can trigger callbacks for topics. - """ - - if self._event_system is None: - self._event_system = EventSystem() - return self._event_system - - def _emit_event(self, topic, data=None): - if data is None: - data = {} - self.event_system.emit(topic, data, "controller") + pass @abstractproperty def project_name(self): @@ -1261,10 +1217,22 @@ class AbstractPublisherController(object): class BasePublishController(AbstractPublisherController): - def __init__(self): - # Controller must implement it's update - self._creator_items = {} + """Implement common logic for controllers. + Implement event system, logger and common attributes. Attributes are + triggering value changes so anyone can listen to their topics. + + Prepare implementation for creator items. Controller must implement just + their filling by '_collect_creator_items'. + + All prepared implementation is based on calling super '__init__'. + """ + + def __init__(self): + self._log = None + self._event_system = None + + # Host is valid for creation self._host_is_valid = False # Any other exception that happened during publishing @@ -1281,17 +1249,65 @@ class BasePublishController(AbstractPublisherController): self._publish_max_progress = 0 self._publish_progress = 0 + # Controller must '_collect_creator_items' to fill the value + self._creator_items = None + @property - def creator_items(self): - """Creators that can be shown in create dialog.""" + def log(self): + """Controller's logger object. - return self._creator_items + Returns: + logging.Logger: Logger object that can be used for logging. + """ - def get_creator_icon(self, identifier): - creator_item = self._creator_items.get(identifier) - if creator_item is not None: - return creator_item.icon - return None + if self._log is None: + self._log = logging.getLogget(self.__class__.__name__) + return self._log + + @property + def event_system(self): + """Inner event system for publisher controller. + + Is used for communication with UI. Event system is autocreated. + + Known topics: + "show.detailed.help" - Detailed help requested (UI related). + "show.card.message" - Show card message request (UI related). + "instances.refresh.finished" - Instances are refreshed. + "plugins.refresh.finished" - Plugins refreshed. + "publish.reset.finished" - Publish context reset finished. + "controller.reset.finished" - Controller reset finished. + "publish.process.started" - Publishing started. Can be started from + paused state. + "publish.process.stopped" - Publishing stopped/paused process. + "publish.process.plugin.changed" - Plugin state has changed. + "publish.process.instance.changed" - Instance state has changed. + "publish.has_validated.changed" - Attr 'publish_has_validated' + changed. + "publish.is_running.changed" - Attr 'publish_is_running' changed. + "publish.has_validated.changed" - Attr 'has_validated' changed. + "publish.has_crashed.changed" - Attr 'publish_has_crashed' changed. + "publish.publish_error.changed" - Attr 'publish_error' + "publish.has_validation_errors.changed" - Attr + 'has_validation_errors' changed. + "publish.max_progress.changed" - Attr 'publish_max_progress' + changed. + "publish.progress.changed" - Attr 'publish_progress' changed. + "publish.host_is_valid.changed" - Attr 'host_is_valid' changed. + "publish.finished.changed" - Attr 'publish_finished' changed. + + Returns: + EventSystem: Event system which can trigger callbacks for topics. + """ + + if self._event_system is None: + self._event_system = EventSystem() + return self._event_system + + def _emit_event(self, topic, data=None): + if data is None: + data = {} + self.event_system.emit(topic, data, "controller") def _get_host_is_valid(self): return self._host_is_valid @@ -1399,6 +1415,9 @@ class BasePublishController(AbstractPublisherController): def _reset_attributes(self): """Reset most of attributes that can be reset.""" + # Reset creator items + self._creator_items = None + self.publish_is_running = False self.publish_has_validated = False self.publish_has_crashed = False @@ -1408,6 +1427,35 @@ class BasePublishController(AbstractPublisherController): self.publish_error_msg = None self.publish_progress = 0 + @property + def creator_items(self): + """Creators that can be shown in create dialog.""" + if self._creator_items is None: + self._creator_items = self._collect_creator_items() + return self._creator_items + + @abstractmethod + def _collect_creator_items(self): + """Receive CreatorItems to work with. + + Returns: + Dict[str, CreatorItem]: Creator items by their identifier. + """ + + pass + + def get_creator_icon(self, identifier): + """Function to receive icon for creator identifier. + + Args: + str: Creator's identifier for which should be icon returned. + """ + + creator_item = self.creator_items.get(identifier) + if creator_item is not None: + return creator_item.icon + return None + class PublisherController(BasePublishController): """Middleware between UI, CreateContext and publish Context. @@ -1598,15 +1646,16 @@ class PublisherController(BasePublishController): self._create_context.reset_plugins() - self._creator_items = { - identifier: CreatorItem.from_creator(creator) - for identifier, creator in self._create_context.creators.items() - } - self._resetting_plugins = False self._emit_event("plugins.refresh.finished") + def _collect_creator_items(self): + return { + identifier: CreatorItem.from_creator(creator) + for identifier, creator in self._create_context.creators.items() + } + def _reset_instances(self): """Reset create instances.""" if self._resetting_instances: @@ -1638,7 +1687,7 @@ class PublisherController(BasePublishController): _attr_defs = {} for instance in instances: creator_identifier = instance.creator_identifier - creator_item = self._creator_items[creator_identifier] + creator_item = self.creator_items[creator_identifier] for attr_def in creator_item.instance_attributes_defs: found_idx = None for idx, _attr_def in _attr_defs.items(): From 2baa3a5b5449ece1972a4471ccad8396fd826af0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 10 Oct 2022 17:08:40 +0200 Subject: [PATCH 58/90] fix typo --- openpype/tools/publisher/widgets/validations_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/validations_widget.py b/openpype/tools/publisher/widgets/validations_widget.py index 74be672f3b..06ac0bad8a 100644 --- a/openpype/tools/publisher/widgets/validations_widget.py +++ b/openpype/tools/publisher/widgets/validations_widget.py @@ -678,7 +678,7 @@ class ValidationsWidget(QtWidgets.QFrame): self._set_errors(validation_errors) return - if self._contoller.publish_has_finished: + if self._controller.publish_has_finished: self._set_current_widget(self._publish_stop_ok_widget) return From 4d40024bdbaf4fbee276e2957d0de534675bb3b2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 10 Oct 2022 18:35:32 +0200 Subject: [PATCH 59/90] added double click for asset dialog --- .../tools/publisher/widgets/assets_widget.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/openpype/tools/publisher/widgets/assets_widget.py b/openpype/tools/publisher/widgets/assets_widget.py index 39bf3886ea..996c9029d4 100644 --- a/openpype/tools/publisher/widgets/assets_widget.py +++ b/openpype/tools/publisher/widgets/assets_widget.py @@ -1,6 +1,7 @@ import collections from Qt import QtWidgets, QtCore, QtGui + from openpype.tools.utils import ( PlaceholderLineEdit, RecursiveSortFilterProxyModel, @@ -163,6 +164,16 @@ class AssetsHierarchyModel(QtGui.QStandardItemModel): return item_name in self._items_by_name +class AssetDialogView(QtWidgets.QTreeView): + double_clicked = QtCore.Signal(QtCore.QModelIndex) + + def mouseDoubleClickEvent(self, event): + index = self.indexAt(event.pos()) + if index.isValid(): + self.double_clicked.emit(index) + event.accept() + + class AssetsDialog(QtWidgets.QDialog): """Dialog to select asset for a context of instance.""" @@ -178,7 +189,7 @@ class AssetsDialog(QtWidgets.QDialog): filter_input = PlaceholderLineEdit(self) filter_input.setPlaceholderText("Filter assets..") - asset_view = QtWidgets.QTreeView(self) + asset_view = AssetDialogView(self) asset_view.setModel(proxy_model) asset_view.setHeaderHidden(True) asset_view.setFrameShape(QtWidgets.QFrame.NoFrame) @@ -200,6 +211,7 @@ class AssetsDialog(QtWidgets.QDialog): layout.addWidget(asset_view, 1) layout.addLayout(btns_layout, 0) + asset_view.double_clicked.connect(self._on_ok_clicked) filter_input.textChanged.connect(self._on_filter_change) ok_btn.clicked.connect(self._on_ok_clicked) cancel_btn.clicked.connect(self._on_cancel_clicked) @@ -274,7 +286,7 @@ class AssetsDialog(QtWidgets.QDialog): index = self._asset_view.currentIndex() asset_name = None if index.isValid(): - asset_name = index.data(QtCore.Qt.DisplayRole) + asset_name = index.data(ASSET_NAME_ROLE) self._selected_asset = asset_name self.done(1) From d46ca7ed50b314fa3ae61e106cfd8297b96c630e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 10 Oct 2022 19:23:29 +0200 Subject: [PATCH 60/90] cache assets hierarchy and stringify object ids --- openpype/tools/publisher/control.py | 46 +++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 43721b9229..c0ffa942a4 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -60,15 +60,17 @@ class AssetDocsCache: def __init__(self, controller): self._controller = controller self._asset_docs = None - # TODO use asset ids instead + self._asset_docs_hierarchy = None self._task_names_by_asset_name = {} self._asset_docs_by_name = {} self._full_asset_docs_by_name = {} def reset(self): self._asset_docs = None + self._asset_docs_hierarchy = None self._task_names_by_asset_name = {} self._asset_docs_by_name = {} + self._full_asset_docs_by_name = {} def _query(self): if self._asset_docs is not None: @@ -81,8 +83,13 @@ class AssetDocsCache: asset_docs_by_name = {} task_names_by_asset_name = {} for asset_doc in asset_docs: + if "data" not in asset_doc: + asset_doc["data"] = {"tasks": {}, "visualParent": None} + elif "tasks" not in asset_doc["data"]: + asset_doc["data"]["tasks"] = {} + asset_name = asset_doc["name"] - asset_tasks = asset_doc.get("data", {}).get("tasks") or {} + asset_tasks = asset_doc["data"]["tasks"] task_names_by_asset_name[asset_name] = list(asset_tasks.keys()) asset_docs_by_name[asset_name] = asset_doc @@ -94,11 +101,38 @@ class AssetDocsCache: self._query() return copy.deepcopy(self._asset_docs) + def get_asset_hierarchy(self): + """Prepare asset documents into hierarchy. + + Convert ObjectId to string. Asset id is not used during whole + process of publisher but asset name is used rather. + + Returns: + Dict[Union[str, None]: Any]: Mapping of parent id to it's children. + Top level assets have parent id 'None'. + """ + + if self._asset_docs_hierarchy is None: + _queue = collections.deque(self.get_asset_docs()) + + output = collections.defaultdict(list) + while _queue: + asset_doc = _queue.popleft() + asset_doc["_id"] = str(asset_doc["_id"]) + parent_id = asset_doc["data"]["visualParent"] + if parent_id is not None: + parent_id = str(parent_id) + asset_doc["data"]["visualParent"] = parent_id + output[parent_id].append(asset_doc) + self._asset_docs_hierarchy = output + return copy.deepcopy(self._asset_docs_hierarchy) + def get_task_names_by_asset_name(self): self._query() return copy.deepcopy(self._task_names_by_asset_name) def get_asset_by_name(self, asset_name): + self._query() asset_doc = self._asset_docs_by_name.get(asset_name) if asset_doc is None: return None @@ -1588,14 +1622,8 @@ class PublisherController(BasePublishController): def get_asset_hierarchy(self): """Prepare asset documents into hierarchy.""" - _queue = collections.deque(self.get_asset_docs()) - output = collections.defaultdict(list) - while _queue: - asset_doc = _queue.popleft() - parent_id = asset_doc["data"]["visualParent"] - output[parent_id].append(asset_doc) - return output + return self._asset_docs_cache.get_asset_hierarchy() def get_task_names_by_asset_names(self, asset_names): """Prepare task names by asset name.""" From 5f1bfe2790e1864fca60be2895caa54333a7ca09 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 10 Oct 2022 19:24:01 +0200 Subject: [PATCH 61/90] use 'get_subset_name' on controller instead of calling directly creator --- openpype/tools/publisher/widgets/widgets.py | 28 +++++---------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 903ce70f01..c6c8ed3c7d 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1060,24 +1060,6 @@ class GlobalAttrsWidget(QtWidgets.QWidget): if self.task_value_widget.has_value_changed(): task_name = self.task_value_widget.get_selected_items()[0] - asset_docs_by_name = {} - asset_names = set() - if asset_name is None: - for instance in self._current_instances: - asset_names.add(instance.get("asset")) - else: - asset_names.add(asset_name) - - for asset_doc in self._controller.get_asset_docs(): - _asset_name = asset_doc["name"] - if _asset_name in asset_names: - asset_names.remove(_asset_name) - asset_docs_by_name[_asset_name] = asset_doc - - if not asset_names: - break - - project_name = self._controller.project_name subset_names = set() invalid_tasks = False for instance in self._current_instances: @@ -1093,11 +1075,13 @@ class GlobalAttrsWidget(QtWidgets.QWidget): if task_name is not None: new_task_name = task_name - asset_doc = asset_docs_by_name[new_asset_name] - try: - new_subset_name = instance.creator.get_subset_name( - new_variant_value, new_task_name, asset_doc, project_name + new_subset_name = self._controller.get_subset_name( + instance.creator_identifier, + new_variant_value, + new_task_name, + new_asset_name, + instance.id ) except TaskNotSetError: invalid_tasks = True From 2e86d0329357a938d540808db54c3831357fed1c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 11 Oct 2022 10:55:09 +0200 Subject: [PATCH 62/90] fix import of PublisherWindow and add ability to pass controller --- openpype/tools/utils/host_tools.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index 552ce0d432..eababfee32 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -269,25 +269,25 @@ class HostToolsHelper: dialog.activateWindow() dialog.showNormal() - def get_publisher_tool(self, parent): + def get_publisher_tool(self, parent=None, controller=None): """Create, cache and return publisher window.""" if self._publisher_tool is None: - from openpype.tools.publisher import PublisherWindow + from openpype.tools.publisher.window import PublisherWindow host = registered_host() ILoadHost.validate_load_methods(host) publisher_window = PublisherWindow( - parent=parent or self._parent + controller=controller, parent=parent or self._parent ) self._publisher_tool = publisher_window return self._publisher_tool - def show_publisher_tool(self, parent=None): + def show_publisher_tool(self, parent=None, controller=None): with qt_app_context(): - dialog = self.get_publisher_tool(parent) + dialog = self.get_publisher_tool(controller, parent) dialog.show() dialog.raise_() From 67f4112256d6ac3b6c5812f9a700e61e1d539c03 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 11 Oct 2022 14:06:51 +0200 Subject: [PATCH 63/90] removed duplicated topic from docstring --- openpype/tools/publisher/control.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index c0ffa942a4..11006dbc08 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1320,7 +1320,6 @@ class BasePublishController(AbstractPublisherController): "publish.has_validated.changed" - Attr 'publish_has_validated' changed. "publish.is_running.changed" - Attr 'publish_is_running' changed. - "publish.has_validated.changed" - Attr 'has_validated' changed. "publish.has_crashed.changed" - Attr 'publish_has_crashed' changed. "publish.publish_error.changed" - Attr 'publish_error' "publish.has_validation_errors.changed" - Attr From e883f8743b179f8fadcc29b9ce9ffb25d6e43060 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 11 Oct 2022 14:13:39 +0200 Subject: [PATCH 64/90] renamed 'BasePublishController' to 'BasePublisherController' --- 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 11006dbc08..05b0bb39be 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1251,7 +1251,7 @@ class AbstractPublisherController(object): pass -class BasePublishController(AbstractPublisherController): +class BasePublisherController(AbstractPublisherController): """Implement common logic for controllers. Implement event system, logger and common attributes. Attributes are @@ -1491,7 +1491,7 @@ class BasePublishController(AbstractPublisherController): return None -class PublisherController(BasePublishController): +class PublisherController(BasePublisherController): """Middleware between UI, CreateContext and publish Context. Handle both creation and publishing parts. From 75769804e974a4a33e83321b5e4eb0791d74281e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 11 Oct 2022 14:13:52 +0200 Subject: [PATCH 65/90] use 'BasePublisherController' for 'QtRemotePublishController' --- openpype/tools/publisher/control_qt.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/tools/publisher/control_qt.py b/openpype/tools/publisher/control_qt.py index 10f576a3f3..006303ec6c 100644 --- a/openpype/tools/publisher/control_qt.py +++ b/openpype/tools/publisher/control_qt.py @@ -6,7 +6,11 @@ from Qt import QtCore from openpype.lib.events import Event from openpype.pipeline.create import CreatedInstance -from .control import MainThreadItem, PublisherController +from .control import ( + MainThreadItem, + PublisherController, + BasePublisherController, +) class MainThreadProcess(QtCore.QObject): @@ -92,7 +96,7 @@ class QtPublisherController(PublisherController): self._main_thread_processor.stop() -class QtRemotePublishController(PublisherController): +class QtRemotePublishController(BasePublisherController): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) From 18ebea7eb8825607cd13ea7d6adb23b6d486ba85 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 11 Oct 2022 14:14:03 +0200 Subject: [PATCH 66/90] handle 'host_is_valid' attribute change --- openpype/tools/publisher/control_qt.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/tools/publisher/control_qt.py b/openpype/tools/publisher/control_qt.py index 006303ec6c..51aeec65d1 100644 --- a/openpype/tools/publisher/control_qt.py +++ b/openpype/tools/publisher/control_qt.py @@ -167,6 +167,10 @@ class QtRemotePublishController(BasePublisherController): self.publish_finished = event["value"] return + if event.topic == "publish.host_is_valid.changed": + self.host_is_valid = event["value"] + return + # Topics that can be just passed by because are not affecting # controller itself # - "show.card.message" @@ -174,6 +178,7 @@ class QtRemotePublishController(BasePublisherController): # - "publish.reset.finished" # - "instances.refresh.finished" # - "plugins.refresh.finished" + # - "controller.reset.finished" # - "publish.process.started" # - "publish.process.stopped" # - "publish.process.plugin.changed" From e0a222c75ea49c0e82d24120600137ddf3b5f3c1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 12 Oct 2022 12:13:20 +0200 Subject: [PATCH 67/90] modified remote qt controller --- openpype/tools/publisher/control_qt.py | 59 +++++++++++++------------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/openpype/tools/publisher/control_qt.py b/openpype/tools/publisher/control_qt.py index 51aeec65d1..edcbb0c9f0 100644 --- a/openpype/tools/publisher/control_qt.py +++ b/openpype/tools/publisher/control_qt.py @@ -187,7 +187,7 @@ class QtRemotePublishController(BasePublisherController): @abstractproperty def project_name(self): - """Current context project name. + """Current context project name from client. Returns: str: Name of project. @@ -197,7 +197,7 @@ class QtRemotePublishController(BasePublisherController): @abstractproperty def current_asset_name(self): - """Current context asset name. + """Current context asset name from client. Returns: Union[str, None]: Name of asset. @@ -207,7 +207,7 @@ class QtRemotePublishController(BasePublisherController): @abstractproperty def current_task_name(self): - """Current context task name. + """Current context task name from client. Returns: Union[str, None]: Name of task. @@ -215,19 +215,6 @@ class QtRemotePublishController(BasePublisherController): pass - @abstractproperty - def host_is_valid(self): - """Host is valid for creation part. - - Host must have implemented certain functionality to be able create - in Publisher tool. - - Returns: - bool: Host can handle creation of instances. - """ - - pass - @property def instances(self): """Collected/created instances. @@ -260,16 +247,6 @@ class QtRemotePublishController(BasePublisherController): def get_existing_subset_names(self, asset_name): pass - @abstractmethod - def reset(self): - """Reset whole controller. - - This should reset create context, publish context and all variables - that are related to it. - """ - - pass - @abstractmethod def get_subset_name( self, @@ -311,17 +288,26 @@ class QtRemotePublishController(BasePublisherController): pass - @abstractmethod - def save_changes(self): - """Save changes happened during creation.""" + def _get_instance_changes_for_client(self): + """Preimplemented method to receive instance changes for client.""" created_instance_changes = {} for instance_id, instance in self._created_instances.items(): created_instance_changes[instance_id] = ( instance.remote_changes() ) + return created_instance_changes - # Send 'created_instance_changes' value to client + @abstractmethod + def _send_instance_changes_to_client(self): + instance_changes = self._get_instance_changes_for_client() + # Implement to send 'instance_changes' value to client + + @abstractmethod + def save_changes(self): + """Save changes happened during creation.""" + + self._send_instance_changes_to_client() @abstractmethod def remove_instances(self, instance_ids): @@ -338,16 +324,29 @@ class QtRemotePublishController(BasePublisherController): def get_validation_errors(self): pass + @abstractmethod + def reset(self): + """Reset whole controller. + + This should reset create context, publish context and all variables + that are related to it. + """ + + self._send_instance_changes_to_client() + pass + @abstractmethod def publish(self): """Trigger publishing without any order limitations.""" + self._send_instance_changes_to_client() pass @abstractmethod def validate(self): """Trigger publishing which will stop after validation order.""" + self._send_instance_changes_to_client() pass @abstractmethod From 8c3ffcc5675561b0322edd1e83eac0d184456124 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 12 Oct 2022 12:21:57 +0200 Subject: [PATCH 68/90] added a docstring to remote controller --- openpype/tools/publisher/control_qt.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/openpype/tools/publisher/control_qt.py b/openpype/tools/publisher/control_qt.py index edcbb0c9f0..ddc2dfa3e4 100644 --- a/openpype/tools/publisher/control_qt.py +++ b/openpype/tools/publisher/control_qt.py @@ -97,12 +97,24 @@ class QtPublisherController(PublisherController): class QtRemotePublishController(BasePublisherController): + """Abstract Remote controller for Qt UI. + + This controller should be used in process where UI is running and should + listen and ask for data on a client side. + + All objects that are used during UI processing should be able to convert + on client side to json serializable data and then recreated here. Keep in + mind that all changes made here should be send back to client controller + before critical actions. + + ATM Was not tested and will require some changes. All code written here is + based on theoretical idea how it could work. + """ + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._created_instances = {} - self._main_thread_processor = MainThreadProcess() - self._main_thread_processor.start() @abstractmethod def _get_serialized_instances(self): @@ -114,9 +126,6 @@ class QtRemotePublishController(BasePublisherController): pass - def _process_main_thread_item(self, item): - self._main_thread_processor.add_item(item) - def _on_create_instance_change(self): serialized_instances = self._get_serialized_instances() From df4f3d45aa6c48ee209845b2a35a773b189455e0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 12 Oct 2022 14:44:54 +0200 Subject: [PATCH 69/90] fix instances access in 'get_subset_name' --- openpype/tools/publisher/control.py | 5 ++++- openpype/tools/publisher/widgets/widgets.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 05b0bb39be..699b8843cc 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1820,9 +1820,12 @@ class PublisherController(BasePublisherController): creator = self._creators[creator_identifier] project_name = self.project_name asset_doc = self._asset_docs_cache.get_full_asset_by_name(asset_name) + instance = None + if instance_id: + instance = self.instances[instance_id] return creator.get_subset_name( - variant, task_name, asset_doc, project_name + variant, task_name, asset_doc, project_name, instance=instance ) def create( diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index edd9d55c75..536650e209 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1082,8 +1082,8 @@ class GlobalAttrsWidget(QtWidgets.QWidget): new_task_name, new_asset_name, instance.id, - instance=instance ) + except TaskNotSetError: invalid_tasks = True instance.set_task_invalid(True) From 64f9d98c53f746a180e54e3022abfe734cd78f97 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 12 Oct 2022 14:46:11 +0200 Subject: [PATCH 70/90] hound fix --- openpype/tools/publisher/control.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 699b8843cc..da320b1f39 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -652,18 +652,22 @@ class PublishValidationErrorsReport: Dict[str, Any]: Serialized data. """ + error_items = [ + item.to_data() + for item in self._error_items + ] + + plugin_action_items = { + plugin_id: [ + action_item.to_data() + for action_item in action_items + ] + for plugin_id, action_items in self._plugin_action_items.items() + } + return { - "error_items": [ - item.to_data() - for item in self._error_items - ], - "plugin_action_items": { - plugin_id: [ - action_item.to_data() - for action_item in action_items - ] - for plugin_id, action_items in self._plugin_action_items.items() - } + "error_items": error_items, + "plugin_action_items": plugin_action_items } @classmethod From 9955ffe95c90fe181740aa81fd21015a0b99caba Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 14 Oct 2022 10:12:30 +0200 Subject: [PATCH 71/90] fix validation errors access --- .../tools/publisher/widgets/publish_frame.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/openpype/tools/publisher/widgets/publish_frame.py b/openpype/tools/publisher/widgets/publish_frame.py index 0a04b2a665..c5685461a7 100644 --- a/openpype/tools/publisher/widgets/publish_frame.py +++ b/openpype/tools/publisher/widgets/publish_frame.py @@ -384,12 +384,10 @@ class PublishFrame(QtWidgets.QWidget): self._validate_btn.setEnabled(validate_enabled) self._publish_btn.setEnabled(publish_enabled) - error_msg = self._controller.publish_error_msg - validation_errors = self._controller.get_validation_errors() - if error_msg: - self._set_error_msg(error_msg) + if self._controller.publish_has_crashed: + self._set_error_msg() - elif validation_errors: + elif self._controller.publish_has_validation_errors: self._set_progress_visibility(False) self._set_validation_errors() @@ -411,16 +409,12 @@ class PublishFrame(QtWidgets.QWidget): self._set_success_property(-1) - def _set_error_msg(self, error_msg): - """Show error message to artist. - - Args: - error_msg (str): Message which is showed to artist. - """ + def _set_error_msg(self): + """Show error message to artist on publish crash.""" self._set_main_label("Error happened") - self._message_label_top.setText(error_msg) + self._message_label_top.setText(self._controller.publish_error_msg) self._set_success_property(0) From 45536f613d6a9414830cf0d8dff99296b82c570a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Fri, 14 Oct 2022 16:28:48 +0200 Subject: [PATCH 72/90] :sparkles: add originalBasename data to Tray Publisher --- .../traypublisher/plugins/publish/collect_simple_instances.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py b/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py index c0ae694c3c..0ccef3f375 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py @@ -1,5 +1,6 @@ import os import tempfile +from pathlib import Path import clique import pyblish.api @@ -72,6 +73,8 @@ class CollectSettingsSimpleInstances(pyblish.api.InstancePlugin): instance.data["source"] = source instance.data["sourceFilepaths"] = list(set(source_filepaths)) + instance.data["originalBasename"] = Path( + instance.data["sourceFilepaths"][0]).stem self.log.debug( ( From 1b8dff405a6737c45e62617da2fba8ea4604b308 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 15 Oct 2022 18:31:03 +0200 Subject: [PATCH 73/90] add process time to publish report --- openpype/tools/publisher/control.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index da320b1f39..9eff431171 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -3,6 +3,7 @@ import copy import logging import traceback import collections +import time from abc import ABCMeta, abstractmethod, abstractproperty import six @@ -232,15 +233,17 @@ class PublishReport: """Set that current plugin has been skipped.""" self._current_plugin_data["skipped"] = True - def add_result(self, result): + def add_result(self, result, process_time): """Handle result of one plugin and it's instance.""" + instance = result["instance"] instance_id = None if instance is not None: instance_id = instance.id self._current_plugin_data["instances_data"].append({ "id": instance_id, - "logs": self._extract_instance_log_items(result) + "logs": self._extract_instance_log_items(result), + "process_time": process_time }) def add_action_result(self, action, result): @@ -2100,9 +2103,11 @@ class PublisherController(BasePublisherController): ) def _process_and_continue(self, plugin, instance): + start = time.time() result = pyblish.plugin.process( plugin, self._publish_context, instance ) + process_time = time.time() - start self._publish_report.add_result(result) From 2787dbd83a1d621ddbcf0372d36fad825acf87d9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 15 Oct 2022 18:32:09 +0200 Subject: [PATCH 74/90] add report version to report data --- 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 9eff431171..17db324a68 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -4,6 +4,7 @@ import logging import traceback import collections import time +import uuid from abc import ABCMeta, abstractmethod, abstractproperty import six @@ -293,7 +294,9 @@ class PublishReport: "plugins_data": plugins_data, "instances": instances_details, "context": self._extract_context_data(self._current_context), - "crashed_file_paths": crashed_file_paths + "crashed_file_paths": crashed_file_paths, + "id": str(uuid.uuid4()), + "report_version": "1.0.0" } def _extract_context_data(self, context): From d3e5041379291c899ddbe5a14082915176c5776a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 15 Oct 2022 20:39:02 +0200 Subject: [PATCH 75/90] fix not passed argument --- openpype/tools/publisher/control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 17db324a68..b415644a43 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -2112,7 +2112,7 @@ class PublisherController(BasePublisherController): ) process_time = time.time() - start - self._publish_report.add_result(result) + self._publish_report.add_result(result, process_time) exception = result.get("error") if exception: From a877176b39836b0d048bb0dc235225c6949008b5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 Oct 2022 00:26:08 +0200 Subject: [PATCH 76/90] don't crash in collection when files are not filled --- .../plugins/publish/collect_simple_instances.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py b/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py index d91694ef69..7035a61d7b 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py @@ -70,11 +70,17 @@ class CollectSettingsSimpleInstances(pyblish.api.InstancePlugin): repre_names, representation_files_mapping ) - + source_filepaths = list(set(source_filepaths)) instance.data["source"] = source - instance.data["sourceFilepaths"] = list(set(source_filepaths)) - instance.data["originalBasename"] = Path( - instance.data["sourceFilepaths"][0]).stem + instance.data["sourceFilepaths"] = source_filepaths + + # NOTE: Missing filepaths should not cause crashes (at least not here) + # - if filepaths are required they should crash on validation + if source_filepaths: + # NOTE: Original basename is not handling sequences + # - we should maybe not fill the key when sequence is used? + origin_basename = Path(source_filepaths[0]).stem + instance.data["originalBasename"] = origin_basename self.log.debug( ( From 0df15975b16aabac45a61f2f025956180537c2b2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 Oct 2022 00:34:02 +0200 Subject: [PATCH 77/90] fix unwanted zooming if control was released in different widget --- .../publisher/publish_report_viewer/widgets.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/openpype/tools/publisher/publish_report_viewer/widgets.py b/openpype/tools/publisher/publish_report_viewer/widgets.py index dc82448495..4770bdcc65 100644 --- a/openpype/tools/publisher/publish_report_viewer/widgets.py +++ b/openpype/tools/publisher/publish_report_viewer/widgets.py @@ -148,12 +148,12 @@ class ZoomPlainText(QtWidgets.QPlainTextEdit): anim_timer.timeout.connect(self._scaling_callback) self._anim_timer = anim_timer - self._zoom_enabled = False self._scheduled_scalings = 0 self._point_size = None def wheelEvent(self, event): - if not self._zoom_enabled: + modifiers = QtWidgets.QApplication.keyboardModifiers() + if modifiers != QtCore.Qt.ControlModifier: super(ZoomPlainText, self).wheelEvent(event) return @@ -189,16 +189,6 @@ class ZoomPlainText(QtWidgets.QPlainTextEdit): else: self._scheduled_scalings += 1 - def keyPressEvent(self, event): - if event.key() == QtCore.Qt.Key_Control: - self._zoom_enabled = True - super(ZoomPlainText, self).keyPressEvent(event) - - def keyReleaseEvent(self, event): - if event.key() == QtCore.Qt.Key_Control: - self._zoom_enabled = False - super(ZoomPlainText, self).keyReleaseEvent(event) - class DetailsWidget(QtWidgets.QWidget): def __init__(self, parent): From 3afee7370a0c44baa4c611250be6c9c176c6dd82 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 Oct 2022 00:34:16 +0200 Subject: [PATCH 78/90] define min/max of text sizes --- .../publish_report_viewer/widgets.py | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/openpype/tools/publisher/publish_report_viewer/widgets.py b/openpype/tools/publisher/publish_report_viewer/widgets.py index 4770bdcc65..ff388fb277 100644 --- a/openpype/tools/publisher/publish_report_viewer/widgets.py +++ b/openpype/tools/publisher/publish_report_viewer/widgets.py @@ -139,6 +139,9 @@ class PluginLoadReportWidget(QtWidgets.QWidget): class ZoomPlainText(QtWidgets.QPlainTextEdit): + min_point_size = 1.0 + max_point_size = 200.0 + def __init__(self, *args, **kwargs): super(ZoomPlainText, self).__init__(*args, **kwargs) @@ -172,19 +175,36 @@ class ZoomPlainText(QtWidgets.QPlainTextEdit): factor = 1.0 + (self._scheduled_scalings / 300) font = self.font() + if self._point_size is None: - self._point_size = font.pointSizeF() + point_size = font.pointSizeF() + else: + point_size = self._point_size - self._point_size *= factor - if self._point_size < 1: - self._point_size = 1.0 + point_size *= factor + min_hit = False + max_hit = False + if point_size < self.min_point_size: + point_size = self.min_point_size + min_hit = True + elif point_size > self.max_point_size: + point_size = self.max_point_size + max_hit = True - font.setPointSizeF(self._point_size) + self._point_size = point_size + + font.setPointSizeF(point_size) # Using 'self.setFont(font)' would not be propagated when stylesheets # are applied on this widget self.setStyleSheet("font-size: {}pt".format(font.pointSize())) - if self._scheduled_scalings > 0: + if ( + (max_hit and self._scheduled_scalings > 0) + or (min_hit and self._scheduled_scalings < 0) + ): + self._scheduled_scalings = 0 + + elif self._scheduled_scalings > 0: self._scheduled_scalings -= 1 else: self._scheduled_scalings += 1 From 890b77214acd24482501ce19722f6701f6dac537 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 17 Oct 2022 10:49:39 +0200 Subject: [PATCH 79/90] import lib content from lib directly --- openpype/hosts/flame/api/workio.py | 2 +- openpype/hosts/flame/hooks/pre_flame_setup.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/flame/api/workio.py b/openpype/hosts/flame/api/workio.py index 0c96c0752a..e49321c75a 100644 --- a/openpype/hosts/flame/api/workio.py +++ b/openpype/hosts/flame/api/workio.py @@ -1,7 +1,7 @@ """Host API required Work Files tool""" import os -from openpype.api import Logger +from openpype.lib import Logger # from .. import ( # get_project_manager, # get_current_project diff --git a/openpype/hosts/flame/hooks/pre_flame_setup.py b/openpype/hosts/flame/hooks/pre_flame_setup.py index f0fdaa86ba..713daf1031 100644 --- a/openpype/hosts/flame/hooks/pre_flame_setup.py +++ b/openpype/hosts/flame/hooks/pre_flame_setup.py @@ -3,16 +3,17 @@ import json import tempfile import contextlib import socket +from pprint import pformat + from openpype.lib import ( PreLaunchHook, - get_openpype_username + get_openpype_username, + run_subprocess, ) from openpype.lib.applications import ( ApplicationLaunchFailed ) from openpype.hosts import flame as opflame -import openpype -from pprint import pformat class FlamePrelaunch(PreLaunchHook): @@ -127,7 +128,6 @@ class FlamePrelaunch(PreLaunchHook): except OSError as exc: self.log.warning("Not able to open files: {}".format(exc)) - def _get_flame_fps(self, fps_num): fps_table = { float(23.976): "23.976 fps", @@ -179,7 +179,7 @@ class FlamePrelaunch(PreLaunchHook): "env": self.launch_context.env } - openpype.api.run_subprocess(args, **process_kwargs) + run_subprocess(args, **process_kwargs) # process returned json file to pass launch args return_json_data = open(tmp_json_path).read() From 1ecc673c6ccf3abeb6acdf2529a617daf447a51e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 17 Oct 2022 17:15:51 +0200 Subject: [PATCH 80/90] error message fix --- openpype/pipeline/create/context.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index a35541f339..4ec6d7bdad 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1096,7 +1096,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 From 495b5479140af18f5bfbd8342b2ba20132dc9888 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 17 Oct 2022 17:21:29 +0200 Subject: [PATCH 81/90] fix args order --- openpype/tools/utils/host_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index eababfee32..046dcbdf6a 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -287,7 +287,7 @@ class HostToolsHelper: def show_publisher_tool(self, parent=None, controller=None): with qt_app_context(): - dialog = self.get_publisher_tool(controller, parent) + dialog = self.get_publisher_tool(parent, controller) dialog.show() dialog.raise_() From 609f9f12851dfc775edd04344a2c9aa1eaba8426 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 17 Oct 2022 17:44:08 +0200 Subject: [PATCH 82/90] fix attribute access --- openpype/tools/publisher/control.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index b415644a43..911d464f80 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1563,10 +1563,10 @@ class PublisherController(BasePublisherController): str: Project name. """ - if not hasattr(self.host, "get_current_context"): + if not hasattr(self._host, "get_current_context"): return legacy_io.active_project() - return self.host.get_current_context()["project_name"] + return self._host.get_current_context()["project_name"] @property def current_asset_name(self): @@ -1576,10 +1576,10 @@ class PublisherController(BasePublisherController): Union[str, None]: Asset name or None if asset is not set. """ - if not hasattr(self.host, "get_current_context"): + if not hasattr(self._host, "get_current_context"): return legacy_io.Session["AVALON_ASSET"] - return self.host.get_current_context()["asset_name"] + return self._host.get_current_context()["asset_name"] @property def current_task_name(self): @@ -1589,10 +1589,10 @@ class PublisherController(BasePublisherController): Union[str, None]: Task name or None if task is not set. """ - if not hasattr(self.host, "get_current_context"): + if not hasattr(self._host, "get_current_context"): return legacy_io.Session["AVALON_TASK"] - return self.host.get_current_context()["task_name"] + return self._host.get_current_context()["task_name"] @property def instances(self): From 27d4f1fc70684fe2abb9f70ccbe04e9e7cae42fb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 17 Oct 2022 18:47:58 +0200 Subject: [PATCH 83/90] reuse duration from pyblish result instead of calculating own --- openpype/tools/publisher/control.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 911d464f80..c8f38cb080 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -234,7 +234,7 @@ class PublishReport: """Set that current plugin has been skipped.""" self._current_plugin_data["skipped"] = True - def add_result(self, result, process_time): + def add_result(self, result): """Handle result of one plugin and it's instance.""" instance = result["instance"] @@ -244,7 +244,7 @@ class PublishReport: self._current_plugin_data["instances_data"].append({ "id": instance_id, "logs": self._extract_instance_log_items(result), - "process_time": process_time + "process_time": result["duration"] }) def add_action_result(self, action, result): @@ -2106,13 +2106,11 @@ class PublisherController(BasePublisherController): ) def _process_and_continue(self, plugin, instance): - start = time.time() result = pyblish.plugin.process( plugin, self._publish_context, instance ) - process_time = time.time() - start - self._publish_report.add_result(result, process_time) + self._publish_report.add_result(result) exception = result.get("error") if exception: From b4d6fa3a3af7874d05a48cbeb1ab1010af7d7d52 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 17 Oct 2022 18:54:27 +0200 Subject: [PATCH 84/90] removed unused import --- openpype/tools/publisher/control.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index c8f38cb080..13c1044201 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -3,7 +3,6 @@ import copy import logging import traceback import collections -import time import uuid from abc import ABCMeta, abstractmethod, abstractproperty From 3fd2a8826c183fbc7f549f1aa573fca90e47ecbb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 Oct 2022 23:54:00 +0200 Subject: [PATCH 85/90] fix wrong attribute name --- openpype/tools/publisher/control.py | 14 +++++++------- openpype/tools/publisher/control_qt.py | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 13c1044201..a340f8c1d2 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1289,7 +1289,7 @@ class BasePublisherController(AbstractPublisherController): self._publish_has_validation_errors = False self._publish_has_crashed = False # All publish plugins are processed - self._publish_finished = False + self._publish_has_finished = False self._publish_max_progress = 0 self._publish_progress = 0 @@ -1337,7 +1337,7 @@ class BasePublisherController(AbstractPublisherController): changed. "publish.progress.changed" - Attr 'publish_progress' changed. "publish.host_is_valid.changed" - Attr 'host_is_valid' changed. - "publish.finished.changed" - Attr 'publish_finished' changed. + "publish.finished.changed" - Attr 'publish_has_finished' changed. Returns: EventSystem: Event system which can trigger callbacks for topics. @@ -1361,11 +1361,11 @@ class BasePublisherController(AbstractPublisherController): self._emit_event("publish.host_is_valid.changed", {"value": value}) def _get_publish_has_finished(self): - return self._publish_finished + return self._publish_has_finished def _set_publish_has_finished(self, value): - if self._publish_finished != value: - self._publish_finished = value + if self._publish_has_finished != value: + self._publish_has_finished = value self._emit_event("publish.finished.changed", {"value": value}) def _get_publish_is_running(self): @@ -1465,7 +1465,7 @@ class BasePublisherController(AbstractPublisherController): self.publish_has_validated = False self.publish_has_crashed = False self.publish_has_validation_errors = False - self.publish_finished = False + self.publish_has_finished = False self.publish_error_msg = None self.publish_progress = 0 @@ -2092,7 +2092,7 @@ class PublisherController(BasePublisherController): self._publish_report.set_plugin_skipped() # Cleanup of publishing process - self.publish_finished = True + self.publish_has_finished = True self.publish_progress = self.publish_max_progress yield MainThreadItem(self.stop_publish) diff --git a/openpype/tools/publisher/control_qt.py b/openpype/tools/publisher/control_qt.py index ddc2dfa3e4..56132a4046 100644 --- a/openpype/tools/publisher/control_qt.py +++ b/openpype/tools/publisher/control_qt.py @@ -173,7 +173,7 @@ class QtRemotePublishController(BasePublisherController): return if event.topic == "publish.finished.changed": - self.publish_finished = event["value"] + self.publish_has_finished = event["value"] return if event.topic == "publish.host_is_valid.changed": From 7190c0785cebc78b3b68dab21923e19e958c23fe Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 Oct 2022 00:05:34 +0200 Subject: [PATCH 86/90] go to report on publish stop if on publish tab --- 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 a0d1ac68fb..1424a3eccd 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -499,6 +499,9 @@ class PublisherWindow(QtWidgets.QDialog): publish_has_crashed = self._controller.publish_has_crashed validate_enabled = not publish_has_crashed publish_enabled = not publish_has_crashed + if self._tabs_widget.is_current_tab("publish"): + self._go_to_report_tab() + if validate_enabled: validate_enabled = not self._controller.publish_has_validated if publish_enabled: @@ -507,8 +510,6 @@ class PublisherWindow(QtWidgets.QDialog): and self._controller.publish_has_validation_errors ): publish_enabled = False - if self._tabs_widget.is_current_tab("publish"): - self._go_to_report_tab() else: publish_enabled = not self._controller.publish_has_finished From c9e10f6147356c618aaeb30251a39f428ae88ad5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 Oct 2022 00:14:35 +0200 Subject: [PATCH 87/90] change progress bar on validation error --- openpype/style/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index 4d13dc7c89..b466bd0820 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -1086,7 +1086,7 @@ ValidationArtistMessage QLabel { border-color: {color:publisher:error}; } -#PublishProgressBar[state="0"]::chunk { +#PublishProgressBar[state="0"]::chunk, #PublishProgressBar[state="2"]::chunk { background: {color:bg-buttons}; } From 9e37f3448e2f266dc9fe019e303ecca6864bdb76 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 Oct 2022 00:14:46 +0200 Subject: [PATCH 88/90] change page to publish on reset --- openpype/tools/publisher/window.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 1424a3eccd..39075d2489 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -470,6 +470,11 @@ class PublisherWindow(QtWidgets.QDialog): self._set_publish_visibility(False) self._set_footer_enabled(False) self._update_publish_details_widget() + if ( + not self._tabs_widget.is_current_tab("create") + or not self._tabs_widget.is_current_tab("publish") + ): + self._tabs_widget.set_current_tab("publish") def _on_publish_start(self): self._create_tab.setEnabled(False) From 7f533390712c308e5eaa2c8c73c7ad2cb3bdc20a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 Oct 2022 00:46:01 +0200 Subject: [PATCH 89/90] change progress bar colors on pause --- openpype/style/style.css | 15 +++++++-------- .../tools/publisher/widgets/publish_frame.py | 19 +++++++++++++++---- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index b466bd0820..a6818a5792 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -973,23 +973,22 @@ VariantInputsWidget QToolButton { background: {color:bg}; border-radius: 0.3em; } - -#PublishInfoFrame[state="-1"] { - background: rgb(194, 226, 236); -} - #PublishInfoFrame[state="0"] { - background: {color:publisher:crash}; + background: {color:publisher:success}; } #PublishInfoFrame[state="1"] { - background: {color:publisher:success}; + background: {color:publisher:crash}; } #PublishInfoFrame[state="2"] { background: {color:publisher:warning}; } +#PublishInfoFrame[state="3"], #PublishInfoFrame[state="4"] { + background: rgb(194, 226, 236); +} + #PublishInfoFrame QLabel { color: black; font-style: bold; @@ -1086,7 +1085,7 @@ ValidationArtistMessage QLabel { border-color: {color:publisher:error}; } -#PublishProgressBar[state="0"]::chunk, #PublishProgressBar[state="2"]::chunk { +#PublishProgressBar[state="1"]::chunk, #PublishProgressBar[state="4"]::chunk { background: {color:bg-buttons}; } diff --git a/openpype/tools/publisher/widgets/publish_frame.py b/openpype/tools/publisher/widgets/publish_frame.py index c5685461a7..e6333a104f 100644 --- a/openpype/tools/publisher/widgets/publish_frame.py +++ b/openpype/tools/publisher/widgets/publish_frame.py @@ -328,7 +328,7 @@ class PublishFrame(QtWidgets.QWidget): if self._last_instance_label: self._instance_label.setText(self._last_instance_label) - self._set_success_property(-1) + self._set_success_property(3) self._set_progress_visibility(True) self._set_main_label("Publishing...") @@ -407,7 +407,7 @@ class PublishFrame(QtWidgets.QWidget): "Hit publish (play button) to continue." ) - self._set_success_property(-1) + self._set_success_property(4) def _set_error_msg(self): """Show error message to artist on publish crash.""" @@ -416,7 +416,7 @@ class PublishFrame(QtWidgets.QWidget): self._message_label_top.setText(self._controller.publish_error_msg) - self._set_success_property(0) + self._set_success_property(1) def _set_validation_errors(self): self._set_main_label("Your publish didn't pass studio validations") @@ -426,7 +426,7 @@ class PublishFrame(QtWidgets.QWidget): def _set_finished(self): self._set_main_label("Finished") self._message_label_top.setText("") - self._set_success_property(1) + self._set_success_property(0) def _set_progress_visibility(self, visible): window_height = self.height() @@ -447,6 +447,17 @@ class PublishFrame(QtWidgets.QWidget): self.move(window_pos.x(), window_pos_y) def _set_success_property(self, state=None): + """Apply styles by state. + + State enum: + - None - Default state after restart + - 0 - Success finish + - 1 - Error happened + - 2 - Validation error + - 3 - In progress + - 4 - Stopped/Paused + """ + if state is None: state = "" else: From 4641cb5bae28620e53fec1f2b75dd673b33de55c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 18 Oct 2022 14:03:05 +0200 Subject: [PATCH 90/90] added backrwards compatibility for PyQt4 --- openpype/tools/publisher/widgets/help_widget.py | 4 +++- openpype/tools/publisher/widgets/validations_widget.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/tools/publisher/widgets/help_widget.py b/openpype/tools/publisher/widgets/help_widget.py index 7da07b1e78..0090111889 100644 --- a/openpype/tools/publisher/widgets/help_widget.py +++ b/openpype/tools/publisher/widgets/help_widget.py @@ -44,8 +44,10 @@ class HelpWidget(QtWidgets.QWidget): if commonmark: html = commonmark.commonmark(text) self._detail_description_input.setHtml(html) - else: + elif hasattr(self._detail_description_input, "setMarkdown"): self._detail_description_input.setMarkdown(text) + else: + self._detail_description_input.setText(text) class HelpDialog(QtWidgets.QDialog): diff --git a/openpype/tools/publisher/widgets/validations_widget.py b/openpype/tools/publisher/widgets/validations_widget.py index 772a561504..8c483e8088 100644 --- a/openpype/tools/publisher/widgets/validations_widget.py +++ b/openpype/tools/publisher/widgets/validations_widget.py @@ -709,5 +709,7 @@ class ValidationsWidget(QtWidgets.QFrame): if commonmark: html = commonmark.commonmark(description) self._error_details_input.setHtml(html) - else: + elif hasattr(self._error_details_input, "setMarkdown"): self._error_details_input.setMarkdown(description) + else: + self._error_details_input.setText(description)