From f4824cdc426c47f5db65d4ac417d1328259d0cb4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 12 Nov 2025 14:41:24 +0100 Subject: [PATCH 01/13] Allow creation of farm instances without colorspace data --- .../pipeline/farm/pyblish_functions.py | 61 ++++++++++++------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index 2193e96cb1..5e632c3599 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -591,22 +591,6 @@ def create_instances_for_aov( # AOV product of its own. log = Logger.get_logger("farm_publishing") - additional_color_data = { - "renderProducts": instance.data["renderProducts"], - "colorspaceConfig": instance.data["colorspaceConfig"], - "display": instance.data["colorspaceDisplay"], - "view": instance.data["colorspaceView"] - } - - # Get templated path from absolute config path. - anatomy = instance.context.data["anatomy"] - colorspace_template = instance.data["colorspaceConfig"] - try: - additional_color_data["colorspaceTemplate"] = remap_source( - colorspace_template, anatomy) - except ValueError as e: - log.warning(e) - additional_color_data["colorspaceTemplate"] = colorspace_template # if there are product to attach to and more than one AOV, # we cannot proceed. @@ -618,6 +602,29 @@ def create_instances_for_aov( "attaching multiple AOVs or renderable cameras to " "product is not supported yet.") + additional_data = { + "renderProducts": instance.data["renderProducts"], + } + + # Collect color management data if present + if "colorspaceConfig" in instance.data: + additional_data.update({ + "colorspaceConfig": instance.data["colorspaceConfig"], + # Display/View are optional + "display": instance.data.get("colorspaceDisplay"), + "view": instance.data.get("colorspaceView") + }) + + # Get templated path from absolute config path. + anatomy = instance.context.data["anatomy"] + colorspace_template = instance.data["colorspaceConfig"] + try: + additional_data["colorspaceTemplate"] = remap_source( + colorspace_template, anatomy) + except ValueError as e: + log.warning(e) + additional_data["colorspaceTemplate"] = colorspace_template + # create instances for every AOV we found in expected files. # NOTE: this is done for every AOV and every render camera (if # there are multiple renderable cameras in scene) @@ -625,7 +632,7 @@ def create_instances_for_aov( instance, skeleton, aov_filter, - additional_color_data, + additional_data, skip_integration_repre_list, do_not_add_review, frames_to_render @@ -936,16 +943,28 @@ def _create_instances_for_aov( "stagingDir": staging_dir, "fps": new_instance.get("fps"), "tags": ["review"] if preview else [], - "colorspaceData": { + } + + if colorspace and additional_data["colorspaceConfig"]: + # Only apply colorspace data if the image has a colorspace + colorspace_data: dict = { "colorspace": colorspace, "config": { "path": additional_data["colorspaceConfig"], "template": additional_data["colorspaceTemplate"] }, - "display": additional_data["display"], - "view": additional_data["view"] } - } + # Display/View are optional + display = additional_data.get("display") + if display: + additional_data["display"] = display + view = additional_data.get("view") + if view: + additional_data["view"] = view + + rep["colorspaceData"] = colorspace_data + else: + log.debug("No colorspace data for representation: {}".format(rep)) # support conversion from tiled to scanline if instance.data.get("convertToScanline"): From 4d90d35fc7204e97e4588c9f7969d58e7037630a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:01:08 +0100 Subject: [PATCH 02/13] Extended open file possibilities --- client/ayon_core/plugins/loader/open_file.py | 286 ++++++++++++++++--- 1 file changed, 254 insertions(+), 32 deletions(-) diff --git a/client/ayon_core/plugins/loader/open_file.py b/client/ayon_core/plugins/loader/open_file.py index 9b5a6fec20..80ddf925d3 100644 --- a/client/ayon_core/plugins/loader/open_file.py +++ b/client/ayon_core/plugins/loader/open_file.py @@ -1,8 +1,10 @@ import os import sys import subprocess +import platform import collections -from typing import Optional, Any +import ctypes +from typing import Optional, Any, Callable from ayon_core.pipeline.load import get_representation_path_with_anatomy from ayon_core.pipeline.actions import ( @@ -13,6 +15,232 @@ from ayon_core.pipeline.actions import ( ) +WINDOWS_USER_REG_PATH = ( + r"Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts" + r"\{ext}\UserChoice" +) + + +class _Cache: + supported_exts: set[str] = set() + unsupported_exts: set[str] = set() + + @classmethod + def is_supported(cls, ext: str) -> bool: + return ext in cls.supported_exts + + @classmethod + def already_checked(cls, ext: str) -> bool: + return ( + ext in cls.supported_exts + or ext in cls.unsupported_exts + ) + + @classmethod + def set_ext_support(cls, ext: str, supported: bool) -> None: + if supported: + cls.supported_exts.add(ext) + else: + cls.unsupported_exts.add(ext) + + +def _extension_has_assigned_app_windows(ext: str) -> bool: + import winreg + progid = None + try: + with winreg.OpenKey( + winreg.HKEY_CURRENT_USER, + WINDOWS_USER_REG_PATH.format(ext=ext), + ) as k: + progid, _ = winreg.QueryValueEx(k, "ProgId") + except OSError: + pass + + if progid: + return True + + try: + with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, ext) as k: + progid = winreg.QueryValueEx(k, None)[0] + except OSError: + pass + return bool(progid) + + +def _linux_find_desktop_file(desktop: str) -> Optional[str]: + for p in ( + os.path.join(os.path.expanduser("~/.local/share/applications"), desktop), + os.path.join("/usr/share/applications", desktop), + os.path.join("/usr/local/share/applications", desktop), + ): + if os.path.isfile(p): + return p + return None + + +def _extension_has_assigned_app_linux(ext: str) -> bool: + import mimetypes + + mime, _ = mimetypes.guess_type(f"file{ext}") + if not mime: + return False + + try: + # xdg-mime query default + desktop = subprocess.check_output( + ["xdg-mime", "query", "default", mime], + text=True + ).strip() or None + except Exception: + desktop = None + + if not desktop: + return False + + desktop_path = _linux_find_desktop_file(desktop) + if not desktop_path: + return False + if desktop_path and os.path.isfile(desktop_path): + return True + return False + + +def _extension_has_assigned_app_macos(ext: str): + # Uses CoreServices/LaunchServices and Uniform Type Identifiers via ctypes. + # Steps: ext -> UTI -> default handler bundle id for role 'all'. + cf = ctypes.cdll.LoadLibrary( + "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation" + ) + ls = ctypes.cdll.LoadLibrary( + "/System/Library/Frameworks/CoreServices.framework/Frameworks" + "/LaunchServices.framework/LaunchServices" + ) + + # CFType/CFString helpers + CFStringRef = ctypes.c_void_p + CFURLRef = ctypes.c_void_p + CFAllocatorRef = ctypes.c_void_p + CFIndex = ctypes.c_long + UniChar = ctypes.c_ushort + + kCFStringEncodingUTF8 = 0x08000100 + + cf.CFStringCreateWithCString.argtypes = [CFAllocatorRef, ctypes.c_char_p, ctypes.c_uint32] + cf.CFStringCreateWithCString.restype = CFStringRef + + cf.CFStringGetCStringPtr.argtypes = [CFStringRef, ctypes.c_uint32] + cf.CFStringGetCStringPtr.restype = ctypes.c_char_p + + cf.CFStringGetCString.argtypes = [CFStringRef, ctypes.c_char_p, CFIndex, ctypes.c_uint32] + cf.CFStringGetCString.restype = ctypes.c_bool + + cf.CFRelease.argtypes = [ctypes.c_void_p] + cf.CFRelease.restype = None + + try: + UTTypeCreatePreferredIdentifierForTag = ctypes.cdll.LoadLibrary( + "/System/Library/Frameworks/CoreServices.framework/CoreServices" + ).UTTypeCreatePreferredIdentifierForTag + except OSError: + # Fallback path (older systems) + UTTypeCreatePreferredIdentifierForTag = ( + ls.UTTypeCreatePreferredIdentifierForTag + ) + UTTypeCreatePreferredIdentifierForTag.argtypes = [ + CFStringRef, CFStringRef, CFStringRef + ] + UTTypeCreatePreferredIdentifierForTag.restype = CFStringRef + + LSRolesMask = ctypes.c_uint + kLSRolesAll = 0xFFFFFFFF + ls.LSCopyDefaultRoleHandlerForContentType.argtypes = [ + CFStringRef, LSRolesMask + ] + ls.LSCopyDefaultRoleHandlerForContentType.restype = CFStringRef + + def cfstr(py_s: str) -> CFStringRef: + return cf.CFStringCreateWithCString( + None, py_s.encode("utf-8"), kCFStringEncodingUTF8 + ) + + def to_pystr(cf_s: CFStringRef) -> Optional[str]: + if not cf_s: + return None + # Try fast pointer + ptr = cf.CFStringGetCStringPtr(cf_s, kCFStringEncodingUTF8) + if ptr: + return ctypes.cast(ptr, ctypes.c_char_p).value.decode("utf-8") + + # Fallback buffer + buf_size = 1024 + buf = ctypes.create_string_buffer(buf_size) + ok = cf.CFStringGetCString( + cf_s, buf, buf_size, kCFStringEncodingUTF8 + ) + if ok: + return buf.value.decode("utf-8") + return None + + # Convert extension (without dot) to UTI + tag_class = cfstr("public.filename-extension") + tag_value = cfstr(ext.lstrip(".")) + + uti_ref = UTTypeCreatePreferredIdentifierForTag( + tag_class, tag_value, None + ) + uti = to_pystr(uti_ref) + + # Clean up temporary CFStrings + for ref in (tag_class, tag_value): + if ref: + cf.CFRelease(ref) + + bundle_id = None + if uti_ref: + # Get default handler for the UTI + default_bundle_ref = ls.LSCopyDefaultRoleHandlerForContentType( + uti_ref, kLSRolesAll + ) + bundle_id = to_pystr(default_bundle_ref) + if default_bundle_ref: + cf.CFRelease(default_bundle_ref) + cf.CFRelease(uti_ref) + return bundle_id is not None + + +def _filter_supported_exts( + extensions: set[str], test_func: Callable +) -> set[str]: + filtered_exs: set[str] = set() + for ext in extensions: + if not _Cache.already_checked(ext): + r = test_func(ext) + print(ext, r) + _Cache.set_ext_support(ext, r) + if _Cache.is_supported(ext): + filtered_exs.add(ext) + return filtered_exs + + +def filter_supported_exts(extensions: set[str]) -> set[str]: + if not extensions: + return set() + platform_name = platform.system().lower() + if platform_name == "windows": + return _filter_supported_exts( + extensions, _extension_has_assigned_app_windows + ) + if platform_name == "linux": + return _filter_supported_exts( + extensions, _extension_has_assigned_app_linux + ) + if platform_name == "darwin": + return _filter_supported_exts( + extensions, _extension_has_assigned_app_macos + ) + return set() + + def open_file(filepath: str) -> None: """Open file with system default executable""" if sys.platform.startswith("darwin"): @@ -27,8 +255,6 @@ class OpenFileAction(LoaderActionPlugin): """Open Image Sequence or Video with system default""" identifier = "core.open-file" - product_types = {"render2d"} - def get_action_items( self, selection: LoaderActionSelection ) -> list[LoaderActionItem]: @@ -46,37 +272,32 @@ class OpenFileAction(LoaderActionPlugin): if not repres: return [] - repre_ids = {repre["id"] for repre in repres} - versions = selection.entities.get_representations_versions( - repre_ids - ) - product_ids = {version["productId"] for version in versions} - products = selection.entities.get_products(product_ids) - filtered_product_ids = { - product["id"] - for product in products - if product["productType"] in self.product_types - } - if not filtered_product_ids: + repres_by_ext = collections.defaultdict(list) + for repre in repres: + repre_context = repre.get("context") + if not repre_context: + continue + ext = repre_context.get("ext") + if not ext: + path = repre["attrib"].get("path") + if path: + ext = os.path.splitext(path)[1] + + if ext: + ext = ext.lower() + if not ext.startswith("."): + ext = f".{ext}" + repres_by_ext[ext.lower()].append(repre) + + if not repres_by_ext: return [] - versions_by_product_id = collections.defaultdict(list) - for version in versions: - versions_by_product_id[version["productId"]].append(version) - - repres_by_version_ids = collections.defaultdict(list) - for repre in repres: - repres_by_version_ids[repre["versionId"]].append(repre) - - filtered_repres = [] - for product_id in filtered_product_ids: - for version in versions_by_product_id[product_id]: - for repre in repres_by_version_ids[version["id"]]: - filtered_repres.append(repre) + filtered_exts = filter_supported_exts(set(repres_by_ext)) repre_ids_by_name = collections.defaultdict(set) - for repre in filtered_repres: - repre_ids_by_name[repre["name"]].add(repre["id"]) + for ext in filtered_exts: + for repre in repres_by_ext[ext]: + repre_ids_by_name[repre["name"]].add(repre["id"]) return [ LoaderActionItem( @@ -86,8 +307,8 @@ class OpenFileAction(LoaderActionPlugin): data={"representation_ids": list(repre_ids)}, icon={ "type": "material-symbols", - "name": "play_circle", - "color": "#FFA500", + "name": "file_open", + "color": "#ffffff", } ) for repre_name, repre_ids in repre_ids_by_name.items() @@ -122,6 +343,7 @@ class OpenFileAction(LoaderActionPlugin): ) self.log.info(f"Opening: {path}") + open_file(path) return LoaderActionResult( From 3936270266f67e5e4707a39a3ba845f9eda7d023 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:16:51 +0100 Subject: [PATCH 03/13] fix formatting --- client/ayon_core/plugins/loader/open_file.py | 24 ++++++++++++-------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/plugins/loader/open_file.py b/client/ayon_core/plugins/loader/open_file.py index 80ddf925d3..b29dfd1710 100644 --- a/client/ayon_core/plugins/loader/open_file.py +++ b/client/ayon_core/plugins/loader/open_file.py @@ -68,13 +68,14 @@ def _extension_has_assigned_app_windows(ext: str) -> bool: def _linux_find_desktop_file(desktop: str) -> Optional[str]: - for p in ( - os.path.join(os.path.expanduser("~/.local/share/applications"), desktop), - os.path.join("/usr/share/applications", desktop), - os.path.join("/usr/local/share/applications", desktop), + for dirpath in ( + os.path.expanduser("~/.local/share/applications"), + "/usr/share/applications", + "/usr/local/share/applications", ): - if os.path.isfile(p): - return p + path = os.path.join(dirpath, desktop) + if os.path.isfile(path): + return path return None @@ -106,7 +107,8 @@ def _extension_has_assigned_app_linux(ext: str) -> bool: def _extension_has_assigned_app_macos(ext: str): - # Uses CoreServices/LaunchServices and Uniform Type Identifiers via ctypes. + # Uses CoreServices/LaunchServices and Uniform Type Identifiers via + # ctypes. # Steps: ext -> UTI -> default handler bundle id for role 'all'. cf = ctypes.cdll.LoadLibrary( "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation" @@ -125,13 +127,17 @@ def _extension_has_assigned_app_macos(ext: str): kCFStringEncodingUTF8 = 0x08000100 - cf.CFStringCreateWithCString.argtypes = [CFAllocatorRef, ctypes.c_char_p, ctypes.c_uint32] + cf.CFStringCreateWithCString.argtypes = [ + CFAllocatorRef, ctypes.c_char_p, ctypes.c_uint32 + ] cf.CFStringCreateWithCString.restype = CFStringRef cf.CFStringGetCStringPtr.argtypes = [CFStringRef, ctypes.c_uint32] cf.CFStringGetCStringPtr.restype = ctypes.c_char_p - cf.CFStringGetCString.argtypes = [CFStringRef, ctypes.c_char_p, CFIndex, ctypes.c_uint32] + cf.CFStringGetCString.argtypes = [ + CFStringRef, ctypes.c_char_p, CFIndex, ctypes.c_uint32 + ] cf.CFStringGetCString.restype = ctypes.c_bool cf.CFRelease.argtypes = [ctypes.c_void_p] From 84a40336065b93f057e616ddb7775640770b8687 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:21:14 +0100 Subject: [PATCH 04/13] remove unused variables --- client/ayon_core/plugins/loader/open_file.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/client/ayon_core/plugins/loader/open_file.py b/client/ayon_core/plugins/loader/open_file.py index b29dfd1710..13d255a682 100644 --- a/client/ayon_core/plugins/loader/open_file.py +++ b/client/ayon_core/plugins/loader/open_file.py @@ -106,7 +106,7 @@ def _extension_has_assigned_app_linux(ext: str) -> bool: return False -def _extension_has_assigned_app_macos(ext: str): +def _extension_has_assigned_app_macos(ext: str) -> bool: # Uses CoreServices/LaunchServices and Uniform Type Identifiers via # ctypes. # Steps: ext -> UTI -> default handler bundle id for role 'all'. @@ -120,10 +120,8 @@ def _extension_has_assigned_app_macos(ext: str): # CFType/CFString helpers CFStringRef = ctypes.c_void_p - CFURLRef = ctypes.c_void_p CFAllocatorRef = ctypes.c_void_p CFIndex = ctypes.c_long - UniChar = ctypes.c_ushort kCFStringEncodingUTF8 = 0x08000100 @@ -194,7 +192,6 @@ def _extension_has_assigned_app_macos(ext: str): uti_ref = UTTypeCreatePreferredIdentifierForTag( tag_class, tag_value, None ) - uti = to_pystr(uti_ref) # Clean up temporary CFStrings for ref in (tag_class, tag_value): From 0262a8e7630080a5c4d8e5a64a729febb2dee3c5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 13 Nov 2025 16:33:00 +0100 Subject: [PATCH 05/13] Apply suggestion from @BigRoy --- client/ayon_core/pipeline/farm/pyblish_functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index 5e632c3599..6d116dcece 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -957,10 +957,10 @@ def _create_instances_for_aov( # Display/View are optional display = additional_data.get("display") if display: - additional_data["display"] = display + colorspace_data["display"] = display view = additional_data.get("view") if view: - additional_data["view"] = view + colorspace_data["view"] = view rep["colorspaceData"] = colorspace_data else: From f29470a08ca34d467c1070e9b05ec08b6fcc1f26 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 13 Nov 2025 16:34:08 +0100 Subject: [PATCH 06/13] Apply suggestion from @iLLiCiTiT Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/farm/pyblish_functions.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index 6d116dcece..265d79b53e 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -607,9 +607,10 @@ def create_instances_for_aov( } # Collect color management data if present - if "colorspaceConfig" in instance.data: + colorspace_config = instance.data.get("colorspaceConfig") + if colorspace_config: additional_data.update({ - "colorspaceConfig": instance.data["colorspaceConfig"], + "colorspaceConfig": colorspace_config, # Display/View are optional "display": instance.data.get("colorspaceDisplay"), "view": instance.data.get("colorspaceView") @@ -617,13 +618,12 @@ def create_instances_for_aov( # Get templated path from absolute config path. anatomy = instance.context.data["anatomy"] - colorspace_template = instance.data["colorspaceConfig"] try: additional_data["colorspaceTemplate"] = remap_source( - colorspace_template, anatomy) + colorspace_config, anatomy) except ValueError as e: log.warning(e) - additional_data["colorspaceTemplate"] = colorspace_template + additional_data["colorspaceTemplate"] = colorspace_config # create instances for every AOV we found in expected files. # NOTE: this is done for every AOV and every render camera (if From bab249a54a4f50e018d4f403abf5b6f9e04b2b4a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 13 Nov 2025 17:11:02 +0100 Subject: [PATCH 07/13] remove debug print Co-authored-by: Roy Nieterau --- client/ayon_core/plugins/loader/open_file.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/plugins/loader/open_file.py b/client/ayon_core/plugins/loader/open_file.py index 13d255a682..3118bfa3db 100644 --- a/client/ayon_core/plugins/loader/open_file.py +++ b/client/ayon_core/plugins/loader/open_file.py @@ -218,7 +218,6 @@ def _filter_supported_exts( for ext in extensions: if not _Cache.already_checked(ext): r = test_func(ext) - print(ext, r) _Cache.set_ext_support(ext, r) if _Cache.is_supported(ext): filtered_exs.add(ext) From 46b534cfcce245dd0a7231e86efdd9e2685629eb Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 13 Nov 2025 17:11:38 +0100 Subject: [PATCH 08/13] merge two lines into one --- client/ayon_core/plugins/loader/open_file.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/loader/open_file.py b/client/ayon_core/plugins/loader/open_file.py index 3118bfa3db..8bc4913da5 100644 --- a/client/ayon_core/plugins/loader/open_file.py +++ b/client/ayon_core/plugins/loader/open_file.py @@ -217,8 +217,7 @@ def _filter_supported_exts( filtered_exs: set[str] = set() for ext in extensions: if not _Cache.already_checked(ext): - r = test_func(ext) - _Cache.set_ext_support(ext, r) + _Cache.set_ext_support(ext, test_func(ext)) if _Cache.is_supported(ext): filtered_exs.add(ext) return filtered_exs From efa702405c75d016a46f692e8f678598adf9c91c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 13 Nov 2025 17:26:55 +0100 Subject: [PATCH 09/13] tune out orders --- client/ayon_core/plugins/loader/copy_file.py | 2 ++ client/ayon_core/plugins/loader/delete_old_versions.py | 2 +- client/ayon_core/plugins/loader/open_file.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/loader/copy_file.py b/client/ayon_core/plugins/loader/copy_file.py index 2380b465ed..a1a98a2bf0 100644 --- a/client/ayon_core/plugins/loader/copy_file.py +++ b/client/ayon_core/plugins/loader/copy_file.py @@ -45,6 +45,7 @@ class CopyFileActionPlugin(LoaderActionPlugin): output.append( LoaderActionItem( label=repre_name, + order=32, group_label="Copy file path", data={ "representation_id": repre_id, @@ -60,6 +61,7 @@ class CopyFileActionPlugin(LoaderActionPlugin): output.append( LoaderActionItem( label=repre_name, + order=33, group_label="Copy file", data={ "representation_id": repre_id, diff --git a/client/ayon_core/plugins/loader/delete_old_versions.py b/client/ayon_core/plugins/loader/delete_old_versions.py index 7499650cbe..ce67df1c0c 100644 --- a/client/ayon_core/plugins/loader/delete_old_versions.py +++ b/client/ayon_core/plugins/loader/delete_old_versions.py @@ -66,7 +66,7 @@ class DeleteOldVersions(LoaderActionPlugin): ), LoaderActionItem( label="Calculate Versions size", - order=30, + order=34, data={ "product_ids": list(product_ids), "action": "calculate-versions-size", diff --git a/client/ayon_core/plugins/loader/open_file.py b/client/ayon_core/plugins/loader/open_file.py index 8bc4913da5..ef92990f57 100644 --- a/client/ayon_core/plugins/loader/open_file.py +++ b/client/ayon_core/plugins/loader/open_file.py @@ -304,7 +304,7 @@ class OpenFileAction(LoaderActionPlugin): LoaderActionItem( label=repre_name, group_label="Open file", - order=-10, + order=30, data={"representation_ids": list(repre_ids)}, icon={ "type": "material-symbols", From 42b249a6b3732bafa3557abc5462857fe03e855e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 13 Nov 2025 17:32:22 +0100 Subject: [PATCH 10/13] add note about caching --- client/ayon_core/plugins/loader/open_file.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/ayon_core/plugins/loader/open_file.py b/client/ayon_core/plugins/loader/open_file.py index ef92990f57..018b9aeab0 100644 --- a/client/ayon_core/plugins/loader/open_file.py +++ b/client/ayon_core/plugins/loader/open_file.py @@ -22,6 +22,13 @@ WINDOWS_USER_REG_PATH = ( class _Cache: + """Cache extensions information. + + Notes: + The cache is cleared when loader tool is refreshed so it might be + moved to other place which is not cleared of refresh. + + """ supported_exts: set[str] = set() unsupported_exts: set[str] = set() From 8478899b67c7c3aeec4b62ee179ebaaba87bcc0a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 13 Nov 2025 17:40:47 +0100 Subject: [PATCH 11/13] Apply suggestion from @BigRoy --- client/ayon_core/plugins/loader/open_file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/loader/open_file.py b/client/ayon_core/plugins/loader/open_file.py index 018b9aeab0..d226786bc2 100644 --- a/client/ayon_core/plugins/loader/open_file.py +++ b/client/ayon_core/plugins/loader/open_file.py @@ -26,7 +26,7 @@ class _Cache: Notes: The cache is cleared when loader tool is refreshed so it might be - moved to other place which is not cleared of refresh. + moved to other place which is not cleared on refresh. """ supported_exts: set[str] = set() From 1c25e357776f0a3a04686ffc96439f6b567635e4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 17 Nov 2025 12:18:07 +0100 Subject: [PATCH 12/13] Fix Context card being clickable in Nuke 14/15 only outside the Context label area. Previously you could only click on the far left or far right side of the context card to be able to select it and access the Context attributes. Cosmetically the removal of the `` doesn't do much to the Context card because it doesn't have a sublabel. --- client/ayon_core/tools/publisher/widgets/card_view_widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index ca95b1ff1a..aef3f85e0c 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -211,7 +211,7 @@ class ContextCardWidget(CardWidget): icon_widget = PublishPixmapLabel(None, self) icon_widget.setObjectName("ProductTypeIconLabel") - label_widget = QtWidgets.QLabel(f"{CONTEXT_LABEL}", self) + label_widget = QtWidgets.QLabel(CONTEXT_LABEL, self) icon_layout = QtWidgets.QHBoxLayout() icon_layout.setContentsMargins(5, 5, 5, 5) From 82128c30c5d8a1e12939ffd3db09002f3c87b9d6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 17 Nov 2025 14:55:23 +0100 Subject: [PATCH 13/13] Disable text interaction instead --- .../ayon_core/tools/publisher/widgets/card_view_widgets.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index aef3f85e0c..a9abd56584 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -211,7 +211,12 @@ class ContextCardWidget(CardWidget): icon_widget = PublishPixmapLabel(None, self) icon_widget.setObjectName("ProductTypeIconLabel") - label_widget = QtWidgets.QLabel(CONTEXT_LABEL, self) + label_widget = QtWidgets.QLabel(f"{CONTEXT_LABEL}", self) + # HTML text will cause that label start catch mouse clicks + # - disabling with changing interaction flag + label_widget.setTextInteractionFlags( + QtCore.Qt.NoTextInteraction + ) icon_layout = QtWidgets.QHBoxLayout() icon_layout.setContentsMargins(5, 5, 5, 5)