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()