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 7a830ad133..0000000000
Binary files a/client/ayon_core/tools/standalonepublish/widgets/resources/file.png and /dev/null differ
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 f6f89fe149..0000000000
Binary files a/client/ayon_core/tools/standalonepublish/widgets/resources/files.png and /dev/null differ
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 11cfa46dce..0000000000
Binary files a/client/ayon_core/tools/standalonepublish/widgets/resources/houdini.png and /dev/null differ
diff --git a/client/ayon_core/tools/standalonepublish/widgets/resources/image_file.png b/client/ayon_core/tools/standalonepublish/widgets/resources/image_file.png
deleted file mode 100644
index adea862e5b..0000000000
Binary files a/client/ayon_core/tools/standalonepublish/widgets/resources/image_file.png and /dev/null differ
diff --git a/client/ayon_core/tools/standalonepublish/widgets/resources/image_files.png b/client/ayon_core/tools/standalonepublish/widgets/resources/image_files.png
deleted file mode 100644
index 2db779ab30..0000000000
Binary files a/client/ayon_core/tools/standalonepublish/widgets/resources/image_files.png and /dev/null differ
diff --git a/client/ayon_core/tools/standalonepublish/widgets/resources/information.svg b/client/ayon_core/tools/standalonepublish/widgets/resources/information.svg
deleted file mode 100644
index e0f73a7eb1..0000000000
--- a/client/ayon_core/tools/standalonepublish/widgets/resources/information.svg
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
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 e84a6a3742..0000000000
Binary files a/client/ayon_core/tools/standalonepublish/widgets/resources/maya.png and /dev/null differ
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 da83b45244..0000000000
Binary files a/client/ayon_core/tools/standalonepublish/widgets/resources/menu.png and /dev/null differ
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 e4758f0b19..0000000000
Binary files a/client/ayon_core/tools/standalonepublish/widgets/resources/menu_disabled.png and /dev/null differ
diff --git a/client/ayon_core/tools/standalonepublish/widgets/resources/menu_hover.png b/client/ayon_core/tools/standalonepublish/widgets/resources/menu_hover.png
deleted file mode 100644
index dfe8ed53b2..0000000000
Binary files a/client/ayon_core/tools/standalonepublish/widgets/resources/menu_hover.png and /dev/null differ
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 a5f931b2c4..0000000000
Binary files a/client/ayon_core/tools/standalonepublish/widgets/resources/menu_pressed.png and /dev/null differ
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 51503add0f..0000000000
Binary files a/client/ayon_core/tools/standalonepublish/widgets/resources/menu_pressed_hover.png and /dev/null differ
diff --git a/client/ayon_core/tools/standalonepublish/widgets/resources/nuke.png b/client/ayon_core/tools/standalonepublish/widgets/resources/nuke.png
deleted file mode 100644
index 4234454096..0000000000
Binary files a/client/ayon_core/tools/standalonepublish/widgets/resources/nuke.png and /dev/null differ
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 eb5b3d1ba2..0000000000
Binary files a/client/ayon_core/tools/standalonepublish/widgets/resources/premiere.png and /dev/null differ
diff --git a/client/ayon_core/tools/standalonepublish/widgets/resources/trash.png b/client/ayon_core/tools/standalonepublish/widgets/resources/trash.png
deleted file mode 100644
index 8d12d5f8e0..0000000000
Binary files a/client/ayon_core/tools/standalonepublish/widgets/resources/trash.png and /dev/null differ
diff --git a/client/ayon_core/tools/standalonepublish/widgets/resources/trash_disabled.png b/client/ayon_core/tools/standalonepublish/widgets/resources/trash_disabled.png
deleted file mode 100644
index 06f5ae5276..0000000000
Binary files a/client/ayon_core/tools/standalonepublish/widgets/resources/trash_disabled.png and /dev/null differ
diff --git a/client/ayon_core/tools/standalonepublish/widgets/resources/trash_hover.png b/client/ayon_core/tools/standalonepublish/widgets/resources/trash_hover.png
deleted file mode 100644
index 4725c0f8ab..0000000000
Binary files a/client/ayon_core/tools/standalonepublish/widgets/resources/trash_hover.png and /dev/null differ
diff --git a/client/ayon_core/tools/standalonepublish/widgets/resources/trash_pressed.png b/client/ayon_core/tools/standalonepublish/widgets/resources/trash_pressed.png
deleted file mode 100644
index 901b0e6d35..0000000000
Binary files a/client/ayon_core/tools/standalonepublish/widgets/resources/trash_pressed.png and /dev/null differ
diff --git a/client/ayon_core/tools/standalonepublish/widgets/resources/trash_pressed_hover.png b/client/ayon_core/tools/standalonepublish/widgets/resources/trash_pressed_hover.png
deleted file mode 100644
index 076ced260f..0000000000
Binary files a/client/ayon_core/tools/standalonepublish/widgets/resources/trash_pressed_hover.png and /dev/null differ
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 346277e40f..0000000000
Binary files a/client/ayon_core/tools/standalonepublish/widgets/resources/video_file.png and /dev/null differ
diff --git a/client/ayon_core/tools/standalonepublish/widgets/widget_asset.py b/client/ayon_core/tools/standalonepublish/widgets/widget_asset.py
deleted file mode 100644
index 5ccd54398a..0000000000
--- a/client/ayon_core/tools/standalonepublish/widgets/widget_asset.py
+++ /dev/null
@@ -1,443 +0,0 @@
-import contextlib
-from qtpy import QtWidgets, QtCore
-import qtawesome
-
-from ayon_core import AYON_SERVER_ENABLED
-from ayon_core.client import (
- get_projects,
- get_project,
- get_asset_by_id,
-)
-from ayon_core.tools.utils import PlaceholderLineEdit
-
-from ayon_core.style import get_default_tools_icon_color
-
-from . import RecursiveSortFilterProxyModel, AssetModel
-from . import TasksTemplateModel, DeselectableTreeView
-from . import _iter_model_rows
-
-@contextlib.contextmanager
-def preserve_expanded_rows(tree_view,
- column=0,
- role=QtCore.Qt.DisplayRole):
- """Preserves expanded row in QTreeView by column's data role.
-
- This function is created to maintain the expand vs collapse status of
- the model items. When refresh is triggered the items which are expanded
- will stay expanded and vice versa.
-
- Arguments:
- tree_view (QWidgets.QTreeView): the tree view which is
- nested in the application
- column (int): the column to retrieve the data from
- role (int): the role which dictates what will be returned
-
- Returns:
- None
-
- """
-
- model = tree_view.model()
-
- expanded = set()
-
- for index in _iter_model_rows(model,
- column=column,
- include_root=False):
- if tree_view.isExpanded(index):
- value = index.data(role)
- expanded.add(value)
-
- try:
- yield
- finally:
- if not expanded:
- return
-
- for index in _iter_model_rows(model,
- column=column,
- include_root=False):
- value = index.data(role)
- state = value in expanded
- if state:
- tree_view.expand(index)
- else:
- tree_view.collapse(index)
-
-
-@contextlib.contextmanager
-def preserve_selection(tree_view,
- column=0,
- role=QtCore.Qt.DisplayRole,
- current_index=True):
- """Preserves row selection in QTreeView by column's data role.
-
- This function is created to maintain the selection status of
- the model items. When refresh is triggered the items which are expanded
- will stay expanded and vice versa.
-
- tree_view (QWidgets.QTreeView): the tree view nested in the application
- column (int): the column to retrieve the data from
- role (int): the role which dictates what will be returned
-
- Returns:
- None
-
- """
-
- model = tree_view.model()
- selection_model = tree_view.selectionModel()
- flags = (
- QtCore.QItemSelectionModel.Select
- | QtCore.QItemSelectionModel.Rows
- )
-
- if current_index:
- current_index_value = tree_view.currentIndex().data(role)
- else:
- current_index_value = None
-
- selected_rows = selection_model.selectedRows()
- if not selected_rows:
- yield
- return
-
- selected = set(row.data(role) for row in selected_rows)
- try:
- yield
- finally:
- if not selected:
- return
-
- # Go through all indices, select the ones with similar data
- for index in _iter_model_rows(model,
- column=column,
- include_root=False):
-
- value = index.data(role)
- state = value in selected
- if state:
- tree_view.scrollTo(index) # Ensure item is visible
- selection_model.select(index, flags)
-
- if current_index_value and value == current_index_value:
- tree_view.setCurrentIndex(index)
-
-
-class AssetWidget(QtWidgets.QWidget):
- """A Widget to display a tree of assets with filter
-
- To list the assets of the active project:
- >>> # 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()