From 53c9acc004566c79869d1da59c1620736f4c0d7c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 5 Feb 2024 16:23:44 +0100 Subject: [PATCH] removed standalone publisher --- .../hosts/standalonepublisher/__init__.py | 6 - .../hosts/standalonepublisher/addon.py | 59 -- .../plugins/publish/collect_app_name.py | 13 - .../publish/collect_bulk_mov_instances.py | 91 --- .../plugins/publish/collect_context.py | 276 --------- .../plugins/publish/collect_editorial.py | 125 ---- .../publish/collect_editorial_instances.py | 214 ------- .../publish/collect_editorial_resources.py | 271 --------- .../plugins/publish/collect_harmony_scenes.py | 102 ---- .../plugins/publish/collect_harmony_zips.py | 82 --- .../plugins/publish/collect_hierarchy.py | 304 ---------- .../plugins/publish/collect_instance_data.py | 30 - .../plugins/publish/collect_matching_asset.py | 127 ----- .../plugins/publish/collect_remove_marked.py | 21 - .../publish/collect_representation_names.py | 31 - .../plugins/publish/collect_texture.py | 524 ----------------- .../plugins/publish/extract_resources.py | 42 -- .../plugins/publish/extract_thumbnail.py | 127 ----- .../publish/extract_workfile_location.py | 44 -- .../help/validate_editorial_resources.xml | 17 - .../publish/help/validate_frame_ranges.xml | 15 - .../publish/help/validate_shot_duplicates.xml | 15 - .../help/validate_simple_texture_naming.xml | 17 - .../plugins/publish/help/validate_sources.xml | 16 - .../publish/help/validate_task_existence.xml | 16 - .../publish/help/validate_texture_batch.xml | 15 - .../help/validate_texture_has_workfile.xml | 15 - .../publish/help/validate_texture_name.xml | 32 -- .../help/validate_texture_versions.xml | 35 -- .../help/validate_texture_workfiles.xml | 23 - .../publish/validate_editorial_resources.py | 28 - .../plugins/publish/validate_frame_ranges.py | 67 --- .../publish/validate_shot_duplicates.py | 31 - .../plugins/publish/validate_sources.py | 47 -- .../publish/validate_task_existence.py | 59 -- .../plugins/publish/validate_texture_batch.py | 28 - .../publish/validate_texture_has_workfile.py | 26 - .../plugins/publish/validate_texture_name.py | 63 --- .../publish/validate_texture_versions.py | 49 -- .../publish/validate_texture_workfiles.py | 58 -- client/ayon_core/plugins/publish/cleanup.py | 1 - .../plugins/publish/collect_audio.py | 1 - .../plugins/publish/extract_burnin.py | 1 - .../plugins/publish/extract_review.py | 1 - .../publish/extract_trim_video_audio.py | 2 +- .../publish/validate_editorial_asset_name.py | 1 - .../plugins/publish/validate_version.py | 2 +- .../settings/entities/enum_entity.py | 1 - .../tools/standalonepublish/__init__.py | 10 - .../ayon_core/tools/standalonepublish/app.py | 244 -------- .../tools/standalonepublish/publish.py | 27 - .../standalonepublish/widgets/__init__.py | 28 - .../standalonepublish/widgets/constants.py | 1 - .../standalonepublish/widgets/model_asset.py | 186 ------ .../widgets/model_filter_proxy_exact_match.py | 28 - .../model_filter_proxy_recursive_sort.py | 32 -- .../standalonepublish/widgets/model_node.py | 56 -- .../widgets/model_tasks_template.py | 66 --- .../standalonepublish/widgets/model_tree.py | 122 ---- .../widgets/model_tree_view_deselectable.py | 16 - .../widgets/resources/__init__.py | 14 - .../widgets/resources/edit.svg | 9 - .../widgets/resources/file.png | Bin 803 -> 0 bytes .../widgets/resources/files.png | Bin 484 -> 0 bytes .../widgets/resources/houdini.png | Bin 262950 -> 0 bytes .../widgets/resources/image_file.png | Bin 5118 -> 0 bytes .../widgets/resources/image_files.png | Bin 8560 -> 0 bytes .../widgets/resources/information.svg | 14 - .../widgets/resources/maya.png | Bin 41557 -> 0 bytes .../widgets/resources/menu.png | Bin 1629 -> 0 bytes .../widgets/resources/menu_disabled.png | Bin 1629 -> 0 bytes .../widgets/resources/menu_hover.png | Bin 1626 -> 0 bytes .../widgets/resources/menu_pressed.png | Bin 1626 -> 0 bytes .../widgets/resources/menu_pressed_hover.png | Bin 1488 -> 0 bytes .../widgets/resources/nuke.png | Bin 49012 -> 0 bytes .../widgets/resources/premiere.png | Bin 20121 -> 0 bytes .../widgets/resources/trash.png | Bin 1235 -> 0 bytes .../widgets/resources/trash_disabled.png | Bin 1235 -> 0 bytes .../widgets/resources/trash_hover.png | Bin 1232 -> 0 bytes .../widgets/resources/trash_pressed.png | Bin 1232 -> 0 bytes .../widgets/resources/trash_pressed_hover.png | Bin 1094 -> 0 bytes .../widgets/resources/video_file.png | Bin 120 -> 0 bytes .../standalonepublish/widgets/widget_asset.py | 443 --------------- .../widgets/widget_component_item.py | 534 ------------------ .../widgets/widget_components.py | 212 ------- .../widgets/widget_components_list.py | 91 --- .../widgets/widget_drop_empty.py | 47 -- .../widgets/widget_drop_frame.py | 485 ---------------- .../widgets/widget_family.py | 421 -------------- .../widgets/widget_family_desc.py | 96 ---- .../widgets/widget_shadow.py | 42 -- 91 files changed, 2 insertions(+), 6393 deletions(-) delete mode 100644 client/ayon_core/hosts/standalonepublisher/__init__.py delete mode 100644 client/ayon_core/hosts/standalonepublisher/addon.py delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_app_name.py delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_bulk_mov_instances.py delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_context.py delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_editorial.py delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_editorial_resources.py delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_harmony_scenes.py delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_harmony_zips.py delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_hierarchy.py delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_instance_data.py delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_matching_asset.py delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_remove_marked.py delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_representation_names.py delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_texture.py delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/extract_resources.py delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_editorial_resources.xml delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_frame_ranges.xml delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_shot_duplicates.xml delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_simple_texture_naming.xml delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_sources.xml delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_task_existence.xml delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_texture_batch.xml delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_texture_has_workfile.xml delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_texture_name.xml delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_texture_versions.xml delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_texture_workfiles.xml delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_editorial_resources.py delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_shot_duplicates.py delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_sources.py delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_task_existence.py delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_texture_batch.py delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_texture_has_workfile.py delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_texture_name.py delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py delete mode 100644 client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py delete mode 100644 client/ayon_core/tools/standalonepublish/__init__.py delete mode 100644 client/ayon_core/tools/standalonepublish/app.py delete mode 100644 client/ayon_core/tools/standalonepublish/publish.py delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/__init__.py delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/constants.py delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/model_asset.py delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/model_filter_proxy_exact_match.py delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/model_filter_proxy_recursive_sort.py delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/model_node.py delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/model_tasks_template.py delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/model_tree.py delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/model_tree_view_deselectable.py delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/resources/__init__.py delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/resources/edit.svg delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/resources/file.png delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/resources/files.png delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/resources/houdini.png delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/resources/image_file.png delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/resources/image_files.png delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/resources/information.svg delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/resources/maya.png delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/resources/menu.png delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/resources/menu_disabled.png delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/resources/menu_hover.png delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/resources/menu_pressed.png delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/resources/menu_pressed_hover.png delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/resources/nuke.png delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/resources/premiere.png delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/resources/trash.png delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/resources/trash_disabled.png delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/resources/trash_hover.png delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/resources/trash_pressed.png delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/resources/trash_pressed_hover.png delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/resources/video_file.png delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/widget_asset.py delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/widget_component_item.py delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/widget_components.py delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/widget_components_list.py delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/widget_drop_empty.py delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/widget_drop_frame.py delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/widget_family.py delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/widget_family_desc.py delete mode 100644 client/ayon_core/tools/standalonepublish/widgets/widget_shadow.py diff --git a/client/ayon_core/hosts/standalonepublisher/__init__.py b/client/ayon_core/hosts/standalonepublisher/__init__.py deleted file mode 100644 index f47fa6b573..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .addon import StandAlonePublishAddon - - -__all__ = ( - "StandAlonePublishAddon", -) diff --git a/client/ayon_core/hosts/standalonepublisher/addon.py b/client/ayon_core/hosts/standalonepublisher/addon.py deleted file mode 100644 index c357a65617..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/addon.py +++ /dev/null @@ -1,59 +0,0 @@ -import os - -from ayon_core.lib import get_openpype_execute_args -from ayon_core.lib.execute import run_detached_process -from ayon_core.modules import ( - click_wrap, - OpenPypeModule, - ITrayAction, - IHostAddon, -) - -STANDALONEPUBLISH_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) - - -class StandAlonePublishAddon(OpenPypeModule, ITrayAction, IHostAddon): - label = "Publisher (legacy)" - name = "standalonepublisher" - host_name = "standalonepublisher" - - def initialize(self, modules_settings): - self.enabled = modules_settings["standalonepublish_tool"]["enabled"] - self.publish_paths = [ - os.path.join(STANDALONEPUBLISH_ROOT_DIR, "plugins", "publish") - ] - - def tray_init(self): - return - - def on_action_trigger(self): - self.run_standalone_publisher() - - def connect_with_modules(self, enabled_modules): - """Collect publish paths from other modules.""" - - publish_paths = self.manager.collect_plugin_paths()["publish"] - self.publish_paths.extend(publish_paths) - - def run_standalone_publisher(self): - args = get_openpype_execute_args("module", self.name, "launch") - run_detached_process(args) - - def cli(self, click_group): - click_group.add_command(cli_main.to_click_obj()) - - -@click_wrap.group( - StandAlonePublishAddon.name, - help="StandalonePublisher related commands.") -def cli_main(): - pass - - -@cli_main.command() -def launch(): - """Launch StandalonePublisher tool UI.""" - - from ayon_core.tools import standalonepublish - - standalonepublish.main() diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_app_name.py b/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_app_name.py deleted file mode 100644 index 857f3dca20..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_app_name.py +++ /dev/null @@ -1,13 +0,0 @@ -import pyblish.api - - -class CollectSAAppName(pyblish.api.ContextPlugin): - """Collect app name and label.""" - - label = "Collect App Name/Label" - order = pyblish.api.CollectorOrder - 0.5 - hosts = ["standalonepublisher"] - - def process(self, context): - context.data["appName"] = "standalone publisher" - context.data["appLabel"] = "Standalone publisher" diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_bulk_mov_instances.py b/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_bulk_mov_instances.py deleted file mode 100644 index 019ab95fa7..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_bulk_mov_instances.py +++ /dev/null @@ -1,91 +0,0 @@ -import copy -import json -import pyblish.api - -from ayon_core.client import get_asset_by_name -from ayon_core.pipeline.create import get_subset_name - - -class CollectBulkMovInstances(pyblish.api.InstancePlugin): - """Collect all available instances for batch publish.""" - - label = "Collect Bulk Mov Instances" - order = pyblish.api.CollectorOrder + 0.489 - hosts = ["standalonepublisher"] - families = ["render_mov_batch"] - - new_instance_family = "render" - instance_task_names = [ - "compositing", - "comp" - ] - default_task_name = "compositing" - subset_name_variant = "Default" - - def process(self, instance): - context = instance.context - project_name = context.data["projectEntity"]["name"] - asset_name = instance.data["asset"] - asset_doc = get_asset_by_name(project_name, asset_name) - if not asset_doc: - raise AssertionError(( - "Couldn't find Asset document with name \"{}\"" - ).format(asset_name)) - - available_task_names = {} - asset_tasks = asset_doc.get("data", {}).get("tasks") or {} - for task_name in asset_tasks.keys(): - available_task_names[task_name.lower()] = task_name - - task_name = self.default_task_name - for _task_name in self.instance_task_names: - _task_name_low = _task_name.lower() - if _task_name_low in available_task_names: - task_name = available_task_names[_task_name_low] - break - - subset_name = get_subset_name( - self.new_instance_family, - self.subset_name_variant, - task_name, - asset_doc, - project_name, - host_name=context.data["hostName"], - project_settings=context.data["project_settings"] - ) - instance_name = f"{asset_name}_{subset_name}" - - # create new instance - new_instance = context.create_instance(instance_name) - new_instance_data = { - "name": instance_name, - "label": instance_name, - "family": self.new_instance_family, - "subset": subset_name, - "task": task_name - } - new_instance.data.update(new_instance_data) - # add original instance data except name key - for key, value in instance.data.items(): - if key in new_instance_data: - continue - # Make sure value is copy since value may be object which - # can be shared across all new created objects - new_instance.data[key] = copy.deepcopy(value) - - # Add `render_mov_batch` for specific validators - if "families" not in new_instance.data: - new_instance.data["families"] = [] - new_instance.data["families"].append("render_mov_batch") - - # delete original instance - context.remove(instance) - - self.log.info(f"Created new instance: {instance_name}") - - def converter(value): - return str(value) - - self.log.debug("Instance data: {}".format( - json.dumps(new_instance.data, indent=4, default=converter) - )) diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_context.py b/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_context.py deleted file mode 100644 index c51bd8722a..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_context.py +++ /dev/null @@ -1,276 +0,0 @@ -""" -Requires: - environment -> SAPUBLISH_INPATH - environment -> SAPUBLISH_OUTPATH - -Provides: - context -> returnJsonPath (str) - context -> project - context -> asset - instance -> destination_list (list) - instance -> representations (list) - instance -> source (list) - instance -> representations -""" - -import os -import json -import copy -from pprint import pformat -import clique -import pyblish.api - -from ayon_core.pipeline import legacy_io - - -class CollectContextDataSAPublish(pyblish.api.ContextPlugin): - """ - Collecting temp json data sent from a host context - and path for returning json data back to hostself. - """ - - label = "Collect Context - SA Publish" - order = pyblish.api.CollectorOrder - 0.49 - hosts = ["standalonepublisher"] - - # presets - batch_extensions = ["edl", "xml", "psd"] - - def process(self, context): - # get json paths from os and load them - legacy_io.install() - - # get json file context - input_json_path = os.environ.get("SAPUBLISH_INPATH") - - with open(input_json_path, "r") as f: - in_data = json.load(f) - self.log.debug(f"_ in_data: {pformat(in_data)}") - - self.add_files_to_ignore_cleanup(in_data, context) - # exception for editorial - if in_data["family"] == "render_mov_batch": - in_data_list = self.prepare_mov_batch_instances(in_data) - - elif in_data["family"] in ["editorial", "background_batch"]: - in_data_list = self.multiple_instances(context, in_data) - - else: - in_data_list = [in_data] - - self.log.debug(f"_ in_data_list: {pformat(in_data_list)}") - - for in_data in in_data_list: - # create instance - self.create_instance(context, in_data) - - def add_files_to_ignore_cleanup(self, in_data, context): - all_filepaths = context.data.get("skipCleanupFilepaths") or [] - for repre in in_data["representations"]: - files = repre["files"] - if not isinstance(files, list): - files = [files] - - dirpath = repre["stagingDir"] - for filename in files: - filepath = os.path.normpath(os.path.join(dirpath, filename)) - if filepath not in all_filepaths: - all_filepaths.append(filepath) - - context.data["skipCleanupFilepaths"] = all_filepaths - - def multiple_instances(self, context, in_data): - # avoid subset name duplicity - if not context.data.get("subsetNamesCheck"): - context.data["subsetNamesCheck"] = list() - - in_data_list = list() - representations = in_data.pop("representations") - for repr in representations: - in_data_copy = copy.deepcopy(in_data) - ext = repr["ext"][1:] - subset = in_data_copy["subset"] - # filter out non editorial files - if ext not in self.batch_extensions: - in_data_copy["representations"] = [repr] - in_data_copy["subset"] = f"{ext}{subset}" - in_data_list.append(in_data_copy) - - files = repr.get("files") - - # delete unneeded keys - delete_repr_keys = ["frameStart", "frameEnd"] - for k in delete_repr_keys: - if repr.get(k): - repr.pop(k) - - # convert files to list if it isn't - if not isinstance(files, (tuple, list)): - files = [files] - - self.log.debug(f"_ files: {files}") - for index, f in enumerate(files): - index += 1 - # copy dictionaries - in_data_copy = copy.deepcopy(in_data_copy) - repr_new = copy.deepcopy(repr) - - repr_new["files"] = f - repr_new["name"] = ext - in_data_copy["representations"] = [repr_new] - - # create subset Name - new_subset = f"{ext}{index}{subset}" - while new_subset in context.data["subsetNamesCheck"]: - index += 1 - new_subset = f"{ext}{index}{subset}" - - context.data["subsetNamesCheck"].append(new_subset) - in_data_copy["subset"] = new_subset - in_data_list.append(in_data_copy) - self.log.info(f"Creating subset: {ext}{index}{subset}") - - return in_data_list - - def prepare_mov_batch_instances(self, in_data): - """Copy of `multiple_instances` method. - - Method was copied because `batch_extensions` is used in - `multiple_instances` but without any family filtering. Since usage - of the filtering is unknown and modification of that part may break - editorial or PSD batch publishing it was decided to create a copy with - this family specific filtering. Also "frameStart" and "frameEnd" keys - are removed from instance which is needed for this processing. - - Instance data will also care about families. - - TODO: - - Merge possible logic with `multiple_instances` method. - """ - self.log.info("Preparing data for mov batch processing.") - in_data_list = [] - - representations = in_data.pop("representations") - for repre in representations: - self.log.debug("Processing representation with files {}".format( - str(repre["files"]) - )) - ext = repre["ext"][1:] - - # Rename representation name - repre_name = repre["name"] - if repre_name.startswith(ext + "_"): - repre["name"] = ext - # Skip files that are not available for mov batch publishing - # TODO add dynamic expected extensions by family from `in_data` - # - with this modification it would be possible to use only - # `multiple_instances` method - expected_exts = ["mov"] - if ext not in expected_exts: - self.log.warning(( - "Skipping representation." - " Does not match expected extensions <{}>. {}" - ).format(", ".join(expected_exts), str(repre))) - continue - - files = repre["files"] - # Convert files to list if it isn't - if not isinstance(files, (tuple, list)): - files = [files] - - # Loop through files and create new instance per each file - for filename in files: - # Create copy of representation and change it's files and name - new_repre = copy.deepcopy(repre) - new_repre["files"] = filename - new_repre["name"] = ext - new_repre["thumbnail"] = True - - if "tags" not in new_repre: - new_repre["tags"] = [] - new_repre["tags"].append("review") - - # Prepare new subset name (temporary name) - # - subset name will be changed in batch specific plugins - new_subset_name = "{}{}".format( - in_data["subset"], - os.path.basename(filename) - ) - # Create copy of instance data as new instance and pass in new - # representation - in_data_copy = copy.deepcopy(in_data) - in_data_copy["representations"] = [new_repre] - in_data_copy["subset"] = new_subset_name - if "families" not in in_data_copy: - in_data_copy["families"] = [] - in_data_copy["families"].append("review") - - in_data_list.append(in_data_copy) - - return in_data_list - - def create_instance(self, context, in_data): - subset = in_data["subset"] - # If instance data already contain families then use it - instance_families = in_data.get("families") or [] - - instance = context.create_instance(subset) - instance.data.update( - { - "subset": subset, - "asset": in_data["asset"], - "label": subset, - "name": subset, - "family": in_data["family"], - "frameStart": in_data.get("representations", [None])[0].get( - "frameStart", None - ), - "frameEnd": in_data.get("representations", [None])[0].get( - "frameEnd", None - ), - "families": instance_families - } - ) - # Fill version only if 'use_next_available_version' is disabled - # and version is filled in instance data - version = in_data.get("version") - use_next_available_version = in_data.get( - "use_next_available_version", True) - if not use_next_available_version and version is not None: - instance.data["version"] = version - - self.log.info("collected instance: {}".format(pformat(instance.data))) - self.log.info("parsing data: {}".format(pformat(in_data))) - - instance.data["destination_list"] = list() - instance.data["representations"] = list() - instance.data["source"] = "standalone publisher" - - for component in in_data["representations"]: - component["destination"] = component["files"] - component["stagingDir"] = component["stagingDir"] - - if isinstance(component["files"], list): - collections, _remainder = clique.assemble(component["files"]) - self.log.debug("collecting sequence: {}".format(collections)) - instance.data["frameStart"] = int(component["frameStart"]) - instance.data["frameEnd"] = int(component["frameEnd"]) - if component.get("fps"): - instance.data["fps"] = int(component["fps"]) - - ext = component["ext"] - if ext.startswith("."): - component["ext"] = ext[1:] - - # Remove 'preview' key from representation data - preview = component.pop("preview") - if preview: - instance.data["families"].append("review") - component["tags"] = ["review"] - self.log.debug("Adding review family") - - if "psd" in component["name"]: - instance.data["source"] = component["files"] - self.log.debug("Adding image:background_batch family") - - instance.data["representations"].append(component) diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_editorial.py b/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_editorial.py deleted file mode 100644 index 6a78ae093a..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_editorial.py +++ /dev/null @@ -1,125 +0,0 @@ -""" -Optional: - presets -> extensions ( - example of use: - ["mov", "mp4"] - ) - presets -> source_dir ( - example of use: - "C:/pathToFolder" - "{root}/{project[name]}/inputs" - "{root[work]}/{project[name]}/inputs" - "./input" - "../input" - "" - ) -""" - -import os -import opentimelineio as otio -import pyblish.api -from ayon_core import lib as plib -from ayon_core.pipeline.context_tools import get_current_project_asset - - -class OTIO_View(pyblish.api.Action): - """Currently disabled because OTIO requires PySide2. Issue on Qt.py: - https://github.com/PixarAnimationStudios/OpenTimelineIO/issues/289 - """ - - label = "OTIO View" - icon = "wrench" - on = "failed" - - def process(self, context, plugin): - instance = context[0] - representation = instance.data["representations"][0] - file_path = os.path.join( - representation["stagingDir"], representation["files"] - ) - plib.run_subprocess(["otioview", file_path]) - - -class CollectEditorial(pyblish.api.InstancePlugin): - """Collect Editorial OTIO timeline""" - - order = pyblish.api.CollectorOrder - label = "Collect Editorial" - hosts = ["standalonepublisher"] - families = ["editorial"] - actions = [] - - # presets - extensions = ["mov", "mp4"] - source_dir = None - - def process(self, instance): - root_dir = None - # remove context test attribute - if instance.context.data.get("subsetNamesCheck"): - instance.context.data.pop("subsetNamesCheck") - - self.log.debug(f"__ instance: `{instance}`") - # get representation with editorial file - for representation in instance.data["representations"]: - self.log.debug(f"__ representation: `{representation}`") - # make editorial sequence file path - staging_dir = representation["stagingDir"] - file_path = os.path.join( - staging_dir, str(representation["files"]) - ) - instance.context.data["currentFile"] = file_path - - # get video file path - video_path = None - basename = os.path.splitext(os.path.basename(file_path))[0] - - if self.source_dir != "": - source_dir = self.source_dir.replace("\\", "/") - if ("./" in source_dir) or ("../" in source_dir): - # get current working dir - cwd = os.getcwd() - # set cwd to staging dir for absolute path solving - os.chdir(staging_dir) - root_dir = os.path.abspath(source_dir) - # set back original cwd - os.chdir(cwd) - elif "{" in source_dir: - root_dir = source_dir - else: - root_dir = os.path.normpath(source_dir) - - if root_dir: - # search for source data will need to be done - instance.data["editorialSourceRoot"] = root_dir - instance.data["editorialSourcePath"] = None - else: - # source data are already found - for f in os.listdir(staging_dir): - # filter out by not sharing the same name - if os.path.splitext(f)[0] not in basename: - continue - # filter out by respected extensions - if os.path.splitext(f)[1][1:] not in self.extensions: - continue - video_path = os.path.join( - staging_dir, f - ) - self.log.debug(f"__ video_path: `{video_path}`") - instance.data["editorialSourceRoot"] = staging_dir - instance.data["editorialSourcePath"] = video_path - - instance.data["stagingDir"] = staging_dir - - # get editorial sequence file into otio timeline object - extension = os.path.splitext(file_path)[1] - kwargs = {} - if extension == ".edl": - # EDL has no frame rate embedded so needs explicit - # frame rate else 24 is assumed. - kwargs["rate"] = get_current_project_asset()["data"]["fps"] - - instance.data["otio_timeline"] = otio.adapters.read_from_file( - file_path, **kwargs) - - self.log.info(f"Added OTIO timeline from: `{file_path}`") diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py b/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py deleted file mode 100644 index 3aa59dba75..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py +++ /dev/null @@ -1,214 +0,0 @@ -import os -from copy import deepcopy - -import opentimelineio as otio -import pyblish.api - -from ayon_core import lib as plib -from ayon_core.pipeline.context_tools import get_current_project_asset - - -class CollectInstances(pyblish.api.InstancePlugin): - """Collect instances from editorial's OTIO sequence""" - - order = pyblish.api.CollectorOrder + 0.01 - label = "Collect Editorial Instances" - hosts = ["standalonepublisher"] - families = ["editorial"] - - # presets - subsets = { - "referenceMain": { - "family": "review", - "families": ["clip"], - "extensions": ["mp4"] - }, - "audioMain": { - "family": "audio", - "families": ["clip"], - "extensions": ["wav"], - } - } - timeline_frame_start = 900000 # starndard edl default (10:00:00:00) - timeline_frame_offset = None - custom_start_frame = None - - def process(self, instance): - # get context - context = instance.context - - instance_data_filter = [ - "editorialSourceRoot", - "editorialSourcePath" - ] - - # attribute for checking duplicity during creation - if not context.data.get("assetNameCheck"): - context.data["assetNameCheck"] = list() - - # create asset_names conversion table - if not context.data.get("assetsShared"): - context.data["assetsShared"] = dict() - - # get timeline otio data - timeline = instance.data["otio_timeline"] - fps = get_current_project_asset()["data"]["fps"] - - tracks = timeline.each_child( - descended_from_type=otio.schema.Track - ) - - # get data from avalon - asset_entity = instance.context.data["assetEntity"] - asset_data = asset_entity["data"] - asset_name = asset_entity["name"] - - # Timeline data. - handle_start = int(asset_data["handleStart"]) - handle_end = int(asset_data["handleEnd"]) - - for track in tracks: - self.log.debug(f"track.name: {track.name}") - try: - track_start_frame = ( - abs(track.source_range.start_time.value) - ) - self.log.debug(f"track_start_frame: {track_start_frame}") - track_start_frame -= self.timeline_frame_start - except AttributeError: - track_start_frame = 0 - - self.log.debug(f"track_start_frame: {track_start_frame}") - - for clip in track.each_child(): - if clip.name is None: - continue - - if isinstance(clip, otio.schema.Gap): - continue - - # skip all generators like black empty - if isinstance( - clip.media_reference, - otio.schema.GeneratorReference): - continue - - # Transitions are ignored, because Clips have the full frame - # range. - if isinstance(clip, otio.schema.Transition): - continue - - # basic unique asset name - clip_name = os.path.splitext(clip.name)[0].lower() - name = f"{asset_name.split('_')[0]}_{clip_name}" - - if name not in context.data["assetNameCheck"]: - context.data["assetNameCheck"].append(name) - else: - self.log.warning(f"duplicate shot name: {name}") - - # frame ranges data - clip_in = clip.range_in_parent().start_time.value - clip_in += track_start_frame - clip_out = clip.range_in_parent().end_time_inclusive().value - clip_out += track_start_frame - self.log.info(f"clip_in: {clip_in} | clip_out: {clip_out}") - - # add offset in case there is any - if self.timeline_frame_offset: - clip_in += self.timeline_frame_offset - clip_out += self.timeline_frame_offset - - clip_duration = clip.duration().value - self.log.info(f"clip duration: {clip_duration}") - - source_in = clip.trimmed_range().start_time.value - source_out = source_in + clip_duration - source_in_h = source_in - handle_start - source_out_h = source_out + handle_end - - clip_in_h = clip_in - handle_start - clip_out_h = clip_out + handle_end - - # define starting frame for future shot - if self.custom_start_frame is not None: - frame_start = self.custom_start_frame - else: - frame_start = clip_in - - frame_end = frame_start + (clip_duration - 1) - - # create shared new instance data - instance_data = { - # shared attributes - "asset": name, - "assetShareName": name, - "item": clip, - "clipName": clip_name, - - # parent time properties - "trackStartFrame": track_start_frame, - "handleStart": handle_start, - "handleEnd": handle_end, - "fps": fps, - - # media source - "sourceIn": source_in, - "sourceOut": source_out, - "sourceInH": source_in_h, - "sourceOutH": source_out_h, - - # timeline - "clipIn": clip_in, - "clipOut": clip_out, - "clipDuration": clip_duration, - "clipInH": clip_in_h, - "clipOutH": clip_out_h, - "clipDurationH": clip_duration + handle_start + handle_end, - - # task - "frameStart": frame_start, - "frameEnd": frame_end, - "frameStartH": frame_start - handle_start, - "frameEndH": frame_end + handle_end, - "newAssetPublishing": True - } - - for data_key in instance_data_filter: - instance_data.update({ - data_key: instance.data.get(data_key)}) - - # adding subsets to context as instances - self.subsets.update({ - "shotMain": { - "family": "shot", - "families": [] - } - }) - for subset, properties in self.subsets.items(): - version = properties.get("version") - if version == 0: - properties.pop("version") - - # adding Review-able instance - subset_instance_data = deepcopy(instance_data) - subset_instance_data.update(deepcopy(properties)) - subset_instance_data.update({ - # unique attributes - "name": f"{name}_{subset}", - "label": f"{name} {subset} ({clip_in}-{clip_out})", - "subset": subset - }) - # create new instance - _instance = instance.context.create_instance( - **subset_instance_data) - self.log.debug( - f"Instance: `{_instance}` | " - f"families: `{subset_instance_data['families']}`") - - context.data["assetsShared"][name] = { - "_clipIn": clip_in, - "_clipOut": clip_out - } - - self.log.debug("Instance: `{}` | families: `{}`") diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_editorial_resources.py b/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_editorial_resources.py deleted file mode 100644 index 4d7a13fcf2..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_editorial_resources.py +++ /dev/null @@ -1,271 +0,0 @@ -import os -import re -import tempfile -import pyblish.api -from copy import deepcopy -import clique - - -class CollectInstanceResources(pyblish.api.InstancePlugin): - """Collect instance's resources""" - - # must be after `CollectInstances` - order = pyblish.api.CollectorOrder + 0.011 - label = "Collect Editorial Resources" - hosts = ["standalonepublisher"] - families = ["clip"] - - def process(self, instance): - self.context = instance.context - self.log.info(f"Processing instance: {instance}") - self.new_instances = [] - subset_files = dict() - subset_dirs = list() - anatomy = self.context.data["anatomy"] - anatomy_data = deepcopy(self.context.data["anatomyData"]) - anatomy_data.update({"root": anatomy.roots}) - - subset = instance.data["subset"] - clip_name = instance.data["clipName"] - - editorial_source_root = instance.data["editorialSourceRoot"] - editorial_source_path = instance.data["editorialSourcePath"] - - # if `editorial_source_path` then loop through - if editorial_source_path: - # add family if mov or mp4 found which is longer for - # cutting `trimming` to enable `ExtractTrimmingVideoAudio` plugin - staging_dir = os.path.normpath( - tempfile.mkdtemp(prefix="pyblish_tmp_") - ) - instance.data["stagingDir"] = staging_dir - instance.data["families"] += ["trimming"] - return - - # if template pattern in path then fill it with `anatomy_data` - if "{" in editorial_source_root: - editorial_source_root = editorial_source_root.format( - **anatomy_data) - - self.log.debug(f"root: {editorial_source_root}") - # loop `editorial_source_root` and find clip name in folders - # and look for any subset name alternatives - for root, dirs, _files in os.walk(editorial_source_root): - # search only for directories related to clip name - correct_clip_dir = None - for _d_search in dirs: - # avoid all non clip dirs - if _d_search not in clip_name: - continue - # found correct dir for clip - correct_clip_dir = _d_search - - # continue if clip dir was not found - if not correct_clip_dir: - continue - - clip_dir_path = os.path.join(root, correct_clip_dir) - subset_files_items = list() - # list content of clip dir and search for subset items - for subset_item in os.listdir(clip_dir_path): - # avoid all items which are not defined as subsets by name - if subset not in subset_item: - continue - - subset_item_path = os.path.join( - clip_dir_path, subset_item) - # if it is dir store it to `subset_dirs` list - if os.path.isdir(subset_item_path): - subset_dirs.append(subset_item_path) - - # if it is file then store it to `subset_files` list - if os.path.isfile(subset_item_path): - subset_files_items.append(subset_item_path) - - if subset_files_items: - subset_files.update({clip_dir_path: subset_files_items}) - - # break the loop if correct_clip_dir was captured - # no need to cary on if correct folder was found - if correct_clip_dir: - break - - if subset_dirs: - # look all dirs and check for subset name alternatives - for _dir in subset_dirs: - instance_data = deepcopy( - {k: v for k, v in instance.data.items()}) - sub_dir = os.path.basename(_dir) - # if subset name is only alternative then create new instance - if sub_dir != subset: - instance_data = self.duplicate_instance( - instance_data, subset, sub_dir) - - # create all representations - self.create_representations( - os.listdir(_dir), instance_data, _dir) - - if sub_dir == subset: - self.new_instances.append(instance_data) - # instance.data.update(instance_data) - - if subset_files: - unique_subset_names = list() - root_dir = list(subset_files.keys()).pop() - files_list = subset_files[root_dir] - search_pattern = f"({subset}[A-Za-z0-9]+)(?=[\\._\\s])" - for _file in files_list: - pattern = re.compile(search_pattern) - match = pattern.findall(_file) - if not match: - continue - match_subset = match.pop() - if match_subset in unique_subset_names: - continue - unique_subset_names.append(match_subset) - - self.log.debug(f"unique_subset_names: {unique_subset_names}") - - for _un_subs in unique_subset_names: - instance_data = self.duplicate_instance( - instance.data, subset, _un_subs) - - # create all representations - self.create_representations( - [os.path.basename(f) for f in files_list - if _un_subs in f], - instance_data, root_dir) - - # remove the original instance as it had been used only - # as template and is duplicated - self.context.remove(instance) - - # create all instances in self.new_instances into context - for new_instance in self.new_instances: - _new_instance = self.context.create_instance( - new_instance["name"]) - _new_instance.data.update(new_instance) - - def duplicate_instance(self, instance_data, subset, new_subset): - - new_instance_data = dict() - for _key, _value in instance_data.items(): - new_instance_data[_key] = _value - if not isinstance(_value, str): - continue - if subset in _value: - new_instance_data[_key] = _value.replace( - subset, new_subset) - - self.log.info(f"Creating new instance: {new_instance_data['name']}") - self.new_instances.append(new_instance_data) - return new_instance_data - - def create_representations( - self, files_list, instance_data, staging_dir): - """ Create representations from Collection object - """ - # collecting frames for later frame start/end reset - frames = list() - # break down Collection object to collections and reminders - collections, remainder = clique.assemble(files_list) - # add staging_dir to instance_data - instance_data["stagingDir"] = staging_dir - # add representations to instance_data - instance_data["representations"] = list() - - collection_head_name = None - # loop through collections and create representations - for _collection in collections: - ext = _collection.tail[1:] - collection_head_name = _collection.head - frame_start = list(_collection.indexes)[0] - frame_end = list(_collection.indexes)[-1] - repre_data = { - "frameStart": frame_start, - "frameEnd": frame_end, - "name": ext, - "ext": ext, - "files": [item for item in _collection], - "stagingDir": staging_dir - } - - if instance_data.get("keepSequence"): - repre_data_keep = deepcopy(repre_data) - instance_data["representations"].append(repre_data_keep) - - if "review" in instance_data["families"]: - repre_data.update({ - "thumbnail": True, - "frameStartFtrack": frame_start, - "frameEndFtrack": frame_end, - "step": 1, - "fps": self.context.data.get("fps"), - "name": "review", - "tags": ["review", "ftrackreview", "delete"], - }) - instance_data["representations"].append(repre_data) - - # add to frames for frame range reset - frames.append(frame_start) - frames.append(frame_end) - - # loop through reminders and create representations - for _reminding_file in remainder: - ext = os.path.splitext(_reminding_file)[-1][1:] - if ext not in instance_data["extensions"]: - continue - if collection_head_name and ( - (collection_head_name + ext) not in _reminding_file - ) and (ext in ["mp4", "mov"]): - self.log.info(f"Skipping file: {_reminding_file}") - continue - frame_start = 1 - frame_end = 1 - - repre_data = { - "name": ext, - "ext": ext, - "files": _reminding_file, - "stagingDir": staging_dir - } - - # exception for thumbnail - if "thumb" in _reminding_file: - repre_data.update({ - 'name': "thumbnail", - 'thumbnail': True - }) - - # exception for mp4 preview - if ext in ["mp4", "mov"]: - frame_start = 0 - frame_end = ( - (instance_data["frameEnd"] - instance_data["frameStart"]) - + 1) - # add review ftrack family into families - for _family in ["review", "ftrack"]: - if _family not in instance_data["families"]: - instance_data["families"].append(_family) - repre_data.update({ - "frameStart": frame_start, - "frameEnd": frame_end, - "frameStartFtrack": frame_start, - "frameEndFtrack": frame_end, - "step": 1, - "fps": self.context.data.get("fps"), - "name": "review", - "thumbnail": True, - "tags": ["review", "ftrackreview", "delete"], - }) - - # add to frames for frame range reset only if no collection - if not collections: - frames.append(frame_start) - frames.append(frame_end) - - instance_data["representations"].append(repre_data) - - # reset frame start / end - instance_data["frameStart"] = min(frames) - instance_data["frameEnd"] = max(frames) diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_harmony_scenes.py b/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_harmony_scenes.py deleted file mode 100644 index c435ca2096..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_harmony_scenes.py +++ /dev/null @@ -1,102 +0,0 @@ -# -*- coding: utf-8 -*- -"""Collect Harmony scenes in Standalone Publisher.""" -import copy -import glob -import os -from pprint import pformat - -import pyblish.api - - -class CollectHarmonyScenes(pyblish.api.InstancePlugin): - """Collect Harmony xstage files.""" - - order = pyblish.api.CollectorOrder + 0.498 - label = "Collect Harmony Scene" - hosts = ["standalonepublisher"] - families = ["harmony.scene"] - - # presets - ignored_instance_data_keys = ("name", "label", "stagingDir", "version") - - def process(self, instance): - """Plugin entry point.""" - context = instance.context - asset_data = instance.context.data["assetEntity"] - asset_name = instance.data["asset"] - subset_name = instance.data.get("subset", "sceneMain") - anatomy_data = instance.context.data["anatomyData"] - repres = instance.data["representations"] - staging_dir = repres[0]["stagingDir"] - files = repres[0]["files"] - - if not files.endswith(".zip"): - # A harmony project folder / .xstage was dropped - instance_name = f"{asset_name}_{subset_name}" - task = instance.data.get("task", "harmonyIngest") - - # create new instance - new_instance = context.create_instance(instance_name) - - # add original instance data except name key - for key, value in instance.data.items(): - # Make sure value is copy since value may be object which - # can be shared across all new created objects - if key not in self.ignored_instance_data_keys: - new_instance.data[key] = copy.deepcopy(value) - - self.log.info("Copied data: {}".format(new_instance.data)) - - # fix anatomy data - anatomy_data_new = copy.deepcopy(anatomy_data) - - project_entity = context.data["projectEntity"] - asset_entity = context.data["assetEntity"] - - task_type = asset_entity["data"]["tasks"].get(task, {}).get("type") - project_task_types = project_entity["config"]["tasks"] - task_code = project_task_types.get(task_type, {}).get("short_name") - - # updating hierarchy data - anatomy_data_new.update({ - "asset": asset_data["name"], - "folder": { - "name": asset_data["name"], - }, - "task": { - "name": task, - "type": task_type, - "short": task_code, - }, - "subset": subset_name - }) - - new_instance.data["label"] = f"{instance_name}" - new_instance.data["subset"] = subset_name - new_instance.data["extension"] = ".zip" - new_instance.data["anatomyData"] = anatomy_data_new - new_instance.data["publish"] = True - - # When a project folder was dropped vs. just an xstage file, find - # the latest file xstage version and update the instance - if not files.endswith(".xstage"): - - source_dir = os.path.join( - staging_dir, files - ).replace("\\", "/") - - latest_file = max(glob.iglob(source_dir + "/*.xstage"), - key=os.path.getctime).replace("\\", "/") - - new_instance.data["representations"][0]["stagingDir"] = ( - source_dir - ) - new_instance.data["representations"][0]["files"] = ( - os.path.basename(latest_file) - ) - self.log.info(f"Created new instance: {instance_name}") - self.log.debug(f"_ inst_data: {pformat(new_instance.data)}") - - # set original instance for removal - self.log.info("Context data: {}".format(context.data)) - instance.data["remove"] = True diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_harmony_zips.py b/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_harmony_zips.py deleted file mode 100644 index d90215e767..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_harmony_zips.py +++ /dev/null @@ -1,82 +0,0 @@ -# -*- coding: utf-8 -*- -"""Collect zips as Harmony scene files.""" -import copy -from pprint import pformat - -import pyblish.api - - -class CollectHarmonyZips(pyblish.api.InstancePlugin): - """Collect Harmony zipped projects.""" - - order = pyblish.api.CollectorOrder + 0.497 - label = "Collect Harmony Zipped Projects" - hosts = ["standalonepublisher"] - families = ["harmony.scene"] - extensions = ["zip"] - - # presets - ignored_instance_data_keys = ("name", "label", "stagingDir", "version") - - def process(self, instance): - """Plugin entry point.""" - context = instance.context - asset_data = instance.context.data["assetEntity"] - asset_name = instance.data["asset"] - subset_name = instance.data.get("subset", "sceneMain") - anatomy_data = instance.context.data["anatomyData"] - repres = instance.data["representations"] - files = repres[0]["files"] - project_entity = context.data["projectEntity"] - - if files.endswith(".zip"): - # A zip file was dropped - instance_name = f"{asset_name}_{subset_name}" - task = instance.data.get("task", "harmonyIngest") - - # create new instance - new_instance = context.create_instance(instance_name) - - # add original instance data except name key - for key, value in instance.data.items(): - # Make sure value is copy since value may be object which - # can be shared across all new created objects - if key not in self.ignored_instance_data_keys: - new_instance.data[key] = copy.deepcopy(value) - - self.log.info("Copied data: {}".format(new_instance.data)) - - task_type = asset_data["data"]["tasks"].get(task, {}).get("type") - project_task_types = project_entity["config"]["tasks"] - task_code = project_task_types.get(task_type, {}).get("short_name") - - # fix anatomy data - anatomy_data_new = copy.deepcopy(anatomy_data) - # updating hierarchy data - anatomy_data_new.update( - { - "asset": asset_data["name"], - "folder": { - "name": asset_data["name"], - }, - "task": { - "name": task, - "type": task_type, - "short": task_code, - }, - "subset": subset_name - } - ) - - new_instance.data["label"] = f"{instance_name}" - new_instance.data["subset"] = subset_name - new_instance.data["extension"] = ".zip" - new_instance.data["anatomyData"] = anatomy_data_new - new_instance.data["publish"] = True - - self.log.info(f"Created new instance: {instance_name}") - self.log.debug(f"_ inst_data: {pformat(new_instance.data)}") - - # set original instance for removal - self.log.info("Context data: {}".format(context.data)) - instance.data["remove"] = True diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_hierarchy.py b/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_hierarchy.py deleted file mode 100644 index 244c38695c..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_hierarchy.py +++ /dev/null @@ -1,304 +0,0 @@ -import os -from pprint import pformat -import re -from copy import deepcopy -import pyblish.api - -from ayon_core.client import get_asset_by_id - - -class CollectHierarchyInstance(pyblish.api.ContextPlugin): - """Collecting hierarchy context from `parents` and `hierarchy` data - present in `clip` family instances coming from the request json data file - - It will add `hierarchical_context` into each instance for integrate - plugins to be able to create needed parents for the context if they - don't exist yet - """ - - label = "Collect Hierarchy Clip" - order = pyblish.api.CollectorOrder + 0.101 - hosts = ["standalonepublisher"] - families = ["shot"] - - # presets - shot_rename = True - shot_rename_template = None - shot_rename_search_patterns = None - shot_add_hierarchy = None - shot_add_tasks = None - - def convert_to_entity(self, key, value): - # ftrack compatible entity types - types = {"shot": "Shot", - "folder": "Folder", - "episode": "Episode", - "sequence": "Sequence", - "track": "Sequence", - } - # convert to entity type - entity_type = types.get(key, None) - - # return if any - if entity_type: - return {"entity_type": entity_type, "entity_name": value} - - def rename_with_hierarchy(self, instance): - search_text = "" - parent_name = instance.context.data["assetEntity"]["name"] - clip = instance.data["item"] - clip_name = os.path.splitext(clip.name)[0].lower() - if self.shot_rename_search_patterns and self.shot_rename: - search_text += parent_name + clip_name - instance.data["anatomyData"].update({"clip_name": clip_name}) - for type, pattern in self.shot_rename_search_patterns.items(): - p = re.compile(pattern) - match = p.findall(search_text) - if not match: - continue - instance.data["anatomyData"][type] = match[-1] - - # format to new shot name - instance.data["asset"] = self.shot_rename_template.format( - **instance.data["anatomyData"]) - - def create_hierarchy(self, instance): - asset_doc = instance.context.data["assetEntity"] - project_doc = instance.context.data["projectEntity"] - project_name = project_doc["name"] - visual_hierarchy = [asset_doc] - current_doc = asset_doc - while True: - visual_parent_id = current_doc["data"]["visualParent"] - visual_parent = None - if visual_parent_id: - visual_parent = get_asset_by_id(project_name, visual_parent_id) - - if not visual_parent: - visual_hierarchy.append(project_doc) - break - visual_hierarchy.append(visual_parent) - current_doc = visual_parent - - # add current selection context hierarchy from standalonepublisher - parents = list() - for entity in reversed(visual_hierarchy): - parents.append({ - "entity_type": entity["data"]["entityType"], - "entity_name": entity["name"] - }) - - hierarchy = list() - if self.shot_add_hierarchy.get("enabled"): - parent_template_patern = re.compile(r"\{([a-z]*?)\}") - # fill the parents parts from presets - shot_add_hierarchy = self.shot_add_hierarchy.copy() - hierarchy_parents = shot_add_hierarchy["parents"].copy() - - # fill parent keys data template from anatomy data - for parent_key in hierarchy_parents: - hierarchy_parents[parent_key] = hierarchy_parents[ - parent_key].format(**instance.data["anatomyData"]) - - for _index, _parent in enumerate( - shot_add_hierarchy["parents_path"].split("/")): - parent_filled = _parent.format(**hierarchy_parents) - parent_key = parent_template_patern.findall(_parent).pop() - - # in case SP context is set to the same folder - if (_index == 0) and ("folder" in parent_key) \ - and (parents[-1]["entity_name"] == parent_filled): - self.log.debug(f" skipping : {parent_filled}") - continue - - # in case first parent is project then start parents from start - if (_index == 0) and ("project" in parent_key): - self.log.debug("rebuilding parents from scratch") - project_parent = parents[0] - parents = [project_parent] - self.log.debug(f"project_parent: {project_parent}") - self.log.debug(f"parents: {parents}") - continue - - prnt = self.convert_to_entity( - parent_key, parent_filled) - parents.append(prnt) - hierarchy.append(parent_filled) - - # convert hierarchy to string - hierarchy = "/".join(hierarchy) - - # assign to instance data - instance.data["hierarchy"] = hierarchy - instance.data["parents"] = parents - - # print - self.log.warning(f"Hierarchy: {hierarchy}") - self.log.info(f"parents: {parents}") - - tasks_to_add = dict() - if self.shot_add_tasks: - project_tasks = project_doc["config"]["tasks"] - for task_name, task_data in self.shot_add_tasks.items(): - _task_data = deepcopy(task_data) - - # fixing enumerator from settings - _task_data["type"] = task_data["type"][0] - - # check if task type in project task types - if _task_data["type"] in project_tasks.keys(): - tasks_to_add.update({task_name: _task_data}) - else: - raise KeyError( - "Wrong FtrackTaskType `{}` for `{}` is not" - " existing in `{}``".format( - _task_data["type"], - task_name, - list(project_tasks.keys()))) - - instance.data["tasks"] = tasks_to_add - - # updating hierarchy data - instance.data["anatomyData"].update({ - "asset": instance.data["asset"], - "task": "conform" - }) - - def process(self, context): - self.log.info("self.shot_add_hierarchy: {}".format( - pformat(self.shot_add_hierarchy) - )) - for instance in context: - if instance.data["family"] in self.families: - self.processing_instance(instance) - - def processing_instance(self, instance): - self.log.info(f"_ instance: {instance}") - # adding anatomyData for burnins - instance.data["anatomyData"] = deepcopy( - instance.context.data["anatomyData"]) - - asset = instance.data["asset"] - assets_shared = instance.context.data.get("assetsShared") - - frame_start = instance.data["frameStart"] - frame_end = instance.data["frameEnd"] - - if self.shot_rename_template: - self.rename_with_hierarchy(instance) - - self.create_hierarchy(instance) - - shot_name = instance.data["asset"] - self.log.debug(f"Shot Name: {shot_name}") - - label = f"{shot_name} ({frame_start}-{frame_end})" - instance.data["label"] = label - - # dealing with shared attributes across instances - # with the same asset name - if assets_shared.get(asset): - asset_shared = assets_shared.get(asset) - else: - asset_shared = assets_shared[asset] - - asset_shared.update({ - "asset": instance.data["asset"], - "hierarchy": instance.data["hierarchy"], - "parents": instance.data["parents"], - "tasks": instance.data["tasks"], - "anatomyData": instance.data["anatomyData"] - }) - - -class CollectHierarchyContext(pyblish.api.ContextPlugin): - '''Collecting Hierarchy from instances and building - context hierarchy tree - ''' - - label = "Collect Hierarchy Context" - order = pyblish.api.CollectorOrder + 0.102 - hosts = ["standalonepublisher"] - families = ["shot"] - - def update_dict(self, ex_dict, new_dict): - for key in ex_dict: - if key in new_dict and isinstance(ex_dict[key], dict): - new_dict[key] = self.update_dict(ex_dict[key], new_dict[key]) - else: - if ex_dict.get(key) and new_dict.get(key): - continue - else: - new_dict[key] = ex_dict[key] - - return new_dict - - def process(self, context): - instances = context - # create hierarchyContext attr if context has none - assets_shared = context.data.get("assetsShared") - final_context = {} - for instance in instances: - if 'editorial' in instance.data.get('family', ''): - continue - # inject assetsShared to other instances with - # the same `assetShareName` attribute in data - asset_shared_name = instance.data.get("assetShareName") - - s_asset_data = assets_shared.get(asset_shared_name) - if s_asset_data: - instance.data["asset"] = s_asset_data["asset"] - instance.data["parents"] = s_asset_data["parents"] - instance.data["hierarchy"] = s_asset_data["hierarchy"] - instance.data["tasks"] = s_asset_data["tasks"] - instance.data["anatomyData"] = s_asset_data["anatomyData"] - - # generate hierarchy data only on shot instances - if 'shot' not in instance.data.get('family', ''): - continue - - # get handles - handle_start = int(instance.data["handleStart"]) - handle_end = int(instance.data["handleEnd"]) - - in_info = {} - - # suppose that all instances are Shots - in_info['entity_type'] = 'Shot' - - # get custom attributes of the shot - - in_info['custom_attributes'] = { - "handleStart": handle_start, - "handleEnd": handle_end, - "frameStart": instance.data["frameStart"], - "frameEnd": instance.data["frameEnd"], - "clipIn": instance.data["clipIn"], - "clipOut": instance.data["clipOut"], - 'fps': instance.data["fps"] - } - - in_info['tasks'] = instance.data['tasks'] - - from pprint import pformat - parents = instance.data.get('parents', []) - self.log.debug(f"parents: {pformat(parents)}") - - # Split by '/' for AYON where asset is a path - name = instance.data["asset"].split("/")[-1] - actual = {name: in_info} - - for parent in reversed(parents): - next_dict = {} - parent_name = parent["entity_name"] - next_dict[parent_name] = {} - next_dict[parent_name]["entity_type"] = parent["entity_type"] - next_dict[parent_name]["childs"] = actual - actual = next_dict - - final_context = self.update_dict(final_context, actual) - - # adding hierarchy context to instance - context.data["hierarchyContext"] = final_context - self.log.debug(f"hierarchyContext: {pformat(final_context)}") - self.log.info("Hierarchy instance collected") diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_instance_data.py b/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_instance_data.py deleted file mode 100644 index be87e72302..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_instance_data.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -Requires: - Nothing - -Provides: - Instance -""" - -import pyblish.api -from pprint import pformat - - -class CollectInstanceData(pyblish.api.InstancePlugin): - """ - Collector with only one reason for its existence - remove 'ftrack' - family implicitly added by Standalone Publisher - """ - - label = "Collect instance data" - order = pyblish.api.CollectorOrder + 0.49 - families = ["render", "plate", "review"] - hosts = ["standalonepublisher"] - - def process(self, instance): - fps = instance.context.data["fps"] - - instance.data.update({ - "fps": fps - }) - self.log.debug(f"instance.data: {pformat(instance.data)}") diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_matching_asset.py b/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_matching_asset.py deleted file mode 100644 index 426e015a90..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_matching_asset.py +++ /dev/null @@ -1,127 +0,0 @@ -import os -import re -import collections -import pyblish.api -from pprint import pformat - -from ayon_core.client import get_assets - - -class CollectMatchingAssetToInstance(pyblish.api.InstancePlugin): - """ - Collecting temp json data sent from a host context - and path for returning json data back to hostself. - """ - - label = "Collect Matching Asset to Instance" - order = pyblish.api.CollectorOrder - 0.05 - hosts = ["standalonepublisher"] - families = ["background_batch", "render_mov_batch"] - - # Version regex to parse asset name and version from filename - version_regex = re.compile(r"^(.+)_v([0-9]+)$") - - def process(self, instance): - source_filename = self.get_source_filename(instance) - self.log.info("Looking for asset document for file \"{}\"".format( - source_filename - )) - asset_name = os.path.splitext(source_filename)[0].lower() - - asset_docs_by_name = self.selection_children_by_name(instance) - - version_number = None - # Always first check if source filename is in assets - matching_asset_doc = asset_docs_by_name.get(asset_name) - if matching_asset_doc is None: - # Check if source file contain version in name - self.log.debug(( - "Asset doc by \"{}\" was not found trying version regex." - ).format(asset_name)) - regex_result = self.version_regex.findall(asset_name) - if regex_result: - _asset_name, _version_number = regex_result[0] - matching_asset_doc = asset_docs_by_name.get(_asset_name) - if matching_asset_doc: - version_number = int(_version_number) - - if matching_asset_doc is None: - for asset_name_low, asset_doc in asset_docs_by_name.items(): - if asset_name_low in asset_name: - matching_asset_doc = asset_doc - break - - if not matching_asset_doc: - self.log.debug("Available asset names {}".format( - str(list(asset_docs_by_name.keys())) - )) - # TODO better error message - raise AssertionError(( - "Filename \"{}\" does not match" - " any name of asset documents in database for your selection." - ).format(source_filename)) - - instance.data["asset"] = matching_asset_doc["name"] - instance.data["assetEntity"] = matching_asset_doc - if version_number is not None: - instance.data["version"] = version_number - - self.log.info( - f"Matching asset found: {pformat(matching_asset_doc)}" - ) - - def get_source_filename(self, instance): - if instance.data["family"] == "background_batch": - return os.path.basename(instance.data["source"]) - - if len(instance.data["representations"]) != 1: - raise ValueError(( - "Implementation bug: Instance data contain" - " more than one representation." - )) - - repre = instance.data["representations"][0] - repre_files = repre["files"] - if not isinstance(repre_files, str): - raise ValueError(( - "Implementation bug: Instance's representation contain" - " unexpected value (expected single file). {}" - ).format(str(repre_files))) - return repre_files - - def selection_children_by_name(self, instance): - storing_key = "childrenDocsForSelection" - - children_docs = instance.context.data.get(storing_key) - if children_docs is None: - top_asset_doc = instance.context.data["assetEntity"] - assets_by_parent_id = self._asset_docs_by_parent_id(instance) - _children_docs = self._children_docs( - assets_by_parent_id, top_asset_doc - ) - children_docs = { - children_doc["name"].lower(): children_doc - for children_doc in _children_docs - } - instance.context.data[storing_key] = children_docs - return children_docs - - def _children_docs(self, documents_by_parent_id, parent_doc): - # Find all children in reverse order, last children is at first place. - output = [] - children = documents_by_parent_id.get(parent_doc["_id"]) or tuple() - for child in children: - output.extend( - self._children_docs(documents_by_parent_id, child) - ) - output.append(parent_doc) - return output - - def _asset_docs_by_parent_id(self, instance): - # Query all assets for project and store them by parent's id to list - project_name = instance.context.data["projectEntity"]["name"] - asset_docs_by_parent_id = collections.defaultdict(list) - for asset_doc in get_assets(project_name): - parent_id = asset_doc["data"]["visualParent"] - asset_docs_by_parent_id[parent_id].append(asset_doc) - return asset_docs_by_parent_id diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_remove_marked.py b/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_remove_marked.py deleted file mode 100644 index 4279d67655..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_remove_marked.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -"""Collect instances that are marked for removal and remove them.""" -import pyblish.api - - -class CollectRemoveMarked(pyblish.api.ContextPlugin): - """Clean up instances marked for removal. - - Note: - This is a workaround for race conditions and removing of instances - used to generate other instances. - """ - - order = pyblish.api.CollectorOrder + 0.499 - label = 'Remove Marked Instances' - - def process(self, context): - """Plugin entry point.""" - for instance in context: - if instance.data.get('remove'): - context.remove(instance) diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_representation_names.py b/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_representation_names.py deleted file mode 100644 index 82dbba3345..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_representation_names.py +++ /dev/null @@ -1,31 +0,0 @@ -import re -import os -import pyblish.api - - -class CollectRepresentationNames(pyblish.api.InstancePlugin): - """ - Sets the representation names for given families based on RegEx filter - """ - - label = "Collect Representation Names" - order = pyblish.api.CollectorOrder - families = [] - hosts = ["standalonepublisher"] - name_filter = "" - - def process(self, instance): - for repre in instance.data['representations']: - new_repre_name = None - if isinstance(repre['files'], list): - shortened_name = os.path.splitext(repre['files'][0])[0] - new_repre_name = re.search(self.name_filter, - shortened_name).group() - else: - new_repre_name = re.search(self.name_filter, - repre['files']).group() - - if new_repre_name: - repre['name'] = new_repre_name - - repre['outputName'] = repre['name'] diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_texture.py b/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_texture.py deleted file mode 100644 index b687d81e2e..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/collect_texture.py +++ /dev/null @@ -1,524 +0,0 @@ -import os -import re -import pyblish.api -import json - -from ayon_core.lib import ( - prepare_template_data, - StringTemplate, -) - - -class CollectTextures(pyblish.api.ContextPlugin): - """Collect workfile (and its resource_files) and textures. - - Currently implements use case with Mari and Substance Painter, where - one workfile is main (.mra - Mari) with possible additional workfiles - (.spp - Substance) - - - Provides: - 1 instance per workfile (with 'resources' filled if needed) - (workfile family) - 1 instance per group of textures - (textures family) - """ - - order = pyblish.api.CollectorOrder - label = "Collect Textures" - hosts = ["standalonepublisher"] - families = ["texture_batch"] - actions = [] - - # from presets - main_workfile_extensions = ['mra'] - other_workfile_extensions = ['spp', 'psd'] - texture_extensions = ["exr", "dpx", "jpg", "jpeg", "png", "tiff", "tga", - "gif", "svg"] - - # additional families (ftrack etc.) - workfile_families = [] - textures_families = [] - - color_space = ["linsRGB", "raw", "acesg"] - - # currently implemented placeholders ["color_space"] - # describing patterns in file names splitted by regex groups - input_naming_patterns = { - # workfile: corridorMain_v001.mra > - # texture: corridorMain_aluminiumID_v001_baseColor_linsRGB_1001.exr - "workfile": r'^([^.]+)(_[^_.]*)?_v([0-9]{3,}).+', - "textures": r'^([^_.]+)_([^_.]+)_v([0-9]{3,})_([^_.]+)_({color_space})_(1[0-9]{3}).+', # noqa - } - # matching regex group position to 'input_naming_patterns' - input_naming_groups = { - "workfile": ('asset', 'filler', 'version'), - "textures": ('asset', 'shader', 'version', 'channel', 'color_space', - 'udim') - } - - workfile_subset_template = "textures{Subset}Workfile" - # implemented keys: ["color_space", "channel", "subset", "shader"] - texture_subset_template = "textures{Subset}_{Shader}_{Channel}" - - def process(self, context): - self.context = context - - resource_files = {} - workfile_files = {} - representations = {} - version_data = {} - asset_builds = set() - asset = None - for instance in context: - if not self.input_naming_patterns: - raise ValueError("Naming patterns are not configured. \n" - "Ask admin to provide naming conventions " - "for workfiles and textures.") - - if not asset: - asset = instance.data["asset"] # selected from SP - - parsed_subset = instance.data["subset"].replace( - instance.data["family"], '') - - explicit_data = { - "subset": parsed_subset - } - - processed_instance = False - for repre in instance.data["representations"]: - ext = repre["ext"].replace('.', '') - asset_build = version = None - - if isinstance(repre["files"], list): - repre_file = repre["files"][0] - else: - repre_file = repre["files"] - - if ext in self.main_workfile_extensions or \ - ext in self.other_workfile_extensions: - - formatting_data = self._get_parsed_groups( - repre_file, - self.input_naming_patterns["workfile"], - self.input_naming_groups["workfile"], - self.color_space - ) - self.log.info("Parsed groups from workfile " - "name '{}': {}".format(repre_file, - formatting_data)) - - formatting_data.update(explicit_data) - fill_pairs = prepare_template_data(formatting_data) - workfile_subset = StringTemplate.format_strict_template( - self.workfile_subset_template, fill_pairs - ) - - asset_build = self._get_asset_build( - repre_file, - self.input_naming_patterns["workfile"], - self.input_naming_groups["workfile"], - self.color_space - ) - version = self._get_version( - repre_file, - self.input_naming_patterns["workfile"], - self.input_naming_groups["workfile"], - self.color_space - ) - asset_builds.add((asset_build, version, - workfile_subset, 'workfile')) - processed_instance = True - - if not representations.get(workfile_subset): - representations[workfile_subset] = [] - - if ext in self.main_workfile_extensions: - # workfiles can have only single representation - # currently OP is not supporting different extensions in - # representation files - representations[workfile_subset] = [repre] - - workfile_files[asset_build] = repre_file - - if ext in self.other_workfile_extensions: - # add only if not added already from main - if not representations.get(workfile_subset): - representations[workfile_subset] = [repre] - - # only overwrite if not present - if not workfile_files.get(asset_build): - workfile_files[asset_build] = repre_file - - if not resource_files.get(workfile_subset): - resource_files[workfile_subset] = [] - item = { - "files": [os.path.join(repre["stagingDir"], - repre["files"])], - "source": "standalone publisher" - } - resource_files[workfile_subset].append(item) - - if ext in self.texture_extensions: - formatting_data = self._get_parsed_groups( - repre_file, - self.input_naming_patterns["textures"], - self.input_naming_groups["textures"], - self.color_space - ) - - self.log.info("Parsed groups from texture " - "name '{}': {}".format(repre_file, - formatting_data)) - - c_space = self._get_color_space( - repre_file, - self.color_space - ) - - # optional value - channel = self._get_channel_name( - repre_file, - self.input_naming_patterns["textures"], - self.input_naming_groups["textures"], - self.color_space - ) - - # optional value - shader = self._get_shader_name( - repre_file, - self.input_naming_patterns["textures"], - self.input_naming_groups["textures"], - self.color_space - ) - - explicit_data = { - "color_space": c_space or '', # None throws exception - "channel": channel or '', - "shader": shader or '', - "subset": parsed_subset or '' - } - - formatting_data.update(explicit_data) - - fill_pairs = prepare_template_data(formatting_data) - subset = StringTemplate.format_strict_template( - self.texture_subset_template, fill_pairs - ) - - asset_build = self._get_asset_build( - repre_file, - self.input_naming_patterns["textures"], - self.input_naming_groups["textures"], - self.color_space - ) - version = self._get_version( - repre_file, - self.input_naming_patterns["textures"], - self.input_naming_groups["textures"], - self.color_space - ) - if not representations.get(subset): - representations[subset] = [] - representations[subset].append(repre) - - ver_data = { - "color_space": c_space or '', - "channel_name": channel or '', - "shader_name": shader or '' - } - version_data[subset] = ver_data - - asset_builds.add( - (asset_build, version, subset, "textures")) - processed_instance = True - - if processed_instance: - self.context.remove(instance) - - self._create_new_instances(context, - asset, - asset_builds, - resource_files, - representations, - version_data, - workfile_files) - - def _create_new_instances(self, context, asset, asset_builds, - resource_files, representations, - version_data, workfile_files): - """Prepare new instances from collected data. - - Args: - context (ContextPlugin) - asset (string): selected asset from SP - asset_builds (set) of tuples - (asset_build, version, subset, family) - resource_files (list) of resource dicts - to store additional - files to main workfile - representations (list) of dicts - to store workfile info OR - all collected texture files, key is asset_build - version_data (dict) - prepared to store into version doc in DB - workfile_files (dict) - to store workfile to add to textures - key is asset_build - """ - # sort workfile first - asset_builds = sorted(asset_builds, - key=lambda tup: tup[3], reverse=True) - - # workfile must have version, textures might - main_version = None - for asset_build, version, subset, family in asset_builds: - if not main_version: - main_version = version - - try: - version_int = int(version or main_version or 1) - except ValueError: - self.log.error("Parsed version {} is not " - "an number".format(version)) - - new_instance = context.create_instance(subset) - new_instance.data.update( - { - "subset": subset, - "asset": asset, - "label": subset, - "name": subset, - "family": family, - "version": version_int, - "asset_build": asset_build # remove in validator - } - ) - - workfile = workfile_files.get(asset_build) - - if resource_files.get(subset): - # add resources only when workfile is main style - for ext in self.main_workfile_extensions: - if ext in workfile: - new_instance.data.update({ - "resources": resource_files.get(subset) - }) - break - - # store origin - if family == 'workfile': - families = self.workfile_families - families.append("texture_batch_workfile") - - new_instance.data["source"] = "standalone publisher" - else: - families = self.textures_families - - repre = representations.get(subset)[0] - new_instance.context.data["currentFile"] = os.path.join( - repre["stagingDir"], workfile or 'dummy.txt') - - new_instance.data["families"] = families - - # add data for version document - ver_data = version_data.get(subset) - if ver_data: - if workfile: - ver_data['workfile'] = workfile - - new_instance.data.update( - {"versionData": ver_data} - ) - - upd_representations = representations.get(subset) - if upd_representations and family != 'workfile': - upd_representations = self._update_representations( - upd_representations) - - new_instance.data["representations"] = upd_representations - - self.log.debug("new instance - {}:: {}".format( - family, - json.dumps(new_instance.data, indent=4))) - - def _get_asset_build(self, name, - input_naming_patterns, input_naming_groups, - color_spaces): - """Loops through configured workfile patterns to find asset name. - - Asset name used to bind workfile and its textures. - - Args: - name (str): workfile name - input_naming_patterns (list): - [workfile_pattern] or [texture_pattern] - input_naming_groups (list) - ordinal position of regex groups matching to input_naming.. - color_spaces (list) - predefined color spaces - """ - asset_name = "NOT_AVAIL" - - return (self._parse_key(name, input_naming_patterns, - input_naming_groups, color_spaces, 'asset') or - asset_name) - - def _get_version(self, name, input_naming_patterns, input_naming_groups, - color_spaces): - found = self._parse_key(name, input_naming_patterns, - input_naming_groups, color_spaces, 'version') - - if found: - return found.replace('v', '') - - self.log.info("No version found in the name {}".format(name)) - - def _get_udim(self, name, input_naming_patterns, input_naming_groups, - color_spaces): - """Parses from 'name' udim value.""" - found = self._parse_key(name, input_naming_patterns, - input_naming_groups, color_spaces, 'udim') - if found: - return found - - self.log.warning("Didn't find UDIM in {}".format(name)) - - def _get_color_space(self, name, color_spaces): - """Looks for color_space from a list in a file name. - - Color space seems not to be recognizable by regex pattern, set of - known space spaces must be provided. - """ - color_space = None - found = [cs for cs in color_spaces if - re.search("_{}_".format(cs), name)] - - if not found: - self.log.warning("No color space found in {}".format(name)) - else: - if len(found) > 1: - msg = "Multiple color spaces found in {}->{}".format(name, - found) - self.log.warning(msg) - - color_space = found[0] - - return color_space - - def _get_shader_name(self, name, input_naming_patterns, - input_naming_groups, color_spaces): - """Return parsed shader name. - - Shader name is needed for overlapping udims (eg. udims might be - used for different materials, shader needed to not overwrite). - - Unknown format of channel name and color spaces >> cs are known - list - 'color_space' used as a placeholder - """ - found = None - try: - found = self._parse_key(name, input_naming_patterns, - input_naming_groups, color_spaces, - 'shader') - except ValueError: - self.log.warning("Didn't find shader in {}".format(name)) - - return found - - def _get_channel_name(self, name, input_naming_patterns, - input_naming_groups, color_spaces): - """Return parsed channel name. - - Unknown format of channel name and color spaces >> cs are known - list - 'color_space' used as a placeholder - """ - found = None - try: - found = self._parse_key(name, input_naming_patterns, - input_naming_groups, color_spaces, - 'channel') - except ValueError: - self.log.warning("Didn't find channel in {}".format(name)) - - return found - - def _parse_key(self, name, input_naming_patterns, input_naming_groups, - color_spaces, key): - """Universal way to parse 'name' with configurable regex groups. - - Args: - name (str): workfile name - input_naming_patterns (list): - [workfile_pattern] or [texture_pattern] - input_naming_groups (list) - ordinal position of regex groups matching to input_naming.. - color_spaces (list) - predefined color spaces - - Raises: - ValueError - if broken 'input_naming_groups' - """ - parsed_groups = self._get_parsed_groups(name, - input_naming_patterns, - input_naming_groups, - color_spaces) - - try: - parsed_value = parsed_groups[key] - return parsed_value - except (IndexError, KeyError): - msg = ("'Textures group positions' must " + - "have '{}' key".format(key)) - raise ValueError(msg) - - def _get_parsed_groups(self, name, input_naming_patterns, - input_naming_groups, color_spaces): - """Universal way to parse 'name' with configurable regex groups. - - Args: - name (str): workfile name or texture name - input_naming_patterns (list): - [workfile_pattern] or [texture_pattern] - input_naming_groups (list) - ordinal position of regex groups matching to input_naming.. - color_spaces (list) - predefined color spaces - - Returns: - (dict) {group_name:parsed_value} - """ - for input_pattern in input_naming_patterns: - for cs in color_spaces: - pattern = input_pattern.replace('{color_space}', cs) - regex_result = re.findall(pattern, name) - if regex_result: - if len(regex_result[0]) == len(input_naming_groups): - return dict(zip(input_naming_groups, regex_result[0])) - else: - self.log.warning("No of parsed groups doesn't match " - "no of group labels") - - raise ValueError("Name '{}' cannot be parsed by any " - "'{}' patterns".format(name, input_naming_patterns)) - - def _update_representations(self, upd_representations): - """Frames dont have sense for textures, add collected udims instead.""" - udims = [] - for repre in upd_representations: - repre.pop("frameStart", None) - repre.pop("frameEnd", None) - repre.pop("fps", None) - - # ignore unique name from SP, use extension instead - # SP enforces unique name, here different subsets >> unique repres - repre["name"] = repre["ext"].replace('.', '') - - files = repre.get("files", []) - if not isinstance(files, list): - files = [files] - - for file_name in files: - udim = self._get_udim(file_name, - self.input_naming_patterns["textures"], - self.input_naming_groups["textures"], - self.color_space) - udims.append(udim) - - repre["udim"] = udims # must be this way, used for filling path - - return upd_representations diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/extract_resources.py b/client/ayon_core/hosts/standalonepublisher/plugins/publish/extract_resources.py deleted file mode 100644 index 1183180833..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/extract_resources.py +++ /dev/null @@ -1,42 +0,0 @@ -import os -import pyblish.api - - -class ExtractResources(pyblish.api.InstancePlugin): - """ - Extracts files from instance.data["resources"]. - - These files are additional (textures etc.), currently not stored in - representations! - - Expects collected 'resourcesDir'. (list of dicts with 'files' key and - list of source urls) - - Provides filled 'transfers' (list of tuples (source_url, target_url)) - """ - - label = "Extract Resources SP" - hosts = ["standalonepublisher"] - order = pyblish.api.ExtractorOrder - - families = ["workfile"] - - def process(self, instance): - if not instance.data.get("resources"): - self.log.info("No resources") - return - - if not instance.data.get("transfers"): - instance.data["transfers"] = [] - - publish_dir = instance.data["resourcesDir"] - - transfers = [] - for resource in instance.data["resources"]: - for file_url in resource.get("files", []): - file_name = os.path.basename(file_url) - dest_url = os.path.join(publish_dir, file_name) - transfers.append((file_url, dest_url)) - - self.log.info("transfers:: {}".format(transfers)) - instance.data["transfers"].extend(transfers) diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py b/client/ayon_core/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py deleted file mode 100644 index f7c76c9e32..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py +++ /dev/null @@ -1,127 +0,0 @@ -import os -import subprocess -import tempfile -import pyblish.api -from ayon_core.lib import ( - get_ffmpeg_tool_args, - get_ffprobe_streams, - path_to_subprocess_arg, - run_subprocess, -) - - -class ExtractThumbnailSP(pyblish.api.InstancePlugin): - """Extract jpeg thumbnail from component input from standalone publisher - - Uses jpeg file from component if possible (when single or multiple jpegs - are loaded to component selected as thumbnail) otherwise extracts from - input file/s single jpeg to temp. - """ - - label = "Extract Thumbnail SP" - hosts = ["standalonepublisher"] - order = pyblish.api.ExtractorOrder - - # Presetable attribute - ffmpeg_args = None - - def process(self, instance): - repres = instance.data.get('representations') - if not repres: - return - - thumbnail_repre = None - for repre in repres: - if repre.get("thumbnail"): - thumbnail_repre = repre - break - - if not thumbnail_repre: - return - - thumbnail_repre.pop("thumbnail") - files = thumbnail_repre.get("files") - if not files: - return - - if isinstance(files, list): - first_filename = str(files[0]) - else: - first_filename = files - - # Convert to jpeg if not yet - full_input_path = os.path.join( - thumbnail_repre["stagingDir"], first_filename - ) - self.log.info("input {}".format(full_input_path)) - with tempfile.NamedTemporaryFile(suffix=".jpg") as tmp: - full_thumbnail_path = tmp.name - - self.log.info("output {}".format(full_thumbnail_path)) - - instance.context.data["cleanupFullPaths"].append(full_thumbnail_path) - - ffmpeg_executable_args = get_ffmpeg_tool_args("ffmpeg") - - ffmpeg_args = self.ffmpeg_args or {} - - jpeg_items = [ - subprocess.list2cmdline(ffmpeg_executable_args), - # override file if already exists - "-y" - ] - - # add input filters from peresets - jpeg_items.extend(ffmpeg_args.get("input") or []) - # input file - jpeg_items.extend([ - "-i", path_to_subprocess_arg(full_input_path), - # extract only single file - "-frames:v", "1", - # Add black background for transparent images - "-filter_complex", ( - "\"color=black,format=rgb24[c]" - ";[c][0]scale2ref[c][i]" - ";[c][i]overlay=format=auto:shortest=1,setsar=1\"" - ), - ]) - - jpeg_items.extend(ffmpeg_args.get("output") or []) - - # output file - jpeg_items.append(path_to_subprocess_arg(full_thumbnail_path)) - - subprocess_jpeg = " ".join(jpeg_items) - - # run subprocess - self.log.debug("Executing: {}".format(subprocess_jpeg)) - run_subprocess( - subprocess_jpeg, shell=True, logger=self.log - ) - - # remove thumbnail key from origin repre - streams = get_ffprobe_streams(full_thumbnail_path) - width = height = None - for stream in streams: - if "width" in stream and "height" in stream: - width = stream["width"] - height = stream["height"] - break - - staging_dir, filename = os.path.split(full_thumbnail_path) - - # create new thumbnail representation - representation = { - 'name': 'thumbnail', - 'ext': 'jpg', - 'files': filename, - "stagingDir": staging_dir, - "tags": ["thumbnail", "delete"], - "thumbnail": True - } - if width and height: - representation["width"] = width - representation["height"] = height - - self.log.info(f"New representation {representation}") - instance.data["representations"].append(representation) diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py b/client/ayon_core/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py deleted file mode 100644 index 9ff84e32fb..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py +++ /dev/null @@ -1,44 +0,0 @@ -import os -import pyblish.api - - -class ExtractWorkfileUrl(pyblish.api.ContextPlugin): - """ - Modifies 'workfile' field to contain link to published workfile. - - Expects that batch contains only single workfile and matching - (multiple) textures. - """ - - label = "Extract Workfile Url SP" - hosts = ["standalonepublisher"] - order = pyblish.api.ExtractorOrder - - families = ["textures"] - - def process(self, context): - filepath = None - - # first loop for workfile - for instance in context: - if instance.data["family"] == 'workfile': - anatomy = context.data['anatomy'] - template_data = instance.data.get("anatomyData") - rep_name = instance.data.get("representations")[0].get("name") - template_data["representation"] = rep_name - template_data["ext"] = rep_name - template_obj = anatomy.templates_obj["publish"]["path"] - template_filled = template_obj.format_strict(template_data) - filepath = os.path.normpath(template_filled) - self.log.info("Using published scene for render {}".format( - filepath)) - break - - if not filepath: - self.log.info("Texture batch doesn't contain workfile.") - return - - # then apply to all textures - for instance in context: - if instance.data["family"] == 'textures': - instance.data["versionData"]["workfile"] = filepath diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_editorial_resources.xml b/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_editorial_resources.xml deleted file mode 100644 index 803de6bf11..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_editorial_resources.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - -Missing source video file - -## No attached video file found - -Process expects presence of source video file with same name prefix as an editorial file in same folder. -(example `simple_editorial_setup_Layer1.edl` expects `simple_editorial_setup.mp4` in same folder) - - -### How to repair? - -Copy source video file to the folder next to `.edl` file. (On a disk, do not put it into Standalone Publisher.) - - - diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_frame_ranges.xml b/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_frame_ranges.xml deleted file mode 100644 index 933df1c7c5..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_frame_ranges.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - -Invalid frame range - -## Invalid frame range - -Expected duration or '{duration}' frames set in database, workfile contains only '{found}' frames. - -### How to repair? - -Modify configuration in the database or tweak frame range in the workfile. - - - \ No newline at end of file diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_shot_duplicates.xml b/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_shot_duplicates.xml deleted file mode 100644 index 77b8727162..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_shot_duplicates.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - -Duplicate shots - -## Duplicate shot names - -Process contains duplicated shot names '{duplicates_str}'. - -### How to repair? - -Remove shot duplicates. - - - \ No newline at end of file diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_simple_texture_naming.xml b/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_simple_texture_naming.xml deleted file mode 100644 index b65d274fe5..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_simple_texture_naming.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - -Invalid texture name - -## Invalid file name - -Submitted file has invalid name: -'{invalid_file}' - -### How to repair? - - Texture file must adhere to naming conventions for Unreal: - T_{asset}_*.ext - - - \ No newline at end of file diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_sources.xml b/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_sources.xml deleted file mode 100644 index d527d2173e..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_sources.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - -Files not found - -## Source files not found - -Process contains duplicated shot names: -'{files_not_found}' - -### How to repair? - -Add missing files or run Publish again to collect new publishable files. - - - \ No newline at end of file diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_task_existence.xml b/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_task_existence.xml deleted file mode 100644 index a943f560d0..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_task_existence.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - -Task not found - -## Task not found in database - -Process contains tasks that don't exist in database: -'{task_not_found}' - -### How to repair? - -Remove set task or add task into database into proper place. - - - \ No newline at end of file diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_texture_batch.xml b/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_texture_batch.xml deleted file mode 100644 index a645df8d02..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_texture_batch.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - -No texture files found - -## Batch doesn't contain texture files - -Batch must contain at least one texture file. - -### How to repair? - -Add texture file to the batch or check name if it follows naming convention to match texture files to the batch. - - - \ No newline at end of file diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_texture_has_workfile.xml b/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_texture_has_workfile.xml deleted file mode 100644 index 077987a96d..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_texture_has_workfile.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - -No workfile found - -## Batch should contain workfile - -It is expected that published contains workfile that served as a source for textures. - -### How to repair? - -Add workfile to the batch, or disable this validator if you do not want workfile published. - - - \ No newline at end of file diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_texture_name.xml b/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_texture_name.xml deleted file mode 100644 index 2610917736..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_texture_name.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - -Asset name not found - -## Couldn't parse asset name from a file - -Unable to parse asset name from '{file_name}'. File name doesn't match configured naming convention. - -### How to repair? - -Check Settings: project_settings/standalonepublisher/publish/CollectTextures for naming convention. - - -### __Detailed Info__ (optional) - -This error happens when parsing cannot figure out name of asset texture files belong under. - - - -Missing keys - -## Texture file name is missing some required keys - -Texture '{file_name}' is missing values for {missing_str} keys. - -### How to repair? - -Fix name of texture file and Publish again. - - - diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_texture_versions.xml b/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_texture_versions.xml deleted file mode 100644 index 1e536e604f..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_texture_versions.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - -Texture version - -## Texture version mismatch with workfile - -Workfile '{file_name}' version doesn't match with '{version}' of a texture. - -### How to repair? - -Rename either workfile or texture to contain matching versions - - -### __Detailed Info__ (optional) - -This might happen if you are trying to publish textures for older version of workfile (or the other way). -(Eg. publishing 'workfile_v001' and 'texture_file_v002') - - - -Too many versions - -## Too many versions published at same time - -It is currently expected to publish only batch with single version. - -Found {found} versions. - -### How to repair? - -Please remove files with different version and split publishing into multiple steps. - - - diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_texture_workfiles.xml b/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_texture_workfiles.xml deleted file mode 100644 index 8187eb0bc8..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/help/validate_texture_workfiles.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - -No secondary workfile - -## No secondary workfile found - -Current process expects that primary workfile (for example with a extension '{extension}') will contain also 'secondary' workfile. - -Secondary workfile for '{file_name}' wasn't found. - -### How to repair? - -Attach secondary workfile or disable this validator and Publish again. - - -### __Detailed Info__ (optional) - -This process was implemented for a possible use case of first workfile coming from Mari, secondary workfile for textures from Substance. -Publish should contain both if primary workfile is present. - - - diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_editorial_resources.py b/client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_editorial_resources.py deleted file mode 100644 index 7ab29d1b17..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_editorial_resources.py +++ /dev/null @@ -1,28 +0,0 @@ -import pyblish.api -from ayon_core.pipeline.publish import ( - ValidateContentsOrder, - PublishXmlValidationError, -) - - -class ValidateEditorialResources(pyblish.api.InstancePlugin): - """Validate there is a "mov" next to the editorial file.""" - - label = "Validate Editorial Resources" - hosts = ["standalonepublisher"] - families = ["clip", "trimming"] - - # make sure it is enabled only if at least both families are available - match = pyblish.api.Subset - - order = ValidateContentsOrder - - def process(self, instance): - self.log.debug( - f"Instance: {instance}, Families: " - f"{[instance.data['family']] + instance.data['families']}") - check_file = instance.data["editorialSourcePath"] - msg = "Missing source video file." - - if not check_file: - raise PublishXmlValidationError(self, msg) diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py b/client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py deleted file mode 100644 index 7add98d954..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py +++ /dev/null @@ -1,67 +0,0 @@ -import re - -import pyblish.api - -from ayon_core.pipeline.context_tools import get_current_project_asset -from ayon_core.pipeline.publish import ( - ValidateContentsOrder, - PublishXmlValidationError, -) - - -class ValidateFrameRange(pyblish.api.InstancePlugin): - """Validating frame range of rendered files against state in DB.""" - - label = "Validate Frame Range" - hosts = ["standalonepublisher"] - families = ["render"] - order = ValidateContentsOrder - - optional = True - # published data might be sequence (.mov, .mp4) in that counting files - # doesnt make sense - check_extensions = ["exr", "dpx", "jpg", "jpeg", "png", "tiff", "tga", - "gif", "svg"] - skip_timelines_check = [] # skip for specific task names (regex) - - def process(self, instance): - if any(re.search(pattern, instance.data["task"]) - for pattern in self.skip_timelines_check): - self.log.info("Skipping for {} task".format(instance.data["task"])) - - # TODO replace query with using 'instance.data["assetEntity"]' - asset_data = get_current_project_asset(instance.data["asset"])["data"] - frame_start = asset_data["frameStart"] - frame_end = asset_data["frameEnd"] - handle_start = asset_data["handleStart"] - handle_end = asset_data["handleEnd"] - duration = (frame_end - frame_start + 1) + handle_start + handle_end - - repre = instance.data.get("representations", [None]) - if not repre: - self.log.info("No representations, skipping.") - return - - ext = repre[0]['ext'].replace(".", '') - - if not ext or ext.lower() not in self.check_extensions: - self.log.warning("Cannot check for extension {}".format(ext)) - return - - files = instance.data.get("representations", [None])[0]["files"] - if isinstance(files, str): - files = [files] - frames = len(files) - - msg = "Frame duration from DB:'{}' ". format(int(duration)) +\ - " doesn't match number of files:'{}'".format(frames) +\ - " Please change frame range for Asset or limit no. of files" - - formatting_data = {"duration": duration, - "found": frames} - if frames != duration: - raise PublishXmlValidationError(self, msg, - formatting_data=formatting_data) - - self.log.debug("Valid ranges expected '{}' - found '{}'". - format(int(duration), frames)) diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_shot_duplicates.py b/client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_shot_duplicates.py deleted file mode 100644 index eed3177b4c..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_shot_duplicates.py +++ /dev/null @@ -1,31 +0,0 @@ -import pyblish.api - -from ayon_core.pipeline.publish import ( - ValidateContentsOrder, - PublishXmlValidationError, -) - - -class ValidateShotDuplicates(pyblish.api.ContextPlugin): - """Validating no duplicate names are in context.""" - - label = "Validate Shot Duplicates" - hosts = ["standalonepublisher"] - order = ValidateContentsOrder - - def process(self, context): - shot_names = [] - duplicate_names = [] - for instance in context: - name = instance.data["name"] - if name in shot_names: - duplicate_names.append(name) - else: - shot_names.append(name) - - msg = "There are duplicate shot names:\n{}".format(duplicate_names) - - formatting_data = {"duplicates_str": ','.join(duplicate_names)} - if duplicate_names: - raise PublishXmlValidationError(self, msg, - formatting_data=formatting_data) diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_sources.py b/client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_sources.py deleted file mode 100644 index a0a0a43dcd..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_sources.py +++ /dev/null @@ -1,47 +0,0 @@ -import os - -import pyblish.api - -from ayon_core.pipeline.publish import ( - ValidateContentsOrder, - PublishXmlValidationError, -) - - -class ValidateSources(pyblish.api.InstancePlugin): - """Validates source files. - - Loops through all 'files' in 'stagingDir' if actually exist. They might - got deleted between starting of SP and now. - - """ - order = ValidateContentsOrder - label = "Check source files" - - optional = True # only for unforeseeable cases - - hosts = ["standalonepublisher"] - - def process(self, instance): - self.log.info("instance {}".format(instance.data)) - - missing_files = set() - for repre in instance.data.get("representations") or []: - files = [] - if isinstance(repre["files"], str): - files.append(repre["files"]) - else: - files = list(repre["files"]) - - for file_name in files: - source_file = os.path.join(repre["stagingDir"], - file_name) - - if not os.path.exists(source_file): - missing_files.add(source_file) - - msg = "Files '{}' not found".format(','.join(missing_files)) - formatting_data = {"files_not_found": ' - {}'.join(missing_files)} - if missing_files: - raise PublishXmlValidationError(self, msg, - formatting_data=formatting_data) diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_task_existence.py b/client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_task_existence.py deleted file mode 100644 index e7c70d1131..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_task_existence.py +++ /dev/null @@ -1,59 +0,0 @@ -import pyblish.api - -from ayon_core.client import get_assets -from ayon_core.pipeline import PublishXmlValidationError - - -class ValidateTaskExistence(pyblish.api.ContextPlugin): - """Validating tasks on instances are filled and existing.""" - - label = "Validate Task Existence" - order = pyblish.api.ValidatorOrder - - hosts = ["standalonepublisher"] - families = ["render_mov_batch"] - - def process(self, context): - asset_names = set() - for instance in context: - asset_names.add(instance.data["asset"]) - - project_name = context.data["projectEntity"]["name"] - asset_docs = get_assets( - project_name, - asset_names=asset_names, - fields=["name", "data.tasks"] - ) - tasks_by_asset_names = {} - for asset_doc in asset_docs: - asset_name = asset_doc["name"] - asset_tasks = asset_doc.get("data", {}).get("tasks") or {} - tasks_by_asset_names[asset_name] = list(asset_tasks.keys()) - - missing_tasks = [] - for instance in context: - asset_name = instance.data["asset"] - task_name = instance.data["task"] - task_names = tasks_by_asset_names.get(asset_name) or [] - if task_name and task_name in task_names: - continue - missing_tasks.append((asset_name, task_name)) - - # Everything is OK - if not missing_tasks: - return - - # Raise an exception - msg = "Couldn't find task name/s required for publishing.\n{}" - pair_msgs = [] - for missing_pair in missing_tasks: - pair_msgs.append( - "Asset: \"{}\" Task: \"{}\"".format(*missing_pair) - ) - - msg = msg.format("\n".join(pair_msgs)) - - formatting_data = {"task_not_found": ' - {}'.join(pair_msgs)} - if pair_msgs: - raise PublishXmlValidationError(self, msg, - formatting_data=formatting_data) diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_texture_batch.py b/client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_texture_batch.py deleted file mode 100644 index fd3650b71d..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_texture_batch.py +++ /dev/null @@ -1,28 +0,0 @@ -import pyblish.api - -from ayon_core.pipeline.publish import ( - ValidateContentsOrder, - PublishXmlValidationError, -) - - -class ValidateTextureBatch(pyblish.api.InstancePlugin): - """Validates that some texture files are present.""" - - label = "Validate Texture Presence" - hosts = ["standalonepublisher"] - order = ValidateContentsOrder - families = ["texture_batch_workfile"] - optional = False - - def process(self, instance): - present = False - for instance in instance.context: - if instance.data["family"] == "textures": - self.log.info("At least some textures present.") - - return - - msg = "No textures found in published batch!" - if not present: - raise PublishXmlValidationError(self, msg) diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_texture_has_workfile.py b/client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_texture_has_workfile.py deleted file mode 100644 index e43cd1a3b3..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_texture_has_workfile.py +++ /dev/null @@ -1,26 +0,0 @@ -import pyblish.api - -from ayon_core.pipeline.publish import ( - ValidateContentsOrder, - PublishXmlValidationError, -) - - -class ValidateTextureHasWorkfile(pyblish.api.InstancePlugin): - """Validates that textures have appropriate workfile attached. - - Workfile is optional, disable this Validator after Refresh if you are - sure it is not needed. - """ - label = "Validate Texture Has Workfile" - hosts = ["standalonepublisher"] - order = ValidateContentsOrder - families = ["textures"] - optional = True - - def process(self, instance): - wfile = instance.data["versionData"].get("workfile") - - msg = "Textures are missing attached workfile" - if not wfile: - raise PublishXmlValidationError(self, msg) diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_texture_name.py b/client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_texture_name.py deleted file mode 100644 index dca5fcc4aa..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_texture_name.py +++ /dev/null @@ -1,63 +0,0 @@ -import pyblish.api - -from ayon_core.pipeline.publish import ( - ValidateContentsOrder, - PublishXmlValidationError, -) - -class ValidateTextureBatchNaming(pyblish.api.InstancePlugin): - """Validates that all instances had properly formatted name.""" - - label = "Validate Texture Batch Naming" - hosts = ["standalonepublisher"] - order = ValidateContentsOrder - families = ["texture_batch_workfile", "textures"] - optional = False - - def process(self, instance): - file_name = instance.data["representations"][0]["files"] - if isinstance(file_name, list): - file_name = file_name[0] - - msg = "Couldn't find asset name in '{}'\n".format(file_name) + \ - "File name doesn't follow configured pattern.\n" + \ - "Please rename the file." - - formatting_data = {"file_name": file_name} - if "NOT_AVAIL" in instance.data["asset_build"]: - raise PublishXmlValidationError(self, msg, - formatting_data=formatting_data) - - instance.data.pop("asset_build") # not needed anymore - - if instance.data["family"] == "textures": - file_name = instance.data["representations"][0]["files"][0] - self._check_proper_collected(instance.data["versionData"], - file_name) - - def _check_proper_collected(self, versionData, file_name): - """ - Loop through collected versionData to check if name parsing was OK. - Args: - versionData: (dict) - - Returns: - raises AssertionException - """ - missing_key_values = [] - for key, value in versionData.items(): - if not value: - missing_key_values.append(key) - - msg = "Collected data {} doesn't contain values for {}".format( - versionData, missing_key_values) + "\n" + \ - "Name of the texture file doesn't match expected pattern.\n" + \ - "Please rename file(s) {}".format(file_name) - - missing_str = ','.join(["'{}'".format(key) - for key in missing_key_values]) - formatting_data = {"file_name": file_name, - "missing_str": missing_str} - if missing_key_values: - raise PublishXmlValidationError(self, msg, key="missing_values", - formatting_data=formatting_data) diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py b/client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py deleted file mode 100644 index 2209878abb..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py +++ /dev/null @@ -1,49 +0,0 @@ -import pyblish.api - -from ayon_core.pipeline.publish import ( - ValidateContentsOrder, - PublishXmlValidationError, -) - - -class ValidateTextureBatchVersions(pyblish.api.InstancePlugin): - """Validates that versions match in workfile and textures. - - Workfile is optional, so if you are sure, you can disable this - validator after Refresh. - - Validates that only single version is published at a time. - """ - label = "Validate Texture Batch Versions" - hosts = ["standalonepublisher"] - order = ValidateContentsOrder - families = ["textures"] - optional = False - - def process(self, instance): - wfile = instance.data["versionData"].get("workfile") - - version_str = "v{:03d}".format(instance.data["version"]) - - if not wfile: # no matching workfile, do not check versions - self.log.info("No workfile present for textures") - return - - if version_str not in wfile: - msg = "Not matching version: texture v{:03d} - workfile {}" - msg.format( - instance.data["version"], wfile - ) - raise PublishXmlValidationError(self, msg) - - present_versions = set() - for instance in instance.context: - present_versions.add(instance.data["version"]) - - if len(present_versions) != 1: - msg = "Too many versions in a batch!" - found = ','.join(["'{}'".format(val) for val in present_versions]) - formatting_data = {"found": found} - - raise PublishXmlValidationError(self, msg, key="too_many", - formatting_data=formatting_data) diff --git a/client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py b/client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py deleted file mode 100644 index d6f2fd63a5..0000000000 --- a/client/ayon_core/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py +++ /dev/null @@ -1,58 +0,0 @@ -import pyblish.api - -from ayon_core.pipeline.publish import ( - ValidateContentsOrder, - PublishXmlValidationError, -) - - -class ValidateTextureBatchWorkfiles(pyblish.api.InstancePlugin): - """Validates that textures workfile has collected resources (optional). - - Collected resources means secondary workfiles (in most cases). - """ - - label = "Validate Texture Workfile Has Resources" - hosts = ["standalonepublisher"] - order = ValidateContentsOrder - families = ["texture_batch_workfile"] - optional = True - - def process(self, instance): - if instance.data["family"] != "workfile": - return - - ext = instance.data["representations"][0]["ext"] - main_workfile_extensions = self.get_main_workfile_extensions( - instance - ) - if ext not in main_workfile_extensions: - self.log.warning("Only secondary workfile present!") - return - - if not instance.data.get("resources"): - msg = "No secondary workfile present for workfile '{}'". \ - format(instance.data["name"]) - ext = main_workfile_extensions[0] - formatting_data = {"file_name": instance.data["name"], - "extension": ext} - - raise PublishXmlValidationError( - self, msg, formatting_data=formatting_data) - - @staticmethod - def get_main_workfile_extensions(instance): - project_settings = instance.context.data["project_settings"] - - try: - extensions = (project_settings["standalonepublisher"] - ["publish"] - ["CollectTextures"] - ["main_workfile_extensions"]) - except KeyError: - raise Exception("Setting 'Main workfile extensions' not found." - " The setting must be set for the" - " 'Collect Texture' publish plugin of the" - " 'Standalone Publish' tool.") - - return extensions diff --git a/client/ayon_core/plugins/publish/cleanup.py b/client/ayon_core/plugins/publish/cleanup.py index 050b6f3186..7bed3269c2 100644 --- a/client/ayon_core/plugins/publish/cleanup.py +++ b/client/ayon_core/plugins/publish/cleanup.py @@ -32,7 +32,6 @@ class CleanUp(pyblish.api.InstancePlugin): "resolve", "tvpaint", "unreal", - "standalonepublisher", "webpublisher", "shell" ] diff --git a/client/ayon_core/plugins/publish/collect_audio.py b/client/ayon_core/plugins/publish/collect_audio.py index 5091000c11..94477e5578 100644 --- a/client/ayon_core/plugins/publish/collect_audio.py +++ b/client/ayon_core/plugins/publish/collect_audio.py @@ -34,7 +34,6 @@ class CollectAudio(pyblish.api.ContextPlugin): "premiere", "harmony", "traypublisher", - "standalonepublisher", "fusion", "tvpaint", "resolve", diff --git a/client/ayon_core/plugins/publish/extract_burnin.py b/client/ayon_core/plugins/publish/extract_burnin.py index 730dbda550..222e3c14cf 100644 --- a/client/ayon_core/plugins/publish/extract_burnin.py +++ b/client/ayon_core/plugins/publish/extract_burnin.py @@ -42,7 +42,6 @@ class ExtractBurnin(publish.Extractor): "hiero", "premiere", "traypublisher", - "standalonepublisher", "harmony", "fusion", "aftereffects", diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index e349366034..fd6a4506b6 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -57,7 +57,6 @@ class ExtractReview(pyblish.api.InstancePlugin): "premiere", "harmony", "traypublisher", - "standalonepublisher", "fusion", "tvpaint", "resolve", diff --git a/client/ayon_core/plugins/publish/extract_trim_video_audio.py b/client/ayon_core/plugins/publish/extract_trim_video_audio.py index 08c9e47a8c..78e2aec972 100644 --- a/client/ayon_core/plugins/publish/extract_trim_video_audio.py +++ b/client/ayon_core/plugins/publish/extract_trim_video_audio.py @@ -16,7 +16,7 @@ class ExtractTrimVideoAudio(publish.Extractor): # must be before `ExtractThumbnailSP` order = pyblish.api.ExtractorOrder - 0.01 label = "Extract Trim Video/Audio" - hosts = ["standalonepublisher", "traypublisher"] + hosts = ["traypublisher"] families = ["clip", "trimming"] # make sure it is enabled only if at least both families are available diff --git a/client/ayon_core/plugins/publish/validate_editorial_asset_name.py b/client/ayon_core/plugins/publish/validate_editorial_asset_name.py index 8f0f86a044..d40263d7f3 100644 --- a/client/ayon_core/plugins/publish/validate_editorial_asset_name.py +++ b/client/ayon_core/plugins/publish/validate_editorial_asset_name.py @@ -16,7 +16,6 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin): label = "Validate Editorial Asset Name" hosts = [ "hiero", - "standalonepublisher", "resolve", "flame", "traypublisher" diff --git a/client/ayon_core/plugins/publish/validate_version.py b/client/ayon_core/plugins/publish/validate_version.py index 42073e16f6..6d5694eb5b 100644 --- a/client/ayon_core/plugins/publish/validate_version.py +++ b/client/ayon_core/plugins/publish/validate_version.py @@ -11,7 +11,7 @@ class ValidateVersion(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder label = "Validate Version" - hosts = ["nuke", "maya", "houdini", "blender", "standalonepublisher", + hosts = ["nuke", "maya", "houdini", "blender", "photoshop", "aftereffects"] optional = False diff --git a/client/ayon_core/settings/entities/enum_entity.py b/client/ayon_core/settings/entities/enum_entity.py index 26ecd33551..fa5d652ba5 100644 --- a/client/ayon_core/settings/entities/enum_entity.py +++ b/client/ayon_core/settings/entities/enum_entity.py @@ -169,7 +169,6 @@ class HostsEnumEntity(BaseEnumEntity): "resolve", "tvpaint", "unreal", - "standalonepublisher", "substancepainter", "traypublisher", "webpublisher" diff --git a/client/ayon_core/tools/standalonepublish/__init__.py b/client/ayon_core/tools/standalonepublish/__init__.py deleted file mode 100644 index d2ef73af00..0000000000 --- a/client/ayon_core/tools/standalonepublish/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .app import ( - main, - Window -) - - -__all__ = ( - "main", - "Window" -) diff --git a/client/ayon_core/tools/standalonepublish/app.py b/client/ayon_core/tools/standalonepublish/app.py deleted file mode 100644 index 428cd1d2bb..0000000000 --- a/client/ayon_core/tools/standalonepublish/app.py +++ /dev/null @@ -1,244 +0,0 @@ -import os -import sys -import ctypes -import signal - -from bson.objectid import ObjectId -from qtpy import QtWidgets, QtCore, QtGui - -from ayon_core.client import get_asset_by_id - -from .widgets import ( - AssetWidget, FamilyWidget, ComponentsWidget, ShadowWidget -) -from .widgets.constants import HOST_NAME -from ayon_core import style -from ayon_core import resources -from ayon_core.pipeline import AvalonMongoDB -from ayon_core.modules import ModulesManager - - -class Window(QtWidgets.QDialog): - """Main window of Standalone publisher. - - :param parent: Main widget that cares about all GUIs - :type parent: QtWidgets.QMainWindow - """ - _jobs = {} - valid_family = False - valid_components = False - initialized = False - WIDTH = 1100 - HEIGHT = 500 - - def __init__(self, pyblish_paths, parent=None): - super(Window, self).__init__(parent=parent) - self._db = AvalonMongoDB() - self._db.install() - - try: - settings = QtCore.QSettings("pypeclub", "StandalonePublisher") - except Exception: - settings = None - - self._settings = settings - - self.pyblish_paths = pyblish_paths - - self.setWindowTitle("Standalone Publish") - self.setFocusPolicy(QtCore.Qt.StrongFocus) - self.setAttribute(QtCore.Qt.WA_DeleteOnClose) - - # Validators - self.valid_parent = False - - # assets widget - widget_assets = AssetWidget(self._db, settings, self) - - # family widget - widget_family = FamilyWidget(dbcon=self._db, parent=self) - - # components widget - widget_components = ComponentsWidget(parent=self) - - # Body - body = QtWidgets.QSplitter() - body.setContentsMargins(0, 0, 0, 0) - body.setSizePolicy( - QtWidgets.QSizePolicy.Expanding, - QtWidgets.QSizePolicy.Expanding - ) - body.setOrientation(QtCore.Qt.Horizontal) - body.addWidget(widget_assets) - body.addWidget(widget_family) - body.addWidget(widget_components) - body.setStretchFactor(body.indexOf(widget_assets), 2) - body.setStretchFactor(body.indexOf(widget_family), 3) - body.setStretchFactor(body.indexOf(widget_components), 5) - - layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(body) - - self.resize(self.WIDTH, self.HEIGHT) - - # signals - widget_assets.selection_changed.connect(self.on_asset_changed) - widget_assets.task_changed.connect(self._on_task_change) - widget_assets.project_changed.connect(self.on_project_change) - widget_family.stateChanged.connect(self.set_valid_family) - - self.widget_assets = widget_assets - self.widget_family = widget_family - self.widget_components = widget_components - - # on start - self.on_start() - - @property - def db(self): - ''' Returns DB object for MongoDB I/O - ''' - return self._db - - def on_start(self): - ''' Things must be done when initialized. - ''' - # Refresh asset input in Family widget - self.on_asset_changed() - self.widget_components.validation() - # Initializing shadow widget - self.shadow_widget = ShadowWidget(self) - self.shadow_widget.setVisible(False) - - def resizeEvent(self, event=None): - ''' Helps resize shadow widget - ''' - position_x = ( - self.frameGeometry().width() - - self.shadow_widget.frameGeometry().width() - ) / 2 - position_y = ( - self.frameGeometry().height() - - self.shadow_widget.frameGeometry().height() - ) / 2 - self.shadow_widget.move(position_x, position_y) - w = self.frameGeometry().width() - h = self.frameGeometry().height() - self.shadow_widget.resize(QtCore.QSize(w, h)) - if event: - super().resizeEvent(event) - - def on_project_change(self, project_name): - self.widget_family.refresh() - - def on_asset_changed(self): - '''Callback on asset selection changed - - Updates the task view. - - ''' - selected = [ - asset_id for asset_id in self.widget_assets.get_selected_assets() - if isinstance(asset_id, ObjectId) - ] - if len(selected) == 1: - self.valid_parent = True - project_name = self.db.active_project() - asset = get_asset_by_id( - project_name, selected[0], fields=["name"] - ) - self.widget_family.change_asset(asset['name']) - else: - self.valid_parent = False - self.widget_family.change_asset(None) - self.widget_family.on_data_changed() - - def _on_task_change(self): - self.widget_family.on_task_change() - - def keyPressEvent(self, event): - ''' Handling Ctrl+V KeyPress event - Can handle: - - files/folders in clipboard (tested only on Windows OS) - - copied path of file/folder in clipboard ('c:/path/to/folder') - ''' - if ( - event.key() == QtCore.Qt.Key_V - and event.modifiers() == QtCore.Qt.ControlModifier - ): - clip = QtWidgets.QApplication.clipboard() - self.widget_components.process_mime_data(clip) - super().keyPressEvent(event) - - def working_start(self, msg=None): - ''' Shows shadowed foreground with message - :param msg: Message that will be displayed - (set to `Please wait...` if `None` entered) - :type msg: str - ''' - if msg is None: - msg = 'Please wait...' - self.shadow_widget.message = msg - self.shadow_widget.setVisible(True) - self.resizeEvent() - QtWidgets.QApplication.processEvents() - - def working_stop(self): - ''' Hides shadowed foreground - ''' - if self.shadow_widget.isVisible(): - self.shadow_widget.setVisible(False) - # Refresh version - self.widget_family.on_version_refresh() - - def set_valid_family(self, valid): - ''' Sets `valid_family` attribute for validation - - .. note:: - if set to `False` publishing is not possible - ''' - self.valid_family = valid - # If widget_components not initialized yet - if hasattr(self, 'widget_components'): - self.widget_components.validation() - - def collect_data(self): - ''' Collecting necessary data for pyblish from child widgets - ''' - data = {} - data.update(self.widget_assets.collect_data()) - data.update(self.widget_family.collect_data()) - data.update(self.widget_components.collect_data()) - - return data - - -def main(): - os.environ["AVALON_APP"] = HOST_NAME - - # Allow to change icon of running process in windows taskbar - if os.name == "nt": - ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( - u"standalonepublish" - ) - - qt_app = QtWidgets.QApplication([]) - # app.setQuitOnLastWindowClosed(False) - qt_app.setStyleSheet(style.load_stylesheet()) - icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) - qt_app.setWindowIcon(icon) - - def signal_handler(sig, frame): - print("You pressed Ctrl+C. Process ended.") - qt_app.quit() - - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - - modules_manager = ModulesManager() - module = modules_manager.modules_by_name["standalonepublisher"] - - window = Window(module.publish_paths) - window.show() - - sys.exit(qt_app.exec_()) diff --git a/client/ayon_core/tools/standalonepublish/publish.py b/client/ayon_core/tools/standalonepublish/publish.py deleted file mode 100644 index 08c95228cd..0000000000 --- a/client/ayon_core/tools/standalonepublish/publish.py +++ /dev/null @@ -1,27 +0,0 @@ -import os -import sys - -import pyblish.api -from ayon_core.pipeline import install_openpype_plugins -from ayon_core.tools.utils.host_tools import show_publish - - -def main(env): - # Registers pype's Global pyblish plugins - install_openpype_plugins() - - # Register additional paths - addition_paths_str = env.get("PUBLISH_PATHS") or "" - addition_paths = addition_paths_str.split(os.pathsep) - for path in addition_paths: - path = os.path.normpath(path) - if not os.path.exists(path): - continue - pyblish.api.register_plugin_path(path) - - return show_publish() - - -if __name__ == "__main__": - result = main(os.environ) - sys.exit(not bool(result)) diff --git a/client/ayon_core/tools/standalonepublish/widgets/__init__.py b/client/ayon_core/tools/standalonepublish/widgets/__init__.py deleted file mode 100644 index d79654498d..0000000000 --- a/client/ayon_core/tools/standalonepublish/widgets/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -from qtpy import QtCore - -HelpRole = QtCore.Qt.UserRole + 2 -FamilyRole = QtCore.Qt.UserRole + 3 -ExistsRole = QtCore.Qt.UserRole + 4 -PluginRole = QtCore.Qt.UserRole + 5 -PluginKeyRole = QtCore.Qt.UserRole + 6 - -from .model_node import Node -from .model_tree import TreeModel -from .model_asset import AssetModel, _iter_model_rows -from .model_filter_proxy_exact_match import ExactMatchesFilterProxyModel -from .model_filter_proxy_recursive_sort import RecursiveSortFilterProxyModel -from .model_tasks_template import TasksTemplateModel -from .model_tree_view_deselectable import DeselectableTreeView - -from .widget_asset import AssetWidget - -from .widget_family_desc import FamilyDescriptionWidget -from .widget_family import FamilyWidget - -from .widget_drop_empty import DropEmpty -from .widget_component_item import ComponentItem -from .widget_components_list import ComponentsList -from .widget_drop_frame import DropDataFrame -from .widget_components import ComponentsWidget - -from .widget_shadow import ShadowWidget diff --git a/client/ayon_core/tools/standalonepublish/widgets/constants.py b/client/ayon_core/tools/standalonepublish/widgets/constants.py deleted file mode 100644 index 0ecc8e82e7..0000000000 --- a/client/ayon_core/tools/standalonepublish/widgets/constants.py +++ /dev/null @@ -1 +0,0 @@ -HOST_NAME = "standalonepublisher" diff --git a/client/ayon_core/tools/standalonepublish/widgets/model_asset.py b/client/ayon_core/tools/standalonepublish/widgets/model_asset.py deleted file mode 100644 index 003c2b106f..0000000000 --- a/client/ayon_core/tools/standalonepublish/widgets/model_asset.py +++ /dev/null @@ -1,186 +0,0 @@ -import logging -import collections - -from qtpy import QtCore, QtGui -import qtawesome - -from ayon_core.client import get_assets -from ayon_core.style import ( - get_default_entity_icon_color, - get_deprecated_entity_font_color, -) - -from . import TreeModel, Node - -log = logging.getLogger(__name__) - - -def _iter_model_rows(model, - column, - include_root=False): - """Iterate over all row indices in a model""" - indices = [QtCore.QModelIndex()] # start iteration at root - - for index in indices: - - # Add children to the iterations - child_rows = model.rowCount(index) - for child_row in range(child_rows): - child_index = model.index(child_row, column, index) - indices.append(child_index) - - if not include_root and not index.isValid(): - continue - - yield index - - -class AssetModel(TreeModel): - """A model listing assets in the active project. - - The assets are displayed in a treeview, they are visually parented by - a `visualParent` field in the database containing an `_id` to a parent - asset. - - """ - - COLUMNS = ["label"] - Name = 0 - Deprecated = 2 - ObjectId = 3 - - DocumentRole = QtCore.Qt.UserRole + 2 - ObjectIdRole = QtCore.Qt.UserRole + 3 - - def __init__(self, dbcon, parent=None): - super(AssetModel, self).__init__(parent=parent) - self.dbcon = dbcon - - self._default_asset_icon_color = QtGui.QColor( - get_default_entity_icon_color() - ) - self._deprecated_asset_font_color = QtGui.QColor( - get_deprecated_entity_font_color() - ) - - self.refresh() - - def _add_hierarchy(self, assets, parent=None): - """Add the assets that are related to the parent as children items. - - This method does *not* query the database. These instead are queried - in a single batch upfront as an optimization to reduce database - queries. Resulting in up to 10x speed increase. - - Args: - assets (dict): All assets from current project. - """ - parent_id = parent["_id"] if parent else None - current_assets = assets.get(parent_id, list()) - - for asset in current_assets: - # get label from data, otherwise use name - data = asset.get("data", {}) - label = data.get("label") or asset["name"] - tags = data.get("tags", []) - - # store for the asset for optimization - deprecated = "deprecated" in tags - - node = Node({ - "_id": asset["_id"], - "name": asset["name"], - "label": label, - "type": asset["type"], - "tags": ", ".join(tags), - "deprecated": deprecated, - "_document": asset - }) - self.add_child(node, parent=parent) - - # Add asset's children recursively if it has children - if asset["_id"] in assets: - self._add_hierarchy(assets, parent=node) - - def refresh(self): - """Refresh the data for the model.""" - - project_name = self.dbcon.active_project() - self.clear() - if not project_name: - return - - self.beginResetModel() - - # Get all assets in current project sorted by name - asset_docs = get_assets(project_name) - db_assets = list( - sorted(asset_docs, key=lambda item: item["name"]) - ) - - # Group the assets by their visual parent's id - assets_by_parent = collections.defaultdict(list) - for asset in db_assets: - parent_id = asset.get("data", {}).get("visualParent") - assets_by_parent[parent_id].append(asset) - - # Build the hierarchical tree items recursively - self._add_hierarchy( - assets_by_parent, - parent=None - ) - - self.endResetModel() - - def flags(self, index): - return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable - - def data(self, index, role): - - if not index.isValid(): - return - - node = index.internalPointer() - if role == QtCore.Qt.DecorationRole: # icon - - column = index.column() - if column == self.Name: - - # Allow a custom icon and custom icon color to be defined - data = node.get("_document", {}).get("data", {}) - icon = data.get("icon", None) - color = data.get("color", self._default_asset_icon_color) - - if icon is None: - # Use default icons if no custom one is specified. - # If it has children show a full folder, otherwise - # show an open folder - has_children = self.rowCount(index) > 0 - icon = "folder" if has_children else "folder-o" - - # Make the color darker when the asset is deprecated - if node.get("deprecated", False): - color = QtGui.QColor(color).darker(250) - - try: - key = "fa.{0}".format(icon) # font-awesome key - icon = qtawesome.icon(key, color=color) - return icon - except Exception as exception: - # Log an error message instead of erroring out completely - # when the icon couldn't be created (e.g. invalid name) - log.error(exception) - - return - - if role == QtCore.Qt.ForegroundRole: # font color - if "deprecated" in node.get("tags", []): - return QtGui.QColor(self._deprecated_asset_font_color) - - if role == self.ObjectIdRole: - return node.get("_id", None) - - if role == self.DocumentRole: - return node.get("_document", None) - - return super(AssetModel, self).data(index, role) diff --git a/client/ayon_core/tools/standalonepublish/widgets/model_filter_proxy_exact_match.py b/client/ayon_core/tools/standalonepublish/widgets/model_filter_proxy_exact_match.py deleted file mode 100644 index df9c6fb35f..0000000000 --- a/client/ayon_core/tools/standalonepublish/widgets/model_filter_proxy_exact_match.py +++ /dev/null @@ -1,28 +0,0 @@ -from qtpy import QtCore - - -class ExactMatchesFilterProxyModel(QtCore.QSortFilterProxyModel): - """Filter model to where key column's value is in the filtered tags""" - - def __init__(self, *args, **kwargs): - super(ExactMatchesFilterProxyModel, self).__init__(*args, **kwargs) - self._filters = set() - - def setFilters(self, filters): - self._filters = set(filters) - - def filterAcceptsRow(self, source_row, source_parent): - - # No filter - if not self._filters: - return True - - else: - model = self.sourceModel() - column = self.filterKeyColumn() - idx = model.index(source_row, column, source_parent) - data = model.data(idx, self.filterRole()) - if data in self._filters: - return True - else: - return False diff --git a/client/ayon_core/tools/standalonepublish/widgets/model_filter_proxy_recursive_sort.py b/client/ayon_core/tools/standalonepublish/widgets/model_filter_proxy_recursive_sort.py deleted file mode 100644 index 602faaa489..0000000000 --- a/client/ayon_core/tools/standalonepublish/widgets/model_filter_proxy_recursive_sort.py +++ /dev/null @@ -1,32 +0,0 @@ -import re -from qtpy import QtCore - - -class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel): - """Filters to the regex if any of the children matches allow parent""" - def filterAcceptsRow(self, row, parent): - if hasattr(self, "filterRegExp"): - regex = self.filterRegExp() - else: - regex = self.filterRegularExpression() - pattern = regex.pattern() - if pattern: - model = self.sourceModel() - source_index = model.index(row, self.filterKeyColumn(), parent) - if source_index.isValid(): - # Check current index itself - key = model.data(source_index, self.filterRole()) - if re.search(pattern, key, re.IGNORECASE): - return True - - # Check children - rows = model.rowCount(source_index) - for i in range(rows): - if self.filterAcceptsRow(i, source_index): - return True - - # Otherwise filter it - return False - - return super(RecursiveSortFilterProxyModel, - self).filterAcceptsRow(row, parent) diff --git a/client/ayon_core/tools/standalonepublish/widgets/model_node.py b/client/ayon_core/tools/standalonepublish/widgets/model_node.py deleted file mode 100644 index e8326d5b90..0000000000 --- a/client/ayon_core/tools/standalonepublish/widgets/model_node.py +++ /dev/null @@ -1,56 +0,0 @@ -import logging - - -log = logging.getLogger(__name__) - - -class Node(dict): - """A node that can be represented in a tree view. - - The node can store data just like a dictionary. - - >>> data = {"name": "John", "score": 10} - >>> node = Node(data) - >>> assert node["name"] == "John" - - """ - - def __init__(self, data=None): - super(Node, self).__init__() - - self._children = list() - self._parent = None - - if data is not None: - assert isinstance(data, dict) - self.update(data) - - def childCount(self): - return len(self._children) - - def child(self, row): - - if row >= len(self._children): - log.warning("Invalid row as child: {0}".format(row)) - return - - return self._children[row] - - def children(self): - return self._children - - def parent(self): - return self._parent - - def row(self): - """ - Returns: - int: Index of this node under parent""" - if self._parent is not None: - siblings = self.parent().children() - return siblings.index(self) - - def add_child(self, child): - """Add a child to this node""" - child._parent = self - self._children.append(child) diff --git a/client/ayon_core/tools/standalonepublish/widgets/model_tasks_template.py b/client/ayon_core/tools/standalonepublish/widgets/model_tasks_template.py deleted file mode 100644 index 5c2f282304..0000000000 --- a/client/ayon_core/tools/standalonepublish/widgets/model_tasks_template.py +++ /dev/null @@ -1,66 +0,0 @@ -from qtpy import QtCore -import qtawesome - -from ayon_core.style import get_default_entity_icon_color - -from . import Node, TreeModel - - -class TasksTemplateModel(TreeModel): - """A model listing the tasks combined for a list of assets""" - - COLUMNS = ["Tasks"] - - def __init__(self, selectable=True): - super(TasksTemplateModel, self).__init__() - self.selectable = selectable - self.icon = qtawesome.icon( - 'fa.calendar-check-o', - color=get_default_entity_icon_color() - ) - - def set_tasks(self, tasks): - """Set assets to track by their database id - - Arguments: - asset_ids (list): List of asset ids. - - """ - - self.clear() - - # let cleared task view if no tasks are available - if len(tasks) == 0: - return - - self.beginResetModel() - - for task in tasks: - node = Node({ - "Tasks": task, - "icon": self.icon - }) - self.add_child(node) - - self.endResetModel() - - def flags(self, index): - if self.selectable is False: - return QtCore.Qt.ItemIsEnabled - else: - return ( - QtCore.Qt.ItemIsEnabled | - QtCore.Qt.ItemIsSelectable - ) - - def data(self, index, role): - - if not index.isValid(): - return - - # Add icon to the first column - if role == QtCore.Qt.DecorationRole: - if index.column() == 0: - return index.internalPointer()['icon'] - - return super(TasksTemplateModel, self).data(index, role) diff --git a/client/ayon_core/tools/standalonepublish/widgets/model_tree.py b/client/ayon_core/tools/standalonepublish/widgets/model_tree.py deleted file mode 100644 index 040e95d944..0000000000 --- a/client/ayon_core/tools/standalonepublish/widgets/model_tree.py +++ /dev/null @@ -1,122 +0,0 @@ -from qtpy import QtCore -from . import Node - - -class TreeModel(QtCore.QAbstractItemModel): - - COLUMNS = list() - ItemRole = QtCore.Qt.UserRole + 1 - - def __init__(self, parent=None): - super(TreeModel, self).__init__(parent) - self._root_node = Node() - - def rowCount(self, parent): - if parent.isValid(): - node = parent.internalPointer() - else: - node = self._root_node - - return node.childCount() - - def columnCount(self, parent): - return len(self.COLUMNS) - - def data(self, index, role): - - if not index.isValid(): - return None - - if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole: - - node = index.internalPointer() - column = index.column() - - key = self.COLUMNS[column] - return node.get(key, None) - - if role == self.ItemRole: - return index.internalPointer() - - def setData(self, index, value, role=QtCore.Qt.EditRole): - """Change the data on the nodes. - - Returns: - bool: Whether the edit was successful - """ - - if index.isValid(): - if role == QtCore.Qt.EditRole: - - node = index.internalPointer() - column = index.column() - key = self.COLUMNS[column] - node[key] = value - - # passing `list()` for PyQt5 (see PYSIDE-462) - self.dataChanged.emit(index, index, list()) - - # must return true if successful - return True - - return False - - def setColumns(self, keys): - assert isinstance(keys, (list, tuple)) - self.COLUMNS = keys - - def headerData(self, section, orientation, role): - - if role == QtCore.Qt.DisplayRole: - if section < len(self.COLUMNS): - return self.COLUMNS[section] - - super(TreeModel, self).headerData(section, orientation, role) - - def flags(self, index): - return ( - QtCore.Qt.ItemIsEnabled | - QtCore.Qt.ItemIsSelectable - ) - - def parent(self, index): - - node = index.internalPointer() - parent_node = node.parent() - - # If it has no parents we return invalid - if parent_node == self._root_node or not parent_node: - return QtCore.QModelIndex() - - return self.createIndex(parent_node.row(), 0, parent_node) - - def index(self, row, column, parent): - """Return index for row/column under parent""" - - if not parent.isValid(): - parentNode = self._root_node - else: - parentNode = parent.internalPointer() - - childItem = parentNode.child(row) - if childItem: - return self.createIndex(row, column, childItem) - else: - return QtCore.QModelIndex() - - def add_child(self, node, parent=None): - if parent is None: - parent = self._root_node - - parent.add_child(node) - - def column_name(self, column): - """Return column key by index""" - - if column < len(self.COLUMNS): - return self.COLUMNS[column] - - def clear(self): - self.beginResetModel() - self._root_node = Node() - self.endResetModel() diff --git a/client/ayon_core/tools/standalonepublish/widgets/model_tree_view_deselectable.py b/client/ayon_core/tools/standalonepublish/widgets/model_tree_view_deselectable.py deleted file mode 100644 index 3c8c760eca..0000000000 --- a/client/ayon_core/tools/standalonepublish/widgets/model_tree_view_deselectable.py +++ /dev/null @@ -1,16 +0,0 @@ -from qtpy import QtWidgets, QtCore - - -class DeselectableTreeView(QtWidgets.QTreeView): - """A tree view that deselects on clicking on an empty area in the view""" - - def mousePressEvent(self, event): - - index = self.indexAt(event.pos()) - if not index.isValid(): - # clear the selection - self.clearSelection() - # clear the current index - self.setCurrentIndex(QtCore.QModelIndex()) - - QtWidgets.QTreeView.mousePressEvent(self, event) diff --git a/client/ayon_core/tools/standalonepublish/widgets/resources/__init__.py b/client/ayon_core/tools/standalonepublish/widgets/resources/__init__.py deleted file mode 100644 index ce329ee585..0000000000 --- a/client/ayon_core/tools/standalonepublish/widgets/resources/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -import os - - -resource_path = os.path.dirname(__file__) - - -def get_resource(*args): - """ Serves to simple resources access - - :param \*args: should contain *subfolder* names and *filename* of - resource from resources folder - :type \*args: list - """ - return os.path.normpath(os.path.join(resource_path, *args)) diff --git a/client/ayon_core/tools/standalonepublish/widgets/resources/edit.svg b/client/ayon_core/tools/standalonepublish/widgets/resources/edit.svg deleted file mode 100644 index 26451b4a9d..0000000000 --- a/client/ayon_core/tools/standalonepublish/widgets/resources/edit.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/client/ayon_core/tools/standalonepublish/widgets/resources/file.png b/client/ayon_core/tools/standalonepublish/widgets/resources/file.png deleted file mode 100644 index 7a830ad13305a4dbaf73489ed9f373f5382243c0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 803 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD3?#3*wSy!+2l#}z0_p!F3`DHD_7v#tfRZ4; zUDcV|DfRTh`5BL%&h#P@`jH7S&J4gS-O11s&(r( zY}~YY%hnw`_w2oP{l?8(cke%Z@%qh&PhY-$|MC0J-+!HdPj>>%W=!&ScL{i$=d%aM zZt`?-49U3n_SW$rCPx9b2XT?iOi5zKO`ObHT}elJx(@C6f4So3%AoQaFB{m`zI$Er zH~#B85t%B+ec}6V?5Ht0p~bv)*(zD*m8D7l{&Xvf<+cS~pK)JAO!DWHq;K7XCcnQmtP_R_Bv-WZSWV&Q5R@v)N*{l zAdvGoUz@vOL6cYo1B-P;Bd5cLZjl`fEY%i`ybiAzR6j6!$R@EcGCUM?n!q4%?N1X& z!-pH%Cm1;VYnlWaHoOyD!oV|6jXQ~HCBtEHPbPK`Emn~q?-^dN4`*Z*32;Co z8tqwcr2gp8sW<7$JnHvmRiPi7j*H>-jl#Qi_MYY;Gp00i_>zopr0D++3 A5&!@I diff --git a/client/ayon_core/tools/standalonepublish/widgets/resources/files.png b/client/ayon_core/tools/standalonepublish/widgets/resources/files.png deleted file mode 100644 index f6f89fe14992cde5d9509c2606efe946c1a9e95c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 484 zcmVsb@n$`BycGxbTj??esuHn0x-zMy6fcmeDIGr%

