From 5558c1eb46c8e2045c26c19ae4afcf8a47520eba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Albert?= Date: Fri, 9 Sep 2022 12:07:56 +0200 Subject: [PATCH 001/111] Kitsu : add launcher action --- .../kitsu/actions/launcher_show_in_kitsu.py | 131 ++++++++++++++++++ openpype/modules/kitsu/kitsu_module.py | 5 +- 2 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 openpype/modules/kitsu/actions/launcher_show_in_kitsu.py diff --git a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py new file mode 100644 index 0000000000..0ac9c6e9b7 --- /dev/null +++ b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py @@ -0,0 +1,131 @@ +import webbrowser + +from openpype.pipeline import LauncherAction +from openpype.modules import ModulesManager +from openpype.client import get_project, get_asset_by_name + + +class ShowInKitsu(LauncherAction): + name = "showinkitsu" + label = "Show in Kitsu" + icon = "external-link-square" + color = "#e0e1e1" + order = 10 + + @staticmethod + def get_kitsu_module(): + return ModulesManager().modules_by_name.get("kitsu") + + def is_compatible(self, session): + if not session.get("AVALON_PROJECT"): + return False + + return True + + def process(self, session, **kwargs): + + # Context inputs + project_name = session["AVALON_PROJECT"] + asset_name = session.get("AVALON_ASSET", None) + task_name = session.get("AVALON_TASK", None) + + project = get_project(project_name=project_name, + fields=["data.zou_id"]) + if not project: + raise RuntimeError(f"Project {project_name} not found.") + + project_zou_id = project["data"].get("zou_id") + if not project_zou_id: + raise RuntimeError(f"Project {project_name} has no " + f"connected ftrack id.") + + asset_zou_data = None + task_zou_id = None + asset_zou_name = None + asset_zou_id = None + asset_zou_type = 'Assets' + zou_sub_type = ['AssetType','Sequence'] + if asset_name: + asset_zou_name = asset_name + asset_fields = ["data.zou.id", "data.zou.type"] + if task_name: + asset_fields.append(f"data.tasks.{task_name}.zou.id") + + asset = get_asset_by_name(project_name, + asset_name=asset_name, + fields=asset_fields) + + asset_zou_data = asset["data"].get("zou") + + if asset_zou_data: + asset_zou_type = asset_zou_data["type"] + if not asset_zou_type in zou_sub_type: + asset_zou_id = asset_zou_data["id"] + else: + asset_zou_type = asset_name + + + if task_name: + task_data = asset["data"]["tasks"][task_name] + task_zou_data = task_data.get("zou", {}) + if not task_zou_data: + self.log.debug(f"No zou task data for task: {task_name}") + task_zou_id = task_zou_data["id"] + + # Define URL + url = self.get_url(project_id=project_zou_id, + asset_name=asset_zou_name, + asset_id=asset_zou_id, + asset_type=asset_zou_type, + task_id=task_zou_id) + + # Open URL in webbrowser + self.log.info(f"Opening URL: {url}") + webbrowser.open(url, + # Try in new tab + new=2) + + def get_url(self, + project_id, + asset_name=None, + asset_id=None, + asset_type=None, + task_id=None): + + shots_url = ['Shots','Sequence','Shot'] + sub_type = ['AssetType','Sequence'] + kitsu_module = self.get_kitsu_module() + + # Get kitsu url with /api stripped + kitsu_url = kitsu_module.server_url + if kitsu_url.endswith("/api"): + kitsu_url = kitsu_url[:-len("/api")] + + sub_url = f"/productions/{project_id}" + asset_type_url = "Assets" + + # Add redirection url for shots_url list + if asset_type in shots_url: + asset_type_url = 'Shots' + + if task_id: + # Go to task page + # /productions/{project-id}/{asset_type}/tasks/{task_id} + sub_url += f"/{asset_type_url}/tasks/{task_id}" + + elif asset_id: + # Go to asset or shot page + # /productions/{project-id}/assets/{entity_id} + # /productions/{project-id}/shots/{entity_id} + sub_url += f"/{asset_type_url}/{asset_id}" + + else: + # Go to project page + # Project page must end with a view + # /productions/{project-id}/assets/ + # Add search method if is a sub_type + sub_url += f"/{asset_type_url}" + if asset_type in sub_type: + sub_url += f'?search={asset_name}' + + return f"{kitsu_url}{sub_url}" diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index d19d14dda7..23c032715b 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -89,7 +89,10 @@ class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction): """Implementation of abstract method for `IPluginPaths`.""" current_dir = os.path.dirname(os.path.abspath(__file__)) - return {"publish": [os.path.join(current_dir, "plugins", "publish")]} + return { + "publish": [os.path.join(current_dir, "plugins", "publish")], + "actions": [os.path.join(current_dir, "actions")] + } def cli(self, click_group): click_group.add_command(cli_main) From 4b7ecac2bbc5fc0f33839c3b0efa0cf2ff304e4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Albert?= Date: Wed, 14 Sep 2022 12:31:13 +0200 Subject: [PATCH 002/111] Fix Format Document --- .../modules/kitsu/actions/launcher_show_in_kitsu.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py index 0ac9c6e9b7..bca57ce4c6 100644 --- a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py +++ b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py @@ -44,7 +44,7 @@ class ShowInKitsu(LauncherAction): asset_zou_name = None asset_zou_id = None asset_zou_type = 'Assets' - zou_sub_type = ['AssetType','Sequence'] + zou_sub_type = ['AssetType', 'Sequence'] if asset_name: asset_zou_name = asset_name asset_fields = ["data.zou.id", "data.zou.type"] @@ -59,11 +59,10 @@ class ShowInKitsu(LauncherAction): if asset_zou_data: asset_zou_type = asset_zou_data["type"] - if not asset_zou_type in zou_sub_type: + if asset_zou_type not in zou_sub_type: asset_zou_id = asset_zou_data["id"] else: asset_zou_type = asset_name - if task_name: task_data = asset["data"]["tasks"][task_name] @@ -71,7 +70,7 @@ class ShowInKitsu(LauncherAction): if not task_zou_data: self.log.debug(f"No zou task data for task: {task_name}") task_zou_id = task_zou_data["id"] - + # Define URL url = self.get_url(project_id=project_zou_id, asset_name=asset_zou_name, @@ -92,8 +91,8 @@ class ShowInKitsu(LauncherAction): asset_type=None, task_id=None): - shots_url = ['Shots','Sequence','Shot'] - sub_type = ['AssetType','Sequence'] + shots_url = ['Shots', 'Sequence', 'Shot'] + sub_type = ['AssetType', 'Sequence'] kitsu_module = self.get_kitsu_module() # Get kitsu url with /api stripped @@ -103,7 +102,7 @@ class ShowInKitsu(LauncherAction): sub_url = f"/productions/{project_id}" asset_type_url = "Assets" - + # Add redirection url for shots_url list if asset_type in shots_url: asset_type_url = 'Shots' From 2fb40a9db7c3fde1c1842958c597c07693bb17e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Albert?= <89812691+sebasti1a@users.noreply.github.com> Date: Wed, 14 Sep 2022 14:49:44 +0200 Subject: [PATCH 003/111] Update openpype/modules/kitsu/actions/launcher_show_in_kitsu.py Co-authored-by: Roy Nieterau --- openpype/modules/kitsu/actions/launcher_show_in_kitsu.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py index bca57ce4c6..0ee95e773d 100644 --- a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py +++ b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py @@ -101,11 +101,7 @@ class ShowInKitsu(LauncherAction): kitsu_url = kitsu_url[:-len("/api")] sub_url = f"/productions/{project_id}" - asset_type_url = "Assets" - - # Add redirection url for shots_url list - if asset_type in shots_url: - asset_type_url = 'Shots' + asset_type_url = "Shots" if asset_type in shots_url else "Assets" if task_id: # Go to task page From 9b34573361ef292fcb68e692efc74268ac165ca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Albert?= <89812691+sebasti1a@users.noreply.github.com> Date: Wed, 14 Sep 2022 14:50:29 +0200 Subject: [PATCH 004/111] Update openpype/modules/kitsu/actions/launcher_show_in_kitsu.py Co-authored-by: Roy Nieterau --- openpype/modules/kitsu/actions/launcher_show_in_kitsu.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py index 0ee95e773d..68da3e3a0e 100644 --- a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py +++ b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py @@ -39,7 +39,6 @@ class ShowInKitsu(LauncherAction): raise RuntimeError(f"Project {project_name} has no " f"connected ftrack id.") - asset_zou_data = None task_zou_id = None asset_zou_name = None asset_zou_id = None From 40e3dbb38fe91cdc07814707b56f2550138c7eac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Albert?= <89812691+sebasti1a@users.noreply.github.com> Date: Wed, 14 Sep 2022 14:50:42 +0200 Subject: [PATCH 005/111] Update openpype/modules/kitsu/actions/launcher_show_in_kitsu.py Co-authored-by: Roy Nieterau --- openpype/modules/kitsu/actions/launcher_show_in_kitsu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py index 68da3e3a0e..575d1eecd0 100644 --- a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py +++ b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py @@ -39,10 +39,10 @@ class ShowInKitsu(LauncherAction): raise RuntimeError(f"Project {project_name} has no " f"connected ftrack id.") - task_zou_id = None asset_zou_name = None asset_zou_id = None asset_zou_type = 'Assets' + task_zou_id = None zou_sub_type = ['AssetType', 'Sequence'] if asset_name: asset_zou_name = asset_name From ab03df702a0f14c8d0dd94ed3c917c78fde9cb4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Albert?= <89812691+sebasti1a@users.noreply.github.com> Date: Wed, 14 Sep 2022 14:50:50 +0200 Subject: [PATCH 006/111] Update openpype/modules/kitsu/actions/launcher_show_in_kitsu.py Co-authored-by: Roy Nieterau --- openpype/modules/kitsu/actions/launcher_show_in_kitsu.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py index 575d1eecd0..ab523876ed 100644 --- a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py +++ b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py @@ -90,8 +90,8 @@ class ShowInKitsu(LauncherAction): asset_type=None, task_id=None): - shots_url = ['Shots', 'Sequence', 'Shot'] - sub_type = ['AssetType', 'Sequence'] + shots_url = {'Shots', 'Sequence', 'Shot'} + sub_type = {'AssetType', 'Sequence'} kitsu_module = self.get_kitsu_module() # Get kitsu url with /api stripped 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 007/111] 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 008/111] 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 009/111] 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 010/111] 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 011/111] 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 012/111] 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 013/111] 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 014/111] 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 015/111] 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 016/111] 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 017/111] 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 018/111] 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 019/111] 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 020/111] 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 021/111] 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 022/111] 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 023/111] 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 024/111] 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 025/111] 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 026/111] 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 027/111] 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 028/111] 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 029/111] 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 030/111] 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 031/111] 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 032/111] 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 033/111] 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 034/111] 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 035/111] 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 036/111] 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 037/111] 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 038/111] 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 039/111] 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 040/111] 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 041/111] 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 042/111] 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 043/111] 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 044/111] 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 045/111] 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 046/111] 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 047/111] 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 048/111] 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 049/111] 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 050/111] 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 051/111] 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 052/111] 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 ea4ede05a8983118987f75a9f6a169a5d84770be Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 7 Oct 2022 20:31:46 +0200 Subject: [PATCH 053/111] Hotfix for Maya + Deadline + Redshift renders without merge AOVs Fix #3953 --- .../deadline/plugins/publish/submit_maya_deadline.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index 4d6068f3c0..75a3921237 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -32,6 +32,9 @@ from maya import cmds from openpype.pipeline import legacy_io +from openpype.hosts.maya.api.lib_rendersettings import RenderSettings +from openpype.hosts.maya.api.lib import get_attr_in_layer + from openpype_modules.deadline import abstract_submit_deadline from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo @@ -471,9 +474,10 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): job_info.AssetDependency += self.scene_path # Get layer prefix - render_products = self._instance.data["renderProducts"] - layer_metadata = render_products.layer_data - layer_prefix = layer_metadata.filePrefix + renderlayer = self._instance.data["setMembers"] + renderer = self._instance.data["renderer"] + layer_prefix_attr = RenderSettings.get_image_prefix_attr(renderer) + layer_prefix = get_attr_in_layer(layer_prefix_attr, layer=renderlayer) plugin_info = copy.deepcopy(self.plugin_info) plugin_info.update({ From 811e7853e5c031d23fa51dabbc07cc0caf3bc2f1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 10 Oct 2022 14:16:53 +0200 Subject: [PATCH 054/111] 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 055/111] 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 056/111] 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 057/111] 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 058/111] 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 059/111] 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 060/111] 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 061/111] 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 062/111] 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 063/111] 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 064/111] 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 065/111] 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 066/111] 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 067/111] 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 068/111] 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 069/111] 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 070/111] 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 071/111] 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 072/111] 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 073/111] 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 074/111] 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 075/111] 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 076/111] 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 077/111] 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 543d22ffc76eed10874f8c9cc8d516b369aed0b3 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 13 Oct 2022 16:25:29 +0200 Subject: [PATCH 078/111] :bug: fix token for non-multipart outputs and unify variable names --- openpype/hosts/maya/api/lib_renderproducts.py | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index 1ab771cfe6..cd204445b7 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -260,20 +260,20 @@ class ARenderProducts: """ try: - file_prefix_attr = IMAGE_PREFIXES[self.renderer] + prefix_attr = IMAGE_PREFIXES[self.renderer] except KeyError: raise UnsupportedRendererException( "Unsupported renderer {}".format(self.renderer) ) - file_prefix = self._get_attr(file_prefix_attr) + prefix = self._get_attr(prefix_attr) - if not file_prefix: + if not prefix: # Fall back to scene name by default log.debug("Image prefix not set, using ") file_prefix = "" - return file_prefix + return prefix def get_render_attribute(self, attribute): """Get attribute from render options. @@ -730,13 +730,16 @@ class RenderProductsVray(ARenderProducts): """Get image prefix for V-Ray. This overrides :func:`ARenderProducts.get_renderer_prefix()` as - we must add `` token manually. + we must add `` token manually. This is done only for + non-multipart outputs, where `` token doesn't make sense. See also: :func:`ARenderProducts.get_renderer_prefix()` """ prefix = super(RenderProductsVray, self).get_renderer_prefix() + if self.multipart: + return prefix aov_separator = self._get_aov_separator() prefix = "{}{}".format(prefix, aov_separator) return prefix @@ -974,15 +977,18 @@ class RenderProductsRedshift(ARenderProducts): """Get image prefix for Redshift. This overrides :func:`ARenderProducts.get_renderer_prefix()` as - we must add `` token manually. + we must add `` token manually. This is done only for + non-multipart outputs, where `` token doesn't make sense. See also: :func:`ARenderProducts.get_renderer_prefix()` """ - file_prefix = super(RenderProductsRedshift, self).get_renderer_prefix() - separator = self.extract_separator(file_prefix) - prefix = "{}{}".format(file_prefix, separator or "_") + prefix = super(RenderProductsRedshift, self).get_renderer_prefix() + if self.multipart: + return prefix + separator = self.extract_separator(prefix) + prefix = "{}{}".format(prefix, separator or "_") return prefix def get_render_products(self): From 91d3056534bc30190d5d5f31977afd0724635016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Albert?= <89812691+sebasti1a@users.noreply.github.com> Date: Thu, 13 Oct 2022 16:48:29 +0200 Subject: [PATCH 079/111] Update openpype/modules/kitsu/actions/launcher_show_in_kitsu.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/modules/kitsu/actions/launcher_show_in_kitsu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py index ab523876ed..c95079e042 100644 --- a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py +++ b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py @@ -37,7 +37,7 @@ class ShowInKitsu(LauncherAction): project_zou_id = project["data"].get("zou_id") if not project_zou_id: raise RuntimeError(f"Project {project_name} has no " - f"connected ftrack id.") + f"connected kitsu id.") asset_zou_name = None asset_zou_id = None From 840792a82c2c443dee77b4e8ec02ce6ec72b1e70 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 13 Oct 2022 19:14:48 +0200 Subject: [PATCH 080/111] added mechanism to define custom paths to ffmpeg and oiio tools and more detailed validation of them --- openpype/lib/vendor_bin_utils.py | 220 +++++++++++++++++++++++++++++-- 1 file changed, 212 insertions(+), 8 deletions(-) diff --git a/openpype/lib/vendor_bin_utils.py b/openpype/lib/vendor_bin_utils.py index e5ab2872a0..31245d4ee4 100644 --- a/openpype/lib/vendor_bin_utils.py +++ b/openpype/lib/vendor_bin_utils.py @@ -1,10 +1,33 @@ import os import logging import platform +import subprocess log = logging.getLogger("Vendor utils") +class CachedToolPaths: + """Cache already used and discovered tools and their executables. + + Discovering path can take some time and can trigger subprocesses so it's + better to cache the paths on first get. + """ + + _cached_paths = {} + + @classmethod + def is_tool_cached(cls, tool): + return tool in cls._cached_paths + + @classmethod + def get_executable_path(cls, tool): + return cls._cached_paths.get(tool) + + @classmethod + def cache_executable_path(cls, tool, path): + cls._cached_paths[tool] = path + + def is_file_executable(filepath): """Filepath lead to executable file. @@ -98,6 +121,7 @@ def get_vendor_bin_path(bin_app): Returns: str: Path to vendorized binaries folder. """ + return os.path.join( os.environ["OPENPYPE_ROOT"], "vendor", @@ -107,6 +131,112 @@ def get_vendor_bin_path(bin_app): ) +def find_tool_in_custom_paths(paths, tool, validation_func=None): + """Find a tool executable in custom paths. + + Args: + paths (Iterable[str]): Iterable of paths where to look for tool. + tool (str): Name of tool (binary file) to find in passed paths. + validation_func (Function): Custom validation function of path. + Function must expect one argument which is path to executable. + If not passed only 'find_executable' is used to be able identify + if path is valid. + + Reuturns: + Union[str, None]: Path to validated executable or None if was not + found. + """ + + for path in paths: + # Skip empty strings + if not path: + continue + + # Handle cases when path is just an executable + # - it allows to use executable from PATH + # - basename must match 'tool' value (without extension) + extless_path, ext = os.path.splitext(path) + if extless_path == tool: + executable_path = find_executable(tool) + if executable_path and ( + validation_func is None + or validation_func(executable_path) + ): + return executable_path + continue + + # Normalize path because it should be a path and check if exists + normalized = os.path.normpath(path) + if not os.path.exists(normalized): + continue + + # Note: Path can be both file and directory + + # If path is a file validate it + if os.path.isfile(normalized): + basename, ext = os.path.splitext(os.path.basename(path)) + # Check if the filename has actually the sane bane as 'tool' + if basename == tool: + executable_path = find_executable(normalized) + if executable_path and ( + validation_func is None + or validation_func(executable_path) + ): + return executable_path + + # Check if path is a directory and look for tool inside the dir + if os.path.isdir(normalized): + executable_path = find_executable(os.path.join(normalized, tool)) + if executable_path and ( + validation_func is None + or validation_func(executable_path) + ): + return executable_path + return None + + +def _oiio_executable_validation(filepath): + """Validate oiio tool executable if can be executed. + + Validation has 2 steps. First is using 'find_executable' to fill possible + missing extension or fill directory then launch executable and validate + that it can be executed. For that is used '--help' argument which is fast + and does not need any other inputs. + + Any possible crash of missing libraries or invalid build should be catched. + + Main reason is to validate if executable can be executed on OS just running + which can be issue ob linux machines. + + Note: + It does not validate if the executable is really a oiio tool which + should be used. + + Args: + filepath (str): Path to executable. + + Returns: + bool: Filepath is valid executable. + """ + + filepath = find_executable(filepath) + if not filepath: + return False + + try: + proc = subprocess.Popen( + [filepath, "--help"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + proc.wait() + return proc.returncode == 0 + + except Exception: + pass + return False + + def get_oiio_tools_path(tool="oiiotool"): """Path to vendorized OpenImageIO tool executables. @@ -117,10 +247,67 @@ def get_oiio_tools_path(tool="oiiotool"): Default is "oiiotool". """ - oiio_dir = get_vendor_bin_path("oiio") - if platform.system().lower() == "linux": - oiio_dir = os.path.join(oiio_dir, "bin") - return find_executable(os.path.join(oiio_dir, tool)) + if CachedToolPaths.is_tool_cached(tool): + return CachedToolPaths.get_executable_path(tool) + + custom_paths_str = os.environ.get("OPENPYPE_CUSTOM_OIIO_PATHS") or "" + tool_executable_path = find_tool_in_custom_paths( + custom_paths_str.split(os.pathsep), + tool, + _oiio_executable_validation + ) + + if not tool_executable_path: + oiio_dir = get_vendor_bin_path("oiio") + if platform.system().lower() == "linux": + oiio_dir = os.path.join(oiio_dir, "bin") + default_path = os.path.join(oiio_dir, tool) + if _oiio_executable_validation(default_path): + tool_executable_path = default_path + + CachedToolPaths.cache_executable_path(tool, tool_executable_path) + return tool_executable_path + + +def _ffmpeg_executable_validation(filepath): + """Validate ffmpeg tool executable if can be executed. + + Validation has 2 steps. First is using 'find_executable' to fill possible + missing extension or fill directory then launch executable and validate + that it can be executed. For that is used '-version' argument which is fast + and does not need any other inputs. + + Any possible crash of missing libraries or invalid build should be catched. + + Main reason is to validate if executable can be executed on OS just running + which can be issue ob linux machines. + + Note: + It does not validate if the executable is really a ffmpeg tool. + + Args: + filepath (str): Path to executable. + + Returns: + bool: Filepath is valid executable. + """ + + filepath = find_executable(filepath) + if not filepath: + return False + + try: + proc = subprocess.Popen( + [filepath, "-version"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + proc.wait() + return proc.returncode == 0 + + except Exception: + pass + return False def get_ffmpeg_tool_path(tool="ffmpeg"): @@ -133,10 +320,27 @@ def get_ffmpeg_tool_path(tool="ffmpeg"): Returns: str: Full path to ffmpeg executable. """ - ffmpeg_dir = get_vendor_bin_path("ffmpeg") - if platform.system().lower() == "windows": - ffmpeg_dir = os.path.join(ffmpeg_dir, "bin") - return find_executable(os.path.join(ffmpeg_dir, tool)) + + if CachedToolPaths.is_tool_cached(tool): + return CachedToolPaths.get_executable_path(tool) + + custom_paths_str = os.environ.get("OPENPYPE_CUSTOM_FFMPEG_PATHS") or "" + tool_executable_path = find_tool_in_custom_paths( + custom_paths_str.split(os.pathsep), + tool, + _ffmpeg_executable_validation + ) + + if not tool_executable_path: + ffmpeg_dir = get_vendor_bin_path("ffmpeg") + if platform.system().lower() == "windows": + ffmpeg_dir = os.path.join(ffmpeg_dir, "bin") + tool_path = find_executable(os.path.join(ffmpeg_dir, tool)) + if tool_path and _ffmpeg_executable_validation(tool_path): + tool_executable_path = tool_path + + CachedToolPaths.cache_executable_path(tool, tool_executable_path) + return tool_executable_path def is_oiio_supported(): From e3c2bb5a5e4d8a91508e0cd0db9ea2727424d6ed Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 14 Oct 2022 09:46:14 +0200 Subject: [PATCH 081/111] added one more last check for executable --- openpype/lib/vendor_bin_utils.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/openpype/lib/vendor_bin_utils.py b/openpype/lib/vendor_bin_utils.py index 31245d4ee4..7b52341290 100644 --- a/openpype/lib/vendor_bin_utils.py +++ b/openpype/lib/vendor_bin_utils.py @@ -265,6 +265,12 @@ def get_oiio_tools_path(tool="oiiotool"): if _oiio_executable_validation(default_path): tool_executable_path = default_path + # Look to PATH for the tool + if not tool_executable_path: + from_path = find_executable(tool) + if from_path and _oiio_executable_validation(from_path): + tool_executable_path = from_path + CachedToolPaths.cache_executable_path(tool, tool_executable_path) return tool_executable_path @@ -339,6 +345,12 @@ def get_ffmpeg_tool_path(tool="ffmpeg"): if tool_path and _ffmpeg_executable_validation(tool_path): tool_executable_path = tool_path + # Look to PATH for the tool + if not tool_executable_path: + from_path = find_executable(tool) + if from_path and _oiio_executable_validation(from_path): + tool_executable_path = from_path + CachedToolPaths.cache_executable_path(tool, tool_executable_path) return tool_executable_path From 9955ffe95c90fe181740aa81fd21015a0b99caba Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 14 Oct 2022 10:12:30 +0200 Subject: [PATCH 082/111] 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 5009a37b73412c76f3d15d8a9a5ec60911af7149 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 14 Oct 2022 18:57:37 +0200 Subject: [PATCH 083/111] return instance ids instead of instance objects --- .../publisher/widgets/card_view_widgets.py | 2 +- .../publisher/widgets/list_view_widgets.py | 17 +++++++--------- .../publisher/widgets/overview_widget.py | 20 ++++++++++--------- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/openpype/tools/publisher/widgets/card_view_widgets.py b/openpype/tools/publisher/widgets/card_view_widgets.py index 2be37ea44c..4c7d6ce109 100644 --- a/openpype/tools/publisher/widgets/card_view_widgets.py +++ b/openpype/tools/publisher/widgets/card_view_widgets.py @@ -541,6 +541,6 @@ class InstanceCardView(AbstractInstanceView): context_selected = True elif selected_widget is not None: - instances.append(selected_widget.instance) + instances.append(selected_widget.instance.id) return instances, context_selected diff --git a/openpype/tools/publisher/widgets/list_view_widgets.py b/openpype/tools/publisher/widgets/list_view_widgets.py index 17b50b764a..6d90e63683 100644 --- a/openpype/tools/publisher/widgets/list_view_widgets.py +++ b/openpype/tools/publisher/widgets/list_view_widgets.py @@ -723,13 +723,13 @@ class InstanceListView(AbstractInstanceView): widget.update_instance_values() def _on_active_changed(self, changed_instance_id, new_value): - selected_instances, _ = self.get_selected_items() + selected_instance_ids, _ = self.get_selected_items() selected_ids = set() found = False - for instance in selected_instances: - selected_ids.add(instance.id) - if not found and instance.id == changed_instance_id: + for instance_id in selected_instance_ids: + selected_ids.add(instance_id) + if not found and instance_id == changed_instance_id: found = True if not found: @@ -767,9 +767,8 @@ class InstanceListView(AbstractInstanceView): tuple: Selected instance ids and boolean if context is selected. """ - instances = [] + instance_ids = [] context_selected = False - instances_by_id = self._controller.instances for index in self._instance_view.selectionModel().selectedIndexes(): instance_id = index.data(INSTANCE_ID_ROLE) @@ -777,11 +776,9 @@ class InstanceListView(AbstractInstanceView): context_selected = True elif instance_id is not None: - instance = instances_by_id.get(instance_id) - if instance: - instances.append(instance) + instance_ids.append(instance_id) - return instances, context_selected + return instance_ids, context_selected def _on_selection_change(self, *_args): self.selection_changed.emit() diff --git a/openpype/tools/publisher/widgets/overview_widget.py b/openpype/tools/publisher/widgets/overview_widget.py index 3c67e6298e..8759d2ad49 100644 --- a/openpype/tools/publisher/widgets/overview_widget.py +++ b/openpype/tools/publisher/widgets/overview_widget.py @@ -201,16 +201,16 @@ class OverviewWidget(QtWidgets.QFrame): self.create_requested.emit() def _on_delete_clicked(self): - instances, _ = self.get_selected_items() + instance_ids, _ = self.get_selected_items() # Ask user if he really wants to remove instances dialog = QtWidgets.QMessageBox(self) dialog.setIcon(QtWidgets.QMessageBox.Question) dialog.setWindowTitle("Are you sure?") - if len(instances) > 1: + if len(instance_ids) > 1: msg = ( "Do you really want to remove {} instances?" - ).format(len(instances)) + ).format(len(instance_ids)) else: msg = ( "Do you really want to remove the instance?" @@ -224,10 +224,7 @@ class OverviewWidget(QtWidgets.QFrame): dialog.exec_() # Skip if OK was not clicked if dialog.result() == QtWidgets.QMessageBox.Ok: - instance_ids = { - instance.id - for instance in instances - } + instance_ids = set(instance_ids) self._controller.remove_instances(instance_ids) def _on_change_view_clicked(self): @@ -238,11 +235,16 @@ class OverviewWidget(QtWidgets.QFrame): if self._refreshing_instances: return - instances, context_selected = self.get_selected_items() + instance_ids, context_selected = self.get_selected_items() # Disable delete button if nothing is selected - self._delete_btn.setEnabled(len(instances) > 0) + self._delete_btn.setEnabled(len(instance_ids) > 0) + instances_by_id = self._controller.instances + instances = [ + instances_by_id[instance_id] + for instance_id in instance_ids + ] self._subset_attributes_widget.set_current_instances( instances, context_selected ) From 804a92384f86df6fded616abf45061f7144761d5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 14 Oct 2022 19:00:08 +0200 Subject: [PATCH 084/111] pass type of selection from clicked widget --- .../publisher/widgets/card_view_widgets.py | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/openpype/tools/publisher/widgets/card_view_widgets.py b/openpype/tools/publisher/widgets/card_view_widgets.py index 4c7d6ce109..28db844303 100644 --- a/openpype/tools/publisher/widgets/card_view_widgets.py +++ b/openpype/tools/publisher/widgets/card_view_widgets.py @@ -41,9 +41,26 @@ from ..constants import ( ) +class SelectionType: + def __init__(self, name): + self.name = name + + def __eq__(self, other): + if isinstance(other, SelectionType): + other = other.name + return self.name == other + + +class SelectionTypes: + clear = SelectionType("clear") + extend = SelectionType("extend") + extend_to = SelectionType("extend_to") + + class GroupWidget(QtWidgets.QWidget): """Widget wrapping instances under group.""" - selected = QtCore.Signal(str, str) + + selected = QtCore.Signal(str, str, SelectionType) active_changed = QtCore.Signal() removed_selected = QtCore.Signal() @@ -135,17 +152,21 @@ class GroupWidget(QtWidgets.QWidget): widget = InstanceCardWidget( instance, group_icon, self ) - widget.selected.connect(self.selected) + widget.selected.connect(self._on_widget_selection) widget.active_changed.connect(self.active_changed) self._widgets_by_id[instance.id] = widget self._content_layout.insertWidget(widget_idx, widget) widget_idx += 1 + def _on_widget_selection(self, instance_id, group_id, selection_type): + self.selected.emit(instance_id, group_id, selection_type) + + class CardWidget(BaseClickableFrame): """Clickable card used as bigger button.""" - selected = QtCore.Signal(str, str) + selected = QtCore.Signal(str, str, SelectionType) # Group identifier of card # - this must be set because if send when mouse is released with card id _group_identifier = None @@ -173,7 +194,16 @@ class CardWidget(BaseClickableFrame): def _mouse_release_callback(self): """Trigger selected signal.""" - self.selected.emit(self._id, self._group_identifier) + + modifiers = QtWidgets.QApplication.keyboardModifiers() + selection_type = SelectionTypes.clear + if bool(modifiers & QtCore.Qt.ShiftModifier): + selection_type = SelectionTypes.extend_to + + elif bool(modifiers & QtCore.Qt.ControlModifier): + selection_type = SelectionTypes.extend + + self.selected.emit(self._id, self._group_identifier, selection_type) class ContextCardWidget(CardWidget): @@ -498,7 +528,7 @@ class InstanceCardView(AbstractInstanceView): def _on_active_changed(self): self.active_changed.emit() - def _on_widget_selection(self, instance_id, group_name): + def _on_widget_selection(self, instance_id, group_name, selection_type): self.select_item(instance_id, group_name) def select_item(self, instance_id, group_name): From d20adf201ac7073ed3f4a7aa320ce220c6b34f5f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 14 Oct 2022 19:00:58 +0200 Subject: [PATCH 085/111] added additional helper attributes and methods --- .../publisher/widgets/card_view_widgets.py | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/openpype/tools/publisher/widgets/card_view_widgets.py b/openpype/tools/publisher/widgets/card_view_widgets.py index 28db844303..80a3bf0fb1 100644 --- a/openpype/tools/publisher/widgets/card_view_widgets.py +++ b/openpype/tools/publisher/widgets/card_view_widgets.py @@ -89,21 +89,73 @@ class GroupWidget(QtWidgets.QWidget): self._group_icons = group_icons self._widgets_by_id = {} + self._ordered_instance_ids = [] self._label_widget = label_widget self._content_layout = layout + @property + def group_name(self): + """Group which widget represent. + + Returns: + str: Name of group. + """ + + return self._group + + def get_selected_instance_ids(self): + """Selected instance ids. + + Returns: + Set[str]: Instance ids that are selected. + """ + + return { + instance_id + for instance_id, widget in self._widgets_by_id.items() + if widget.is_selected + } + + def get_selected_widgets(self): + """Access to widgets marked as selected. + + Returns: + List[InstanceCardWidget]: Instance widgets that are selected. + """ + + return [ + widget + for instance_id, widget in self._widgets_by_id.items() + if widget.is_selected + ] + + def get_ordered_widgets(self): + """Get instance ids in order as are shown in ui. + + Returns: + List[str]: Instance ids. + """ + + return [ + self._widgets_by_id[instance_id] + for instance_id in self._ordered_instance_ids + ] + def get_widget_by_instance_id(self, instance_id): """Get instance widget by it's id.""" + return self._widgets_by_id.get(instance_id) def update_instance_values(self): """Trigger update on instance widgets.""" + for widget in self._widgets_by_id.values(): widget.update_instance_values() def confirm_remove_instance_id(self, instance_id): """Delete widget by instance id.""" + widget = self._widgets_by_id.pop(instance_id) widget.setVisible(False) self._content_layout.removeWidget(widget) @@ -140,6 +192,7 @@ class GroupWidget(QtWidgets.QWidget): # Sort instances by subset name sorted_subset_names = list(sorted(instances_by_subset_name.keys())) + # Add new instances to widget widget_idx = 1 for subset_names in sorted_subset_names: @@ -158,6 +211,15 @@ class GroupWidget(QtWidgets.QWidget): self._content_layout.insertWidget(widget_idx, widget) widget_idx += 1 + ordered_instance_ids = [] + for idx in range(self._content_layout.count()): + if idx > 0: + item = self._content_layout.itemAt(idx) + widget = item.widget() + if widget is not None: + ordered_instance_ids.append(widget.id) + + self._ordered_instance_ids = ordered_instance_ids def _on_widget_selection(self, instance_id, group_id, selection_type): self.selected.emit(instance_id, group_id, selection_type) @@ -178,6 +240,12 @@ class CardWidget(BaseClickableFrame): self._selected = False self._id = None + @property + def id(self): + """Id of card.""" + + return self._id + @property def is_selected(self): """Is card selected.""" From 0c27f807955ec312fe0628b331b3ba8416be85c9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 14 Oct 2022 19:02:53 +0200 Subject: [PATCH 086/111] implemented logic to handle multiselection --- .../publisher/widgets/card_view_widgets.py | 355 +++++++++++++++--- 1 file changed, 308 insertions(+), 47 deletions(-) diff --git a/openpype/tools/publisher/widgets/card_view_widgets.py b/openpype/tools/publisher/widgets/card_view_widgets.py index 80a3bf0fb1..c0cc3389c7 100644 --- a/openpype/tools/publisher/widgets/card_view_widgets.py +++ b/openpype/tools/publisher/widgets/card_view_widgets.py @@ -480,11 +480,12 @@ class InstanceCardView(AbstractInstanceView): self._content_layout = content_layout self._content_widget = content_widget - self._widgets_by_group = {} self._context_widget = None + self._widgets_by_group = {} + self._ordered_groups = [] - self._selected_group = None - self._selected_instance_id = None + self._explicitly_selected_instance_ids = [] + self._explicitly_selected_groups = [] self.setSizePolicy( QtWidgets.QSizePolicy.Minimum, @@ -504,21 +505,30 @@ class InstanceCardView(AbstractInstanceView): result.setWidth(width) return result - def _get_selected_widget(self): - if self._selected_instance_id == CONTEXT_ID: - return self._context_widget + def _get_selected_widgets(self): + output = [] + if ( + self._context_widget is not None + and self._context_widget.is_selected + ): + output.append(self._context_widget) - group_widget = self._widgets_by_group.get( - self._selected_group - ) - if group_widget is not None: - widget = group_widget.get_widget_by_instance_id( - self._selected_instance_id - ) - if widget is not None: - return widget + for group_widget in self._widgets_by_group.values(): + for widget in group_widget.get_selected_widgets(): + output.append(widget) + return output - return None + def _get_selected_instance_ids(self): + output = [] + if ( + self._context_widget is not None + and self._context_widget.is_selected + ): + output.append(CONTEXT_ID) + + for group_widget in self._widgets_by_group.values(): + output.extend(group_widget.get_selected_instance_ids()) + return output def refresh(self): """Refresh instances in view based on CreatedContext.""" @@ -534,8 +544,6 @@ class InstanceCardView(AbstractInstanceView): self.selection_changed.emit() self._content_layout.insertWidget(0, widget) - self.select_item(CONTEXT_ID, None) - # Prepare instances by group and identifiers by group instances_by_group = collections.defaultdict(list) identifiers_by_group = collections.defaultdict(set) @@ -551,15 +559,17 @@ class InstanceCardView(AbstractInstanceView): if group_name in instances_by_group: continue - if group_name == self._selected_group: - self._on_remove_selected() widget = self._widgets_by_group.pop(group_name) widget.setVisible(False) self._content_layout.removeWidget(widget) widget.deleteLater() + if group_name in self._explicitly_selected_groups: + self._explicitly_selected_groups.remove(group_name) + # Sort groups sorted_group_names = list(sorted(instances_by_group.keys())) + # Keep track of widget indexes # - we start with 1 because Context item as at the top widget_idx = 1 @@ -577,9 +587,6 @@ class InstanceCardView(AbstractInstanceView): ) group_widget.active_changed.connect(self._on_active_changed) group_widget.selected.connect(self._on_widget_selection) - group_widget.removed_selected.connect( - self._on_remove_selected - ) self._content_layout.insertWidget(widget_idx, group_widget) self._widgets_by_group[group_name] = group_widget @@ -588,6 +595,16 @@ class InstanceCardView(AbstractInstanceView): instances_by_group[group_name] ) + ordered_group_names = [""] + for idx in range(self._content_layout.count()): + if idx > 0: + item = self._content_layout.itemAt(idx) + group_widget = item.widget() + if group_widget is not None: + ordered_group_names.append(group_widget.group_name) + + self._ordered_groups = ordered_group_names + def refresh_instance_states(self): """Trigger update of instances on group widgets.""" for widget in self._widgets_by_group.values(): @@ -597,9 +614,6 @@ class InstanceCardView(AbstractInstanceView): self.active_changed.emit() def _on_widget_selection(self, instance_id, group_name, selection_type): - self.select_item(instance_id, group_name) - - def select_item(self, instance_id, group_name): """Select specific item by instance id. Pass `CONTEXT_ID` as instance id and empty string as group to select @@ -611,34 +625,281 @@ class InstanceCardView(AbstractInstanceView): group_widget = self._widgets_by_group[group_name] new_widget = group_widget.get_widget_by_instance_id(instance_id) - selected_widget = self._get_selected_widget() - if new_widget is selected_widget: - return - - if selected_widget is not None: - selected_widget.set_selected(False) - - self._selected_instance_id = instance_id - self._selected_group = group_name - if new_widget is not None: - new_widget.set_selected(True) + if selection_type is SelectionTypes.clear: + self._select_item_clear(instance_id, group_name, new_widget) + elif selection_type is SelectionTypes.extend: + self._select_item_extend(instance_id, group_name, new_widget) + elif selection_type is SelectionTypes.extend_to: + self._select_item_extend_to(instance_id, group_name, new_widget) self.selection_changed.emit() - def _on_remove_selected(self): - selected_widget = self._get_selected_widget() - if selected_widget is None: - self._on_widget_selection(CONTEXT_ID, None) + def _select_item_clear(self, instance_id, group_name, new_widget): + """Select specific item by instance id and clear previous selection. + + Pass `CONTEXT_ID` as instance id and empty string as group to select + global context item. + """ + + selected_widgets = self._get_selected_widgets() + for widget in selected_widgets: + if widget.id != instance_id: + widget.set_selected(False) + + self._explicitly_selected_groups = [group_name] + self._explicitly_selected_instance_ids = [instance_id] + + if new_widget is not None: + new_widget.set_selected(True) + + def _select_item_extend(self, instance_id, group_name, new_widget): + """Add/Remove single item to/from current selection. + + If item is already selected the selection is removed. + """ + + self._explicitly_selected_instance_ids = ( + self._get_selected_instance_ids() + ) + if new_widget.is_selected: + self._explicitly_selected_instance_ids.remove(instance_id) + new_widget.set_selected(False) + remove_group = False + if instance_id == CONTEXT_ID: + remove_group = True + else: + group_widget = self._widgets_by_group[group_name] + if not group_widget.get_selected_widgets(): + remove_group = True + + if remove_group: + self._explicitly_selected_groups.remove(group_name) + return + + self._explicitly_selected_instance_ids.append(instance_id) + if group_name in self._explicitly_selected_groups: + self._explicitly_selected_groups.remove(group_name) + self._explicitly_selected_groups.append(group_name) + new_widget.set_selected(True) + + def _select_item_extend_to(self, instance_id, group_name, new_widget): + """Extend selected items to specific instance id. + + This method is handling Shift+click selection of widgets. Selection + is not stored to explicit selection items. That's because user can + shift select again and it should use last explicit selected item as + source item for selection. + + Items selected via this function can get to explicit selection only if + selection is extended by one specific item ('_select_item_extend'). + From that moment the selection is locked to new last explicit selected + item. + + It's required to traverse through group widgets in their UI order and + through their instances in UI order. All explicitly selected items + must not change their selection state during this function. Passed + instance id can be above or under last selected item so a start item + and end item must be found to be able know which direction is selection + happening. + """ + + # Start group name (in '_ordered_groups') + start_group = None + # End group name (in '_ordered_groups') + end_group = None + # Instance id of first selected item + start_instance_id = None + # Instance id of last selected item + end_instance_id = None + + # Get previously selected group by explicit selected groups + previous_group = None + if self._explicitly_selected_groups: + previous_group = self._explicitly_selected_groups[-1] + + # Find last explicitly selected instance id + previous_last_selected_id = None + if self._explicitly_selected_instance_ids: + previous_last_selected_id = ( + self._explicitly_selected_instance_ids[-1] + ) + + # If last instance id was not found or available then last selected + # group is also invalid. + # NOTE: This probably never happen? + if previous_last_selected_id is None: + previous_group = None + + # Check if previously selected group is available and find out if + # new instance group is above or under previous selection + # - based on these information are start/end group/instance filled + if previous_group in self._ordered_groups: + new_idx = self._ordered_groups.index(group_name) + prev_idx = self._ordered_groups.index(previous_group) + if new_idx < prev_idx: + start_group = group_name + end_group = previous_group + start_instance_id = instance_id + end_instance_id = previous_last_selected_id + else: + start_group = previous_group + end_group = group_name + start_instance_id = previous_last_selected_id + end_instance_id = instance_id + + # If start group is not set then use context item group name + if start_group is None: + start_group = "" + + # If start instance id is not filled then use context id (similar to + # group) + if start_instance_id is None: + start_instance_id = CONTEXT_ID + + # If end group is not defined then use passed group name + # - this can be happen when previous group was not selected + # - when this happens the selection will probably happen from context + # item to item selected by user + if end_group is None: + end_group = group_name + + # If end instance is not filled then use instance selected by user + if end_instance_id is None: + end_instance_id = instance_id + + # Start and end group are the same + # - a different logic is needed in that case + same_group = start_group == end_group + + # Process known information and change selection of items + passed_start_group = False + passed_end_group = False + # Go through ordered groups (from top to bottom) and change selection + for name in self._ordered_groups: + # Prepare sorted instance widgets + if name == "": + sorted_widgets = [self._context_widget] + else: + group_widget = self._widgets_by_group[name] + sorted_widgets = group_widget.get_ordered_widgets() + + # Change selection based on explicit selection if start group + # was not passed yet + if not passed_start_group: + if name != start_group: + for widget in sorted_widgets: + widget.set_selected( + widget.id in self._explicitly_selected_instance_ids + ) + continue + + # Change selection based on explicit selection if end group + # already passed + if passed_end_group: + for widget in sorted_widgets: + widget.set_selected( + widget.id in self._explicitly_selected_instance_ids + ) + continue + + # Start group is already passed and end group was not yet hit + if same_group: + passed_start_group = True + passed_end_group = True + passed_start_instance = False + passed_end_instance = False + for widget in sorted_widgets: + if not passed_start_instance: + if widget.id in (start_instance_id, end_instance_id): + if widget.id != start_instance_id: + # Swap start/end instance if start instance is + # after end + # - fix 'passed_end_instance' check + start_instance_id, end_instance_id = ( + end_instance_id, start_instance_id + ) + passed_start_instance = True + + # Find out if widget should be selected + select = False + if passed_end_instance: + select = False + + elif passed_start_instance: + select = True + + # Check if instance is in explicitly selected items if + # should ont be selected + if ( + not select + and widget.id in self._explicitly_selected_instance_ids + ): + select = True + + widget.set_selected(select) + + if ( + not passed_end_instance + and widget.id == end_instance_id + ): + passed_end_instance = True + + elif name == start_group: + # First group from which selection should start + # - look for start instance first from which the selection + # should happen + passed_start_group = True + passed_start_instance = False + for widget in sorted_widgets: + if widget.id == start_instance_id: + passed_start_instance = True + + select = False + # Check if passed start instance or instance is + # in explicitly selected items to be selected + if ( + passed_start_instance + or widget.id in self._explicitly_selected_instance_ids + ): + select = True + widget.set_selected(select) + + elif name == end_group: + # Last group where selection should happen + # - look for end instance first after which the selection + # should stop + passed_end_group = True + passed_end_instance = False + for widget in sorted_widgets: + select = False + # Check if not yet passed end instance or if instance is + # in explicitly selected items to be selected + if ( + not passed_end_instance + or widget.id in self._explicitly_selected_instance_ids + ): + select = True + + widget.set_selected(select) + + if widget.id == end_instance_id: + passed_end_instance = True + + else: + # Just select everything between start and end group + for widget in sorted_widgets: + widget.set_selected(True) def get_selected_items(self): """Get selected instance ids and context.""" instances = [] - context_selected = False - selected_widget = self._get_selected_widget() - if selected_widget is self._context_widget: - context_selected = True + selected_widgets = self._get_selected_widgets() - elif selected_widget is not None: - instances.append(selected_widget.instance.id) + context_selected = False + for widget in selected_widgets: + if widget is self._context_widget: + context_selected = True + else: + instances.append(widget.id) return instances, context_selected From 1b8dff405a6737c45e62617da2fba8ea4604b308 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 15 Oct 2022 18:31:03 +0200 Subject: [PATCH 087/111] 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 088/111] 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 089/111] 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 0df15975b16aabac45a61f2f025956180537c2b2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 Oct 2022 00:34:02 +0200 Subject: [PATCH 090/111] 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 091/111] 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 092/111] 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 49190b9f7876a9ed86136c6004dc671288c8e031 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 17 Oct 2022 12:19:26 +0200 Subject: [PATCH 093/111] keep selectio between instance views --- .../publisher/widgets/card_view_widgets.py | 37 ++++++ .../publisher/widgets/list_view_widgets.py | 119 +++++++++++++++--- .../publisher/widgets/overview_widget.py | 10 +- openpype/tools/publisher/widgets/widgets.py | 15 +++ 4 files changed, 159 insertions(+), 22 deletions(-) diff --git a/openpype/tools/publisher/widgets/card_view_widgets.py b/openpype/tools/publisher/widgets/card_view_widgets.py index c0cc3389c7..5daf8059b0 100644 --- a/openpype/tools/publisher/widgets/card_view_widgets.py +++ b/openpype/tools/publisher/widgets/card_view_widgets.py @@ -903,3 +903,40 @@ class InstanceCardView(AbstractInstanceView): instances.append(widget.id) return instances, context_selected + + def set_selected_items(self, instance_ids, context_selected): + s_instance_ids = set(instance_ids) + cur_ids, cur_context = self.get_selected_items() + if ( + set(cur_ids) == s_instance_ids + and cur_context == context_selected + ): + return + + selected_groups = [] + selected_instances = [] + if context_selected: + selected_groups.append("") + selected_instances.append(CONTEXT_ID) + + self._context_widget.set_selected(context_selected) + + for group_name in self._ordered_groups: + if group_name == "": + continue + + group_widget = self._widgets_by_group[group_name] + group_selected = False + for widget in group_widget.get_ordered_widgets(): + select = False + if widget.id in s_instance_ids: + selected_instances.append(widget.id) + group_selected = True + select = True + widget.set_selected(select) + + if group_selected: + selected_groups.append(group_name) + + self._explicitly_selected_groups = selected_groups + self._explicitly_selected_instance_ids = selected_instances diff --git a/openpype/tools/publisher/widgets/list_view_widgets.py b/openpype/tools/publisher/widgets/list_view_widgets.py index 6d90e63683..c329ca0e8c 100644 --- a/openpype/tools/publisher/widgets/list_view_widgets.py +++ b/openpype/tools/publisher/widgets/list_view_widgets.py @@ -760,26 +760,6 @@ class InstanceListView(AbstractInstanceView): if changed_ids: self.active_changed.emit() - def get_selected_items(self): - """Get selected instance ids and context selection. - - Returns: - tuple: Selected instance ids and boolean if context - is selected. - """ - instance_ids = [] - context_selected = False - - for index in self._instance_view.selectionModel().selectedIndexes(): - instance_id = index.data(INSTANCE_ID_ROLE) - if not context_selected and instance_id == CONTEXT_ID: - context_selected = True - - elif instance_id is not None: - instance_ids.append(instance_id) - - return instance_ids, context_selected - def _on_selection_change(self, *_args): self.selection_changed.emit() @@ -819,3 +799,102 @@ class InstanceListView(AbstractInstanceView): proxy_index = self._proxy_model.mapFromSource(group_item.index()) if not self._instance_view.isExpanded(proxy_index): self._instance_view.expand(proxy_index) + + def get_selected_items(self): + """Get selected instance ids and context selection. + + Returns: + tuple: Selected instance ids and boolean if context + is selected. + """ + instance_ids = [] + context_selected = False + + for index in self._instance_view.selectionModel().selectedIndexes(): + instance_id = index.data(INSTANCE_ID_ROLE) + if not context_selected and instance_id == CONTEXT_ID: + context_selected = True + + elif instance_id is not None: + instance_ids.append(instance_id) + + return instance_ids, context_selected + + def set_selected_items(self, instance_ids, context_selected): + s_instance_ids = set(instance_ids) + cur_ids, cur_context = self.get_selected_items() + if ( + set(cur_ids) == s_instance_ids + and cur_context == context_selected + ): + return + + view = self._instance_view + src_model = self._instance_model + proxy_model = self._proxy_model + + select_indexes = [] + + select_queue = collections.deque() + select_queue.append( + (src_model.invisibleRootItem(), []) + ) + while select_queue: + queue_item = select_queue.popleft() + item, parent_items = queue_item + + if item.hasChildren(): + new_parent_items = list(parent_items) + new_parent_items.append(item) + for row in range(item.rowCount()): + select_queue.append( + (item.child(row), list(new_parent_items)) + ) + + instance_id = item.data(INSTANCE_ID_ROLE) + if not instance_id: + continue + + if instance_id in s_instance_ids: + select_indexes.append(item.index()) + for parent_item in parent_items: + index = parent_item.index() + proxy_index = proxy_model.mapFromSource(index) + if not view.isExpanded(proxy_index): + view.expand(proxy_index) + + elif context_selected and instance_id == CONTEXT_ID: + select_indexes.append(item.index()) + + selection_model = view.selectionModel() + if not select_indexes: + selection_model.clear() + return + + if len(select_indexes) == 1: + proxy_index = proxy_model.mapFromSource(select_indexes[0]) + selection_model.setCurrentIndex( + proxy_index, + selection_model.ClearAndSelect | selection_model.Rows + ) + return + + first_index = proxy_model.mapFromSource(select_indexes.pop(0)) + last_index = proxy_model.mapFromSource(select_indexes.pop(-1)) + + selection_model.setCurrentIndex( + first_index, + selection_model.ClearAndSelect | selection_model.Rows + ) + + for index in select_indexes: + proxy_index = proxy_model.mapFromSource(index) + selection_model.select( + proxy_index, + selection_model.Select | selection_model.Rows + ) + + selection_model.setCurrentIndex( + last_index, + selection_model.Select | selection_model.Rows + ) diff --git a/openpype/tools/publisher/widgets/overview_widget.py b/openpype/tools/publisher/widgets/overview_widget.py index 8759d2ad49..5bd3017c2a 100644 --- a/openpype/tools/publisher/widgets/overview_widget.py +++ b/openpype/tools/publisher/widgets/overview_widget.py @@ -321,15 +321,21 @@ class OverviewWidget(QtWidgets.QFrame): def _change_view_type(self): idx = self._subset_views_layout.currentIndex() new_idx = (idx + 1) % self._subset_views_layout.count() - self._subset_views_layout.setCurrentIndex(new_idx) - new_view = self._subset_views_layout.currentWidget() + old_view = self._subset_views_layout.currentWidget() + new_view = self._subset_views_layout.widget(new_idx) + if not new_view.refreshed: new_view.refresh() new_view.set_refreshed(True) else: new_view.refresh_instance_states() + instance_ids, context_selected = old_view.get_selected_items() + new_view.set_selected_items(instance_ids, context_selected) + + self._subset_views_layout.setCurrentIndex(new_idx) + self._on_subset_change() def _refresh_instances(self): diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 536650e209..ddbe1eb6b7 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -306,10 +306,25 @@ class AbstractInstanceView(QtWidgets.QWidget): Example: When delete button is clicked to know what should be deleted. """ + raise NotImplementedError(( "{} Method 'get_selected_items' is not implemented." ).format(self.__class__.__name__)) + def set_selected_items(self, instance_ids, context_selected): + """Change selection for instances and context. + + Used to applying selection from one view to other. + + Args: + instance_ids (List[str]): Selected instance ids. + context_selected (bool): Context is selected. + """ + + raise NotImplementedError(( + "{} Method 'set_selected_items' is not implemented." + ).format(self.__class__.__name__)) + class ClickableLineEdit(QtWidgets.QLineEdit): """QLineEdit capturing left mouse click. From 1ecc673c6ccf3abeb6acdf2529a617daf447a51e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 17 Oct 2022 17:15:51 +0200 Subject: [PATCH 094/111] 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 095/111] 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 d1bd6943167a860d1fdd2584771ce16b665c75b0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 17 Oct 2022 17:29:54 +0200 Subject: [PATCH 096/111] disable sequences in mov fielpaths input --- .../hosts/traypublisher/plugins/create/create_movie_batch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/traypublisher/plugins/create/create_movie_batch.py b/openpype/hosts/traypublisher/plugins/create/create_movie_batch.py index abe29d7473..cf25a37918 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_movie_batch.py +++ b/openpype/hosts/traypublisher/plugins/create/create_movie_batch.py @@ -188,6 +188,7 @@ class BatchMovieCreator(TrayPublishCreator): folders=False, single_item=False, extensions=self.extensions, + allow_sequences=False, label="Filepath" ), BoolDef( From 609f9f12851dfc775edd04344a2c9aa1eaba8426 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 17 Oct 2022 17:44:08 +0200 Subject: [PATCH 097/111] 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 098/111] 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 099/111] 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 100/111] 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 101/111] 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 102/111] 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 103/111] 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 104/111] 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 88438127ce6bae249ea76fb1b8a47e28574d6dde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Tue, 18 Oct 2022 11:06:20 +0200 Subject: [PATCH 105/111] upgrading change log generator to 2.3 also rising sinceTag to 3.12.0 --- .github/workflows/prerelease.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index bf39f8f956..81d5f05b17 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -40,13 +40,13 @@ jobs: - name: "✏️ Generate full changelog" if: steps.version_type.outputs.type != 'skip' id: generate-full-changelog - uses: heinrichreimer/github-changelog-generator-action@v2.2 + uses: heinrichreimer/github-changelog-generator-action@v2.3 with: token: ${{ secrets.ADMIN_TOKEN }} addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}, "refactor":{"prefix":"**🔀 Refactored code**", "labels":["refactor"]}}' issues: false issuesWoLabels: false - sinceTag: "3.0.0" + sinceTag: "3.12.0" maxIssues: 100 pullRequests: true prWoLabels: false @@ -92,4 +92,4 @@ jobs: github_token: ${{ secrets.ADMIN_TOKEN }} source_ref: 'main' target_branch: 'develop' - commit_message_template: '[Automated] Merged {source_ref} into {target_branch}' \ No newline at end of file + commit_message_template: '[Automated] Merged {source_ref} into {target_branch}' From 4c99546b035def8a9d4854e9d90df7edffc76a7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Tue, 18 Oct 2022 11:07:55 +0200 Subject: [PATCH 106/111] update change log generator to 2.3 rise sinceTag to 3.12.0 --- .github/workflows/release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 85864b4442..cc69e1643a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,13 +36,13 @@ jobs: - name: "✏️ Generate full changelog" if: steps.version.outputs.release_tag != 'skip' id: generate-full-changelog - uses: heinrichreimer/github-changelog-generator-action@v2.2 + uses: heinrichreimer/github-changelog-generator-action@v2.3 with: token: ${{ secrets.ADMIN_TOKEN }} addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}, "refactor":{"prefix":"**🔀 Refactored code**", "labels":["refactor"]}}' issues: false issuesWoLabels: false - sinceTag: "3.0.0" + sinceTag: "3.12.0" maxIssues: 100 pullRequests: true prWoLabels: false @@ -121,4 +121,4 @@ jobs: github_token: ${{ secrets.ADMIN_TOKEN }} source_ref: 'main' target_branch: 'develop' - commit_message_template: '[Automated] Merged release {source_ref} into {target_branch}' \ No newline at end of file + commit_message_template: '[Automated] Merged release {source_ref} into {target_branch}' From 4641cb5bae28620e53fec1f2b75dd673b33de55c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 18 Oct 2022 14:03:05 +0200 Subject: [PATCH 107/111] 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) From def2fc4bc81d3ec21f8a6cb59726456d7c3d4080 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 18 Oct 2022 14:51:54 +0200 Subject: [PATCH 108/111] removed '_CUSTOM' from env keys --- openpype/lib/vendor_bin_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/lib/vendor_bin_utils.py b/openpype/lib/vendor_bin_utils.py index 7b52341290..eb7987c8a1 100644 --- a/openpype/lib/vendor_bin_utils.py +++ b/openpype/lib/vendor_bin_utils.py @@ -250,7 +250,7 @@ def get_oiio_tools_path(tool="oiiotool"): if CachedToolPaths.is_tool_cached(tool): return CachedToolPaths.get_executable_path(tool) - custom_paths_str = os.environ.get("OPENPYPE_CUSTOM_OIIO_PATHS") or "" + custom_paths_str = os.environ.get("OPENPYPE_OIIO_PATHS") or "" tool_executable_path = find_tool_in_custom_paths( custom_paths_str.split(os.pathsep), tool, @@ -330,7 +330,7 @@ def get_ffmpeg_tool_path(tool="ffmpeg"): if CachedToolPaths.is_tool_cached(tool): return CachedToolPaths.get_executable_path(tool) - custom_paths_str = os.environ.get("OPENPYPE_CUSTOM_FFMPEG_PATHS") or "" + custom_paths_str = os.environ.get("OPENPYPE_FFMPEG_PATHS") or "" tool_executable_path = find_tool_in_custom_paths( custom_paths_str.split(os.pathsep), tool, From 8e90bf73c9bb0ccbb46fedcc557118b2cfb8adb8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 18 Oct 2022 15:00:56 +0200 Subject: [PATCH 109/111] added information about ffmeg and oiio to documentation --- website/docs/admin_settings_system.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/website/docs/admin_settings_system.md b/website/docs/admin_settings_system.md index 8daba91db1..800ec1c840 100644 --- a/website/docs/admin_settings_system.md +++ b/website/docs/admin_settings_system.md @@ -26,7 +26,14 @@ as a naive barier to prevent artists from accidental setting changes. **`Disk mapping`** - Platform dependent configuration for mapping of virtual disk(s) on an artist's OpenPype machines before OP starts up. Uses `subst` command, if configured volume character in `Destination` field already exists, no re-mapping is done for that character(volume). + +### FFmpeg and OpenImageIO tools + +We bundle FFmpeg and OpenImageIO tools with OpenPype build for Windows and Linux builds. For MacOs support or to use different build is it possible to set environment variables `OPENPYPE_FFMPEG_PATHS` and `OPENPYPE_OIIO_PATHS` in system settings. Both should lead to directory where tool executables are located. Multiple paths are supported. + + ### OpenPype deployment control + **`Versions Repository`** - Location where automatic update mechanism searches for zip files with OpenPype update packages. To read more about preparing OpenPype for automatic updates go to [Admin Distribute docs](admin_distribute.md#2-openpype-codebase) From 952fd6c15c6f60a062bd3e14e0bc6df5dd7b33ee Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 18 Oct 2022 15:01:43 +0200 Subject: [PATCH 110/111] removed unnecessary lines --- website/docs/admin_settings_system.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/website/docs/admin_settings_system.md b/website/docs/admin_settings_system.md index 800ec1c840..cef4571c84 100644 --- a/website/docs/admin_settings_system.md +++ b/website/docs/admin_settings_system.md @@ -26,14 +26,10 @@ as a naive barier to prevent artists from accidental setting changes. **`Disk mapping`** - Platform dependent configuration for mapping of virtual disk(s) on an artist's OpenPype machines before OP starts up. Uses `subst` command, if configured volume character in `Destination` field already exists, no re-mapping is done for that character(volume). - ### FFmpeg and OpenImageIO tools - We bundle FFmpeg and OpenImageIO tools with OpenPype build for Windows and Linux builds. For MacOs support or to use different build is it possible to set environment variables `OPENPYPE_FFMPEG_PATHS` and `OPENPYPE_OIIO_PATHS` in system settings. Both should lead to directory where tool executables are located. Multiple paths are supported. - ### OpenPype deployment control - **`Versions Repository`** - Location where automatic update mechanism searches for zip files with OpenPype update packages. To read more about preparing OpenPype for automatic updates go to [Admin Distribute docs](admin_distribute.md#2-openpype-codebase) From e9db3dbadce7bee241608f010785a3b14ce95641 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 18 Oct 2022 15:07:56 +0200 Subject: [PATCH 111/111] rephrase sentence --- website/docs/admin_settings_system.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/admin_settings_system.md b/website/docs/admin_settings_system.md index cef4571c84..66715e7288 100644 --- a/website/docs/admin_settings_system.md +++ b/website/docs/admin_settings_system.md @@ -27,7 +27,7 @@ as a naive barier to prevent artists from accidental setting changes. Uses `subst` command, if configured volume character in `Destination` field already exists, no re-mapping is done for that character(volume). ### FFmpeg and OpenImageIO tools -We bundle FFmpeg and OpenImageIO tools with OpenPype build for Windows and Linux builds. For MacOs support or to use different build is it possible to set environment variables `OPENPYPE_FFMPEG_PATHS` and `OPENPYPE_OIIO_PATHS` in system settings. Both should lead to directory where tool executables are located. Multiple paths are supported. +We bundle FFmpeg tools for all platforms and OpenImageIO tools for Windows and Linux. By default are used bundled tools but it is possible to set environment variables `OPENPYPE_FFMPEG_PATHS` and `OPENPYPE_OIIO_PATHS` in system settings environments to look for them in different directory e.g. for different linux distributions or to add oiio support for MacOs. Values of both environment variables should lead to directory where tool executables are located (multiple paths are supported). ### OpenPype deployment control **`Versions Repository`** - Location where automatic update mechanism searches for zip files with