n4igvKbDOy^aUL%8G~`^%3w6m;|=edlB&^ zXNJwBvu=~lO3zkHdwf;99pEl7)#UUwJA4JK72!=_BQ?l2@SO9P+(wIP`i&iPew~vk zY&9bG)s>d8F7U7_nZg>Gp`_2i?TXw7U?B%3*0#;XJ*cdj3RcheW zuaepUuJz+O6Xdn}z9nxSxHSl7X*~-(Z3+8FRoA%7|9|XZ^apsG?JOdGsCR(tBZBoJ z;#&&q0!Ia~N5v%1Ne$pJa0NJwh)-!v3LC}@3&3)*-U70)OEE)E9WKcXxkLb{R~fMx aa*4m35Kv^)H)-Dh00008qo>> diff --git a/client/ayon_core/tools/standalonepublish/widgets/resources/houdini.png b/client/ayon_core/tools/standalonepublish/widgets/resources/houdini.png deleted file mode 100644 index 11cfa46dce2afe609a525b9b741f612445d8fe98..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 262950 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 l89UyV%&>*Q|1Nzx^yCixAVVXrD*fNYN^ - - - - - - - - - diff --git a/client/ayon_core/tools/standalonepublish/widgets/resources/maya.png b/client/ayon_core/tools/standalonepublish/widgets/resources/maya.png deleted file mode 100644 index e84a6a3742f325769d6cf619045d17107d925f3c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41557 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 diff --git a/client/ayon_core/tools/standalonepublish/widgets/resources/menu.png b/client/ayon_core/tools/standalonepublish/widgets/resources/menu.png deleted file mode 100644 index da83b4524468dff7e927baac8927e655e004632b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1629 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+yeRz~mX=6XFWw0zpGVLuhCykW5QU z1Cl^cSXc-oOG-+BWLa4mkSs4RFDNLetgI|9E(Vf7sj8}~($Z2O2~<#BT@4|DvK18- zKv^IOl!cH$DWDdh2|&d_5-1Ll0*V6_z-1v)5E3p0Q2-=?;t)fCdV#V)E|3HQWCajq zKtYHsgoFsfNw6l6E}$%&glNI49-<7*OrRhfzzqS4K-h58;3CLskd1;H0%yQgA{&BB z7ONVFVoaC74FNJBvdAKou>b%6&)f007MPL_mIV0)GcYnSv#_#paB^|;@bd8s2#bh{ ziAzXINz2H}D<~={tEj4LY3t}37#W+GnweYK*xK1UIyt+zdU$$y`}p|>1Obje8tLDtJkbuzhUF1&0Dwa*tL7l{sRXO9XWdJ_{meJ&zw7d;o_ysSFT;ZefQq| z2M-@VefIpt%hzw-zI*@S zB;xSi=|7`+6Ge`nf2-v!IBBg?blE0#EluxfDW_-YTx@b`aXBe;vs_nYiFfACy8+=} zexCnx;IPl#nbyYND{Ja!S)MMxSNv?x`|7?9HmB<;G3)X&UcSi4T%mW;Lr!&>f%SGd z)2jPV&KSM2&AD#fFzYI=<_)-)Vo%{QER>p7X^06Yt9e@NEAT`D^L{$Jra5 zHp;ej*XDj!xLmPRu4!@BNj8hTW!L-+1OHP;OtUVk-yn3;kU!I zjw>sJGPsVf5afKDx-zs$qRQP%>78O2XH%8OgxV`zsaH0-uM0?;yI_iU(BZn*EHgt+ zx^Ih6zu~pmB67Ldt;K0sN~hLs^IaHw=g8{Tr&ZMpeR5|vMDG(`E*5uLdB>`4y~}Sq za|jBX2wv20F)N(#%v(r$$*Jx4e>in%ZuJW|S9#z2=JT^Af7A`bIKw_E-}uga^t*Dd zl}og81WT}u>}%F^lfVZrBfkGllDxAjOK;NYk_CHiab-yT_+G?oJin2%UO${OZ|NZ$ z$EW*W$C^1Uiu}Lzl-|j2BFj(mDc)I{Q<$=-`}nh2Ytk(aPi?v}KW4M^_wDh|k0)(? z!SF;<-GZ_D_o+_{Pb7#ipF7<7FYs<#Qu5hvqANap(5+Uy;!^d!e@*MpAHGRn!Q&278>FVdQ&MBb@0KA_L4*&oF diff --git a/client/ayon_core/tools/standalonepublish/widgets/resources/menu_disabled.png b/client/ayon_core/tools/standalonepublish/widgets/resources/menu_disabled.png deleted file mode 100644 index e4758f0b199c554da8bd0932cea2d13b2d2bcced..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1629 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+yeRz~mX=6XFWwLO^I}sDp!pv9YnX zwl$>*?tM+1A$9=H})=E|6_zW(HJbYiny|Wd#&8H8lmYfeL`?fm}ONQ=qto zg@v7&nT?GNkPQ(BDzG&-2a*sGh#DIt5gQ8&ph{#BxMHAbK85AR7YLh)@hS0j3_P2CEjh zULfQD|NkdRQ@5x&}tZCZ=ZQRyMYF_Kr@@F0LM)Ufw=_{sBS3A)#U65z#TRafvCZY3UhRd4(mV zW#yGMb@dI6P0cN>Z5^Fm-95d1{SzimnmlFdjG43M&0nx^(c&dbmn~nha@FcJYu9hs zxM}m&Z98`D-n0L}!9z!m9y@;W)af(l&R@8A>GG9p*KgmwcmKh|$4{RDTZ?yo{GGGBQ`_o%E1XU1ngtUCy-X{*yCCuWWO!TQ|(R zo+`Og)6-LPCHtvYRSfsu$z&aMek`sbfA*GX^SR%0-YeR2iZjy8 zf0m>9gS?LKI_-DbUo-zc&79{vasR~oG66i>e?|V9dcbk^MyHLkZQZrGpA{}wER}0o zoOP1TB5&C>|3g(ztDlI=@fkQj_|FpFqO@Z9mL9|HcaLrT_#$4)>`;+mr-iG)!L?Jm zGw&?1d~yD9+`c|@e}R9!ck9J2oD^(T+WBdon$~2Iwwo69`ia)Di$y<4l?1FklWcz5 z#bwDnr>B<|^}k5icj=SPdKZ^x2Vyq1R2n$D6jkJJc1!r}Fs}QYjqd9LlIAX$;vICj?lsHIkdyA)BGhkqEw+eUE_Q2i zT9(qOb=!Ow#@;!yy7g&Q^+KQA*$vVAgqMrOT~^+)YFqE}+s+(20o?BcQQa`>I@fy!>dbw;qTi-pMwSV`&F}9hp>bc!pMHBst z*V^xXwkx@NQGchY@2}+h%dg%`;<N{%q*;I9GqO- zJiL7T0>UDqV&W2#QqnTA@(PMd$||brTG~3g21dpvre@|=Hnw*5j!w=lt{$FV-adZ* z0YSkbp<&?>(J`@ci7BaR=^0shg(anB<&`yc^$m?p%`L5M9i3g>J-vPX6DCfYJZ0*P znX~52U$Ah|;w4L$Enl&6)#^2C*KgRkY4g@?J9h2fv;V-sLr0DtJAU%i=`-ifU$}Vb z@|A1XZ{NLl|G~q@PoF)1@$&VXx9{G6`1s}9_a8rh{r>a!pRdp7O+e2uCV9KNF#c!K zy$8%IUp-wMLn03Eo&GbLH&NvH`L|l$f|J%NMVD<-*V6Q!mU4QQ&c!CD7MGJkH_LTZ zmUw6Gyc-by<>&c72M+t(ooQ|Sy|Si$mgVX4d&ST8ysz%-U~{^j60Ht%oTbk zJ>*oE8CY+ZGp)M+{ojDv*)dUD`N{1 zd+v!Ji)+ZAy=B^b?zf!xing5Mj5PC~dy0MGVc zk-w%MaGbr-X`^ggcWv%xh07I7<(d{}on*7fTXxO=P}S4wC*pE^2F?%uvxK)OtysRL z$8h`IV_QGIh?g=uRAks`;VN)&?Ue4!J4-BIoPQj*ug~0H;9u|Eda(;91zVMNewwGI zHCd$XrbWGeqIK+I(N9t(0c+1Bo8NYESu)S*>7_;eFB0}$`lPeo#pT(7n2jx!2F@-; z75SUp5`H^O>$tKqD1+&u|A$kT=2pLebCvhKZ$3Y3@<-h;j5F+$ z@{RAzN53oQTDe3kN3aCj$i8MxHwk?3GUEH+B*{Cgvh*gME?KbW7FUMUkMBji#`7CF z>-EDq^Ohd6aeTV}b*!1wqR9VSPwAcfCbIk_pW>aRIfW^Ux{p7bwIS-~DflZDy=`ZueHvME~No_Pd|$O733N-)ZXmEBXHNtM`(4?%Z8xTd{4a xwB^C(OIzLU%QpT`{}I;nzt6A0vgG*x_!Q-*UelL+F$LxZ22WQ%mvv4FO#m7ceop`Z diff --git a/client/ayon_core/tools/standalonepublish/widgets/resources/menu_pressed.png b/client/ayon_core/tools/standalonepublish/widgets/resources/menu_pressed.png deleted file mode 100644 index a5f931b2c4107986d389c1df0fe910cae0145d0d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1626 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+yeRz~mO-6XFWw{s)6qt5)sbzyHjc zGgq%(1u~8uI|gK5ym%4FJ#ys8i4!M)-1FzppFDXI$T)ZI98d}C-@|vuDo&xj;5h9H1Obje8tLDtJkbuzhUF1&0Dwa*tL7l{sRXO z9XWdJ_{meJ&zw7d;o_ysSFT;ZefQq|2M-@VefIpt%hzw-zI*@SlFzi8#D>`p;&~JgEwON2BZ`8!c3^6kg@7?ixX=%gnJ500LuO3n|-;)3B^5^+~gm!&8 zp1tz0U+Lk`p11z3j4ep)xhH-st|5Q+mTB|3-*Vn7+H#6B(#(IBqxplpj_*3{ciLYw z|31x}=R9%$#QQP1jk0aswYi@aE>|p-Yg(LjlFcG-*){(|RZpv* zh|BRAI6wH$65gV;V)>RH!|ivEZT#V(u_Y*pI%X`Y(aWRbR;7WMjx*0GC4KS`AYtUZ%#e%r-m$vmg0mlpNENZ5Dj zlg@e8XnV|>jIMIE|}sSbhz#{%gm6I?%N{NZ+I=Xh+HmqYjIkZ(y4XZd>6*vIkLL- zX;t+?pWN9E(ffp#i^W}5-mz+1@ABKu9D>3of*18$%nBzw^A?g`a%%hiA5LAGTm1sg zRo?f$`TVTOA9cep&ah9)H@-6;{jQvAB=MH!N3%uKwlzjG^ z=!y>?bgLDwxKw@bU(XC+ z=Sb9bF>#kQr=mIs?JZFRdZ+xS2IM_AAQKEDFXlH>p5Q7%TxdW diff --git a/client/ayon_core/tools/standalonepublish/widgets/resources/menu_pressed_hover.png b/client/ayon_core/tools/standalonepublish/widgets/resources/menu_pressed_hover.png deleted file mode 100644 index 51503add0fbc0c7352c764623632509e14dd669b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1488 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

n)d3@oZpYFsmbJyGIDGW; zkj~p+Ug+g{ca`d=jmR)JMc46nsau7cr{9rlWSI<(D@A6YglerOVYn@6o{}b=2qyUA zpigCL_6f=R$5a4u5>T1x*p9QKe|*7*3(ACJ0up3rP}dO-2nM-!`duIRJY~;kB4nC= z10NX<9tLycH1p!Epgk2jb~hXuALBM9bkhMuj921^xKq3(xY$Tm4Mh;cA>XM;Nkj=| z6Tp zDoSltDBFT)g(zgSI;gN6AHfQ#TtQ@nXj_nY?Dq%%(_FT3lrs-{gyZEg?xc>55{w&s zU;$iOq@Z-2Bntv4(=BiuHJh)wnK%Dz4(e#a_%PS0FO^UU<`jew63`~)6beHUvT@go zNW(EyFdJV8#L@ijN)=Vg`Pi&&q7P6LK6uE42NkPL@0&s*Z1J0Ku*ZAEv!Xu{c)^=*;w9xbiR~dDvN0ub}4ti@A~`6!6cVwNXkRN zQ45konOaXr0z>j*c;7$c=U5#iBpLc{`8$tRxPkKE-?Sz6XJ623qJ~SWhy^?TT~wb) zsJue&C9|;(AKlTzU&%3a2g>-lRtc0Tm3i7+dmHp#1?7e0!(m8|HymVNX|0&T&2gwx z1=rpq3wWpfl_hH~*WTc&9vspM?ec`UiK*^(zi z^_PEn;VA3r45m6HV)oiogJ5_t2cdcw|7mzI#cfOIGBR{7%RlXmCQ5(hAf;bE7brwX9WEen2W_xvurVq?>L_40vX z5nd%zTo!idT>LG7!?Im0Fxb_)oou7re2?IOSYq@cyduUMawwBbOR5em9iov65Muik zaBK>1%lDz&_vBTta6*h^x(6P` zwHMJBKZVUd_k=91DbbMTi6Sxzt$Qy5!ST{h>o1UCEZ*u4oaA;ML5nW~jxV*Mg+LlP zgcA-&_qug++EYPKwa>#&G)hAtWS`!3L9fRp~M)q(aMY@iLw`J76U!~Fz??$LcUpK&z(Q0q3-G$rXyy9`Y5kig$xRvF%T8)?(nrM* ze?N4)%k)*yyI1-I_!tlU&^9~_Kc>j!k&RHWwP!IbF7`cFxG#`U%NAwk;OHtx<~s${ zbb+OxC;_N-1dh=R&t_H)*vc^~OPbgx<)qC>shtD@7iCe-e75YULBf-;VERXQNt#@F zbA(r8=hDciG8rkS8iPk3#&C+DLw=PICW87^E=JdXGdpye7Oc2{Os6tJ+ARvnuEpaf z+e%=--gL2H@aOe@2eaw2o38NZ~p7jQnh;t+byp43REo(7mtUJQM zaf^Qf_MU{e^T@pVobZsC2Fmn8TN&x2Oa-}!xtG10W7>?+8`L=*d_Y>MEX6pUQId3k z$GYgEiw;uqPbTiBO9a;^QtJ5wYS%{fbq8)geHtgad}G$Tiw$QD0!C^$a5#lrVxfH0 zj8h4~1nvMZM{~b?K0SyENM`K#^S-Q&{$Yr$iy!IA+b6Y^cL_%t8*y-4V^S_m2p9_a zc@orn^IvJnW2LcUEL)wA6!&!5@RF9~*Z?8@$|^(`8yC^cmI73O7~b3=n<1t3DDh^g zG)*eKdB|fBXJs3r4EjXj@ztt#L_20RST!WD|ClK&#Geml;7JSTW~ zUesT2B6=@L^HEN%r@IU5_6iLK#Bj+CCsGX_z0C;z;|*!j!b1y!hQT&)hCtNn7V1mP z@!{(TV9XjU8DPnhTSg%h3ZF=MuUa>{TocGSrao@D(+^51?kG8LpDIxP?F#8&*P#wB zu&lDe=eu)JmjrX)6#+Q0g^ofW=y^iv>rF}JKC|qK1GJ;SoLwmEeHvGu+czAYla5~` z@0V-(DV<*$;GLbh&+FPUp7jqNG0cT+HcMA*b*E#{6Z2L^F8uq z<(|D5eH4>typ0N_)rpuc)!FA45|Y-#?=fiMwhpC!+q)$1X?H^hIhH)<07$kZwxh|<8H5cVK`iN1+P|$1JrxNVtJVie%Bj{D-f`m#R=!dfH~_#P#?E6dh`XzvbSaJ5^ct-v;18Q z?7FF;c>AKvpo&6XydULK`cgArlSe@Ap=BTX-uSpgjh~rebdoKF9s}IBQ^-Dn*z%In zivU5=6vC;Ex{6F$TMUDGB?0~3AAaCidKK+7&L;3r5z3Us6USx*<}uD^301p3FBjas z`446t9Km8%ov`D$o7rrv3+?2lLE@38V27BzBJw>1tkoU=fSGyfa|f=mwCO3wm;tXD z1fU{>A}MuT)IkdD0V{-tnRgBA%6|WhPtYZ3))ZSgb8VHLJFN5bc6t1vvhuNw4Ox3_ zY)zGTLHGlniRC)KKX~#lUkBK-D-ck`;A6(ge5H_YNCezz9rcdq^>I~?s;s0jF3Um| zwFSYlYaQC}X9=a=?z}CVlu^#*9cqdnzqTl#nlibqYJ)nbqT?9T&-EG3&4E(Qf2Za8 z_j|oy@>00$;J*F7i zF3ipR7eIg_tO4EHS!SmsOeeFLoJ1dlsODU5BKXXHHbaKwu-49LvjBg7#O-@p0)wBl zY2cbgBE$)1|7-25AEIo&x0hxK=?3ZUMq0X&?rs5*?(UZE5=6SCr8}e>VF4-WkX)AU z{e0ek;pKN`XXc(cab4G(qaTV-^q=R=m+LGHY-J&y>9KO@f2y9wUF*#K>_9)?tWzxA zXnV`a_|}gj=Yt?2lkN>M7027rPn&j`_y`Qv=7kg8l^yKguzUr4$8Zp@Hkg(^sX*-r z<_j#WjVh4)9PAUEy5ax|rEwba?_RU@z7@$Q+~}19G8~MS>Zxl?>D`9;g7+Vi6Su^R zv}-y(9+Td%OH8$7hEXxCd$lS79M*YmfFsCx{~oKbw~F*=*{&rG^qF{sJ0z^$EdBGT zS-_ZkEy{^matLcOCx6^)=Ya0d7k5)NhCe?mo`Nyqr^zzDf4%;vB4$%N25(KOWB#5U zDA*Sh*!s;9V5)dG%RQg|Ep;j?MTJTa9dl_4U7LvPz=>9H3qz2k42^1{d^(PK%Rvt!DQso>k+(-M~oO zaAyS2%N?J9YYEXNDufsdzQ}!e3T6+hF4tD+j7Vv*mh;@q)_C!h889e)NciY4dWstM z;L^I$4l1|@2r8B*Qs7jj^!VMaece4~Wx=Uv&;^ke_%qF1jv{BTC|4CMi5sPVu;o_@N6qKOS`yxRTz%T=?1?0N&mwPgU#Ps z<<}by0rv7YNQhaIjU0ublC+uzLZ3b|eyAyBl$2UC zj34J#j)Oa~u7m%V*(X>8K5niw5%4|2LA3bXmjtv^aY6-P;{PC_crGG7Qsqlb&9tHJmMpK?q<%fs7eLoYMvl zb@#Q=duz^Bdxf&d+0bEWGSip0*)hSnk649!DgA>AdO03KN!Ax^;gWC%IF(UqV(FvR z5NAsxk>>4{s#X=5?FAJwUx zcWotAVI3t+>duefFe~9bpyDwFBPrb2?}gyJb9vvOK}v-XIti{p#8DW06A!|_K3}DL z!#2)?T@#@DbG@#!G~Y_h+8^EzV2DwhM6CbS3~h1vwjw*SF^geeVyFn>e1m|GZv?$yMCOBf-V%7c)cUkhLwC&8cS6W7* zok2UAZm7!*YUKmiyN*EN&cRtr$eM1IJ)AbQwFa#&Ei^-e?>E6fYiR`<;PWFmqdEZi zKVT3qL;(0Y=!*Q!pbDLS+AQ7G>pxY9Vm{D$rG;xE)u!qF;H#^xr%R?#>kEbAI;Cjh zs#`q~f4FvD$X`OFEI?&OL#c|0EmK#)@eh(@rfp%OjIS>R?)%)Ypdi^5CLzY*_Fdk{ z_%*4}IO6^wPO{7L=B(%+=|>+@l2_~ZCwvA?a`0zvt`mIa-PqF4Q`ijiRpw!1BqeBA z=OS%ren$BA-w@4(`dnTSrg@eHSljnoc-oyGHk}BsEzb#bB=ZKoW1OnmYG=-#tP#SG z2bHK^{sliWZOFm%9!LV3pad_HEK0q&0r)C-Y#9!79aHE|D$A|sm}IpIShA;7NnuIx z=t6szn>V6yw|slxuP3^YMG;K^lpJBbRG<^)sz0v~V|S_H8JbSDZkbi^^{up>`Epm9DPN$&vl86iv;~t%um&9Vmc)vnm2JlA!TF$+*Xn?l40S_^ ztg_ub`P(u*hWdfkhbmeH92Jr@;f5dstADfgZ<{KQAiu51LdZH*>|5}b`sW1)8wJk^ z;)XTud7iKFD&IGH5z-gGmSQF@t(zhpX3L6QE!>M`h|FCE63G^zrhQBxP2i2K&_U#l!nb(o z&^e*Iku&VJq125nds0BHLKhF5dNhAkrXloXnJF~MDmA%z>d?ehRG)t>IwO+a__0Re z#H{P?pC;Z5zU^s2)5jT<%(J}vM1R)(!nn%s@$CJjh9#tB^_-H@45BTeSPFwLirec{IT;z z0TLhas8NnuDZc;BlkvZ{>D~t#@L%*QQajominRtPzw23Oj}^K%(LhE#88de_bdmH) zBj2`lNzv8u?asHtkNZw&!zC@~+@;jouxDiD%I&!8jBvi;h}u?GheNa=8okhp@Gim& z{gw{;gK&_$t+Mi3jTPDOD&z08_}JPj?REXVy3qpY7GH1L+I3sBh&L~5A5VZrXZO*h zcHlPRfyrgD;Ro7m4Ty7iHpm4#ZDhU`@bC!UxmsALknrKD&K(VSh`rCHxkNXzJ8-{X5=V_Ox?r)PP9Hde&g2ApG*cb@yFbVM0EUx)$-D%wsZ#U<6*S%p<*8o z()cH&wjz@#bCNSKRg7xl%-KMFIpH2&2HfvB+A#7)i_~0Cj;~^I)?EqPMS8J+HT{1L zONNHrB%wmkGjbT5qip^6wn?Qketk_0_NA(@OiVGqzfMg&!wlsdy*#IsCGYYEnYQu1 zQ4J&Jc=!p=AzF@Twx}$bB_+`JCJF@tv#q`bst{x2$*pOTcy>J4S5Qt~U|r4k8YHSN zubL(gu_3?)TTASAF@b$u;ZD5ZRgwBW8JCRA?x=`r9Nq zfmPDlxj0)!c4~@W#(6;Lhvc|nW9)88G7(=v{e?<^z$TJo)5q=IG9Pm~NC^Mtw*+P; zi@wgx=l(f*RQy$KoNKVqS)XOVWxmeSMRjxA?aYpt%{GH2PH4K>u5Mz~Nam^m(vb<9 zt2fULGbHglt&qox)HO0NbAf~X0cX4Py$#@@z<0#HlnOwNnk{(!%vmMbFwhfJVbXe8 z^0GzB`+(K5{z_+UziXtZD*l0LzvzSDNWJ53q+&*(1&}CaNg%Z_A2bz5_STLZBy_`} zT55eTIT)D&+OM^Yi;jJ`^d=rRJo#OO@@Hftd^M;gaHe?2P4N8Pcle(ax<#QDZF@x% zPZ;bx2%b7N@XQk5s&_Lp-(izAG$Upjv*l1u@Ygmy+8U?`^dnxCg7w<3W@Yz@A<;10 zb;Kj4-5l>%0yI-!(S)LNW7K5TCH4Bz$h7Z>ms{@3O+_%j^9Q@<`!+Tm#xs^L*LwDN z-BF~4<&LKmA`)yT4_cB^0%uIy*^^Te$XU{FG{VO8^fSoRjddLiC%#4szzIjvxjZe@ zD9_lr;@FXsOZK^JADSP!1_3V`nv`Cf!uP_D7<_75EAAQs2sf<_Tv1=enDe4sq%Hy6z_s1thw@3i8t45XUwpeu?;Ke}#R zbmEium#^U+d{9+)EX8m-(p}J70+DQ`1e}#6^qF=KC79iY-Ya20jtPxNcjkRmaa`+> z^Mn5!F%#Tuy?!A8NT4Z3@mHUoU&jR^eU9g_FE#N4r!|@ZMaBfiRwi>{bv6AYxEp+P*? zR!&8Q|Ma`B=#>WB;3H6I&=HN#F0t{!9Ci!g9Ufw{JOC%^p=lM|?8~uiNW4fYYvzrg z{8Em#gGP!;7jrRrXBA-In}-WRS%n%dA5P+T^U4o3`XfT_*YEu~**$j3wI&)BGTjBmS5*iIl=aGB!laONO~%)vOuW{hM0@tIwL~h zfi3oIDbUK{DUg$0?K-EDPZJ=s_fLY*_t-i_LEaIMCYIhYv8#3 zEJu3@m^ABJyR~~oR>-Jb=J#UXrlkEM7qu)9DNW)68a$_yOg7T(4-`X#eO5$j6Z9O~ zQNmV>Cj0mkCNqQ~6$l0^Mk9{ysHEb}oZek%PW?IAQLe(qTqlFZqZzluhIj%tn2i+W zoxMiH6Wc4UkzGUinueU;dg`2x%aN$6hpQnu8rc~827saBe^IDnUWZfC5PVJ?Y>^|h z2T0)DgseD($S`09aUEOUupf7MxgGQQ4eFqxMILjc;QO}izbj!sC+%RBw-WF+Vn}0l ze@UN(L=yRM8`z@M9ZAa1WM4ptS7xr@>{f5k#ndDnh}0Q=O72pfqjhTP!L51S$dQZs zU{a}mlocyFG^2Zs*D*@{#C~lCl}Ipv3wJ(n6GX#0$xB)j2nQYPdDaCI5_|AKsLv+6 zT8?JIeUX)9n-n;t00R?7q2d11ckuW7Q=t(Uq*tc}^mm%Sd)<6p?BvJVlUV$}0E2u< z(lsTA>tBkhr(?^=?4KMaBu>|Xa(uL@z(H_|fo}^jgS8-1uJGKj3xCsuHzx{rfgw5v7chWQj`SWcD9rHwOQ8sC)u5NPi-yhT%leH<2hJQ2B6KZGIufrenD<6 zRU+2^+IN)B{XC$ASjh~ykX3xIow;uhqRl=d#fW{&Z|Tolqq8iJ3JLzu`);)5WFl|2 z9~=(rFVN&!MRn%>fb8><_8{gTy8B`koWlGbiXCVqC-`ajYa4E#WdJyOJbj_dD*seko+d-hBC77S$Z|t z5>E7-tz9JPe(%vBC+T(M^fxq(<(a0k(fu*4UXsRHApj?$LqOlg%;qFHMI_&%GK`24 z&8prRnj{nerdiR>>U5pqWG!xEku*v-1_>6(b`U^CY|JCWTKydx?w8lVu#Kc^8gAMM zSOzyn%h+1iV|9)C%8-FRel@m2*=zZas}K4sWzmn(3`UBQ!KDeIpO6y05xHG?o6s3z zKIEV@&s0kWlhK3n+I?M?6_HySthm7Kq}?mtyn z#+NYTA>uk%rN>Q**f{fXAsEsTQ}7gabfQ@iC!*le<|%wbNj5bGdyEn5OI3#m4o{n@ zhkw&(KCPcdwK#M|G62JN+_28Y+Q?5jdAg8xaT%3kv7X7;pQr;lEqWl_d|u>jaZ{$5 zEIG{$(YhDZ0(u2QkR{|uZ--Q9n`8x18ut!hHHfd* zQbctkmGISBOuD1JxwViP3$qhI_49Y+0SWi``3`{?d?jaM<=AvLn%gkp?nsKi=_dD% zMHx8H#5qGWZ`ra<5j-^E8x*&oS);RU#{Fq}dKP&aYs}9?kDEfTmzn`7U#4Gs>ufH5 zZyVk+h_}}1TENHNN)Jhhc9Q1g)dqpoSa|QD>N*1iqZwaozKFudq>KVZiXIaAZNr@? z=|K_?2SrL_eetEUU3_r{M0)1=ebNNbgM}pLTsQyyf3|1%YTsVWQIo%+D7NcgOXV< ztOR@JC6L(ZQ4!whzX>)lUoVvP*<+q-5xa04`*bO&eO&J+C7dg@YvKO$9f@`YJ{Nk6L+-| zcbXjNv00n-6h)m@K&HNQVK5uCr@SUdNc!UHVdn(L2=odGKDLEd+9gM6KhsxX?h1~blJZsGikF^P3_j*XldyJ^zVW&v12;R4) zhoMwYe)t$I6Kj|-mXkg4*OuKZ>%0eCuo$&?o@!e0RzDr&k;$}(bvOMbj@(reFQVYI zEs)w2iU3T!YmQULrItk9lU4t-2_{4Geg7d&6A{##sc(MmeVNZ%5z4F2vhwS10G0|r zcu{NxV;g5Gep$?!ZZf^teN~5ti3*0{-IvgX5O0w;xl_Y#^;ZMn6_zyMFDa)3AJ%5s z^4ehaCv;PyZSZ?mTQzqW6g=a}btnT|nX=P)!90b(x3fwbS1ON_(>1&@k&j9J*&X-A zmY%FGTn-dL!Ea(9e_oiuR8gGrDjWlO0alxujuC7m-g2{^@Z9 zYcYVcS$E4`;wW78@rKF^sUtaasBP_Cv%yAqE5J7LjZ9D9f0xq+S-zG#2PNoA3^}vTd~imwrvpBElYa5`J$n zt{^TwkN39E2Qzt%c=HGVMuw|UW*n282bcp6z#KHMobvF2V|J%IaDV~|pdcP}nkaCj zIMJ_n-NclfmjCm|ACZD+IgIk4rX-Qxj`YyG1Rek}8z|UE@6+&qXuRe&y*n%o;_)N0 zdpU3^>-}{|@d~JtfM2O>6BkudS5UtsS#)B63m1b!n8b$$7RH!S>Lyr9(8I~V2IW^c z-j(B<+2hWBqZ$sML{$Y%3I2=B;dss9Cgzlz`)5wfsLm`W+bB}KMJ3)A;vm61P@Nn- z*SooXTz>;8I&+oKaZ+4T?9#ks+&2z^@Bn$doUyiN;>fF)HXg3F#>nrwYB5m`HJ^b#-Af~wc z;>negbvb{R0F+m)Q-l}(%C$@T9sWy#(JlW(UmTOJH?zJFZA>@=!NB3KAkX^psYFLL zra?P&8ApEiI;R|+;|n<{@wAZ_g`~_n#Zgige_BRG$)7)zd`zmt80)KsBBn(@&qaVz z?6flgC%i^5Yh+z)5+yE$3tE^oOXHdykq;z*I#Ul$f6uy+>#ccxW9*=xf#Utr1n;<; z`slB)ba-la6Z!)p&w;Bz#~4qBMDp>0uSbk)LlG41tx{?dI!9f4zD&iS&J>a*0LJx| z0VMVY02o-W3b!sN314h`8bC&m;lZlE@m6||;{C$p_Tf0Jw#baMH9gP=+QdS4VpFUd znliRDBM+5XS^bes$%dm)7J$vEuY^;ss!L(^^>yr$s$|?J)BWISC&~_N#{;$O?0nOL#y7_QAvdeit+D{8Jfu3hJ zw+k|(_e@IKMS|&;AxX2J2AIy8rers_hD&g$+}bI+kE0smcS`MqrV!pYJ^;OTIe(Ks zSzQy=yu4EYy1g+9jt;9gqUW2c=%*HW-0?b`2!7wE?tN$V_(EMQhQ>GE;nZh5+*V)b zj24ZE%S;*B$sHutU&pO|XOQ%XDAUH?8^36nr0!h_BLDuUzvxuts%(Gy!yKzZ&v)`b z*}+-L*2E@gFk!tRodqRo-A;F5U)O{8aDZF~@hCM^jybx&$Ox)*Ut6t{4NSgSWzw9T zCvcWWh^UO`X3;U%26_%vO0Y0(2aF{7gb?7fRhMl4RrT8V(1YZ1VF!E^XyVDr+5Wx+ zUK>qRb`p+yxgR>nRtoh;Mq_Fl2Rn_$V;h!^>#^#q)2u^fNZ&{~dse1=aoNjp{8Mx(KsumE^vqQsg#z+-$CCs{@B#v|6dxYFsN8j%`$0&TU<@Eqxz0llyWk&tY>VVN;v9h!(?WV#|wuOLsej+uOZwA-^(`m;40;jXLOm(;2?Uh-(Nu- zdgtEk|4~?`{!>a#vtK3k+=-z&ckD>P^D)?z4&2(uclkQ-QNk!3dJUaABd6%q4H>7j zy`TY{Q?a?^eXOX^LxNlW12d#P(m?>or{8Nv9CTmN-D@yG(=bdoivLvTqX4k)5|tQ~ z?3R6+BmgJ0-%_!Ec~CER0NBPv;=nA#iqmT2&ZKX+P4B2@SMX#v#Hh);wW#CJjub=2 z*ilX)qm#)I(}+Lnsj+ZD{0rP>oN&tc7#`yfRT@$!XZ%90MKp^-=7K1K`0Ex4SO9qd zfbMJg4M)wnFK(~{_5LEhU26uo+}n+^iJq}FN^|3|62oXhUAe0}(J1s}miMa6fW<%W zoX6U(MXi^hhM$q2)4PBmhM)xT^V%%1M{Wqv z1%#_Szk)iv^sF7{MHPO-%5Ye>h_kQ24}2?=@=tmAkVoR4PwAJCNP< zH^^S)=($cr+d)hpj~yeZvU4i2SF2wS6Ia*{*>wwtJ|^<#WqbZrb3&A8JM51p0Qdd7 zk*S6kyFbb3-EoImUrUo-Nolp~udLELh)?aS$|c`?vRNZXB0Y*L+3UehT8r;B(&P^r z`!p;3(T#d5fM&M_QF1y!_4B>>3E}VUZ;)zX@Y%h7XmE1_ig+6@%=AHPs}K+Ru!qU7 z&^jTR>?9n{u-5_eb&KvDtNk!c8Y)9$kQAzzSWxYFT9E(N?TW7ogDp09e4?edbI;GxMBIde3GaF}ERAMZ5K^!N{fIh5Gmf+nYiC%^_y_BRmNGn3uLzY^2fwtz zOxkXDLeZ-(8(H8uWu{ewqJ$sZC+TaZ2u}#^u^`s|L8>d+N zCj#O^$G=?5xLWdGXht`GpH>nlazyISGMx)dBHs`!mmBJZUnu%KA4BZR$w^09q+0NYbPPwF9b`1G%u@BJFBI zE9&q+tFcK|Ozp*71WP|W+=nWt-dRIJ9&h$ufMM@#-VTj4Zk7D&|b5PV)$JE@PbIVu`S0D zkNv6XEH9dR!xD$0V93w{+wQ^poUM>7s|RatZ=9N;AiS4FIt8eG`|HpG@DBfnCH^eV zUjMm7@LS#>Xr-`tC!SA<;5Tu2Mj-OZy+9_%9+vwp=je8Tk^y z+h$4y+0;)c0Z>}?M`WXzqD(8|J&vC5z1Vj$V(dL;FC8t@mG)Ar{j?G6zyR)Q0tWT5oQ%Et1 z7G6R=U`?Sxi`(qKz%liwl0(UFOjYBtJZJJ0L||7kr)}9=jA+9v>3ROTRQEG$#r0R^ zKi4S<75#3z+-t0^o+fAwtVNq0Nm)>EsOg(=fjZ*xY~`|kh5h<_dt^$Ywk8+?E)8Is zYR1e8@P{<|Sf0U)Latp$K=>j4s#tAh^w7Q+mSR#^HB4{Q7=o z*tOm$jSuCd(47G#V|Qcbr1hVuRx(%sK@IKdb@6@;+925VKGfZnHa!pKwmMocUwdHK z`}H6qsIm4@yu^C3@eDX0>488Ta_uc>k^wu9k*1c`{Gq@@Wc#o5~GN-LN(8@iT0W{^*ab_;0iZ&3?&hf(L)cg1O(X^$jbUv19K7ykmju zo=?3+RP4Ab3Z$F`t{-X+8w;a0I5A>NSFSVG?EGAJ`hUA$Io|* zmW$gIGEIpRS;JF7$l+N}1tbq?<-C+7XB z?Usx6&^KpboBtT+I7Z^7qa{mCS?YK_ZyKN8F~6xW?Oo7JEjc*N_zibyIL9@HUTkQw&z zBlpK(qwr*YFr-{gxuX*26)Mu$e<8ALK>vEF4S2KtB0nNtS#<@y%fQVy@3aL(xy_x) z_^{+^$Rjy{m)9MF39RZ&!=H?vKIke;M#Vrw8S6G&&Igg?GrsO5kp}1jw}0|$xkE}% z%)57&SYD6Y()7W&%rlXXd*}&>&pCCm|GHB~Nl>Pc#Sq!#`;i?D&^c4;3}{MDVAw!q za&oO!R>_70T)2Ubbd=5PZXLKf@e=&$`-q0B_sF{XUPCXcXqv8!IS)|A)oX1UHvxmE zRE0r>%?8?Lol9vua=w!iLyS6#a~INE>=^2`63DRpV79L*ebQzv>KrN7rmy|26o&f1 z{#){vQcB)mZ1VS~*geh}p&2Jgu=cd=M^#+_?O`xrJd1w}lLvNrP+7DZ0QqXlMmj0h zsM=vi#kp%6Th8?GfhHQ=j~+lbnyP^u6auU6-EZP>GD97ZH1CUb&U|GjFPsfJTSw@PGz5apk4PaX+bm^!f{r5x83k7WnX3 zN%=({e8Y0co*~W;-mqggN0PhT59Wr$o#eRWF+0jSjbv#fe$(_~=$~o^y3l2StX3BS za62=V+&+8BBCdtOOs9_S-xb0rl;Q&v%5ch5!Xb}-#)b!UDEK?AmFoT&XZlvKlq7|Lc@&6~dvMPKN~Y|OC8pG=X~Xp%HcxwXmo zFK(*XWAt2!coYWz{Qcr`MB1gR-rx#YHd0?HqncTAqQx?KkBq=S>4V3;X91KRipunK z#j=l|RzkLz_kKTO#DBuv?Zi`Rycm*_aol9OoW+B?dU3Vcx?@bd*$Ep|=DM$9f$oo^ zF8>(sbE}?879a@>klyzG7IcUbBu?n``)PPk!^i}nD9TdUO)(WJ=Q;iHYJQKW!As+X zZcRB)&elY|uV%sKeJs})n24tJ`6HMi^gL^+LaG;{5Q*|B8j)zA10Vt#RTtwOo(Xrh z%=9eB1E~WD?T$`%+g|ug;v5_bl&q^XKjsFYLP@kJZSnxoPW%&6%tZxi z`&86Ct=IitChz3jXCq8ltK?8;t4eGyqx*&n@u~87^!-aP^L=nG?1}IGO5Q{#2QYIA zWb^AjOipw|dkaIJiGj!I(oVEh@VYd88jlw2fP_rYj9a<&L>M3Xl8jM%kLY^Hsq6|B zhdbr__3`cPtin!ADH~+OXV|XiL|x(={@>{(pTJLu+iWQz`*Au47azE0=>{LpZ!*-| z0^gkM<4FMW0nSQUd30d4x4lD$A&KoxKMKuPj$H3j>$nF=P}WCM$brtNbad|Z!ksm0 z&cAX6NG{mE%nIqqJ{rOFfWU4i{;@k zGIv`2B^rcDo}%QpJxMDpS0}DX?pe1`!S-iH@0&d%smj438Ja+s5?>Er9%{9@r7`Kw zi7oeuoz_N-t#v;~lp<8RTS2TRH2s>h50~dOV6k4TozQp|*K?dlPQQN- z3y9*(NUajf7VQCjR|7*@nx@gS`@*Ry*{LDWL?ptQyuharGa}V~E9gisrWvU;ktQW^OfNRE9 z*~DRDvn{*_dA1mryqLA7qApElMEJeD!Kse=`YLp2Q$rjW(Iigp??Xe%4owps-zA zRYJ{_OL0oO0_dde!i3_RbE?9gZnR#pZ{&=W9}-?#Z>%=5iE7ppY1VZv!Ty72`F)?;R3*Ocw?jpgiI4?Z49D6DOsqS?0yN}Yn=(>s;#>?93d_k8 zA9Yw_@Zk#HZZovtbt9^R2{=TW979-CCW+*OhY64Uf6}Ghv=Ry3ZrAy$GO#cE=4Zmy z^58vWLqDB3ZsQ_tFHSy~!wouO$V!l;GsdiLrNex3_cx)Whr<9Wytw8;t$(63z zOV(ISgeQ1oQFI0s3fiVI7O2H<{#761rA1(xPcYOqBiFz}E$xyKT9>dQ?VX#Hz^z{D zq!vK@ceV+})xQ6l=iOUI0=~i!UyeROZ2Qm9bl_99`vDkLNuw6qE9jWajyyZBUD%w5 zj-(=OPOr#7dolNl;z+xutXMi~i*1ipbtD>{c*}<_;a68!cCa(S`I8O_hj8c6CY?8g zPQr`ya8PJGUZI05=;Bi4O~5Cjk4h%Qw0;eH`jq7WG}Q7FLFi%(cS@)l!@a&@AeH1j z^tgm;tBBE(pnix?F>}z0XcXVEra7ecND`54T+uS<0e!490pjpGUAX&7?fhHyVLSCM;(kY>txkev%?wf`?oyC0UltrJlsBwE+UKVGw7;sJbXg(V=>~- z3KrMWJ`2UL71rXK)oJ>=ZYUIcnF6L*okt(kskcL$!N$HXU3+ zwnwBuhdVyL+d2R(AGk+9%GIy0R`DJQ&3!5BYrqZfSAz4-oTNhzXM$Ech^INFHny4$ zWc|P>n!>wEW~BA`*U!~?j!EO{j#=2#N!rX=H9%pPv6H)}yb$^WA9$#pfOp3{kdlV( zI;Ix6>f$ePm;b?>0rS5AE}{bHAVOqC&k5*zsrCG+bp86OU<8c=;d3}Ay<~&H}%i9hLL}ZLvR{BfIOv0#(qsk z%i*jnCr*dUd@+Ov*W*L?tBluC8vxU2xI-%7q=5_v5Ua9X7>1a^vYeb@#P7v5P2S|8^1n)y@KqxCxczl4VrA z4tRT6+4kC!kCm7hceS=&m)<&L0Z3He3-oSllhV4E3X)$xe}-2~^gM|j`z9CWEFTIu zv;Ch_GQ}J`g?flQwo;DyZV+MkjONfkRMJI{HRfl6M=pP?Q<2o-Qx)-}0-V!o%cxa@ ztHXLQD(TTM)J~A?HNGfWy|fV`9H<7qSkv9SI;bsJzK|i4^0wVa@%>wl+tmK;C+M4& z;*+<1r1^?_Gg2QXB1wSopRvYdEMeEJ_)p7W`(Av+y5f}Aj=b-UoC%imPtZiTQ`92W zKinsknhB}kL9AkNtt9mYJ7jnCL2K#PUnz!luPI%RI+>73zlX#kl>opWHW1aL>Q9HA z3IQ~3F6T)!MKXK8H(l@W#s~_)+E3zH^mxR7f&!uKP;4^qf1-4~L9NFF9uqu0%_0zY zz|OCn-4{P8@wk~QEj*k`-ht1Z?EaU^`yYeHrFr!urZ27l@Iq++a&ERH{YEwWxU&ng&H;)x<~qVp2WNR6kyJBFIgA5TN2!l|e8=smOuqH6bHQYe z?;GO44%irkKz5@!B_e=sON?7WW24}Xf{(c~l{}J8@I7-n#{^qnCX}H8nl{d&_y_S$B^n2fh<(k+3&3$OEQzE)# zWQ5%g6|njKQ~Yl?QhA559L@#C@)~QbC56|jFk&b=M_?t@PC#ygxr3o!U)Pj;i)hsE z@)Y2!14w+b)JrbC19SiT-7?WdbDlm{1Za+=x`I3zpctX$sEV4{D*hgZGbsopi<~<< zE>EJ0DnJh8vGCtNSTMm<`dr)tj^j*cT90(T718l~acs+**I6vBb~*Hv%MI+H8s!~W ziTyI2=XdC-Yjj*N3!bRi(6v#f&! zXkrrM{&*c==%b($ydRS4$Ns{^z-32f{(}%=q83YqMX}CH((#^jZLhPI%h#LM@1Du8 z8C$t4-5Zr<1kdGrgChD5?RA&AytZ7dl(3GB0Z1zHO$#`fuUVafR&*Z$1ao*_ya#`V zZC|awXTl%CgzVi$x}Ne8m9J2LTt3p84NX9z^V6GWUd|yxem_Cvd&#V`BXTqK2Rw<- zr2WbRC?IGXEkXgo$pDSXp&=H^_cU*!4Kj?$-NGoG;rYO%tvU}`+fTSZYujXZHy!3E zx>Q=)C*o`9mmHYZ9G^^Ce($s3+wE2eRl%zhoICW(0RuWcg-MQ=*V8`mN@U0Oznv@2 z#y{;inqE5ir-pWJz&7pwFY|EX#&wHb;gp!kso*u)6&9?kc`PpHQrPBw_`K_fcF?_k zohsp>(#8F8`Xo)v?g13S{@r>}Zrhp5+q}NDG^73Re15{Wchz;_tyHxw$$PUu7fq0T zMf__9er%?_%^>y;5)A9Z9uS3 zaBre|6AI{5XaOC8g1UmrnSY9qMNLcOE83E_*@x)exL-{hdx$QFe8UabcUn5@7caY1 z@)>C#KYrD!K^t{6{leFxp1bncO7*L2McD1Ab?%Jl$H4yIhkCNub1|=u*x^5C(RIOFY;-PW zE%9j|L>DW7Zn^WW(P)>8a+RH^1S)^ML<56WtrjmdGzwEijqXAJ|Nit>%nTe@GM^00 R3jP~ZQC3Z+PRcy&{{SdSemwvH diff --git a/client/ayon_core/tools/standalonepublish/widgets/resources/premiere.png b/client/ayon_core/tools/standalonepublish/widgets/resources/premiere.png deleted file mode 100644 index eb5b3d1ba2adb45f6feb2c35f19bc1bc49a22b93..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20121 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 SPreCMG(?(`x)jJ*w{G3CWy>H8APEF(*REZ@ zd^wN=0w8zInl%t|#flZk3?LUM1q2ZB)vH$nWmm3T2_%64$c3;W3^*4k2xkKs$SNV~ z(M$lUfdjY#AOixxR)Oq-m;*NrC<0Uf=Rz0|WkB{&0piollYuchvn0qbn1PXrnT3s= zgOiJ!mycgSP)JxrR7^rzMpj-)MO9rxQ%hS%SKrXY!qUpd&fdw*-NVz%$JZ|)C^#fE zJUS*eJ|QVNEj>H8pt!8Oy0)RYv#Ymn!oGpI?4{ z@OAC1d17~xgZ6eWe;jY+<#zGuxxZJP9X?%@yx&~-AhtKzf#2YUg6Cz%V!@FfFgX#f$#y3^3)=yH|*}PX<)l*b_ex)qKu?Gz*FV*f$Re9SMFM6z@Xo2@6 zk(PgF_S$b}(O~py5PD$GvO}mKWRgh3@kvYgX3R*fTlww7KEFv@Y^P;RSJAD>{#*5l z+04IE{M8zvWo7e1nRI97tYM5QX1d1o_Ne@{+ylq%$nTYC>0*B8P*nFWguxRW{xNrfe z0LTD}oIZW}?AfzGl|Tki4Ui2K1WG};5CuRIE)FpVr~oJeQ4eGQ6$1f~0bxVLA%bu< zm{JfCAPG?eB!ODcNMv0=DIkE0ps^ubkO~-ptAsNkGH6_wa)il11#m%HGnjs4%>>5i z%#t9#U_=Ke7wDj!Ug5t9B>e`0p&aU3R2@@wznL2&O%vrPN%w4c_ z)#^2C*KgdkdCS&q+js2RyZ`W!qsNY)I&=2o}GwI%AU|{0*ba4!cIQ({cdT_9#M5}*>7FXGk zhz$#poC5`XLyq*V5ZrxCE}Cazf-9?wiCK=P$BEDRe-+FR_i9%Ee9&WTIlt_F`8ng; zY2Ra4Hkid$et!A+!Pm93=84@&4%*wj{BgXMm)pgs=l))GcKCEr@_uvSgV^3=2Y!Pa z3ZAFg8dm%LHMM`>{CEDhIf@~1mnKWG(pV2QiQWS7vtv!1=+ zq*&y{ruXcp&%R0hz0av?u5E?bgWCtZg?kx)Y+f-nFhk(ow;%75v|M(L z=gVEaRUfX*KD12NA^4+e4DXhy45|n08Q)aBT0co;XY*cdRZmgz`IWK^#~w7Oyi~h0 zRpo74yy&rpq6OZQL|Xox*=xU@MT60+LFj=!%MPJ}kVzs9$0se}n=vD`ZsoTR`}`(t zv7MGNT}8Jh`)}1JW;6dv@mFhvmX*y5WzwCQvxYIMnCTkR+oST+at|E4BfnRorHlEU zLs8wk5C)6TCC|!B86Vo5JSOtt&y!B3SMS&_RmNXPP76&c|HoJ+B{F-Xa_h2)`otX% i-sjdl*>wBe58i04jx6hY@=d@r!r{^^}<`IdjNGca2VJUHJ=5(0*(LzTY-Cl3nR@j;HD75blo0nMSUgr z=^jzHE}`439?ogsO5wRO)Qiu7iTcm9lK>VKXx4-u?oQaNC6^Ysf z@JDof}kz8BS-i~4k02)znClWU#^ej;0B^cy3hM~cZkEF8-;wU|0?j*-=KTWALy_i;y5vuJeYF&B zePI&+PhRxv?z%2DtAI5tu(l-4Ga$8r)HDI4HjtVofYb(3(*%&(Kx&!*QX5E36F_PM zsc8a8Z6GyG0I3b6rU@Xmfz;Fyz#=doofj1-%yy|+BrANVl*$7LAqebgo&E}ZP$1n$ zHovCNSv?ZKz7RsXDi!sPZVDvSjcVIX73wAGWgRp_>Qx;yH{`tC-%cOX&5n_O za_1YCRJ8~p9Pf0&G8NpdI;Gz_$TsHAt1sPASF1Bo%9E83vP{(dk*@{y^123kFL$fG zV8Rl5v(otIa-P2E<{DjXoK#22HbKVKqTiD}tmc>OR3C$6n=QwpHi6qi2#0DLRo!j> z@_cO*&xNo{J)L^C5i{!3J(+wI!Y1`~^{8ofmCc*&3VA~vS?)ak14O|_7j^>z%K!iX M07*qoM6N<$fz>% diff --git a/client/ayon_core/tools/standalonepublish/widgets/resources/video_file.png b/client/ayon_core/tools/standalonepublish/widgets/resources/video_file.png deleted file mode 100644 index 346277e40f83ec507d17e0175b121bc0bb26d9f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 120 zcmeAS@N?(olHy`uVBq!ia0vp^0zfRp$P6SizMjzpQfvV}A+A9B|Ns9>Z_d99WHFWm z`2{mLJiCzw;v{*yyD>> # widget = AssetWidget() - >>> # widget.refresh() - >>> # widget.show() - - """ - - project_changed = QtCore.Signal(str) - assets_refreshed = QtCore.Signal() # on model refresh - selection_changed = QtCore.Signal() # on view selection change - current_changed = QtCore.Signal() # on view current index change - task_changed = QtCore.Signal() - - def __init__(self, dbcon, settings, parent=None): - super(AssetWidget, self).__init__(parent=parent) - self.setContentsMargins(0, 0, 0, 0) - - self.dbcon = dbcon - self._settings = settings - - layout = QtWidgets.QVBoxLayout() - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(4) - - # Project - self.combo_projects = QtWidgets.QComboBox() - # Change delegate so stylysheets are applied - project_delegate = QtWidgets.QStyledItemDelegate(self.combo_projects) - self.combo_projects.setItemDelegate(project_delegate) - - self._set_projects() - self.combo_projects.currentTextChanged.connect(self.on_project_change) - # Tree View - model = AssetModel(dbcon=self.dbcon, parent=self) - proxy = RecursiveSortFilterProxyModel() - proxy.setSourceModel(model) - proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) - - view = DeselectableTreeView() - view.setIndentation(15) - view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - view.setHeaderHidden(True) - view.setModel(proxy) - - # Header - header = QtWidgets.QHBoxLayout() - - icon = qtawesome.icon( - "fa.refresh", color=get_default_tools_icon_color() - ) - refresh = QtWidgets.QPushButton(icon, "") - refresh.setToolTip("Refresh items") - - filter = PlaceholderLineEdit() - filter.textChanged.connect(proxy.setFilterFixedString) - filter.setPlaceholderText("Filter {}..".format( - "folders" if AYON_SERVER_ENABLED else "assets")) - - header.addWidget(filter) - header.addWidget(refresh) - - # Layout - layout.addWidget(self.combo_projects) - layout.addLayout(header) - layout.addWidget(view) - - # tasks - task_view = DeselectableTreeView() - task_view.setIndentation(0) - task_view.setHeaderHidden(True) - task_view.setVisible(False) - - task_model = TasksTemplateModel() - task_view.setModel(task_model) - - main_layout = QtWidgets.QVBoxLayout(self) - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.setSpacing(4) - main_layout.addLayout(layout, 80) - main_layout.addWidget(task_view, 20) - - # Signals/Slots - selection = view.selectionModel() - selection.selectionChanged.connect(self.selection_changed) - selection.currentChanged.connect(self.current_changed) - task_view.selectionModel().selectionChanged.connect( - self._on_task_change - ) - refresh.clicked.connect(self.refresh) - - self.selection_changed.connect(self._refresh_tasks) - - self.project_delegate = project_delegate - self.task_view = task_view - self.task_model = task_model - self.refreshButton = refresh - self.model = model - self.proxy = proxy - self.view = view - - def collect_data(self): - project_name = self.dbcon.active_project() - project = get_project(project_name, fields=["name"]) - asset = self.get_active_asset() - - try: - index = self.task_view.selectedIndexes()[0] - task = self.task_model.itemData(index)[0] - except Exception: - task = None - data = { - 'project': project['name'], - 'asset': asset['name'], - 'parents': self.get_parents(asset), - 'task': task - } - - return data - - def get_parents(self, entity): - ent_parents = entity.get("data", {}).get("parents") - if ent_parents is not None and isinstance(ent_parents, list): - return ent_parents - - output = [] - parent_asset_id = entity.get('data', {}).get('visualParent', None) - if parent_asset_id is None: - return output - - project_name = self.dbcon.active_project() - parent = get_asset_by_id( - project_name, - parent_asset_id, - fields=["name", "data.visualParent"] - ) - output.append(parent['name']) - output.extend(self.get_parents(parent)) - return output - - def _get_last_projects(self): - if not self._settings: - return [] - - project_names = [] - for project_name in self._settings.value("projects", "").split("|"): - if project_name: - project_names.append(project_name) - return project_names - - def _add_last_project(self, project_name): - if not self._settings: - return - - last_projects = [] - for _project_name in self._settings.value("projects", "").split("|"): - if _project_name: - last_projects.append(_project_name) - - if project_name in last_projects: - last_projects.remove(project_name) - - last_projects.insert(0, project_name) - while len(last_projects) > 5: - last_projects.pop(-1) - - self._settings.setValue("projects", "|".join(last_projects)) - - def _set_projects(self): - project_names = list() - - for doc in get_projects(fields=["name"]): - project_name = doc.get("name") - if project_name: - project_names.append(project_name) - - self.combo_projects.clear() - - if not project_names: - return - - sorted_project_names = list(sorted(project_names)) - self.combo_projects.addItems(list(sorted(sorted_project_names))) - - last_project = sorted_project_names[0] - for project_name in self._get_last_projects(): - if project_name in sorted_project_names: - last_project = project_name - break - - index = sorted_project_names.index(last_project) - self.combo_projects.setCurrentIndex(index) - - self.dbcon.Session["AVALON_PROJECT"] = last_project - - def on_project_change(self): - projects = list() - - for project in get_projects(fields=["name"]): - projects.append(project['name']) - project_name = self.combo_projects.currentText() - if project_name in projects: - self.dbcon.Session["AVALON_PROJECT"] = project_name - self._add_last_project(project_name) - - self.project_changed.emit(project_name) - - self.refresh() - - def _refresh_model(self): - with preserve_expanded_rows( - self.view, column=0, role=self.model.ObjectIdRole - ): - with preserve_selection( - self.view, column=0, role=self.model.ObjectIdRole - ): - self.model.refresh() - - self.assets_refreshed.emit() - - def refresh(self): - self._refresh_model() - - def _on_task_change(self): - try: - index = self.task_view.selectedIndexes()[0] - task_name = self.task_model.itemData(index)[0] - except Exception: - task_name = None - - self.dbcon.Session["AVALON_TASK"] = task_name - self.task_changed.emit() - - def _refresh_tasks(self): - self.dbcon.Session["AVALON_TASK"] = None - tasks = [] - selected = self.get_selected_assets() - if len(selected) == 1: - project_name = self.dbcon.active_project() - asset = get_asset_by_id( - project_name, selected[0], fields=["data.tasks"] - ) - if asset: - tasks = asset.get('data', {}).get('tasks', []) - self.task_model.set_tasks(tasks) - self.task_view.setVisible(len(tasks) > 0) - self.task_changed.emit() - - def get_active_asset(self): - """Return the asset id the current asset.""" - current = self.view.currentIndex() - return current.data(self.model.ItemRole) - - def get_active_index(self): - return self.view.currentIndex() - - def get_selected_assets(self): - """Return the assets' ids that are selected.""" - selection = self.view.selectionModel() - rows = selection.selectedRows() - return [row.data(self.model.ObjectIdRole) for row in rows] - - def select_assets(self, assets, expand=True, key="name"): - """Select assets by name. - - Args: - assets (list): List of asset names - expand (bool): Whether to also expand to the asset in the view - - Returns: - None - - """ - # TODO: Instead of individual selection optimize for many assets - - if not isinstance(assets, (tuple, list)): - assets = [assets] - assert isinstance( - assets, (tuple, list) - ), "Assets must be list or tuple" - - # convert to list - tuple can't be modified - assets = list(assets) - - # Clear selection - selection_model = self.view.selectionModel() - selection_model.clearSelection() - - # Select - mode = ( - QtCore.QItemSelectionModel.Select - | QtCore.QItemSelectionModel.Rows - ) - for index in _iter_model_rows( - self.proxy, column=0, include_root=False - ): - # stop iteration if there are no assets to process - if not assets: - break - - value = index.data(self.model.ItemRole).get(key) - if value not in assets: - continue - - # Remove processed asset - assets.pop(assets.index(value)) - - selection_model.select(index, mode) - - if expand: - # Expand parent index - self.view.expand(self.proxy.parent(index)) - - # Set the currently active index - self.view.setCurrentIndex(index) diff --git a/client/ayon_core/tools/standalonepublish/widgets/widget_component_item.py b/client/ayon_core/tools/standalonepublish/widgets/widget_component_item.py deleted file mode 100644 index 523c3977e3..0000000000 --- a/client/ayon_core/tools/standalonepublish/widgets/widget_component_item.py +++ /dev/null @@ -1,534 +0,0 @@ -import os -from qtpy import QtCore, QtGui, QtWidgets -from .resources import get_resource - - -class ComponentItem(QtWidgets.QFrame): - - signal_remove = QtCore.Signal(object) - signal_thumbnail = QtCore.Signal(object) - signal_preview = QtCore.Signal(object) - signal_repre_change = QtCore.Signal(object, object) - - preview_text = "PREVIEW" - thumbnail_text = "THUMBNAIL" - - def __init__(self, parent, main_parent): - super().__init__() - - self.setObjectName("ComponentItem") - - self.has_valid_repre = True - self.actions = [] - self.resize(290, 70) - self.setMinimumSize(QtCore.QSize(0, 70)) - self.parent_list = parent - self.parent_widget = main_parent - # Font - font = QtGui.QFont() - font.setFamily("DejaVu Sans Condensed") - font.setPointSize(9) - font.setBold(True) - font.setWeight(50) - font.setKerning(True) - - # Main widgets - frame = QtWidgets.QFrame(self) - frame.setObjectName("ComponentFrame") - frame.setFrameShape(QtWidgets.QFrame.StyledPanel) - frame.setFrameShadow(QtWidgets.QFrame.Raised) - - layout_main = QtWidgets.QHBoxLayout(frame) - layout_main.setSpacing(2) - layout_main.setContentsMargins(2, 2, 2, 2) - - # Image + Info - frame_image_info = QtWidgets.QFrame(frame) - - # Layout image info - layout = QtWidgets.QVBoxLayout(frame_image_info) - layout.setSpacing(2) - layout.setContentsMargins(2, 2, 2, 2) - - self.icon = QtWidgets.QLabel(frame) - self.icon.setMinimumSize(QtCore.QSize(22, 22)) - self.icon.setMaximumSize(QtCore.QSize(22, 22)) - self.icon.setText("") - self.icon.setScaledContents(True) - - self.btn_action_menu = PngButton( - name="menu", size=QtCore.QSize(22, 22) - ) - - self.action_menu = QtWidgets.QMenu(self.btn_action_menu) - - expanding_sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding - ) - expanding_sizePolicy.setHorizontalStretch(0) - expanding_sizePolicy.setVerticalStretch(0) - - layout.addWidget(self.icon, alignment=QtCore.Qt.AlignCenter) - layout.addWidget(self.btn_action_menu, alignment=QtCore.Qt.AlignCenter) - - layout_main.addWidget(frame_image_info) - - # Name + representation - self.name = QtWidgets.QLabel(frame) - self.file_info = QtWidgets.QLabel(frame) - self.ext = QtWidgets.QLabel(frame) - - self.name.setFont(font) - self.file_info.setFont(font) - self.ext.setFont(font) - - self.file_info.setStyleSheet('padding-left:3px;') - - expanding_sizePolicy.setHeightForWidth( - self.name.sizePolicy().hasHeightForWidth() - ) - - frame_name_repre = QtWidgets.QFrame(frame) - - self.file_info.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) - self.ext.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) - self.name.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) - - layout = QtWidgets.QHBoxLayout(frame_name_repre) - layout.setSpacing(0) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self.name, alignment=QtCore.Qt.AlignLeft) - layout.addWidget(self.file_info, alignment=QtCore.Qt.AlignLeft) - layout.addWidget(self.ext, alignment=QtCore.Qt.AlignRight) - - frame_name_repre.setSizePolicy( - QtWidgets.QSizePolicy.MinimumExpanding, - QtWidgets.QSizePolicy.MinimumExpanding - ) - - # Repre + icons - frame_repre_icons = QtWidgets.QFrame(frame) - - frame_repre = QtWidgets.QFrame(frame_repre_icons) - - label_repre = QtWidgets.QLabel() - label_repre.setText('Representation:') - - self.input_repre = QtWidgets.QLineEdit() - self.input_repre.setMaximumWidth(50) - - layout = QtWidgets.QHBoxLayout(frame_repre) - layout.setSpacing(0) - layout.setContentsMargins(0, 0, 0, 0) - - layout.addWidget(label_repre, alignment=QtCore.Qt.AlignLeft) - layout.addWidget(self.input_repre, alignment=QtCore.Qt.AlignLeft) - - frame_icons = QtWidgets.QFrame(frame_repre_icons) - - self.preview = LightingButton(self.preview_text) - self.thumbnail = LightingButton(self.thumbnail_text) - - layout = QtWidgets.QHBoxLayout(frame_icons) - layout.setSpacing(6) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self.thumbnail) - layout.addWidget(self.preview) - - layout = QtWidgets.QHBoxLayout(frame_repre_icons) - layout.setSpacing(0) - layout.setContentsMargins(0, 0, 0, 0) - - layout.addWidget(frame_repre, alignment=QtCore.Qt.AlignLeft) - layout.addWidget(frame_icons, alignment=QtCore.Qt.AlignRight) - - frame_middle = QtWidgets.QFrame(frame) - - layout = QtWidgets.QVBoxLayout(frame_middle) - layout.setSpacing(0) - layout.setContentsMargins(4, 0, 4, 0) - layout.addWidget(frame_name_repre) - layout.addWidget(frame_repre_icons) - - layout.setStretchFactor(frame_name_repre, 1) - layout.setStretchFactor(frame_repre_icons, 1) - - layout_main.addWidget(frame_middle) - - self.remove = PngButton(name="trash", size=QtCore.QSize(22, 22)) - layout_main.addWidget(self.remove) - - layout = QtWidgets.QVBoxLayout(self) - layout.setSpacing(0) - layout.setContentsMargins(2, 2, 2, 2) - layout.addWidget(frame) - - self.preview.setToolTip('Mark component as Preview') - self.thumbnail.setToolTip('Component will be selected as thumbnail') - - # self.frame.setStyleSheet("border: 1px solid black;") - - def set_context(self, data): - self.btn_action_menu.setVisible(False) - self.in_data = data - self.remove.clicked.connect(self._remove) - self.thumbnail.clicked.connect(self._thumbnail_clicked) - self.preview.clicked.connect(self._preview_clicked) - self.input_repre.textChanged.connect(self._handle_duplicate_repre) - name = data['name'] - representation = data['representation'] - ext = data['ext'] - file_info = data['file_info'] - thumb = data['thumb'] - prev = data['prev'] - icon = data['icon'] - - resource = None - if icon is not None: - resource = get_resource('{}.png'.format(icon)) - - if resource is None or not os.path.isfile(resource): - if data['is_sequence']: - resource = get_resource('files.png') - else: - resource = get_resource('file.png') - - pixmap = QtGui.QPixmap(resource) - self.icon.setPixmap(pixmap) - - self.name.setText(name) - self.input_repre.setText(representation) - self.ext.setText('( {} )'.format(ext)) - if file_info is None: - self.file_info.setVisible(False) - else: - self.file_info.setText('[{}]'.format(file_info)) - - self.thumbnail.setVisible(thumb) - self.preview.setVisible(prev) - - def add_action(self, action_name): - if action_name.lower() == 'split': - for action in self.actions: - if action.text() == 'Split to frames': - return - new_action = QtWidgets.QAction('Split to frames', self) - new_action.triggered.connect(self.split_sequence) - elif action_name.lower() == 'merge': - for action in self.actions: - if action.text() == 'Merge components': - return - new_action = QtWidgets.QAction('Merge components', self) - new_action.triggered.connect(self.merge_sequence) - else: - print('unknown action') - return - self.action_menu.addAction(new_action) - self.actions.append(new_action) - if not self.btn_action_menu.isVisible(): - self.btn_action_menu.setVisible(True) - self.btn_action_menu.clicked.connect(self.show_actions) - - def set_repre_name_valid(self, valid): - self.has_valid_repre = valid - if valid: - self.input_repre.setStyleSheet("") - else: - self.input_repre.setStyleSheet("border: 1px solid red;") - - def split_sequence(self): - self.parent_widget.split_items(self) - - def merge_sequence(self): - self.parent_widget.merge_items(self) - - def show_actions(self): - position = QtGui.QCursor().pos() - self.action_menu.popup(position) - - def _remove(self): - self.signal_remove.emit(self) - - def _thumbnail_clicked(self): - self.signal_thumbnail.emit(self) - - def _preview_clicked(self): - self.signal_preview.emit(self) - - def _handle_duplicate_repre(self, repre_name): - self.signal_repre_change.emit(self, repre_name) - - def is_thumbnail(self): - return self.thumbnail.isChecked() - - def change_thumbnail(self, hover=True): - self.thumbnail.setChecked(hover) - - def is_preview(self): - return self.preview.isChecked() - - def change_preview(self, hover=True): - self.preview.setChecked(hover) - - def collect_data(self): - in_files = self.in_data['files'] - staging_dir = os.path.dirname(in_files[0]) - - files = [os.path.basename(file) for file in in_files] - if len(files) == 1: - files = files[0] - - data = { - 'ext': self.in_data['ext'], - 'label': self.name.text(), - 'name': self.input_repre.text(), - 'stagingDir': staging_dir, - 'files': files, - 'thumbnail': self.is_thumbnail(), - 'preview': self.is_preview() - } - - if ("frameStart" in self.in_data and "frameEnd" in self.in_data): - data["frameStart"] = self.in_data["frameStart"] - data["frameEnd"] = self.in_data["frameEnd"] - - if 'fps' in self.in_data: - data['fps'] = self.in_data['fps'] - - return data - - -class LightingButton(QtWidgets.QPushButton): - lightingbtnstyle = """ - QPushButton { - font: %(font_size_pt)spt; - text-align: center; - color: #777777; - background-color: transparent; - border-width: 1px; - border-color: #777777; - border-style: solid; - padding-top: 0px; - padding-bottom: 0px; - padding-left: 3px; - padding-right: 3px; - border-radius: 3px; - } - - QPushButton:hover { - border-color: #cccccc; - color: #cccccc; - } - - QPushButton:pressed { - border-color: #ffffff; - color: #ffffff; - } - - QPushButton:disabled { - border-color: #3A3939; - color: #3A3939; - } - - QPushButton:checked { - border-color: #4BB543; - color: #4BB543; - } - - QPushButton:checked:hover { - border-color: #4Bd543; - color: #4Bd543; - } - - QPushButton:checked:pressed { - border-color: #4BF543; - color: #4BF543; - } - """ - - def __init__(self, text, font_size_pt=8, *args, **kwargs): - super(LightingButton, self).__init__(text, *args, **kwargs) - self.setStyleSheet(self.lightingbtnstyle % { - "font_size_pt": font_size_pt - }) - self.setCheckable(True) - - -class PngFactory: - png_names = None - - @classmethod - def init(cls): - cls.png_names = { - "trash": { - "normal": QtGui.QIcon(get_resource("trash.png")), - "hover": QtGui.QIcon(get_resource("trash_hover.png")), - "pressed": QtGui.QIcon(get_resource("trash_pressed.png")), - "pressed_hover": QtGui.QIcon( - get_resource("trash_pressed_hover.png") - ), - "disabled": QtGui.QIcon(get_resource("trash_disabled.png")) - }, - - "menu": { - "normal": QtGui.QIcon(get_resource("menu.png")), - "hover": QtGui.QIcon(get_resource("menu_hover.png")), - "pressed": QtGui.QIcon(get_resource("menu_pressed.png")), - "pressed_hover": QtGui.QIcon( - get_resource("menu_pressed_hover.png") - ), - "disabled": QtGui.QIcon(get_resource("menu_disabled.png")) - } - } - - @classmethod - def get_png(cls, name): - if cls.png_names is None: - cls.init() - return cls.png_names.get(name) - - -class PngButton(QtWidgets.QPushButton): - png_button_style = """ - QPushButton { - border: none; - background-color: transparent; - padding-top: 0px; - padding-bottom: 0px; - padding-left: 0px; - padding-right: 0px; - } - QPushButton:hover {} - QPushButton:pressed {} - QPushButton:disabled {} - QPushButton:checked {} - QPushButton:checked:hover {} - QPushButton:checked:pressed {} - """ - - def __init__( - self, name=None, path=None, hover_path=None, pressed_path=None, - hover_pressed_path=None, disabled_path=None, - size=None, *args, **kwargs - ): - self._hovered = False - self._pressed = False - super(PngButton, self).__init__(*args, **kwargs) - self.setStyleSheet(self.png_button_style) - - png_dict = {} - if name: - png_dict = PngFactory.get_png(name) or {} - if not png_dict: - print(( - "WARNING: There is not set icon with name \"{}\"" - "in PngFactory!" - ).format(name)) - - ico_normal = png_dict.get("normal") - ico_hover = png_dict.get("hover") - ico_pressed = png_dict.get("pressed") - ico_hover_pressed = png_dict.get("pressed_hover") - ico_disabled = png_dict.get("disabled") - - if path: - ico_normal = QtGui.QIcon(path) - if hover_path: - ico_hover = QtGui.QIcon(hover_path) - - if pressed_path: - ico_pressed = QtGui.QIcon(hover_path) - - if hover_pressed_path: - ico_hover_pressed = QtGui.QIcon(hover_pressed_path) - - if disabled_path: - ico_disabled = QtGui.QIcon(disabled_path) - - self.setIcon(ico_normal) - if size: - self.setIconSize(size) - self.setMaximumSize(size) - - self.ico_normal = ico_normal - self.ico_hover = ico_hover - self.ico_pressed = ico_pressed - self.ico_hover_pressed = ico_hover_pressed - self.ico_disabled = ico_disabled - - def setDisabled(self, in_bool): - super(PngButton, self).setDisabled(in_bool) - icon = self.ico_normal - if in_bool and self.ico_disabled: - icon = self.ico_disabled - self.setIcon(icon) - - def enterEvent(self, event): - self._hovered = True - if not self.isEnabled(): - return - icon = self.ico_normal - if self.ico_hover: - icon = self.ico_hover - - if self._pressed and self.ico_hover_pressed: - icon = self.ico_hover_pressed - - if self.icon() != icon: - self.setIcon(icon) - - def mouseMoveEvent(self, event): - super(PngButton, self).mouseMoveEvent(event) - if self._pressed: - mouse_pos = event.pos() - hovering = self.rect().contains(mouse_pos) - if hovering and not self._hovered: - self.enterEvent(event) - elif not hovering and self._hovered: - self.leaveEvent(event) - - def leaveEvent(self, event): - self._hovered = False - if not self.isEnabled(): - return - icon = self.ico_normal - if self._pressed and self.ico_pressed: - icon = self.ico_pressed - - if self.icon() != icon: - self.setIcon(icon) - - def mousePressEvent(self, event): - self._pressed = True - if not self.isEnabled(): - return - icon = self.ico_hover - if self.ico_pressed: - icon = self.ico_pressed - - if self.ico_hover_pressed: - mouse_pos = event.pos() - if self.rect().contains(mouse_pos): - icon = self.ico_hover_pressed - - if icon is None: - icon = self.ico_normal - - if self.icon() != icon: - self.setIcon(icon) - - def mouseReleaseEvent(self, event): - if not self.isEnabled(): - return - if self._pressed: - self._pressed = False - mouse_pos = event.pos() - if self.rect().contains(mouse_pos): - self.clicked.emit() - - icon = self.ico_normal - if self._hovered and self.ico_hover: - icon = self.ico_hover - - if self.icon() != icon: - self.setIcon(icon) diff --git a/client/ayon_core/tools/standalonepublish/widgets/widget_components.py b/client/ayon_core/tools/standalonepublish/widgets/widget_components.py deleted file mode 100644 index 65488878f9..0000000000 --- a/client/ayon_core/tools/standalonepublish/widgets/widget_components.py +++ /dev/null @@ -1,212 +0,0 @@ -import os -import json -import tempfile -import random -import string - -from qtpy import QtWidgets, QtCore - -from ayon_core.pipeline import legacy_io -from ayon_core.lib import ( - execute, - Logger, - get_openpype_execute_args, - apply_project_environments_value -) - -from . import DropDataFrame -from .constants import HOST_NAME - -log = Logger.get_logger("standalonepublisher") - - -class ComponentsWidget(QtWidgets.QWidget): - def __init__(self, parent): - super().__init__() - self.initialized = False - self.valid_components = False - self.valid_family = False - self.valid_repre_names = False - - body = QtWidgets.QWidget() - self.parent_widget = parent - self.drop_frame = DropDataFrame(self) - - buttons = QtWidgets.QWidget() - - layout = QtWidgets.QHBoxLayout(buttons) - - self.btn_browse = QtWidgets.QPushButton('Browse') - self.btn_browse.setToolTip('Browse for file(s).') - self.btn_browse.setFocusPolicy(QtCore.Qt.NoFocus) - - self.btn_publish = QtWidgets.QPushButton('Publish') - self.btn_publish.setToolTip('Publishes data.') - self.btn_publish.setFocusPolicy(QtCore.Qt.NoFocus) - - layout.addWidget(self.btn_browse, alignment=QtCore.Qt.AlignLeft) - layout.addWidget(self.btn_publish, alignment=QtCore.Qt.AlignRight) - - layout = QtWidgets.QVBoxLayout(body) - layout.setSpacing(0) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self.drop_frame) - layout.addWidget(buttons) - - layout = QtWidgets.QVBoxLayout(self) - layout.setSpacing(0) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(body) - - self.btn_browse.clicked.connect(self._browse) - self.btn_publish.clicked.connect(self._publish) - self.initialized = True - - def validation(self): - if self.initialized is False: - return - valid = ( - self.parent_widget.valid_family and - self.valid_components and - self.valid_repre_names - ) - self.btn_publish.setEnabled(valid) - - def set_valid_components(self, valid): - self.valid_components = valid - self.validation() - - def set_valid_repre_names(self, valid): - self.valid_repre_names = valid - self.validation() - - def process_mime_data(self, mime_data): - self.drop_frame.process_ent_mime(mime_data) - - def collect_data(self): - return self.drop_frame.collect_data() - - def _browse(self): - options = [ - QtWidgets.QFileDialog.DontResolveSymlinks, - QtWidgets.QFileDialog.DontUseNativeDialog - ] - folders = False - if folders: - # browse folders specifics - caption = "Browse folders to publish image sequences" - file_mode = QtWidgets.QFileDialog.Directory - options.append(QtWidgets.QFileDialog.ShowDirsOnly) - else: - # browse files specifics - caption = "Browse files to publish" - file_mode = QtWidgets.QFileDialog.ExistingFiles - - # create the dialog - file_dialog = QtWidgets.QFileDialog(parent=self, caption=caption) - file_dialog.setLabelText(QtWidgets.QFileDialog.Accept, "Select") - file_dialog.setLabelText(QtWidgets.QFileDialog.Reject, "Cancel") - file_dialog.setFileMode(file_mode) - - # set the appropriate options - for option in options: - file_dialog.setOption(option) - - # browse! - if not file_dialog.exec_(): - return - - # process the browsed files/folders for publishing - paths = file_dialog.selectedFiles() - self.drop_frame._process_paths(paths) - - def working_start(self, msg=None): - if hasattr(self, 'parent_widget'): - self.parent_widget.working_start(msg) - - def working_stop(self): - if hasattr(self, 'parent_widget'): - self.parent_widget.working_stop() - - def _publish(self): - log.info(self.parent_widget.pyblish_paths) - self.working_start('Pyblish is running') - try: - data = self.parent_widget.collect_data() - set_context( - data['project'], - data['asset'], - data['task'] - ) - result = cli_publish(data, self.parent_widget.pyblish_paths) - # Clear widgets from components list if publishing was successful - if result: - self.drop_frame.components_list.clear_widgets() - self.drop_frame._refresh_view() - finally: - self.working_stop() - - -def set_context(project, asset, task): - ''' Sets context for pyblish (must be done before pyblish is launched) - :param project: Name of `Project` where instance should be published - :type project: str - :param asset: Name of `Asset` where instance should be published - :type asset: str - ''' - os.environ["AVALON_PROJECT"] = project - legacy_io.Session["AVALON_PROJECT"] = project - os.environ["AVALON_ASSET"] = asset - legacy_io.Session["AVALON_ASSET"] = asset - if not task: - task = '' - os.environ["AVALON_TASK"] = task - legacy_io.Session["AVALON_TASK"] = task - - legacy_io.Session["current_dir"] = os.path.normpath(os.getcwd()) - - os.environ["AVALON_APP"] = HOST_NAME - legacy_io.Session["AVALON_APP"] = HOST_NAME - - -def cli_publish(data, publish_paths, gui=True): - PUBLISH_SCRIPT_PATH = os.path.join( - os.path.dirname(os.path.dirname(__file__)), - "publish.py" - ) - legacy_io.install() - - # Create hash name folder in temp - chars = "".join([random.choice(string.ascii_letters) for i in range(15)]) - staging_dir = tempfile.mkdtemp(chars) - - # create also json and fill with data - json_data_path = staging_dir + os.path.basename(staging_dir) + '.json' - with open(json_data_path, 'w') as outfile: - json.dump(data, outfile) - - envcopy = os.environ.copy() - envcopy["PYBLISH_HOSTS"] = "standalonepublisher" - envcopy["SAPUBLISH_INPATH"] = json_data_path - envcopy["PYBLISHGUI"] = "pyblish_pype" - envcopy["PUBLISH_PATHS"] = os.pathsep.join(publish_paths) - if data.get("family", "").lower() == "editorial": - envcopy["PYBLISH_SUSPEND_LOGS"] = "1" - - project_name = os.environ["AVALON_PROJECT"] - env_copy = apply_project_environments_value(project_name, envcopy) - - args = get_openpype_execute_args("run", PUBLISH_SCRIPT_PATH) - result = execute(args, env=envcopy) - - result = {} - if os.path.exists(json_data_path): - with open(json_data_path, "r") as f: - result = json.load(f) - os.remove(json_data_path) - - log.info(f"Publish result: {result}") - - legacy_io.uninstall() - - return False diff --git a/client/ayon_core/tools/standalonepublish/widgets/widget_components_list.py b/client/ayon_core/tools/standalonepublish/widgets/widget_components_list.py deleted file mode 100644 index e29ab3c127..0000000000 --- a/client/ayon_core/tools/standalonepublish/widgets/widget_components_list.py +++ /dev/null @@ -1,91 +0,0 @@ -from qtpy import QtWidgets - - -class ComponentsList(QtWidgets.QTableWidget): - def __init__(self, parent=None): - super().__init__(parent=parent) - - self.setObjectName("ComponentList") - - self._main_column = 0 - - self.setColumnCount(1) - self.setSelectionBehavior( - QtWidgets.QAbstractItemView.SelectRows - ) - self.setSelectionMode( - QtWidgets.QAbstractItemView.ExtendedSelection - ) - self.setVerticalScrollMode( - QtWidgets.QAbstractItemView.ScrollPerPixel - ) - self.verticalHeader().hide() - - try: - self.verticalHeader().setResizeMode( - QtWidgets.QHeaderView.ResizeToContents - ) - except Exception: - self.verticalHeader().setSectionResizeMode( - QtWidgets.QHeaderView.ResizeToContents - ) - - self.horizontalHeader().setStretchLastSection(True) - self.horizontalHeader().hide() - - def count(self): - return self.rowCount() - - def add_widget(self, widget, row=None): - if row is None: - row = self.count() - - self.insertRow(row) - self.setCellWidget(row, self._main_column, widget) - - self.resizeRowToContents(row) - - return row - - def remove_widget(self, row): - self.removeRow(row) - - def move_widget(self, widget, newRow): - oldRow = self.indexOfWidget(widget) - if oldRow: - self.insertRow(newRow) - # Collect the oldRow after insert to make sure we move the correct - # widget. - oldRow = self.indexOfWidget(widget) - - self.setCellWidget(newRow, self._main_column, widget) - self.resizeRowToContents(oldRow) - - # Remove the old row - self.removeRow(oldRow) - - def clear_widgets(self): - '''Remove all widgets.''' - self.clear() - self.setRowCount(0) - - def widget_index(self, widget): - index = None - for row in range(self.count()): - candidateWidget = self.widget_at(row) - if candidateWidget == widget: - index = row - break - - return index - - def widgets(self): - widgets = [] - for row in range(self.count()): - widget = self.widget_at(row) - widgets.append(widget) - - return widgets - - def widget_at(self, row): - return self.cellWidget(row, self._main_column) diff --git a/client/ayon_core/tools/standalonepublish/widgets/widget_drop_empty.py b/client/ayon_core/tools/standalonepublish/widgets/widget_drop_empty.py deleted file mode 100644 index 110e4d6353..0000000000 --- a/client/ayon_core/tools/standalonepublish/widgets/widget_drop_empty.py +++ /dev/null @@ -1,47 +0,0 @@ -from qtpy import QtWidgets, QtCore, QtGui - - -class DropEmpty(QtWidgets.QWidget): - - def __init__(self, parent): - '''Initialise DataDropZone widget.''' - super().__init__(parent) - - layout = QtWidgets.QVBoxLayout(self) - - BottomCenterAlignment = QtCore.Qt.AlignBottom | QtCore.Qt.AlignHCenter - TopCenterAlignment = QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter - - font = QtGui.QFont() - font.setFamily("DejaVu Sans Condensed") - font.setPointSize(26) - font.setBold(True) - font.setWeight(50) - font.setKerning(True) - - self._label = QtWidgets.QLabel('Drag & Drop') - self._label.setFont(font) - self._label.setAttribute(QtCore.Qt.WA_TranslucentBackground) - - font.setPointSize(12) - self._sub_label = QtWidgets.QLabel('(drop files here)') - self._sub_label.setFont(font) - self._sub_label.setAttribute(QtCore.Qt.WA_TranslucentBackground) - - layout.addWidget(self._label, alignment=BottomCenterAlignment) - layout.addWidget(self._sub_label, alignment=TopCenterAlignment) - - def paintEvent(self, event): - super().paintEvent(event) - painter = QtGui.QPainter(self) - pen = QtGui.QPen() - pen.setWidth(1) - pen.setBrush(QtCore.Qt.darkGray) - pen.setStyle(QtCore.Qt.DashLine) - painter.setPen(pen) - painter.drawRect( - 10, - 10, - self.rect().width() - 15, - self.rect().height() - 15 - ) diff --git a/client/ayon_core/tools/standalonepublish/widgets/widget_drop_frame.py b/client/ayon_core/tools/standalonepublish/widgets/widget_drop_frame.py deleted file mode 100644 index 3fdd507da9..0000000000 --- a/client/ayon_core/tools/standalonepublish/widgets/widget_drop_frame.py +++ /dev/null @@ -1,485 +0,0 @@ -import os -import re -import json -import clique -import subprocess -import ayon_core.lib -from qtpy import QtWidgets, QtCore - -from ayon_core.lib import get_ffprobe_data -from . import DropEmpty, ComponentsList, ComponentItem - - -class DropDataFrame(QtWidgets.QFrame): - image_extensions = [ - ".ani", ".anim", ".apng", ".art", ".bmp", ".bpg", ".bsave", ".cal", - ".cin", ".cpc", ".cpt", ".dds", ".dpx", ".ecw", ".exr", ".fits", - ".flic", ".flif", ".fpx", ".gif", ".hdri", ".hevc", ".icer", - ".icns", ".ico", ".cur", ".ics", ".ilbm", ".jbig", ".jbig2", - ".jng", ".jpeg", ".jpeg-ls", ".jpeg", ".2000", ".jpg", ".xr", - ".jpeg", ".xt", ".jpeg-hdr", ".kra", ".mng", ".miff", ".nrrd", - ".ora", ".pam", ".pbm", ".pgm", ".ppm", ".pnm", ".pcx", ".pgf", - ".pictor", ".png", ".psb", ".psp", ".qtvr", ".ras", - ".rgbe", ".logluv", ".tiff", ".sgi", ".tga", ".tiff", ".tiff/ep", - ".tiff/it", ".ufo", ".ufp", ".wbmp", ".webp", ".xbm", ".xcf", - ".xpm", ".xwd" - ] - video_extensions = [ - ".3g2", ".3gp", ".amv", ".asf", ".avi", ".drc", ".f4a", ".f4b", - ".f4p", ".f4v", ".flv", ".gif", ".gifv", ".m2v", ".m4p", ".m4v", - ".mkv", ".mng", ".mov", ".mp2", ".mp4", ".mpe", ".mpeg", ".mpg", - ".mpv", ".mxf", ".nsv", ".ogg", ".ogv", ".qt", ".rm", ".rmvb", - ".roq", ".svi", ".vob", ".webm", ".wmv", ".yuv" - ] - extensions = { - "nuke": [".nk"], - "maya": [".ma", ".mb"], - "houdini": [".hip"], - "image_file": image_extensions, - "video_file": video_extensions - } - - sequence_types = [ - ".bgeo", ".vdb", ".bgeosc", ".bgeogz" - ] - - def __init__(self, parent): - super().__init__() - self.parent_widget = parent - - self.setAcceptDrops(True) - layout = QtWidgets.QVBoxLayout(self) - self.components_list = ComponentsList(self) - layout.addWidget(self.components_list) - - self.drop_widget = DropEmpty(self) - - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth( - self.drop_widget.sizePolicy().hasHeightForWidth() - ) - self.drop_widget.setSizePolicy(sizePolicy) - - layout.addWidget(self.drop_widget) - - self._refresh_view() - - def dragEnterEvent(self, event): - event.setDropAction(QtCore.Qt.CopyAction) - event.accept() - - def dragLeaveEvent(self, event): - event.accept() - - def dropEvent(self, event): - self.process_ent_mime(event) - event.accept() - - def process_ent_mime(self, ent): - paths = [] - if ent.mimeData().hasUrls(): - paths = self._processMimeData(ent.mimeData()) - else: - # If path is in clipboard as string - try: - path = os.path.normpath(ent.text()) - if os.path.exists(path): - paths.append(path) - else: - print('Dropped invalid file/folder') - except Exception: - pass - if paths: - self._process_paths(paths) - - def _processMimeData(self, mimeData): - paths = [] - - for path in mimeData.urls(): - local_path = path.toLocalFile() - if os.path.isfile(local_path) or os.path.isdir(local_path): - paths.append(local_path) - else: - print('Invalid input: "{}"'.format(local_path)) - return paths - - def _add_item(self, data, actions=[]): - # Assign to self so garbage collector won't remove the component - # during initialization - new_component = ComponentItem(self.components_list, self) - new_component.set_context(data) - self.components_list.add_widget(new_component) - - new_component.signal_remove.connect(self._remove_item) - new_component.signal_preview.connect(self._set_preview) - new_component.signal_thumbnail.connect( - self._set_thumbnail - ) - new_component.signal_repre_change.connect(self.repre_name_changed) - for action in actions: - new_component.add_action(action) - - if len(self.components_list.widgets()) == 1: - self.parent_widget.set_valid_repre_names(True) - self._refresh_view() - - def _set_thumbnail(self, in_item): - current_state = in_item.is_thumbnail() - in_item.change_thumbnail(not current_state) - - checked_item = None - for item in self.components_list.widgets(): - if item.is_thumbnail(): - checked_item = item - break - if checked_item is not None and checked_item != in_item: - checked_item.change_thumbnail(False) - - in_item.change_thumbnail(current_state) - - def _set_preview(self, in_item): - current_state = in_item.is_preview() - in_item.change_preview(not current_state) - - checked_item = None - for item in self.components_list.widgets(): - if item.is_preview(): - checked_item = item - break - if checked_item is not None and checked_item != in_item: - checked_item.change_preview(False) - - in_item.change_preview(current_state) - - def _remove_item(self, in_item): - valid_repre = in_item.has_valid_repre is True - - self.components_list.remove_widget( - self.components_list.widget_index(in_item) - ) - self._refresh_view() - if valid_repre: - return - for item in self.components_list.widgets(): - if item.has_valid_repre: - continue - self.repre_name_changed(item, item.input_repre.text()) - - def _refresh_view(self): - _bool = len(self.components_list.widgets()) == 0 - self.components_list.setVisible(not _bool) - self.drop_widget.setVisible(_bool) - - self.parent_widget.set_valid_components(not _bool) - - def _process_paths(self, in_paths): - self.parent_widget.working_start() - paths = self._get_all_paths(in_paths) - collectionable_paths = [] - non_collectionable_paths = [] - for path in paths: - ext = os.path.splitext(path)[1] - if ext in self.image_extensions or ext in self.sequence_types: - collectionable_paths.append(path) - else: - non_collectionable_paths.append(path) - - collections, remainders = clique.assemble(collectionable_paths) - non_collectionable_paths.extend(remainders) - for collection in collections: - self._process_collection(collection) - - for remainder in non_collectionable_paths: - self._process_remainder(remainder) - self.parent_widget.working_stop() - - def _get_all_paths(self, paths): - output_paths = [] - for path in paths: - path = os.path.normpath(path) - if os.path.isfile(path): - output_paths.append(path) - elif os.path.isdir(path): - s_paths = [] - for s_item in os.listdir(path): - s_path = os.path.sep.join([path, s_item]) - s_paths.append(s_path) - output_paths.extend(self._get_all_paths(s_paths)) - else: - print('Invalid path: "{}"'.format(path)) - return output_paths - - def _process_collection(self, collection): - file_base = os.path.basename(collection.head) - folder_path = os.path.dirname(collection.head) - if file_base[-1] in ['.', '_']: - file_base = file_base[:-1] - file_ext = os.path.splitext( - collection.format('{head}{padding}{tail}'))[1] - repr_name = file_ext.replace('.', '') - range = collection.format('{ranges}') - - # TODO: ranges must not be with missing frames!!! - # - this is goal implementation: - # startFrame, endFrame = range.split('-') - rngs = range.split(',') - startFrame = rngs[0].split('-')[0] - endFrame = rngs[-1].split('-')[-1] - - actions = [] - - data = { - 'files': [file for file in collection], - 'name': file_base, - 'ext': file_ext, - 'file_info': range, - "frameStart": startFrame, - "frameEnd": endFrame, - 'representation': repr_name, - 'folder_path': folder_path, - 'is_sequence': True, - 'actions': actions - } - - self._process_data(data) - - def _process_remainder(self, remainder): - filename = os.path.basename(remainder) - folder_path = os.path.dirname(remainder) - file_base, file_ext = os.path.splitext(filename) - repr_name = file_ext.replace('.', '') - file_info = None - - files = [] - files.append(remainder) - - actions = [] - - data = { - 'files': files, - 'name': file_base, - 'ext': file_ext, - 'representation': repr_name, - 'folder_path': folder_path, - 'is_sequence': False, - 'actions': actions - } - - self._process_data(data) - - def load_data_with_probe(self, filepath): - ffprobe_data = get_ffprobe_data(filepath) - return ffprobe_data["streams"][0] - - def get_file_data(self, data): - filepath = data['files'][0] - ext = data['ext'].lower() - output = {"fps": None} - - file_info = None - if 'file_info' in data: - file_info = data['file_info'] - - if ext in self.image_extensions or ext in self.video_extensions: - probe_data = self.load_data_with_probe(filepath) - if 'fps' not in data: - # default value - fps = 25 - fps_string = probe_data.get('r_frame_rate') - if fps_string: - fps = int(fps_string.split('/')[0]) - - output['fps'] = fps - - if "frameStart" not in data or "frameEnd" not in data: - startFrame = endFrame = 1 - endFrame_string = probe_data.get('nb_frames') - - if endFrame_string: - endFrame = int(endFrame_string) - - output["frameStart"] = startFrame - output["frameEnd"] = endFrame - - if (ext == '.mov') and (not file_info): - file_info = probe_data.get('codec_name') - - output['file_info'] = file_info - - return output - - def _process_data(self, data): - ext = data['ext'] - # load file data info - file_data = self.get_file_data(data) - for key, value in file_data.items(): - data[key] = value - - icon = 'default' - for ico, exts in self.extensions.items(): - if ext in exts: - icon = ico - break - - new_is_seq = data['is_sequence'] - # Add 's' to icon_name if is sequence (image -> images) - if new_is_seq: - icon += 's' - data['icon'] = icon - data['thumb'] = ( - ext in self.image_extensions - or ext in self.video_extensions - ) - data['prev'] = ( - ext in self.video_extensions - or (new_is_seq and ext in self.image_extensions) - ) - - actions = [] - - found = False - if data["ext"] in self.image_extensions: - for item in self.components_list.widgets(): - if data['ext'] != item.in_data['ext']: - continue - if data['folder_path'] != item.in_data['folder_path']: - continue - - ex_is_seq = item.in_data['is_sequence'] - - # If both are single files - if not new_is_seq and not ex_is_seq: - if data['name'] == item.in_data['name']: - found = True - break - paths = list(data['files']) - paths.extend(item.in_data['files']) - c, r = clique.assemble(paths) - if len(c) == 0: - continue - a_name = 'merge' - item.add_action(a_name) - if a_name not in actions: - actions.append(a_name) - - # If new is sequence and ex is single file - elif new_is_seq and not ex_is_seq: - if data['name'] not in item.in_data['name']: - continue - ex_file = item.in_data['files'][0] - - a_name = 'merge' - item.add_action(a_name) - if a_name not in actions: - actions.append(a_name) - continue - - # If new is single file existing is sequence - elif not new_is_seq and ex_is_seq: - if item.in_data['name'] not in data['name']: - continue - a_name = 'merge' - item.add_action(a_name) - if a_name not in actions: - actions.append(a_name) - - # If both are sequence - else: - if data['name'] != item.in_data['name']: - continue - if data['files'] == list(item.in_data['files']): - found = True - break - a_name = 'merge' - item.add_action(a_name) - if a_name not in actions: - actions.append(a_name) - - if new_is_seq: - actions.append('split') - - if found is False: - new_repre = self.handle_new_repre_name(data['representation']) - data['representation'] = new_repre - self._add_item(data, actions) - - def handle_new_repre_name(self, repre_name): - renamed = False - for item in self.components_list.widgets(): - if repre_name == item.input_repre.text(): - check_regex = '_\w+$' - result = re.findall(check_regex, repre_name) - next_num = 2 - if len(result) == 1: - repre_name = repre_name.replace(result[0], '') - next_num = int(result[0].replace('_', '')) - next_num += 1 - repre_name = '{}_{}'.format(repre_name, next_num) - renamed = True - break - if renamed: - return self.handle_new_repre_name(repre_name) - return repre_name - - def repre_name_changed(self, in_item, repre_name): - is_valid = True - if repre_name.strip() == '': - in_item.set_repre_name_valid(False) - is_valid = False - else: - for item in self.components_list.widgets(): - if item == in_item: - continue - if item.input_repre.text() == repre_name: - item.set_repre_name_valid(False) - in_item.set_repre_name_valid(False) - is_valid = False - global_valid = is_valid - if is_valid: - in_item.set_repre_name_valid(True) - for item in self.components_list.widgets(): - if item.has_valid_repre: - continue - self.repre_name_changed(item, item.input_repre.text()) - for item in self.components_list.widgets(): - if not item.has_valid_repre: - global_valid = False - break - self.parent_widget.set_valid_repre_names(global_valid) - - def merge_items(self, in_item): - self.parent_widget.working_start() - items = [] - in_paths = in_item.in_data['files'] - paths = in_paths - for item in self.components_list.widgets(): - if item.in_data['files'] == in_paths: - items.append(item) - continue - copy_paths = paths.copy() - copy_paths.extend(item.in_data['files']) - collections, remainders = clique.assemble(copy_paths) - if len(collections) == 1 and len(remainders) == 0: - paths.extend(item.in_data['files']) - items.append(item) - for item in items: - self._remove_item(item) - self._process_paths(paths) - self.parent_widget.working_stop() - - def split_items(self, item): - self.parent_widget.working_start() - paths = item.in_data['files'] - self._remove_item(item) - for path in paths: - self._process_remainder(path) - self.parent_widget.working_stop() - - def collect_data(self): - data = {'representations' : []} - for item in self.components_list.widgets(): - data['representations'].append(item.collect_data()) - return data diff --git a/client/ayon_core/tools/standalonepublish/widgets/widget_family.py b/client/ayon_core/tools/standalonepublish/widgets/widget_family.py deleted file mode 100644 index 13ef65ee28..0000000000 --- a/client/ayon_core/tools/standalonepublish/widgets/widget_family.py +++ /dev/null @@ -1,421 +0,0 @@ -import re - -from qtpy import QtWidgets, QtCore - -from ayon_core.client import ( - get_asset_by_name, - get_subset_by_name, - get_subsets, - get_last_version_by_subset_id, -) -from ayon_core.settings import get_project_settings -from ayon_core.pipeline import LegacyCreator -from ayon_core.pipeline.version_start import get_versioning_start -from ayon_core.pipeline.create import ( - SUBSET_NAME_ALLOWED_SYMBOLS, - TaskNotSetError, -) - -from . import HelpRole, FamilyRole, ExistsRole, PluginRole, PluginKeyRole -from . import FamilyDescriptionWidget - - -class FamilyWidget(QtWidgets.QWidget): - - stateChanged = QtCore.Signal(bool) - data = dict() - _jobs = dict() - Separator = "---separator---" - NOT_SELECTED = '< Nothing is selected >' - - def __init__(self, dbcon, parent=None): - super(FamilyWidget, self).__init__(parent=parent) - # Store internal states in here - self.state = {"valid": False} - self.dbcon = dbcon - self.asset_name = self.NOT_SELECTED - - body = QtWidgets.QWidget() - lists = QtWidgets.QWidget() - - container = QtWidgets.QWidget() - - list_families = QtWidgets.QListWidget() - - input_subset = QtWidgets.QLineEdit() - input_result = QtWidgets.QLineEdit() - input_result.setEnabled(False) - - # region Menu for default subset names - btn_subset = QtWidgets.QPushButton() - btn_subset.setFixedWidth(18) - menu_subset = QtWidgets.QMenu(btn_subset) - btn_subset.setMenu(menu_subset) - - # endregion - name_layout = QtWidgets.QHBoxLayout() - name_layout.addWidget(input_subset, 1) - name_layout.addWidget(btn_subset, 0) - name_layout.setContentsMargins(0, 0, 0, 0) - - # version - version_spinbox = QtWidgets.QSpinBox() - version_spinbox.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) - version_spinbox.setMinimum(1) - version_spinbox.setMaximum(9999) - version_spinbox.setEnabled(False) - - version_checkbox = QtWidgets.QCheckBox("Next Available Version") - version_checkbox.setCheckState(QtCore.Qt.CheckState(2)) - - version_layout = QtWidgets.QHBoxLayout() - version_layout.addWidget(version_spinbox) - version_layout.addWidget(version_checkbox) - - layout = QtWidgets.QVBoxLayout(container) - - header = FamilyDescriptionWidget(parent=self) - layout.addWidget(header) - - layout.addWidget(QtWidgets.QLabel("Family")) - layout.addWidget(list_families) - layout.addWidget(QtWidgets.QLabel("Subset")) - layout.addLayout(name_layout) - layout.addWidget(input_result) - layout.addWidget(QtWidgets.QLabel("Version")) - layout.addLayout(version_layout) - layout.setContentsMargins(0, 0, 0, 0) - - options = QtWidgets.QWidget() - - layout = QtWidgets.QGridLayout(options) - layout.setContentsMargins(0, 0, 0, 0) - - layout = QtWidgets.QHBoxLayout(lists) - layout.addWidget(container) - layout.setContentsMargins(0, 0, 0, 0) - - layout = QtWidgets.QVBoxLayout(body) - - layout.addWidget(lists) - layout.addWidget(options, 0, QtCore.Qt.AlignLeft) - layout.setContentsMargins(0, 0, 0, 0) - - layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(body) - - input_subset.textChanged.connect(self.on_data_changed) - list_families.currentItemChanged.connect(self.on_selection_changed) - list_families.currentItemChanged.connect(header.set_item) - version_checkbox.stateChanged.connect(self.on_version_refresh) - - self.stateChanged.connect(self._on_state_changed) - - self.input_subset = input_subset - self.menu_subset = menu_subset - self.btn_subset = btn_subset - self.list_families = list_families - self.input_result = input_result - self.version_checkbox = version_checkbox - self.version_spinbox = version_spinbox - - self.refresh() - - def collect_data(self): - plugin = self.list_families.currentItem().data(PluginRole) - key = self.list_families.currentItem().data(PluginKeyRole) - family = plugin.family.rsplit(".", 1)[-1] - data = { - 'family_preset_key': key, - 'family': family, - 'subset': self.input_result.text(), - 'version': self.version_spinbox.value(), - 'use_next_available_version': self.version_checkbox.isChecked(), - } - return data - - def on_task_change(self): - self.on_data_changed() - - def change_asset(self, name): - if name is None: - name = self.NOT_SELECTED - self.asset_name = name - self.on_data_changed() - - def _on_state_changed(self, state): - self.state['valid'] = state - - def _build_menu(self, default_names): - """Create optional predefined subset names - - Args: - default_names(list): all predefined names - - Returns: - None - """ - - # Get and destroy the action group - group = self.btn_subset.findChild(QtWidgets.QActionGroup) - if group: - group.deleteLater() - - state = any(default_names) - self.btn_subset.setEnabled(state) - if state is False: - return - - # Build new action group - group = QtWidgets.QActionGroup(self.btn_subset) - for name in default_names: - if name == self.Separator: - self.menu_subset.addSeparator() - continue - action = group.addAction(name) - self.menu_subset.addAction(action) - - group.triggered.connect(self._on_action_clicked) - - def _on_action_clicked(self, action): - self.input_subset.setText(action.text()) - - def _on_data_changed(self): - asset_name = self.asset_name - user_input_text = self.input_subset.text() - item = self.list_families.currentItem() - - if item is None: - return - - # Early exit if no asset name - if ( - asset_name == self.NOT_SELECTED - or not asset_name.strip() - ): - self._build_menu([]) - item.setData(ExistsRole, False) - print("Asset name is required ..") - self.stateChanged.emit(False) - return - - # Get the asset from the database which match with the name - project_name = self.dbcon.active_project() - asset_doc = get_asset_by_name( - project_name, asset_name, fields=["_id"] - ) - - # Get plugin - plugin = item.data(PluginRole) - - if asset_doc and plugin: - asset_id = asset_doc["_id"] - task_name = self.dbcon.Session["AVALON_TASK"] - - # Calculate subset name with Creator plugin - try: - subset_name = plugin.get_subset_name( - user_input_text, task_name, asset_id, project_name - ) - # Force replacement of prohibited symbols - # QUESTION should Creator care about this and here should be - # only validated with schema regex? - subset_name = re.sub( - "[^{}]+".format(SUBSET_NAME_ALLOWED_SYMBOLS), - "", - subset_name - ) - self.input_result.setText(subset_name) - - except TaskNotSetError: - subset_name = "" - self.input_result.setText("Select task please") - - # Get all subsets of the current asset - subset_docs = get_subsets( - project_name, asset_ids=[asset_id], fields=["name"] - ) - - existing_subset_names = { - subset_doc["name"] - for subset_doc in subset_docs - } - - # Defaults to dropdown - defaults = [] - # Check if Creator plugin has set defaults - if ( - plugin.defaults - and isinstance(plugin.defaults, (list, tuple, set)) - ): - defaults = list(plugin.defaults) - - # Replace - compare_regex = re.compile(re.sub( - user_input_text, "(.+)", subset_name, flags=re.IGNORECASE - )) - subset_hints = set() - if user_input_text: - for _name in existing_subset_names: - _result = compare_regex.search(_name) - if _result: - subset_hints |= set(_result.groups()) - - subset_hints = subset_hints - set(defaults) - if subset_hints: - if defaults: - defaults.append(self.Separator) - defaults.extend(subset_hints) - self._build_menu(defaults) - - item.setData(ExistsRole, True) - - else: - subset_name = user_input_text - self._build_menu([]) - item.setData(ExistsRole, False) - - if not plugin: - print("No registered families ..") - else: - print("Asset '%s' not found .." % asset_name) - - self.on_version_refresh() - - # Update the valid state - valid = ( - asset_name != self.NOT_SELECTED and - subset_name.strip() != "" and - item.data(QtCore.Qt.ItemIsEnabled) and - item.data(ExistsRole) - ) - self.stateChanged.emit(valid) - - def on_version_refresh(self): - auto_version = self.version_checkbox.isChecked() - self.version_spinbox.setEnabled(not auto_version) - if not auto_version: - return - - project_name = self.dbcon.active_project() - asset_name = self.asset_name - subset_name = self.input_result.text() - plugin = self.list_families.currentItem().data(PluginRole) - family = plugin.family.rsplit(".", 1)[-1] - version = get_versioning_start( - project_name, - "standalonepublisher", - task_name=self.dbcon.Session["AVALON_TASK"], - family=family, - subset=subset_name - ) - - asset_doc = None - subset_doc = None - if ( - asset_name != self.NOT_SELECTED and - subset_name.strip() != '' - ): - asset_doc = get_asset_by_name( - project_name, asset_name, fields=["_id"] - ) - - if asset_doc: - subset_doc = get_subset_by_name( - project_name, - subset_name, - asset_doc['_id'], - fields=["_id"] - ) - - if subset_doc: - last_version = get_last_version_by_subset_id( - project_name, - subset_doc["_id"], - fields=["name"] - ) - if last_version: - version = last_version["name"] + 1 - - self.version_spinbox.setValue(version) - - def on_data_changed(self, *args): - - # Set invalid state until it's reconfirmed to be valid by the - # scheduled callback so any form of creation is held back until - # valid again - self.stateChanged.emit(False) - self.schedule(self._on_data_changed, 500, channel="gui") - - def on_selection_changed(self, *args): - item = self.list_families.currentItem() - if not item: - return - plugin = item.data(PluginRole) - if plugin is None: - return - - if plugin.defaults and isinstance(plugin.defaults, list): - default = plugin.defaults[0] - else: - default = "Default" - - self.input_subset.setText(default) - - self.on_data_changed() - - def keyPressEvent(self, event): - """Custom keyPressEvent. - - Override keyPressEvent to do nothing so that Maya's panels won't - take focus when pressing "SHIFT" whilst mouse is over viewport or - outliner. This way users don't accidentally perform Maya commands - whilst trying to name an instance. - - """ - - def refresh(self): - self.list_families.clear() - - has_families = False - project_name = self.dbcon.Session.get("AVALON_PROJECT") - if not project_name: - return - - settings = get_project_settings(project_name) - sp_settings = settings.get('standalonepublisher', {}) - - for key, creator_data in sp_settings.get("create", {}).items(): - creator = type(key, (LegacyCreator, ), creator_data) - - label = creator.label or creator.family - item = QtWidgets.QListWidgetItem(label) - item.setData(QtCore.Qt.ItemIsEnabled, True) - item.setData(HelpRole, creator.help or "") - item.setData(FamilyRole, creator.family) - item.setData(PluginRole, creator) - item.setData(PluginKeyRole, key) - item.setData(ExistsRole, False) - self.list_families.addItem(item) - - has_families = True - - if not has_families: - item = QtWidgets.QListWidgetItem("No registered families") - item.setData(QtCore.Qt.ItemIsEnabled, False) - self.list_families.addItem(item) - - self.list_families.setCurrentItem(self.list_families.item(0)) - - def schedule(self, func, time, channel="default"): - try: - self._jobs[channel].stop() - except (AttributeError, KeyError): - pass - - timer = QtCore.QTimer() - timer.setSingleShot(True) - timer.timeout.connect(func) - timer.start(time) - - self._jobs[channel] = timer diff --git a/client/ayon_core/tools/standalonepublish/widgets/widget_family_desc.py b/client/ayon_core/tools/standalonepublish/widgets/widget_family_desc.py deleted file mode 100644 index 33174a852b..0000000000 --- a/client/ayon_core/tools/standalonepublish/widgets/widget_family_desc.py +++ /dev/null @@ -1,96 +0,0 @@ -import six -from qtpy import QtWidgets, QtCore, QtGui -import qtawesome -from . import FamilyRole, PluginRole - - -class FamilyDescriptionWidget(QtWidgets.QWidget): - """A family description widget. - - Shows a family icon, family name and a help description. - Used in creator header. - - _________________ - | ____ | - | |icon| FAMILY | - | |____| help | - |_________________| - - """ - - SIZE = 35 - - def __init__(self, parent=None): - super(FamilyDescriptionWidget, self).__init__(parent=parent) - - # Header font - font = QtGui.QFont() - font.setBold(True) - font.setPointSize(14) - - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - - icon = QtWidgets.QLabel() - icon.setSizePolicy(QtWidgets.QSizePolicy.Maximum, - QtWidgets.QSizePolicy.Maximum) - - # Add 4 pixel padding to avoid icon being cut off - icon.setFixedWidth(self.SIZE + 4) - icon.setFixedHeight(self.SIZE + 4) - icon.setStyleSheet(""" - QLabel { - padding-right: 5px; - } - """) - - label_layout = QtWidgets.QVBoxLayout() - label_layout.setSpacing(0) - - family = QtWidgets.QLabel("family") - family.setFont(font) - family.setAlignment(QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft) - - help = QtWidgets.QLabel("help") - help.setWordWrap(True) - help.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft) - - label_layout.addWidget(family) - label_layout.addWidget(help) - - layout.addWidget(icon) - layout.addLayout(label_layout) - - self.help = help - self.family = family - self.icon = icon - - def set_item(self, item): - """Update elements to display information of a family item. - - Args: - family (dict): A family item as registered with name, help and icon - - Returns: - None - - """ - if not item: - return - - # Support a font-awesome icon - plugin = item.data(PluginRole) - icon = getattr(plugin, "icon", "info-circle") - assert isinstance(icon, six.string_types) - icon = qtawesome.icon("fa.{}".format(icon), color="white") - pixmap = icon.pixmap(self.SIZE, self.SIZE) - pixmap = pixmap.scaled(self.SIZE, self.SIZE) - - # Parse a clean line from the Creator's docstring - docstring = plugin.help or "" - - help = docstring.splitlines()[0] if docstring else "" - - self.icon.setPixmap(pixmap) - self.family.setText(item.data(FamilyRole)) - self.help.setText(help) diff --git a/client/ayon_core/tools/standalonepublish/widgets/widget_shadow.py b/client/ayon_core/tools/standalonepublish/widgets/widget_shadow.py deleted file mode 100644 index 64cb9544fa..0000000000 --- a/client/ayon_core/tools/standalonepublish/widgets/widget_shadow.py +++ /dev/null @@ -1,42 +0,0 @@ -from qtpy import QtWidgets, QtCore, QtGui - - -class ShadowWidget(QtWidgets.QWidget): - def __init__(self, parent): - self.parent_widget = parent - super().__init__(parent) - w = self.parent_widget.frameGeometry().width() - h = self.parent_widget.frameGeometry().height() - self.resize(QtCore.QSize(w, h)) - palette = QtGui.QPalette(self.palette()) - palette.setColor(palette.Background, QtCore.Qt.transparent) - self.setPalette(palette) - self.message = '' - - font = QtGui.QFont() - font.setFamily("DejaVu Sans Condensed") - font.setPointSize(40) - font.setBold(True) - font.setWeight(50) - font.setKerning(True) - self.font = font - - def paintEvent(self, event): - painter = QtGui.QPainter() - painter.begin(self) - painter.setFont(self.font) - painter.setRenderHint(QtGui.QPainter.Antialiasing) - painter.fillRect( - event.rect(), QtGui.QBrush(QtGui.QColor(0, 0, 0, 127)) - ) - painter.drawText( - QtCore.QRectF( - 0.0, - 0.0, - self.parent_widget.frameGeometry().width(), - self.parent_widget.frameGeometry().height() - ), - QtCore.Qt.AlignCenter | QtCore.Qt.AlignCenter, - self.message - ) - painter.end()