From 0a207ad032273365f41b9032631d9eb26dfc0d5e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Jun 2024 20:19:08 +0200 Subject: [PATCH 001/155] Add simple Create Project Structure launcher action --- .../create_project_folder_structure.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 client/ayon_core/plugins/actions/create_project_folder_structure.py diff --git a/client/ayon_core/plugins/actions/create_project_folder_structure.py b/client/ayon_core/plugins/actions/create_project_folder_structure.py new file mode 100644 index 0000000000..6df52abbcb --- /dev/null +++ b/client/ayon_core/plugins/actions/create_project_folder_structure.py @@ -0,0 +1,20 @@ +from ayon_core.pipeline import LauncherAction +from ayon_core.pipeline import project_folders + + +class CreateProjectStructureAction(LauncherAction): + """Create project structure as defined in settings.""" + name = "create_project_structure" + label = "Create Project Structure" + icon = "sitemap" + color = "#e0e1e1" + order = 1000 + + def is_compatible(self, selection) -> bool: + return ( + selection.is_project_selected and + not selection.is_folder_selected + ) + + def process(self, selection, **kwargs): + project_folders.create_project_folders(selection.project_name) \ No newline at end of file From 905241f6f4c0674c173b8070579431802b8f1b88 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Jun 2024 20:25:24 +0200 Subject: [PATCH 002/155] Cosmetics --- .../plugins/actions/create_project_folder_structure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/actions/create_project_folder_structure.py b/client/ayon_core/plugins/actions/create_project_folder_structure.py index 6df52abbcb..e555f08301 100644 --- a/client/ayon_core/plugins/actions/create_project_folder_structure.py +++ b/client/ayon_core/plugins/actions/create_project_folder_structure.py @@ -17,4 +17,4 @@ class CreateProjectStructureAction(LauncherAction): ) def process(self, selection, **kwargs): - project_folders.create_project_folders(selection.project_name) \ No newline at end of file + project_folders.create_project_folders(selection.project_name) From fbd27357729146efed795db1302cc97897807621 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Jun 2024 20:26:47 +0200 Subject: [PATCH 003/155] Cosmetics --- .../plugins/actions/create_project_folder_structure.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/actions/create_project_folder_structure.py b/client/ayon_core/plugins/actions/create_project_folder_structure.py index e555f08301..95c46099d7 100644 --- a/client/ayon_core/plugins/actions/create_project_folder_structure.py +++ b/client/ayon_core/plugins/actions/create_project_folder_structure.py @@ -1,5 +1,4 @@ -from ayon_core.pipeline import LauncherAction -from ayon_core.pipeline import project_folders +from ayon_core.pipeline import LauncherAction, project_folders class CreateProjectStructureAction(LauncherAction): From 18f750f39a893d93fe66c5447c7c033c531e8baa Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 21 Jun 2024 09:38:57 +0200 Subject: [PATCH 004/155] Update client/ayon_core/plugins/actions/create_project_folder_structure.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../plugins/actions/create_project_folder_structure.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/actions/create_project_folder_structure.py b/client/ayon_core/plugins/actions/create_project_folder_structure.py index 95c46099d7..c12db9b353 100644 --- a/client/ayon_core/plugins/actions/create_project_folder_structure.py +++ b/client/ayon_core/plugins/actions/create_project_folder_structure.py @@ -11,8 +11,8 @@ class CreateProjectStructureAction(LauncherAction): def is_compatible(self, selection) -> bool: return ( - selection.is_project_selected and - not selection.is_folder_selected + selection.is_project_selected + and not selection.is_folder_selected ) def process(self, selection, **kwargs): From 56fb5c4c021777664b64922b153499ee7cf8ee26 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 4 Oct 2024 11:08:56 +0200 Subject: [PATCH 005/155] Rename action to Create Project Folders --- ...ject_folder_structure.py => create_project_folders.py} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename client/ayon_core/plugins/actions/{create_project_folder_structure.py => create_project_folders.py} (68%) diff --git a/client/ayon_core/plugins/actions/create_project_folder_structure.py b/client/ayon_core/plugins/actions/create_project_folders.py similarity index 68% rename from client/ayon_core/plugins/actions/create_project_folder_structure.py rename to client/ayon_core/plugins/actions/create_project_folders.py index c12db9b353..2dd26b578a 100644 --- a/client/ayon_core/plugins/actions/create_project_folder_structure.py +++ b/client/ayon_core/plugins/actions/create_project_folders.py @@ -1,10 +1,10 @@ from ayon_core.pipeline import LauncherAction, project_folders -class CreateProjectStructureAction(LauncherAction): - """Create project structure as defined in settings.""" - name = "create_project_structure" - label = "Create Project Structure" +class CreateProjectFoldersAction(LauncherAction): + """Create project folders as defined in settings.""" + name = "create_project_folders" + label = "Create Project Folders" icon = "sitemap" color = "#e0e1e1" order = 1000 From 8383fdefa40814aa40fe3ca1ef311f7261f8cebb Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 4 Oct 2024 11:12:03 +0200 Subject: [PATCH 006/155] Hide action if setting is empty --- .../plugins/actions/create_project_folders.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/client/ayon_core/plugins/actions/create_project_folders.py b/client/ayon_core/plugins/actions/create_project_folders.py index 2dd26b578a..934a9169d7 100644 --- a/client/ayon_core/plugins/actions/create_project_folders.py +++ b/client/ayon_core/plugins/actions/create_project_folders.py @@ -10,6 +10,16 @@ class CreateProjectFoldersAction(LauncherAction): order = 1000 def is_compatible(self, selection) -> bool: + + # Disable when the project folder structure setting is empty + # in settings + project_settings = selection.get_project_settings() + folder_structure = ( + project_settings["core"]["project_folder_structure"] + ).strip() + if not folder_structure or folder_structure == "{}": + return False + return ( selection.is_project_selected and not selection.is_folder_selected From 5aa7e9c8caa7a400f0b01db3a12a8e9e49fbed64 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 4 Oct 2024 11:17:33 +0200 Subject: [PATCH 007/155] Add description to setting --- server/settings/main.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/settings/main.py b/server/settings/main.py index 249bab85fd..5eaacc7072 100644 --- a/server/settings/main.py +++ b/server/settings/main.py @@ -301,6 +301,11 @@ class CoreSettings(BaseSettingsModel): "{}", widget="textarea", title="Project folder structure", + description=( + "Defines project folders to create on 'Create project folders'." + " When empty, the action will be hidden from users in the" + " launcher because there is nothing to create." + ), section="---" ) project_environments: str = SettingsField( From 5065e0e8dbb4da9e4a05918abced984b2f077e5d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 4 Oct 2024 12:04:14 +0200 Subject: [PATCH 008/155] Explicit description to describe diff between ftrack and launcher --- server/settings/main.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/server/settings/main.py b/server/settings/main.py index 5eaacc7072..a5129e69af 100644 --- a/server/settings/main.py +++ b/server/settings/main.py @@ -303,8 +303,12 @@ class CoreSettings(BaseSettingsModel): title="Project folder structure", description=( "Defines project folders to create on 'Create project folders'." - " When empty, the action will be hidden from users in the" - " launcher because there is nothing to create." + "\n\n" + "- In the launcher, this only creates the folders on disk and " + " when the setting is empty it will be hidden from users in the" + " launcher.\n" + "- In `ayon-ftrack` this will create the folders on disk **and**" + " will also create ftrack entities. It is never hidden there." ), section="---" ) From d5b2642e8d3f8b3bc6ba48b7dd3ab994efc2c32f Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 13 Mar 2025 14:55:40 +0200 Subject: [PATCH 009/155] implement `createprojectstructure` cli action. --- client/ayon_core/cli.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index d7cd3ba7f5..21354bec4a 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -22,7 +22,7 @@ from ayon_core.lib.env_tools import ( compute_env_variables_structure, merge_env_variables, ) - +from ayon_core.pipeline import project_folders @click.group(invoke_without_command=True) @@ -258,6 +258,28 @@ def _set_global_environments() -> None: os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" +@main_cli.command() +@click.option( + "--project", + type=str, + help="Define project (project must be set).", + required=True) +def createprojectstructure( + project, +): + """Create project folder structure as defined in setting + `ayon+settings://core/project_folder_structure` + + Args: + project (str): The name of the project for which you + want to create its additional folder structure. + + """ + + print(f">>> Creating project folder structure for project '{project}'.") + project_folders.create_project_folders(project) + + def _set_addons_environments(addons_manager): """Set global environments for AYON addons.""" From 4f6ea8709226fd8a3ddfb12bc2aeedb1ae45b100 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 13 Mar 2025 14:59:10 +0200 Subject: [PATCH 010/155] Implement `Create Project Folder Structure` web action --- server/__init__.py | 59 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/server/__init__.py b/server/__init__.py index d60f50f471..1ae5284f4d 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -1,6 +1,11 @@ from typing import Any from ayon_server.addons import BaseServerAddon +from ayon_server.actions import ( + ActionExecutor, + ExecuteResponseModel, + SimpleActionManifest, +) from .settings import ( CoreSettings, @@ -9,6 +14,9 @@ from .settings import ( ) +IDENTIFIER_PREFIX = "core.launch" + + class CoreAddon(BaseServerAddon): settings_model = CoreSettings @@ -26,3 +34,54 @@ class CoreAddon(BaseServerAddon): return await super().convert_settings_overrides( source_version, overrides ) + + async def get_simple_actions( + self, + project_name: str | None = None, + variant: str = "production", + ) -> list[SimpleActionManifest]: + """Return a list of simple actions provided by the addon""" + output = [] + + # Add 'Create Project Folder Structure' action to folders. + output.append( + SimpleActionManifest( + identifier=f"{IDENTIFIER_PREFIX}.createprojectstructure", + label="Create Project Folder Structure", + icon={ + "type": "material-symbols", + "name": "create_new_folder", + }, + order=100, + entity_type="folder", + entity_subtypes=None, + allow_multiselection=False, + ) + ) + + return output + + async def execute_action( + self, + executor: "ActionExecutor", + ) -> "ExecuteResponseModel": + """Execute actions. + + Note: + Executes CLI actions defined in the + addon's client code or other addons. + + """ + + project_name = executor.context.project_name + + if executor.identifier == \ + f"{IDENTIFIER_PREFIX}.createprojectstructure": + return await executor.get_launcher_action_response( + args=[ + "createprojectstructure", + "--project", project_name, + ] + ) + + raise ValueError(f"Unknown action: {executor.identifier}") From 6c650748637691c999f95ba4b2b53637f7c2770c Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 13 Mar 2025 15:00:10 +0200 Subject: [PATCH 011/155] remove `CreateProjectFoldersAction` launcher action. --- .../plugins/actions/create_project_folders.py | 29 ------------------- 1 file changed, 29 deletions(-) delete mode 100644 client/ayon_core/plugins/actions/create_project_folders.py diff --git a/client/ayon_core/plugins/actions/create_project_folders.py b/client/ayon_core/plugins/actions/create_project_folders.py deleted file mode 100644 index 934a9169d7..0000000000 --- a/client/ayon_core/plugins/actions/create_project_folders.py +++ /dev/null @@ -1,29 +0,0 @@ -from ayon_core.pipeline import LauncherAction, project_folders - - -class CreateProjectFoldersAction(LauncherAction): - """Create project folders as defined in settings.""" - name = "create_project_folders" - label = "Create Project Folders" - icon = "sitemap" - color = "#e0e1e1" - order = 1000 - - def is_compatible(self, selection) -> bool: - - # Disable when the project folder structure setting is empty - # in settings - project_settings = selection.get_project_settings() - folder_structure = ( - project_settings["core"]["project_folder_structure"] - ).strip() - if not folder_structure or folder_structure == "{}": - return False - - return ( - selection.is_project_selected - and not selection.is_folder_selected - ) - - def process(self, selection, **kwargs): - project_folders.create_project_folders(selection.project_name) From 8b501187a46218cce92f4bb0a55f778188a2b2da Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 19 Mar 2025 12:07:30 +0200 Subject: [PATCH 012/155] move the function above private functions --- client/ayon_core/cli.py | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index ad108f59fc..eef0b80d54 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -237,26 +237,6 @@ def version(build): print(os.environ["AYON_VERSION"]) -def _set_global_environments() -> None: - """Set global AYON environments.""" - # First resolve general environment - general_env = parse_env_variables_structure(get_general_environments()) - - # Merge environments with current environments and update values - merged_env = merge_env_variables( - compute_env_variables_structure(general_env), - dict(os.environ) - ) - env = compute_env_variables_structure(merged_env) - os.environ.clear() - os.environ.update(env) - - # Hardcoded default values - # Change scale factor only if is not set - if "QT_AUTO_SCREEN_SCALE_FACTOR" not in os.environ: - os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" - - @main_cli.command() @click.option( "--project", @@ -279,6 +259,26 @@ def createprojectstructure( project_folders.create_project_folders(project) +def _set_global_environments() -> None: + """Set global AYON environments.""" + # First resolve general environment + general_env = parse_env_variables_structure(get_general_environments()) + + # Merge environments with current environments and update values + merged_env = merge_env_variables( + compute_env_variables_structure(general_env), + dict(os.environ) + ) + env = compute_env_variables_structure(merged_env) + os.environ.clear() + os.environ.update(env) + + # Hardcoded default values + # Change scale factor only if is not set + if "QT_AUTO_SCREEN_SCALE_FACTOR" not in os.environ: + os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" + + def _set_addons_environments(addons_manager): """Set global environments for AYON addons.""" From 7cf02d97ae58340b26289e5af36da77524d6c634 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 19 Mar 2025 12:10:11 +0200 Subject: [PATCH 013/155] update help of project argument in create project structure. --- client/ayon_core/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index eef0b80d54..aaec9efba0 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -241,7 +241,7 @@ def version(build): @click.option( "--project", type=str, - help="Define project (project must be set).", + help="Project name", required=True) def createprojectstructure( project, From 71819f1ed6dbade2817ec6184f812d3809dd3100 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 19 Mar 2025 12:10:57 +0200 Subject: [PATCH 014/155] refactor dunction name to `create_project_structure` --- client/ayon_core/cli.py | 2 +- server/__init__.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index aaec9efba0..84e5428938 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -243,7 +243,7 @@ def version(build): type=str, help="Project name", required=True) -def createprojectstructure( +def create_project_structure( project, ): """Create project folder structure as defined in setting diff --git a/server/__init__.py b/server/__init__.py index 1ae5284f4d..2521061822 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -46,7 +46,7 @@ class CoreAddon(BaseServerAddon): # Add 'Create Project Folder Structure' action to folders. output.append( SimpleActionManifest( - identifier=f"{IDENTIFIER_PREFIX}.createprojectstructure", + identifier=f"{IDENTIFIER_PREFIX}.create_project_structure", label="Create Project Folder Structure", icon={ "type": "material-symbols", @@ -76,10 +76,10 @@ class CoreAddon(BaseServerAddon): project_name = executor.context.project_name if executor.identifier == \ - f"{IDENTIFIER_PREFIX}.createprojectstructure": + f"{IDENTIFIER_PREFIX}.create_project_structure": return await executor.get_launcher_action_response( args=[ - "createprojectstructure", + "create_project_structure", "--project", project_name, ] ) From 3636f5d9ccf10d410a6621fa7f349124e26d653f Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 19 Mar 2025 12:12:15 +0200 Subject: [PATCH 015/155] fix command argument --- server/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/__init__.py b/server/__init__.py index 2521061822..904d006e00 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -79,7 +79,7 @@ class CoreAddon(BaseServerAddon): f"{IDENTIFIER_PREFIX}.create_project_structure": return await executor.get_launcher_action_response( args=[ - "create_project_structure", + "create-project-structure", "--project", project_name, ] ) From 1331167e1390c696933a8d4d71ebf05eb2ecbd49 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 19 Mar 2025 12:13:00 +0200 Subject: [PATCH 016/155] update identifier prefix --- server/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/__init__.py b/server/__init__.py index 904d006e00..7a1d181b1b 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -14,7 +14,7 @@ from .settings import ( ) -IDENTIFIER_PREFIX = "core.launch" +IDENTIFIER_PREFIX = "core" class CoreAddon(BaseServerAddon): From 45109825af75492f4c693234cf96f162af1bfcf0 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 19 Mar 2025 12:14:56 +0200 Subject: [PATCH 017/155] update function type hints and docstring --- server/__init__.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/server/__init__.py b/server/__init__.py index 7a1d181b1b..1b9e7717c9 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -63,15 +63,9 @@ class CoreAddon(BaseServerAddon): async def execute_action( self, - executor: "ActionExecutor", - ) -> "ExecuteResponseModel": - """Execute actions. - - Note: - Executes CLI actions defined in the - addon's client code or other addons. - - """ + executor: ActionExecutor, + ) -> ExecuteResponseModel: + """Execute webactions.""" project_name = executor.context.project_name From ff7617850de783a5e2a61a6250fdd4657e5477d3 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 19 Mar 2025 12:22:27 +0200 Subject: [PATCH 018/155] only import the needed function --- client/ayon_core/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index 84e5428938..186e11dfb5 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -22,7 +22,7 @@ from ayon_core.lib.env_tools import ( compute_env_variables_structure, merge_env_variables, ) -from ayon_core.pipeline import project_folders +from ayon_core.pipeline.project_folders import create_project_folders @click.group(invoke_without_command=True) @@ -256,7 +256,7 @@ def create_project_structure( """ print(f">>> Creating project folder structure for project '{project}'.") - project_folders.create_project_folders(project) + create_project_folders(project) def _set_global_environments() -> None: From 207f6d8a93387a5ab4c95d053a315b067ccffb65 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 19 Mar 2025 12:24:07 +0200 Subject: [PATCH 019/155] move import to the function. --- client/ayon_core/cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index 186e11dfb5..dc794c41c2 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -22,7 +22,6 @@ from ayon_core.lib.env_tools import ( compute_env_variables_structure, merge_env_variables, ) -from ayon_core.pipeline.project_folders import create_project_folders @click.group(invoke_without_command=True) @@ -255,6 +254,8 @@ def create_project_structure( """ + from ayon_core.pipeline.project_folders import create_project_folders + print(f">>> Creating project folder structure for project '{project}'.") create_project_folders(project) From 0ec82bd34d83542949ab27ae209c012082714916 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 7 Apr 2025 18:48:12 +0200 Subject: [PATCH 020/155] check project name when adding or executing `create_project_structure` --- server/__init__.py | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/server/__init__.py b/server/__init__.py index 1b9e7717c9..1de7412d18 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -43,21 +43,22 @@ class CoreAddon(BaseServerAddon): """Return a list of simple actions provided by the addon""" output = [] - # Add 'Create Project Folder Structure' action to folders. - output.append( - SimpleActionManifest( - identifier=f"{IDENTIFIER_PREFIX}.create_project_structure", - label="Create Project Folder Structure", - icon={ - "type": "material-symbols", - "name": "create_new_folder", - }, - order=100, - entity_type="folder", - entity_subtypes=None, - allow_multiselection=False, + if project_name: + # Add 'Create Project Folder Structure' action to folders. + output.append( + SimpleActionManifest( + identifier=f"{IDENTIFIER_PREFIX}.createprojectstructure", + label="Create Project Folder Structure", + icon={ + "type": "material-symbols", + "name": "create_new_folder", + }, + order=100, + entity_type="folder", + entity_subtypes=None, + allow_multiselection=False, + ) ) - ) return output @@ -71,6 +72,14 @@ class CoreAddon(BaseServerAddon): if executor.identifier == \ f"{IDENTIFIER_PREFIX}.create_project_structure": + + if not project_name: + raise ValueError( + f"Can't execute {executor.identifier} because" + " of missing project name." + ) + return + return await executor.get_launcher_action_response( args=[ "create-project-structure", From 7a7f4b44a04af44d9d3fc8debaaefa52f297c81c Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 7 Apr 2025 18:51:02 +0200 Subject: [PATCH 021/155] use logger from backend with fallback to nxtools --- pyproject.toml | 1 + server/__init__.py | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 94badd2f1a..6b8997d66c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ attrs = "^25.0.0" pyblish-base = "^1.8.7" clique = "^2.0.0" opentimelineio = "^0.17.0" +nxtools = "^1.6" [tool.ruff] diff --git a/server/__init__.py b/server/__init__.py index 1de7412d18..aafd8d6fa5 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -6,6 +6,10 @@ from ayon_server.actions import ( ExecuteResponseModel, SimpleActionManifest, ) +try: + from ayon_server.logging import logger +except ImportError: + from nxtools import logging as logger from .settings import ( CoreSettings, @@ -74,7 +78,7 @@ class CoreAddon(BaseServerAddon): f"{IDENTIFIER_PREFIX}.create_project_structure": if not project_name: - raise ValueError( + logger.error( f"Can't execute {executor.identifier} because" " of missing project name." ) @@ -87,4 +91,4 @@ class CoreAddon(BaseServerAddon): ] ) - raise ValueError(f"Unknown action: {executor.identifier}") + logger.debug(f"Unknown action: {executor.identifier}") From 01069f62ac38d4f7b0668fbd21a2ee7ecdd3a60f Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 7 Apr 2025 19:24:59 +0200 Subject: [PATCH 022/155] update poetry lock --- poetry.lock | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index 96e1dc0f4c..235bb6770d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. [[package]] name = "appdirs" @@ -887,6 +887,22 @@ files = [ {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, ] +[[package]] +name = "nxtools" +version = "1.6" +description = "nxtools is a set of various tools and helpers used by Nebula broadcast automation system and other software by imm studios, z.s." +optional = false +python-versions = ">=3.6,<4.0" +groups = ["dev"] +files = [ + {file = "nxtools-1.6-py3-none-any.whl", hash = "sha256:cde2d2c19193a2bd9c8071eeb3f121474ec34ab5c014de9a90f0c434cdfee716"}, + {file = "nxtools-1.6.tar.gz", hash = "sha256:e91839eef643483a3165165a46a693ad976d40a393ee5476a341e4e8bb102b92"}, +] + +[package.dependencies] +colorama = ">=0.4.4,<0.5.0" +Unidecode = ">=1.2.0,<2.0.0" + [[package]] name = "opentimelineio" version = "0.17.0" @@ -1512,4 +1528,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.9.1,<3.10" -content-hash = "24b6215b9c20a4f64f844d3deb121618aef510b1c5ee54242e50305db6c0c4f4" +content-hash = "c0d98dae3faeed5237f286362f9d280ab17c3d42f7cd41fdd69ee4546983c553" From 0fdff60147f61b0a31a94b9a7f19ce022f6e8380 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Tue, 15 Apr 2025 19:08:14 +0200 Subject: [PATCH 023/155] remove the poetry.lock - again --- poetry.lock | 1531 --------------------------------------------------- 1 file changed, 1531 deletions(-) delete mode 100644 poetry.lock diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 235bb6770d..0000000000 --- a/poetry.lock +++ /dev/null @@ -1,1531 +0,0 @@ -# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. - -[[package]] -name = "appdirs" -version = "1.4.4" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, - {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, -] - -[[package]] -name = "attrs" -version = "25.1.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a"}, - {file = "attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e"}, -] - -[package.extras] -benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] - -[[package]] -name = "ayon-python-api" -version = "1.0.12" -description = "AYON Python API" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "ayon-python-api-1.0.12.tar.gz", hash = "sha256:8e4c03436df8afdda4c6ad4efce436068771995bb0153a90e003364afa0e7f55"}, - {file = "ayon_python_api-1.0.12-py3-none-any.whl", hash = "sha256:65f61c2595dd6deb26fed5e3fda7baef887f475fa4b21df12513646ddccf4a7d"}, -] - -[package.dependencies] -appdirs = ">=1,<2" -requests = ">=2.27.1" -Unidecode = ">=1.3.0" - -[[package]] -name = "babel" -version = "2.17.0" -description = "Internationalization utilities" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, - {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, -] - -[package.extras] -dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] - -[[package]] -name = "backrefs" -version = "5.8" -description = "A wrapper around re and regex that adds additional back references." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "backrefs-5.8-py310-none-any.whl", hash = "sha256:c67f6638a34a5b8730812f5101376f9d41dc38c43f1fdc35cb54700f6ed4465d"}, - {file = "backrefs-5.8-py311-none-any.whl", hash = "sha256:2e1c15e4af0e12e45c8701bd5da0902d326b2e200cafcd25e49d9f06d44bb61b"}, - {file = "backrefs-5.8-py312-none-any.whl", hash = "sha256:bbef7169a33811080d67cdf1538c8289f76f0942ff971222a16034da88a73486"}, - {file = "backrefs-5.8-py313-none-any.whl", hash = "sha256:e3a63b073867dbefd0536425f43db618578528e3896fb77be7141328642a1585"}, - {file = "backrefs-5.8-py39-none-any.whl", hash = "sha256:a66851e4533fb5b371aa0628e1fee1af05135616b86140c9d787a2ffdf4b8fdc"}, - {file = "backrefs-5.8.tar.gz", hash = "sha256:2cab642a205ce966af3dd4b38ee36009b31fa9502a35fd61d59ccc116e40a6bd"}, -] - -[package.extras] -extras = ["regex"] - -[[package]] -name = "certifi" -version = "2025.1.31" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, - {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, -] - -[[package]] -name = "cfgv" -version = "3.4.0" -description = "Validate configuration and produce human readable error messages." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, - {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.1" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, - {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, - {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, -] - -[[package]] -name = "click" -version = "8.1.8" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, - {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "clique" -version = "2.0.0" -description = "Manage collections with common numerical component" -optional = false -python-versions = ">=3.0, <4.0" -groups = ["dev"] -files = [ - {file = "clique-2.0.0-py2.py3-none-any.whl", hash = "sha256:45e2a4c6078382e0b217e5e369494279cf03846d95ee601f93290bed5214c22e"}, - {file = "clique-2.0.0.tar.gz", hash = "sha256:6e1115dbf21b1726f4b3db9e9567a662d6bdf72487c4a0a1f8cb7f10cf4f4754"}, -] - -[package.extras] -dev = ["lowdown (>=0.2.0,<1)", "pytest (>=2.3.5,<5)", "pytest-cov (>=2,<3)", "pytest-runner (>=2.7,<3)", "sphinx (>=2,<4)", "sphinx-rtd-theme (>=0.1.6,<1)"] -doc = ["lowdown (>=0.2.0,<1)", "sphinx (>=2,<4)", "sphinx-rtd-theme (>=0.1.6,<1)"] -test = ["pytest (>=2.3.5,<5)", "pytest-cov (>=2,<3)", "pytest-runner (>=2.7,<3)"] - -[[package]] -name = "codespell" -version = "2.4.1" -description = "Fix common misspellings in text files" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "codespell-2.4.1-py3-none-any.whl", hash = "sha256:3dadafa67df7e4a3dbf51e0d7315061b80d265f9552ebd699b3dd6834b47e425"}, - {file = "codespell-2.4.1.tar.gz", hash = "sha256:299fcdcb09d23e81e35a671bbe746d5ad7e8385972e65dbb833a2eaac33c01e5"}, -] - -[package.extras] -dev = ["Pygments", "build", "chardet", "pre-commit", "pytest", "pytest-cov", "pytest-dependency", "ruff", "tomli", "twine"] -hard-encoding-detection = ["chardet"] -toml = ["tomli ; python_version < \"3.11\""] -types = ["chardet (>=5.1.0)", "mypy", "pytest", "pytest-cov", "pytest-dependency"] - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["dev"] -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "csscompressor" -version = "0.9.5" -description = "A python port of YUI CSS Compressor" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "csscompressor-0.9.5.tar.gz", hash = "sha256:afa22badbcf3120a4f392e4d22f9fff485c044a1feda4a950ecc5eba9dd31a05"}, -] - -[[package]] -name = "distlib" -version = "0.3.9" -description = "Distribution utilities" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, - {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, -] - -[[package]] -name = "exceptiongroup" -version = "1.2.2" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, -] - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "filelock" -version = "3.17.0" -description = "A platform independent file lock." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338"}, - {file = "filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e"}, -] - -[package.extras] -docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] -typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] - -[[package]] -name = "ghp-import" -version = "2.1.0" -description = "Copy your docs directly to the gh-pages branch." -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, - {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, -] - -[package.dependencies] -python-dateutil = ">=2.8.1" - -[package.extras] -dev = ["flake8", "markdown", "twine", "wheel"] - -[[package]] -name = "griffe" -version = "1.6.2" -description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "griffe-1.6.2-py3-none-any.whl", hash = "sha256:6399f7e663150e4278a312a8e8a14d2f3d7bd86e2ef2f8056a1058e38579c2ee"}, - {file = "griffe-1.6.2.tar.gz", hash = "sha256:3a46fa7bd83280909b63c12b9a975732a927dd97809efe5b7972290b606c5d91"}, -] - -[package.dependencies] -colorama = ">=0.4" - -[[package]] -name = "htmlmin2" -version = "0.1.13" -description = "An HTML Minifier" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "htmlmin2-0.1.13-py3-none-any.whl", hash = "sha256:75609f2a42e64f7ce57dbff28a39890363bde9e7e5885db633317efbdf8c79a2"}, -] - -[[package]] -name = "identify" -version = "2.6.7" -description = "File identification library for Python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "identify-2.6.7-py2.py3-none-any.whl", hash = "sha256:155931cb617a401807b09ecec6635d6c692d180090a1cedca8ef7d58ba5b6aa0"}, - {file = "identify-2.6.7.tar.gz", hash = "sha256:3fa266b42eba321ee0b2bb0936a6a6b9e36a1351cbb69055b3082f4193035684"}, -] - -[package.extras] -license = ["ukkonen"] - -[[package]] -name = "idna" -version = "3.10" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, -] - -[package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] - -[[package]] -name = "importlib-metadata" -version = "8.6.1" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e"}, - {file = "importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580"}, -] - -[package.dependencies] -zipp = ">=3.20" - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -perf = ["ipython"] -test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["pytest-mypy"] - -[[package]] -name = "importlib-resources" -version = "6.5.2" -description = "Read resources from Python packages" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec"}, - {file = "importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c"}, -] - -[package.dependencies] -zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"] -type = ["pytest-mypy"] - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "jinja2" -version = "3.1.6" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, - {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "jsmin" -version = "3.0.1" -description = "JavaScript minifier." -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "jsmin-3.0.1.tar.gz", hash = "sha256:c0959a121ef94542e807a674142606f7e90214a2b3d1eb17300244bbb5cc2bfc"}, -] - -[[package]] -name = "markdown" -version = "3.7" -description = "Python implementation of John Gruber's Markdown." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"}, - {file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"}, -] - -[package.dependencies] -importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} - -[package.extras] -docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] -testing = ["coverage", "pyyaml"] - -[[package]] -name = "markdown-checklist" -version = "0.4.4" -description = "Python Markdown extension for task lists with checkboxes" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "markdown-checklist-0.4.4.tar.gz", hash = "sha256:69c93850798b1e01cdc6fcd4a80592d941f669f6451bbf69c71a4ffd1142f849"}, -] - -[package.dependencies] -markdown = "*" - -[package.extras] -coverage = ["coverage", "figleaf"] -testing = ["pytest"] - -[[package]] -name = "markupsafe" -version = "3.0.2" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, - {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, -] - -[[package]] -name = "mdx-gh-links" -version = "0.4" -description = "An extension to Python-Markdown which adds support for shorthand links to GitHub users, repositories, issues and commits." -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "mdx_gh_links-0.4-py3-none-any.whl", hash = "sha256:9057bca1fa5280bf1fcbf354381e46c9261cc32c2d5c0407801f8a910be5f099"}, - {file = "mdx_gh_links-0.4.tar.gz", hash = "sha256:41d5aac2ab201425aa0a19373c4095b79e5e015fdacfe83c398199fe55ca3686"}, -] - -[package.dependencies] -markdown = ">=3.0.0" - -[[package]] -name = "mergedeep" -version = "1.3.4" -description = "A deep merge function for 🐍." -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, - {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, -] - -[[package]] -name = "mike" -version = "2.1.3" -description = "Manage multiple versions of your MkDocs-powered documentation" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "mike-2.1.3-py3-none-any.whl", hash = "sha256:d90c64077e84f06272437b464735130d380703a76a5738b152932884c60c062a"}, - {file = "mike-2.1.3.tar.gz", hash = "sha256:abd79b8ea483fb0275b7972825d3082e5ae67a41820f8d8a0dc7a3f49944e810"}, -] - -[package.dependencies] -importlib-metadata = "*" -importlib-resources = "*" -jinja2 = ">=2.7" -mkdocs = ">=1.0" -pyparsing = ">=3.0" -pyyaml = ">=5.1" -pyyaml-env-tag = "*" -verspec = "*" - -[package.extras] -dev = ["coverage", "flake8 (>=3.0)", "flake8-quotes", "shtab"] -test = ["coverage", "flake8 (>=3.0)", "flake8-quotes", "shtab"] - -[[package]] -name = "mkdocs" -version = "1.6.1" -description = "Project documentation with Markdown." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, - {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, -] - -[package.dependencies] -click = ">=7.0" -colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} -ghp-import = ">=1.0" -importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} -jinja2 = ">=2.11.1" -markdown = ">=3.3.6" -markupsafe = ">=2.0.1" -mergedeep = ">=1.3.4" -mkdocs-get-deps = ">=0.2.0" -packaging = ">=20.5" -pathspec = ">=0.11.1" -pyyaml = ">=5.1" -pyyaml-env-tag = ">=0.1" -watchdog = ">=2.0" - -[package.extras] -i18n = ["babel (>=2.9.0)"] -min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4) ; platform_system == \"Windows\"", "ghp-import (==1.0)", "importlib-metadata (==4.4) ; python_version < \"3.10\"", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] - -[[package]] -name = "mkdocs-autoapi" -version = "0.4.0" -description = "MkDocs plugin providing automatic API reference generation" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "mkdocs_autoapi-0.4.0-py3-none-any.whl", hash = "sha256:ce2b5e8b4d1df37bd1273a6bce1dded152107e2280cedbfa085cea10edf8531b"}, - {file = "mkdocs_autoapi-0.4.0.tar.gz", hash = "sha256:409c2da8eb297ef51381b066744ac1cdf846220bcc779bdba680fbc5a080df1e"}, -] - -[package.dependencies] -mkdocs = ">=1.4.0" -mkdocstrings = ">=0.19.0" - -[package.extras] -python = ["mkdocstrings[python] (>=0.19.0)"] -python-legacy = ["mkdocstrings[python-legacy] (>=0.19.0)"] -vba = ["mkdocstrings-vba (>=0.0.10)"] - -[[package]] -name = "mkdocs-autorefs" -version = "1.4.1" -description = "Automatically link across pages in MkDocs." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "mkdocs_autorefs-1.4.1-py3-none-any.whl", hash = "sha256:9793c5ac06a6ebbe52ec0f8439256e66187badf4b5334b5fde0b128ec134df4f"}, - {file = "mkdocs_autorefs-1.4.1.tar.gz", hash = "sha256:4b5b6235a4becb2b10425c2fa191737e415b37aa3418919db33e5d774c9db079"}, -] - -[package.dependencies] -Markdown = ">=3.3" -markupsafe = ">=2.0.1" -mkdocs = ">=1.1" - -[[package]] -name = "mkdocs-get-deps" -version = "0.2.0" -description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, - {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, -] - -[package.dependencies] -importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} -mergedeep = ">=1.3.4" -platformdirs = ">=2.2.0" -pyyaml = ">=5.1" - -[[package]] -name = "mkdocs-material" -version = "9.6.9" -description = "Documentation that simply works" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "mkdocs_material-9.6.9-py3-none-any.whl", hash = "sha256:6e61b7fb623ce2aa4622056592b155a9eea56ff3487d0835075360be45a4c8d1"}, - {file = "mkdocs_material-9.6.9.tar.gz", hash = "sha256:a4872139715a1f27b2aa3f3dc31a9794b7bbf36333c0ba4607cf04786c94f89c"}, -] - -[package.dependencies] -babel = ">=2.10,<3.0" -backrefs = ">=5.7.post1,<6.0" -colorama = ">=0.4,<1.0" -jinja2 = ">=3.0,<4.0" -markdown = ">=3.2,<4.0" -mkdocs = ">=1.6,<2.0" -mkdocs-material-extensions = ">=1.3,<2.0" -paginate = ">=0.5,<1.0" -pygments = ">=2.16,<3.0" -pymdown-extensions = ">=10.2,<11.0" -requests = ">=2.26,<3.0" - -[package.extras] -git = ["mkdocs-git-committers-plugin-2 (>=1.1,<3)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"] -imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<11.0)"] -recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] - -[[package]] -name = "mkdocs-material-extensions" -version = "1.3.1" -description = "Extension pack for Python Markdown and MkDocs Material." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, - {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, -] - -[[package]] -name = "mkdocs-minify-plugin" -version = "0.8.0" -description = "An MkDocs plugin to minify HTML, JS or CSS files prior to being written to disk" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "mkdocs-minify-plugin-0.8.0.tar.gz", hash = "sha256:bc11b78b8120d79e817308e2b11539d790d21445eb63df831e393f76e52e753d"}, - {file = "mkdocs_minify_plugin-0.8.0-py3-none-any.whl", hash = "sha256:5fba1a3f7bd9a2142c9954a6559a57e946587b21f133165ece30ea145c66aee6"}, -] - -[package.dependencies] -csscompressor = ">=0.9.5" -htmlmin2 = ">=0.1.13" -jsmin = ">=3.0.1" -mkdocs = ">=1.4.1" - -[[package]] -name = "mkdocstrings" -version = "0.29.0" -description = "Automatic documentation from sources, for MkDocs." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "mkdocstrings-0.29.0-py3-none-any.whl", hash = "sha256:8ea98358d2006f60befa940fdebbbc88a26b37ecbcded10be726ba359284f73d"}, - {file = "mkdocstrings-0.29.0.tar.gz", hash = "sha256:3657be1384543ce0ee82112c3e521bbf48e41303aa0c229b9ffcccba057d922e"}, -] - -[package.dependencies] -importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} -Jinja2 = ">=2.11.1" -Markdown = ">=3.6" -MarkupSafe = ">=1.1" -mkdocs = ">=1.6" -mkdocs-autorefs = ">=1.4" -pymdown-extensions = ">=6.3" -typing-extensions = {version = ">=4.1", markers = "python_version < \"3.10\""} - -[package.extras] -crystal = ["mkdocstrings-crystal (>=0.3.4)"] -python = ["mkdocstrings-python (>=1.16.2)"] -python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] - -[[package]] -name = "mkdocstrings-python" -version = "1.16.8" -description = "A Python handler for mkdocstrings." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "mkdocstrings_python-1.16.8-py3-none-any.whl", hash = "sha256:211b7aaf776cd45578ecb531e5ad0d3a35a8be9101a6bfa10de38a69af9d8fd8"}, - {file = "mkdocstrings_python-1.16.8.tar.gz", hash = "sha256:9453ccae69be103810c1cf6435ce71c8f714ae37fef4d87d16aa92a7c800fe1d"}, -] - -[package.dependencies] -griffe = ">=1.6.2" -mkdocs-autorefs = ">=1.4" -mkdocstrings = ">=0.28.3" -typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} - -[[package]] -name = "mkdocstrings-shell" -version = "1.0.3" -description = "A shell scripts/libraries handler for mkdocstrings." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "mkdocstrings_shell-1.0.3-py3-none-any.whl", hash = "sha256:b23ebe43d06c9c19a541548f34d42ee4e4324ae06423eba8a9136e295c67f345"}, - {file = "mkdocstrings_shell-1.0.3.tar.gz", hash = "sha256:3bdea6a1e794a5d0e15d461f33b92e0b9f3b9a1e2c33671d9a2b7d83c761096a"}, -] - -[package.dependencies] -mkdocstrings = ">=0.28.3" -shellman = ">=1.0.2" - -[[package]] -name = "mock" -version = "5.1.0" -description = "Rolling backport of unittest.mock for all Pythons" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "mock-5.1.0-py3-none-any.whl", hash = "sha256:18c694e5ae8a208cdb3d2c20a993ca1a7b0efa258c247a1e565150f477f83744"}, - {file = "mock-5.1.0.tar.gz", hash = "sha256:5e96aad5ccda4718e0a229ed94b2024df75cc2d55575ba5762d31f5767b8767d"}, -] - -[package.extras] -build = ["blurb", "twine", "wheel"] -docs = ["sphinx"] -test = ["pytest", "pytest-cov"] - -[[package]] -name = "nodeenv" -version = "1.9.1" -description = "Node.js virtual environment builder" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["dev"] -files = [ - {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, - {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, -] - -[[package]] -name = "nxtools" -version = "1.6" -description = "nxtools is a set of various tools and helpers used by Nebula broadcast automation system and other software by imm studios, z.s." -optional = false -python-versions = ">=3.6,<4.0" -groups = ["dev"] -files = [ - {file = "nxtools-1.6-py3-none-any.whl", hash = "sha256:cde2d2c19193a2bd9c8071eeb3f121474ec34ab5c014de9a90f0c434cdfee716"}, - {file = "nxtools-1.6.tar.gz", hash = "sha256:e91839eef643483a3165165a46a693ad976d40a393ee5476a341e4e8bb102b92"}, -] - -[package.dependencies] -colorama = ">=0.4.4,<0.5.0" -Unidecode = ">=1.2.0,<2.0.0" - -[[package]] -name = "opentimelineio" -version = "0.17.0" -description = "Editorial interchange format and API" -optional = false -python-versions = "!=3.9.0,>=3.7" -groups = ["dev"] -files = [ - {file = "OpenTimelineIO-0.17.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:2dd31a570cabfd6227c1b1dd0cc038da10787492c26c55de058326e21fe8a313"}, - {file = "OpenTimelineIO-0.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5a1da5d4803d1ba5e846b181a9e0f4a392c76b9acc5e08947772bc086f2ebfc0"}, - {file = "OpenTimelineIO-0.17.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3527977aec8202789a42d60e1e0dc11b4154f585ef72921760445f43e7967a00"}, - {file = "OpenTimelineIO-0.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b3aafb4c50455832ed2627c2cac654b896473a5c1f8348ddc07c10be5cfbd59"}, - {file = "OpenTimelineIO-0.17.0-cp310-cp310-win32.whl", hash = "sha256:fee45af9f6330773893cd0858e92f8256bb5bde4229b44a76f03e59a9fb1b1b6"}, - {file = "OpenTimelineIO-0.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:d51887619689c21d67cc4b11b1088f99ae44094513315e7a144be00f1393bfa8"}, - {file = "OpenTimelineIO-0.17.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:cbf05c3e8c0187969f79e91f7495d1f0dc3609557874d8e601ba2e072c70ddb1"}, - {file = "OpenTimelineIO-0.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d3430c3f4e88c5365d7b6afbee920b0815b62ecf141abe44cd739c9eedc04284"}, - {file = "OpenTimelineIO-0.17.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1912345227b0bd1654c7153863eadbcee60362aa46340678e576e5d2aa3106a"}, - {file = "OpenTimelineIO-0.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51e06eb11a868d970c1534e39faf916228d5163bf3598076d408d8f393ab0bd4"}, - {file = "OpenTimelineIO-0.17.0-cp311-cp311-win32.whl", hash = "sha256:5c3a3f4780b25a8c1a80d788becba691d12b629069ad8783d0db21027639276f"}, - {file = "OpenTimelineIO-0.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:43c8726b33af30ba42928972192311ea0f986edbbd5f74651bada182d4fe805c"}, - {file = "OpenTimelineIO-0.17.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:9a9af4105a088c0ab131780e49db268db7e37871aac33db842de6b2b16f14e39"}, - {file = "OpenTimelineIO-0.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8e653ad1dd3b85f5c312a742dc24b61b330964aa391dc5bc072fe8b9c85adff1"}, - {file = "OpenTimelineIO-0.17.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a77823c27a1b93c6b87682372c3734ac5fddc10bfe53875e657d43c60fb885"}, - {file = "OpenTimelineIO-0.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4f4efcf3ddd81b62c4feb49a0bcc309b50ffeb6a8c48ab173d169a029006f4d"}, - {file = "OpenTimelineIO-0.17.0-cp312-cp312-win32.whl", hash = "sha256:9872ab74a20bb2bb3a50af04e80fe9238998d67d6be4e30e45aebe25d3eefac6"}, - {file = "OpenTimelineIO-0.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:c83b78be3312d3152d7e07ab32b0086fe220acc2a5b035b70ad69a787c0ece62"}, - {file = "OpenTimelineIO-0.17.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:0e671a6f2a1f772445bb326c7640dc977cfc3db589fe108a783a0311939cfac8"}, - {file = "OpenTimelineIO-0.17.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b931a3189b4ce064f06f15a89fe08ef4de01f7dcf0abc441fe2e02ef2a3311bb"}, - {file = "OpenTimelineIO-0.17.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:923cb54d806c981cf1e91916c3e57fba5664c22f37763dd012bad5a5a7bd4db4"}, - {file = "OpenTimelineIO-0.17.0-cp37-cp37m-win32.whl", hash = "sha256:8e16598c5084dcb21df3d83978b0e5f72300af9edd4cdcb85e3b0ba5da0df4e8"}, - {file = "OpenTimelineIO-0.17.0-cp37-cp37m-win_amd64.whl", hash = "sha256:7eed5033494888fb3f802af50e60559e279b2f398802748872903c2f54efd2c9"}, - {file = "OpenTimelineIO-0.17.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:118baa22b9227da5003bee653601a68686ae2823682dcd7d13c88178c63081c3"}, - {file = "OpenTimelineIO-0.17.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:43389eacdee2169de454e1c79ecfea82f54a9e73b67151427a9b621349a22b7f"}, - {file = "OpenTimelineIO-0.17.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:17659b1e6aa42ed617a942f7a2bfc6ecc375d0464ec127ce9edf896278ecaee9"}, - {file = "OpenTimelineIO-0.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36d5ea8cfbebf3c9013cc680eef5be48bffb515aafa9dc31e99bf66052a4ca3d"}, - {file = "OpenTimelineIO-0.17.0-cp38-cp38-win32.whl", hash = "sha256:cc67c74eb4b73bc0f7d135d3ff3dbbd86b2d451a9b142690a8d1631ad79c46f2"}, - {file = "OpenTimelineIO-0.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:69b39079bee6fa4aff34c6ad6544df394bc7388483fa5ce958ecd16e243a53ad"}, - {file = "OpenTimelineIO-0.17.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:a33554894dea17c22feec0201991e705c2c90a679ba2a012a0c558a7130df711"}, - {file = "OpenTimelineIO-0.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6b1ad3b3155370245b851b2f7b60006b2ebbb5bb76dd0fdc49bb4dce73fa7d96"}, - {file = "OpenTimelineIO-0.17.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:030454a9c0e9e82e5a153119f9afb8f3f4e64a3b27f80ac0dcde44b029fd3f3f"}, - {file = "OpenTimelineIO-0.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bce64376a28919533bd4f744ff8885118abefa73f78fd408f95fa7a9489855b6"}, - {file = "OpenTimelineIO-0.17.0-cp39-cp39-win32.whl", hash = "sha256:fa8cdceb25f9003c3c0b5b32baef2c764949d88b867161ddc6f44f48f6bbfa4a"}, - {file = "OpenTimelineIO-0.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:fbcf8a000cd688633c8dc5d22e91912013c67c674329eba603358e3b54da32bf"}, - {file = "opentimelineio-0.17.0.tar.gz", hash = "sha256:10ef324e710457e9977387cd9ef91eb24a9837bfb370aec3330f9c0f146cea85"}, -] - -[package.extras] -dev = ["check-manifest", "coverage (>=4.5)", "flake8 (>=3.5)", "urllib3 (>=1.24.3)"] -view = ["PySide2 (>=5.11,<6.0) ; platform_machine == \"x86_64\"", "PySide6 (>=6.2,<7.0) ; platform_machine == \"aarch64\""] - -[[package]] -name = "packaging" -version = "24.2" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, - {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, -] - -[[package]] -name = "paginate" -version = "0.5.7" -description = "Divides large result sets into pages for easier browsing" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591"}, - {file = "paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945"}, -] - -[package.extras] -dev = ["pytest", "tox"] -lint = ["black"] - -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - -[[package]] -name = "platformdirs" -version = "4.3.6" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, - {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, -] - -[package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.11.2)"] - -[[package]] -name = "pluggy" -version = "1.5.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "pre-commit" -version = "3.8.0" -description = "A framework for managing and maintaining multi-language pre-commit hooks." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, - {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, -] - -[package.dependencies] -cfgv = ">=2.0.0" -identify = ">=1.0.0" -nodeenv = ">=0.11.1" -pyyaml = ">=5.1" -virtualenv = ">=20.10.0" - -[[package]] -name = "pyblish-base" -version = "1.8.12" -description = "Plug-in driven automation framework for content" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "pyblish-base-1.8.12.tar.gz", hash = "sha256:ebc184eb038864380555227a8b58055dd24ece7e6ef7f16d33416c718512871b"}, - {file = "pyblish_base-1.8.12-py2.py3-none-any.whl", hash = "sha256:2cbe956bfbd4175a2d7d22b344cd345800f4d4437153434ab658fc12646a11e8"}, -] - -[[package]] -name = "pygments" -version = "2.19.1" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, - {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pymdown-extensions" -version = "10.14.3" -description = "Extension pack for Python Markdown." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pymdown_extensions-10.14.3-py3-none-any.whl", hash = "sha256:05e0bee73d64b9c71a4ae17c72abc2f700e8bc8403755a00580b49a4e9f189e9"}, - {file = "pymdown_extensions-10.14.3.tar.gz", hash = "sha256:41e576ce3f5d650be59e900e4ceff231e0aed2a88cf30acaee41e02f063a061b"}, -] - -[package.dependencies] -markdown = ">=3.6" -pyyaml = "*" - -[package.extras] -extra = ["pygments (>=2.19.1)"] - -[[package]] -name = "pyparsing" -version = "3.2.3" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf"}, - {file = "pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be"}, -] - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - -[[package]] -name = "pytest" -version = "8.3.4" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, - {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=1.5,<2" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-print" -version = "1.0.2" -description = "pytest-print adds the printer fixture you can use to print messages to the user (directly to the pytest runner, not stdout)" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pytest_print-1.0.2-py3-none-any.whl", hash = "sha256:3ae7891085dddc3cd697bd6956787240107fe76d6b5cdcfcd782e33ca6543de9"}, - {file = "pytest_print-1.0.2.tar.gz", hash = "sha256:2780350a7bbe7117f99c5d708dc7b0431beceda021b1fd3f11200670d7f33679"}, -] - -[package.dependencies] -pytest = ">=8.3.2" - -[package.extras] -test = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "pytest-mock (>=3.14)"] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["dev"] -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "pyyaml" -version = "6.0.2" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, - {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, - {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, - {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, - {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, - {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, - {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, - {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, - {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, - {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, - {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, - {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, - {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, - {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, - {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, - {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, - {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, -] - -[[package]] -name = "pyyaml-env-tag" -version = "0.1" -description = "A custom YAML tag for referencing environment variables in YAML files. " -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, - {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, -] - -[package.dependencies] -pyyaml = "*" - -[[package]] -name = "requests" -version = "2.32.3" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, - {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "ruff" -version = "0.3.7" -description = "An extremely fast Python linter and code formatter, written in Rust." -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0e8377cccb2f07abd25e84fc5b2cbe48eeb0fea9f1719cad7caedb061d70e5ce"}, - {file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:15a4d1cc1e64e556fa0d67bfd388fed416b7f3b26d5d1c3e7d192c897e39ba4b"}, - {file = "ruff-0.3.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d28bdf3d7dc71dd46929fafeec98ba89b7c3550c3f0978e36389b5631b793663"}, - {file = "ruff-0.3.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:379b67d4f49774ba679593b232dcd90d9e10f04d96e3c8ce4a28037ae473f7bb"}, - {file = "ruff-0.3.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c060aea8ad5ef21cdfbbe05475ab5104ce7827b639a78dd55383a6e9895b7c51"}, - {file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ebf8f615dde968272d70502c083ebf963b6781aacd3079081e03b32adfe4d58a"}, - {file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d48098bd8f5c38897b03604f5428901b65e3c97d40b3952e38637b5404b739a2"}, - {file = "ruff-0.3.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da8a4fda219bf9024692b1bc68c9cff4b80507879ada8769dc7e985755d662ea"}, - {file = "ruff-0.3.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c44e0149f1d8b48c4d5c33d88c677a4aa22fd09b1683d6a7ff55b816b5d074f"}, - {file = "ruff-0.3.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3050ec0af72b709a62ecc2aca941b9cd479a7bf2b36cc4562f0033d688e44fa1"}, - {file = "ruff-0.3.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a29cc38e4c1ab00da18a3f6777f8b50099d73326981bb7d182e54a9a21bb4ff7"}, - {file = "ruff-0.3.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b15cc59c19edca917f51b1956637db47e200b0fc5e6e1878233d3a938384b0b"}, - {file = "ruff-0.3.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e491045781b1e38b72c91247cf4634f040f8d0cb3e6d3d64d38dcf43616650b4"}, - {file = "ruff-0.3.7-py3-none-win32.whl", hash = "sha256:bc931de87593d64fad3a22e201e55ad76271f1d5bfc44e1a1887edd0903c7d9f"}, - {file = "ruff-0.3.7-py3-none-win_amd64.whl", hash = "sha256:5ef0e501e1e39f35e03c2acb1d1238c595b8bb36cf7a170e7c1df1b73da00e74"}, - {file = "ruff-0.3.7-py3-none-win_arm64.whl", hash = "sha256:789e144f6dc7019d1f92a812891c645274ed08af6037d11fc65fcbc183b7d59f"}, - {file = "ruff-0.3.7.tar.gz", hash = "sha256:d5c1aebee5162c2226784800ae031f660c350e7a3402c4d1f8ea4e97e232e3ba"}, -] - -[[package]] -name = "semver" -version = "3.0.4" -description = "Python helper for Semantic Versioning (https://semver.org)" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746"}, - {file = "semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602"}, -] - -[[package]] -name = "shellman" -version = "1.0.2" -description = "Write documentation in comments and render it with templates." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "shellman-1.0.2-py3-none-any.whl", hash = "sha256:f8c960fd2d3785e195f86fcd8f110a8d51a950e759d82c14a5af0bd71b918b3c"}, - {file = "shellman-1.0.2.tar.gz", hash = "sha256:48cba79d6415c0d013ad4dfd2205ed81b0e468795d1886dcda943ac78eaffd38"}, -] - -[package.dependencies] -importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} -jinja2 = ">=3" -typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} - -[[package]] -name = "six" -version = "1.17.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["dev"] -files = [ - {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, - {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, -] - -[[package]] -name = "tomli" -version = "2.2.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, - {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, - {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, - {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, - {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, - {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, - {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, - {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, - {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, - {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, -] - -[[package]] -name = "tomlkit" -version = "0.13.2" -description = "Style preserving TOML library" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, - {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, -] - -[[package]] -name = "typing-extensions" -version = "4.12.2" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, -] - -[[package]] -name = "unidecode" -version = "1.3.8" -description = "ASCII transliterations of Unicode text" -optional = false -python-versions = ">=3.5" -groups = ["dev"] -files = [ - {file = "Unidecode-1.3.8-py3-none-any.whl", hash = "sha256:d130a61ce6696f8148a3bd8fe779c99adeb4b870584eeb9526584e9aa091fd39"}, - {file = "Unidecode-1.3.8.tar.gz", hash = "sha256:cfdb349d46ed3873ece4586b96aa75258726e2fa8ec21d6f00a591d98806c2f4"}, -] - -[[package]] -name = "urllib3" -version = "2.3.0" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, - {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "verspec" -version = "0.1.0" -description = "Flexible version handling" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "verspec-0.1.0-py3-none-any.whl", hash = "sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31"}, - {file = "verspec-0.1.0.tar.gz", hash = "sha256:c4504ca697b2056cdb4bfa7121461f5a0e81809255b41c03dda4ba823637c01e"}, -] - -[package.extras] -test = ["coverage", "flake8 (>=3.7)", "mypy", "pretend", "pytest"] - -[[package]] -name = "virtualenv" -version = "20.29.2" -description = "Virtual Python Environment builder" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "virtualenv-20.29.2-py3-none-any.whl", hash = "sha256:febddfc3d1ea571bdb1dc0f98d7b45d24def7428214d4fb73cc486c9568cce6a"}, - {file = "virtualenv-20.29.2.tar.gz", hash = "sha256:fdaabebf6d03b5ba83ae0a02cfe96f48a716f4fae556461d180825866f75b728"}, -] - -[package.dependencies] -distlib = ">=0.3.7,<1" -filelock = ">=3.12.2,<4" -platformdirs = ">=3.9.1,<5" - -[package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] - -[[package]] -name = "watchdog" -version = "6.0.0" -description = "Filesystem events monitoring" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, - {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, - {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, - {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, - {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, - {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, - {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, - {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, - {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, - {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, - {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, - {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, - {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"}, - {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"}, - {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"}, - {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, - {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, - {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"}, - {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, - {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, - {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, - {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, - {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, -] - -[package.extras] -watchmedo = ["PyYAML (>=3.10)"] - -[[package]] -name = "zipp" -version = "3.21.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, - {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] - -[metadata] -lock-version = "2.1" -python-versions = ">=3.9.1,<3.10" -content-hash = "c0d98dae3faeed5237f286362f9d280ab17c3d42f7cd41fdd69ee4546983c553" From bc99edd74d5765968fb34aada785bf0eda72141b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 27 May 2025 10:47:46 +0200 Subject: [PATCH 024/155] remo identifier constance --- server/__init__.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/server/__init__.py b/server/__init__.py index aafd8d6fa5..c252f27762 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -18,9 +18,6 @@ from .settings import ( ) -IDENTIFIER_PREFIX = "core" - - class CoreAddon(BaseServerAddon): settings_model = CoreSettings @@ -51,7 +48,7 @@ class CoreAddon(BaseServerAddon): # Add 'Create Project Folder Structure' action to folders. output.append( SimpleActionManifest( - identifier=f"{IDENTIFIER_PREFIX}.createprojectstructure", + identifier="core.createprojectstructure", label="Create Project Folder Structure", icon={ "type": "material-symbols", @@ -74,9 +71,7 @@ class CoreAddon(BaseServerAddon): project_name = executor.context.project_name - if executor.identifier == \ - f"{IDENTIFIER_PREFIX}.create_project_structure": - + if executor.identifier == "core.createprojectstructure": if not project_name: logger.error( f"Can't execute {executor.identifier} because" From 6a7e1abf5da77d3d4884bed3e9db3f895cf325a5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 27 May 2025 10:48:01 +0200 Subject: [PATCH 025/155] use new executor methods --- server/__init__.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/server/__init__.py b/server/__init__.py index c252f27762..208268bbf7 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -77,13 +77,26 @@ class CoreAddon(BaseServerAddon): f"Can't execute {executor.identifier} because" " of missing project name." ) + # Works since AYON server 1.8.3 + if hasattr(executor, "get_simple_response"): + return await executor.get_simple_response( + "Missing project name", success=False + ) return - return await executor.get_launcher_action_response( - args=[ - "create-project-structure", - "--project", project_name, - ] - ) + args = [ + "create-project-structure", "--project", project_name, + ] + # Works since AYON server 1.8.3 + if hasattr(executor, "get_launcher_response"): + return await executor.get_launcher_response(args) + + return await executor.get_launcher_action_response(args) logger.debug(f"Unknown action: {executor.identifier}") + # Works since AYON server 1.8.3 + if hasattr(executor, "get_simple_response"): + return await executor.get_simple_response( + "Unknown action", success=False + ) + return From 6dbbe75099326609cecd47db2de493ddb0a9027e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 27 May 2025 10:50:58 +0200 Subject: [PATCH 026/155] remove unnecessary return --- server/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/server/__init__.py b/server/__init__.py index 208268bbf7..662c11679e 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -99,4 +99,3 @@ class CoreAddon(BaseServerAddon): return await executor.get_simple_response( "Unknown action", success=False ) - return From ef1c77bf4c3b225e2c023793865a7244bdafad84 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 27 May 2025 10:51:43 +0200 Subject: [PATCH 027/155] change entity type to project --- server/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/__init__.py b/server/__init__.py index 662c11679e..620cb3285c 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -55,7 +55,7 @@ class CoreAddon(BaseServerAddon): "name": "create_new_folder", }, order=100, - entity_type="folder", + entity_type="project", entity_subtypes=None, allow_multiselection=False, ) From 39b85970e25839fddd81d9ed77803e0c22d5b0f1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 27 May 2025 11:03:14 +0200 Subject: [PATCH 028/155] simplified description --- server/settings/main.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/server/settings/main.py b/server/settings/main.py index a5129e69af..67f92f0037 100644 --- a/server/settings/main.py +++ b/server/settings/main.py @@ -302,13 +302,8 @@ class CoreSettings(BaseSettingsModel): widget="textarea", title="Project folder structure", description=( - "Defines project folders to create on 'Create project folders'." - "\n\n" - "- In the launcher, this only creates the folders on disk and " - " when the setting is empty it will be hidden from users in the" - " launcher.\n" - "- In `ayon-ftrack` this will create the folders on disk **and**" - " will also create ftrack entities. It is never hidden there." + "Defines project folders to create on disk" + " for 'Create project folders' action." ), section="---" ) From 91df75f8eaaf0b6a74113c1d4facfd9be597d90f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 30 May 2025 16:10:20 +0200 Subject: [PATCH 029/155] :sparkles: add product base type support to loaders --- client/ayon_core/pipeline/load/plugins.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 4a11b929cc..7a92ed943d 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -1,3 +1,5 @@ +"""Plugins for loading representations and products into host applications.""" +from __future__ import annotations import os import logging @@ -15,7 +17,8 @@ from .utils import get_representation_path_from_context class LoaderPlugin(list): """Load representation into host application""" - product_types = set() + product_types: set[str] = set() + product_base_types: set[str] = set() representations = set() extensions = {"*"} order = 0 @@ -122,9 +125,11 @@ class LoaderPlugin(list): plugin_repre_names = cls.get_representations() plugin_product_types = cls.product_types + plugin_product_base_types = cls.product_base_types if ( not plugin_repre_names or not plugin_product_types + or not plugin_product_base_types or not cls.extensions ): return False @@ -147,10 +152,20 @@ class LoaderPlugin(list): if "*" in plugin_product_types: return True + plugin_product_base_types = set(plugin_product_base_types) + if "*" in plugin_product_base_types: + # If plugin supports all product base types, then it is compatible + # with any product type. + return True + product_entity = context["product"] product_type = product_entity["productType"] + product_base_type = product_entity.get("productBaseType") - return product_type in plugin_product_types + if product_type in plugin_product_types: + return True + + return product_base_type in plugin_product_base_types @classmethod def get_representations(cls): From fac933c16ab73d7f893e27ab6f0a022e2dd5dcf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Fri, 30 May 2025 16:46:18 +0200 Subject: [PATCH 030/155] :recycle: make the check backwards compatible Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- client/ayon_core/pipeline/load/plugins.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 7a92ed943d..32b96e3e7a 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -128,8 +128,7 @@ class LoaderPlugin(list): plugin_product_base_types = cls.product_base_types if ( not plugin_repre_names - or not plugin_product_types - or not plugin_product_base_types + or (not plugin_product_types and not plugin_product_base_types) or not cls.extensions ): return False From 6ea717bc3624cd17da53dd676772278704ac87d3 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 6 Jun 2025 10:01:32 +0200 Subject: [PATCH 031/155] :wrench: WIP on product base type support in loader tool --- client/ayon_core/tools/loader/abstract.py | 140 +++++++++++++----- .../ayon_core/tools/loader/models/products.py | 132 +++++++++++++++-- 2 files changed, 226 insertions(+), 46 deletions(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index d0d7cd430b..741eb59f81 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -1,5 +1,6 @@ +from __future__ import annotations from abc import ABC, abstractmethod -from typing import List +from typing import List, Optional, TypedDict from ayon_core.lib.attribute_definitions import ( AbstractAttrDef, @@ -8,15 +9,62 @@ from ayon_core.lib.attribute_definitions import ( ) +IconData = TypedDict("IconData", { + "type": str, + "name": str, + "color": str +}) + +ProductBaseTypeItemData = TypedDict("ProductBaseTypeItemData", { + "name": str, + "icon": IconData +}) + + +VersionItemData = TypedDict("VersionItemData", { + "version_id": str, + "version": int, + "is_hero": bool, + "product_id": str, + "task_id": Optional[str], + "thumbnail_id": Optional[str], + "published_time": Optional[str], + "author": Optional[str], + "status": Optional[str], + "frame_range": Optional[str], + "duration": Optional[int], + "handles": Optional[str], + "step": Optional[int], + "comment": Optional[str], + "source": Optional[str] +}) + + +ProductItemData = TypedDict("ProductItemData", { + "product_id": str, + "product_type": str, + "product_base_type": str, + "product_name": str, + "product_icon": IconData, + "product_type_icon": IconData, + "product_base_type_icon": IconData, + "group_name": str, + "folder_id": str, + "folder_label": str, + "version_items": dict[str, VersionItemData], + "product_in_scene": bool +}) + + class ProductTypeItem: """Item representing product type. Args: name (str): Product type name. - icon (dict[str, Any]): Product type icon definition. + icon (IconData): Product type icon definition. """ - def __init__(self, name, icon): + def __init__(self, name: str, icon: IconData): self.name = name self.icon = icon @@ -31,6 +79,24 @@ class ProductTypeItem: return cls(**data) +class ProductBaseTypeItem: + """Item representing product base type.""" + + def __init__(self, name: str, icon: IconData): + self.name = name + self.icon = icon + + def to_data(self) -> ProductBaseTypeItemData: + return { + "name": self.name, + "icon": self.icon, + } + + @classmethod + def from_data(cls, data: ProductBaseTypeItemData): + return cls(**data) + + class ProductItem: """Product item with it versions. @@ -38,8 +104,8 @@ class ProductItem: product_id (str): Product id. product_type (str): Product type. product_name (str): Product name. - product_icon (dict[str, Any]): Product icon definition. - product_type_icon (dict[str, Any]): Product type icon definition. + product_icon (IconData): Product icon definition. + product_type_icon (IconData): Product type icon definition. product_in_scene (bool): Is product in scene (only when used in DCC). group_name (str): Group name. folder_id (str): Folder id. @@ -49,35 +115,42 @@ class ProductItem: def __init__( self, - product_id, - product_type, - product_name, - product_icon, - product_type_icon, - product_in_scene, - group_name, - folder_id, - folder_label, - version_items, + product_id: str, + product_type: str, + product_base_type: str, + product_name: str, + product_icon: IconData, + product_type_icon: IconData, + product_base_type_icon: IconData, + group_name: str, + folder_id: str, + folder_label: str, + version_items: dict[str, VersionItem], + *, + product_in_scene: bool, ): self.product_id = product_id self.product_type = product_type + self.product_base_type = product_base_type self.product_name = product_name self.product_icon = product_icon self.product_type_icon = product_type_icon + self.product_base_type_icon = product_base_type_icon self.product_in_scene = product_in_scene self.group_name = group_name self.folder_id = folder_id self.folder_label = folder_label self.version_items = version_items - def to_data(self): + def to_data(self) -> ProductItemData: return { "product_id": self.product_id, "product_type": self.product_type, + "product_base_type": self.product_base_type, "product_name": self.product_name, "product_icon": self.product_icon, "product_type_icon": self.product_type_icon, + "product_base_type_icon": self.product_base_type_icon, "product_in_scene": self.product_in_scene, "group_name": self.group_name, "folder_id": self.folder_id, @@ -124,21 +197,22 @@ class VersionItem: def __init__( self, - version_id, - version, - is_hero, - product_id, - task_id, - thumbnail_id, - published_time, - author, - status, - frame_range, - duration, - handles, - step, - comment, - source, + *, + version_id: str, + version: int, + is_hero: bool, + product_id: str, + task_id: Optional[str] = None, + thumbnail_id: Optional[str] = None, + published_time: Optional[str] = None, + author: Optional[str] = None, + status: Optional[str] = None, + frame_range: Optional[str] = None, + duration: Optional[int] = None, + handles: Optional[str] = None, + step: Optional[int] = None, + comment: Optional[str] = None, + source: Optional[str] = None, ): self.version_id = version_id self.product_id = product_id @@ -198,7 +272,7 @@ class VersionItem: def __le__(self, other): return self.__eq__(other) or self.__lt__(other) - def to_data(self): + def to_data(self) -> VersionItemData: return { "version_id": self.version_id, "product_id": self.product_id, @@ -218,7 +292,7 @@ class VersionItem: } @classmethod - def from_data(cls, data): + def from_data(cls, data: VersionItemData): return cls(**data) diff --git a/client/ayon_core/tools/loader/models/products.py b/client/ayon_core/tools/loader/models/products.py index 34acc0550c..da2b049f50 100644 --- a/client/ayon_core/tools/loader/models/products.py +++ b/client/ayon_core/tools/loader/models/products.py @@ -1,19 +1,29 @@ +"""Products model for loader tools.""" +from __future__ import annotations import collections import contextlib +from typing import TYPE_CHECKING, Iterable, Optional import arrow import ayon_api from ayon_api.operations import OperationsSession + from ayon_core.lib import NestedCacheItem from ayon_core.style import get_default_entity_icon_color from ayon_core.tools.loader.abstract import ( + IconData, ProductTypeItem, + ProductBaseTypeItem, ProductItem, VersionItem, RepreItem, ) +if TYPE_CHECKING: + from ayon_api.typing import ProductBaseTypeDict, ProductDict, VersionDict + + PRODUCTS_MODEL_SENDER = "products.model" @@ -70,9 +80,10 @@ def version_item_from_entity(version): def product_item_from_entity( - product_entity, + product_entity: ProductDict, version_entities, - product_type_items_by_name, + product_type_items_by_name: dict[str, ProductTypeItem], + product_base_type_items_by_name: dict[str, ProductBaseTypeItem], folder_label, product_in_scene, ): @@ -88,9 +99,21 @@ def product_item_from_entity( # Cache the item for future use product_type_items_by_name[product_type] = product_type_item - product_type_icon = product_type_item.icon + product_base_type = product_entity.get("productBaseType") + product_base_type_item = product_base_type_items_by_name.get( + product_base_type) + # Same as for product type item above. Not sure if this is still needed + # though. + if product_base_type_item is None: + product_base_type_item = create_default_product_base_type_item( + product_base_type) + # Cache the item for future use + product_base_type_items_by_name[product_base_type] = ( + product_base_type_item) - product_icon = { + product_type_icon = product_type_item.icon + product_base_type_icon = product_base_type_item.icon + product_icon: IconData = { "type": "awesome-font", "name": "fa.file-o", "color": get_default_entity_icon_color(), @@ -103,9 +126,11 @@ def product_item_from_entity( return ProductItem( product_id=product_entity["id"], product_type=product_type, + product_base_type=product_base_type, product_name=product_entity["name"], product_icon=product_icon, product_type_icon=product_type_icon, + product_base_type_icon=product_base_type_icon, product_in_scene=product_in_scene, group_name=group, folder_id=product_entity["folderId"], @@ -114,11 +139,12 @@ def product_item_from_entity( ) -def product_type_item_from_data(product_type_data): +def product_type_item_from_data( + product_type_data: ProductDict) -> ProductTypeItem: # TODO implement icon implementation # icon = product_type_data["icon"] # color = product_type_data["color"] - icon = { + icon: IconData = { "type": "awesome-font", "name": "fa.folder", "color": "#0091B2", @@ -127,8 +153,30 @@ def product_type_item_from_data(product_type_data): return ProductTypeItem(product_type_data["name"], icon) -def create_default_product_type_item(product_type): - icon = { +def product_base_type_item_from_data( + product_base_type_data: ProductBaseTypeDict +) -> ProductBaseTypeItem: + """Create product base type item from data. + + Args: + product_base_type_data (ProductBaseTypeDict): Product base type data. + + Returns: + ProductBaseTypeDict: Product base type item. + + """ + icon: IconData = { + "type": "awesome-font", + "name": "fa.folder", + "color": "#0091B2", + } + return ProductBaseTypeItem( + name=product_base_type_data["name"], + icon=icon) + + +def create_default_product_type_item(product_type: str) -> ProductTypeItem: + icon: IconData = { "type": "awesome-font", "name": "fa.folder", "color": "#0091B2", @@ -136,10 +184,28 @@ def create_default_product_type_item(product_type): return ProductTypeItem(product_type, icon) +def create_default_product_base_type_item( + product_base_type: str) -> ProductBaseTypeItem: + """Create default product base type item. + + Args: + product_base_type (str): Product base type name. + + Returns: + ProductBaseTypeItem: Default product base type item. + """ + icon: IconData = { + "type": "awesome-font", + "name": "fa.folder", + "color": "#0091B2", + } + return ProductBaseTypeItem(product_base_type, icon) + + class ProductsModel: """Model for products, version and representation. - All of the entities are product based. This model prepares data for UI + All the entities are product based. This model prepares data for UI and caches it for faster access. Note: @@ -161,6 +227,8 @@ class ProductsModel: # Cache helpers self._product_type_items_cache = NestedCacheItem( levels=1, default_factory=list, lifetime=self.lifetime) + self._product_base_type_items_cache = NestedCacheItem( + levels=1, default_factory=list, lifetime=self.lifetime) self._product_items_cache = NestedCacheItem( levels=2, default_factory=dict, lifetime=self.lifetime) self._repre_items_cache = NestedCacheItem( @@ -199,6 +267,31 @@ class ProductsModel: ]) return cache.get_data() + def get_product_base_type_items( + self, + project_name: Optional[str]) -> list[ProductBaseTypeItem]: + """Product base type items for project. + + Args: + project_name (optional, str): Project name. + + Returns: + list[ProductBaseTypeDict]: Product base type items. + + """ + if not project_name: + return [] + + cache = self._product_base_type_items_cache[project_name] + if not cache.is_valid: + product_base_types = ayon_api.get_project_product_base_types( + project_name) + cache.update_data([ + product_base_type_item_from_data(product_base_type) + for product_base_type in product_base_types + ]) + return cache.get_data() + def get_product_items(self, project_name, folder_ids, sender): """Product items with versions for project and folder ids. @@ -449,11 +542,12 @@ class ProductsModel: def _create_product_items( self, - project_name, - products, - versions, + project_name: str, + products: Iterable[ProductDict], + versions: Iterable[VersionDict], folder_items=None, product_type_items=None, + product_base_type_items: Optional[Iterable[ProductBaseTypeItem]] = None ): if folder_items is None: folder_items = self._controller.get_folder_items(project_name) @@ -461,6 +555,11 @@ class ProductsModel: if product_type_items is None: product_type_items = self.get_product_type_items(project_name) + if product_base_type_items is None: + product_base_type_items = self.get_product_base_type_items( + project_name + ) + loaded_product_ids = self._controller.get_loaded_product_ids() versions_by_product_id = collections.defaultdict(list) @@ -470,7 +569,13 @@ class ProductsModel: product_type_item.name: product_type_item for product_type_item in product_type_items } - output = {} + + product_base_type_items_by_name: dict[str, ProductBaseTypeItem] = { + product_base_type_item.name: product_base_type_item + for product_base_type_item in product_base_type_items + } + + output: dict[str, ProductItem] = {} for product in products: product_id = product["id"] folder_id = product["folderId"] @@ -484,6 +589,7 @@ class ProductsModel: product, versions, product_type_items_by_name, + product_base_type_items_by_name, folder_item.label, product_id in loaded_product_ids, ) From 3a2f470dce3690c335466ecc01d1ff14588753be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Fri, 6 Jun 2025 14:03:31 +0200 Subject: [PATCH 032/155] :sparkles: show product base type in the loader --- client/ayon_core/tools/loader/abstract.py | 26 +++++++-- .../ayon_core/tools/loader/models/products.py | 2 +- .../tools/loader/ui/products_model.py | 55 +++++++++++-------- .../tools/loader/ui/products_widget.py | 1 + 4 files changed, 55 insertions(+), 29 deletions(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index 741eb59f81..d6a4bf40cb 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -1,14 +1,15 @@ +"""Abstract base classes for loader tool.""" from __future__ import annotations + from abc import ABC, abstractmethod from typing import List, Optional, TypedDict from ayon_core.lib.attribute_definitions import ( AbstractAttrDef, - serialize_attr_defs, deserialize_attr_defs, + serialize_attr_defs, ) - IconData = TypedDict("IconData", { "type": str, "name": str, @@ -80,20 +81,37 @@ class ProductTypeItem: class ProductBaseTypeItem: - """Item representing product base type.""" + """Item representing the product base type.""" def __init__(self, name: str, icon: IconData): + """Initialize product base type item.""" self.name = name self.icon = icon def to_data(self) -> ProductBaseTypeItemData: + """Convert item to data dictionary. + + Returns: + ProductBaseTypeItemData: Data representation of the item. + + """ return { "name": self.name, "icon": self.icon, } @classmethod - def from_data(cls, data: ProductBaseTypeItemData): + def from_data( + cls, data: ProductBaseTypeItemData) -> ProductBaseTypeItem: + """Create item from data dictionary. + + Args: + data (ProductBaseTypeItemData): Data to create item from. + + Returns: + ProductBaseTypeItem: Item created from the provided data. + + """ return cls(**data) diff --git a/client/ayon_core/tools/loader/models/products.py b/client/ayon_core/tools/loader/models/products.py index da2b049f50..41919461d0 100644 --- a/client/ayon_core/tools/loader/models/products.py +++ b/client/ayon_core/tools/loader/models/products.py @@ -270,7 +270,7 @@ class ProductsModel: def get_product_base_type_items( self, project_name: Optional[str]) -> list[ProductBaseTypeItem]: - """Product base type items for project. + """Product base type items for the project. Args: project_name (optional, str): Project name. diff --git a/client/ayon_core/tools/loader/ui/products_model.py b/client/ayon_core/tools/loader/ui/products_model.py index cebae9bca7..24050fc0c1 100644 --- a/client/ayon_core/tools/loader/ui/products_model.py +++ b/client/ayon_core/tools/loader/ui/products_model.py @@ -16,31 +16,32 @@ TASK_ID_ROLE = QtCore.Qt.UserRole + 5 PRODUCT_ID_ROLE = QtCore.Qt.UserRole + 6 PRODUCT_NAME_ROLE = QtCore.Qt.UserRole + 7 PRODUCT_TYPE_ROLE = QtCore.Qt.UserRole + 8 -PRODUCT_TYPE_ICON_ROLE = QtCore.Qt.UserRole + 9 -PRODUCT_IN_SCENE_ROLE = QtCore.Qt.UserRole + 10 -VERSION_ID_ROLE = QtCore.Qt.UserRole + 11 -VERSION_HERO_ROLE = QtCore.Qt.UserRole + 12 -VERSION_NAME_ROLE = QtCore.Qt.UserRole + 13 -VERSION_NAME_EDIT_ROLE = QtCore.Qt.UserRole + 14 -VERSION_PUBLISH_TIME_ROLE = QtCore.Qt.UserRole + 15 -VERSION_STATUS_NAME_ROLE = QtCore.Qt.UserRole + 16 -VERSION_STATUS_SHORT_ROLE = QtCore.Qt.UserRole + 17 -VERSION_STATUS_COLOR_ROLE = QtCore.Qt.UserRole + 18 -VERSION_STATUS_ICON_ROLE = QtCore.Qt.UserRole + 19 -VERSION_AUTHOR_ROLE = QtCore.Qt.UserRole + 20 -VERSION_FRAME_RANGE_ROLE = QtCore.Qt.UserRole + 21 -VERSION_DURATION_ROLE = QtCore.Qt.UserRole + 22 -VERSION_HANDLES_ROLE = QtCore.Qt.UserRole + 23 -VERSION_STEP_ROLE = QtCore.Qt.UserRole + 24 -VERSION_AVAILABLE_ROLE = QtCore.Qt.UserRole + 25 -VERSION_THUMBNAIL_ID_ROLE = QtCore.Qt.UserRole + 26 -ACTIVE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 27 -REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 28 -REPRESENTATIONS_COUNT_ROLE = QtCore.Qt.UserRole + 29 -SYNC_ACTIVE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 30 -SYNC_REMOTE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 31 +PRODUCT_BASE_TYPE_ROLE = QtCore.Qt.UserRole + 9 +PRODUCT_TYPE_ICON_ROLE = QtCore.Qt.UserRole + 10 +PRODUCT_IN_SCENE_ROLE = QtCore.Qt.UserRole + 11 +VERSION_ID_ROLE = QtCore.Qt.UserRole + 12 +VERSION_HERO_ROLE = QtCore.Qt.UserRole + 13 +VERSION_NAME_ROLE = QtCore.Qt.UserRole + 14 +VERSION_NAME_EDIT_ROLE = QtCore.Qt.UserRole + 15 +VERSION_PUBLISH_TIME_ROLE = QtCore.Qt.UserRole + 16 +VERSION_STATUS_NAME_ROLE = QtCore.Qt.UserRole + 17 +VERSION_STATUS_SHORT_ROLE = QtCore.Qt.UserRole + 18 +VERSION_STATUS_COLOR_ROLE = QtCore.Qt.UserRole + 19 +VERSION_STATUS_ICON_ROLE = QtCore.Qt.UserRole + 20 +VERSION_AUTHOR_ROLE = QtCore.Qt.UserRole + 21 +VERSION_FRAME_RANGE_ROLE = QtCore.Qt.UserRole + 22 +VERSION_DURATION_ROLE = QtCore.Qt.UserRole + 23 +VERSION_HANDLES_ROLE = QtCore.Qt.UserRole + 24 +VERSION_STEP_ROLE = QtCore.Qt.UserRole + 25 +VERSION_AVAILABLE_ROLE = QtCore.Qt.UserRole + 26 +VERSION_THUMBNAIL_ID_ROLE = QtCore.Qt.UserRole + 27 +ACTIVE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 28 +REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 29 +REPRESENTATIONS_COUNT_ROLE = QtCore.Qt.UserRole + 30 +SYNC_ACTIVE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 31 +SYNC_REMOTE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 32 -STATUS_NAME_FILTER_ROLE = QtCore.Qt.UserRole + 32 +STATUS_NAME_FILTER_ROLE = QtCore.Qt.UserRole + 33 class ProductsModel(QtGui.QStandardItemModel): @@ -49,6 +50,7 @@ class ProductsModel(QtGui.QStandardItemModel): column_labels = [ "Product name", "Product type", + "Product base type", "Folder", "Version", "Status", @@ -79,6 +81,7 @@ class ProductsModel(QtGui.QStandardItemModel): product_name_col = column_labels.index("Product name") product_type_col = column_labels.index("Product type") + product_base_type_col = column_labels.index("Product base type") folders_label_col = column_labels.index("Folder") version_col = column_labels.index("Version") status_col = column_labels.index("Status") @@ -93,6 +96,7 @@ class ProductsModel(QtGui.QStandardItemModel): _display_role_mapping = { product_name_col: QtCore.Qt.DisplayRole, product_type_col: PRODUCT_TYPE_ROLE, + product_base_type_col: PRODUCT_BASE_TYPE_ROLE, folders_label_col: FOLDER_LABEL_ROLE, version_col: VERSION_NAME_ROLE, status_col: VERSION_STATUS_NAME_ROLE, @@ -432,6 +436,9 @@ class ProductsModel(QtGui.QStandardItemModel): model_item.setData(icon, QtCore.Qt.DecorationRole) model_item.setData(product_id, PRODUCT_ID_ROLE) model_item.setData(product_item.product_name, PRODUCT_NAME_ROLE) + model_item.setData( + product_item.product_base_type, PRODUCT_BASE_TYPE_ROLE + ) model_item.setData(product_item.product_type, PRODUCT_TYPE_ROLE) model_item.setData(product_type_icon, PRODUCT_TYPE_ICON_ROLE) model_item.setData(product_item.folder_id, FOLDER_ID_ROLE) diff --git a/client/ayon_core/tools/loader/ui/products_widget.py b/client/ayon_core/tools/loader/ui/products_widget.py index 94d95b9026..67116ad544 100644 --- a/client/ayon_core/tools/loader/ui/products_widget.py +++ b/client/ayon_core/tools/loader/ui/products_widget.py @@ -142,6 +142,7 @@ class ProductsWidget(QtWidgets.QWidget): default_widths = ( 200, # Product name 90, # Product type + 90, # Product base type 130, # Folder label 60, # Version 100, # Status From a3357a3ace0039a171c510a1e6a30ab19a231bea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Fri, 6 Jun 2025 14:22:02 +0200 Subject: [PATCH 033/155] :dog: fix some linting issue --- client/ayon_core/tools/loader/ui/products_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/loader/ui/products_model.py b/client/ayon_core/tools/loader/ui/products_model.py index 24050fc0c1..8b8d4a67bf 100644 --- a/client/ayon_core/tools/loader/ui/products_model.py +++ b/client/ayon_core/tools/loader/ui/products_model.py @@ -16,7 +16,7 @@ TASK_ID_ROLE = QtCore.Qt.UserRole + 5 PRODUCT_ID_ROLE = QtCore.Qt.UserRole + 6 PRODUCT_NAME_ROLE = QtCore.Qt.UserRole + 7 PRODUCT_TYPE_ROLE = QtCore.Qt.UserRole + 8 -PRODUCT_BASE_TYPE_ROLE = QtCore.Qt.UserRole + 9 +PRODUCT_BASE_TYPE_ROLE = QtCore.Qt.UserRole + 9 PRODUCT_TYPE_ICON_ROLE = QtCore.Qt.UserRole + 10 PRODUCT_IN_SCENE_ROLE = QtCore.Qt.UserRole + 11 VERSION_ID_ROLE = QtCore.Qt.UserRole + 12 From 98eb281adc2533548fa807d23862e93e675299a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Fri, 6 Jun 2025 14:22:25 +0200 Subject: [PATCH 034/155] :recycle: hide product base types if support is disabled --- client/ayon_core/tools/loader/ui/products_widget.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/ayon_core/tools/loader/ui/products_widget.py b/client/ayon_core/tools/loader/ui/products_widget.py index 67116ad544..511c346cb9 100644 --- a/client/ayon_core/tools/loader/ui/products_widget.py +++ b/client/ayon_core/tools/loader/ui/products_widget.py @@ -4,6 +4,7 @@ from typing import Optional from qtpy import QtWidgets, QtCore +from ayon_core.pipeline.compatibility import is_supporting_product_base_type from ayon_core.tools.utils import ( RecursiveSortFilterProxyModel, DeselectableTreeView, @@ -262,6 +263,12 @@ class ProductsWidget(QtWidgets.QWidget): self._controller.is_sitesync_enabled() ) + if not is_supporting_product_base_type(): + # Hide product base type column + products_view.setColumnHidden( + products_model.product_base_type_col, True + ) + def set_name_filter(self, name): """Set filter of product name. From 74ed8bf2bb6b84499c48b395ab6ff70dacf1f2e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 9 Jun 2025 13:52:13 +0200 Subject: [PATCH 035/155] :recycle: refactor support check function name --- client/ayon_core/tools/loader/ui/products_widget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/products_widget.py b/client/ayon_core/tools/loader/ui/products_widget.py index 511c346cb9..47046d5ec2 100644 --- a/client/ayon_core/tools/loader/ui/products_widget.py +++ b/client/ayon_core/tools/loader/ui/products_widget.py @@ -4,7 +4,7 @@ from typing import Optional from qtpy import QtWidgets, QtCore -from ayon_core.pipeline.compatibility import is_supporting_product_base_type +from ayon_core.pipeline.compatibility import is_product_base_type_supported from ayon_core.tools.utils import ( RecursiveSortFilterProxyModel, DeselectableTreeView, @@ -263,7 +263,7 @@ class ProductsWidget(QtWidgets.QWidget): self._controller.is_sitesync_enabled() ) - if not is_supporting_product_base_type(): + if not is_product_base_type_supported(): # Hide product base type column products_view.setColumnHidden( products_model.product_base_type_col, True From ceef6876e92691cb435031db8092cb6b14f8ee6c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Jun 2025 17:27:27 +0200 Subject: [PATCH 036/155] base implementation of search bar --- client/ayon_core/style/style.css | 40 ++ .../ayon_core/tools/loader/ui/search_bar.py | 637 ++++++++++++++++++ client/ayon_core/tools/loader/ui/window.py | 150 +++-- 3 files changed, 786 insertions(+), 41 deletions(-) create mode 100644 client/ayon_core/tools/loader/ui/search_bar.py diff --git a/client/ayon_core/style/style.css b/client/ayon_core/style/style.css index 0e19702d53..9270ddee30 100644 --- a/client/ayon_core/style/style.css +++ b/client/ayon_core/style/style.css @@ -862,6 +862,46 @@ HintedLineEditButton { border-radius: 0.1em; } +/* Launcher specific stylesheets */ +FiltersBar { + background: {color:bg-inputs}; + border-radius: 5px; +} + +FiltersBar #SearchButton { + background: transparent; +} + +FiltersPopup #PopupWrapper, FilterValuePopup #PopupWrapper { + border-radius: 5px; + background: {color:bg-inputs}; +} + +FilterItemButton, FilterValueItemWidget { + border-radius: 5px; + background: transparent; +} +FilterItemButton:hover, FilterValueItemWidget:hover { + background: {color:bg-buttons-hover}; +} +FilterValueItemWidget[selected="1"] { + background: {color:bg-view-selection}; +} +FilterValueItemWidget[selected="1"]:hover { + background: {color:bg-view-selection-hover}; +} +SearchItemDisplayWidget { + border-radius: 5px; +} +SearchItemDisplayWidget #CloseButton { + background: transparent; + border-radius: 5px; +} +SearchItemDisplayWidget #ValueWidget { + border-radius: 3px; + background: {color:bg-buttons}; +} + /* Subset Manager */ #SubsetManagerDetailsText {} #SubsetManagerDetailsText[state="invalid"] { diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py new file mode 100644 index 0000000000..72c16e5566 --- /dev/null +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -0,0 +1,637 @@ +import copy +import uuid +from dataclasses import dataclass +from typing import Any, Optional + +from qtpy import QtCore, QtWidgets + +from ayon_core.style import load_stylesheet +from ayon_core.tools.utils import ( + get_qt_icon, + SquareButton, + BaseClickableFrame, + ClickableFrame, + PixmapLabel, +) + + +@dataclass +class FilterDefinition: + """Search bar definition. + + Attributes: + name (str): Name of the definition. + title (str): Title of the search bar. + icon (str): Icon name for the search bar. + placeholder (str): Placeholder text for the search bar. + + """ + name: str + title: str + filter_type: str + icon: Optional[dict[str, Any]] = None + placeholder: Optional[str] = None + items: Optional[list[dict[str, str]]] = None + + +class SearchItemDisplayWidget(QtWidgets.QFrame): + close_requested = QtCore.Signal(str) + edit_requested = QtCore.Signal(str) + + def __init__( + self, + filter_def: FilterDefinition, + parent: QtWidgets.QWidget, + ): + super().__init__(parent) + + self._filter_def = filter_def + + close_icon = get_qt_icon({ + "type": "material-symbols", + "name": "close", + "color": "#FFFFFF", + }) + + title_widget = QtWidgets.QLabel(f"{filter_def.title}:", self) + + value_wrapper = QtWidgets.QWidget(self) + value_widget = QtWidgets.QLabel(value_wrapper) + value_widget.setObjectName("ValueWidget") + value_widget.setText(6 * " ") + value_layout = QtWidgets.QVBoxLayout(value_wrapper) + value_layout.setContentsMargins(2, 2, 2, 2) + value_layout.addWidget(value_widget) + + close_btn = SquareButton(self) + close_btn.setObjectName("CloseButton") + close_btn.setIcon(close_icon) + close_btn.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(4, 0, 0, 0) + main_layout.setSpacing(0) + main_layout.addWidget(title_widget, 0) + main_layout.addWidget(value_wrapper, 0) + main_layout.addWidget(close_btn, 0) + + close_btn.clicked.connect(self._on_remove_clicked) + + self._value_wrapper = value_wrapper + self._value_widget = value_widget + self._value = None + + def set_value(self, value: "str | list[str]"): + text = "" + if isinstance(value, str): + text = value + elif len(value) == 1: + text = value[0] + elif len(value) > 1: + text = str(len(value)) + + if len(text) > 9: + text = text[:9] + "..." + + text = " " + text + " " + text_diff = 4 - len(text) + if text_diff > 0: + text = " " * text_diff + text + + self._value = copy.deepcopy(value) + self._value_widget.setText(text) + + def get_value(self): + return copy.deepcopy(self._value) + + def _on_remove_clicked(self): + self.close_requested.emit(self._filter_def.name) + + def _request_edit(self): + self.edit_requested.emit(self._filter_def.name) + + +class FilterItemButton(BaseClickableFrame): + filter_requested = QtCore.Signal(str) + + def __init__( + self, + filter_def: FilterDefinition, + parent: QtWidgets.QWidget, + ): + super().__init__(parent) + + self._filter_def = filter_def + + title_widget = QtWidgets.QLabel(filter_def.title, self) + title_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(5, 5, 5, 5) + main_layout.addWidget(title_widget, 1) + + def _mouse_release_callback(self): + """Handle mouse release event to emit filter request.""" + self.filter_requested.emit(self._filter_def.name) + + +class FiltersPopup(QtWidgets.QWidget): + filter_requested = QtCore.Signal(str) + + def __init__(self, parent): + super().__init__(parent) + self.setWindowFlags(QtCore.Qt.Popup | QtCore.Qt.FramelessWindowHint) + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + wrapper = QtWidgets.QWidget(self) + wrapper.setObjectName("PopupWrapper") + + wraper_layout = QtWidgets.QVBoxLayout(wrapper) + wraper_layout.setContentsMargins(5, 5, 5, 5) + wraper_layout.setSpacing(0) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(wrapper) + + self._wrapper = wrapper + self._wrapper_layout = wraper_layout + self._preferred_width = None + + def set_preferred_width(self, width: int): + self._preferred_width = width + + def sizeHint(self): + sh = super().sizeHint() + if self._preferred_width is not None: + sh.setWidth(self._preferred_width) + return sh + + def set_filter_items(self, filter_items): + while self._wrapper_layout.count() > 0: + item = self._wrapper_layout.takeAt(0) + widget = item.widget() + if widget is not None: + widget.setVisible(False) + widget.deleteLater() + + for item in filter_items: + widget = FilterItemButton(item, self._wrapper) + widget.filter_requested.connect(self.filter_requested) + self._wrapper_layout.addWidget(widget) + + if self._wrapper_layout.count() == 0: + empty_label = QtWidgets.QLabel( + "No filters available...", self._wrapper + ) + self._wrapper_layout.addWidget(empty_label) + + +class FilterValueItemWidget(BaseClickableFrame): + selected = QtCore.Signal(str) + + def __init__(self, widget_id, value, icon, color, parent): + super().__init__(parent) + + label_widget = QtWidgets.QLabel(str(value), self) + if color: + label_widget.setStyleSheet(f"color: {color};") + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(5, 5, 5, 5) + main_layout.addWidget(label_widget, 1) + + self._icon_widget = None + self._label_widget = label_widget + self._main_layout = main_layout + self._selected = False + self._value = value + self._widget_id = widget_id + + if icon: + self.set_icon(icon) + + def set_icon(self, icon: dict[str, Any]): + """Set the icon for the widget.""" + icon = get_qt_icon(icon) + pixmap = icon.pixmap(64, 64) + if self._icon_widget is None: + self._icon_widget = PixmapLabel(pixmap, self) + self._main_layout.insertWidget(0, self._icon_widget, 0) + else: + self._icon_widget.setPixmap(pixmap) + + def get_value(self): + return self._value + + def set_selected(self, selected: bool): + """Set the selection state of the widget.""" + if self._selected == selected: + return + self._selected = selected + self.setProperty("selected", "1" if selected else "") + self.style().polish(self) + + def is_selected(self) -> bool: + return self._selected + + def _mouse_release_callback(self): + """Handle mouse release event to emit filter request.""" + self.selected.emit(self._widget_id) + + +class FilterValueItemsView(QtWidgets.QWidget): + value_changed = QtCore.Signal() + + def __init__(self, parent): + super().__init__(parent) + self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + + self._multiselection = False + self._main_layout = main_layout + self._last_selected_widget = None + self._widgets_by_id = {} + + def set_value(self, value): + current_value = self.get_value() + if self._multiselection: + if value is None: + value = [] + if not isinstance(value, list): + value = [value] + for widget in self._widgets_by_id.values(): + selected = widget.get_value() in value + if selected and self._last_selected_widget is None: + self._last_selected_widget = widget + widget.set_selected(selected) + + if value != current_value: + self.value_changed.emit() + return + + if isinstance(value, list): + if len(value) > 0: + value = value[0] + else: + value = None + + if value is None: + widget = next(iter(self._widgets_by_id.values())) + value = widget.get_value() + + self._last_selected_widget = None + for widget in self._widgets_by_id.values(): + selected = widget.get_value() in value + widget.set_selected(selected) + if selected: + self._last_selected_widget = widget + + if self._last_selected_widget is None: + widget = next(iter(self._widgets_by_id.values())) + self._last_selected_widget = widget + widget.set_selected(True) + + if value != current_value: + self.value_changed.emit() + + def set_multiselection(self, multiselection: bool): + self._multiselection = multiselection + if not self._widgets_by_id or self._multiselection: + return + + value_changed = False + if self._last_selected_widget is None: + value_changed = True + self._last_selected_widget = next( + iter(self._widgets_by_id.values()) + ) + for widget in self._widgets_by_id.values(): + widget.set_selected(widget is self._last_selected_widget) + + if value_changed: + self.value_changed.emit() + + def get_value(self): + """Get the value from the items view.""" + if self._multiselection: + return [ + widget.get_value() + for widget in self._widgets_by_id.values() + if widget.is_selected() + ] + if self._last_selected_widget is not None: + return self._last_selected_widget.get_value() + return None + + def set_items(self, items: list[dict[str, Any]]): + while self._main_layout.count() > 0: + item = self._main_layout.takeAt(0) + widget = item.widget() + if widget is not None: + widget.setVisible(False) + widget.deleteLater() + self._widgets_by_id = {} + self._last_selected_widget = None + + for item in items: + widget_id = uuid.uuid4().hex + widget = FilterValueItemWidget( + widget_id, + item["value"], + item.get("icon"), + item.get("color"), + self, + ) + widget.selected.connect(self._on_item_clicked) + self._widgets_by_id[widget_id] = widget + self._main_layout.addWidget(widget) + + def _on_item_clicked(self, widget_id): + widget = self._widgets_by_id.get(widget_id) + if widget is None: + return + + previous_widget = self._last_selected_widget + self._last_selected_widget = widget + if self._multiselection: + widget.set_selected(not widget.is_selected()) + else: + widget.set_selected(True) + if previous_widget is not None: + previous_widget.set_selected(False) + self.value_changed.emit() + + +class FilterValuePopup(QtWidgets.QWidget): + value_changed = QtCore.Signal(str) + closed = QtCore.Signal(str) + + def __init__(self, parent): + super().__init__(parent) + self.setWindowFlags(QtCore.Qt.Popup | QtCore.Qt.FramelessWindowHint) + self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) + + wrapper = QtWidgets.QWidget(self) + wrapper.setObjectName("PopupWrapper") + + text_input = QtWidgets.QLineEdit(wrapper) + text_input.setVisible(False) + + items_view = FilterValueItemsView(wrapper) + items_view.setVisible(False) + + wraper_layout = QtWidgets.QVBoxLayout(wrapper) + wraper_layout.setContentsMargins(5, 5, 5, 5) + wraper_layout.addWidget(text_input, 0) + wraper_layout.addWidget(items_view, 0) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(wrapper) + + text_input.textChanged.connect(self._text_changed) + text_input.returnPressed.connect(self._text_confirmed) + + items_view.value_changed.connect(self._selection_changed) + + self._wrapper = wrapper + self._wrapper_layout = wraper_layout + self._text_input = text_input + self._items_view = items_view + + self._active_widget = None + self._filter_name = None + self._preferred_width = None + + def set_preferred_width(self, width: int): + self._preferred_width = width + + def sizeHint(self): + sh = super().sizeHint() + if self._preferred_width is not None: + sh.setWidth(self._preferred_width) + return sh + + def set_filter_item( + self, + filter_def: FilterDefinition, + value, + ): + self._text_input.setVisible(False) + self._items_view.setVisible(False) + self._filter_name = filter_def.name + self._active_widget = None + if filter_def.filter_type == "text": + if filter_def.items: + if value is None: + value = filter_def.items[0]["value"] + self._active_widget = self._items_view + self._items_view.set_items(filter_def.items) + self._items_view.set_multiselection(False) + self._items_view.set_value(value) + else: + if value is None: + value = "" + self._text_input.setPlaceholderText( + filter_def.placeholder or "" + ) + self._text_input.setText(value) + self._active_widget = self._text_input + + elif filter_def.filter_type == "list": + if value is None: + value = [] + self._items_view.set_items(filter_def.items) + self._items_view.set_multiselection(True) + self._items_view.set_value(value) + self._active_widget = self._items_view + + if self._active_widget is not None: + self._active_widget.setVisible(True) + + def showEvent(self, event): + super().showEvent(event) + if self._active_widget is not None: + self._active_widget.setFocus() + + def closeEvent(self, event): + super().closeEvent(event) + self.closed.emit(self._filter_name) + + def hideEvent(self, event): + super().hideEvent(event) + self.closed.emit(self._filter_name) + + def get_value(self): + """Get the value from the active widget.""" + if self._active_widget is self._text_input: + return self._text_input.text() + elif self._active_widget is self._items_view: + return self._active_widget.get_value() + return None + + def _text_changed(self): + """Handle text change in the text input.""" + if self._active_widget == self._text_input: + # Emit value changed signal if text input is active + self.value_changed.emit(self._filter_name) + + def _text_confirmed(self): + self.close() + + def _selection_changed(self): + self.value_changed.emit(self._filter_name) + + +class FiltersBar(ClickableFrame): + filters_changed = QtCore.Signal() + + def __init__(self, parent): + super().__init__(parent) + + search_icon = get_qt_icon({ + "type": "material-symbols", + "name": "search", + "color": "#FFFFFF", + }) + search_btn = SquareButton(self) + search_btn.setIcon(search_icon) + search_btn.setFlat(True) + search_btn.setObjectName("SearchButton") + + filters_widget = QtWidgets.QWidget(self) + filters_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) + filters_layout = QtWidgets.QHBoxLayout(filters_widget) + filters_layout.setContentsMargins(0, 0, 0, 0) + filters_layout.addStretch(1) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(4, 4, 4, 4) + main_layout.setSpacing(5) + main_layout.addWidget(search_btn, 0) + main_layout.addWidget(filters_widget, 1) + + search_btn.clicked.connect(self._on_filters_request) + self.clicked.connect(self._on_clicked) + + self._search_btn = search_btn + self._filters_widget = filters_widget + self._filters_layout = filters_layout + self._widgets_by_name = {} + self._filter_defs_by_name = {} + self._filters_popup = FiltersPopup(self) + self._filter_value_popup = FilterValuePopup(self) + + def set_search_items(self, filter_defs: list[FilterDefinition]): + self._filter_defs_by_name = { + filter_def.name: filter_def + for filter_def in filter_defs + } + + def add_item(self, name: str): + """Add a new item to the search bar. + + Args: + name (str): Search definition name. + + """ + filter_def = self._filter_defs_by_name.get(name) + if filter_def is None: + return + + item_widget = self._widgets_by_name.get(name) + if item_widget is not None: + return + + item_widget = SearchItemDisplayWidget( + filter_def, + parent=self._filters_widget, + ) + item_widget.close_requested.connect(self._on_item_close_requested) + self._widgets_by_name[name] = item_widget + idx = self._filters_layout.count() - 1 + self._filters_layout.insertWidget(idx, item_widget, 0) + + def _on_clicked(self): + self._show_filters_popup() + + def _show_filters_popup(self): + filter_defs = [ + filter_def + for filter_def in self._filter_defs_by_name.values() + if filter_def.name not in self._widgets_by_name + ] + filters_popup = FiltersPopup(self) + filters_popup.filter_requested.connect(self._on_filter_request) + filters_popup.set_filter_items(filter_defs) + filters_popup.set_preferred_width(self.width()) + + old_popup, self._filters_popup = self._filters_popup, filters_popup + + self._show_popup(filters_popup) + + old_popup.setVisible(False) + old_popup.deleteLater() + + def _on_filters_request(self): + self._show_filters_popup() + + def _on_filter_request(self, filter_name: str): + """Handle filter request from the popup.""" + self.add_item(filter_name) + self._filters_popup.hide() + filter_def = self._filter_defs_by_name.get(filter_name) + widget = self._widgets_by_name.get(filter_name) + value = None + if widget is not None: + value = widget.get_value() + + filter_value_popup = FilterValuePopup(self) + filter_value_popup.set_preferred_width(self.width()) + filter_value_popup.set_filter_item(filter_def, value) + filter_value_popup.value_changed.connect(self._on_filter_value_change) + filter_value_popup.closed.connect(self._on_filter_value_closed) + + old_popup, self._filter_value_popup = ( + self._filter_value_popup, filter_value_popup + ) + + self._show_popup(filter_value_popup) + self._on_filter_value_change(filter_def.name) + + old_popup.setVisible(False) + old_popup.deleteLater() + + def _show_popup(self, popup: QtWidgets.QWidget): + """Show a popup widget.""" + geo = self.geometry() + bl_pos_g = self.mapToGlobal(QtCore.QPoint(0, geo.height() + 5)) + popup.show() + popup.move(bl_pos_g.x(), bl_pos_g.y()) + popup.raise_() + + def _on_filter_value_change(self, name): + value = self._filter_value_popup.get_value() + item_widget = self._widgets_by_name.get(name) + item_widget.set_value(value) + + def _on_filter_value_closed(self, name): + widget = self._widgets_by_name.get(name) + if widget is None: + return + + value = widget.get_value() + if not value: + self._on_item_close_requested(name) + + def _on_item_close_requested(self, name): + widget = self._widgets_by_name.pop(name, None) + if widget is not None: + idx = self._filters_layout.indexOf(widget) + if idx > -1: + self._filters_layout.takeAt(idx) + widget.setVisible(False) + widget.deleteLater() diff --git a/client/ayon_core/tools/loader/ui/window.py b/client/ayon_core/tools/loader/ui/window.py index b70f5554c7..580934f2b2 100644 --- a/client/ayon_core/tools/loader/ui/window.py +++ b/client/ayon_core/tools/loader/ui/window.py @@ -11,6 +11,8 @@ from ayon_core.tools.utils import ( ) from ayon_core.tools.utils.lib import center_window from ayon_core.tools.utils import ProjectsCombobox +from ayon_core.tools.common_models import StatusItem +from ayon_core.tools.loader.abstract import ProductTypeItem from ayon_core.tools.loader.control import LoaderController from .folders_widget import LoaderFoldersWidget @@ -21,6 +23,7 @@ from .product_group_dialog import ProductGroupDialog from .info_widget import InfoWidget from .repres_widget import RepresentationsWidget from .statuses_combo import StatusesCombobox +from .search_bar import FiltersBar, FilterDefinition class LoadErrorMessageBox(ErrorMessageBox): @@ -182,32 +185,34 @@ class LoaderWindow(QtWidgets.QWidget): products_wrap_widget = QtWidgets.QWidget(main_splitter) products_inputs_widget = QtWidgets.QWidget(products_wrap_widget) - - products_filter_input = PlaceholderLineEdit(products_inputs_widget) - products_filter_input.setPlaceholderText("Product name filter...") - - product_types_filter_combo = ProductTypesCombobox( - controller, products_inputs_widget - ) - - product_status_filter_combo = StatusesCombobox(controller, self) + search_bar = FiltersBar(products_inputs_widget) + # + # products_filter_input = PlaceholderLineEdit(products_inputs_widget) + # products_filter_input.setPlaceholderText("Product name filter...") + # + # product_types_filter_combo = ProductTypesCombobox( + # controller, products_inputs_widget + # ) + # + # product_status_filter_combo = StatusesCombobox(controller, self) product_group_checkbox = QtWidgets.QCheckBox( "Enable grouping", products_inputs_widget) product_group_checkbox.setChecked(True) - products_widget = ProductsWidget(controller, products_wrap_widget) - products_inputs_layout = QtWidgets.QHBoxLayout(products_inputs_widget) products_inputs_layout.setContentsMargins(0, 0, 0, 0) - products_inputs_layout.addWidget(products_filter_input, 1) - products_inputs_layout.addWidget(product_types_filter_combo, 1) - products_inputs_layout.addWidget(product_status_filter_combo, 1) + # products_inputs_layout.addWidget(products_filter_input, 1) + # products_inputs_layout.addWidget(product_types_filter_combo, 1) + # products_inputs_layout.addWidget(product_status_filter_combo, 1) + products_inputs_layout.addWidget(search_bar, 1) products_inputs_layout.addWidget(product_group_checkbox, 0) + products_widget = ProductsWidget(controller, products_wrap_widget) + products_wrap_layout = QtWidgets.QVBoxLayout(products_wrap_widget) products_wrap_layout.setContentsMargins(0, 0, 0, 0) - products_wrap_layout.addWidget(products_inputs_widget, 0) + products_wrap_layout.addWidget(search_bar, 0) products_wrap_layout.addWidget(products_widget, 1) right_panel_splitter = QtWidgets.QSplitter(main_splitter) @@ -250,15 +255,16 @@ class LoaderWindow(QtWidgets.QWidget): folders_filter_input.textChanged.connect( self._on_folder_filter_change ) - products_filter_input.textChanged.connect( - self._on_product_filter_change - ) - product_types_filter_combo.value_changed.connect( - self._on_product_type_filter_change - ) - product_status_filter_combo.value_changed.connect( - self._on_status_filter_change - ) + search_bar.filters_changed.connect(self._on_filters_change) + # products_filter_input.textChanged.connect( + # self._on_product_filter_change + # ) + # product_types_filter_combo.value_changed.connect( + # self._on_product_type_filter_change + # ) + # product_status_filter_combo.value_changed.connect( + # self._on_status_filter_change + # ) product_group_checkbox.stateChanged.connect( self._on_product_group_change ) @@ -316,9 +322,10 @@ class LoaderWindow(QtWidgets.QWidget): self._tasks_widget = tasks_widget - self._products_filter_input = products_filter_input - self._product_types_filter_combo = product_types_filter_combo - self._product_status_filter_combo = product_status_filter_combo + self._search_bar = search_bar + # self._products_filter_input = products_filter_input + # self._product_types_filter_combo = product_types_filter_combo + # self._product_status_filter_combo = product_status_filter_combo self._product_group_checkbox = product_group_checkbox self._products_widget = products_widget @@ -344,6 +351,7 @@ class LoaderWindow(QtWidgets.QWidget): def refresh(self): self._reset_on_show = False self._controller.reset() + self._update_filters() def showEvent(self, event): super().showEvent(event) @@ -356,11 +364,11 @@ class LoaderWindow(QtWidgets.QWidget): def closeEvent(self, event): super().closeEvent(event) - ( - self - ._product_types_filter_combo - .reset_product_types_filter_on_refresh() - ) + # ( + # self + # ._product_types_filter_combo + # .reset_product_types_filter_on_refresh() + # ) self._reset_on_show = True @@ -435,19 +443,22 @@ class LoaderWindow(QtWidgets.QWidget): self._product_group_checkbox.isChecked() ) - def _on_product_filter_change(self, text): - self._products_widget.set_name_filter(text) + def _on_filters_change(self): + pass def _on_tasks_selection_change(self, event): self._products_widget.set_tasks_filter(event["task_ids"]) - def _on_status_filter_change(self): - status_names = self._product_status_filter_combo.get_value() - self._products_widget.set_statuses_filter(status_names) - - def _on_product_type_filter_change(self): - product_types = self._product_types_filter_combo.get_value() - self._products_widget.set_product_type_filter(product_types) + # def _on_product_filter_change(self, text): + # self._products_widget.set_name_filter(text) + # + # def _on_status_filter_change(self): + # status_names = self._product_status_filter_combo.get_value() + # self._products_widget.set_statuses_filter(status_names) + # + # def _on_product_type_filter_change(self): + # product_types = self._product_types_filter_combo.get_value() + # self._products_widget.set_product_type_filter(product_types) def _on_merged_products_selection_change(self): items = self._products_widget.get_selected_merged_products() @@ -491,6 +502,63 @@ class LoaderWindow(QtWidgets.QWidget): def _on_project_selection_changed(self, event): self._selected_project_name = event["project_name"] + self._update_filters() + + def _update_filters(self): + project_name = self._selected_project_name + product_type_items: list[ProductTypeItem] = [] + status_items: list[StatusItem] = [] + if project_name: + product_type_items = self._controller.get_product_type_items( + project_name + ) + status_items = self._controller.get_project_status_items( + project_name + ) + + filter_product_type_items = [ + { + "value": item.name, + "icon": item.icon, + } + for item in product_type_items + ] + filter_status_items = [ + { + "icon": { + "type": "material-symbols", + "name": status_item.icon, + "color": status_item.color + }, + "value": status_item.name, + } + for status_item in status_items + ] + + self._search_bar.set_search_items([ + FilterDefinition( + name="product_name", + title="Product name", + filter_type="text", + icon=None, + placeholder="Product name filter...", + items=None, + ), + FilterDefinition( + name="product_types", + title="Product type", + filter_type="list", + icon=None, + items=filter_product_type_items, + ), + FilterDefinition( + name="statuses", + title="Statuses", + filter_type="list", + icon=None, + items=filter_status_items, + ), + ]) def _on_folders_selection_changed(self, event): self._selected_folder_ids = set(event["folder_ids"]) From 98fbeb96ec5cdc661d26565c12dbab4569df05a1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Jun 2025 17:31:56 +0200 Subject: [PATCH 037/155] set status color to text --- client/ayon_core/tools/loader/ui/window.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/tools/loader/ui/window.py b/client/ayon_core/tools/loader/ui/window.py index 580934f2b2..f236a1e3ae 100644 --- a/client/ayon_core/tools/loader/ui/window.py +++ b/client/ayon_core/tools/loader/ui/window.py @@ -530,6 +530,7 @@ class LoaderWindow(QtWidgets.QWidget): "name": status_item.icon, "color": status_item.color }, + "color": status_item.color, "value": status_item.name, } for status_item in status_items From 2c145ca30e52ea0884345598d581da144c679765 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 11 Jun 2025 10:00:28 +0200 Subject: [PATCH 038/155] added scroll area to popups --- client/ayon_core/style/style.css | 18 ++++---- .../ayon_core/tools/loader/ui/search_bar.py | 43 +++++++++++++------ 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/client/ayon_core/style/style.css b/client/ayon_core/style/style.css index 9270ddee30..adf27a0e03 100644 --- a/client/ayon_core/style/style.css +++ b/client/ayon_core/style/style.css @@ -868,6 +868,9 @@ FiltersBar { border-radius: 5px; } +FiltersBar #ScrollArea { + background: {color:bg-inputs}; +} FiltersBar #SearchButton { background: transparent; } @@ -877,24 +880,23 @@ FiltersPopup #PopupWrapper, FilterValuePopup #PopupWrapper { background: {color:bg-inputs}; } -FilterItemButton, FilterValueItemWidget { +FilterItemButton, FilterValueItemButton { border-radius: 5px; background: transparent; } -FilterItemButton:hover, FilterValueItemWidget:hover { +FilterItemButton:hover, FilterValueItemButton:hover { background: {color:bg-buttons-hover}; } -FilterValueItemWidget[selected="1"] { +FilterValueItemButton[selected="1"] { background: {color:bg-view-selection}; } -FilterValueItemWidget[selected="1"]:hover { +FilterValueItemButton[selected="1"]:hover { background: {color:bg-view-selection-hover}; } -SearchItemDisplayWidget { - border-radius: 5px; +FilterValueItemsView #ContentWidget { + background: {color:bg-inputs}; } -SearchItemDisplayWidget #CloseButton { - background: transparent; +SearchItemDisplayWidget { border-radius: 5px; } SearchItemDisplayWidget #ValueWidget { diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py index 72c16e5566..d1946d4ab3 100644 --- a/client/ayon_core/tools/loader/ui/search_bar.py +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -124,12 +124,13 @@ class FilterItemButton(BaseClickableFrame): self._filter_def = filter_def title_widget = QtWidgets.QLabel(filter_def.title, self) - title_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) main_layout = QtWidgets.QHBoxLayout(self) main_layout.setContentsMargins(5, 5, 5, 5) main_layout.addWidget(title_widget, 1) + self._title_widget = title_widget + def _mouse_release_callback(self): """Handle mouse release event to emit filter request.""" self.filter_requested.emit(self._filter_def.name) @@ -148,7 +149,7 @@ class FiltersPopup(QtWidgets.QWidget): wraper_layout = QtWidgets.QVBoxLayout(wrapper) wraper_layout.setContentsMargins(5, 5, 5, 5) - wraper_layout.setSpacing(0) + wraper_layout.setSpacing(5) main_layout = QtWidgets.QHBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) @@ -187,22 +188,22 @@ class FiltersPopup(QtWidgets.QWidget): self._wrapper_layout.addWidget(empty_label) -class FilterValueItemWidget(BaseClickableFrame): +class FilterValueItemButton(BaseClickableFrame): selected = QtCore.Signal(str) def __init__(self, widget_id, value, icon, color, parent): super().__init__(parent) - label_widget = QtWidgets.QLabel(str(value), self) + title_widget = QtWidgets.QLabel(str(value), self) if color: - label_widget.setStyleSheet(f"color: {color};") + title_widget.setStyleSheet(f"color: {color};") main_layout = QtWidgets.QHBoxLayout(self) main_layout.setContentsMargins(5, 5, 5, 5) - main_layout.addWidget(label_widget, 1) + main_layout.addWidget(title_widget, 1) self._icon_widget = None - self._label_widget = label_widget + self._title_widget = title_widget self._main_layout = main_layout self._selected = False self._value = value @@ -245,13 +246,28 @@ class FilterValueItemsView(QtWidgets.QWidget): def __init__(self, parent): super().__init__(parent) - self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) + + scroll_area = QtWidgets.QScrollArea(self) + scroll_area.setObjectName("ScrollArea") + srcoll_viewport = scroll_area.viewport() + srcoll_viewport.setContentsMargins(0, 0, 0, 0) + scroll_area.setWidgetResizable(True) + scroll_area.setMinimumHeight(20) + scroll_area.setMaximumHeight(400) + + content_widget = QtWidgets.QWidget(scroll_area) + content_widget.setObjectName("ContentWidget") + content_layout = QtWidgets.QVBoxLayout(content_widget) + content_layout.setContentsMargins(0, 0, 0, 0) + + scroll_area.setWidget(content_widget) main_layout = QtWidgets.QVBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(scroll_area) self._multiselection = False - self._main_layout = main_layout + self._content_layout = content_layout self._last_selected_widget = None self._widgets_by_id = {} @@ -327,8 +343,8 @@ class FilterValueItemsView(QtWidgets.QWidget): return None def set_items(self, items: list[dict[str, Any]]): - while self._main_layout.count() > 0: - item = self._main_layout.takeAt(0) + while self._content_layout.count() > 0: + item = self._content_layout.takeAt(0) widget = item.widget() if widget is not None: widget.setVisible(False) @@ -338,7 +354,7 @@ class FilterValueItemsView(QtWidgets.QWidget): for item in items: widget_id = uuid.uuid4().hex - widget = FilterValueItemWidget( + widget = FilterValueItemButton( widget_id, item["value"], item.get("icon"), @@ -347,7 +363,7 @@ class FilterValueItemsView(QtWidgets.QWidget): ) widget.selected.connect(self._on_item_clicked) self._widgets_by_id[widget_id] = widget - self._main_layout.addWidget(widget) + self._content_layout.addWidget(widget) def _on_item_clicked(self, widget_id): widget = self._widgets_by_id.get(widget_id) @@ -385,6 +401,7 @@ class FilterValuePopup(QtWidgets.QWidget): wraper_layout = QtWidgets.QVBoxLayout(wrapper) wraper_layout.setContentsMargins(5, 5, 5, 5) + wraper_layout.setSpacing(5) wraper_layout.addWidget(text_input, 0) wraper_layout.addWidget(items_view, 0) From 2845ac39b47f17bf5e544be81c090ed3cdd0ec5d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 11 Jun 2025 10:00:49 +0200 Subject: [PATCH 039/155] enhanced close button --- .../ayon_core/tools/loader/ui/search_bar.py | 57 +++++++++++++++---- 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py index d1946d4ab3..a9dea848bd 100644 --- a/client/ayon_core/tools/loader/ui/search_bar.py +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -3,9 +3,9 @@ import uuid from dataclasses import dataclass from typing import Any, Optional -from qtpy import QtCore, QtWidgets +from qtpy import QtCore, QtWidgets, QtGui -from ayon_core.style import load_stylesheet +from ayon_core.style import load_stylesheet, get_objected_colors from ayon_core.tools.utils import ( get_qt_icon, SquareButton, @@ -34,7 +34,49 @@ class FilterDefinition: items: Optional[list[dict[str, str]]] = None +class CloseButton(SquareButton): + """Close button for search item display widget.""" + _icon = None + _hover_color = None + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.__class__._icon is None: + self.__class__._icon = get_qt_icon({ + "type": "material-symbols", + "name": "close", + "color": "#FFFFFF", + }) + if self.__class__._hover_color is None: + color = get_objected_colors("bg-view-selection-hover") + self.__class__._hover_color = color.get_qcolor() + + self.setIcon(self.__class__._icon) + + def paintEvent(self, event): + """Override paint event to draw a close button.""" + painter = QtWidgets.QStylePainter(self) + painter.setRenderHint(QtGui.QPainter.Antialiasing) + option = QtWidgets.QStyleOptionButton() + self.initStyleOption(option) + icon = self.icon() + size = min(self.width(), self.height()) + rect = QtCore.QRect(0, 0, size, size) + rect.adjust(2, 2, -2, -2) + painter.setPen(QtCore.Qt.NoPen) + bg_color = QtCore.Qt.transparent + if option.state & QtWidgets.QStyle.State_MouseOver: + bg_color = self._hover_color + + painter.setBrush(bg_color) + painter.setClipRect(event.rect()) + painter.drawEllipse(rect) + rect.adjust(2, 2, -2, -2) + icon.paint(painter, rect) + + class SearchItemDisplayWidget(QtWidgets.QFrame): + """Widget displaying a set filter in the bar.""" close_requested = QtCore.Signal(str) edit_requested = QtCore.Signal(str) @@ -47,12 +89,6 @@ class SearchItemDisplayWidget(QtWidgets.QFrame): self._filter_def = filter_def - close_icon = get_qt_icon({ - "type": "material-symbols", - "name": "close", - "color": "#FFFFFF", - }) - title_widget = QtWidgets.QLabel(f"{filter_def.title}:", self) value_wrapper = QtWidgets.QWidget(self) @@ -63,10 +99,7 @@ class SearchItemDisplayWidget(QtWidgets.QFrame): value_layout.setContentsMargins(2, 2, 2, 2) value_layout.addWidget(value_widget) - close_btn = SquareButton(self) - close_btn.setObjectName("CloseButton") - close_btn.setIcon(close_icon) - close_btn.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) + close_btn = CloseButton(self) main_layout = QtWidgets.QHBoxLayout(self) main_layout.setContentsMargins(4, 0, 0, 0) From 02d26f9d2d7b3551ec125276d792874214eb2693 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 11 Jun 2025 10:01:00 +0200 Subject: [PATCH 040/155] avoid fake spacing --- client/ayon_core/tools/loader/ui/search_bar.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py index a9dea848bd..27d65bee30 100644 --- a/client/ayon_core/tools/loader/ui/search_bar.py +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -94,7 +94,7 @@ class SearchItemDisplayWidget(QtWidgets.QFrame): value_wrapper = QtWidgets.QWidget(self) value_widget = QtWidgets.QLabel(value_wrapper) value_widget.setObjectName("ValueWidget") - value_widget.setText(6 * " ") + value_widget.setText("") value_layout = QtWidgets.QVBoxLayout(value_wrapper) value_layout.setContentsMargins(2, 2, 2, 2) value_layout.addWidget(value_widget) @@ -116,20 +116,19 @@ class SearchItemDisplayWidget(QtWidgets.QFrame): def set_value(self, value: "str | list[str]"): text = "" + ellide = True if isinstance(value, str): text = value elif len(value) == 1: text = value[0] elif len(value) > 1: - text = str(len(value)) + ellide = False + text = f"Items: {len(value)}" - if len(text) > 9: + if ellide and len(text) > 9: text = text[:9] + "..." text = " " + text + " " - text_diff = 4 - len(text) - if text_diff > 0: - text = " " * text_diff + text self._value = copy.deepcopy(value) self._value_widget.setText(text) From 2da85dd1f4ac5c07182d12f79e2d9243f5547d6a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 11 Jun 2025 11:08:22 +0200 Subject: [PATCH 041/155] added shadow --- client/ayon_core/style/style.css | 5 +++ .../ayon_core/tools/loader/ui/search_bar.py | 43 +++++++++++++++++-- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/style/style.css b/client/ayon_core/style/style.css index adf27a0e03..525cf28633 100644 --- a/client/ayon_core/style/style.css +++ b/client/ayon_core/style/style.css @@ -880,6 +880,11 @@ FiltersPopup #PopupWrapper, FilterValuePopup #PopupWrapper { background: {color:bg-inputs}; } +FiltersPopup #ShadowFrame, FilterValuePopup #ShadowFrame { + border-radius: 5px; + background: rgba(0, 0, 0, 0.5); +} + FilterItemButton, FilterValueItemButton { border-radius: 5px; background: transparent; diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py index 27d65bee30..ac0718f988 100644 --- a/client/ayon_core/tools/loader/ui/search_bar.py +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -176,6 +176,9 @@ class FiltersPopup(QtWidgets.QWidget): self.setWindowFlags(QtCore.Qt.Popup | QtCore.Qt.FramelessWindowHint) self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + shadow_frame = QtWidgets.QFrame(self) + shadow_frame.setObjectName("ShadowFrame") + wrapper = QtWidgets.QWidget(self) wrapper.setObjectName("PopupWrapper") @@ -184,9 +187,12 @@ class FiltersPopup(QtWidgets.QWidget): wraper_layout.setSpacing(5) main_layout = QtWidgets.QHBoxLayout(self) - main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setContentsMargins(2, 2, 2, 2) main_layout.addWidget(wrapper) + shadow_frame.stackUnder(wrapper) + + self._shadow_frame = shadow_frame self._wrapper = wrapper self._wrapper_layout = wraper_layout self._preferred_width = None @@ -211,13 +217,26 @@ class FiltersPopup(QtWidgets.QWidget): for item in filter_items: widget = FilterItemButton(item, self._wrapper) widget.filter_requested.connect(self.filter_requested) - self._wrapper_layout.addWidget(widget) + self._wrapper_layout.addWidget(widget, 0) if self._wrapper_layout.count() == 0: empty_label = QtWidgets.QLabel( "No filters available...", self._wrapper ) - self._wrapper_layout.addWidget(empty_label) + self._wrapper_layout.addWidget(empty_label, 0) + + def showEvent(self, event): + super().showEvent(event) + self._update_shadow() + + def resizeEvent(self, event): + super().resizeEvent(event) + self._update_shadow() + + def _update_shadow(self): + geo = self.geometry() + geo.moveTopLeft(QtCore.QPoint(0, 0)) + self._shadow_frame.setGeometry(geo) class FilterValueItemButton(BaseClickableFrame): @@ -422,6 +441,9 @@ class FilterValuePopup(QtWidgets.QWidget): self.setWindowFlags(QtCore.Qt.Popup | QtCore.Qt.FramelessWindowHint) self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) + shadow_frame = QtWidgets.QFrame(self) + shadow_frame.setObjectName("ShadowFrame") + wrapper = QtWidgets.QWidget(self) wrapper.setObjectName("PopupWrapper") @@ -438,7 +460,7 @@ class FilterValuePopup(QtWidgets.QWidget): wraper_layout.addWidget(items_view, 0) main_layout = QtWidgets.QHBoxLayout(self) - main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setContentsMargins(2, 2, 2, 2) main_layout.addWidget(wrapper) text_input.textChanged.connect(self._text_changed) @@ -446,6 +468,9 @@ class FilterValuePopup(QtWidgets.QWidget): items_view.value_changed.connect(self._selection_changed) + shadow_frame.stackUnder(wrapper) + + self._shadow_frame = shadow_frame self._wrapper = wrapper self._wrapper_layout = wraper_layout self._text_input = text_input @@ -505,6 +530,7 @@ class FilterValuePopup(QtWidgets.QWidget): super().showEvent(event) if self._active_widget is not None: self._active_widget.setFocus() + self._update_shadow() def closeEvent(self, event): super().closeEvent(event) @@ -514,6 +540,15 @@ class FilterValuePopup(QtWidgets.QWidget): super().hideEvent(event) self.closed.emit(self._filter_name) + def resizeEvent(self, event): + super().resizeEvent(event) + self._update_shadow() + + def _update_shadow(self): + geo = self.geometry() + geo.moveTopLeft(QtCore.QPoint(0, 0)) + self._shadow_frame.setGeometry(geo) + def get_value(self): """Get the value from the active widget.""" if self._active_widget is self._text_input: From fc9945959043f2aeba7116635afc5693ab5f9b13 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 11 Jun 2025 11:08:34 +0200 Subject: [PATCH 042/155] make filters empty --- client/ayon_core/tools/loader/ui/window.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/ayon_core/tools/loader/ui/window.py b/client/ayon_core/tools/loader/ui/window.py index f236a1e3ae..7a3d198ab6 100644 --- a/client/ayon_core/tools/loader/ui/window.py +++ b/client/ayon_core/tools/loader/ui/window.py @@ -506,6 +506,10 @@ class LoaderWindow(QtWidgets.QWidget): def _update_filters(self): project_name = self._selected_project_name + if not project_name: + self._search_bar.set_search_items([]) + return + product_type_items: list[ProductTypeItem] = [] status_items: list[StatusItem] = [] if project_name: From 7ae9fa9cf5b2dff79bd582b4aeaa4b51c77b741a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 11 Jun 2025 11:14:10 +0200 Subject: [PATCH 043/155] add no items to select from item and stretch --- client/ayon_core/tools/loader/ui/search_bar.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py index ac0718f988..c19a3a1e18 100644 --- a/client/ayon_core/tools/loader/ui/search_bar.py +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -414,7 +414,14 @@ class FilterValueItemsView(QtWidgets.QWidget): ) widget.selected.connect(self._on_item_clicked) self._widgets_by_id[widget_id] = widget - self._content_layout.addWidget(widget) + self._content_layout.addWidget(widget, 0) + + if self._content_layout.count() == 0: + empty_label = QtWidgets.QLabel( + "No items to select from...", self + ) + self._content_layout.addWidget(empty_label, 0) + self._content_layout.addStretch(1) def _on_item_clicked(self, widget_id): widget = self._widgets_by_id.get(widget_id) From 9961f203c50b440b8d8a1146e9e7fccc72eb0949 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 11 Jun 2025 11:31:00 +0200 Subject: [PATCH 044/155] handle edit requests --- client/ayon_core/tools/loader/ui/search_bar.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py index c19a3a1e18..eba7d8f7cb 100644 --- a/client/ayon_core/tools/loader/ui/search_bar.py +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -641,6 +641,7 @@ class FiltersBar(ClickableFrame): filter_def, parent=self._filters_widget, ) + item_widget.edit_requested.connect(self._on_filter_request) item_widget.close_requested.connect(self._on_item_close_requested) self._widgets_by_name[name] = item_widget idx = self._filters_layout.count() - 1 @@ -662,11 +663,12 @@ class FiltersBar(ClickableFrame): old_popup, self._filters_popup = self._filters_popup, filters_popup - self._show_popup(filters_popup) - + self._filter_value_popup.setVisible(False) old_popup.setVisible(False) old_popup.deleteLater() + self._show_popup(filters_popup) + def _on_filters_request(self): self._show_filters_popup() @@ -690,12 +692,14 @@ class FiltersBar(ClickableFrame): self._filter_value_popup, filter_value_popup ) - self._show_popup(filter_value_popup) - self._on_filter_value_change(filter_def.name) - old_popup.setVisible(False) old_popup.deleteLater() + self._filters_popup.setVisible(False) + + self._show_popup(filter_value_popup) + self._on_filter_value_change(filter_def.name) + def _show_popup(self, popup: QtWidgets.QWidget): """Show a popup widget.""" geo = self.geometry() From bbeaad95a0bd15d09b4b5e79e4a49d9f552e6f88 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 11 Jun 2025 11:40:16 +0200 Subject: [PATCH 045/155] wrap filters widget --- .../ayon_core/tools/loader/ui/search_bar.py | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py index eba7d8f7cb..ffd08e71a1 100644 --- a/client/ayon_core/tools/loader/ui/search_bar.py +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -593,7 +593,13 @@ class FiltersBar(ClickableFrame): search_btn.setFlat(True) search_btn.setObjectName("SearchButton") - filters_widget = QtWidgets.QWidget(self) + # Wrapper is used to avoid squashing filters + # - the filters are positioned manually without layout + filters_wrap = QtWidgets.QWidget(self) + filters_wrap.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) + + # Widget where set filters are displayed + filters_widget = QtWidgets.QWidget(filters_wrap) filters_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) filters_layout = QtWidgets.QHBoxLayout(filters_widget) filters_layout.setContentsMargins(0, 0, 0, 0) @@ -603,12 +609,13 @@ class FiltersBar(ClickableFrame): main_layout.setContentsMargins(4, 4, 4, 4) main_layout.setSpacing(5) main_layout.addWidget(search_btn, 0) - main_layout.addWidget(filters_widget, 1) + main_layout.addWidget(filters_wrap, 1) search_btn.clicked.connect(self._on_filters_request) self.clicked.connect(self._on_clicked) self._search_btn = search_btn + self._filters_wrap = filters_wrap self._filters_widget = filters_widget self._filters_layout = filters_layout self._widgets_by_name = {} @@ -616,6 +623,14 @@ class FiltersBar(ClickableFrame): self._filters_popup = FiltersPopup(self) self._filter_value_popup = FilterValuePopup(self) + def showEvent(self, event): + super().showEvent(event) + self._update_filters_geo() + + def resizeEvent(self, event): + super().resizeEvent(event) + self._update_filters_geo() + def set_search_items(self, filter_defs: list[FilterDefinition]): self._filter_defs_by_name = { filter_def.name: filter_def @@ -647,6 +662,13 @@ class FiltersBar(ClickableFrame): idx = self._filters_layout.count() - 1 self._filters_layout.insertWidget(idx, item_widget, 0) + def _update_filters_geo(self): + geo = self._filters_wrap.geometry() + geo.moveTopLeft(QtCore.QPoint(0, 0)) + geo.setWidth(geo.width() * 10) + + self._filters_widget.setGeometry(geo) + def _on_clicked(self): self._show_filters_popup() From d0889e33f65d6f1fb77e3e9b4df9093ff0d962fe Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 11 Jun 2025 11:41:41 +0200 Subject: [PATCH 046/155] set arbitrary width --- client/ayon_core/tools/loader/ui/search_bar.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py index ffd08e71a1..a48324bc53 100644 --- a/client/ayon_core/tools/loader/ui/search_bar.py +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -665,7 +665,8 @@ class FiltersBar(ClickableFrame): def _update_filters_geo(self): geo = self._filters_wrap.geometry() geo.moveTopLeft(QtCore.QPoint(0, 0)) - geo.setWidth(geo.width() * 10) + # Arbitrary width + geo.setWidth(1000) self._filters_widget.setGeometry(geo) From 75154db1858395d9ccce5cd108c56a1e3148dc5d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 11 Jun 2025 12:03:17 +0200 Subject: [PATCH 047/155] fix events propagation --- client/ayon_core/style/style.css | 3 +++ .../ayon_core/tools/loader/ui/search_bar.py | 13 ++++++------ client/ayon_core/tools/utils/widgets.py | 20 ++++++++++++------- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/client/ayon_core/style/style.css b/client/ayon_core/style/style.css index 525cf28633..c123751ac4 100644 --- a/client/ayon_core/style/style.css +++ b/client/ayon_core/style/style.css @@ -904,6 +904,9 @@ FilterValueItemsView #ContentWidget { SearchItemDisplayWidget { border-radius: 5px; } +SearchItemDisplayWidget:hover { + background: {color:bg-buttons}; +} SearchItemDisplayWidget #ValueWidget { border-radius: 3px; background: {color:bg-buttons}; diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py index a48324bc53..c234d7d47b 100644 --- a/client/ayon_core/tools/loader/ui/search_bar.py +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -10,7 +10,6 @@ from ayon_core.tools.utils import ( get_qt_icon, SquareButton, BaseClickableFrame, - ClickableFrame, PixmapLabel, ) @@ -75,7 +74,7 @@ class CloseButton(SquareButton): icon.paint(painter, rect) -class SearchItemDisplayWidget(QtWidgets.QFrame): +class SearchItemDisplayWidget(BaseClickableFrame): """Widget displaying a set filter in the bar.""" close_requested = QtCore.Signal(str) edit_requested = QtCore.Signal(str) @@ -92,6 +91,7 @@ class SearchItemDisplayWidget(QtWidgets.QFrame): title_widget = QtWidgets.QLabel(f"{filter_def.title}:", self) value_wrapper = QtWidgets.QWidget(self) + value_wrapper.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) value_widget = QtWidgets.QLabel(value_wrapper) value_widget.setObjectName("ValueWidget") value_widget.setText("") @@ -139,7 +139,7 @@ class SearchItemDisplayWidget(QtWidgets.QFrame): def _on_remove_clicked(self): self.close_requested.emit(self._filter_def.name) - def _request_edit(self): + def _mouse_release_callback(self): self.edit_requested.emit(self._filter_def.name) @@ -174,7 +174,7 @@ class FiltersPopup(QtWidgets.QWidget): def __init__(self, parent): super().__init__(parent) self.setWindowFlags(QtCore.Qt.Popup | QtCore.Qt.FramelessWindowHint) - self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) shadow_frame = QtWidgets.QFrame(self) shadow_frame.setObjectName("ShadowFrame") @@ -577,7 +577,7 @@ class FilterValuePopup(QtWidgets.QWidget): self.value_changed.emit(self._filter_name) -class FiltersBar(ClickableFrame): +class FiltersBar(BaseClickableFrame): filters_changed = QtCore.Signal() def __init__(self, parent): @@ -612,7 +612,6 @@ class FiltersBar(ClickableFrame): main_layout.addWidget(filters_wrap, 1) search_btn.clicked.connect(self._on_filters_request) - self.clicked.connect(self._on_clicked) self._search_btn = search_btn self._filters_wrap = filters_wrap @@ -670,7 +669,7 @@ class FiltersBar(ClickableFrame): self._filters_widget.setGeometry(geo) - def _on_clicked(self): + def _mouse_release_callback(self): self._show_filters_popup() def _show_filters_popup(self): diff --git a/client/ayon_core/tools/utils/widgets.py b/client/ayon_core/tools/utils/widgets.py index 0cd6d68ab3..70e5e3a0e3 100644 --- a/client/ayon_core/tools/utils/widgets.py +++ b/client/ayon_core/tools/utils/widgets.py @@ -426,7 +426,7 @@ class BaseClickableFrame(QtWidgets.QFrame): Callback is defined by overriding `_mouse_release_callback`. """ def __init__(self, parent): - super(BaseClickableFrame, self).__init__(parent) + super().__init__(parent) self._mouse_pressed = False @@ -434,17 +434,23 @@ class BaseClickableFrame(QtWidgets.QFrame): pass def mousePressEvent(self, event): + super().mousePressEvent(event) + if event.isAccepted(): + return if event.button() == QtCore.Qt.LeftButton: self._mouse_pressed = True - super(BaseClickableFrame, self).mousePressEvent(event) + event.accept() def mouseReleaseEvent(self, event): - if self._mouse_pressed: - self._mouse_pressed = False - if self.rect().contains(event.pos()): - self._mouse_release_callback() + pressed, self._mouse_pressed = self._mouse_pressed, False + super().mouseReleaseEvent(event) + if event.isAccepted(): + return - super(BaseClickableFrame, self).mouseReleaseEvent(event) + accepted = pressed and self.rect().contains(event.pos()) + if accepted: + event.accept() + self._mouse_release_callback() class ClickableFrame(BaseClickableFrame): From e9981081aabf7974276342a58b4a2f02f612ad23 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 11 Jun 2025 12:03:30 +0200 Subject: [PATCH 048/155] extend the size to 3000 --- client/ayon_core/tools/loader/ui/search_bar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py index c234d7d47b..629212876a 100644 --- a/client/ayon_core/tools/loader/ui/search_bar.py +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -665,7 +665,7 @@ class FiltersBar(BaseClickableFrame): geo = self._filters_wrap.geometry() geo.moveTopLeft(QtCore.QPoint(0, 0)) # Arbitrary width - geo.setWidth(1000) + geo.setWidth(3000) self._filters_widget.setGeometry(geo) From de15350fe67c1cb0e9fd5ac4ec3ea5b7204a9d94 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 11 Jun 2025 12:38:13 +0200 Subject: [PATCH 049/155] emit filter changed signal --- client/ayon_core/tools/loader/ui/search_bar.py | 13 ++++++++++++- client/ayon_core/tools/loader/ui/window.py | 15 ++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py index 629212876a..6852500f9f 100644 --- a/client/ayon_core/tools/loader/ui/search_bar.py +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -578,7 +578,7 @@ class FilterValuePopup(QtWidgets.QWidget): class FiltersBar(BaseClickableFrame): - filters_changed = QtCore.Signal() + filter_changed = QtCore.Signal(str) def __init__(self, parent): super().__init__(parent) @@ -636,6 +636,16 @@ class FiltersBar(BaseClickableFrame): for filter_def in filter_defs } + def get_filter_value(self, name: str) -> Optional[Any]: + """Get the value of a filter by its name.""" + item_widget = self._widgets_by_name.get(name) + if item_widget is not None: + value = item_widget.get_value() + if isinstance(value, list) and len(value) == 0: + return None + return value + return None + def add_item(self, name: str): """Add a new item to the search bar. @@ -734,6 +744,7 @@ class FiltersBar(BaseClickableFrame): value = self._filter_value_popup.get_value() item_widget = self._widgets_by_name.get(name) item_widget.set_value(value) + self.filter_changed.emit(name) def _on_filter_value_closed(self, name): widget = self._widgets_by_name.get(name) diff --git a/client/ayon_core/tools/loader/ui/window.py b/client/ayon_core/tools/loader/ui/window.py index 7a3d198ab6..1e2904b667 100644 --- a/client/ayon_core/tools/loader/ui/window.py +++ b/client/ayon_core/tools/loader/ui/window.py @@ -255,7 +255,7 @@ class LoaderWindow(QtWidgets.QWidget): folders_filter_input.textChanged.connect( self._on_folder_filter_change ) - search_bar.filters_changed.connect(self._on_filters_change) + search_bar.filter_changed.connect(self._on_filter_change) # products_filter_input.textChanged.connect( # self._on_product_filter_change # ) @@ -443,8 +443,17 @@ class LoaderWindow(QtWidgets.QWidget): self._product_group_checkbox.isChecked() ) - def _on_filters_change(self): - pass + def _on_filter_change(self, filter_name): + if filter_name == "product_name": + self._products_widget.set_name_filter( + self._search_bar.get_filter_value("product_name") + ) + elif filter_name == "product_types": + product_types = self._search_bar.get_filter_value("product_types") + self._products_widget.set_product_type_filter(product_types) + elif filter_name == "statuses": + status_names = self._search_bar.get_filter_value("statuses") + self._products_widget.set_statuses_filter(status_names) def _on_tasks_selection_change(self, event): self._products_widget.set_tasks_filter(event["task_ids"]) From de6c4c434218320bf4530d2f54d394d95371d20a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 11 Jun 2025 12:38:26 +0200 Subject: [PATCH 050/155] fix status names filter --- client/ayon_core/tools/loader/ui/products_delegates.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/loader/ui/products_delegates.py b/client/ayon_core/tools/loader/ui/products_delegates.py index 8cece4687f..7a7ffe1b90 100644 --- a/client/ayon_core/tools/loader/ui/products_delegates.py +++ b/client/ayon_core/tools/loader/ui/products_delegates.py @@ -185,7 +185,9 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): widget.set_tasks_filter(task_ids) def set_statuses_filter(self, status_names): - self._statuses_filter = set(status_names) + if status_names is not None: + status_names = set(status_names) + self._statuses_filter = status_names for widget in self._editor_by_id.values(): widget.set_statuses_filter(status_names) From 7a03ba98f9cdabbbdea77f9bd3cdf6290a62acf5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 11 Jun 2025 12:39:55 +0200 Subject: [PATCH 051/155] emi signal on fitler removement --- client/ayon_core/tools/loader/ui/search_bar.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py index 6852500f9f..cbecccb594 100644 --- a/client/ayon_core/tools/loader/ui/search_bar.py +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -763,3 +763,4 @@ class FiltersBar(BaseClickableFrame): self._filters_layout.takeAt(idx) widget.setVisible(False) widget.deleteLater() + self.filter_changed.emit(name) From badfcbfaa5acf0028eeea5b537700ea183d9e33f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 11 Jun 2025 12:41:49 +0200 Subject: [PATCH 052/155] get rid of super --- client/ayon_core/tools/utils/widgets.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/utils/widgets.py b/client/ayon_core/tools/utils/widgets.py index 70e5e3a0e3..c4862304f1 100644 --- a/client/ayon_core/tools/utils/widgets.py +++ b/client/ayon_core/tools/utils/widgets.py @@ -1159,7 +1159,7 @@ class SquareButton(QtWidgets.QPushButton): """ def __init__(self, *args, **kwargs): - super(SquareButton, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) sp = self.sizePolicy() sp.setVerticalPolicy(QtWidgets.QSizePolicy.Minimum) @@ -1168,17 +1168,17 @@ class SquareButton(QtWidgets.QPushButton): self._ideal_width = None def showEvent(self, event): - super(SquareButton, self).showEvent(event) + super().showEvent(event) self._ideal_width = self.height() self.updateGeometry() def resizeEvent(self, event): - super(SquareButton, self).resizeEvent(event) + super().resizeEvent(event) self._ideal_width = self.height() self.updateGeometry() def sizeHint(self): - sh = super(SquareButton, self).sizeHint() + sh = super().sizeHint() ideal_width = self._ideal_width if ideal_width is None: ideal_width = sh.height() From 05f2230f18f1e64a51569c07b0972648b4c75709 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 11 Jun 2025 12:53:36 +0200 Subject: [PATCH 053/155] added border to bar --- client/ayon_core/style/style.css | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/style/style.css b/client/ayon_core/style/style.css index c123751ac4..17f852b6e3 100644 --- a/client/ayon_core/style/style.css +++ b/client/ayon_core/style/style.css @@ -865,6 +865,7 @@ HintedLineEditButton { /* Launcher specific stylesheets */ FiltersBar { background: {color:bg-inputs}; + border: 1px solid {color:border}; border-radius: 5px; } From 0f56f06b2c26fcfc1045abe87177fd4f3ae06ed5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 11 Jun 2025 14:35:25 +0200 Subject: [PATCH 054/155] fix None value handling --- client/ayon_core/tools/loader/ui/search_bar.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py index cbecccb594..b2c0e73bb5 100644 --- a/client/ayon_core/tools/loader/ui/search_bar.py +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -117,7 +117,9 @@ class SearchItemDisplayWidget(BaseClickableFrame): def set_value(self, value: "str | list[str]"): text = "" ellide = True - if isinstance(value, str): + if value is None: + pass + elif isinstance(value, str): text = value elif len(value) == 1: text = value[0] From 2bc62fc2a2de51474382ecbb65855503b4fb1c0b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 11 Jun 2025 14:35:56 +0200 Subject: [PATCH 055/155] added helper buttons --- .../ayon_core/tools/loader/ui/search_bar.py | 61 +++++++++++++++++++ client/ayon_core/tools/loader/ui/window.py | 1 + 2 files changed, 62 insertions(+) diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py index b2c0e73bb5..bf04fec926 100644 --- a/client/ayon_core/tools/loader/ui/search_bar.py +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -315,10 +315,30 @@ class FilterValueItemsView(QtWidgets.QWidget): scroll_area.setWidget(content_widget) + btns_widget = QtWidgets.QWidget(self) + btns_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) + + select_all_btn = QtWidgets.QPushButton("Select all", btns_widget) + clear_btn = QtWidgets.QPushButton("Clear", btns_widget) + swap_btn = QtWidgets.QPushButton("Swap", btns_widget) + + btns_layout = QtWidgets.QHBoxLayout(btns_widget) + btns_layout.setContentsMargins(0, 0, 0, 0) + btns_layout.addStretch(1) + btns_layout.addWidget(select_all_btn, 0) + btns_layout.addWidget(clear_btn, 0) + btns_layout.addWidget(swap_btn, 0) + main_layout = QtWidgets.QVBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) main_layout.addWidget(scroll_area) + main_layout.addWidget(btns_widget, 0) + select_all_btn.clicked.connect(self._on_select_all) + clear_btn.clicked.connect(self._on_clear_selection) + swap_btn.clicked.connect(self._on_swap_selection) + + self._btns_widget = btns_widget self._multiselection = False self._content_layout = content_layout self._last_selected_widget = None @@ -368,6 +388,11 @@ class FilterValueItemsView(QtWidgets.QWidget): def set_multiselection(self, multiselection: bool): self._multiselection = multiselection + if not self._widgets_by_id or not self._multiselection: + self._btns_widget.setVisible(False) + else: + self._btns_widget.setVisible(True) + if not self._widgets_by_id or self._multiselection: return @@ -422,9 +447,45 @@ class FilterValueItemsView(QtWidgets.QWidget): empty_label = QtWidgets.QLabel( "No items to select from...", self ) + self._btns_widget.setVisible(False) self._content_layout.addWidget(empty_label, 0) + else: + self._btns_widget.setVisible(self._multiselection) self._content_layout.addStretch(1) + def _on_select_all(self): + changed = False + for widget in self._widgets_by_id.values(): + if not widget.is_selected(): + changed = True + widget.set_selected(True) + if self._last_selected_widget is None: + self._last_selected_widget = widget + + if changed: + self.value_changed.emit() + + def _on_swap_selection(self): + self._last_selected_widget = None + for widget in self._widgets_by_id.values(): + selected = not widget.is_selected() + widget.set_selected(selected) + if selected and self._last_selected_widget is None: + self._last_selected_widget = widget + + self.value_changed.emit() + + def _on_clear_selection(self): + self._last_selected_widget = None + changed = False + for widget in self._widgets_by_id.values(): + if widget.is_selected(): + changed = True + widget.set_selected(False) + + if changed: + self.value_changed.emit() + def _on_item_clicked(self, widget_id): widget = self._widgets_by_id.get(widget_id) if widget is None: diff --git a/client/ayon_core/tools/loader/ui/window.py b/client/ayon_core/tools/loader/ui/window.py index 1e2904b667..6f87e95375 100644 --- a/client/ayon_core/tools/loader/ui/window.py +++ b/client/ayon_core/tools/loader/ui/window.py @@ -451,6 +451,7 @@ class LoaderWindow(QtWidgets.QWidget): elif filter_name == "product_types": product_types = self._search_bar.get_filter_value("product_types") self._products_widget.set_product_type_filter(product_types) + elif filter_name == "statuses": status_names = self._search_bar.get_filter_value("statuses") self._products_widget.set_statuses_filter(status_names) From ff2645e335af1e9b1b91fe45d7c22c178c7ee64b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 11 Jun 2025 15:27:21 +0200 Subject: [PATCH 056/155] remove commented code --- client/ayon_core/tools/loader/ui/window.py | 58 +++------------------- 1 file changed, 6 insertions(+), 52 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/window.py b/client/ayon_core/tools/loader/ui/window.py index 6f87e95375..a8361caeab 100644 --- a/client/ayon_core/tools/loader/ui/window.py +++ b/client/ayon_core/tools/loader/ui/window.py @@ -18,11 +18,9 @@ from ayon_core.tools.loader.control import LoaderController from .folders_widget import LoaderFoldersWidget from .tasks_widget import LoaderTasksWidget from .products_widget import ProductsWidget -from .product_types_combo import ProductTypesCombobox from .product_group_dialog import ProductGroupDialog from .info_widget import InfoWidget from .repres_widget import RepresentationsWidget -from .statuses_combo import StatusesCombobox from .search_bar import FiltersBar, FilterDefinition @@ -186,15 +184,6 @@ class LoaderWindow(QtWidgets.QWidget): products_inputs_widget = QtWidgets.QWidget(products_wrap_widget) search_bar = FiltersBar(products_inputs_widget) - # - # products_filter_input = PlaceholderLineEdit(products_inputs_widget) - # products_filter_input.setPlaceholderText("Product name filter...") - # - # product_types_filter_combo = ProductTypesCombobox( - # controller, products_inputs_widget - # ) - # - # product_status_filter_combo = StatusesCombobox(controller, self) product_group_checkbox = QtWidgets.QCheckBox( "Enable grouping", products_inputs_widget) @@ -202,9 +191,6 @@ class LoaderWindow(QtWidgets.QWidget): products_inputs_layout = QtWidgets.QHBoxLayout(products_inputs_widget) products_inputs_layout.setContentsMargins(0, 0, 0, 0) - # products_inputs_layout.addWidget(products_filter_input, 1) - # products_inputs_layout.addWidget(product_types_filter_combo, 1) - # products_inputs_layout.addWidget(product_status_filter_combo, 1) products_inputs_layout.addWidget(search_bar, 1) products_inputs_layout.addWidget(product_group_checkbox, 0) @@ -256,15 +242,6 @@ class LoaderWindow(QtWidgets.QWidget): self._on_folder_filter_change ) search_bar.filter_changed.connect(self._on_filter_change) - # products_filter_input.textChanged.connect( - # self._on_product_filter_change - # ) - # product_types_filter_combo.value_changed.connect( - # self._on_product_type_filter_change - # ) - # product_status_filter_combo.value_changed.connect( - # self._on_status_filter_change - # ) product_group_checkbox.stateChanged.connect( self._on_product_group_change ) @@ -323,9 +300,6 @@ class LoaderWindow(QtWidgets.QWidget): self._tasks_widget = tasks_widget self._search_bar = search_bar - # self._products_filter_input = products_filter_input - # self._product_types_filter_combo = product_types_filter_combo - # self._product_status_filter_combo = product_status_filter_combo self._product_group_checkbox = product_group_checkbox self._products_widget = products_widget @@ -364,12 +338,6 @@ class LoaderWindow(QtWidgets.QWidget): def closeEvent(self, event): super().closeEvent(event) - # ( - # self - # ._product_types_filter_combo - # .reset_product_types_filter_on_refresh() - # ) - self._reset_on_show = True def keyPressEvent(self, event): @@ -459,17 +427,6 @@ class LoaderWindow(QtWidgets.QWidget): def _on_tasks_selection_change(self, event): self._products_widget.set_tasks_filter(event["task_ids"]) - # def _on_product_filter_change(self, text): - # self._products_widget.set_name_filter(text) - # - # def _on_status_filter_change(self): - # status_names = self._product_status_filter_combo.get_value() - # self._products_widget.set_statuses_filter(status_names) - # - # def _on_product_type_filter_change(self): - # product_types = self._product_types_filter_combo.get_value() - # self._products_widget.set_product_type_filter(product_types) - def _on_merged_products_selection_change(self): items = self._products_widget.get_selected_merged_products() self._folders_widget.set_merged_products_selection(items) @@ -520,15 +477,12 @@ class LoaderWindow(QtWidgets.QWidget): self._search_bar.set_search_items([]) return - product_type_items: list[ProductTypeItem] = [] - status_items: list[StatusItem] = [] - if project_name: - product_type_items = self._controller.get_product_type_items( - project_name - ) - status_items = self._controller.get_project_status_items( - project_name - ) + product_type_items: list[ProductTypeItem] = ( + self._controller.get_product_type_items(project_name) + ) + status_items: list[StatusItem] = ( + self._controller.get_project_status_items(project_name) + ) filter_product_type_items = [ { From 93b5ea5c31fe1bb56ee1049dbc885f0b5546a7d1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 11 Jun 2025 15:28:01 +0200 Subject: [PATCH 057/155] implemented helper functions to get tags from project --- .../ayon_core/tools/common_models/__init__.py | 2 ++ .../tools/common_models/hierarchy.py | 28 ++++++++++++++++ .../ayon_core/tools/common_models/projects.py | 27 ++++++++++++++++ client/ayon_core/tools/loader/abstract.py | 32 ++++++++++++++++++- client/ayon_core/tools/loader/control.py | 11 +++++++ 5 files changed, 99 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/common_models/__init__.py b/client/ayon_core/tools/common_models/__init__.py index ece189fdc6..ec69e20b64 100644 --- a/client/ayon_core/tools/common_models/__init__.py +++ b/client/ayon_core/tools/common_models/__init__.py @@ -2,6 +2,7 @@ from .cache import CacheItem, NestedCacheItem from .projects import ( + TagItem, StatusItem, StatusStates, ProjectItem, @@ -25,6 +26,7 @@ __all__ = ( "CacheItem", "NestedCacheItem", + "TagItem", "StatusItem", "StatusStates", "ProjectItem", diff --git a/client/ayon_core/tools/common_models/hierarchy.py b/client/ayon_core/tools/common_models/hierarchy.py index 891eb80960..6b861d8fa5 100644 --- a/client/ayon_core/tools/common_models/hierarchy.py +++ b/client/ayon_core/tools/common_models/hierarchy.py @@ -217,6 +217,8 @@ class HierarchyModel(object): lifetime = 60 # A minute def __init__(self, controller): + self._tags_by_entity_type = NestedCacheItem( + levels=1, default_factory=dict, lifetime=self.lifetime) self._folders_items = NestedCacheItem( levels=1, default_factory=dict, lifetime=self.lifetime) self._folders_by_id = NestedCacheItem( @@ -235,6 +237,7 @@ class HierarchyModel(object): self._controller = controller def reset(self): + self._tags_by_entity_type.reset() self._folders_items.reset() self._folders_by_id.reset() @@ -514,6 +517,31 @@ class HierarchyModel(object): return output + def get_available_tags_by_entity_type( + self, project_name: str + ) -> dict[str, list[str]]: + """Get available tags for all entity types in a project.""" + cache = self._tags_by_entity_type.get(project_name) + if not cache.is_valid: + tags = None + if project_name: + response = ayon_api.get(f"projects/{project_name}/tags") + if response.status_code == 200: + tags = response.data + + # Fake empty tags + if tags is None: + tags = { + "folders": [], + "tasks": [], + "products": [], + "versions": [], + "representations": [], + "workfiles": [] + } + cache.update_data(tags) + return cache.get_data() + @contextlib.contextmanager def _folder_refresh_event_manager(self, project_name, sender): self._folders_refreshing.add(project_name) diff --git a/client/ayon_core/tools/common_models/projects.py b/client/ayon_core/tools/common_models/projects.py index 7ec941e6bd..8f3135b2d5 100644 --- a/client/ayon_core/tools/common_models/projects.py +++ b/client/ayon_core/tools/common_models/projects.py @@ -1,6 +1,9 @@ +from __future__ import annotations + import contextlib from abc import ABC, abstractmethod from typing import Dict, Any +from dataclasses import dataclass import ayon_api @@ -72,6 +75,14 @@ class StatusItem: ) +@dataclass +class TagItem: + """Tag definition set on project anatomy.""" + name: str + color: str + + + class FolderTypeItem: """Item representing folder type of project. @@ -288,6 +299,22 @@ class ProjectsModel(object): project_cache.update_data(entity) return project_cache.get_data() + def get_project_anatomy_tags(self, project_name: str) -> list[TagItem]: + """Get project anatomy tags. + + Args: + project_name (str): Project name. + + Returns: + list[TagItem]: Tag definitions. + + """ + project_entity = self.get_project_entity(project_name) + return [ + TagItem(tag["name"], tag["color"]) + for tag in project_entity["tags"] + ] + def get_project_status_items(self, project_name, sender): """Get project status items. diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index d0d7cd430b..8ae82c7e02 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -1,3 +1,4 @@ +from __future__ import annotations from abc import ABC, abstractmethod from typing import List @@ -6,6 +7,7 @@ from ayon_core.lib.attribute_definitions import ( serialize_attr_defs, deserialize_attr_defs, ) +from ayon_core.tools.common_models import TagItem class ProductTypeItem: @@ -517,8 +519,21 @@ class FrontendLoaderController(_BaseLoaderController): Returns: list[ProjectItem]: List of project items. - """ + """ + pass + + @abstractmethod + def get_project_anatomy_tags(self, project_name: str) -> list[TagItem]: + """Tag items defined on project anatomy. + + Args: + project_name (str): Project name. + + Returns: + list[TagItem]: Tag definition items. + + """ pass @abstractmethod @@ -590,6 +605,21 @@ class FrontendLoaderController(_BaseLoaderController): """ pass + @abstractmethod + def get_available_tags_by_entity_type( + self, project_name: str + ) -> dict[str, list[str]]: + """Get available tags by entity type. + + Args: + project_name (str): Project name. + + Returns: + dict[str, list[str]]: Available tags by entity type. + + """ + pass + @abstractmethod def get_project_status_items(self, project_name, sender=None): """Items for all projects available on server. diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index b3a80b34d4..95f48b3519 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -13,6 +13,7 @@ from ayon_core.tools.common_models import ( ProjectsModel, HierarchyModel, ThumbnailsModel, + TagItem, ) from .abstract import ( @@ -223,6 +224,16 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): output[folder_id] = label return output + def get_available_tags_by_entity_type( + self, project_name: str + ) -> dict[str, list[str]]: + return self._hierarchy_model.get_available_tags_by_entity_type( + project_name + ) + + def get_project_anatomy_tags(self, project_name: str) -> list[TagItem]: + return self._projects_model.get_project_anatomy_tags(project_name) + def get_product_items(self, project_name, folder_ids, sender=None): return self._products_model.get_product_items( project_name, folder_ids, sender) From fb6386cb8f13078247350800f423b6d73471a207 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 11 Jun 2025 15:53:51 +0200 Subject: [PATCH 058/155] added tags to version item --- client/ayon_core/tools/loader/abstract.py | 4 ++++ client/ayon_core/tools/loader/models/products.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index 8ae82c7e02..09d900074c 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -115,6 +115,7 @@ class VersionItem: published_time (Union[str, None]): Published time in format '%Y%m%dT%H%M%SZ'. status (Union[str, None]): Status name. + tags (Union[list[str], None]): Tags. author (Union[str, None]): Author. frame_range (Union[str, None]): Frame range. duration (Union[int, None]): Duration. @@ -133,6 +134,7 @@ class VersionItem: task_id, thumbnail_id, published_time, + tags, author, status, frame_range, @@ -150,6 +152,7 @@ class VersionItem: self.is_hero = is_hero self.published_time = published_time self.author = author + self.tags = tags self.status = status self.frame_range = frame_range self.duration = duration @@ -210,6 +213,7 @@ class VersionItem: "is_hero": self.is_hero, "published_time": self.published_time, "author": self.author, + "tags": self.tags, "status": self.status, "frame_range": self.frame_range, "duration": self.duration, diff --git a/client/ayon_core/tools/loader/models/products.py b/client/ayon_core/tools/loader/models/products.py index 34acc0550c..a8dd269bc3 100644 --- a/client/ayon_core/tools/loader/models/products.py +++ b/client/ayon_core/tools/loader/models/products.py @@ -19,6 +19,7 @@ PRODUCTS_MODEL_SENDER = "products.model" def version_item_from_entity(version): version_attribs = version["attrib"] + tags = version["tags"] frame_start = version_attribs.get("frameStart") frame_end = version_attribs.get("frameEnd") handle_start = version_attribs.get("handleStart") @@ -59,6 +60,7 @@ def version_item_from_entity(version): thumbnail_id=version["thumbnailId"], published_time=published_time, author=author, + tags=tags, status=version["status"], frame_range=frame_range, duration=duration, From 715031a9b55e6ceb97b74ebc86435d8d8dd37ca9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 11 Jun 2025 15:55:02 +0200 Subject: [PATCH 059/155] added version tags filter --- .../tools/loader/ui/products_delegates.py | 60 +++++++++++++++++-- .../tools/loader/ui/products_model.py | 6 ++ .../tools/loader/ui/products_widget.py | 23 ++++++- client/ayon_core/tools/loader/ui/window.py | 27 +++++++++ 4 files changed, 109 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/products_delegates.py b/client/ayon_core/tools/loader/ui/products_delegates.py index 7a7ffe1b90..2e98d14253 100644 --- a/client/ayon_core/tools/loader/ui/products_delegates.py +++ b/client/ayon_core/tools/loader/ui/products_delegates.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import numbers import uuid from typing import Dict @@ -18,14 +20,22 @@ from .products_model import ( SYNC_REMOTE_SITE_AVAILABILITY, ) -STATUS_NAME_ROLE = QtCore.Qt.UserRole + 1 +VERSION_ID_ROLE = QtCore.Qt.UserRole + 1 TASK_ID_ROLE = QtCore.Qt.UserRole + 2 +STATUS_NAME_ROLE = QtCore.Qt.UserRole + 3 class VersionsModel(QtGui.QStandardItemModel): def __init__(self): super().__init__() self._items_by_id = {} + self._tags_by_version_id = {} + + def get_version_tags(self, version_id: str) -> set[str]: + tags = self._tags_by_version_id.get(version_id) + if tags is None: + tags = set() + return tags def update_versions(self, version_items): version_ids = { @@ -39,6 +49,7 @@ class VersionsModel(QtGui.QStandardItemModel): item = self._items_by_id.pop(item_id) root_item.removeRow(item.row()) + tags_by_version_id = {} for idx, version_item in enumerate(version_items): version_id = version_item.version_id @@ -48,11 +59,14 @@ class VersionsModel(QtGui.QStandardItemModel): item = QtGui.QStandardItem(label) item.setData(version_id, QtCore.Qt.UserRole) self._items_by_id[version_id] = item + item.setData(version_id, VERSION_ID_ROLE) item.setData(version_item.status, STATUS_NAME_ROLE) item.setData(version_item.task_id, TASK_ID_ROLE) + tags_by_version_id[version_id] = set(version_item.tags) if item.row() != idx: root_item.insertRow(idx, item) + self._tags_by_version_id = tags_by_version_id class VersionsFilterModel(QtCore.QSortFilterProxyModel): @@ -60,22 +74,39 @@ class VersionsFilterModel(QtCore.QSortFilterProxyModel): super().__init__() self._status_filter = None self._task_ids_filter = None + self._tags_filter = None def filterAcceptsRow(self, row, parent): + index = None if self._status_filter is not None: if not self._status_filter: return False - - index = self.sourceModel().index(row, 0, parent) + if index is None: + index = self.sourceModel().index(row, 0, parent) status = index.data(STATUS_NAME_ROLE) if status not in self._status_filter: return False if self._task_ids_filter: - index = self.sourceModel().index(row, 0, parent) + if index is None: + index = self.sourceModel().index(row, 0, parent) task_id = index.data(TASK_ID_ROLE) if task_id not in self._task_ids_filter: return False + + if self._tags_filter is not None: + if not self._tags_filter: + return False + + if index is None: + index = self.sourceModel().index(row, 0, parent) + version_id = index.data(VERSION_ID_ROLE) + + model = self.sourceModel() + tags = model.get_version_tags(version_id) + if not tags & self._tags_filter: + return False + return True def set_tasks_filter(self, task_ids): @@ -90,6 +121,12 @@ class VersionsFilterModel(QtCore.QSortFilterProxyModel): self._status_filter = status_names self.invalidateFilter() + def set_tags_filter(self, tags): + if self._tags_filter == tags: + return + self._tags_filter = tags + self.invalidateFilter() + class VersionComboBox(QtWidgets.QComboBox): value_changed = QtCore.Signal(str, str) @@ -130,6 +167,13 @@ class VersionComboBox(QtWidgets.QComboBox): if self.currentIndex() != 0: self.setCurrentIndex(0) + def set_tags_filter(self, tags): + self._proxy_model.set_tags_filter(tags) + if self.count() == 0: + return + if self.currentIndex() != 0: + self.setCurrentIndex(0) + def all_versions_filtered_out(self): if self._items_by_id: return self.count() == 0 @@ -173,6 +217,7 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): self._editor_by_id: Dict[str, VersionComboBox] = {} self._task_ids_filter = None self._statuses_filter = None + self._tags_filter = None def displayText(self, value, locale): if not isinstance(value, numbers.Integral): @@ -191,6 +236,13 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): for widget in self._editor_by_id.values(): widget.set_statuses_filter(status_names) + def set_tags_filter(self, tags): + if tags is not None: + tags = set(tags) + self._tags_filter = tags + for widget in self._editor_by_id.values(): + widget.set_tags_filter(tags) + def paint(self, painter, option, index): fg_color = index.data(QtCore.Qt.ForegroundRole) if fg_color: diff --git a/client/ayon_core/tools/loader/ui/products_model.py b/client/ayon_core/tools/loader/ui/products_model.py index cebae9bca7..06af731f8f 100644 --- a/client/ayon_core/tools/loader/ui/products_model.py +++ b/client/ayon_core/tools/loader/ui/products_model.py @@ -41,6 +41,7 @@ SYNC_ACTIVE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 30 SYNC_REMOTE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 31 STATUS_NAME_FILTER_ROLE = QtCore.Qt.UserRole + 32 +VERSION_TAGS_FILTER_ROLE = QtCore.Qt.UserRole + 33 class ProductsModel(QtGui.QStandardItemModel): @@ -422,6 +423,10 @@ class ProductsModel(QtGui.QStandardItemModel): version_item.status for version_item in product_item.version_items.values() } + tags = set() + for version_item in product_item.version_items.values(): + tags |= set(version_item.tags) + if model_item is None: product_id = product_item.product_id model_item = QtGui.QStandardItem(product_item.product_name) @@ -440,6 +445,7 @@ class ProductsModel(QtGui.QStandardItemModel): self._items_by_id[product_id] = model_item model_item.setData("|".join(statuses), STATUS_NAME_FILTER_ROLE) + model_item.setData("|".join(tags), VERSION_TAGS_FILTER_ROLE) model_item.setData(product_item.folder_label, FOLDER_LABEL_ROLE) in_scene = 1 if product_item.product_in_scene else 0 model_item.setData(in_scene, PRODUCT_IN_SCENE_ROLE) diff --git a/client/ayon_core/tools/loader/ui/products_widget.py b/client/ayon_core/tools/loader/ui/products_widget.py index 94d95b9026..6c18cdc1f9 100644 --- a/client/ayon_core/tools/loader/ui/products_widget.py +++ b/client/ayon_core/tools/loader/ui/products_widget.py @@ -26,6 +26,7 @@ from .products_model import ( VERSION_STATUS_ICON_ROLE, VERSION_THUMBNAIL_ID_ROLE, STATUS_NAME_FILTER_ROLE, + VERSION_TAGS_FILTER_ROLE, ) from .products_delegates import ( VersionDelegate, @@ -41,6 +42,7 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel): self._product_type_filters = None self._statuses_filter = None + self._tags_filter = None self._task_ids_filter = None self._ascending_sort = True @@ -67,6 +69,12 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel): self._statuses_filter = statuses_filter self.invalidateFilter() + def set_version_tags_filter(self, tags): + if self._tags_filter == tags: + return + self._tags_filter = tags + self.invalidateFilter() + def filterAcceptsRow(self, source_row, source_parent): source_model = self.sourceModel() index = source_model.index(source_row, 0, source_parent) @@ -83,6 +91,11 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel): ): return False + if not self._accept_row_by_role_value( + index, self._tags_filter, VERSION_TAGS_FILTER_ROLE + ): + return False + return super().filterAcceptsRow(source_row, source_parent) def _accept_task_ids_filter(self, index): @@ -102,9 +115,9 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel): if not filter_value: return False - status_s = index.data(role) - for status in status_s.split("|"): - if status in filter_value: + value_s = index.data(role) + for value in value_s.split("|"): + if value in filter_value: return True return False @@ -290,6 +303,10 @@ class ProductsWidget(QtWidgets.QWidget): self._version_delegate.set_statuses_filter(status_names) self._products_proxy_model.set_statuses_filter(status_names) + def set_version_tags_filter(self, tags): + self._version_delegate.set_tags_filter(tags) + self._products_proxy_model.set_version_tags_filter(tags) + def set_product_type_filter(self, product_type_filters): """ diff --git a/client/ayon_core/tools/loader/ui/window.py b/client/ayon_core/tools/loader/ui/window.py index a8361caeab..a5f74c2c6f 100644 --- a/client/ayon_core/tools/loader/ui/window.py +++ b/client/ayon_core/tools/loader/ui/window.py @@ -424,6 +424,10 @@ class LoaderWindow(QtWidgets.QWidget): status_names = self._search_bar.get_filter_value("statuses") self._products_widget.set_statuses_filter(status_names) + elif filter_name == "version_tags": + version_tags = self._search_bar.get_filter_value("version_tags") + self._products_widget.set_version_tags_filter(version_tags) + def _on_tasks_selection_change(self, event): self._products_widget.set_tasks_filter(event["task_ids"]) @@ -483,6 +487,14 @@ class LoaderWindow(QtWidgets.QWidget): status_items: list[StatusItem] = ( self._controller.get_project_status_items(project_name) ) + tags_by_entity_type = ( + self._controller.get_available_tags_by_entity_type(project_name) + ) + tag_items = self._controller.get_project_anatomy_tags(project_name) + tag_color_by_name = { + tag_item.name: tag_item.color + for tag_item in tag_items + } filter_product_type_items = [ { @@ -503,6 +515,14 @@ class LoaderWindow(QtWidgets.QWidget): } for status_item in status_items ] + version_tags = [ + { + "value": tag_name, + "color": tag_color_by_name.get(tag_name), + } + for tag_name in tags_by_entity_type.get("versions") or [] + ] + self._search_bar.set_search_items([ FilterDefinition( @@ -527,6 +547,13 @@ class LoaderWindow(QtWidgets.QWidget): icon=None, items=filter_status_items, ), + FilterDefinition( + name="version_tags", + title="Version tags", + filter_type="list", + icon=None, + items=version_tags, + ), ]) def _on_folders_selection_changed(self, event): From 9b7be088907faccdee3faa3ed632e3e0794902c4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 12 Jun 2025 17:59:28 +0200 Subject: [PATCH 060/155] more descriptiove naming 'version_tags' --- .../tools/loader/ui/products_delegates.py | 36 +++++++++---------- .../tools/loader/ui/products_model.py | 6 ++-- .../tools/loader/ui/products_widget.py | 14 ++++---- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/products_delegates.py b/client/ayon_core/tools/loader/ui/products_delegates.py index 2e98d14253..8190fce337 100644 --- a/client/ayon_core/tools/loader/ui/products_delegates.py +++ b/client/ayon_core/tools/loader/ui/products_delegates.py @@ -29,10 +29,10 @@ class VersionsModel(QtGui.QStandardItemModel): def __init__(self): super().__init__() self._items_by_id = {} - self._tags_by_version_id = {} + self._version_tags_by_version_id = {} def get_version_tags(self, version_id: str) -> set[str]: - tags = self._tags_by_version_id.get(version_id) + tags = self._version_tags_by_version_id.get(version_id) if tags is None: tags = set() return tags @@ -49,7 +49,7 @@ class VersionsModel(QtGui.QStandardItemModel): item = self._items_by_id.pop(item_id) root_item.removeRow(item.row()) - tags_by_version_id = {} + version_tags_by_version_id = {} for idx, version_item in enumerate(version_items): version_id = version_item.version_id @@ -62,11 +62,11 @@ class VersionsModel(QtGui.QStandardItemModel): item.setData(version_id, VERSION_ID_ROLE) item.setData(version_item.status, STATUS_NAME_ROLE) item.setData(version_item.task_id, TASK_ID_ROLE) - tags_by_version_id[version_id] = set(version_item.tags) + version_tags_by_version_id[version_id] = set(version_item.tags) if item.row() != idx: root_item.insertRow(idx, item) - self._tags_by_version_id = tags_by_version_id + self._version_tags_by_version_id = version_tags_by_version_id class VersionsFilterModel(QtCore.QSortFilterProxyModel): @@ -74,7 +74,7 @@ class VersionsFilterModel(QtCore.QSortFilterProxyModel): super().__init__() self._status_filter = None self._task_ids_filter = None - self._tags_filter = None + self._version_tags_filter = None def filterAcceptsRow(self, row, parent): index = None @@ -94,8 +94,8 @@ class VersionsFilterModel(QtCore.QSortFilterProxyModel): if task_id not in self._task_ids_filter: return False - if self._tags_filter is not None: - if not self._tags_filter: + if self._version_tags_filter is not None: + if not self._version_tags_filter: return False if index is None: @@ -104,7 +104,7 @@ class VersionsFilterModel(QtCore.QSortFilterProxyModel): model = self.sourceModel() tags = model.get_version_tags(version_id) - if not tags & self._tags_filter: + if not tags & self._version_tags_filter: return False return True @@ -121,10 +121,10 @@ class VersionsFilterModel(QtCore.QSortFilterProxyModel): self._status_filter = status_names self.invalidateFilter() - def set_tags_filter(self, tags): - if self._tags_filter == tags: + def set_version_tags_filter(self, tags): + if self._version_tags_filter == tags: return - self._tags_filter = tags + self._version_tags_filter = tags self.invalidateFilter() @@ -167,8 +167,8 @@ class VersionComboBox(QtWidgets.QComboBox): if self.currentIndex() != 0: self.setCurrentIndex(0) - def set_tags_filter(self, tags): - self._proxy_model.set_tags_filter(tags) + def set_version_tags_filter(self, tags): + self._proxy_model.set_version_tags_filter(tags) if self.count() == 0: return if self.currentIndex() != 0: @@ -217,7 +217,7 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): self._editor_by_id: Dict[str, VersionComboBox] = {} self._task_ids_filter = None self._statuses_filter = None - self._tags_filter = None + self._version_tags_filter = None def displayText(self, value, locale): if not isinstance(value, numbers.Integral): @@ -236,12 +236,12 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): for widget in self._editor_by_id.values(): widget.set_statuses_filter(status_names) - def set_tags_filter(self, tags): + def set_version_tags_filter(self, tags): if tags is not None: tags = set(tags) - self._tags_filter = tags + self._version_tags_filter = tags for widget in self._editor_by_id.values(): - widget.set_tags_filter(tags) + widget.set_version_tags_filter(tags) def paint(self, painter, option, index): fg_color = index.data(QtCore.Qt.ForegroundRole) diff --git a/client/ayon_core/tools/loader/ui/products_model.py b/client/ayon_core/tools/loader/ui/products_model.py index 06af731f8f..d3bf6b2e38 100644 --- a/client/ayon_core/tools/loader/ui/products_model.py +++ b/client/ayon_core/tools/loader/ui/products_model.py @@ -423,9 +423,9 @@ class ProductsModel(QtGui.QStandardItemModel): version_item.status for version_item in product_item.version_items.values() } - tags = set() + version_tags = set() for version_item in product_item.version_items.values(): - tags |= set(version_item.tags) + version_tags |= set(version_item.tags) if model_item is None: product_id = product_item.product_id @@ -445,7 +445,7 @@ class ProductsModel(QtGui.QStandardItemModel): self._items_by_id[product_id] = model_item model_item.setData("|".join(statuses), STATUS_NAME_FILTER_ROLE) - model_item.setData("|".join(tags), VERSION_TAGS_FILTER_ROLE) + model_item.setData("|".join(version_tags), VERSION_TAGS_FILTER_ROLE) model_item.setData(product_item.folder_label, FOLDER_LABEL_ROLE) in_scene = 1 if product_item.product_in_scene else 0 model_item.setData(in_scene, PRODUCT_IN_SCENE_ROLE) diff --git a/client/ayon_core/tools/loader/ui/products_widget.py b/client/ayon_core/tools/loader/ui/products_widget.py index 6c18cdc1f9..0126102d71 100644 --- a/client/ayon_core/tools/loader/ui/products_widget.py +++ b/client/ayon_core/tools/loader/ui/products_widget.py @@ -42,7 +42,7 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel): self._product_type_filters = None self._statuses_filter = None - self._tags_filter = None + self._version_tags_filter = None self._task_ids_filter = None self._ascending_sort = True @@ -70,9 +70,9 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel): self.invalidateFilter() def set_version_tags_filter(self, tags): - if self._tags_filter == tags: + if self._version_tags_filter == tags: return - self._tags_filter = tags + self._version_tags_filter = tags self.invalidateFilter() def filterAcceptsRow(self, source_row, source_parent): @@ -92,7 +92,7 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel): return False if not self._accept_row_by_role_value( - index, self._tags_filter, VERSION_TAGS_FILTER_ROLE + index, self._version_tags_filter, VERSION_TAGS_FILTER_ROLE ): return False @@ -303,9 +303,9 @@ class ProductsWidget(QtWidgets.QWidget): self._version_delegate.set_statuses_filter(status_names) self._products_proxy_model.set_statuses_filter(status_names) - def set_version_tags_filter(self, tags): - self._version_delegate.set_tags_filter(tags) - self._products_proxy_model.set_version_tags_filter(tags) + def set_version_tags_filter(self, version_tags): + self._version_delegate.set_version_tags_filter(version_tags) + self._products_proxy_model.set_version_tags_filter(version_tags) def set_product_type_filter(self, product_type_filters): """ From a404db80451bbbd3c7ba0328f96aecaf07edc4c9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 12 Jun 2025 18:00:25 +0200 Subject: [PATCH 061/155] update flters after refresh --- client/ayon_core/tools/loader/ui/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/loader/ui/window.py b/client/ayon_core/tools/loader/ui/window.py index a5f74c2c6f..ddc7ef7329 100644 --- a/client/ayon_core/tools/loader/ui/window.py +++ b/client/ayon_core/tools/loader/ui/window.py @@ -325,7 +325,6 @@ class LoaderWindow(QtWidgets.QWidget): def refresh(self): self._reset_on_show = False self._controller.reset() - self._update_filters() def showEvent(self, event): super().showEvent(event) @@ -462,6 +461,7 @@ class LoaderWindow(QtWidgets.QWidget): self._projects_combobox.set_current_context_project(project_name) if not self._refresh_handler.project_refreshed: self._projects_combobox.refresh() + self._update_filters() def _on_load_finished(self, event): error_info = event["error_info"] From 9374e19ec3fa13b6fdc3df8b4c0ccb49c31a02ef Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 12 Jun 2025 18:00:48 +0200 Subject: [PATCH 062/155] task items now have tags --- client/ayon_core/tools/common_models/hierarchy.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/common_models/hierarchy.py b/client/ayon_core/tools/common_models/hierarchy.py index 6b861d8fa5..37d97af625 100644 --- a/client/ayon_core/tools/common_models/hierarchy.py +++ b/client/ayon_core/tools/common_models/hierarchy.py @@ -100,12 +100,14 @@ class TaskItem: label: Union[str, None], task_type: str, parent_id: str, + tags: list[str], ): self.task_id = task_id self.name = name self.label = label self.task_type = task_type self.parent_id = parent_id + self.tags = tags self._full_label = None @@ -145,6 +147,7 @@ class TaskItem: "label": self.label, "parent_id": self.parent_id, "task_type": self.task_type, + "tags": self.tags, } @classmethod @@ -176,7 +179,8 @@ def _get_task_items_from_tasks(tasks): task["name"], task["label"], task["type"], - folder_id + folder_id, + task["tags"], )) return output @@ -645,6 +649,6 @@ class HierarchyModel(object): tasks = list(ayon_api.get_tasks( project_name, folder_ids=[folder_id], - fields={"id", "name", "label", "folderId", "type"} + fields={"id", "name", "label", "folderId", "type", "tags"} )) return _get_task_items_from_tasks(tasks) From c2f9e55dd1f9f44f06aaeb427721a3ed3eecf255 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 12 Jun 2025 18:02:10 +0200 Subject: [PATCH 063/155] added task tags filter base --- .../tools/loader/ui/products_widget.py | 3 +++ client/ayon_core/tools/loader/ui/window.py | 19 ++++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/loader/ui/products_widget.py b/client/ayon_core/tools/loader/ui/products_widget.py index 0126102d71..1e391895f8 100644 --- a/client/ayon_core/tools/loader/ui/products_widget.py +++ b/client/ayon_core/tools/loader/ui/products_widget.py @@ -307,6 +307,9 @@ class ProductsWidget(QtWidgets.QWidget): self._version_delegate.set_version_tags_filter(version_tags) self._products_proxy_model.set_version_tags_filter(version_tags) + def set_task_tags_filter(self, task_tags): + pass + def set_product_type_filter(self, product_type_filters): """ diff --git a/client/ayon_core/tools/loader/ui/window.py b/client/ayon_core/tools/loader/ui/window.py index ddc7ef7329..c39f92234e 100644 --- a/client/ayon_core/tools/loader/ui/window.py +++ b/client/ayon_core/tools/loader/ui/window.py @@ -427,6 +427,10 @@ class LoaderWindow(QtWidgets.QWidget): version_tags = self._search_bar.get_filter_value("version_tags") self._products_widget.set_version_tags_filter(version_tags) + elif filter_name == "task_tags": + task_tags = self._search_bar.get_filter_value("task_tags") + self._products_widget.set_task_tags_filter(task_tags) + def _on_tasks_selection_change(self, event): self._products_widget.set_tasks_filter(event["task_ids"]) @@ -522,7 +526,13 @@ class LoaderWindow(QtWidgets.QWidget): } for tag_name in tags_by_entity_type.get("versions") or [] ] - + task_tags = [ + { + "value": tag_name, + "color": tag_color_by_name.get(tag_name), + } + for tag_name in tags_by_entity_type.get("tasks") or [] + ] self._search_bar.set_search_items([ FilterDefinition( @@ -554,6 +564,13 @@ class LoaderWindow(QtWidgets.QWidget): icon=None, items=version_tags, ), + FilterDefinition( + name="task_tags", + title="Task tags", + filter_type="list", + icon=None, + items=task_tags, + ), ]) def _on_folders_selection_changed(self, event): From 84bc798458b5b7fddc39196a0790bfec1fa8e54b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 12 Jun 2025 18:02:49 +0200 Subject: [PATCH 064/155] remove task refresh logic --- client/ayon_core/tools/loader/ui/tasks_widget.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/tasks_widget.py b/client/ayon_core/tools/loader/ui/tasks_widget.py index 5779fc2a01..cc7e2e9c95 100644 --- a/client/ayon_core/tools/loader/ui/tasks_widget.py +++ b/client/ayon_core/tools/loader/ui/tasks_widget.py @@ -332,10 +332,6 @@ class LoaderTasksWidget(QtWidgets.QWidget): "selection.folders.changed", self._on_folders_selection_changed, ) - controller.register_event_callback( - "tasks.refresh.finished", - self._on_tasks_refresh_finished - ) selection_model = tasks_view.selectionModel() selection_model.selectionChanged.connect(self._on_selection_change) @@ -373,10 +369,6 @@ class LoaderTasksWidget(QtWidgets.QWidget): def _clear(self): self._tasks_model.clear() - def _on_tasks_refresh_finished(self, event): - if event["sender"] != TASKS_MODEL_SENDER_NAME: - self._set_project_name(event["project_name"]) - def _on_folders_selection_changed(self, event): project_name = event["project_name"] folder_ids = event["folder_ids"] From da6bc0b72838864665b3524bfa61d2ac2912a996 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 12 Jun 2025 18:05:01 +0200 Subject: [PATCH 065/155] added task tags data to product --- .../tools/loader/ui/products_model.py | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/loader/ui/products_model.py b/client/ayon_core/tools/loader/ui/products_model.py index d3bf6b2e38..2e257073cf 100644 --- a/client/ayon_core/tools/loader/ui/products_model.py +++ b/client/ayon_core/tools/loader/ui/products_model.py @@ -41,7 +41,8 @@ SYNC_ACTIVE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 30 SYNC_REMOTE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 31 STATUS_NAME_FILTER_ROLE = QtCore.Qt.UserRole + 32 -VERSION_TAGS_FILTER_ROLE = QtCore.Qt.UserRole + 33 +TASK_TAGS_FILTER_ROLE = QtCore.Qt.UserRole + 33 +VERSION_TAGS_FILTER_ROLE = QtCore.Qt.UserRole + 34 class ProductsModel(QtGui.QStandardItemModel): @@ -131,6 +132,7 @@ class ProductsModel(QtGui.QStandardItemModel): self._last_folder_ids = [] self._last_project_statuses = {} self._last_status_icons_by_name = {} + self._last_task_tags_by_task_id = {} def get_product_item_indexes(self): return [ @@ -424,8 +426,14 @@ class ProductsModel(QtGui.QStandardItemModel): for version_item in product_item.version_items.values() } version_tags = set() + task_tags = set() for version_item in product_item.version_items.values(): version_tags |= set(version_item.tags) + _task_tags = self._last_task_tags_by_task_id.get( + version_item.task_id + ) + if _task_tags: + task_tags |= set(_task_tags) if model_item is None: product_id = product_item.product_id @@ -446,6 +454,7 @@ class ProductsModel(QtGui.QStandardItemModel): model_item.setData("|".join(statuses), STATUS_NAME_FILTER_ROLE) model_item.setData("|".join(version_tags), VERSION_TAGS_FILTER_ROLE) + model_item.setData("|".join(task_tags), TASK_TAGS_FILTER_ROLE) model_item.setData(product_item.folder_label, FOLDER_LABEL_ROLE) in_scene = 1 if product_item.product_in_scene else 0 model_item.setData(in_scene, PRODUCT_IN_SCENE_ROLE) @@ -476,6 +485,14 @@ class ProductsModel(QtGui.QStandardItemModel): } self._last_status_icons_by_name = {} + task_items = self._controller.get_task_items( + project_name, folder_ids, sender=PRODUCTS_MODEL_SENDER_NAME + ) + self._last_task_tags_by_task_id = { + task_item.task_id: task_item.tags + for task_item in task_items + } + active_site_icon_def = self._controller.get_active_site_icon_def( project_name ) @@ -490,6 +507,7 @@ class ProductsModel(QtGui.QStandardItemModel): folder_ids, sender=PRODUCTS_MODEL_SENDER_NAME ) + product_items_by_id = { product_item.product_id: product_item for product_item in product_items From b2c583d2589fa95db84d1e1a243b0fc5aa123a5a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 12 Jun 2025 18:05:13 +0200 Subject: [PATCH 066/155] fixed variable names --- client/ayon_core/tools/loader/ui/products_model.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/products_model.py b/client/ayon_core/tools/loader/ui/products_model.py index 2e257073cf..59199a48e8 100644 --- a/client/ayon_core/tools/loader/ui/products_model.py +++ b/client/ayon_core/tools/loader/ui/products_model.py @@ -227,9 +227,9 @@ class ProductsModel(QtGui.QStandardItemModel): product_item = self._product_items_by_id.get(product_id) if product_item is None: return None - product_items = list(product_item.version_items.values()) - product_items.sort(reverse=True) - return product_items + version_items = list(product_item.version_items.values()) + version_items.sort(reverse=True) + return version_items if role == QtCore.Qt.EditRole: return None From a471b48b04e84b6b0b2887c38770764ac219e8c3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 12 Jun 2025 18:05:58 +0200 Subject: [PATCH 067/155] implemented tags filter for product items --- .../ayon_core/tools/loader/ui/products_widget.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/loader/ui/products_widget.py b/client/ayon_core/tools/loader/ui/products_widget.py index 1e391895f8..775656f13c 100644 --- a/client/ayon_core/tools/loader/ui/products_widget.py +++ b/client/ayon_core/tools/loader/ui/products_widget.py @@ -27,6 +27,7 @@ from .products_model import ( VERSION_THUMBNAIL_ID_ROLE, STATUS_NAME_FILTER_ROLE, VERSION_TAGS_FILTER_ROLE, + TASK_TAGS_FILTER_ROLE, ) from .products_delegates import ( VersionDelegate, @@ -43,6 +44,7 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel): self._product_type_filters = None self._statuses_filter = None self._version_tags_filter = None + self._task_tags_filter = None self._task_ids_filter = None self._ascending_sort = True @@ -75,6 +77,12 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel): self._version_tags_filter = tags self.invalidateFilter() + def set_task_tags_filter(self, tags): + if self._task_tags_filter == tags: + return + self._task_tags_filter = tags + self.invalidateFilter() + def filterAcceptsRow(self, source_row, source_parent): source_model = self.sourceModel() index = source_model.index(source_row, 0, source_parent) @@ -96,6 +104,11 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel): ): return False + if not self._accept_row_by_role_value( + index, self._task_tags_filter, TASK_TAGS_FILTER_ROLE + ): + return False + return super().filterAcceptsRow(source_row, source_parent) def _accept_task_ids_filter(self, index): @@ -308,7 +321,7 @@ class ProductsWidget(QtWidgets.QWidget): self._products_proxy_model.set_version_tags_filter(version_tags) def set_task_tags_filter(self, task_tags): - pass + self._products_proxy_model.set_task_tags_filter(task_tags) def set_product_type_filter(self, product_type_filters): """ From e02118b93a43a90821a4e05f8b5c39b48b62fe33 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 12 Jun 2025 18:07:34 +0200 Subject: [PATCH 068/155] cleanup naming conflicts --- .../tools/loader/ui/products_delegates.py | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/products_delegates.py b/client/ayon_core/tools/loader/ui/products_delegates.py index 8190fce337..4283b66e61 100644 --- a/client/ayon_core/tools/loader/ui/products_delegates.py +++ b/client/ayon_core/tools/loader/ui/products_delegates.py @@ -20,12 +20,12 @@ from .products_model import ( SYNC_REMOTE_SITE_AVAILABILITY, ) -VERSION_ID_ROLE = QtCore.Qt.UserRole + 1 -TASK_ID_ROLE = QtCore.Qt.UserRole + 2 -STATUS_NAME_ROLE = QtCore.Qt.UserRole + 3 +COMBO_VERSION_ID_ROLE = QtCore.Qt.UserRole + 1 +COMBO_TASK_ID_ROLE = QtCore.Qt.UserRole + 2 +COMBO_STATUS_NAME_ROLE = QtCore.Qt.UserRole + 3 -class VersionsModel(QtGui.QStandardItemModel): +class ComboVersionsModel(QtGui.QStandardItemModel): def __init__(self): super().__init__() self._items_by_id = {} @@ -59,9 +59,9 @@ class VersionsModel(QtGui.QStandardItemModel): item = QtGui.QStandardItem(label) item.setData(version_id, QtCore.Qt.UserRole) self._items_by_id[version_id] = item - item.setData(version_id, VERSION_ID_ROLE) - item.setData(version_item.status, STATUS_NAME_ROLE) - item.setData(version_item.task_id, TASK_ID_ROLE) + item.setData(version_id, COMBO_VERSION_ID_ROLE) + item.setData(version_item.status, COMBO_STATUS_NAME_ROLE) + item.setData(version_item.task_id, COMBO_TASK_ID_ROLE) version_tags_by_version_id[version_id] = set(version_item.tags) if item.row() != idx: @@ -69,7 +69,7 @@ class VersionsModel(QtGui.QStandardItemModel): self._version_tags_by_version_id = version_tags_by_version_id -class VersionsFilterModel(QtCore.QSortFilterProxyModel): +class ComboVersionsFilterModel(QtCore.QSortFilterProxyModel): def __init__(self): super().__init__() self._status_filter = None @@ -83,14 +83,14 @@ class VersionsFilterModel(QtCore.QSortFilterProxyModel): return False if index is None: index = self.sourceModel().index(row, 0, parent) - status = index.data(STATUS_NAME_ROLE) + status = index.data(COMBO_STATUS_NAME_ROLE) if status not in self._status_filter: return False if self._task_ids_filter: if index is None: index = self.sourceModel().index(row, 0, parent) - task_id = index.data(TASK_ID_ROLE) + task_id = index.data(COMBO_TASK_ID_ROLE) if task_id not in self._task_ids_filter: return False @@ -100,7 +100,7 @@ class VersionsFilterModel(QtCore.QSortFilterProxyModel): if index is None: index = self.sourceModel().index(row, 0, parent) - version_id = index.data(VERSION_ID_ROLE) + version_id = index.data(COMBO_VERSION_ID_ROLE) model = self.sourceModel() tags = model.get_version_tags(version_id) @@ -134,8 +134,8 @@ class VersionComboBox(QtWidgets.QComboBox): def __init__(self, product_id, parent): super().__init__(parent) - versions_model = VersionsModel() - proxy_model = VersionsFilterModel() + versions_model = ComboVersionsModel() + proxy_model = ComboVersionsFilterModel() proxy_model.setSourceModel(versions_model) self.setModel(proxy_model) From 9a72f2bafc5da6e3206b18c127036efb084310f5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 12 Jun 2025 18:16:22 +0200 Subject: [PATCH 069/155] better way how to get versions --- client/ayon_core/tools/loader/ui/products_delegates.py | 6 +++++- client/ayon_core/tools/loader/ui/products_model.py | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/loader/ui/products_delegates.py b/client/ayon_core/tools/loader/ui/products_delegates.py index 4283b66e61..c22a99ab55 100644 --- a/client/ayon_core/tools/loader/ui/products_delegates.py +++ b/client/ayon_core/tools/loader/ui/products_delegates.py @@ -317,8 +317,12 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): editor.clear() # Current value of the index - versions = index.data(VERSION_NAME_EDIT_ROLE) or [] + product_id = index.data(PRODUCT_ID_ROLE) version_id = index.data(VERSION_ID_ROLE) + model = index.model() + while hasattr(model, "sourceModel"): + model = model.sourceModel() + versions = model.get_version_items_by_product_id(product_id) editor.update_versions(versions, version_id) editor.set_tasks_filter(self._task_ids_filter) diff --git a/client/ayon_core/tools/loader/ui/products_model.py b/client/ayon_core/tools/loader/ui/products_model.py index 59199a48e8..2015c86f92 100644 --- a/client/ayon_core/tools/loader/ui/products_model.py +++ b/client/ayon_core/tools/loader/ui/products_model.py @@ -173,6 +173,14 @@ class ProductsModel(QtGui.QStandardItemModel): self._last_folder_ids ) + def get_version_items_by_product_id(self, product_id: str): + product_item = self._product_items_by_id.get(product_id) + if product_item is None: + return None + version_items = list(product_item.version_items.values()) + version_items.sort(reverse=True) + return version_items + def flags(self, index): # Make the version column editable if index.column() == self.version_col and index.data(PRODUCT_ID_ROLE): From 5a4a888c222aff03ad568f664a1c23a4d8969840 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 12 Jun 2025 18:24:03 +0200 Subject: [PATCH 070/155] faster filtering --- client/ayon_core/tools/loader/ui/products_model.py | 3 +++ client/ayon_core/tools/loader/ui/products_widget.py | 7 ++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/products_model.py b/client/ayon_core/tools/loader/ui/products_model.py index 2015c86f92..b3042629ff 100644 --- a/client/ayon_core/tools/loader/ui/products_model.py +++ b/client/ayon_core/tools/loader/ui/products_model.py @@ -173,6 +173,9 @@ class ProductsModel(QtGui.QStandardItemModel): self._last_folder_ids ) + def get_task_tags_by_id(self, task_id): + return self._last_task_tags_by_task_id.get(task_id, set()) + def get_version_items_by_product_id(self, product_id: str): product_item = self._product_items_by_id.get(product_id) if product_item is None: diff --git a/client/ayon_core/tools/loader/ui/products_widget.py b/client/ayon_core/tools/loader/ui/products_widget.py index 775656f13c..935b9a147e 100644 --- a/client/ayon_core/tools/loader/ui/products_widget.py +++ b/client/ayon_core/tools/loader/ui/products_widget.py @@ -129,9 +129,10 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel): return False value_s = index.data(role) - for value in value_s.split("|"): - if value in filter_value: - return True + if value_s: + for value in value_s.split("|"): + if value in filter_value: + return True return False def lessThan(self, left, right): From 810b9cc573a0ad07604734153f12083a244bddb2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 12 Jun 2025 18:39:07 +0200 Subject: [PATCH 071/155] implemented version filtering by task tags --- .../tools/loader/ui/products_delegates.py | 86 +++++++++++++++---- .../tools/loader/ui/products_widget.py | 1 + 2 files changed, 69 insertions(+), 18 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/products_delegates.py b/client/ayon_core/tools/loader/ui/products_delegates.py index c22a99ab55..e78b32ceb1 100644 --- a/client/ayon_core/tools/loader/ui/products_delegates.py +++ b/client/ayon_core/tools/loader/ui/products_delegates.py @@ -23,21 +23,16 @@ from .products_model import ( COMBO_VERSION_ID_ROLE = QtCore.Qt.UserRole + 1 COMBO_TASK_ID_ROLE = QtCore.Qt.UserRole + 2 COMBO_STATUS_NAME_ROLE = QtCore.Qt.UserRole + 3 +COMBO_VERSION_TAGS_ROLE = QtCore.Qt.UserRole + 4 +COMBO_TASK_TAGS_ROLE = QtCore.Qt.UserRole + 5 class ComboVersionsModel(QtGui.QStandardItemModel): def __init__(self): super().__init__() self._items_by_id = {} - self._version_tags_by_version_id = {} - def get_version_tags(self, version_id: str) -> set[str]: - tags = self._version_tags_by_version_id.get(version_id) - if tags is None: - tags = set() - return tags - - def update_versions(self, version_items): + def update_versions(self, version_items, task_tags_by_version_id): version_ids = { version_item.version_id for version_item in version_items @@ -59,14 +54,17 @@ class ComboVersionsModel(QtGui.QStandardItemModel): item = QtGui.QStandardItem(label) item.setData(version_id, QtCore.Qt.UserRole) self._items_by_id[version_id] = item + version_tags = set(version_item.tags) + task_tags = task_tags_by_version_id[version_id] item.setData(version_id, COMBO_VERSION_ID_ROLE) item.setData(version_item.status, COMBO_STATUS_NAME_ROLE) item.setData(version_item.task_id, COMBO_TASK_ID_ROLE) + item.setData("|".join(version_tags), COMBO_VERSION_TAGS_ROLE) + item.setData("|".join(task_tags), COMBO_TASK_TAGS_ROLE) version_tags_by_version_id[version_id] = set(version_item.tags) if item.row() != idx: root_item.insertRow(idx, item) - self._version_tags_by_version_id = version_tags_by_version_id class ComboVersionsFilterModel(QtCore.QSortFilterProxyModel): @@ -75,6 +73,7 @@ class ComboVersionsFilterModel(QtCore.QSortFilterProxyModel): self._status_filter = None self._task_ids_filter = None self._version_tags_filter = None + self._task_tags_filter = None def filterAcceptsRow(self, row, parent): index = None @@ -99,12 +98,28 @@ class ComboVersionsFilterModel(QtCore.QSortFilterProxyModel): return False if index is None: - index = self.sourceModel().index(row, 0, parent) - version_id = index.data(COMBO_VERSION_ID_ROLE) + model = self.sourceModel() + index = model.index(row, 0, parent) + version_tags_s = index.data(COMBO_TASK_TAGS_ROLE) + version_tags = set() + if version_tags_s: + version_tags = set(version_tags_s.split("|")) - model = self.sourceModel() - tags = model.get_version_tags(version_id) - if not tags & self._version_tags_filter: + if not version_tags & self._version_tags_filter: + return False + + if self._task_tags_filter is not None: + if not self._task_tags_filter: + return False + + if index is None: + model = self.sourceModel() + index = model.index(row, 0, parent) + task_tags_s = index.data(COMBO_TASK_TAGS_ROLE) + task_tags = set() + if task_tags_s: + task_tags = set(task_tags_s.split("|")) + if not (task_tags & self._task_tags_filter): return False return True @@ -115,6 +130,12 @@ class ComboVersionsFilterModel(QtCore.QSortFilterProxyModel): self._task_ids_filter = task_ids self.invalidateFilter() + def set_task_tags_filter(self, tags): + if self._task_tags_filter == tags: + return + self._task_tags_filter = tags + self.invalidateFilter() + def set_statuses_filter(self, status_names): if self._status_filter == status_names: return @@ -160,6 +181,13 @@ class VersionComboBox(QtWidgets.QComboBox): if self.currentIndex() != 0: self.setCurrentIndex(0) + def set_task_tags_filter(self, tags): + self._proxy_model.set_task_tags_filter(tags) + if self.count() == 0: + return + if self.currentIndex() != 0: + self.setCurrentIndex(0) + def set_statuses_filter(self, status_names): self._proxy_model.set_statuses_filter(status_names) if self.count() == 0: @@ -179,7 +207,12 @@ class VersionComboBox(QtWidgets.QComboBox): return self.count() == 0 return False - def update_versions(self, version_items, current_version_id): + def update_versions( + self, + version_items, + current_version_id, + task_tags_by_version_id, + ): self.blockSignals(True) version_items = list(version_items) version_ids = [ @@ -190,7 +223,9 @@ class VersionComboBox(QtWidgets.QComboBox): current_version_id = version_ids[0] self._current_id = current_version_id - self._versions_model.update_versions(version_items) + self._versions_model.update_versions( + version_items, task_tags_by_version_id + ) index = version_ids.index(current_version_id) if self.currentIndex() != index: @@ -218,6 +253,7 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): self._task_ids_filter = None self._statuses_filter = None self._version_tags_filter = None + self._task_tags_filter = None def displayText(self, value, locale): if not isinstance(value, numbers.Integral): @@ -243,6 +279,13 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): for widget in self._editor_by_id.values(): widget.set_version_tags_filter(tags) + def set_task_tags_filter(self, tags): + if tags is not None: + tags = set(tags) + self._task_tags_filter = tags + for widget in self._editor_by_id.values(): + widget.set_task_tags_filter(tags) + def paint(self, painter, option, index): fg_color = index.data(QtCore.Qt.ForegroundRole) if fg_color: @@ -254,7 +297,7 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): fg_color = None if not fg_color: - return super(VersionDelegate, self).paint(painter, option, index) + return super().paint(painter, option, index) if option.widget: style = option.widget.style() @@ -323,9 +366,16 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): while hasattr(model, "sourceModel"): model = model.sourceModel() versions = model.get_version_items_by_product_id(product_id) + task_tags_by_version_id = { + version_item.version_id: model.get_task_tags_by_id( + version_item.task_id + ) + for version_item in versions + } - editor.update_versions(versions, version_id) + editor.update_versions(versions, version_id, task_tags_by_version_id) editor.set_tasks_filter(self._task_ids_filter) + editor.set_task_tags_filter(self._task_tags_filter) editor.set_statuses_filter(self._statuses_filter) def setModelData(self, editor, model, index): diff --git a/client/ayon_core/tools/loader/ui/products_widget.py b/client/ayon_core/tools/loader/ui/products_widget.py index 935b9a147e..8cb1d48acb 100644 --- a/client/ayon_core/tools/loader/ui/products_widget.py +++ b/client/ayon_core/tools/loader/ui/products_widget.py @@ -322,6 +322,7 @@ class ProductsWidget(QtWidgets.QWidget): self._products_proxy_model.set_version_tags_filter(version_tags) def set_task_tags_filter(self, task_tags): + self._version_delegate.set_task_tags_filter(task_tags) self._products_proxy_model.set_task_tags_filter(task_tags) def set_product_type_filter(self, product_type_filters): From aa252af0a4aca29b16fd8364d0f7f1771b697da7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 13 Jun 2025 09:38:09 +0200 Subject: [PATCH 072/155] add typehints --- client/ayon_core/tools/loader/abstract.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index 09d900074c..8d5e631852 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -1,13 +1,13 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import List +from typing import Iterable, Optional from ayon_core.lib.attribute_definitions import ( AbstractAttrDef, serialize_attr_defs, deserialize_attr_defs, ) -from ayon_core.tools.common_models import TagItem +from ayon_core.tools.common_models import TaskItem, TagItem class ProductTypeItem: @@ -360,8 +360,8 @@ class ProductTypesFilter: Defines the filtering for product types. """ - def __init__(self, product_types: List[str], is_allow_list: bool): - self.product_types: List[str] = product_types + def __init__(self, product_types: list[str], is_allow_list: bool): + self.product_types: list[str] = product_types self.is_allow_list: bool = is_allow_list @@ -561,7 +561,12 @@ class FrontendLoaderController(_BaseLoaderController): pass @abstractmethod - def get_task_items(self, project_name, folder_ids, sender=None): + def get_task_items( + self, + project_name: str, + folder_ids: Iterable[str], + sender: Optional[str] = None, + ) -> list[TaskItem]: """Task items for folder ids. Args: From 61af049a8c3db7410e76e0e934cd318893a6cc61 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 13 Jun 2025 10:44:38 +0200 Subject: [PATCH 073/155] ruff fixes --- client/ayon_core/tools/loader/ui/search_bar.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py index bf04fec926..840013c97c 100644 --- a/client/ayon_core/tools/loader/ui/search_bar.py +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -5,7 +5,7 @@ from typing import Any, Optional from qtpy import QtCore, QtWidgets, QtGui -from ayon_core.style import load_stylesheet, get_objected_colors +from ayon_core.style import get_objected_colors from ayon_core.tools.utils import ( get_qt_icon, SquareButton, @@ -682,7 +682,7 @@ class FiltersBar(BaseClickableFrame): self._filters_layout = filters_layout self._widgets_by_name = {} self._filter_defs_by_name = {} - self._filters_popup = FiltersPopup(self) + self._filters_popup = FiltersPopup(self) self._filter_value_popup = FilterValuePopup(self) def showEvent(self, event): From 60caf47cfffd6ad5507829073f29c085c0fa6324 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 13 Jun 2025 10:48:07 +0200 Subject: [PATCH 074/155] remove unncessary line --- client/ayon_core/tools/common_models/projects.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/tools/common_models/projects.py b/client/ayon_core/tools/common_models/projects.py index 8f3135b2d5..69ac4e34a8 100644 --- a/client/ayon_core/tools/common_models/projects.py +++ b/client/ayon_core/tools/common_models/projects.py @@ -82,7 +82,6 @@ class TagItem: color: str - class FolderTypeItem: """Item representing folder type of project. From 4f0c2a51c87ed1f3234c63bf894eafbcaf11a785 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 13 Jun 2025 14:40:50 +0200 Subject: [PATCH 075/155] fix missing group checkbox --- client/ayon_core/tools/loader/ui/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/loader/ui/window.py b/client/ayon_core/tools/loader/ui/window.py index c39f92234e..40802d3d88 100644 --- a/client/ayon_core/tools/loader/ui/window.py +++ b/client/ayon_core/tools/loader/ui/window.py @@ -198,7 +198,7 @@ class LoaderWindow(QtWidgets.QWidget): products_wrap_layout = QtWidgets.QVBoxLayout(products_wrap_widget) products_wrap_layout.setContentsMargins(0, 0, 0, 0) - products_wrap_layout.addWidget(search_bar, 0) + products_wrap_layout.addWidget(products_inputs_widget, 0) products_wrap_layout.addWidget(products_widget, 1) right_panel_splitter = QtWidgets.QSplitter(main_splitter) From 9033640efa2cf9e7225c5d7ee226a7cd8f2aa218 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 13 Jun 2025 14:48:50 +0200 Subject: [PATCH 076/155] remove unused widgets --- .../tools/loader/ui/product_types_combo.py | 170 ------------------ .../tools/loader/ui/statuses_combo.py | 157 ---------------- 2 files changed, 327 deletions(-) delete mode 100644 client/ayon_core/tools/loader/ui/product_types_combo.py delete mode 100644 client/ayon_core/tools/loader/ui/statuses_combo.py diff --git a/client/ayon_core/tools/loader/ui/product_types_combo.py b/client/ayon_core/tools/loader/ui/product_types_combo.py deleted file mode 100644 index 525f1cae1b..0000000000 --- a/client/ayon_core/tools/loader/ui/product_types_combo.py +++ /dev/null @@ -1,170 +0,0 @@ -from __future__ import annotations -from qtpy import QtGui, QtCore - -from ._multicombobox import ( - CustomPaintMultiselectComboBox, - BaseQtModel, -) - -STATUS_ITEM_TYPE = 0 -SELECT_ALL_TYPE = 1 -DESELECT_ALL_TYPE = 2 -SWAP_STATE_TYPE = 3 - -PRODUCT_TYPE_ROLE = QtCore.Qt.UserRole + 1 -ITEM_TYPE_ROLE = QtCore.Qt.UserRole + 2 -ITEM_SUBTYPE_ROLE = QtCore.Qt.UserRole + 3 - - -class ProductTypesQtModel(BaseQtModel): - refreshed = QtCore.Signal() - - def __init__(self, controller): - self._reset_filters_on_refresh = True - self._refreshing = False - self._bulk_change = False - self._items_by_name = {} - - super().__init__( - item_type_role=ITEM_TYPE_ROLE, - item_subtype_role=ITEM_SUBTYPE_ROLE, - empty_values_label="No product types...", - controller=controller, - ) - - def is_refreshing(self): - return self._refreshing - - def refresh(self, project_name): - self._refreshing = True - super().refresh(project_name) - - self._reset_filters_on_refresh = False - self._refreshing = False - self.refreshed.emit() - - def reset_product_types_filter_on_refresh(self): - self._reset_filters_on_refresh = True - - def _get_standard_items(self) -> list[QtGui.QStandardItem]: - return list(self._items_by_name.values()) - - def _clear_standard_items(self): - self._items_by_name.clear() - - def _prepare_new_value_items(self, project_name: str, _: bool) -> tuple[ - list[QtGui.QStandardItem], list[QtGui.QStandardItem] - ]: - product_type_items = self._controller.get_product_type_items( - project_name) - self._last_project = project_name - - names_to_remove = set(self._items_by_name.keys()) - items = [] - items_filter_required = {} - for product_type_item in product_type_items: - name = product_type_item.name - names_to_remove.discard(name) - item = self._items_by_name.get(name) - # Apply filter to new items or if filters reset is requested - filter_required = self._reset_filters_on_refresh - if item is None: - filter_required = True - item = QtGui.QStandardItem(name) - item.setData(name, PRODUCT_TYPE_ROLE) - item.setEditable(False) - item.setCheckable(True) - self._items_by_name[name] = item - - items.append(item) - - if filter_required: - items_filter_required[name] = item - - if items_filter_required: - product_types_filter = self._controller.get_product_types_filter() - for product_type, item in items_filter_required.items(): - matching = ( - int(product_type in product_types_filter.product_types) - + int(product_types_filter.is_allow_list) - ) - item.setCheckState( - QtCore.Qt.Checked - if matching % 2 == 0 - else QtCore.Qt.Unchecked - ) - - items_to_remove = [] - for name in names_to_remove: - items_to_remove.append( - self._items_by_name.pop(name) - ) - - # Uncheck all if all are checked (same result) - if all( - item.checkState() == QtCore.Qt.Checked - for item in items - ): - for item in items: - item.setCheckState(QtCore.Qt.Unchecked) - - return items, items_to_remove - - -class ProductTypesCombobox(CustomPaintMultiselectComboBox): - def __init__(self, controller, parent): - self._controller = controller - model = ProductTypesQtModel(controller) - super().__init__( - PRODUCT_TYPE_ROLE, - PRODUCT_TYPE_ROLE, - QtCore.Qt.ForegroundRole, - QtCore.Qt.DecorationRole, - item_type_role=ITEM_TYPE_ROLE, - model=model, - parent=parent - ) - - model.refreshed.connect(self._on_model_refresh) - - self.set_placeholder_text("Product types filter...") - self._model = model - self._last_project_name = None - self._fully_disabled_filter = False - - controller.register_event_callback( - "selection.project.changed", - self._on_project_change - ) - controller.register_event_callback( - "projects.refresh.finished", - self._on_projects_refresh - ) - self.setToolTip("Product types filter") - self.value_changed.connect( - self._on_product_type_filter_change - ) - - def reset_product_types_filter_on_refresh(self): - self._model.reset_product_types_filter_on_refresh() - - def _on_model_refresh(self): - self.value_changed.emit() - - def _on_product_type_filter_change(self): - lines = ["Product types filter"] - for item in self.get_value_info(): - status_name, enabled = item - lines.append(f"{'✔' if enabled else '☐'} {status_name}") - - self.setToolTip("\n".join(lines)) - - def _on_project_change(self, event): - project_name = event["project_name"] - self._last_project_name = project_name - self._model.refresh(project_name) - - def _on_projects_refresh(self): - if self._last_project_name: - self._model.refresh(self._last_project_name) - self._on_product_type_filter_change() diff --git a/client/ayon_core/tools/loader/ui/statuses_combo.py b/client/ayon_core/tools/loader/ui/statuses_combo.py deleted file mode 100644 index 2f034d00de..0000000000 --- a/client/ayon_core/tools/loader/ui/statuses_combo.py +++ /dev/null @@ -1,157 +0,0 @@ -from __future__ import annotations - -from qtpy import QtCore, QtGui - -from ayon_core.tools.utils import get_qt_icon -from ayon_core.tools.common_models import StatusItem - -from ._multicombobox import ( - CustomPaintMultiselectComboBox, - BaseQtModel, -) - -STATUS_ITEM_TYPE = 0 -SELECT_ALL_TYPE = 1 -DESELECT_ALL_TYPE = 2 -SWAP_STATE_TYPE = 3 - -STATUSES_FILTER_SENDER = "loader.statuses_filter" -STATUS_NAME_ROLE = QtCore.Qt.UserRole + 1 -STATUS_SHORT_ROLE = QtCore.Qt.UserRole + 2 -STATUS_COLOR_ROLE = QtCore.Qt.UserRole + 3 -STATUS_ICON_ROLE = QtCore.Qt.UserRole + 4 -ITEM_TYPE_ROLE = QtCore.Qt.UserRole + 5 -ITEM_SUBTYPE_ROLE = QtCore.Qt.UserRole + 6 - - -class StatusesQtModel(BaseQtModel): - def __init__(self, controller): - self._items_by_name: dict[str, QtGui.QStandardItem] = {} - self._icons_by_name_n_color: dict[str, QtGui.QIcon] = {} - super().__init__( - ITEM_TYPE_ROLE, - ITEM_SUBTYPE_ROLE, - "No statuses...", - controller, - ) - - def _get_standard_items(self) -> list[QtGui.QStandardItem]: - return list(self._items_by_name.values()) - - def _clear_standard_items(self): - self._items_by_name.clear() - - def _prepare_new_value_items( - self, project_name: str, project_changed: bool - ): - status_items: list[StatusItem] = ( - self._controller.get_project_status_items( - project_name, sender=STATUSES_FILTER_SENDER - ) - ) - items = [] - items_to_remove = [] - if not status_items: - return items, items_to_remove - - names_to_remove = set(self._items_by_name) - for row_idx, status_item in enumerate(status_items): - name = status_item.name - if name in self._items_by_name: - item = self._items_by_name[name] - names_to_remove.discard(name) - else: - item = QtGui.QStandardItem() - item.setData(ITEM_SUBTYPE_ROLE, STATUS_ITEM_TYPE) - item.setCheckState(QtCore.Qt.Unchecked) - item.setFlags( - QtCore.Qt.ItemIsEnabled - | QtCore.Qt.ItemIsSelectable - | QtCore.Qt.ItemIsUserCheckable - ) - self._items_by_name[name] = item - - icon = self._get_icon(status_item) - for role, value in ( - (STATUS_NAME_ROLE, status_item.name), - (STATUS_SHORT_ROLE, status_item.short), - (STATUS_COLOR_ROLE, status_item.color), - (STATUS_ICON_ROLE, icon), - ): - if item.data(role) != value: - item.setData(value, role) - - if project_changed: - item.setCheckState(QtCore.Qt.Unchecked) - items.append(item) - - for name in names_to_remove: - items_to_remove.append(self._items_by_name.pop(name)) - - return items, items_to_remove - - def _get_icon(self, status_item: StatusItem) -> QtGui.QIcon: - name = status_item.name - color = status_item.color - unique_id = "|".join([name or "", color or ""]) - icon = self._icons_by_name_n_color.get(unique_id) - if icon is not None: - return icon - - icon: QtGui.QIcon = get_qt_icon({ - "type": "material-symbols", - "name": status_item.icon, - "color": status_item.color - }) - self._icons_by_name_n_color[unique_id] = icon - return icon - - -class StatusesCombobox(CustomPaintMultiselectComboBox): - def __init__(self, controller, parent): - self._controller = controller - model = StatusesQtModel(controller) - super().__init__( - STATUS_NAME_ROLE, - STATUS_SHORT_ROLE, - STATUS_COLOR_ROLE, - STATUS_ICON_ROLE, - item_type_role=ITEM_TYPE_ROLE, - model=model, - parent=parent - ) - self.set_placeholder_text("Version status filter...") - self._model = model - self._last_project_name = None - self._fully_disabled_filter = False - - controller.register_event_callback( - "selection.project.changed", - self._on_project_change - ) - controller.register_event_callback( - "projects.refresh.finished", - self._on_projects_refresh - ) - self.setToolTip("Statuses filter") - self.value_changed.connect( - self._on_status_filter_change - ) - - def _on_status_filter_change(self): - lines = ["Statuses filter"] - for item in self.get_value_info(): - status_name, enabled = item - lines.append(f"{'✔' if enabled else '☐'} {status_name}") - - self.setToolTip("\n".join(lines)) - - def _on_project_change(self, event): - project_name = event["project_name"] - self._last_project_name = project_name - self._model.refresh(project_name) - - def _on_projects_refresh(self): - if self._last_project_name: - self._model.refresh(self._last_project_name) - self._on_status_filter_change() From c34e3bb15ed6d5c4cb50b993680921856cf567db Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 13 Jun 2025 15:22:22 +0200 Subject: [PATCH 077/155] auto set product type filters --- .../ayon_core/tools/loader/ui/search_bar.py | 13 ++++++++++ client/ayon_core/tools/loader/ui/window.py | 25 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py index 840013c97c..5e9e409ee1 100644 --- a/client/ayon_core/tools/loader/ui/search_bar.py +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -709,6 +709,19 @@ class FiltersBar(BaseClickableFrame): return value return None + def set_filter_value(self, name: str, value: Any): + """Set the value of a filter by its name.""" + if name not in self._filter_defs_by_name: + return + + item_widget = self._widgets_by_name.get(name) + if item_widget is None: + self.add_item(name) + item_widget = self._widgets_by_name.get(name) + + item_widget.set_value(value) + self.filter_changed.emit(name) + def add_item(self, name: str): """Add a new item to the search bar. diff --git a/client/ayon_core/tools/loader/ui/window.py b/client/ayon_core/tools/loader/ui/window.py index 40802d3d88..a3a476b330 100644 --- a/client/ayon_core/tools/loader/ui/window.py +++ b/client/ayon_core/tools/loader/ui/window.py @@ -318,6 +318,8 @@ class LoaderWindow(QtWidgets.QWidget): self._selected_folder_ids = set() self._selected_version_ids = set() + self._set_product_type_filters = True + self._products_widget.set_enable_grouping( self._product_group_checkbox.isChecked() ) @@ -573,6 +575,29 @@ class LoaderWindow(QtWidgets.QWidget): ), ]) + # Set product types filter from settings + if self._set_product_type_filters: + self._set_product_type_filters = False + product_types_filter = self._controller.get_product_types_filter() + product_types = [] + for item in filter_product_type_items: + product_type = item["value"] + matching = ( + int(product_type in product_types_filter.product_types) + + int(product_types_filter.is_allow_list) + ) + if matching % 2 == 0: + product_types.append(product_type) + + if ( + product_types + and len(product_types) < len(filter_product_type_items) + ): + self._search_bar.set_filter_value( + "product_types", + product_types + ) + def _on_folders_selection_changed(self, event): self._selected_folder_ids = set(event["folder_ids"]) self._update_thumbnails() From 93c2e1ad900701147b96c38886791fdbb7be55e5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 13 Jun 2025 15:48:40 +0200 Subject: [PATCH 078/155] added quick filter input for items --- .../ayon_core/tools/loader/ui/search_bar.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py index 5e9e409ee1..df77f5405e 100644 --- a/client/ayon_core/tools/loader/ui/search_bar.py +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -300,6 +300,12 @@ class FilterValueItemsView(QtWidgets.QWidget): def __init__(self, parent): super().__init__(parent) + filter_input = QtWidgets.QLineEdit(self) + + filter_timeout = QtCore.QTimer(self) + filter_timeout.setSingleShot(True) + filter_timeout.setInterval(20) + scroll_area = QtWidgets.QScrollArea(self) scroll_area.setObjectName("ScrollArea") srcoll_viewport = scroll_area.viewport() @@ -310,6 +316,7 @@ class FilterValueItemsView(QtWidgets.QWidget): content_widget = QtWidgets.QWidget(scroll_area) content_widget.setObjectName("ContentWidget") + content_layout = QtWidgets.QVBoxLayout(content_widget) content_layout.setContentsMargins(0, 0, 0, 0) @@ -331,19 +338,31 @@ class FilterValueItemsView(QtWidgets.QWidget): main_layout = QtWidgets.QVBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(filter_input, 0) main_layout.addWidget(scroll_area) main_layout.addWidget(btns_widget, 0) + filter_timeout.timeout.connect(self._on_filter_timeout) + filter_input.textChanged.connect(self._on_filter_change) select_all_btn.clicked.connect(self._on_select_all) clear_btn.clicked.connect(self._on_clear_selection) swap_btn.clicked.connect(self._on_swap_selection) + self._filter_timeout = filter_timeout + self._filter_input = filter_input self._btns_widget = btns_widget self._multiselection = False self._content_layout = content_layout self._last_selected_widget = None self._widgets_by_id = {} + def showEvent(self, event): + super().showEvent(event) + self._filter_timeout.start() + + def _on_filter_timeout(self): + self._filter_input.setFocus() + def set_value(self, value): current_value = self.get_value() if self._multiselection: @@ -453,6 +472,12 @@ class FilterValueItemsView(QtWidgets.QWidget): self._btns_widget.setVisible(self._multiselection) self._content_layout.addStretch(1) + def _on_filter_change(self, text): + text = text.lower() + for widget in self._widgets_by_id.values(): + visible = not text or text in widget.get_value().lower() + widget.setVisible(visible) + def _on_select_all(self): changed = False for widget in self._widgets_by_id.values(): From 5ebdd74bf728621a2d0fb605223c40855408b623 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 13 Jun 2025 15:49:23 +0200 Subject: [PATCH 079/155] reset filter text on fitlers change --- client/ayon_core/tools/loader/ui/search_bar.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py index df77f5405e..7108ea8b11 100644 --- a/client/ayon_core/tools/loader/ui/search_bar.py +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -446,8 +446,11 @@ class FilterValueItemsView(QtWidgets.QWidget): if widget is not None: widget.setVisible(False) widget.deleteLater() + self._widgets_by_id = {} self._last_selected_widget = None + # Change filter + self._filter_input.setText("") for item in items: widget_id = uuid.uuid4().hex From f1e93e980755387a77a62943e8b5329d6ecf82cf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 13 Jun 2025 15:59:40 +0200 Subject: [PATCH 080/155] close popups on enter press --- .../ayon_core/tools/loader/ui/search_bar.py | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py index 7108ea8b11..31069904b0 100644 --- a/client/ayon_core/tools/loader/ui/search_bar.py +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -199,6 +199,13 @@ class FiltersPopup(QtWidgets.QWidget): self._wrapper_layout = wraper_layout self._preferred_width = None + def keyPressEvent(self, event): + if event.key() in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return): + event.accept() + self.close() + return + super().keyPressEvent(event) + def set_preferred_width(self, width: int): self._preferred_width = width @@ -296,12 +303,15 @@ class FilterValueItemButton(BaseClickableFrame): class FilterValueItemsView(QtWidgets.QWidget): value_changed = QtCore.Signal() + close_requested = QtCore.Signal() def __init__(self, parent): super().__init__(parent) filter_input = QtWidgets.QLineEdit(self) + # Timeout is used to delay the filter focus change on 'showEvent' + # - the focus is changed to something else if is not delayed filter_timeout = QtCore.QTimer(self) filter_timeout.setSingleShot(True) filter_timeout.setInterval(20) @@ -344,6 +354,7 @@ class FilterValueItemsView(QtWidgets.QWidget): filter_timeout.timeout.connect(self._on_filter_timeout) filter_input.textChanged.connect(self._on_filter_change) + filter_input.returnPressed.connect(self.close_requested) select_all_btn.clicked.connect(self._on_select_all) clear_btn.clicked.connect(self._on_clear_selection) swap_btn.clicked.connect(self._on_swap_selection) @@ -360,8 +371,12 @@ class FilterValueItemsView(QtWidgets.QWidget): super().showEvent(event) self._filter_timeout.start() - def _on_filter_timeout(self): - self._filter_input.setFocus() + def keyPressEvent(self, event): + if event.key() in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return): + event.accept() + self.close_requested.emit() + return + super().keyPressEvent(event) def set_value(self, value): current_value = self.get_value() @@ -470,11 +485,16 @@ class FilterValueItemsView(QtWidgets.QWidget): "No items to select from...", self ) self._btns_widget.setVisible(False) + self._filter_input.setVisible(False) self._content_layout.addWidget(empty_label, 0) else: + self._filter_input.setVisible(True) self._btns_widget.setVisible(self._multiselection) self._content_layout.addStretch(1) + def _on_filter_timeout(self): + self._filter_input.setFocus() + def _on_filter_change(self, text): text = text.lower() for widget in self._widgets_by_id.values(): @@ -565,6 +585,7 @@ class FilterValuePopup(QtWidgets.QWidget): text_input.returnPressed.connect(self._text_confirmed) items_view.value_changed.connect(self._selection_changed) + items_view.close_requested.connect(self._close_requested) shadow_frame.stackUnder(wrapper) @@ -667,6 +688,9 @@ class FilterValuePopup(QtWidgets.QWidget): def _selection_changed(self): self.value_changed.emit(self._filter_name) + def _close_requested(self): + self.close() + class FiltersBar(BaseClickableFrame): filter_changed = QtCore.Signal(str) From 4180fcfdb4f6a12636eb812ed31c98bcd166a23e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 13 Jun 2025 16:07:34 +0200 Subject: [PATCH 081/155] added ctrl + F shortcut to show filter popup --- client/ayon_core/tools/loader/ui/search_bar.py | 6 +++--- client/ayon_core/tools/loader/ui/window.py | 9 +++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py index 31069904b0..8ab455a158 100644 --- a/client/ayon_core/tools/loader/ui/search_bar.py +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -808,9 +808,9 @@ class FiltersBar(BaseClickableFrame): self._filters_widget.setGeometry(geo) def _mouse_release_callback(self): - self._show_filters_popup() + self.show_filters_popup() - def _show_filters_popup(self): + def show_filters_popup(self): filter_defs = [ filter_def for filter_def in self._filter_defs_by_name.values() @@ -830,7 +830,7 @@ class FiltersBar(BaseClickableFrame): self._show_popup(filters_popup) def _on_filters_request(self): - self._show_filters_popup() + self.show_filters_popup() def _on_filter_request(self, filter_name: str): """Handle filter request from the popup.""" diff --git a/client/ayon_core/tools/loader/ui/window.py b/client/ayon_core/tools/loader/ui/window.py index a3a476b330..314a4f749e 100644 --- a/client/ayon_core/tools/loader/ui/window.py +++ b/client/ayon_core/tools/loader/ui/window.py @@ -346,6 +346,15 @@ class LoaderWindow(QtWidgets.QWidget): ctrl_pressed = QtCore.Qt.ControlModifier & modifiers # Grouping products on pressing Ctrl + G + if ( + ctrl_pressed + and event.key() == QtCore.Qt.Key_F + and not event.isAutoRepeat() + ): + self._search_bar.show_filters_popup() + event.setAccepted(True) + return + if ( ctrl_pressed and event.key() == QtCore.Qt.Key_G From 3f0e96cb6253cabdd4c379cd4d4222eb8366cdb5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 13 Jun 2025 16:08:47 +0200 Subject: [PATCH 082/155] better method position --- .../ayon_core/tools/loader/ui/search_bar.py | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py index 8ab455a158..9b74f63557 100644 --- a/client/ayon_core/tools/loader/ui/search_bar.py +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -745,6 +745,25 @@ class FiltersBar(BaseClickableFrame): super().resizeEvent(event) self._update_filters_geo() + def show_filters_popup(self): + filter_defs = [ + filter_def + for filter_def in self._filter_defs_by_name.values() + if filter_def.name not in self._widgets_by_name + ] + filters_popup = FiltersPopup(self) + filters_popup.filter_requested.connect(self._on_filter_request) + filters_popup.set_filter_items(filter_defs) + filters_popup.set_preferred_width(self.width()) + + old_popup, self._filters_popup = self._filters_popup, filters_popup + + self._filter_value_popup.setVisible(False) + old_popup.setVisible(False) + old_popup.deleteLater() + + self._show_popup(filters_popup) + def set_search_items(self, filter_defs: list[FilterDefinition]): self._filter_defs_by_name = { filter_def.name: filter_def @@ -810,25 +829,6 @@ class FiltersBar(BaseClickableFrame): def _mouse_release_callback(self): self.show_filters_popup() - def show_filters_popup(self): - filter_defs = [ - filter_def - for filter_def in self._filter_defs_by_name.values() - if filter_def.name not in self._widgets_by_name - ] - filters_popup = FiltersPopup(self) - filters_popup.filter_requested.connect(self._on_filter_request) - filters_popup.set_filter_items(filter_defs) - filters_popup.set_preferred_width(self.width()) - - old_popup, self._filters_popup = self._filters_popup, filters_popup - - self._filter_value_popup.setVisible(False) - old_popup.setVisible(False) - old_popup.deleteLater() - - self._show_popup(filters_popup) - def _on_filters_request(self): self.show_filters_popup() From d936c2f4d091bbe4a7e9887ec4deb927ad59ad04 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 13 Jun 2025 16:27:07 +0200 Subject: [PATCH 083/155] define key sequences --- client/ayon_core/tools/loader/ui/window.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/window.py b/client/ayon_core/tools/loader/ui/window.py index 314a4f749e..01d96410ed 100644 --- a/client/ayon_core/tools/loader/ui/window.py +++ b/client/ayon_core/tools/loader/ui/window.py @@ -23,6 +23,13 @@ from .info_widget import InfoWidget from .repres_widget import RepresentationsWidget from .search_bar import FiltersBar, FilterDefinition +FIND_KEY_SEQUENCE = QtGui.QKeySequence( + QtCore.Qt.Modifier.CTRL | QtCore.Qt.Key_F +) +GROUP_KEY_SEQUENCE = QtGui.QKeySequence( + QtCore.Qt.Modifier.CTRL | QtCore.Qt.Key_G +) + class LoadErrorMessageBox(ErrorMessageBox): def __init__(self, messages, parent=None): @@ -342,22 +349,18 @@ class LoaderWindow(QtWidgets.QWidget): self._reset_on_show = True def keyPressEvent(self, event): - modifiers = event.modifiers() - ctrl_pressed = QtCore.Qt.ControlModifier & modifiers - - # Grouping products on pressing Ctrl + G + combination = event.keyCombination() if ( - ctrl_pressed - and event.key() == QtCore.Qt.Key_F + FIND_KEY_SEQUENCE.matches(combination) and not event.isAutoRepeat() ): self._search_bar.show_filters_popup() event.setAccepted(True) return + # Grouping products on pressing Ctrl + G if ( - ctrl_pressed - and event.key() == QtCore.Qt.Key_G + GROUP_KEY_SEQUENCE.matches(combination) and not event.isAutoRepeat() ): self._show_group_dialog() From 26e2b45c9c4f00425a261a5c46de9dfd2451d709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Fri, 13 Jun 2025 16:41:12 +0200 Subject: [PATCH 084/155] Update client/ayon_core/tools/loader/abstract.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/tools/loader/abstract.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index d6a4bf40cb..1c3e30a109 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -215,22 +215,21 @@ class VersionItem: def __init__( self, - *, version_id: str, version: int, is_hero: bool, product_id: str, - task_id: Optional[str] = None, - thumbnail_id: Optional[str] = None, - published_time: Optional[str] = None, - author: Optional[str] = None, - status: Optional[str] = None, - frame_range: Optional[str] = None, - duration: Optional[int] = None, - handles: Optional[str] = None, - step: Optional[int] = None, - comment: Optional[str] = None, - source: Optional[str] = None, + task_id: Optional[str], + thumbnail_id: Optional[str], + published_time: Optional[str], + author: Optional[str], + status: Optional[str], + frame_range: Optional[str], + duration: Optional[int], + handles: Optional[str], + step: Optional[int], + comment: Optional[str], + source: Optional[str], ): self.version_id = version_id self.product_id = product_id From 1d66a86d799863aa51e2161fd0dbed5e337a6426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Fri, 13 Jun 2025 16:41:22 +0200 Subject: [PATCH 085/155] Update client/ayon_core/tools/loader/abstract.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/tools/loader/abstract.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index 1c3e30a109..804956f875 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -144,7 +144,6 @@ class ProductItem: folder_id: str, folder_label: str, version_items: dict[str, VersionItem], - *, product_in_scene: bool, ): self.product_id = product_id From 00f948e9ea08ed3706b9033b630c0372c8749c31 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 13 Jun 2025 16:45:56 +0200 Subject: [PATCH 086/155] fix qkeysequence match comparison --- client/ayon_core/tools/loader/ui/window.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/window.py b/client/ayon_core/tools/loader/ui/window.py index 01d96410ed..047c6fb159 100644 --- a/client/ayon_core/tools/loader/ui/window.py +++ b/client/ayon_core/tools/loader/ui/window.py @@ -351,7 +351,7 @@ class LoaderWindow(QtWidgets.QWidget): def keyPressEvent(self, event): combination = event.keyCombination() if ( - FIND_KEY_SEQUENCE.matches(combination) + FIND_KEY_SEQUENCE == combination and not event.isAutoRepeat() ): self._search_bar.show_filters_popup() @@ -360,7 +360,7 @@ class LoaderWindow(QtWidgets.QWidget): # Grouping products on pressing Ctrl + G if ( - GROUP_KEY_SEQUENCE.matches(combination) + GROUP_KEY_SEQUENCE == combination and not event.isAutoRepeat() ): self._show_group_dialog() From 83398c6a3e92b93181bd17224b9cd9c46531195a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 13 Jun 2025 16:52:29 +0200 Subject: [PATCH 087/155] auto-fill product name filter on typing in filters popup --- .../ayon_core/tools/loader/ui/search_bar.py | 42 ++++++++++++++++++- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py index 9b74f63557..d3e702c031 100644 --- a/client/ayon_core/tools/loader/ui/search_bar.py +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -172,6 +172,7 @@ class FilterItemButton(BaseClickableFrame): class FiltersPopup(QtWidgets.QWidget): filter_requested = QtCore.Signal(str) + text_filter_requested = QtCore.Signal(str) def __init__(self, parent): super().__init__(parent) @@ -204,6 +205,19 @@ class FiltersPopup(QtWidgets.QWidget): event.accept() self.close() return + + if event.key() not in ( + QtCore.Qt.Key_Escape, + QtCore.Qt.Key_Tab, + QtCore.Qt.Key_Backtab, + QtCore.Qt.Key_Backspace, + QtCore.Qt.Key_Return, + ): + text = event.text() + if text: + event.accept() + self.text_filter_requested.emit(text) + return super().keyPressEvent(event) def set_preferred_width(self, width: int): @@ -608,6 +622,17 @@ class FilterValuePopup(QtWidgets.QWidget): sh.setWidth(self._preferred_width) return sh + def set_text_filter(self, text: str): + if self._active_widget is None: + return + + if isinstance(self._active_widget, QtWidgets.QLineEdit): + full_text = self._active_widget.text() + text + self._active_widget.setText(full_text) + self._active_widget.setFocus() + self._active_widget.setCursorPosition(len(full_text)) + return + def set_filter_item( self, filter_def: FilterDefinition, @@ -726,7 +751,13 @@ class FiltersBar(BaseClickableFrame): main_layout.addWidget(search_btn, 0) main_layout.addWidget(filters_wrap, 1) + filters_popup = FiltersPopup(self) + filter_value_popup = FilterValuePopup(self) + search_btn.clicked.connect(self._on_filters_request) + filters_popup.text_filter_requested.connect( + self._on_text_filter_request + ) self._search_btn = search_btn self._filters_wrap = filters_wrap @@ -734,8 +765,8 @@ class FiltersBar(BaseClickableFrame): self._filters_layout = filters_layout self._widgets_by_name = {} self._filter_defs_by_name = {} - self._filters_popup = FiltersPopup(self) - self._filter_value_popup = FilterValuePopup(self) + self._filters_popup = filters_popup + self._filter_value_popup = filter_value_popup def showEvent(self, event): super().showEvent(event) @@ -753,6 +784,9 @@ class FiltersBar(BaseClickableFrame): ] filters_popup = FiltersPopup(self) filters_popup.filter_requested.connect(self._on_filter_request) + filters_popup.text_filter_requested.connect( + self._on_text_filter_request + ) filters_popup.set_filter_items(filter_defs) filters_popup.set_preferred_width(self.width()) @@ -832,6 +866,10 @@ class FiltersBar(BaseClickableFrame): def _on_filters_request(self): self.show_filters_popup() + def _on_text_filter_request(self, text: str): + self._on_filter_request("product_name") + self._filter_value_popup.set_text_filter(text) + def _on_filter_request(self, filter_name: str): """Handle filter request from the popup.""" self.add_item(filter_name) From d07dcb259021ebb22d8a295375a6bbcb0603f8ee Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 16 Jun 2025 15:26:57 +0200 Subject: [PATCH 088/155] fix keycombination --- client/ayon_core/tools/loader/ui/window.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/loader/ui/window.py b/client/ayon_core/tools/loader/ui/window.py index 047c6fb159..d056b62b13 100644 --- a/client/ayon_core/tools/loader/ui/window.py +++ b/client/ayon_core/tools/loader/ui/window.py @@ -349,7 +349,10 @@ class LoaderWindow(QtWidgets.QWidget): self._reset_on_show = True def keyPressEvent(self, event): - combination = event.keyCombination() + if hasattr(event, "keyCombination"): + combination = event.keyCombination() + else: + combination = QtGui.QKeySequence(event.modifiers() | event.key()) if ( FIND_KEY_SEQUENCE == combination and not event.isAutoRepeat() From 6006fcbb713e4724740775d3e59f1f50ed23973c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 16 Jun 2025 15:29:18 +0200 Subject: [PATCH 089/155] go to name filtering on backspace --- client/ayon_core/tools/loader/ui/search_bar.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py index d3e702c031..4620eb815f 100644 --- a/client/ayon_core/tools/loader/ui/search_bar.py +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -206,11 +206,17 @@ class FiltersPopup(QtWidgets.QWidget): self.close() return + if event.key() in ( + QtCore.Qt.Key_Backtab, + QtCore.Qt.Key_Backspace, + ): + self.text_filter_requested.emit("") + event.accept() + return + if event.key() not in ( QtCore.Qt.Key_Escape, QtCore.Qt.Key_Tab, - QtCore.Qt.Key_Backtab, - QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Return, ): text = event.text() From 7c6c054cd7638f40cf6bf5d68d41cc6f4fc32e5f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 16 Jun 2025 15:43:02 +0200 Subject: [PATCH 090/155] handle backspace --- .../ayon_core/tools/loader/ui/search_bar.py | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py index 4620eb815f..cb9dada37c 100644 --- a/client/ayon_core/tools/loader/ui/search_bar.py +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -14,6 +14,23 @@ from ayon_core.tools.utils import ( ) +def set_line_edit_focus( + widget: QtWidgets.QLineEdit, + *, + append_text: Optional[str] = None, + backspace: bool = False, +): + full_text = widget.text() + if backspace and full_text: + full_text = full_text[:-1] + + if append_text: + full_text += append_text + widget.setText(full_text) + widget.setFocus() + widget.setCursorPosition(len(full_text)) + + @dataclass class FilterDefinition: """Search bar definition. @@ -633,11 +650,13 @@ class FilterValuePopup(QtWidgets.QWidget): return if isinstance(self._active_widget, QtWidgets.QLineEdit): - full_text = self._active_widget.text() + text - self._active_widget.setText(full_text) - self._active_widget.setFocus() - self._active_widget.setCursorPosition(len(full_text)) - return + kwargs = {} + if text: + kwargs["append_text"] = text + else: + kwargs["backspace"] = True + + set_line_edit_focus(self._active_widget, **kwargs) def set_filter_item( self, From a3a14f08e4e337236c5dcfca30135dcd27ab3034 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 16 Jun 2025 15:43:15 +0200 Subject: [PATCH 091/155] don't force to add filter by name if is not defined --- client/ayon_core/tools/loader/ui/search_bar.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py index cb9dada37c..1687b17703 100644 --- a/client/ayon_core/tools/loader/ui/search_bar.py +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -892,6 +892,9 @@ class FiltersBar(BaseClickableFrame): self.show_filters_popup() def _on_text_filter_request(self, text: str): + if "product_name" not in self._filter_defs_by_name: + return + self._on_filter_request("product_name") self._filter_value_popup.set_text_filter(text) From bedd605d2e8186c8ada394743d3a27db3e8fbda5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 16 Jun 2025 15:43:28 +0200 Subject: [PATCH 092/155] change filter of list items on writing --- .../ayon_core/tools/loader/ui/search_bar.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py index 1687b17703..1a625cca6d 100644 --- a/client/ayon_core/tools/loader/ui/search_bar.py +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -413,6 +413,26 @@ class FilterValueItemsView(QtWidgets.QWidget): event.accept() self.close_requested.emit() return + + if event.key() in ( + QtCore.Qt.Key_Backtab, + QtCore.Qt.Key_Backspace, + ): + event.accept() + set_line_edit_focus(self._filter_input, backspace=True) + return + + if event.key() not in ( + QtCore.Qt.Key_Escape, + QtCore.Qt.Key_Tab, + QtCore.Qt.Key_Return, + ): + text = event.text() + if text: + event.accept() + set_line_edit_focus(self._filter_input, append_text=text) + return + super().keyPressEvent(event) def set_value(self, value): From cd7351aa5250b1772e21999a740cf49fab04e71e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 16 Jun 2025 15:47:33 +0200 Subject: [PATCH 093/155] added placeholder --- client/ayon_core/tools/loader/ui/search_bar.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py index 1a625cca6d..645d4644d6 100644 --- a/client/ayon_core/tools/loader/ui/search_bar.py +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -346,6 +346,7 @@ class FilterValueItemsView(QtWidgets.QWidget): super().__init__(parent) filter_input = QtWidgets.QLineEdit(self) + filter_input.setPlaceholderText("Filter items...") # Timeout is used to delay the filter focus change on 'showEvent' # - the focus is changed to something else if is not delayed From b9b801f53d128637551002dc22127b62f04b07d1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Jun 2025 09:12:37 +0200 Subject: [PATCH 094/155] support only shift modifier for auto-text filter --- client/ayon_core/tools/loader/ui/search_bar.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py index 645d4644d6..d9b4fc65c7 100644 --- a/client/ayon_core/tools/loader/ui/search_bar.py +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -231,7 +231,11 @@ class FiltersPopup(QtWidgets.QWidget): event.accept() return - if event.key() not in ( + valid_modifiers = event.modifiers() in ( + QtCore.Qt.NoModifier, + QtCore.Qt.ShiftModifier, + ) + if valid_modifiers and event.key() not in ( QtCore.Qt.Key_Escape, QtCore.Qt.Key_Tab, QtCore.Qt.Key_Return, @@ -423,7 +427,11 @@ class FilterValueItemsView(QtWidgets.QWidget): set_line_edit_focus(self._filter_input, backspace=True) return - if event.key() not in ( + valid_modifiers = event.modifiers() in ( + QtCore.Qt.NoModifier, + QtCore.Qt.ShiftModifier, + ) + if valid_modifiers and event.key() not in ( QtCore.Qt.Key_Escape, QtCore.Qt.Key_Tab, QtCore.Qt.Key_Return, From 48e29d57b846b48b64f39eaf5410e58644e0a0a4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Jun 2025 09:54:04 +0200 Subject: [PATCH 095/155] added back and confirm buttons --- client/ayon_core/style/style.css | 13 ++ .../ayon_core/tools/loader/ui/search_bar.py | 138 +++++++++++++++--- 2 files changed, 132 insertions(+), 19 deletions(-) diff --git a/client/ayon_core/style/style.css b/client/ayon_core/style/style.css index 400fde3077..82b958f812 100644 --- a/client/ayon_core/style/style.css +++ b/client/ayon_core/style/style.css @@ -907,6 +907,19 @@ FiltersBar #SearchButton { background: transparent; } +FiltersBar #BackButton { + background: transparent; +} + +FiltersBar #BackButton:hover { + background: {color:bg-buttons-hover}; +} + +FiltersBar #ConfirmButton { + background: #91CDFB; + color: #03344D; +} + FiltersPopup #PopupWrapper, FilterValuePopup #PopupWrapper { border-radius: 5px; background: {color:bg-inputs}; diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py index d9b4fc65c7..1e74426949 100644 --- a/client/ayon_core/tools/loader/ui/search_bar.py +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -11,6 +11,7 @@ from ayon_core.tools.utils import ( SquareButton, BaseClickableFrame, PixmapLabel, + SeparatorWidget, ) @@ -342,9 +343,88 @@ class FilterValueItemButton(BaseClickableFrame): self.selected.emit(self._widget_id) +class FilterValueTextInput(QtWidgets.QWidget): + back_requested = QtCore.Signal() + value_changed = QtCore.Signal(str) + close_requested = QtCore.Signal() + + def __init__(self, parent): + super().__init__(parent) + self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) + + # Timeout is used to delay the filter focus change on 'showEvent' + # - the focus is changed to something else if is not delayed + filter_timeout = QtCore.QTimer(self) + filter_timeout.setSingleShot(True) + filter_timeout.setInterval(20) + + btns_sep = SeparatorWidget(size=1, parent=self) + btns_widget = QtWidgets.QWidget(self) + btns_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) + + text_input = QtWidgets.QLineEdit(self) + + back_btn = QtWidgets.QPushButton("Back", btns_widget) + back_btn.setObjectName("BackButton") + back_btn.setIcon(get_qt_icon({ + "type": "material-symbols", + "name": "arrow_back", + })) + confirm_btn = QtWidgets.QPushButton("Confirm", btns_widget) + confirm_btn.setObjectName("ConfirmButton") + + btns_layout = QtWidgets.QHBoxLayout(btns_widget) + btns_layout.setContentsMargins(0, 0, 0, 0) + btns_layout.addWidget(back_btn, 0) + btns_layout.addStretch(1) + btns_layout.addWidget(confirm_btn, 0) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(text_input, 0) + main_layout.addWidget(btns_sep, 0) + main_layout.addWidget(btns_widget, 0) + + filter_timeout.timeout.connect(self._on_filter_timeout) + text_input.textChanged.connect(self.value_changed) + text_input.returnPressed.connect(self.close_requested) + back_btn.clicked.connect(self.back_requested) + confirm_btn.clicked.connect(self.close_requested) + + self._filter_timeout = filter_timeout + self._text_input = text_input + + def showEvent(self, event): + super().showEvent(event) + + self._filter_timeout.start() + + def get_value(self) -> str: + return self._text_input.text() + + def set_value(self, value: str): + self._text_input.setText(value) + + def set_placeholder_text(self, placeholder_text: str): + self._text_input.setPlaceholderText(placeholder_text) + + def set_text_filter(self, text: str): + kwargs = {} + if text: + kwargs["append_text"] = text + else: + kwargs["backspace"] = True + + set_line_edit_focus(self._text_input, **kwargs) + + def _on_filter_timeout(self): + set_line_edit_focus(self._text_input) + + class FilterValueItemsView(QtWidgets.QWidget): value_changed = QtCore.Signal() close_requested = QtCore.Signal() + back_requested = QtCore.Signal() def __init__(self, parent): super().__init__(parent) @@ -374,29 +454,46 @@ class FilterValueItemsView(QtWidgets.QWidget): scroll_area.setWidget(content_widget) + btns_sep = SeparatorWidget(size=1, parent=self) btns_widget = QtWidgets.QWidget(self) btns_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) + back_btn = QtWidgets.QPushButton("Back", btns_widget) + back_btn.setObjectName("BackButton") + back_btn.setIcon(get_qt_icon({ + "type": "material-symbols", + "name": "arrow_back", + })) + select_all_btn = QtWidgets.QPushButton("Select all", btns_widget) clear_btn = QtWidgets.QPushButton("Clear", btns_widget) - swap_btn = QtWidgets.QPushButton("Swap", btns_widget) + swap_btn = QtWidgets.QPushButton("Invert", btns_widget) + + confirm_btn = QtWidgets.QPushButton("Confirm", btns_widget) + confirm_btn.setObjectName("ConfirmButton") + confirm_btn.clicked.connect(self.close_requested) btns_layout = QtWidgets.QHBoxLayout(btns_widget) btns_layout.setContentsMargins(0, 0, 0, 0) + btns_layout.addWidget(back_btn, 0) btns_layout.addStretch(1) btns_layout.addWidget(select_all_btn, 0) btns_layout.addWidget(clear_btn, 0) btns_layout.addWidget(swap_btn, 0) + btns_layout.addStretch(1) + btns_layout.addWidget(confirm_btn, 0) main_layout = QtWidgets.QVBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) main_layout.addWidget(filter_input, 0) main_layout.addWidget(scroll_area) + main_layout.addWidget(btns_sep, 0) main_layout.addWidget(btns_widget, 0) filter_timeout.timeout.connect(self._on_filter_timeout) filter_input.textChanged.connect(self._on_filter_change) filter_input.returnPressed.connect(self.close_requested) + back_btn.clicked.connect(self.back_requested) select_all_btn.clicked.connect(self._on_select_all) clear_btn.clicked.connect(self._on_clear_selection) swap_btn.clicked.connect(self._on_swap_selection) @@ -619,6 +716,7 @@ class FilterValueItemsView(QtWidgets.QWidget): class FilterValuePopup(QtWidgets.QWidget): value_changed = QtCore.Signal(str) closed = QtCore.Signal(str) + back_requested = QtCore.Signal(str) def __init__(self, parent): super().__init__(parent) @@ -631,7 +729,7 @@ class FilterValuePopup(QtWidgets.QWidget): wrapper = QtWidgets.QWidget(self) wrapper.setObjectName("PopupWrapper") - text_input = QtWidgets.QLineEdit(wrapper) + text_input = FilterValueTextInput(wrapper) text_input.setVisible(False) items_view = FilterValueItemsView(wrapper) @@ -647,11 +745,13 @@ class FilterValuePopup(QtWidgets.QWidget): main_layout.setContentsMargins(2, 2, 2, 2) main_layout.addWidget(wrapper) - text_input.textChanged.connect(self._text_changed) - text_input.returnPressed.connect(self._text_confirmed) + text_input.value_changed.connect(self._text_changed) + text_input.close_requested.connect(self._close_requested) + text_input.back_requested.connect(self._back_requested) items_view.value_changed.connect(self._selection_changed) items_view.close_requested.connect(self._close_requested) + items_view.back_requested.connect(self._back_requested) shadow_frame.stackUnder(wrapper) @@ -678,14 +778,8 @@ class FilterValuePopup(QtWidgets.QWidget): if self._active_widget is None: return - if isinstance(self._active_widget, QtWidgets.QLineEdit): - kwargs = {} - if text: - kwargs["append_text"] = text - else: - kwargs["backspace"] = True - - set_line_edit_focus(self._active_widget, **kwargs) + if self._active_widget is self._text_input: + self._active_widget.set_text_filter(text) def set_filter_item( self, @@ -707,10 +801,10 @@ class FilterValuePopup(QtWidgets.QWidget): else: if value is None: value = "" - self._text_input.setPlaceholderText( + self._text_input.set_placeholder_text( filter_def.placeholder or "" ) - self._text_input.setText(value) + self._text_input.set_value(value) self._active_widget = self._text_input elif filter_def.filter_type == "list": @@ -750,26 +844,27 @@ class FilterValuePopup(QtWidgets.QWidget): def get_value(self): """Get the value from the active widget.""" if self._active_widget is self._text_input: - return self._text_input.text() + return self._text_input.get_value() elif self._active_widget is self._items_view: return self._active_widget.get_value() return None def _text_changed(self): """Handle text change in the text input.""" - if self._active_widget == self._text_input: + if self._active_widget is self._text_input: # Emit value changed signal if text input is active self.value_changed.emit(self._filter_name) - def _text_confirmed(self): - self.close() - def _selection_changed(self): self.value_changed.emit(self._filter_name) def _close_requested(self): self.close() + def _back_requested(self): + self.back_requested.emit(self._filter_name) + self.close() + class FiltersBar(BaseClickableFrame): filter_changed = QtCore.Signal(str) @@ -942,6 +1037,7 @@ class FiltersBar(BaseClickableFrame): filter_value_popup.set_filter_item(filter_def, value) filter_value_popup.value_changed.connect(self._on_filter_value_change) filter_value_popup.closed.connect(self._on_filter_value_closed) + filter_value_popup.back_requested.connect(self._on_filter_value_back) old_popup, self._filter_value_popup = ( self._filter_value_popup, filter_value_popup @@ -978,6 +1074,10 @@ class FiltersBar(BaseClickableFrame): if not value: self._on_item_close_requested(name) + def _on_filter_value_back(self, name): + self._on_filter_value_closed(name) + self.show_filters_popup() + def _on_item_close_requested(self, name): widget = self._widgets_by_name.pop(name, None) if widget is not None: From 42808409894750c8494a07af887146fbaf8ef9f1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Jun 2025 10:50:52 +0200 Subject: [PATCH 096/155] added wheel scrolling --- .../ayon_core/tools/loader/ui/search_bar.py | 47 ++++++++++++++++--- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/search_bar.py b/client/ayon_core/tools/loader/ui/search_bar.py index 1e74426949..ab673df1ac 100644 --- a/client/ayon_core/tools/loader/ui/search_bar.py +++ b/client/ayon_core/tools/loader/ui/search_bar.py @@ -925,6 +925,24 @@ class FiltersBar(BaseClickableFrame): super().resizeEvent(event) self._update_filters_geo() + def wheelEvent(self, event): + scroll_speed = 15 + diff = event.angleDelta().y() / 120.0 + pos_x = self._filters_widget.pos().x() + if diff > 0: + pos_x = min(0, pos_x + scroll_speed) + self._filters_widget.move(pos_x, 0) + return + + rect = self._filters_wrap.rect() + size_hint = self._filters_widget.sizeHint() + if size_hint.width() < rect.width(): + return + pos_x = max( + pos_x - scroll_speed, rect.width() - size_hint.width() + ) + self._filters_widget.move(pos_x, 0) + def show_filters_popup(self): filter_defs = [ filter_def @@ -1009,6 +1027,18 @@ class FiltersBar(BaseClickableFrame): self._filters_widget.setGeometry(geo) + def _reposition_filters_widget(self): + rect = self._filters_wrap.rect() + size_hint = self._filters_widget.sizeHint() + if size_hint.width() < rect.width(): + self._filters_widget.move(0, 0) + return + pos_x = self._filters_widget.pos().x() + pos_x = max( + pos_x, rect.width() - size_hint.width() + ) + self._filters_widget.move(pos_x, 0) + def _mouse_release_callback(self): self.show_filters_popup() @@ -1080,10 +1110,13 @@ class FiltersBar(BaseClickableFrame): def _on_item_close_requested(self, name): widget = self._widgets_by_name.pop(name, None) - if widget is not None: - idx = self._filters_layout.indexOf(widget) - if idx > -1: - self._filters_layout.takeAt(idx) - widget.setVisible(False) - widget.deleteLater() - self.filter_changed.emit(name) + if widget is None: + return + idx = self._filters_layout.indexOf(widget) + if idx > -1: + self._filters_layout.takeAt(idx) + widget.setVisible(False) + widget.deleteLater() + self.filter_changed.emit(name) + + self._reposition_filters_widget() From f4af01f702b7f7fc339f19231e0b88a7ee56fc33 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 18 Jun 2025 18:59:39 +0200 Subject: [PATCH 097/155] :burn: remove `TypedDict` to retain compatibility with pythpn 3.7 but we should get it back (or dataclasses) when we get out of Middle Ages. --- client/ayon_core/tools/loader/abstract.py | 169 +++------------------- 1 file changed, 17 insertions(+), 152 deletions(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index 804956f875..de0a1c7dd8 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -2,7 +2,7 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import List, Optional, TypedDict +from typing import Any, List, Optional from ayon_core.lib.attribute_definitions import ( AbstractAttrDef, @@ -10,62 +10,16 @@ from ayon_core.lib.attribute_definitions import ( serialize_attr_defs, ) -IconData = TypedDict("IconData", { - "type": str, - "name": str, - "color": str -}) - -ProductBaseTypeItemData = TypedDict("ProductBaseTypeItemData", { - "name": str, - "icon": IconData -}) - - -VersionItemData = TypedDict("VersionItemData", { - "version_id": str, - "version": int, - "is_hero": bool, - "product_id": str, - "task_id": Optional[str], - "thumbnail_id": Optional[str], - "published_time": Optional[str], - "author": Optional[str], - "status": Optional[str], - "frame_range": Optional[str], - "duration": Optional[int], - "handles": Optional[str], - "step": Optional[int], - "comment": Optional[str], - "source": Optional[str] -}) - - -ProductItemData = TypedDict("ProductItemData", { - "product_id": str, - "product_type": str, - "product_base_type": str, - "product_name": str, - "product_icon": IconData, - "product_type_icon": IconData, - "product_base_type_icon": IconData, - "group_name": str, - "folder_id": str, - "folder_label": str, - "version_items": dict[str, VersionItemData], - "product_in_scene": bool -}) - class ProductTypeItem: """Item representing product type. Args: name (str): Product type name. - icon (IconData): Product type icon definition. + icon (dict[str, str]): Product type icon definition. """ - def __init__(self, name: str, icon: IconData): + def __init__(self, name: str, icon: dict[str, str]): self.name = name self.icon = icon @@ -83,16 +37,16 @@ class ProductTypeItem: class ProductBaseTypeItem: """Item representing the product base type.""" - def __init__(self, name: str, icon: IconData): + def __init__(self, name: str, icon: dict[str, str]): """Initialize product base type item.""" self.name = name self.icon = icon - def to_data(self) -> ProductBaseTypeItemData: + def to_data(self) -> dict[str, Any]: """Convert item to data dictionary. Returns: - ProductBaseTypeItemData: Data representation of the item. + dict[str, Any]: Data representation of the item. """ return { @@ -102,11 +56,11 @@ class ProductBaseTypeItem: @classmethod def from_data( - cls, data: ProductBaseTypeItemData) -> ProductBaseTypeItem: + cls, data: dict[str, Any]) -> ProductBaseTypeItem: """Create item from data dictionary. Args: - data (ProductBaseTypeItemData): Data to create item from. + data (dict[str, Any]): Data to create item from. Returns: ProductBaseTypeItem: Item created from the provided data. @@ -122,8 +76,8 @@ class ProductItem: product_id (str): Product id. product_type (str): Product type. product_name (str): Product name. - product_icon (IconData): Product icon definition. - product_type_icon (IconData): Product type icon definition. + product_icon (dict[str, str]): Product icon definition. + product_type_icon (dict[str, str]): Product type icon definition. product_in_scene (bool): Is product in scene (only when used in DCC). group_name (str): Group name. folder_id (str): Folder id. @@ -137,9 +91,9 @@ class ProductItem: product_type: str, product_base_type: str, product_name: str, - product_icon: IconData, - product_type_icon: IconData, - product_base_type_icon: IconData, + product_icon: dict[str, str], + product_type_icon: dict[str, str], + product_base_type_icon: dict[str, str], group_name: str, folder_id: str, folder_label: str, @@ -159,7 +113,7 @@ class ProductItem: self.folder_label = folder_label self.version_items = version_items - def to_data(self) -> ProductItemData: + def to_data(self) -> dict[str, Any]: return { "product_id": self.product_id, "product_type": self.product_type, @@ -408,10 +362,8 @@ class ActionItem: # future development of detached UI tools it would be better to be # prepared for it. raise NotImplementedError( - "{}.to_data is not implemented. Use Attribute definitions" - " from 'ayon_core.lib' instead of 'qargparse'.".format( - self.__class__.__name__ - ) + f"{self.__class__.__name__}.to_data is not implemented. Use Attribute definitions" + " from 'ayon_core.lib' instead of 'qargparse'." ) def to_data(self): @@ -470,8 +422,6 @@ class _BaseLoaderController(ABC): dict[str, Union[str, None]]: Context data. """ - pass - @abstractmethod def reset(self): """Reset all cached data to reload everything. @@ -480,8 +430,6 @@ class _BaseLoaderController(ABC): "controller.reset.finished". """ - pass - # Model wrappers @abstractmethod def get_folder_items(self, project_name, sender=None): @@ -495,8 +443,6 @@ class _BaseLoaderController(ABC): list[FolderItem]: Folder items for the project. """ - pass - # Expected selection helpers @abstractmethod def get_expected_selection_data(self): @@ -510,8 +456,6 @@ class _BaseLoaderController(ABC): dict[str, Any]: Expected selection data. """ - pass - @abstractmethod def set_expected_selection(self, project_name, folder_id): """Set expected selection. @@ -521,8 +465,6 @@ class _BaseLoaderController(ABC): folder_id (str): Id of folder to be selected. """ - pass - class BackendLoaderController(_BaseLoaderController): """Backend loader controller abstraction. @@ -542,8 +484,6 @@ class BackendLoaderController(_BaseLoaderController): source (Optional[str]): Event source. """ - pass - @abstractmethod def get_loaded_product_ids(self): """Return set of loaded product ids. @@ -552,8 +492,6 @@ class BackendLoaderController(_BaseLoaderController): set[str]: Set of loaded product ids. """ - pass - class FrontendLoaderController(_BaseLoaderController): @abstractmethod @@ -565,8 +503,6 @@ class FrontendLoaderController(_BaseLoaderController): callback (func): Callback triggered when the event is emitted. """ - pass - # Expected selection helpers @abstractmethod def expected_project_selected(self, project_name): @@ -576,8 +512,6 @@ class FrontendLoaderController(_BaseLoaderController): project_name (str): Project name. """ - pass - @abstractmethod def expected_folder_selected(self, folder_id): """Expected folder was selected in frontend. @@ -586,8 +520,6 @@ class FrontendLoaderController(_BaseLoaderController): folder_id (str): Folder id. """ - pass - # Model wrapper calls @abstractmethod def get_project_items(self, sender=None): @@ -609,8 +541,6 @@ class FrontendLoaderController(_BaseLoaderController): list[ProjectItem]: List of project items. """ - pass - @abstractmethod def get_folder_type_items(self, project_name, sender=None): """Folder type items for a project. @@ -629,7 +559,6 @@ class FrontendLoaderController(_BaseLoaderController): list[FolderTypeItem]: Folder type information. """ - pass @abstractmethod def get_task_items(self, project_name, folder_ids, sender=None): @@ -644,7 +573,6 @@ class FrontendLoaderController(_BaseLoaderController): list[TaskItem]: List of task items. """ - pass @abstractmethod def get_task_type_items(self, project_name, sender=None): @@ -664,7 +592,6 @@ class FrontendLoaderController(_BaseLoaderController): list[TaskTypeItem]: Task type information. """ - pass @abstractmethod def get_folder_labels(self, project_name, folder_ids): @@ -678,7 +605,6 @@ class FrontendLoaderController(_BaseLoaderController): dict[str, Optional[str]]: Folder labels by folder id. """ - pass @abstractmethod def get_project_status_items(self, project_name, sender=None): @@ -699,8 +625,6 @@ class FrontendLoaderController(_BaseLoaderController): list[StatusItem]: List of status items. """ - pass - @abstractmethod def get_product_items(self, project_name, folder_ids, sender=None): """Product items for folder ids. @@ -722,8 +646,6 @@ class FrontendLoaderController(_BaseLoaderController): list[ProductItem]: List of product items. """ - pass - @abstractmethod def get_product_item(self, project_name, product_id): """Receive single product item. @@ -736,8 +658,6 @@ class FrontendLoaderController(_BaseLoaderController): Union[ProductItem, None]: Product info or None if not found. """ - pass - @abstractmethod def get_product_type_items(self, project_name): """Product type items for a project. @@ -751,8 +671,6 @@ class FrontendLoaderController(_BaseLoaderController): list[ProductTypeItem]: List of product type items for a project. """ - pass - @abstractmethod def get_representation_items( self, project_name, version_ids, sender=None @@ -776,8 +694,6 @@ class FrontendLoaderController(_BaseLoaderController): list[RepreItem]: List of representation items. """ - pass - @abstractmethod def get_version_thumbnail_ids(self, project_name, version_ids): """Get thumbnail ids for version ids. @@ -790,8 +706,6 @@ class FrontendLoaderController(_BaseLoaderController): dict[str, Union[str, Any]]: Thumbnail id by version id. """ - pass - @abstractmethod def get_folder_thumbnail_ids(self, project_name, folder_ids): """Get thumbnail ids for folder ids. @@ -804,14 +718,11 @@ class FrontendLoaderController(_BaseLoaderController): dict[str, Union[str, Any]]: Thumbnail id by folder id. """ - pass - @abstractmethod def get_versions_representation_count( self, project_name, version_ids, sender=None ): - """ - Args: + """Args: project_name (str): Project name. version_ids (Iterable[str]): Version ids. sender (Optional[str]): Sender who requested the items. @@ -820,8 +731,6 @@ class FrontendLoaderController(_BaseLoaderController): dict[str, int]: Representation count by version id. """ - pass - @abstractmethod def get_thumbnail_paths( self, @@ -844,8 +753,6 @@ class FrontendLoaderController(_BaseLoaderController): dict[str, Union[str, None]]: Thumbnail path by entity id. """ - pass - # Selection model wrapper calls @abstractmethod def get_selected_project_name(self): @@ -857,8 +764,6 @@ class FrontendLoaderController(_BaseLoaderController): Union[str, None]: Selected project name. """ - pass - @abstractmethod def get_selected_folder_ids(self): """Get selected folder ids. @@ -869,7 +774,6 @@ class FrontendLoaderController(_BaseLoaderController): list[str]: Selected folder ids. """ - pass @abstractmethod def get_selected_task_ids(self): @@ -881,7 +785,6 @@ class FrontendLoaderController(_BaseLoaderController): list[str]: Selected folder ids. """ - pass @abstractmethod def set_selected_tasks(self, task_ids): @@ -891,7 +794,6 @@ class FrontendLoaderController(_BaseLoaderController): task_ids (Iterable[str]): Selected task ids. """ - pass @abstractmethod def get_selected_version_ids(self): @@ -903,7 +805,6 @@ class FrontendLoaderController(_BaseLoaderController): list[str]: Selected version ids. """ - pass @abstractmethod def get_selected_representation_ids(self): @@ -915,8 +816,6 @@ class FrontendLoaderController(_BaseLoaderController): list[str]: Selected representation ids. """ - pass - @abstractmethod def set_selected_project(self, project_name): """Set selected project. @@ -931,8 +830,6 @@ class FrontendLoaderController(_BaseLoaderController): project_name (Union[str, None]): Selected project name. """ - pass - @abstractmethod def set_selected_folders(self, folder_ids): """Set selected folders. @@ -948,8 +845,6 @@ class FrontendLoaderController(_BaseLoaderController): folder_ids (Iterable[str]): Selected folder ids. """ - pass - @abstractmethod def set_selected_versions(self, version_ids): """Set selected versions. @@ -966,8 +861,6 @@ class FrontendLoaderController(_BaseLoaderController): version_ids (Iterable[str]): Selected version ids. """ - pass - @abstractmethod def set_selected_representations(self, repre_ids): """Set selected representations. @@ -985,8 +878,6 @@ class FrontendLoaderController(_BaseLoaderController): repre_ids (Iterable[str]): Selected representation ids. """ - pass - # Load action items @abstractmethod def get_versions_action_items(self, project_name, version_ids): @@ -1000,8 +891,6 @@ class FrontendLoaderController(_BaseLoaderController): list[ActionItem]: List of action items. """ - pass - @abstractmethod def get_representations_action_items( self, project_name, representation_ids @@ -1016,8 +905,6 @@ class FrontendLoaderController(_BaseLoaderController): list[ActionItem]: List of action items. """ - pass - @abstractmethod def trigger_action_item( self, @@ -1050,8 +937,6 @@ class FrontendLoaderController(_BaseLoaderController): representation_ids (Iterable[str]): Representation ids. """ - pass - @abstractmethod def change_products_group(self, project_name, product_ids, group_name): """Change group of products. @@ -1070,8 +955,6 @@ class FrontendLoaderController(_BaseLoaderController): group_name (str): New group name. """ - pass - @abstractmethod def fill_root_in_source(self, source): """Fill root in source path. @@ -1081,8 +964,6 @@ class FrontendLoaderController(_BaseLoaderController): rootless workfile path. """ - pass - # NOTE: Methods 'is_loaded_products_supported' and # 'is_standard_projects_filter_enabled' are both based on being in host # or not. Maybe we could implement only single method 'is_in_host'? @@ -1094,8 +975,6 @@ class FrontendLoaderController(_BaseLoaderController): bool: True if it is supported. """ - pass - @abstractmethod def is_standard_projects_filter_enabled(self): """Is standard projects filter enabled. @@ -1108,8 +987,6 @@ class FrontendLoaderController(_BaseLoaderController): current context project. """ - pass - # Site sync functions @abstractmethod def is_sitesync_enabled(self, project_name=None): @@ -1127,8 +1004,6 @@ class FrontendLoaderController(_BaseLoaderController): bool: True if site sync is enabled. """ - pass - @abstractmethod def get_active_site_icon_def(self, project_name): """Active site icon definition. @@ -1141,8 +1016,6 @@ class FrontendLoaderController(_BaseLoaderController): is not enabled for the project. """ - pass - @abstractmethod def get_remote_site_icon_def(self, project_name): """Remote site icon definition. @@ -1155,8 +1028,6 @@ class FrontendLoaderController(_BaseLoaderController): is not enabled for the project. """ - pass - @abstractmethod def get_version_sync_availability(self, project_name, version_ids): """Version sync availability. @@ -1169,8 +1040,6 @@ class FrontendLoaderController(_BaseLoaderController): dict[str, tuple[int, int]]: Sync availability by version id. """ - pass - @abstractmethod def get_representations_sync_status( self, project_name, representation_ids @@ -1185,8 +1054,6 @@ class FrontendLoaderController(_BaseLoaderController): dict[str, tuple[int, int]]: Sync status by representation id. """ - pass - @abstractmethod def get_product_types_filter(self): """Return product type filter for current context. @@ -1194,5 +1061,3 @@ class FrontendLoaderController(_BaseLoaderController): Returns: ProductTypesFilter: Product type filter for current context """ - - pass From ae1bfc71f78bd43a0e930387561178c799184c43 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 18 Jun 2025 19:00:35 +0200 Subject: [PATCH 098/155] :recycle: change loader filtering --- client/ayon_core/pipeline/load/plugins.py | 81 ++++++++++++++--------- 1 file changed, 48 insertions(+), 33 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 32b96e3e7a..5f206df364 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -1,16 +1,18 @@ """Plugins for loading representations and products into host applications.""" from __future__ import annotations -import os -import logging -from ayon_core.settings import get_project_settings +import logging +import os + from ayon_core.pipeline.plugin_discover import ( + deregister_plugin, + deregister_plugin_path, discover, register_plugin, register_plugin_path, - deregister_plugin, - deregister_plugin_path ) +from ayon_core.settings import get_project_settings + from .utils import get_representation_path_from_context @@ -61,12 +63,12 @@ class LoaderPlugin(list): if not plugin_settings: return - print(">>> We have preset for {}".format(plugin_name)) + print(f">>> We have preset for {plugin_name}") for option, value in plugin_settings.items(): if option == "enabled" and value is False: print(" - is disabled by preset") else: - print(" - setting `{}`: `{}`".format(option, value)) + print(f" - setting `{option}`: `{value}`") setattr(cls, option, value) @classmethod @@ -79,7 +81,6 @@ class LoaderPlugin(list): Returns: bool: Representation has valid extension """ - if "*" in cls.extensions: return True @@ -122,10 +123,21 @@ class LoaderPlugin(list): Returns: bool: Is loader compatible for context. """ + """ + product_types: set[str] = set() + product_base_types: set[str] = set() + representations = set() + extensions = {"*"} + """ plugin_repre_names = cls.get_representations() plugin_product_types = cls.product_types plugin_product_base_types = cls.product_base_types + repre_entity = context.get("representation") + product_entity = context["product"] + + # If no representation names, product types or extensions are defined + # then loader is not compatible with any context. if ( not plugin_repre_names or (not plugin_product_types and not plugin_product_base_types) @@ -133,38 +145,45 @@ class LoaderPlugin(list): ): return False - repre_entity = context.get("representation") + # If no representation entity is provided then loader is not + # compatible with context. if not repre_entity: return False + # Check the compatibility with the representation names. plugin_repre_names = set(plugin_repre_names) - if ( + if not plugin_repre_names or ( "*" not in plugin_repre_names and repre_entity["name"] not in plugin_repre_names ): return False + # Check the compatibility with the extension of the representation. if not cls.has_valid_extension(repre_entity): return False plugin_product_types = set(plugin_product_types) - if "*" in plugin_product_types: - return True - - plugin_product_base_types = set(plugin_product_base_types) - if "*" in plugin_product_base_types: - # If plugin supports all product base types, then it is compatible - # with any product type. - return True - - product_entity = context["product"] - product_type = product_entity["productType"] + product_type = product_entity.get("productType") product_base_type = product_entity.get("productBaseType") - if product_type in plugin_product_types: + # Use product base type if defined, otherwise use product type. + product_filter = product_base_type + # If there is no product base type defined in the product entity, + # then we will use the product type. + if product_filter is None: + product_filter = product_type + + # If no product type isn't defined on the loader plugin, + # then we will use the product types. + plugin_product_filter = ( + plugin_product_base_types or plugin_product_types) + + # If wildcard is used in product types or base types, + # then we will consider the loader compatible with any product type. + if "*" in plugin_product_filter: return True - return product_base_type in plugin_product_base_types + return product_filter in plugin_product_filter @classmethod def get_representations(cls): @@ -219,19 +238,17 @@ class LoaderPlugin(list): bool: Whether the container was deleted """ - raise NotImplementedError("Loader.remove() must be " "implemented by subclass") @classmethod def get_options(cls, contexts): - """ - Returns static (cls) options or could collect from 'contexts'. + """Returns static (cls) options or could collect from 'contexts'. - Args: - contexts (list): of repre or product contexts - Returns: - (list) + Args: + contexts (list): of repre or product contexts + Returns: + (list) """ return cls.options or [] @@ -279,9 +296,7 @@ def discover_loader_plugins(project_name=None): plugin.apply_settings(project_settings) except Exception: log.warning( - "Failed to apply settings to loader {}".format( - plugin.__name__ - ), + f"Failed to apply settings to loader {plugin.__name__}", exc_info=True ) return plugins From 1c63b75a272421e2f0ebdd88df791802b9097204 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 19 Jun 2025 10:06:29 +0200 Subject: [PATCH 099/155] :recycle: make product type and product base types None by default --- client/ayon_core/pipeline/load/plugins.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 5f206df364..966b418db8 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging import os +from typing import Optional from ayon_core.pipeline.plugin_discover import ( deregister_plugin, @@ -19,8 +20,8 @@ from .utils import get_representation_path_from_context class LoaderPlugin(list): """Load representation into host application""" - product_types: set[str] = set() - product_base_types: set[str] = set() + product_types: Optional[set[str]] = None + product_base_types: Optional[set[str]] = None representations = set() extensions = {"*"} order = 0 From a3c04d232a6c91ed303ca40f1b4386601be0d21c Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 19 Jun 2025 10:07:20 +0200 Subject: [PATCH 100/155] :recycle: revert more `TypedDict` changes and fix line length --- client/ayon_core/tools/loader/abstract.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index de0a1c7dd8..c585160672 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -242,7 +242,7 @@ class VersionItem: def __le__(self, other): return self.__eq__(other) or self.__lt__(other) - def to_data(self) -> VersionItemData: + def to_data(self) -> dict[str, Any]: return { "version_id": self.version_id, "product_id": self.product_id, @@ -262,7 +262,7 @@ class VersionItem: } @classmethod - def from_data(cls, data: VersionItemData): + def from_data(cls, data: dict[str, Any]) -> VersionItem: return cls(**data) @@ -362,8 +362,9 @@ class ActionItem: # future development of detached UI tools it would be better to be # prepared for it. raise NotImplementedError( - f"{self.__class__.__name__}.to_data is not implemented. Use Attribute definitions" - " from 'ayon_core.lib' instead of 'qargparse'." + f"{self.__class__.__name__}.to_data is not implemented. " + "Use Attribute definitions " + "from 'ayon_core.lib' instead of 'qargparse'." ) def to_data(self): From e003ef2960727bdc997da5a737f3458786237af0 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 19 Jun 2025 10:15:02 +0200 Subject: [PATCH 101/155] :fire: revert some code cleanup --- client/ayon_core/tools/loader/abstract.py | 96 ++++++++++++++++++++++- 1 file changed, 92 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index c585160672..21f2349544 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -362,9 +362,10 @@ class ActionItem: # future development of detached UI tools it would be better to be # prepared for it. raise NotImplementedError( - f"{self.__class__.__name__}.to_data is not implemented. " - "Use Attribute definitions " - "from 'ayon_core.lib' instead of 'qargparse'." + "{}.to_data is not implemented. Use Attribute definitions" + " from 'ayon_core.lib' instead of 'qargparse'.".format( + self.__class__.__name__ + ) ) def to_data(self): @@ -423,6 +424,8 @@ class _BaseLoaderController(ABC): dict[str, Union[str, None]]: Context data. """ + pass + @abstractmethod def reset(self): """Reset all cached data to reload everything. @@ -431,6 +434,8 @@ class _BaseLoaderController(ABC): "controller.reset.finished". """ + pass + # Model wrappers @abstractmethod def get_folder_items(self, project_name, sender=None): @@ -444,6 +449,8 @@ class _BaseLoaderController(ABC): list[FolderItem]: Folder items for the project. """ + pass + # Expected selection helpers @abstractmethod def get_expected_selection_data(self): @@ -457,6 +464,8 @@ class _BaseLoaderController(ABC): dict[str, Any]: Expected selection data. """ + pass + @abstractmethod def set_expected_selection(self, project_name, folder_id): """Set expected selection. @@ -466,6 +475,8 @@ class _BaseLoaderController(ABC): folder_id (str): Id of folder to be selected. """ + pass + class BackendLoaderController(_BaseLoaderController): """Backend loader controller abstraction. @@ -485,6 +496,8 @@ class BackendLoaderController(_BaseLoaderController): source (Optional[str]): Event source. """ + pass + @abstractmethod def get_loaded_product_ids(self): """Return set of loaded product ids. @@ -493,6 +506,8 @@ class BackendLoaderController(_BaseLoaderController): set[str]: Set of loaded product ids. """ + pass + class FrontendLoaderController(_BaseLoaderController): @abstractmethod @@ -504,6 +519,8 @@ class FrontendLoaderController(_BaseLoaderController): callback (func): Callback triggered when the event is emitted. """ + pass + # Expected selection helpers @abstractmethod def expected_project_selected(self, project_name): @@ -513,6 +530,8 @@ class FrontendLoaderController(_BaseLoaderController): project_name (str): Project name. """ + pass + @abstractmethod def expected_folder_selected(self, folder_id): """Expected folder was selected in frontend. @@ -521,6 +540,8 @@ class FrontendLoaderController(_BaseLoaderController): folder_id (str): Folder id. """ + pass + # Model wrapper calls @abstractmethod def get_project_items(self, sender=None): @@ -542,6 +563,8 @@ class FrontendLoaderController(_BaseLoaderController): list[ProjectItem]: List of project items. """ + pass + @abstractmethod def get_folder_type_items(self, project_name, sender=None): """Folder type items for a project. @@ -560,6 +583,7 @@ class FrontendLoaderController(_BaseLoaderController): list[FolderTypeItem]: Folder type information. """ + pass @abstractmethod def get_task_items(self, project_name, folder_ids, sender=None): @@ -574,6 +598,7 @@ class FrontendLoaderController(_BaseLoaderController): list[TaskItem]: List of task items. """ + pass @abstractmethod def get_task_type_items(self, project_name, sender=None): @@ -593,6 +618,7 @@ class FrontendLoaderController(_BaseLoaderController): list[TaskTypeItem]: Task type information. """ + pass @abstractmethod def get_folder_labels(self, project_name, folder_ids): @@ -606,6 +632,7 @@ class FrontendLoaderController(_BaseLoaderController): dict[str, Optional[str]]: Folder labels by folder id. """ + pass @abstractmethod def get_project_status_items(self, project_name, sender=None): @@ -626,6 +653,8 @@ class FrontendLoaderController(_BaseLoaderController): list[StatusItem]: List of status items. """ + pass + @abstractmethod def get_product_items(self, project_name, folder_ids, sender=None): """Product items for folder ids. @@ -647,6 +676,8 @@ class FrontendLoaderController(_BaseLoaderController): list[ProductItem]: List of product items. """ + pass + @abstractmethod def get_product_item(self, project_name, product_id): """Receive single product item. @@ -659,6 +690,8 @@ class FrontendLoaderController(_BaseLoaderController): Union[ProductItem, None]: Product info or None if not found. """ + pass + @abstractmethod def get_product_type_items(self, project_name): """Product type items for a project. @@ -672,6 +705,8 @@ class FrontendLoaderController(_BaseLoaderController): list[ProductTypeItem]: List of product type items for a project. """ + pass + @abstractmethod def get_representation_items( self, project_name, version_ids, sender=None @@ -695,6 +730,8 @@ class FrontendLoaderController(_BaseLoaderController): list[RepreItem]: List of representation items. """ + pass + @abstractmethod def get_version_thumbnail_ids(self, project_name, version_ids): """Get thumbnail ids for version ids. @@ -707,6 +744,8 @@ class FrontendLoaderController(_BaseLoaderController): dict[str, Union[str, Any]]: Thumbnail id by version id. """ + pass + @abstractmethod def get_folder_thumbnail_ids(self, project_name, folder_ids): """Get thumbnail ids for folder ids. @@ -719,11 +758,14 @@ class FrontendLoaderController(_BaseLoaderController): dict[str, Union[str, Any]]: Thumbnail id by folder id. """ + pass + @abstractmethod def get_versions_representation_count( self, project_name, version_ids, sender=None ): - """Args: + """ + Args: project_name (str): Project name. version_ids (Iterable[str]): Version ids. sender (Optional[str]): Sender who requested the items. @@ -732,6 +774,8 @@ class FrontendLoaderController(_BaseLoaderController): dict[str, int]: Representation count by version id. """ + pass + @abstractmethod def get_thumbnail_paths( self, @@ -754,6 +798,8 @@ class FrontendLoaderController(_BaseLoaderController): dict[str, Union[str, None]]: Thumbnail path by entity id. """ + pass + # Selection model wrapper calls @abstractmethod def get_selected_project_name(self): @@ -765,6 +811,8 @@ class FrontendLoaderController(_BaseLoaderController): Union[str, None]: Selected project name. """ + pass + @abstractmethod def get_selected_folder_ids(self): """Get selected folder ids. @@ -775,6 +823,7 @@ class FrontendLoaderController(_BaseLoaderController): list[str]: Selected folder ids. """ + pass @abstractmethod def get_selected_task_ids(self): @@ -786,6 +835,7 @@ class FrontendLoaderController(_BaseLoaderController): list[str]: Selected folder ids. """ + pass @abstractmethod def set_selected_tasks(self, task_ids): @@ -795,6 +845,7 @@ class FrontendLoaderController(_BaseLoaderController): task_ids (Iterable[str]): Selected task ids. """ + pass @abstractmethod def get_selected_version_ids(self): @@ -806,6 +857,7 @@ class FrontendLoaderController(_BaseLoaderController): list[str]: Selected version ids. """ + pass @abstractmethod def get_selected_representation_ids(self): @@ -817,6 +869,8 @@ class FrontendLoaderController(_BaseLoaderController): list[str]: Selected representation ids. """ + pass + @abstractmethod def set_selected_project(self, project_name): """Set selected project. @@ -831,6 +885,8 @@ class FrontendLoaderController(_BaseLoaderController): project_name (Union[str, None]): Selected project name. """ + pass + @abstractmethod def set_selected_folders(self, folder_ids): """Set selected folders. @@ -846,6 +902,8 @@ class FrontendLoaderController(_BaseLoaderController): folder_ids (Iterable[str]): Selected folder ids. """ + pass + @abstractmethod def set_selected_versions(self, version_ids): """Set selected versions. @@ -862,6 +920,8 @@ class FrontendLoaderController(_BaseLoaderController): version_ids (Iterable[str]): Selected version ids. """ + pass + @abstractmethod def set_selected_representations(self, repre_ids): """Set selected representations. @@ -879,6 +939,8 @@ class FrontendLoaderController(_BaseLoaderController): repre_ids (Iterable[str]): Selected representation ids. """ + pass + # Load action items @abstractmethod def get_versions_action_items(self, project_name, version_ids): @@ -892,6 +954,8 @@ class FrontendLoaderController(_BaseLoaderController): list[ActionItem]: List of action items. """ + pass + @abstractmethod def get_representations_action_items( self, project_name, representation_ids @@ -906,6 +970,8 @@ class FrontendLoaderController(_BaseLoaderController): list[ActionItem]: List of action items. """ + pass + @abstractmethod def trigger_action_item( self, @@ -938,6 +1004,8 @@ class FrontendLoaderController(_BaseLoaderController): representation_ids (Iterable[str]): Representation ids. """ + pass + @abstractmethod def change_products_group(self, project_name, product_ids, group_name): """Change group of products. @@ -956,6 +1024,8 @@ class FrontendLoaderController(_BaseLoaderController): group_name (str): New group name. """ + pass + @abstractmethod def fill_root_in_source(self, source): """Fill root in source path. @@ -965,6 +1035,8 @@ class FrontendLoaderController(_BaseLoaderController): rootless workfile path. """ + pass + # NOTE: Methods 'is_loaded_products_supported' and # 'is_standard_projects_filter_enabled' are both based on being in host # or not. Maybe we could implement only single method 'is_in_host'? @@ -976,6 +1048,8 @@ class FrontendLoaderController(_BaseLoaderController): bool: True if it is supported. """ + pass + @abstractmethod def is_standard_projects_filter_enabled(self): """Is standard projects filter enabled. @@ -988,6 +1062,8 @@ class FrontendLoaderController(_BaseLoaderController): current context project. """ + pass + # Site sync functions @abstractmethod def is_sitesync_enabled(self, project_name=None): @@ -1005,6 +1081,8 @@ class FrontendLoaderController(_BaseLoaderController): bool: True if site sync is enabled. """ + pass + @abstractmethod def get_active_site_icon_def(self, project_name): """Active site icon definition. @@ -1017,6 +1095,8 @@ class FrontendLoaderController(_BaseLoaderController): is not enabled for the project. """ + pass + @abstractmethod def get_remote_site_icon_def(self, project_name): """Remote site icon definition. @@ -1029,6 +1109,8 @@ class FrontendLoaderController(_BaseLoaderController): is not enabled for the project. """ + pass + @abstractmethod def get_version_sync_availability(self, project_name, version_ids): """Version sync availability. @@ -1041,6 +1123,8 @@ class FrontendLoaderController(_BaseLoaderController): dict[str, tuple[int, int]]: Sync availability by version id. """ + pass + @abstractmethod def get_representations_sync_status( self, project_name, representation_ids @@ -1055,6 +1139,8 @@ class FrontendLoaderController(_BaseLoaderController): dict[str, tuple[int, int]]: Sync status by representation id. """ + pass + @abstractmethod def get_product_types_filter(self): """Return product type filter for current context. @@ -1062,3 +1148,5 @@ class FrontendLoaderController(_BaseLoaderController): Returns: ProductTypesFilter: Product type filter for current context """ + + pass From 9738c2cc448371b5d18941c94268fdfa98b4558b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Thu, 19 Jun 2025 16:54:19 +0200 Subject: [PATCH 102/155] Update client/ayon_core/pipeline/load/plugins.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/load/plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 966b418db8..ccee23c5c2 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -20,7 +20,7 @@ from .utils import get_representation_path_from_context class LoaderPlugin(list): """Load representation into host application""" - product_types: Optional[set[str]] = None + product_types: set[str] = set() product_base_types: Optional[set[str]] = None representations = set() extensions = {"*"} From 929418c8cbb82e3f67534884446844a70ac610ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Thu, 19 Jun 2025 16:54:36 +0200 Subject: [PATCH 103/155] Update client/ayon_core/pipeline/load/plugins.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/load/plugins.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index ccee23c5c2..0d0b073ad7 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -124,12 +124,6 @@ class LoaderPlugin(list): Returns: bool: Is loader compatible for context. """ - """ - product_types: set[str] = set() - product_base_types: set[str] = set() - representations = set() - extensions = {"*"} - """ plugin_repre_names = cls.get_representations() plugin_product_types = cls.product_types From 6effd688914e55f9dd1b82d507b3213189f96490 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 20 Jun 2025 10:56:45 +0200 Subject: [PATCH 104/155] show settings icon only on hover --- client/ayon_core/tools/launcher/ui/actions_widget.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/launcher/ui/actions_widget.py b/client/ayon_core/tools/launcher/ui/actions_widget.py index 0459999958..3f292bd358 100644 --- a/client/ayon_core/tools/launcher/ui/actions_widget.py +++ b/client/ayon_core/tools/launcher/ui/actions_widget.py @@ -77,14 +77,22 @@ class ActionOverlayWidget(QtWidgets.QFrame): settings_icon = LauncherSettingsLabel(self) settings_icon.setToolTip("Right click for options") + settings_icon.setVisible(False) main_layout = QtWidgets.QGridLayout(self) main_layout.setContentsMargins(5, 5, 0, 0) main_layout.addWidget(settings_icon, 0, 0) main_layout.setColumnStretch(1, 1) main_layout.setRowStretch(1, 1) + self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) - self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + def enterEvent(self, event): + super().enterEvent(event) + self._settings_icon.setVisible(True) + + def leaveEvent(self, event): + super().leaveEvent(event) + self._settings_icon.setVisible(False) class ActionsQtModel(QtGui.QStandardItemModel): From 057bdd1fbaebc91bcecee29f2b4a82f01e08bab5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 20 Jun 2025 10:58:37 +0200 Subject: [PATCH 105/155] settings icon has width of 1/6 of parent --- .../tools/launcher/ui/actions_widget.py | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/launcher/ui/actions_widget.py b/client/ayon_core/tools/launcher/ui/actions_widget.py index 3f292bd358..32069ee807 100644 --- a/client/ayon_core/tools/launcher/ui/actions_widget.py +++ b/client/ayon_core/tools/launcher/ui/actions_widget.py @@ -51,12 +51,13 @@ def _variant_label_sort_getter(action_item): # --- Replacement for QAction for action variants --- -class LauncherSettingsLabel(PixmapLabel): +class LauncherSettingsLabel(QtWidgets.QWidget): _settings_icon = None def __init__(self, parent): + super().__init__(parent) icon = self._get_settings_icon() - super().__init__(icon.pixmap(64, 64), parent) + self._pixmap = icon.pixmap(64, 64) @classmethod def _get_settings_icon(cls): @@ -67,6 +68,34 @@ class LauncherSettingsLabel(PixmapLabel): }) return cls._settings_icon + def paintEvent(self, event): + painter = QtGui.QPainter() + painter.begin(self) + + render_hints = ( + QtGui.QPainter.Antialiasing + | QtGui.QPainter.SmoothPixmapTransform + ) + if hasattr(QtGui.QPainter, "HighQualityAntialiasing"): + render_hints |= QtGui.QPainter.HighQualityAntialiasing + + rect = event.rect() + size = min(rect.height(), rect.width()) + pix_rect = QtCore.QRect( + rect.x(), rect.y(), + size, size + ) + pixmap = self._pixmap.scaled( + pix_rect.size(), + QtCore.Qt.KeepAspectRatio, + QtCore.Qt.SmoothTransformation + + ) + painter.setRenderHints(render_hints) + painter.drawPixmap(0, 0, pixmap) + + painter.end() + class ActionOverlayWidget(QtWidgets.QFrame): config_requested = QtCore.Signal(str) @@ -82,8 +111,9 @@ class ActionOverlayWidget(QtWidgets.QFrame): main_layout = QtWidgets.QGridLayout(self) main_layout.setContentsMargins(5, 5, 0, 0) main_layout.addWidget(settings_icon, 0, 0) - main_layout.setColumnStretch(1, 1) - main_layout.setRowStretch(1, 1) + main_layout.setColumnStretch(0, 1) + main_layout.setColumnStretch(1, 5) + self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) def enterEvent(self, event): From a52c02581608651bbe6b36779dfef39e5ed7770c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 20 Jun 2025 11:00:19 +0200 Subject: [PATCH 106/155] store settings variable --- client/ayon_core/tools/launcher/ui/actions_widget.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/tools/launcher/ui/actions_widget.py b/client/ayon_core/tools/launcher/ui/actions_widget.py index 32069ee807..4e3a604d44 100644 --- a/client/ayon_core/tools/launcher/ui/actions_widget.py +++ b/client/ayon_core/tools/launcher/ui/actions_widget.py @@ -116,6 +116,8 @@ class ActionOverlayWidget(QtWidgets.QFrame): self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) + self._settings_icon = settings_icon + def enterEvent(self, event): super().enterEvent(event) self._settings_icon.setVisible(True) From 5c515096af9a232dbd8e8183ac814fac024e4220 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 20 Jun 2025 11:31:48 +0200 Subject: [PATCH 107/155] better scaling method --- .../tools/launcher/ui/actions_widget.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/client/ayon_core/tools/launcher/ui/actions_widget.py b/client/ayon_core/tools/launcher/ui/actions_widget.py index 4e3a604d44..64d77e5b33 100644 --- a/client/ayon_core/tools/launcher/ui/actions_widget.py +++ b/client/ayon_core/tools/launcher/ui/actions_widget.py @@ -69,15 +69,12 @@ class LauncherSettingsLabel(QtWidgets.QWidget): return cls._settings_icon def paintEvent(self, event): - painter = QtGui.QPainter() - painter.begin(self) + painter = QtGui.QPainter(self) - render_hints = ( + painter.setRenderHints( QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform ) - if hasattr(QtGui.QPainter, "HighQualityAntialiasing"): - render_hints |= QtGui.QPainter.HighQualityAntialiasing rect = event.rect() size = min(rect.height(), rect.width()) @@ -85,14 +82,11 @@ class LauncherSettingsLabel(QtWidgets.QWidget): rect.x(), rect.y(), size, size ) - pixmap = self._pixmap.scaled( - pix_rect.size(), - QtCore.Qt.KeepAspectRatio, - QtCore.Qt.SmoothTransformation - + src_rect = QtCore.QRect( + 0, 0, + self._pixmap.width(), self._pixmap.height() ) - painter.setRenderHints(render_hints) - painter.drawPixmap(0, 0, pixmap) + painter.drawPixmap(pix_rect, self._pixmap, src_rect) painter.end() From 92fc2caaf4cc504ef03b15ad1ab45e4603f9eaab Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 20 Jun 2025 11:44:16 +0200 Subject: [PATCH 108/155] fix icons resizing --- .../tools/launcher/ui/actions_widget.py | 57 +++++-------------- 1 file changed, 14 insertions(+), 43 deletions(-) diff --git a/client/ayon_core/tools/launcher/ui/actions_widget.py b/client/ayon_core/tools/launcher/ui/actions_widget.py index 64d77e5b33..9a872941d3 100644 --- a/client/ayon_core/tools/launcher/ui/actions_widget.py +++ b/client/ayon_core/tools/launcher/ui/actions_widget.py @@ -16,7 +16,6 @@ from ayon_core.lib.attribute_definitions import ( from ayon_core.tools.flickcharm import FlickCharm from ayon_core.tools.utils import ( get_qt_icon, - PixmapLabel, ) from ayon_core.tools.attribute_defs import AttributeDefinitionsDialog from ayon_core.tools.launcher.abstract import WebactionContext @@ -54,11 +53,6 @@ def _variant_label_sort_getter(action_item): class LauncherSettingsLabel(QtWidgets.QWidget): _settings_icon = None - def __init__(self, parent): - super().__init__(parent) - icon = self._get_settings_icon() - self._pixmap = icon.pixmap(64, 64) - @classmethod def _get_settings_icon(cls): if cls._settings_icon is None: @@ -82,11 +76,8 @@ class LauncherSettingsLabel(QtWidgets.QWidget): rect.x(), rect.y(), size, size ) - src_rect = QtCore.QRect( - 0, 0, - self._pixmap.width(), self._pixmap.height() - ) - painter.drawPixmap(pix_rect, self._pixmap, src_rect) + pix = self._get_settings_icon().pixmap(size, size) + painter.drawPixmap(pix_rect, pix) painter.end() @@ -626,8 +617,7 @@ class ActionMenuPopup(QtWidgets.QWidget): class ActionDelegate(QtWidgets.QStyledItemDelegate): - _cached_extender = {} - _cached_extender_base_pix = None + _extender_icon = None def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -687,27 +677,13 @@ class ActionDelegate(QtWidgets.QStyledItemDelegate): painter.restore() @classmethod - def _get_extender_pixmap(cls, size): - pix = cls._cached_extender.get(size) - if pix is not None: - return pix - - base_pix = cls._cached_extender_base_pix - if base_pix is None: - icon = get_qt_icon({ + def _get_extender_pixmap(cls): + if cls._extender_icon is None: + cls._extender_icon = get_qt_icon({ "type": "material-symbols", "name": "more_horiz", }) - base_pix = icon.pixmap(64, 64) - cls._cached_extender_base_pix = base_pix - - pix = base_pix.scaled( - size, size, - QtCore.Qt.KeepAspectRatio, - QtCore.Qt.SmoothTransformation - ) - cls._cached_extender[size] = pix - return pix + return cls._extender_icon def paint(self, painter, option, index): painter.setRenderHints( @@ -724,20 +700,15 @@ class ActionDelegate(QtWidgets.QStyledItemDelegate): return grid_size = option.widget.gridSize() - x_offset = int( - (grid_size.width() / 2) - - (option.rect.width() / 2) - ) - item_x = option.rect.x() - x_offset - tenth_size = int(grid_size.width() / 10) - extender_size = int(tenth_size * 2.4) + extender_rect = option.rect.adjusted(5, 5, 0, 0) + extender_size = grid_size.width() // 6 + extender_rect.setWidth(extender_size) + extender_rect.setHeight(extender_size) - extender_x = item_x + tenth_size - extender_y = option.rect.y() + tenth_size - - pix = self._get_extender_pixmap(extender_size) - painter.drawPixmap(extender_x, extender_y, pix) + icon = self._get_extender_pixmap() + pix = icon.pixmap(extender_size, extender_size) + painter.drawPixmap(extender_rect, pix) class ActionsProxyModel(QtCore.QSortFilterProxyModel): From 3f8f0b17c827e062c8d77dfef8427c7867ec7f18 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 20 Jun 2025 13:25:23 +0200 Subject: [PATCH 109/155] show popup on click --- .../tools/launcher/ui/actions_widget.py | 70 +++++++------------ 1 file changed, 26 insertions(+), 44 deletions(-) diff --git a/client/ayon_core/tools/launcher/ui/actions_widget.py b/client/ayon_core/tools/launcher/ui/actions_widget.py index 9a872941d3..ae9ef05730 100644 --- a/client/ayon_core/tools/launcher/ui/actions_widget.py +++ b/client/ayon_core/tools/launcher/ui/actions_widget.py @@ -744,7 +744,6 @@ class ActionsProxyModel(QtCore.QSortFilterProxyModel): class ActionsView(QtWidgets.QListView): - action_triggered = QtCore.Signal(str) config_requested = QtCore.Signal(str) def __init__(self, parent): @@ -778,18 +777,6 @@ class ActionsView(QtWidgets.QListView): self._overlay_widgets = [] self._flick = flick self._delegate = delegate - self._popup_widget = None - - def mouseMoveEvent(self, event): - """Handle mouse move event.""" - super().mouseMoveEvent(event) - # Update hover state for the item under mouse - index = self.indexAt(event.pos()) - if index.isValid() and index.data(ACTION_IS_GROUP_ROLE): - self._show_group_popup(index) - - elif self._popup_widget is not None: - self._popup_widget.close() def _on_context_menu(self, point): """Creates menu to force skip opening last workfile.""" @@ -799,33 +786,6 @@ class ActionsView(QtWidgets.QListView): action_id = index.data(ACTION_ID_ROLE) self.config_requested.emit(action_id) - def _get_popup_widget(self): - if self._popup_widget is None: - popup_widget = ActionMenuPopup(self) - - popup_widget.action_triggered.connect(self.action_triggered) - popup_widget.config_requested.connect(self.config_requested) - self._popup_widget = popup_widget - return self._popup_widget - - def _show_group_popup(self, index): - action_id = index.data(ACTION_ID_ROLE) - model = self.model() - while hasattr(model, "sourceModel"): - model = model.sourceModel() - - if not hasattr(model, "get_group_items"): - return - - action_items = model.get_group_items(action_id) - rect = self.visualRect(index) - pos = self.mapToGlobal(rect.topLeft()) - - popup_widget = self._get_popup_widget() - popup_widget.show_items( - action_id, action_items, pos - ) - def update_on_refresh(self): viewport = self.viewport() viewport.update() @@ -882,7 +842,6 @@ class ActionsWidget(QtWidgets.QWidget): animation_timer.timeout.connect(self._on_animation) view.clicked.connect(self._on_clicked) - view.action_triggered.connect(self._trigger_action) view.config_requested.connect(self._on_config_request) model.refreshed.connect(self._on_model_refresh) @@ -893,6 +852,8 @@ class ActionsWidget(QtWidgets.QWidget): self._model = model self._proxy_model = proxy_model + self._popup_widget = None + self._set_row_height(1) def refresh(self): @@ -979,10 +940,31 @@ class ActionsWidget(QtWidgets.QWidget): return is_group = index.data(ACTION_IS_GROUP_ROLE) - if is_group: - return action_id = index.data(ACTION_ID_ROLE) - self._trigger_action(action_id, index) + if is_group: + self._show_group_popup(index) + else: + self._trigger_action(action_id, index) + + def _get_popup_widget(self): + if self._popup_widget is None: + popup_widget = ActionMenuPopup(self) + + popup_widget.action_triggered.connect(self._trigger_action) + popup_widget.config_requested.connect(self._on_config_request) + self._popup_widget = popup_widget + return self._popup_widget + + def _show_group_popup(self, index): + action_id = index.data(ACTION_ID_ROLE) + action_items = self._model.get_group_items(action_id) + rect = self._view.visualRect(index) + pos = self.mapToGlobal(rect.topLeft()) + + popup_widget = self._get_popup_widget() + popup_widget.show_items( + action_id, action_items, pos + ) def _trigger_action(self, action_id, index=None): project_name = self._model.get_selected_project_name() From e417a2a335e0d8d496b38742942f8990a93515e1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 20 Jun 2025 13:42:43 +0200 Subject: [PATCH 110/155] get rid of mode property --- client/ayon_core/style/style.css | 10 +++++----- client/ayon_core/tools/launcher/ui/actions_widget.py | 1 - 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/style/style.css b/client/ayon_core/style/style.css index 4ef903540e..375545e90b 100644 --- a/client/ayon_core/style/style.css +++ b/client/ayon_core/style/style.css @@ -829,7 +829,7 @@ HintedLineEditButton { } /* Launcher specific stylesheets */ -ActionsView[mode="icon"] { +ActionsView { /* font size can't be set on items */ font-size: 9pt; border: 0px; @@ -837,25 +837,25 @@ ActionsView[mode="icon"] { margin: 0px; } -ActionsView[mode="icon"]::item { +ActionsView::item { padding-top: 8px; padding-bottom: 4px; border: 0px; border-radius: 0.3em; } -ActionsView[mode="icon"]::item:hover { +ActionsView::item:hover { color: {color:font-hover}; background: #424A57; } -ActionsView[mode="icon"]::icon {} +ActionsView::icon {} ActionMenuPopup #Wrapper { border-radius: 0.3em; background: #353B46; } -ActionMenuPopup ActionsView[mode="icon"] { +ActionMenuPopup ActionsView { background: transparent; border: none; } diff --git a/client/ayon_core/tools/launcher/ui/actions_widget.py b/client/ayon_core/tools/launcher/ui/actions_widget.py index ae9ef05730..f90fa1ec4a 100644 --- a/client/ayon_core/tools/launcher/ui/actions_widget.py +++ b/client/ayon_core/tools/launcher/ui/actions_widget.py @@ -748,7 +748,6 @@ class ActionsView(QtWidgets.QListView): def __init__(self, parent): super().__init__(parent) - self.setProperty("mode", "icon") self.setViewMode(QtWidgets.QListView.IconMode) self.setResizeMode(QtWidgets.QListView.Adjust) self.setSelectionMode(QtWidgets.QListView.NoSelection) From 03748aeb3bc11c0de9ded42ae25fe6733bbc6e79 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 20 Jun 2025 14:39:26 +0200 Subject: [PATCH 111/155] show config fields dialog under mouse --- .../tools/launcher/ui/actions_widget.py | 54 +++++++++++++------ 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/client/ayon_core/tools/launcher/ui/actions_widget.py b/client/ayon_core/tools/launcher/ui/actions_widget.py index f90fa1ec4a..28c741c93a 100644 --- a/client/ayon_core/tools/launcher/ui/actions_widget.py +++ b/client/ayon_core/tools/launcher/ui/actions_widget.py @@ -83,8 +83,6 @@ class LauncherSettingsLabel(QtWidgets.QWidget): class ActionOverlayWidget(QtWidgets.QFrame): - config_requested = QtCore.Signal(str) - def __init__(self, item_id, parent): super().__init__(parent) self._item_id = item_id @@ -163,6 +161,12 @@ class ActionsQtModel(QtGui.QStandardItemModel): def get_item_by_id(self, action_id): return self._items_by_id.get(action_id) + def get_index_by_id(self, action_id): + item = self.get_item_by_id(action_id) + if item is not None: + return self.indexFromItem(item) + return QtCore.QModelIndex() + def get_group_item_by_action_id(self, action_id): item = self._items_by_id.get(action_id) if item is not None: @@ -370,7 +374,7 @@ class ActionMenuPopupModel(QtGui.QStandardItemModel): class ActionMenuPopup(QtWidgets.QWidget): action_triggered = QtCore.Signal(str) - config_requested = QtCore.Signal(str) + config_requested = QtCore.Signal(str, QtCore.QPoint) def __init__(self, parent): super().__init__(parent) @@ -412,7 +416,7 @@ class ActionMenuPopup(QtWidgets.QWidget): expand_anim.finished.connect(self._on_expand_finish) view.clicked.connect(self._on_clicked) - view.config_requested.connect(self.config_requested) + view.config_requested.connect(self._on_configs_trigger) self._view = view self._wrapper = wrapper @@ -611,8 +615,8 @@ class ActionMenuPopup(QtWidgets.QWidget): self.action_triggered.emit(action_id) self.close() - def _on_configs_trigger(self, action_id): - self.config_requested.emit(action_id) + def _on_configs_trigger(self, action_id, center_pos): + self.config_requested.emit(action_id, center_pos) self.close() @@ -744,7 +748,7 @@ class ActionsProxyModel(QtCore.QSortFilterProxyModel): class ActionsView(QtWidgets.QListView): - config_requested = QtCore.Signal(str) + config_requested = QtCore.Signal(str, QtCore.QPoint) def __init__(self, parent): super().__init__(parent) @@ -783,7 +787,9 @@ class ActionsView(QtWidgets.QListView): if not index.isValid(): return action_id = index.data(ACTION_ID_ROLE) - self.config_requested.emit(action_id) + rect = self.visualRect(index) + global_center = self.mapToGlobal(rect.center()) + self.config_requested.emit(action_id, global_center) def update_on_refresh(self): viewport = self.viewport() @@ -801,9 +807,6 @@ class ActionsView(QtWidgets.QListView): if has_configs: item_id = index.data(ACTION_ID_ROLE) widget = ActionOverlayWidget(item_id, viewport) - widget.config_requested.connect( - self.config_requested - ) overlay_widgets.append(widget) self.setIndexWidget(index, widget) @@ -841,7 +844,7 @@ class ActionsWidget(QtWidgets.QWidget): animation_timer.timeout.connect(self._on_animation) view.clicked.connect(self._on_clicked) - view.config_requested.connect(self._on_config_request) + view.config_requested.connect(self._show_config_dialog) model.refreshed.connect(self._on_model_refresh) self._animated_items = set() @@ -950,7 +953,7 @@ class ActionsWidget(QtWidgets.QWidget): popup_widget = ActionMenuPopup(self) popup_widget.action_triggered.connect(self._trigger_action) - popup_widget.config_requested.connect(self._on_config_request) + popup_widget.config_requested.connect(self._show_config_dialog) self._popup_widget = popup_widget return self._popup_widget @@ -997,10 +1000,7 @@ class ActionsWidget(QtWidgets.QWidget): if index is not None: self._start_animation(index) - def _on_config_request(self, action_id): - self._show_config_dialog(action_id) - - def _show_config_dialog(self, action_id): + def _show_config_dialog(self, action_id, center_point): action_item = self._model.get_action_item_by_id(action_id) config_fields = self._model.get_action_config_fields(action_id) if not config_fields: @@ -1026,11 +1026,31 @@ class ActionsWidget(QtWidgets.QWidget): "Cancel", ) dialog.set_values(values) + dialog.show() + self._center_dialog(dialog, center_point) result = dialog.exec_() if result == QtWidgets.QDialog.Accepted: new_values = dialog.get_values() self._controller.set_action_config_values(context, new_values) + @staticmethod + def _center_dialog(dialog, target_center_pos): + dialog_geo = dialog.geometry() + dialog_geo.moveCenter(target_center_pos) + + screen = dialog.screen() + screen_geo = screen.availableGeometry() + if screen_geo.left() > dialog_geo.left(): + dialog_geo.moveLeft(screen_geo.left()) + elif screen_geo.right() < dialog_geo.right(): + dialog_geo.moveRight(screen_geo.right()) + + if screen_geo.top() > dialog_geo.top(): + dialog_geo.moveTop(screen_geo.top()) + elif screen_geo.bottom() < dialog_geo.bottom(): + dialog_geo.moveBottom(screen_geo.bottom()) + dialog.move(dialog_geo.topLeft()) + def _create_attrs_dialog( self, config_fields, From 5bc3529434e15b62bd8e08a16e7891b6e4c99f27 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 20 Jun 2025 15:07:25 +0200 Subject: [PATCH 112/155] make font smaller --- client/ayon_core/style/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/style/style.css b/client/ayon_core/style/style.css index 375545e90b..6f47a34956 100644 --- a/client/ayon_core/style/style.css +++ b/client/ayon_core/style/style.css @@ -831,7 +831,7 @@ HintedLineEditButton { /* Launcher specific stylesheets */ ActionsView { /* font size can't be set on items */ - font-size: 9pt; + font-size: 8pt; border: 0px; padding: 0px; margin: 0px; From deacb2853e429296b1603d949b8b5006c1821ef6 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 20 Jun 2025 16:13:07 +0200 Subject: [PATCH 113/155] :recycle: refactor filtering and add some tests --- client/ayon_core/pipeline/load/plugins.py | 20 +++-- .../ayon_core/pipeline/load/test_loaders.py | 88 +++++++++++++++++++ 2 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 tests/client/ayon_core/pipeline/load/test_loaders.py diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 39343b76c6..5725133432 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -129,6 +129,10 @@ class LoaderPlugin(list): plugin_repre_names = cls.get_representations() plugin_product_types = cls.product_types plugin_product_base_types = cls.product_base_types + # If product type isn't defined on the loader plugin, + # then we will use the product types. + plugin_product_filter = ( + plugin_product_base_types or plugin_product_types) repre_entity = context.get("representation") product_entity = context["product"] @@ -136,8 +140,8 @@ class LoaderPlugin(list): # then loader is not compatible with any context. if ( not plugin_repre_names - or (not plugin_product_types and not plugin_product_base_types) - or not cls.extensions + and not plugin_product_filter + and not cls.extensions ): return False @@ -148,7 +152,7 @@ class LoaderPlugin(list): # Check the compatibility with the representation names. plugin_repre_names = set(plugin_repre_names) - if not plugin_repre_names or ( + if ( "*" not in plugin_repre_names and repre_entity["name"] not in plugin_repre_names ): @@ -169,8 +173,6 @@ class LoaderPlugin(list): if product_filter is None: product_filter = product_type - # If no product type isn't defined on the loader plugin, - # then we will use the product types. plugin_product_filter = ( plugin_product_base_types or plugin_product_types) @@ -179,6 +181,14 @@ class LoaderPlugin(list): if "*" in plugin_product_filter: return True + # compatibility with legacy loader + if cls.product_base_types is None and product_base_type: + cls.log.error( + f"Loader {cls.__name__} is doesn't specify " + "`product_base_types` but product entity has " + f"`productBaseType` defined as `{product_base_type}`. " + ) + return product_filter in plugin_product_filter @classmethod diff --git a/tests/client/ayon_core/pipeline/load/test_loaders.py b/tests/client/ayon_core/pipeline/load/test_loaders.py new file mode 100644 index 0000000000..490efe1b1e --- /dev/null +++ b/tests/client/ayon_core/pipeline/load/test_loaders.py @@ -0,0 +1,88 @@ +"""Test loaders in the pipeline module.""" + +from ayon_core.pipeline.load import LoaderPlugin + + +def test_is_compatible_loader(): + """Test if a loader is compatible with a given representation.""" + from ayon_core.pipeline.load import is_compatible_loader + + # Create a mock representation context + context = { + "loader": "test_loader", + "representation": {"name": "test_representation"}, + } + + # Create a mock loader plugin + class MockLoader(LoaderPlugin): + name = "test_loader" + version = "1.0.0" + + def is_compatible_loader(self, context): + return True + + # Check compatibility + assert is_compatible_loader(MockLoader(), context) is True + + +def test_complex_is_compatible_loader(): + """Test if a loader is compatible with a complex representation.""" + from ayon_core.pipeline.load import is_compatible_loader + + # Create a mock complex representation context + context = { + "loader": "complex_loader", + "representation": { + "name": "complex_representation", + "extension": "exr" + }, + "additional_data": {"key": "value"}, + "product": { + "name": "complex_product", + "productType": "foo", + "productBaseType": "bar", + }, + } + + # Create a mock loader plugin + class ComplexLoaderA(LoaderPlugin): + name = "complex_loaderA" + + # False because the loader doesn't specify any compatibility (missing + # wildcard for product type and product base type) + assert is_compatible_loader(ComplexLoaderA(), context) is False + + class ComplexLoaderB(LoaderPlugin): + name = "complex_loaderB" + product_types = {"*"} + representations = {"*"} + + # True, it is compatible with any product type + assert is_compatible_loader(ComplexLoaderB(), context) is True + + class ComplexLoaderC(LoaderPlugin): + name = "complex_loaderC" + product_base_types = {"*"} + representations = {"*"} + + # True, it is compatible with any product base type + assert is_compatible_loader(ComplexLoaderC(), context) is True + + class ComplexLoaderD(LoaderPlugin): + name = "complex_loaderD" + product_types = {"foo"} + representations = {"*"} + + # legacy loader defining compatibility only with product type + # is compatible provided the same product type is defined in context + assert is_compatible_loader(ComplexLoaderD(), context) is False + + class ComplexLoaderE(LoaderPlugin): + name = "complex_loaderE" + product_types = {"foo"} + representations = {"*"} + + # remove productBaseType from context to simulate legacy behavior + context["product"].pop("productBaseType", None) + + assert is_compatible_loader(ComplexLoaderE(), context) is True From 2944b70267fcb6af41e70886b75593309c0d5945 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 20 Jun 2025 16:20:57 +0200 Subject: [PATCH 114/155] use viewport margins to calculate size and position --- client/ayon_core/style/style.css | 2 ++ .../tools/launcher/ui/actions_widget.py | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/style/style.css b/client/ayon_core/style/style.css index 6f47a34956..9a86bef960 100644 --- a/client/ayon_core/style/style.css +++ b/client/ayon_core/style/style.css @@ -855,9 +855,11 @@ ActionMenuPopup #Wrapper { border-radius: 0.3em; background: #353B46; } + ActionMenuPopup ActionsView { background: transparent; border: none; + margin: 4px; } #IconView[mode="icon"] { diff --git a/client/ayon_core/tools/launcher/ui/actions_widget.py b/client/ayon_core/tools/launcher/ui/actions_widget.py index 28c741c93a..6a16dd9f8e 100644 --- a/client/ayon_core/tools/launcher/ui/actions_widget.py +++ b/client/ayon_core/tools/launcher/ui/actions_widget.py @@ -396,7 +396,6 @@ class ActionMenuPopup(QtWidgets.QWidget): view = ActionsView(self) view.setGridSize(QtCore.QSize(75, 80)) view.setIconSize(QtCore.QSize(32, 32)) - view.move(QtCore.QPoint(3, 3)) # Background draw wrapper = QtWidgets.QFrame(self) @@ -485,8 +484,9 @@ class ActionMenuPopup(QtWidgets.QWidget): or pos.y() + target_size.height() > window_geo.bottom() ) - pos_x = pos.x() - 5 - pos_y = pos.y() - 4 + viewport_offset = self._view.viewport().geometry().topLeft() + pos_x = pos.x() - (viewport_offset.x() + 2) + pos_y = pos.y() - (viewport_offset.y() + 1) wrap_x = wrap_y = 0 sort_order = QtCore.Qt.DescendingOrder @@ -576,16 +576,19 @@ class ActionMenuPopup(QtWidgets.QWidget): if rows == 1: cols = row_count - m_l, m_t, m_r, m_b = (3, 3, 1, 1) - # QUESTION how to get the margins from Qt? - border = 2 * 1 + viewport_geo = self._view.viewport().geometry() + viewport_offset = viewport_geo.topLeft() + # QUESTION how to get the bottom and right margins from Qt? + vp_lr = viewport_offset.x() + vp_tb = viewport_offset.y() + m_l, m_t, m_r, m_b = (vp_lr, vp_tb, vp_lr, vp_tb) single_width = ( grid_size.width() - + self._view.horizontalOffset() + border + m_l + m_r + 1 + + self._view.horizontalOffset() + m_l + m_r + 1 ) single_height = ( grid_size.height() - + self._view.verticalOffset() + border + m_b + m_t + 1 + + self._view.verticalOffset() + m_b + m_t + 1 ) total_width = single_width total_height = single_height From 0f32e26c7590185b81f95b42c035a52a1ed2833a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 20 Jun 2025 16:21:20 +0200 Subject: [PATCH 115/155] added shadow frame --- client/ayon_core/style/style.css | 9 +++- .../tools/launcher/ui/actions_widget.py | 54 +++++++++++++------ 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/client/ayon_core/style/style.css b/client/ayon_core/style/style.css index 9a86bef960..5f661274af 100644 --- a/client/ayon_core/style/style.css +++ b/client/ayon_core/style/style.css @@ -841,7 +841,7 @@ ActionsView::item { padding-top: 8px; padding-bottom: 4px; border: 0px; - border-radius: 0.3em; + border-radius: 5px; } ActionsView::item:hover { @@ -851,8 +851,13 @@ ActionsView::item:hover { ActionsView::icon {} +ActionMenuPopup #ShadowFrame { + border-radius: 5px; + background: rgba(0, 0, 0, 0.3); +} + ActionMenuPopup #Wrapper { - border-radius: 0.3em; + border-radius: 5px; background: #353B46; } diff --git a/client/ayon_core/tools/launcher/ui/actions_widget.py b/client/ayon_core/tools/launcher/ui/actions_widget.py index 6a16dd9f8e..0e2a56babf 100644 --- a/client/ayon_core/tools/launcher/ui/actions_widget.py +++ b/client/ayon_core/tools/launcher/ui/actions_widget.py @@ -21,6 +21,7 @@ from ayon_core.tools.attribute_defs import AttributeDefinitionsDialog from ayon_core.tools.launcher.abstract import WebactionContext ANIMATION_LEN = 7 +SHADOW_FRAME_MARGINS = (2, 2, 2, 2) ACTION_ID_ROLE = QtCore.Qt.UserRole + 1 ACTION_TYPE_ROLE = QtCore.Qt.UserRole + 2 @@ -392,15 +393,25 @@ class ActionMenuPopup(QtWidgets.QWidget): expand_anim.setDuration(60) expand_anim.setEasingCurve(QtCore.QEasingCurve.InOutQuad) + sh_l, sh_t, sh_r, sh_b = SHADOW_FRAME_MARGINS + # View with actions view = ActionsView(self) view.setGridSize(QtCore.QSize(75, 80)) view.setIconSize(QtCore.QSize(32, 32)) + view.move(QtCore.QPoint(sh_l, sh_t)) # Background draw + bg_frame = QtWidgets.QFrame(self) + bg_frame.setObjectName("ShadowFrame") + bg_frame.stackUnder(view) + wrapper = QtWidgets.QFrame(self) wrapper.setObjectName("Wrapper") - wrapper.stackUnder(view) + + bg_layout = QtWidgets.QVBoxLayout(bg_frame) + bg_layout.setContentsMargins(sh_l, sh_t, sh_r, sh_b) + bg_layout.addWidget(wrapper) model = ActionMenuPopupModel() proxy_model = ActionsProxyModel() @@ -418,7 +429,7 @@ class ActionMenuPopup(QtWidgets.QWidget): view.config_requested.connect(self._on_configs_trigger) self._view = view - self._wrapper = wrapper + self._bg_frame = bg_frame self._model = model self._proxy_model = proxy_model @@ -484,22 +495,23 @@ class ActionMenuPopup(QtWidgets.QWidget): or pos.y() + target_size.height() > window_geo.bottom() ) + sh_l, sh_t, sh_r, sh_b = SHADOW_FRAME_MARGINS viewport_offset = self._view.viewport().geometry().topLeft() - pos_x = pos.x() - (viewport_offset.x() + 2) - pos_y = pos.y() - (viewport_offset.y() + 1) + pos_x = pos.x() - (sh_l + viewport_offset.x() + 2) + pos_y = pos.y() - (sh_t + viewport_offset.y() + 1) - wrap_x = wrap_y = 0 + bg_x = bg_y = 0 sort_order = QtCore.Qt.DescendingOrder if right_to_left: sort_order = QtCore.Qt.AscendingOrder size_diff = target_size - size pos_x -= size_diff.width() pos_y -= size_diff.height() - wrap_x = size_diff.width() - wrap_y = size_diff.height() + bg_x = size_diff.width() + bg_y = size_diff.height() - wrap_geo = QtCore.QRect( - wrap_x, wrap_y, size.width(), size.height() + bg_geo = QtCore.QRect( + bg_x, bg_y, size.width(), size.height() ) if self._expand_anim.state() == QtCore.QAbstractAnimation.Running: self._expand_anim.stop() @@ -508,10 +520,10 @@ class ActionMenuPopup(QtWidgets.QWidget): self._proxy_model.sort(0, sort_order) self.setUpdatesEnabled(False) - self._view.setMask(wrap_geo) + self._view.setMask(bg_geo.adjusted(sh_l, sh_t, -sh_r, -sh_b)) self._view.setMinimumWidth(target_size.width()) self._view.setMaximumWidth(target_size.width()) - self._wrapper.setGeometry(wrap_geo) + self._bg_frame.setGeometry(bg_geo) self.setGeometry( pos_x, pos_y, target_size.width(), target_size.height() @@ -540,9 +552,9 @@ class ActionMenuPopup(QtWidgets.QWidget): self._expand_anim.stop() return - wrapper_geo = self._wrapper.geometry() - wrapper_geo.setWidth(value.width()) - wrapper_geo.setHeight(value.height()) + bg_geo = self._bg_frame.geometry() + bg_geo.setWidth(value.width()) + bg_geo.setHeight(value.height()) if self._right_to_left: geo = self.geometry() @@ -550,10 +562,11 @@ class ActionMenuPopup(QtWidgets.QWidget): geo.width() - value.width(), geo.height() - value.height(), ) - wrapper_geo.setTopLeft(pos) + bg_geo.setTopLeft(pos) - self._view.setMask(wrapper_geo) - self._wrapper.setGeometry(wrapper_geo) + sh_l, sh_t, sh_r, sh_b = SHADOW_FRAME_MARGINS + self._view.setMask(bg_geo.adjusted(sh_l, sh_t, -sh_r, -sh_b)) + self._bg_frame.setGeometry(bg_geo) def _on_expand_finish(self): # Make sure that size is recalculated if src and targe size is same @@ -582,6 +595,13 @@ class ActionMenuPopup(QtWidgets.QWidget): vp_lr = viewport_offset.x() vp_tb = viewport_offset.y() m_l, m_t, m_r, m_b = (vp_lr, vp_tb, vp_lr, vp_tb) + m_l, m_t, m_r, m_b = ( + s_m + vp_m + for s_m, vp_m in zip( + SHADOW_FRAME_MARGINS, + (vp_lr, vp_tb, vp_lr, vp_tb) + ) + ) single_width = ( grid_size.width() + self._view.horizontalOffset() + m_l + m_r + 1 From 56fd29f20a58dc73c313789f750d84b1944fb099 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 20 Jun 2025 16:21:54 +0200 Subject: [PATCH 116/155] remove unnecessary line --- client/ayon_core/tools/launcher/ui/actions_widget.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/tools/launcher/ui/actions_widget.py b/client/ayon_core/tools/launcher/ui/actions_widget.py index 0e2a56babf..334107680b 100644 --- a/client/ayon_core/tools/launcher/ui/actions_widget.py +++ b/client/ayon_core/tools/launcher/ui/actions_widget.py @@ -594,7 +594,6 @@ class ActionMenuPopup(QtWidgets.QWidget): # QUESTION how to get the bottom and right margins from Qt? vp_lr = viewport_offset.x() vp_tb = viewport_offset.y() - m_l, m_t, m_r, m_b = (vp_lr, vp_tb, vp_lr, vp_tb) m_l, m_t, m_r, m_b = ( s_m + vp_m for s_m, vp_m in zip( From 0df7ff3338487c9e1d3b73f0c0f1fb5b95d6ecfa Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 20 Jun 2025 16:29:38 +0200 Subject: [PATCH 117/155] added TextAntialiasing hint --- client/ayon_core/tools/launcher/ui/actions_widget.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/tools/launcher/ui/actions_widget.py b/client/ayon_core/tools/launcher/ui/actions_widget.py index 334107680b..20e9903f97 100644 --- a/client/ayon_core/tools/launcher/ui/actions_widget.py +++ b/client/ayon_core/tools/launcher/ui/actions_widget.py @@ -714,6 +714,7 @@ class ActionDelegate(QtWidgets.QStyledItemDelegate): def paint(self, painter, option, index): painter.setRenderHints( QtGui.QPainter.Antialiasing + | QtGui.QPainter.TextAntialiasing | QtGui.QPainter.SmoothPixmapTransform ) From bf6ac48a66e5dcdf5f06af42007250eccc9a7423 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 23 Jun 2025 14:06:26 +0200 Subject: [PATCH 118/155] better shadow --- client/ayon_core/style/style.css | 2 +- client/ayon_core/tools/launcher/ui/actions_widget.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/style/style.css b/client/ayon_core/style/style.css index 5f661274af..2e3bf3954f 100644 --- a/client/ayon_core/style/style.css +++ b/client/ayon_core/style/style.css @@ -853,7 +853,7 @@ ActionsView::icon {} ActionMenuPopup #ShadowFrame { border-radius: 5px; - background: rgba(0, 0, 0, 0.3); + background: rgba(12, 13, 24, 0.5); } ActionMenuPopup #Wrapper { diff --git a/client/ayon_core/tools/launcher/ui/actions_widget.py b/client/ayon_core/tools/launcher/ui/actions_widget.py index 20e9903f97..8c5c7c7062 100644 --- a/client/ayon_core/tools/launcher/ui/actions_widget.py +++ b/client/ayon_core/tools/launcher/ui/actions_widget.py @@ -21,7 +21,7 @@ from ayon_core.tools.attribute_defs import AttributeDefinitionsDialog from ayon_core.tools.launcher.abstract import WebactionContext ANIMATION_LEN = 7 -SHADOW_FRAME_MARGINS = (2, 2, 2, 2) +SHADOW_FRAME_MARGINS = (1, 1, 1, 1) ACTION_ID_ROLE = QtCore.Qt.UserRole + 1 ACTION_TYPE_ROLE = QtCore.Qt.UserRole + 2 @@ -409,6 +409,10 @@ class ActionMenuPopup(QtWidgets.QWidget): wrapper = QtWidgets.QFrame(self) wrapper.setObjectName("Wrapper") + effect = QtWidgets.QGraphicsBlurEffect(wrapper) + effect.setBlurRadius(3.0) + wrapper.setGraphicsEffect(effect) + bg_layout = QtWidgets.QVBoxLayout(bg_frame) bg_layout.setContentsMargins(sh_l, sh_t, sh_r, sh_b) bg_layout.addWidget(wrapper) @@ -430,6 +434,7 @@ class ActionMenuPopup(QtWidgets.QWidget): self._view = view self._bg_frame = bg_frame + self._effect = effect self._model = model self._proxy_model = proxy_model From b47496e47e9300f5e8f23de0cd884dfe428dbc3b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 23 Jun 2025 14:12:25 +0200 Subject: [PATCH 119/155] define minimum view height --- client/ayon_core/tools/launcher/ui/actions_widget.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/tools/launcher/ui/actions_widget.py b/client/ayon_core/tools/launcher/ui/actions_widget.py index 8c5c7c7062..953821d778 100644 --- a/client/ayon_core/tools/launcher/ui/actions_widget.py +++ b/client/ayon_core/tools/launcher/ui/actions_widget.py @@ -528,6 +528,7 @@ class ActionMenuPopup(QtWidgets.QWidget): self._view.setMask(bg_geo.adjusted(sh_l, sh_t, -sh_r, -sh_b)) self._view.setMinimumWidth(target_size.width()) self._view.setMaximumWidth(target_size.width()) + self._view.setMinimumHeight(target_size.height()) self._bg_frame.setGeometry(bg_geo) self.setGeometry( pos_x, pos_y, From 953c584381610b6be3e12ea68faf9294a72a8e7b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 23 Jun 2025 15:05:44 +0200 Subject: [PATCH 120/155] show tooltips for actions --- client/ayon_core/tools/launcher/ui/actions_widget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/launcher/ui/actions_widget.py b/client/ayon_core/tools/launcher/ui/actions_widget.py index 953821d778..118debe123 100644 --- a/client/ayon_core/tools/launcher/ui/actions_widget.py +++ b/client/ayon_core/tools/launcher/ui/actions_widget.py @@ -252,7 +252,7 @@ class ActionsQtModel(QtGui.QStandardItemModel): item.setFlags(QtCore.Qt.ItemIsEnabled) item.setData(label, QtCore.Qt.DisplayRole) - # item.setData(label, QtCore.Qt.ToolTipRole) + item.setData(label, QtCore.Qt.ToolTipRole) item.setData(icon, QtCore.Qt.DecorationRole) item.setData(is_group, ACTION_IS_GROUP_ROLE) item.setData(has_configs, ACTION_HAS_CONFIGS_ROLE) @@ -325,8 +325,8 @@ class ActionMenuPopupModel(QtGui.QStandardItemModel): item = QtGui.QStandardItem() item.setFlags(QtCore.Qt.ItemIsEnabled) - # item.setData(action_item.full_label, QtCore.Qt.ToolTipRole) item.setData(action_item.full_label, QtCore.Qt.DisplayRole) + item.setData(action_item.full_label, QtCore.Qt.ToolTipRole) item.setData(icon, QtCore.Qt.DecorationRole) item.setData(action_item.identifier, ACTION_ID_ROLE) item.setData( From 6de993af25a416a4f1e6bb515da9a884189eae67 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 23 Jun 2025 15:06:53 +0200 Subject: [PATCH 121/155] remove usage of deprecated 'HighQualityAntialiasing' flag --- .../ayon_core/tools/publisher/widgets/screenshot_widget.py | 2 -- .../ayon_core/tools/publisher/widgets/thumbnail_widget.py | 6 ------ client/ayon_core/tools/utils/color_widgets/color_inputs.py | 2 +- .../ayon_core/tools/utils/color_widgets/color_triangle.py | 2 +- client/ayon_core/tools/utils/lib.py | 3 --- client/ayon_core/tools/utils/sliders.py | 2 +- client/ayon_core/tools/utils/thumbnail_paint_widget.py | 6 ------ client/ayon_core/tools/utils/widgets.py | 4 ---- 8 files changed, 3 insertions(+), 24 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/screenshot_widget.py b/client/ayon_core/tools/publisher/widgets/screenshot_widget.py index 0706299f32..e9749c5b07 100644 --- a/client/ayon_core/tools/publisher/widgets/screenshot_widget.py +++ b/client/ayon_core/tools/publisher/widgets/screenshot_widget.py @@ -348,8 +348,6 @@ class ScreenMarquee(QtCore.QObject): # QtGui.QPainter.Antialiasing # | QtGui.QPainter.SmoothPixmapTransform # ) - # if hasattr(QtGui.QPainter, "HighQualityAntialiasing"): - # render_hints |= QtGui.QPainter.HighQualityAntialiasing # pix_painter.setRenderHints(render_hints) # for item in screen_pixes: # (screen_pix, offset) = item diff --git a/client/ayon_core/tools/publisher/widgets/thumbnail_widget.py b/client/ayon_core/tools/publisher/widgets/thumbnail_widget.py index 261dcfb43d..f767fdf325 100644 --- a/client/ayon_core/tools/publisher/widgets/thumbnail_widget.py +++ b/client/ayon_core/tools/publisher/widgets/thumbnail_widget.py @@ -135,8 +135,6 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform ) - if hasattr(QtGui.QPainter, "HighQualityAntialiasing"): - render_hints |= QtGui.QPainter.HighQualityAntialiasing pix_painter.setRenderHints(render_hints) pix_painter.drawPixmap(pos_x, pos_y, scaled_pix) @@ -171,8 +169,6 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform ) - if hasattr(QtGui.QPainter, "HighQualityAntialiasing"): - render_hints |= QtGui.QPainter.HighQualityAntialiasing pix_painter.setRenderHints(render_hints) tiled_rect = QtCore.QRectF( @@ -265,8 +261,6 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform ) - if hasattr(QtGui.QPainter, "HighQualityAntialiasing"): - render_hints |= QtGui.QPainter.HighQualityAntialiasing final_painter.setRenderHints(render_hints) diff --git a/client/ayon_core/tools/utils/color_widgets/color_inputs.py b/client/ayon_core/tools/utils/color_widgets/color_inputs.py index 795b80fc1e..5a1c2dc50b 100644 --- a/client/ayon_core/tools/utils/color_widgets/color_inputs.py +++ b/client/ayon_core/tools/utils/color_widgets/color_inputs.py @@ -65,7 +65,7 @@ class AlphaSlider(QtWidgets.QSlider): painter.fillRect(event.rect(), QtCore.Qt.transparent) - painter.setRenderHint(QtGui.QPainter.HighQualityAntialiasing) + painter.setRenderHint(QtGui.QPainter.Antialiasing) horizontal = self.orientation() == QtCore.Qt.Horizontal diff --git a/client/ayon_core/tools/utils/color_widgets/color_triangle.py b/client/ayon_core/tools/utils/color_widgets/color_triangle.py index 290a33f0b0..7691c3e78d 100644 --- a/client/ayon_core/tools/utils/color_widgets/color_triangle.py +++ b/client/ayon_core/tools/utils/color_widgets/color_triangle.py @@ -261,7 +261,7 @@ class QtColorTriangle(QtWidgets.QWidget): pix = self.bg_image.copy() pix_painter = QtGui.QPainter(pix) - pix_painter.setRenderHint(QtGui.QPainter.HighQualityAntialiasing) + pix_painter.setRenderHint(QtGui.QPainter.Antialiasing) trigon_path = QtGui.QPainterPath() trigon_path.moveTo(self.point_a) diff --git a/client/ayon_core/tools/utils/lib.py b/client/ayon_core/tools/utils/lib.py index f7919a3317..a99c46199b 100644 --- a/client/ayon_core/tools/utils/lib.py +++ b/client/ayon_core/tools/utils/lib.py @@ -118,9 +118,6 @@ def paint_image_with_color(image, color): QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform ) - # Deprecated since 5.14 - if hasattr(QtGui.QPainter, "HighQualityAntialiasing"): - render_hints |= QtGui.QPainter.HighQualityAntialiasing painter.setRenderHints(render_hints) painter.setClipRegion(alpha_region) diff --git a/client/ayon_core/tools/utils/sliders.py b/client/ayon_core/tools/utils/sliders.py index ea1e01b9ea..c762b6ade0 100644 --- a/client/ayon_core/tools/utils/sliders.py +++ b/client/ayon_core/tools/utils/sliders.py @@ -58,7 +58,7 @@ class NiceSlider(QtWidgets.QSlider): painter.fillRect(event.rect(), QtCore.Qt.transparent) - painter.setRenderHint(QtGui.QPainter.HighQualityAntialiasing) + painter.setRenderHint(QtGui.QPainter.Antialiasing) horizontal = self.orientation() == QtCore.Qt.Horizontal diff --git a/client/ayon_core/tools/utils/thumbnail_paint_widget.py b/client/ayon_core/tools/utils/thumbnail_paint_widget.py index 9dbc2bcdd0..e67b820417 100644 --- a/client/ayon_core/tools/utils/thumbnail_paint_widget.py +++ b/client/ayon_core/tools/utils/thumbnail_paint_widget.py @@ -205,8 +205,6 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform ) - if hasattr(QtGui.QPainter, "HighQualityAntialiasing"): - render_hints |= QtGui.QPainter.HighQualityAntialiasing pix_painter.setRenderHints(render_hints) pix_painter.drawPixmap(pos_x, pos_y, scaled_pix) @@ -241,8 +239,6 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform ) - if hasattr(QtGui.QPainter, "HighQualityAntialiasing"): - render_hints |= QtGui.QPainter.HighQualityAntialiasing pix_painter.setRenderHints(render_hints) tiled_rect = QtCore.QRectF( @@ -335,8 +331,6 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform ) - if hasattr(QtGui.QPainter, "HighQualityAntialiasing"): - render_hints |= QtGui.QPainter.HighQualityAntialiasing final_painter.setRenderHints(render_hints) diff --git a/client/ayon_core/tools/utils/widgets.py b/client/ayon_core/tools/utils/widgets.py index af0745af1f..388eb34cd1 100644 --- a/client/ayon_core/tools/utils/widgets.py +++ b/client/ayon_core/tools/utils/widgets.py @@ -624,8 +624,6 @@ class ClassicExpandBtnLabel(ExpandBtnLabel): QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform ) - if hasattr(QtGui.QPainter, "HighQualityAntialiasing"): - render_hints |= QtGui.QPainter.HighQualityAntialiasing painter.setRenderHints(render_hints) painter.drawPixmap(QtCore.QPoint(pos_x, pos_y), pixmap) painter.end() @@ -788,8 +786,6 @@ class PixmapButtonPainter(QtWidgets.QWidget): QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform ) - if hasattr(QtGui.QPainter, "HighQualityAntialiasing"): - render_hints |= QtGui.QPainter.HighQualityAntialiasing painter.setRenderHints(render_hints) if self._cached_pixmap is None: From c1b9eff2df4a2502c5f831c348ea0763fcafddf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 23 Jun 2025 16:59:45 +0200 Subject: [PATCH 122/155] :bug: fix comment and condition --- client/ayon_core/pipeline/load/plugins.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 5725133432..62fe8150ae 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -129,10 +129,12 @@ class LoaderPlugin(list): plugin_repre_names = cls.get_representations() plugin_product_types = cls.product_types plugin_product_base_types = cls.product_base_types - # If product type isn't defined on the loader plugin, + + # If the product base type isn't defined on the loader plugin, # then we will use the product types. - plugin_product_filter = ( - plugin_product_base_types or plugin_product_types) + plugin_product_filter = plugin_product_base_types + if plugin_product_filter is None: + plugin_product_filter = plugin_product_types repre_entity = context.get("representation") product_entity = context["product"] From 2f9cd88111196ba61632dfc4e6cdf918a32775f0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 24 Jun 2025 11:44:27 +0200 Subject: [PATCH 123/155] revert conditions --- client/ayon_core/pipeline/load/plugins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 62fe8150ae..05584e60e9 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -142,8 +142,8 @@ class LoaderPlugin(list): # then loader is not compatible with any context. if ( not plugin_repre_names - and not plugin_product_filter - and not cls.extensions + or not plugin_product_filter + or not cls.extensions ): return False From 4aefacaf44e567e2695dbe3c1db2b91f43b1ea35 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 24 Jun 2025 11:44:54 +0200 Subject: [PATCH 124/155] remove unncessary product base type filters redefinitins --- client/ayon_core/pipeline/load/plugins.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 05584e60e9..dc5bb0f66f 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -127,14 +127,16 @@ class LoaderPlugin(list): """ plugin_repre_names = cls.get_representations() - plugin_product_types = cls.product_types - plugin_product_base_types = cls.product_base_types # If the product base type isn't defined on the loader plugin, # then we will use the product types. - plugin_product_filter = plugin_product_base_types + plugin_product_filter = cls.product_base_types if plugin_product_filter is None: - plugin_product_filter = plugin_product_types + plugin_product_filter = cls.product_types + + if plugin_product_filter: + plugin_product_filter = set(plugin_product_filter) + repre_entity = context.get("representation") product_entity = context["product"] @@ -164,7 +166,6 @@ class LoaderPlugin(list): if not cls.has_valid_extension(repre_entity): return False - plugin_product_types = set(plugin_product_types) product_type = product_entity.get("productType") product_base_type = product_entity.get("productBaseType") @@ -175,9 +176,6 @@ class LoaderPlugin(list): if product_filter is None: product_filter = product_type - plugin_product_filter = ( - plugin_product_base_types or plugin_product_types) - # If wildcard is used in product types or base types, # then we will consider the loader compatible with any product type. if "*" in plugin_product_filter: From 39d45b9fbe11625f344f83194ac014b02390e73b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 24 Jun 2025 12:06:14 +0200 Subject: [PATCH 125/155] remove not existing 'IconData' --- client/ayon_core/tools/loader/models/products.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/tools/loader/models/products.py b/client/ayon_core/tools/loader/models/products.py index 41919461d0..16ba91e7d5 100644 --- a/client/ayon_core/tools/loader/models/products.py +++ b/client/ayon_core/tools/loader/models/products.py @@ -12,7 +12,6 @@ from ayon_api.operations import OperationsSession from ayon_core.lib import NestedCacheItem from ayon_core.style import get_default_entity_icon_color from ayon_core.tools.loader.abstract import ( - IconData, ProductTypeItem, ProductBaseTypeItem, ProductItem, @@ -113,7 +112,7 @@ def product_item_from_entity( product_type_icon = product_type_item.icon product_base_type_icon = product_base_type_item.icon - product_icon: IconData = { + product_icon = { "type": "awesome-font", "name": "fa.file-o", "color": get_default_entity_icon_color(), @@ -144,7 +143,7 @@ def product_type_item_from_data( # TODO implement icon implementation # icon = product_type_data["icon"] # color = product_type_data["color"] - icon: IconData = { + icon = { "type": "awesome-font", "name": "fa.folder", "color": "#0091B2", @@ -165,7 +164,7 @@ def product_base_type_item_from_data( ProductBaseTypeDict: Product base type item. """ - icon: IconData = { + icon = { "type": "awesome-font", "name": "fa.folder", "color": "#0091B2", @@ -176,7 +175,7 @@ def product_base_type_item_from_data( def create_default_product_type_item(product_type: str) -> ProductTypeItem: - icon: IconData = { + icon = { "type": "awesome-font", "name": "fa.folder", "color": "#0091B2", @@ -194,7 +193,7 @@ def create_default_product_base_type_item( Returns: ProductBaseTypeItem: Default product base type item. """ - icon: IconData = { + icon = { "type": "awesome-font", "name": "fa.folder", "color": "#0091B2", From 8ecb0331f594ed94c2df01063d0fdb1a86adf298 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 24 Jun 2025 12:06:25 +0200 Subject: [PATCH 126/155] use 'get_project_product_base_types' only if is implemented --- client/ayon_core/tools/loader/models/products.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/loader/models/products.py b/client/ayon_core/tools/loader/models/products.py index 16ba91e7d5..8291203697 100644 --- a/client/ayon_core/tools/loader/models/products.py +++ b/client/ayon_core/tools/loader/models/products.py @@ -283,8 +283,11 @@ class ProductsModel: cache = self._product_base_type_items_cache[project_name] if not cache.is_valid: - product_base_types = ayon_api.get_project_product_base_types( - project_name) + product_base_types = [] + if hasattr(ayon_api, "get_project_product_base_types"): + product_base_types = ayon_api.get_project_product_base_types( + project_name + ) cache.update_data([ product_base_type_item_from_data(product_base_type) for product_base_type in product_base_types From 5f4d4d72c21a088ea40bc8347258f3d49d1a7c38 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 24 Jun 2025 12:17:48 +0200 Subject: [PATCH 127/155] add todo --- client/ayon_core/tools/loader/models/products.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/tools/loader/models/products.py b/client/ayon_core/tools/loader/models/products.py index 8291203697..c177be4557 100644 --- a/client/ayon_core/tools/loader/models/products.py +++ b/client/ayon_core/tools/loader/models/products.py @@ -284,6 +284,8 @@ class ProductsModel: cache = self._product_base_type_items_cache[project_name] if not cache.is_valid: product_base_types = [] + # TODO add temp implementation here when it is actually + # implemented and available on server. if hasattr(ayon_api, "get_project_product_base_types"): product_base_types = ayon_api.get_project_product_base_types( project_name From 9e7f5f5256123d610a408c2734557faad22206c6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 24 Jun 2025 13:41:53 +0200 Subject: [PATCH 128/155] Sort the existing variants list --- client/ayon_core/tools/publisher/widgets/create_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/widgets/create_widget.py b/client/ayon_core/tools/publisher/widgets/create_widget.py index aecea2ec44..b9b3afd895 100644 --- a/client/ayon_core/tools/publisher/widgets/create_widget.py +++ b/client/ayon_core/tools/publisher/widgets/create_widget.py @@ -683,7 +683,7 @@ class CreateWidget(QtWidgets.QWidget): options = list(self._current_creator_variant_hints) if options: options.append("---") - options.extend(variant_hints) + options.extend(sorted(variant_hints)) # Add hints to actions self._variant_widget.set_options(options) From 698aca8656a6dbd3404a37106b728aa0344f6b9b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 24 Jun 2025 15:42:32 +0200 Subject: [PATCH 129/155] Extract source_resolution_* fields on representation Comes from requirement for sources from freelancers uploaded from TP or WP. --- client/ayon_core/plugins/publish/extract_review.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 89bc56c670..d650ff7688 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -1598,6 +1598,10 @@ class ExtractReview(pyblish.api.InstancePlugin): "FFprobe couldn't read resolution from input file: \"{}\"" ).format(full_input_path_single_file)) + # collect source values to be potentially used in burnins later + new_repre["source_resolution_width"] = input_width + new_repre["source_resolution_height"] = input_height + # NOTE Setting only one of `width` or `height` is not allowed # - settings value can't have None but has value of 0 output_width = output_def["width"] or output_width or None From 3daa7263ada61b883d805b266f78d91d08fe31d8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 24 Jun 2025 15:43:10 +0200 Subject: [PATCH 130/155] Provide new templates source_resolution_* for burnin text --- client/ayon_core/plugins/publish/extract_burnin.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client/ayon_core/plugins/publish/extract_burnin.py b/client/ayon_core/plugins/publish/extract_burnin.py index 3f7c2f4cba..fa7fd4e504 100644 --- a/client/ayon_core/plugins/publish/extract_burnin.py +++ b/client/ayon_core/plugins/publish/extract_burnin.py @@ -757,6 +757,15 @@ class ExtractBurnin(publish.Extractor): ) }) + # burnin source resolution which might be different than on review + repre_source_resolution_width = repre.get("source_resolution_width") + repre_source_resolution_height = repre.get("source_resolution_height") + if repre_source_resolution_width and repre_source_resolution_height: + burnin_data.update({ + "source_resolution_width": repre_source_resolution_width, + "source_resolution_height": repre_source_resolution_height + }) + def filter_burnins_defs(self, profile, instance): """Filter outputs by their values from settings. From 0f4718beb545e54d16c1db02093a76026a04ce46 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 24 Jun 2025 17:40:21 +0200 Subject: [PATCH 131/155] always update positions and set default geometry --- client/ayon_core/tools/launcher/ui/actions_widget.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/client/ayon_core/tools/launcher/ui/actions_widget.py b/client/ayon_core/tools/launcher/ui/actions_widget.py index 118debe123..c5f66aa5f7 100644 --- a/client/ayon_core/tools/launcher/ui/actions_widget.py +++ b/client/ayon_core/tools/launcher/ui/actions_widget.py @@ -470,21 +470,15 @@ class ActionMenuPopup(QtWidgets.QWidget): self._close_timer.stop() - update_position = False if action_id != self._current_id: - update_position = True + self.setGeometry(pos.x(), pos.y(), 1, 1) self._current_id = action_id self._update_items(action_items) # Make sure is visible if not self._showed: - update_position = True self.show() - if not update_position: - self.raise_() - return - # Set geometry to position # - first make sure widget changes from '_update_items' # are recalculated From bdbbb218f8213db38e407e2fc7bf03cb9066f206 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 24 Jun 2025 17:40:32 +0200 Subject: [PATCH 132/155] use variant label --- client/ayon_core/tools/launcher/ui/actions_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/launcher/ui/actions_widget.py b/client/ayon_core/tools/launcher/ui/actions_widget.py index c5f66aa5f7..ac00e7fe85 100644 --- a/client/ayon_core/tools/launcher/ui/actions_widget.py +++ b/client/ayon_core/tools/launcher/ui/actions_widget.py @@ -325,7 +325,7 @@ class ActionMenuPopupModel(QtGui.QStandardItemModel): item = QtGui.QStandardItem() item.setFlags(QtCore.Qt.ItemIsEnabled) - item.setData(action_item.full_label, QtCore.Qt.DisplayRole) + item.setData(action_item.variant_label, QtCore.Qt.DisplayRole) item.setData(action_item.full_label, QtCore.Qt.ToolTipRole) item.setData(icon, QtCore.Qt.DecorationRole) item.setData(action_item.identifier, ACTION_ID_ROLE) From 5eba658bd15e32fc24b0d694252543fdb8dcc8c5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 24 Jun 2025 17:42:05 +0200 Subject: [PATCH 133/155] adde group label showed on top of popup --- client/ayon_core/style/style.css | 6 ++ .../tools/launcher/ui/actions_widget.py | 90 ++++++++++++++----- 2 files changed, 72 insertions(+), 24 deletions(-) diff --git a/client/ayon_core/style/style.css b/client/ayon_core/style/style.css index 2e3bf3954f..97aef4ff91 100644 --- a/client/ayon_core/style/style.css +++ b/client/ayon_core/style/style.css @@ -851,6 +851,12 @@ ActionsView::item:hover { ActionsView::icon {} +ActionMenuPopup #GroupLabel { + padding: 5px; + border-radius: 3px; + background: #1C2C40; + color: #ffffff; +} ActionMenuPopup #ShadowFrame { border-radius: 5px; background: rgba(12, 13, 24, 0.5); diff --git a/client/ayon_core/tools/launcher/ui/actions_widget.py b/client/ayon_core/tools/launcher/ui/actions_widget.py index ac00e7fe85..5ad539ffca 100644 --- a/client/ayon_core/tools/launcher/ui/actions_widget.py +++ b/client/ayon_core/tools/launcher/ui/actions_widget.py @@ -395,12 +395,17 @@ class ActionMenuPopup(QtWidgets.QWidget): sh_l, sh_t, sh_r, sh_b = SHADOW_FRAME_MARGINS + group_label = QtWidgets.QLabel("|", self) + group_label.setObjectName("GroupLabel") + # View with actions view = ActionsView(self) view.setGridSize(QtCore.QSize(75, 80)) view.setIconSize(QtCore.QSize(32, 32)) view.move(QtCore.QPoint(sh_l, sh_t)) + view.stackUnder(group_label) + # Background draw bg_frame = QtWidgets.QFrame(self) bg_frame.setObjectName("ShadowFrame") @@ -432,6 +437,7 @@ class ActionMenuPopup(QtWidgets.QWidget): view.clicked.connect(self._on_clicked) view.config_requested.connect(self._on_configs_trigger) + self._group_label = group_label self._view = view self._bg_frame = bg_frame self._effect = effect @@ -461,7 +467,8 @@ class ActionMenuPopup(QtWidgets.QWidget): super().leaveEvent(event) self._close_timer.start() - def show_items(self, action_id, action_items, pos): + def show_items(self, group_label, action_id, action_items, pos): + self._group_label.setText(group_label) if not action_items: if self._showed: self._close_timer.start() @@ -484,53 +491,62 @@ class ActionMenuPopup(QtWidgets.QWidget): # are recalculated app = QtWidgets.QApplication.instance() app.processEvents() - items_count, size, target_size = self._get_size_hint() + items_count, start_size, target_size = self._get_size_hint() self._model.fill_to_count(items_count) + label_y_offset = self._get_label_y_offset() window = self.screen() window_geo = window.geometry() + _target_x = pos.x() + target_size.width() + _target_y = pos.y() + target_size.height() + label_y_offset right_to_left = ( - pos.x() + target_size.width() > window_geo.right() - or pos.y() + target_size.height() > window_geo.bottom() + _target_x > window_geo.right() + or _target_y > window_geo.bottom() ) sh_l, sh_t, sh_r, sh_b = SHADOW_FRAME_MARGINS viewport_offset = self._view.viewport().geometry().topLeft() pos_x = pos.x() - (sh_l + viewport_offset.x() + 2) pos_y = pos.y() - (sh_t + viewport_offset.y() + 1) - - bg_x = bg_y = 0 + bg_x = 0 + bg_y = label_y_offset sort_order = QtCore.Qt.DescendingOrder if right_to_left: sort_order = QtCore.Qt.AscendingOrder - size_diff = target_size - size + size_diff = target_size - start_size pos_x -= size_diff.width() pos_y -= size_diff.height() bg_x = size_diff.width() - bg_y = size_diff.height() bg_geo = QtCore.QRect( - bg_x, bg_y, size.width(), size.height() + bg_x, bg_y, start_size.width(), start_size.height() ) if self._expand_anim.state() == QtCore.QAbstractAnimation.Running: self._expand_anim.stop() - self._first_anim_frame = True + self._right_to_left = right_to_left self._proxy_model.sort(0, sort_order) self.setUpdatesEnabled(False) - self._view.setMask(bg_geo.adjusted(sh_l, sh_t, -sh_r, -sh_b)) + self._view.setMask( + bg_geo.adjusted( + sh_l, sh_t - label_y_offset, + -sh_r, -(sh_b + label_y_offset) + ) + ) self._view.setMinimumWidth(target_size.width()) self._view.setMaximumWidth(target_size.width()) self._view.setMinimumHeight(target_size.height()) - self._bg_frame.setGeometry(bg_geo) + self._view.move(0, label_y_offset) self.setGeometry( - pos_x, pos_y, - target_size.width(), target_size.height() + pos_x, pos_y - label_y_offset, + target_size.width(), target_size.height() + label_y_offset ) + self._bg_frame.setGeometry(bg_geo) self.setUpdatesEnabled(True) + self._expand_anim.updateCurrentTime(0) - self._expand_anim.setStartValue(size) + self._expand_anim.setStartValue(start_size) self._expand_anim.setEndValue(target_size) self._expand_anim.start() @@ -546,6 +562,11 @@ class ActionMenuPopup(QtWidgets.QWidget): action_id = index.data(ACTION_ID_ROLE) self.action_triggered.emit(action_id) + def _get_label_y_offset(self): + height = self._group_label.sizeHint().height() + # Is over view but does not cover the settings icon + return height - 5 + def _on_expand_anim(self, value): if not self._showed: if self._expand_anim.state() == QtCore.QAbstractAnimation.Running: @@ -553,20 +574,40 @@ class ActionMenuPopup(QtWidgets.QWidget): return bg_geo = self._bg_frame.geometry() + + label_y_offset = self._get_label_y_offset() + if self._right_to_left: + popup_geo = self.geometry() + diff_size = popup_geo.size() - value + pos = QtCore.QPoint( + diff_size.width(), diff_size.height() + ) + + bg_geo.moveTopLeft(pos) + bg_geo.setWidth(value.width()) bg_geo.setHeight(value.height()) - if self._right_to_left: - geo = self.geometry() - pos = QtCore.QPoint( - geo.width() - value.width(), - geo.height() - value.height(), - ) - bg_geo.setTopLeft(pos) + label_width = self._group_label.sizeHint().width() + label_pos_x = 0 + bgeo_tl = bg_geo.topLeft() + label_pos_y = bgeo_tl.y() - label_y_offset + if label_width < value.width(): + label_pos_x = bgeo_tl.x() + (value.width() - label_width) // 2 + + label_pos = QtCore.QPoint(label_pos_x, label_pos_y) sh_l, sh_t, sh_r, sh_b = SHADOW_FRAME_MARGINS - self._view.setMask(bg_geo.adjusted(sh_l, sh_t, -sh_r, -sh_b)) + self.setUpdatesEnabled(False) + self._view.setMask( + bg_geo.adjusted( + sh_l, sh_t - label_y_offset, + -sh_r, -(sh_b + label_y_offset) + ) + ) + self._group_label.move(label_pos) self._bg_frame.setGeometry(bg_geo) + self.setUpdatesEnabled(True) def _on_expand_finish(self): # Make sure that size is recalculated if src and targe size is same @@ -982,13 +1023,14 @@ class ActionsWidget(QtWidgets.QWidget): def _show_group_popup(self, index): action_id = index.data(ACTION_ID_ROLE) + group_label = index.data(QtCore.Qt.DisplayRole) action_items = self._model.get_group_items(action_id) rect = self._view.visualRect(index) pos = self.mapToGlobal(rect.topLeft()) popup_widget = self._get_popup_widget() popup_widget.show_items( - action_id, action_items, pos + group_label, action_id, action_items, pos ) def _trigger_action(self, action_id, index=None): From 231958c21c3ebffb1a76efd7888491f3ed9eeb7a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 24 Jun 2025 18:10:08 +0200 Subject: [PATCH 134/155] the label is painted over background --- client/ayon_core/style/style.css | 3 +- .../tools/launcher/ui/actions_widget.py | 57 +++++++++---------- 2 files changed, 28 insertions(+), 32 deletions(-) diff --git a/client/ayon_core/style/style.css b/client/ayon_core/style/style.css index 97aef4ff91..0179d10697 100644 --- a/client/ayon_core/style/style.css +++ b/client/ayon_core/style/style.css @@ -853,10 +853,9 @@ ActionsView::icon {} ActionMenuPopup #GroupLabel { padding: 5px; - border-radius: 3px; - background: #1C2C40; color: #ffffff; } + ActionMenuPopup #ShadowFrame { border-radius: 5px; background: rgba(12, 13, 24, 0.5); diff --git a/client/ayon_core/tools/launcher/ui/actions_widget.py b/client/ayon_core/tools/launcher/ui/actions_widget.py index 5ad539ffca..ddb1c20221 100644 --- a/client/ayon_core/tools/launcher/ui/actions_widget.py +++ b/client/ayon_core/tools/launcher/ui/actions_widget.py @@ -494,11 +494,12 @@ class ActionMenuPopup(QtWidgets.QWidget): items_count, start_size, target_size = self._get_size_hint() self._model.fill_to_count(items_count) - label_y_offset = self._get_label_y_offset() + label_sh = self._group_label.sizeHint() + label_width, label_height = label_sh.width(), label_sh.height() window = self.screen() window_geo = window.geometry() _target_x = pos.x() + target_size.width() - _target_y = pos.y() + target_size.height() + label_y_offset + _target_y = pos.y() + target_size.height() + label_height right_to_left = ( _target_x > window_geo.right() or _target_y > window_geo.bottom() @@ -508,8 +509,7 @@ class ActionMenuPopup(QtWidgets.QWidget): viewport_offset = self._view.viewport().geometry().topLeft() pos_x = pos.x() - (sh_l + viewport_offset.x() + 2) pos_y = pos.y() - (sh_t + viewport_offset.y() + 1) - bg_x = 0 - bg_y = label_y_offset + bg_x = bg_y = 0 sort_order = QtCore.Qt.DescendingOrder if right_to_left: sort_order = QtCore.Qt.AscendingOrder @@ -517,10 +517,18 @@ class ActionMenuPopup(QtWidgets.QWidget): pos_x -= size_diff.width() pos_y -= size_diff.height() bg_x = size_diff.width() + bg_y = size_diff.height() - label_height bg_geo = QtCore.QRect( - bg_x, bg_y, start_size.width(), start_size.height() + bg_x, bg_y, + start_size.width(), start_size.height() + label_height ) + + label_pos_x = sh_l + label_pos_y = bg_y + sh_t + if label_width < start_size.width(): + label_pos_x = bg_x + (start_size.width() - label_width) // 2 + if self._expand_anim.state() == QtCore.QAbstractAnimation.Running: self._expand_anim.stop() @@ -529,20 +537,18 @@ class ActionMenuPopup(QtWidgets.QWidget): self._proxy_model.sort(0, sort_order) self.setUpdatesEnabled(False) self._view.setMask( - bg_geo.adjusted( - sh_l, sh_t - label_y_offset, - -sh_r, -(sh_b + label_y_offset) - ) + bg_geo.adjusted(sh_l, sh_t, -sh_r, -sh_b) ) self._view.setMinimumWidth(target_size.width()) self._view.setMaximumWidth(target_size.width()) self._view.setMinimumHeight(target_size.height()) - self._view.move(0, label_y_offset) + self._view.move(0, label_height) self.setGeometry( - pos_x, pos_y - label_y_offset, - target_size.width(), target_size.height() + label_y_offset + pos_x, pos_y - label_height, + target_size.width(), target_size.height() + label_height ) self._bg_frame.setGeometry(bg_geo) + self._group_label.move(label_pos_x, label_pos_y) self.setUpdatesEnabled(True) self._expand_anim.updateCurrentTime(0) @@ -562,11 +568,6 @@ class ActionMenuPopup(QtWidgets.QWidget): action_id = index.data(ACTION_ID_ROLE) self.action_triggered.emit(action_id) - def _get_label_y_offset(self): - height = self._group_label.sizeHint().height() - # Is over view but does not cover the settings icon - return height - 5 - def _on_expand_anim(self, value): if not self._showed: if self._expand_anim.state() == QtCore.QAbstractAnimation.Running: @@ -575,37 +576,33 @@ class ActionMenuPopup(QtWidgets.QWidget): bg_geo = self._bg_frame.geometry() - label_y_offset = self._get_label_y_offset() + label_sh = self._group_label.sizeHint() + label_width, label_height = label_sh.width(), label_sh.height() if self._right_to_left: popup_geo = self.geometry() diff_size = popup_geo.size() - value pos = QtCore.QPoint( - diff_size.width(), diff_size.height() + diff_size.width(), diff_size.height() - label_height ) bg_geo.moveTopLeft(pos) bg_geo.setWidth(value.width()) - bg_geo.setHeight(value.height()) + bg_geo.setHeight(value.height() + label_height) label_width = self._group_label.sizeHint().width() - label_pos_x = 0 bgeo_tl = bg_geo.topLeft() - label_pos_y = bgeo_tl.y() - label_y_offset + sh_l, sh_t, sh_r, sh_b = SHADOW_FRAME_MARGINS + + label_pos_x = sh_l if label_width < value.width(): label_pos_x = bgeo_tl.x() + (value.width() - label_width) // 2 - label_pos = QtCore.QPoint(label_pos_x, label_pos_y) - - sh_l, sh_t, sh_r, sh_b = SHADOW_FRAME_MARGINS self.setUpdatesEnabled(False) self._view.setMask( - bg_geo.adjusted( - sh_l, sh_t - label_y_offset, - -sh_r, -(sh_b + label_y_offset) - ) + bg_geo.adjusted(sh_l, sh_t, -sh_r, -sh_b) ) - self._group_label.move(label_pos) + self._group_label.move(label_pos_x, sh_t) self._bg_frame.setGeometry(bg_geo) self.setUpdatesEnabled(True) From 7bd8578cd28c613fa876a06cd1b7c3aac03b4480 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 24 Jun 2025 18:16:41 +0200 Subject: [PATCH 135/155] fix view position --- client/ayon_core/tools/launcher/ui/actions_widget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/launcher/ui/actions_widget.py b/client/ayon_core/tools/launcher/ui/actions_widget.py index ddb1c20221..fddf88bed6 100644 --- a/client/ayon_core/tools/launcher/ui/actions_widget.py +++ b/client/ayon_core/tools/launcher/ui/actions_widget.py @@ -402,7 +402,7 @@ class ActionMenuPopup(QtWidgets.QWidget): view = ActionsView(self) view.setGridSize(QtCore.QSize(75, 80)) view.setIconSize(QtCore.QSize(32, 32)) - view.move(QtCore.QPoint(sh_l, sh_t)) + view.move(sh_l, sh_t) view.stackUnder(group_label) @@ -542,7 +542,7 @@ class ActionMenuPopup(QtWidgets.QWidget): self._view.setMinimumWidth(target_size.width()) self._view.setMaximumWidth(target_size.width()) self._view.setMinimumHeight(target_size.height()) - self._view.move(0, label_height) + self._view.move(sh_l, sh_t + label_height) self.setGeometry( pos_x, pos_y - label_height, target_size.width(), target_size.height() + label_height From 9a40f533038b968a553b6cb3059f3a64743b1077 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 24 Jun 2025 18:23:01 +0200 Subject: [PATCH 136/155] added some docstring --- .../tools/launcher/ui/actions_widget.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/client/ayon_core/tools/launcher/ui/actions_widget.py b/client/ayon_core/tools/launcher/ui/actions_widget.py index fddf88bed6..51cb8e73bc 100644 --- a/client/ayon_core/tools/launcher/ui/actions_widget.py +++ b/client/ayon_core/tools/launcher/ui/actions_widget.py @@ -374,6 +374,22 @@ class ActionMenuPopupModel(QtGui.QStandardItemModel): class ActionMenuPopup(QtWidgets.QWidget): + """Popup widget for group varaints. + + The popup is handling most of the layout and showing of the items + manually. + + There 4 parts: + 1. Shadow - semi transparent black widget used as shadow. + 2. Background - painted over the shadow with blur effect. All + other items are painted over. + 3. Label - show group label and positioned manually at the top + of the popup. + 4. View - View with variant action items. View is positioned + and resized manually according to the items in the group and then + animated using mask region. + + """ action_triggered = QtCore.Signal(str) config_requested = QtCore.Signal(str, QtCore.QPoint) From 93557a8a69c2feaa83f9b3aeaea12932605f2b87 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 24 Jun 2025 22:34:37 +0200 Subject: [PATCH 137/155] Refactor `conditionalEnum` -> `conditional_enum` Avoid logs like: ``` DEBUG settings.settings_field | Deprecated argument: conditionalEnum ``` --- server/settings/main.py | 4 ++-- server/settings/publish_plugins.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/server/settings/main.py b/server/settings/main.py index dd6af0a104..93cedf2d65 100644 --- a/server/settings/main.py +++ b/server/settings/main.py @@ -106,7 +106,7 @@ class FallbackProductModel(BaseSettingsModel): fallback_type: str = SettingsField( title="Fallback config type", enum_resolver=_fallback_ocio_config_profile_types, - conditionalEnum=True, + conditional_enum=True, default="builtin_path", description=( "Type of config which needs to be used in case published " @@ -162,7 +162,7 @@ class CoreImageIOConfigProfilesModel(BaseSettingsModel): type: str = SettingsField( title="Profile type", enum_resolver=_ocio_config_profile_types, - conditionalEnum=True, + conditional_enum=True, default="builtin_path", section="---", ) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 793ca659e5..d690d79607 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -340,7 +340,7 @@ class ResizeModel(BaseSettingsModel): title="Type", description="Type of resizing", enum_resolver=lambda: _resize_types_enum, - conditionalEnum=True, + conditional_enum=True, default="source" ) @@ -373,7 +373,7 @@ class ExtractThumbnailOIIODefaultsModel(BaseSettingsModel): title="Type", description="Transcoding type", enum_resolver=lambda: _thumbnail_oiio_transcoding_type, - conditionalEnum=True, + conditional_enum=True, default="colorspace" ) @@ -476,7 +476,7 @@ class ExtractOIIOTranscodeOutputModel(BaseSettingsModel): "colorspace", title="Transcoding type", enum_resolver=_extract_oiio_transcoding_type, - conditionalEnum=True, + conditional_enum=True, description=( "Select the transcoding type for your output, choosing either " "*Colorspace* or *Display&View* transform." From e9bc6e07f487d688bbd066f74b430a6b4036e928 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 25 Jun 2025 11:40:58 +0200 Subject: [PATCH 138/155] Do not overwrite if previously collected Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/plugins/publish/extract_review.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index d650ff7688..0ee4cbff07 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -1599,8 +1599,10 @@ class ExtractReview(pyblish.api.InstancePlugin): ).format(full_input_path_single_file)) # collect source values to be potentially used in burnins later - new_repre["source_resolution_width"] = input_width - new_repre["source_resolution_height"] = input_height + if "source_resolution_width" not in new_repre: + new_repre["source_resolution_width"] = input_width + if "source_resolution_height" not in new_repre: + new_repre["source_resolution_height"] = input_height # NOTE Setting only one of `width` or `height` is not allowed # - settings value can't have None but has value of 0 From fd49121831d4856478b0a6dadd9ac39df144e48b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 25 Jun 2025 15:33:43 +0200 Subject: [PATCH 139/155] added support for project webactions --- client/ayon_core/tools/launcher/models/actions.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/launcher/models/actions.py b/client/ayon_core/tools/launcher/models/actions.py index 0ed4bdad3a..adb8d371ed 100644 --- a/client/ayon_core/tools/launcher/models/actions.py +++ b/client/ayon_core/tools/launcher/models/actions.py @@ -352,9 +352,6 @@ class ActionsModel: ) def _get_webaction_request_data(self, selection: LauncherActionSelection): - if not selection.is_project_selected: - return None - entity_type = None entity_id = None entity_subtypes = [] @@ -368,6 +365,13 @@ class ActionsModel: entity_id = selection.folder_entity["id"] entity_subtypes = [selection.folder_entity["folderType"]] + elif selection.is_project_selected: + # Project actions are supported since AYON 1.9.1 + ma, mi, pa, _, _ = ayon_api.get_server_version_tuple() + if (ma, mi, pa) < (1, 9, 1): + return None + entity_type = "project" + entity_ids = [] if entity_id: entity_ids.append(entity_id) @@ -381,10 +385,10 @@ class ActionsModel: } def _get_webactions(self, selection: LauncherActionSelection): - if not selection.is_project_selected: + request_data = self._get_webaction_request_data(selection) + if request_data is None: return [] - request_data = self._get_webaction_request_data(selection) project_name = selection.project_name entity_id = None if request_data["entityIds"]: From b94b6f2de4c0fd3b06e0c3eb741ea12748a7ab5f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 25 Jun 2025 16:24:20 +0200 Subject: [PATCH 140/155] make folders view deselectable --- client/ayon_core/tools/launcher/ui/hierarchy_page.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/tools/launcher/ui/hierarchy_page.py b/client/ayon_core/tools/launcher/ui/hierarchy_page.py index 7c34989947..65efdc27ac 100644 --- a/client/ayon_core/tools/launcher/ui/hierarchy_page.py +++ b/client/ayon_core/tools/launcher/ui/hierarchy_page.py @@ -68,6 +68,7 @@ class HierarchyPage(QtWidgets.QWidget): # - Folders widget folders_widget = FoldersWidget(controller, content_body) folders_widget.set_header_visible(True) + folders_widget.set_deselectable(True) # - Tasks widget tasks_widget = TasksWidget(controller, content_body) From cd344e671068c4374352309c8847baffedef6114 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 27 Jun 2025 10:37:58 +0200 Subject: [PATCH 141/155] don't use 'annotations' import in pyblish plugins --- .../plugins/publish/extract_review.py | 15 +++--- .../plugins/publish/integrate_traits.py | 47 +++++++++---------- 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 89bc56c670..9864e3a320 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -1,4 +1,3 @@ -from __future__ import annotations import os import re import copy @@ -52,7 +51,7 @@ class TempData: pixel_aspect: float resolution_width: int resolution_height: int - origin_repre: dict[str, Any] + origin_repre: "dict[str, Any]" input_is_sequence: bool first_sequence_frame: int input_allow_bg: bool @@ -60,12 +59,12 @@ class TempData: without_handles: bool handles_are_set: bool input_ext: str - explicit_input_paths: list[str] - paths_to_remove: list[str] + explicit_input_paths: "list[str]" + paths_to_remove: "list[str]" # Set later full_output_path: str = "" - filled_files: dict[int, str] = field(default_factory=dict) + filled_files: "dict[int, str]" = field(default_factory=dict) output_ext_is_image: bool = True output_is_sequence: bool = True @@ -1020,7 +1019,7 @@ class ExtractReview(pyblish.api.InstancePlugin): current_repre_name: str, start_frame: int, end_frame: int - ) -> Optional[dict[int, str]]: + ) -> Optional["dict[int, str]"]: """Tries to replace missing frames from ones from last version""" repre_file_paths = self._get_last_version_files( instance, current_repre_name) @@ -1108,7 +1107,7 @@ class ExtractReview(pyblish.api.InstancePlugin): resolution_height: int, extension: str, temp_data: TempData - ) -> Optional[dict[int, str]]: + ) -> Optional["dict[int, str]"]: """Fills missing files by blank frame.""" blank_frame_path = None @@ -1164,7 +1163,7 @@ class ExtractReview(pyblish.api.InstancePlugin): staging_dir: str, start_frame: int, end_frame: int - ) -> dict[int, str]: + ) -> "dict[int, str]": """Fill missing files in sequence by duplicating existing ones. This will take nearest frame file and copy it with so as to fill diff --git a/client/ayon_core/plugins/publish/integrate_traits.py b/client/ayon_core/plugins/publish/integrate_traits.py index 38c9ecdeb4..5829510bdb 100644 --- a/client/ayon_core/plugins/publish/integrate_traits.py +++ b/client/ayon_core/plugins/publish/integrate_traits.py @@ -1,6 +1,4 @@ """Integrate representations with traits.""" -from __future__ import annotations - import contextlib import copy import hashlib @@ -87,7 +85,7 @@ class TransferItem: size: int checksum: str template: str - template_data: dict[str, Any] + template_data: "dict[str, Any]" representation: Representation related_trait: FileLocation @@ -134,7 +132,7 @@ class TemplateItem: """ anatomy: Anatomy template: str - template_data: dict[str, Any] + template_data: "dict[str, Any]" template_object: AnatomyTemplateItem @@ -144,14 +142,14 @@ class RepresentationEntity: id: str versionId: str # noqa: N815 name: str - files: dict[str, Any] - attrib: dict[str, Any] + files: "dict[str, Any]" + attrib: "dict[str, Any]" data: str - tags: list[str] + tags: "list[str]" status: str -def get_instance_families(instance: pyblish.api.Instance) -> list[str]: +def get_instance_families(instance: pyblish.api.Instance) -> "list[str]": """Get all families of the instance. Todo: @@ -177,7 +175,7 @@ def get_instance_families(instance: pyblish.api.Instance) -> list[str]: def get_changed_attributes( - old_entity: dict, new_entity: dict) -> (dict[str, Any]): + old_entity: dict, new_entity: dict) -> ("dict[str, Any]"): """Prepare changes for entity update. Todo: @@ -212,7 +210,7 @@ def get_changed_attributes( return changes -def prepare_for_json(data: dict[str, Any]) -> dict[str, Any]: +def prepare_for_json(data: "dict[str, Any]") -> "dict[str, Any]": """Prepare data for JSON serialization. If there are values that json cannot serialize, this function will @@ -354,7 +352,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): def get_transfers_from_representations( self, instance: pyblish.api.Instance, - representations: list[Representation]) -> list[TransferItem]: + representations: "list[Representation]") -> "list[TransferItem]": """Get transfers from representations. This method will go through all representations and prepare transfers @@ -376,7 +374,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): """ template: str = self.get_publish_template(instance) instance_template_data: dict[str, str] = {} - transfers: list[TransferItem] = [] + transfers: "list[TransferItem]" = [] # prepare template and data to format it for representation in representations: @@ -471,7 +469,8 @@ class IntegrateTraits(pyblish.api.InstancePlugin): @staticmethod def filter_lifecycle( - representations: list[Representation]) -> list[Representation]: + representations: "list[Representation]" + ) -> "list[Representation]": """Filter representations based on LifeCycle traits. Args: @@ -887,7 +886,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): def get_transfers_from_file_locations( representation: Representation, template_item: TemplateItem, - transfers: list[TransferItem]) -> None: + transfers: "list[TransferItem]") -> None: """Get transfers from FileLocations trait. Args: @@ -928,7 +927,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): def get_transfers_from_sequence( representation: Representation, template_item: TemplateItem, - transfers: list[TransferItem] + transfers: "list[TransferItem]" ) -> None: """Get transfers from Sequence trait. @@ -949,7 +948,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): # template is higher, us the one from the template dst_padding = representation.get_trait( Sequence).frame_padding - frames: list[int] = sequence.get_frame_list( + frames: "list[int]" = sequence.get_frame_list( representation.get_trait(FileLocations), regex=sequence.frame_regex) template_padding = template_item.anatomy.templates_obj.frame_padding @@ -1000,7 +999,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): def get_transfers_from_udim( representation: Representation, template_item: TemplateItem, - transfers: list[TransferItem] + transfers: "list[TransferItem]" ) -> None: """Get transfers from UDIM trait. @@ -1056,7 +1055,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): def get_transfers_from_file_location( representation: Representation, template_item: TemplateItem, - transfers: list[TransferItem] + transfers: "list[TransferItem]" ) -> None: """Get transfers from FileLocation trait. @@ -1114,7 +1113,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): def get_transfers_from_bundle( representation: Representation, template_item: TemplateItem, - transfers: list[TransferItem] + transfers: "list[TransferItem]" ) -> None: """Get transfers from Bundle trait. @@ -1152,7 +1151,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): ) def _prepare_file_info( - self, path: Path, anatomy: Anatomy) -> dict[str, Any]: + self, path: Path, anatomy: Anatomy) -> "dict[str, Any]": """Prepare information for one file (asset or resource). Arguments: @@ -1181,10 +1180,10 @@ class IntegrateTraits(pyblish.api.InstancePlugin): def _get_legacy_files_for_representation( self, - transfer_items: list[TransferItem], + transfer_items: "list[TransferItem]", representation: Representation, anatomy: Anatomy, - ) -> list[dict[str, str]]: + ) -> "list[dict[str, str]]": """Get legacy files for a given representation. This expects the file to exist - it must run after the transfer @@ -1194,13 +1193,13 @@ class IntegrateTraits(pyblish.api.InstancePlugin): list: List of legacy files. """ - selected: list[TransferItem] = [] + selected: "list[TransferItem]" = [] selected.extend( item for item in transfer_items if item.representation == representation ) - files: list[dict[str, str]] = [] + files: "list[dict[str, str]]" = [] files.extend( self._prepare_file_info(item.destination, anatomy) for item in selected From cff69e19d04bd05f2d648e52ba420a32e61649fe Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 27 Jun 2025 11:35:09 +0200 Subject: [PATCH 142/155] fix typehints --- .../plugins/publish/integrate_traits.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate_traits.py b/client/ayon_core/plugins/publish/integrate_traits.py index 5829510bdb..7edd7c9cb5 100644 --- a/client/ayon_core/plugins/publish/integrate_traits.py +++ b/client/ayon_core/plugins/publish/integrate_traits.py @@ -130,10 +130,10 @@ class TemplateItem: template_data (dict[str, Any]): Template data. template_object (AnatomyTemplateItem): Template object """ - anatomy: Anatomy + anatomy: "Anatomy" template: str template_data: "dict[str, Any]" - template_object: AnatomyTemplateItem + template_object: "AnatomyTemplateItem" @dataclass @@ -534,7 +534,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): return path_template_obj.template.replace("\\", "/") def get_publish_template_object( - self, instance: pyblish.api.Instance) -> AnatomyTemplateItem: + self, instance: pyblish.api.Instance) -> "AnatomyTemplateItem": """Return anatomy template object to use for integration. Note: What is the actual type of the object? @@ -755,7 +755,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): return version_data - def get_rootless_path(self, anatomy: Anatomy, path: str) -> str: + def get_rootless_path(self, anatomy: "Anatomy", path: str) -> str: r"""Get rootless variant of the path. Returns, if possible, a path without an absolute portion from the root @@ -1014,7 +1014,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): """ udim: UDIM = representation.get_trait(UDIM) - path_template_object: AnatomyStringTemplate = ( + path_template_object: "AnatomyStringTemplate" = ( template_item.template_object["path"] ) for file_loc in representation.get_trait( @@ -1069,7 +1069,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): template_item (TemplateItem): Template item. """ - path_template_object: AnatomyStringTemplate = ( + path_template_object: "AnatomyStringTemplate" = ( template_item.template_object["path"] ) template_item.template_data["ext"] = ( @@ -1151,7 +1151,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): ) def _prepare_file_info( - self, path: Path, anatomy: Anatomy) -> "dict[str, Any]": + self, path: Path, anatomy: "Anatomy") -> "dict[str, Any]": """Prepare information for one file (asset or resource). Arguments: @@ -1182,7 +1182,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): self, transfer_items: "list[TransferItem]", representation: Representation, - anatomy: Anatomy, + anatomy: "Anatomy", ) -> "list[dict[str, str]]": """Get legacy files for a given representation. From 0fb5220738326ef5b55afe72e71572cedd77fc25 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 27 Jun 2025 11:37:06 +0200 Subject: [PATCH 143/155] one more fix --- client/ayon_core/plugins/publish/integrate_traits.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/integrate_traits.py b/client/ayon_core/plugins/publish/integrate_traits.py index 7edd7c9cb5..7961170f3b 100644 --- a/client/ayon_core/plugins/publish/integrate_traits.py +++ b/client/ayon_core/plugins/publish/integrate_traits.py @@ -244,7 +244,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): label = "Integrate Traits of an Asset" order = pyblish.api.IntegratorOrder - log: logging.Logger + log: "logging.Logger" def process(self, instance: pyblish.api.Instance) -> None: """Integrate representations with traits. From 3c3b165e36d985b0d477e29de5270e40b61e45b3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 27 Jun 2025 13:16:03 +0200 Subject: [PATCH 144/155] don't use dataclass for now --- .../plugins/publish/extract_review.py | 91 +++++++++++++------ 1 file changed, 61 insertions(+), 30 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 005229e305..c9596a26dd 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -1,3 +1,4 @@ +from __future__ import annotations import os import re import copy @@ -6,7 +7,6 @@ import shutil import subprocess from abc import ABC, abstractmethod from typing import Any, Optional -from dataclasses import dataclass, field import tempfile import clique @@ -36,37 +36,68 @@ from ayon_core.pipeline.publish import ( from ayon_core.pipeline.publish.lib import add_repre_files_for_cleanup -@dataclass class TempData: """Temporary data used across extractor's process.""" - fps: float - frame_start: int - frame_end: int - handle_start: int - handle_end: int - frame_start_handle: int - frame_end_handle: int - output_frame_start: int - output_frame_end: int - pixel_aspect: float - resolution_width: int - resolution_height: int - origin_repre: "dict[str, Any]" - input_is_sequence: bool - first_sequence_frame: int - input_allow_bg: bool - with_audio: bool - without_handles: bool - handles_are_set: bool - input_ext: str - explicit_input_paths: "list[str]" - paths_to_remove: "list[str]" + def __init__( + self, + fps: float, + frame_start: int, + frame_end: int, + handle_start: int, + handle_end: int, + frame_start_handle: int, + frame_end_handle: int, + output_frame_start: int, + output_frame_end: int, + pixel_aspect: float, + resolution_width: int, + resolution_height: int, + origin_repre: dict[str, Any], + input_is_sequence: bool, + first_sequence_frame: int, + input_allow_bg: bool, + with_audio: bool, + without_handles: bool, + handles_are_set: bool, + input_ext: str, + explicit_input_paths: list[str], + paths_to_remove: list[str], - # Set later - full_output_path: str = "" - filled_files: "dict[int, str]" = field(default_factory=dict) - output_ext_is_image: bool = True - output_is_sequence: bool = True + # Set later + full_output_path: str = "", + filled_files: dict[int, str] = None, + output_ext_is_image: bool = True, + output_is_sequence: bool = True, + ): + if filled_files is None: + filled_files = {} + self.fps = fps + self.frame_start = frame_start + self.frame_end = frame_end + self.handle_start = handle_start + self.handle_end = handle_end + self.frame_start_handle = frame_start_handle + self.frame_end_handle = frame_end_handle + self.output_frame_start = output_frame_start + self.output_frame_end = output_frame_end + self.pixel_aspect = pixel_aspect + self.resolution_width = resolution_width + self.resolution_height = resolution_height + self.origin_repre = origin_repre + self.input_is_sequence = input_is_sequence + self.first_sequence_frame = first_sequence_frame + self.input_allow_bg = input_allow_bg + self.with_audio = with_audio + self.without_handles = without_handles + self.handles_are_set = handles_are_set + self.input_ext = input_ext + self.explicit_input_paths = explicit_input_paths + self.paths_to_remove = paths_to_remove + + self.full_output_path = full_output_path + self.filled_files = filled_files + self.output_ext_is_image = output_ext_is_image + self.output_is_sequence = output_is_sequence def frame_to_timecode(frame: int, fps: float) -> str: @@ -1019,7 +1050,7 @@ class ExtractReview(pyblish.api.InstancePlugin): current_repre_name: str, start_frame: int, end_frame: int - ) -> Optional["dict[int, str]"]: + ) -> Optional[dict[int, str]]: """Tries to replace missing frames from ones from last version""" repre_file_paths = self._get_last_version_files( instance, current_repre_name) From 358efdb8989351cc68e9bd131191a6317605493d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Fri, 27 Jun 2025 13:30:46 +0200 Subject: [PATCH 145/155] :recycle: remove dataclasses we can't use dataclasses in pyblish plugins until new version of pyblish-base is propagated to AYON ecosystem --- .../plugins/publish/integrate_traits.py | 88 ++++++++++++++++--- 1 file changed, 77 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate_traits.py b/client/ayon_core/plugins/publish/integrate_traits.py index 7961170f3b..9f1471d090 100644 --- a/client/ayon_core/plugins/publish/integrate_traits.py +++ b/client/ayon_core/plugins/publish/integrate_traits.py @@ -1,9 +1,9 @@ """Integrate representations with traits.""" +from __future__ import annotations import contextlib import copy import hashlib import json -from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING, Any @@ -62,7 +62,6 @@ if TYPE_CHECKING: ) -@dataclass(frozen=True) class TransferItem: """Represents a single transfer item. @@ -85,10 +84,29 @@ class TransferItem: size: int checksum: str template: str - template_data: "dict[str, Any]" + template_data: dict[str, Any] representation: Representation related_trait: FileLocation + def __init__(self, + source: Path, + destination: Path, + size: int, + checksum: str, + template: str, + template_data: dict[str, Any], + representation: Representation, + related_trait: FileLocation): + + self.source = source + self.destination = destination + self.size = size + self.checksum = checksum + self.template = template + self.template_data = template_data + self.representation = representation + self.related_trait = related_trait + @staticmethod def get_size(file_path: Path) -> int: """Get the size of the file. @@ -118,7 +136,6 @@ class TransferItem: ).hexdigest() -@dataclass class TemplateItem: """Represents single template item. @@ -130,24 +147,73 @@ class TemplateItem: template_data (dict[str, Any]): Template data. template_object (AnatomyTemplateItem): Template object """ - anatomy: "Anatomy" + anatomy: Anatomy template: str - template_data: "dict[str, Any]" - template_object: "AnatomyTemplateItem" + template_data: dict[str, Any] + template_object: AnatomyTemplateItem + + def __init__(self, + anatomy: Anatomy, + template: str, + template_data: dict[str, Any], + template_object: AnatomyTemplateItem): + """Initialize TemplateItem. + + Args: + anatomy (Anatomy): Anatomy object. + template (str): Template path. + template_data (dict[str, Any]): Template data. + template_object (AnatomyTemplateItem): Template object. + + """ + self.anatomy = anatomy + self.template = template + self.template_data = template_data + self.template_object = template_object -@dataclass class RepresentationEntity: """Representation entity data.""" id: str versionId: str # noqa: N815 name: str - files: "dict[str, Any]" - attrib: "dict[str, Any]" + files: dict[str, Any] + attrib: dict[str, Any] data: str - tags: "list[str]" + tags: list[str] status: str + def __init__(self, + id: str, + versionId: str, # noqa: N815 + name: str, + files: dict[str, Any], + attrib: dict[str, Any], + data: str, + tags: list[str], + status: str): + """Initialize RepresentationEntity. + + Args: + id (str): Entity ID. + versionId (str): Version ID. + name (str): Representation name. + files (dict[str, Any]): Files in the representation. + attrib (dict[str, Any]): Attributes of the representation. + data (str): Data of the representation. + tags (list[str]): Tags of the representation. + status (str): Status of the representation. + + """ + self.id = id + self.versionId = versionId + self.name = name + self.files = files + self.attrib = attrib + self.data = data + self.tags = tags + self.status = status + def get_instance_families(instance: pyblish.api.Instance) -> "list[str]": """Get all families of the instance. From 6dc0a0e698f624232e284ff1a6c7e130bd9ba592 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 27 Jun 2025 13:36:44 +0200 Subject: [PATCH 146/155] use ayon-core's publish plugin discovery --- client/ayon_core/pipeline/publish/lib.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 464b2b6d8f..fbd6ed0b0b 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -1052,16 +1052,15 @@ def main_cli_publish( log.info("Running publish ...") - plugins = pyblish.api.discover() - print("Using plugins:") - for plugin in plugins: - print(plugin) + discover_result = publish_plugins_discover() + publish_plugins = discover_result.plugins + print("\n".join(discover_result.get_report(only_errors=False))) # Error exit as soon as any error occurs. error_format = ("Failed {plugin.__name__}: " "{error} -- {error.traceback}") - for result in pyblish.util.publish_iter(): + for result in pyblish.util.publish_iter(plugins=publish_plugins): if result["error"]: log.error(error_format.format(**result)) # uninstall() From 8ad408c50f554c0002a3b77ed6b8a9d0749570b0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 27 Jun 2025 13:56:29 +0200 Subject: [PATCH 147/155] revert stringified typehints --- .../plugins/publish/extract_review.py | 4 +-- .../plugins/publish/integrate_traits.py | 36 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index c9596a26dd..a5f541225c 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -1138,7 +1138,7 @@ class ExtractReview(pyblish.api.InstancePlugin): resolution_height: int, extension: str, temp_data: TempData - ) -> Optional["dict[int, str]"]: + ) -> Optional[dict[int, str]]: """Fills missing files by blank frame.""" blank_frame_path = None @@ -1194,7 +1194,7 @@ class ExtractReview(pyblish.api.InstancePlugin): staging_dir: str, start_frame: int, end_frame: int - ) -> "dict[int, str]": + ) -> dict[int, str]: """Fill missing files in sequence by duplicating existing ones. This will take nearest frame file and copy it with so as to fill diff --git a/client/ayon_core/plugins/publish/integrate_traits.py b/client/ayon_core/plugins/publish/integrate_traits.py index 9f1471d090..3fbf57be88 100644 --- a/client/ayon_core/plugins/publish/integrate_traits.py +++ b/client/ayon_core/plugins/publish/integrate_traits.py @@ -215,7 +215,7 @@ class RepresentationEntity: self.status = status -def get_instance_families(instance: pyblish.api.Instance) -> "list[str]": +def get_instance_families(instance: pyblish.api.Instance) -> list[str]: """Get all families of the instance. Todo: @@ -241,7 +241,7 @@ def get_instance_families(instance: pyblish.api.Instance) -> "list[str]": def get_changed_attributes( - old_entity: dict, new_entity: dict) -> ("dict[str, Any]"): + old_entity: dict, new_entity: dict) -> dict[str, Any]: """Prepare changes for entity update. Todo: @@ -276,7 +276,7 @@ def get_changed_attributes( return changes -def prepare_for_json(data: "dict[str, Any]") -> "dict[str, Any]": +def prepare_for_json(data: dict[str, Any]) -> dict[str, Any]: """Prepare data for JSON serialization. If there are values that json cannot serialize, this function will @@ -418,7 +418,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): def get_transfers_from_representations( self, instance: pyblish.api.Instance, - representations: "list[Representation]") -> "list[TransferItem]": + representations: list[Representation]) -> list[TransferItem]: """Get transfers from representations. This method will go through all representations and prepare transfers @@ -440,7 +440,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): """ template: str = self.get_publish_template(instance) instance_template_data: dict[str, str] = {} - transfers: "list[TransferItem]" = [] + transfers: list[TransferItem] = [] # prepare template and data to format it for representation in representations: @@ -535,8 +535,8 @@ class IntegrateTraits(pyblish.api.InstancePlugin): @staticmethod def filter_lifecycle( - representations: "list[Representation]" - ) -> "list[Representation]": + representations: list[Representation] + ) -> list[Representation]: """Filter representations based on LifeCycle traits. Args: @@ -952,7 +952,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): def get_transfers_from_file_locations( representation: Representation, template_item: TemplateItem, - transfers: "list[TransferItem]") -> None: + transfers: list[TransferItem]) -> None: """Get transfers from FileLocations trait. Args: @@ -993,7 +993,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): def get_transfers_from_sequence( representation: Representation, template_item: TemplateItem, - transfers: "list[TransferItem]" + transfers: list[TransferItem] ) -> None: """Get transfers from Sequence trait. @@ -1014,7 +1014,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): # template is higher, us the one from the template dst_padding = representation.get_trait( Sequence).frame_padding - frames: "list[int]" = sequence.get_frame_list( + frames: list[int] = sequence.get_frame_list( representation.get_trait(FileLocations), regex=sequence.frame_regex) template_padding = template_item.anatomy.templates_obj.frame_padding @@ -1065,7 +1065,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): def get_transfers_from_udim( representation: Representation, template_item: TemplateItem, - transfers: "list[TransferItem]" + transfers: list[TransferItem] ) -> None: """Get transfers from UDIM trait. @@ -1121,7 +1121,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): def get_transfers_from_file_location( representation: Representation, template_item: TemplateItem, - transfers: "list[TransferItem]" + transfers: list[TransferItem] ) -> None: """Get transfers from FileLocation trait. @@ -1179,7 +1179,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): def get_transfers_from_bundle( representation: Representation, template_item: TemplateItem, - transfers: "list[TransferItem]" + transfers: list[TransferItem] ) -> None: """Get transfers from Bundle trait. @@ -1217,7 +1217,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin): ) def _prepare_file_info( - self, path: Path, anatomy: "Anatomy") -> "dict[str, Any]": + self, path: Path, anatomy: "Anatomy") -> dict[str, Any]: """Prepare information for one file (asset or resource). Arguments: @@ -1246,10 +1246,10 @@ class IntegrateTraits(pyblish.api.InstancePlugin): def _get_legacy_files_for_representation( self, - transfer_items: "list[TransferItem]", + transfer_items: list[TransferItem], representation: Representation, anatomy: "Anatomy", - ) -> "list[dict[str, str]]": + ) -> list[dict[str, str]]: """Get legacy files for a given representation. This expects the file to exist - it must run after the transfer @@ -1259,13 +1259,13 @@ class IntegrateTraits(pyblish.api.InstancePlugin): list: List of legacy files. """ - selected: "list[TransferItem]" = [] + selected: list[TransferItem] = [] selected.extend( item for item in transfer_items if item.representation == representation ) - files: "list[dict[str, str]]" = [] + files: list[dict[str, str]] = [] files.extend( self._prepare_file_info(item.destination, anatomy) for item in selected From 06186a2ef04b43da22b1c80f6913a140218781b7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 27 Jun 2025 13:57:48 +0200 Subject: [PATCH 148/155] stringify not imported types --- client/ayon_core/plugins/publish/integrate_traits.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate_traits.py b/client/ayon_core/plugins/publish/integrate_traits.py index 3fbf57be88..45f32be4a0 100644 --- a/client/ayon_core/plugins/publish/integrate_traits.py +++ b/client/ayon_core/plugins/publish/integrate_traits.py @@ -150,13 +150,13 @@ class TemplateItem: anatomy: Anatomy template: str template_data: dict[str, Any] - template_object: AnatomyTemplateItem + template_object: "AnatomyTemplateItem" def __init__(self, - anatomy: Anatomy, + anatomy: "Anatomy", template: str, template_data: dict[str, Any], - template_object: AnatomyTemplateItem): + template_object: "AnatomyTemplateItem"): """Initialize TemplateItem. Args: From bdc3285681ee2f36b473c077aa4a02cd2a379ec4 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 27 Jun 2025 12:36:01 +0000 Subject: [PATCH 149/155] [Automated] Add generated package files from main --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 11fc31799b..8531e3fc42 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.3.2+dev" +__version__ = "1.4.0" diff --git a/package.py b/package.py index 908d34ffa8..2ea60bd4e9 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.3.2+dev" +version = "1.4.0" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index f4a452a2b9..f5a272849f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.3.2+dev" +version = "1.4.0" description = "" authors = ["Ynput Team "] readme = "README.md" From 2352d812cc13929563a51ed4a69798be588477f5 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 27 Jun 2025 12:36:35 +0000 Subject: [PATCH 150/155] [Automated] Update version in package.py for develop --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 8531e3fc42..df92396802 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.4.0" +__version__ = "1.4.0+dev" diff --git a/package.py b/package.py index 2ea60bd4e9..efed91b6cf 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.4.0" +version = "1.4.0+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index f5a272849f..91579f04fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.4.0" +version = "1.4.0+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From 08f8548268f150420c65747356ecb3e2ea71acb5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 27 Jun 2025 12:37:29 +0000 Subject: [PATCH 151/155] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 2cef7d13b0..eff53116a2 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to AYON Tray options: + - 1.4.0 - 1.3.2 - 1.3.1 - 1.3.0 From fddaf75bffebe7b1686f47ab20eb19ffc76d1a16 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 27 Jun 2025 15:16:46 +0200 Subject: [PATCH 152/155] Do not register publish plug-in paths twice --- client/ayon_core/pipeline/publish/lib.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 464b2b6d8f..5d51b75b0c 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -1022,12 +1022,6 @@ def main_cli_publish( if addons_manager is None: addons_manager = AddonsManager() - # TODO validate if this has to happen - # - it should happen during 'install_ayon_plugins' - publish_paths = addons_manager.collect_plugin_paths()["publish"] - for plugin_path in publish_paths: - pyblish.api.register_plugin_path(plugin_path) - applications_addon = addons_manager.get_enabled_addon("applications") if applications_addon is not None: context = get_global_context() From fb7e442a06a97eeb1cddf38a0e095813cae4c99d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 27 Jun 2025 17:42:29 +0200 Subject: [PATCH 153/155] updated type hints in create context --- client/ayon_core/pipeline/create/context.py | 359 +++++++++++--------- 1 file changed, 205 insertions(+), 154 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index f0d9fa8927..1a838a3bd8 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import sys import copy @@ -10,13 +12,8 @@ import typing from typing import ( Optional, Iterable, - Tuple, - List, - Set, - Dict, Any, Callable, - Union, ) import pyblish.logic @@ -27,7 +24,7 @@ from ayon_core.settings import get_project_settings from ayon_core.lib import is_func_signature_supported from ayon_core.lib.events import QueuedEventSystem from ayon_core.lib.attribute_definitions import get_default_values -from ayon_core.host import IPublishHost, IWorkfileHost +from ayon_core.host import IWorkfileHost from ayon_core.pipeline import Anatomy from ayon_core.pipeline.template_data import get_template_data from ayon_core.pipeline.plugin_discover import DiscoverResult @@ -52,7 +49,15 @@ from .creator_plugins import ( discover_convertor_plugins, ) if typing.TYPE_CHECKING: + from ayon_core.host import HostBase, IPublishHost + from ayon_core.lib import AbstractAttrDef + from ayon_core.lib.events import EventCallback + from .structures import CreatedInstance + from .creator_plugins import BaseCreator + + class PublishHost(HostBase, IPublishHost): + pass # Import of functions and classes that were moved to different file # TODO Should be removed in future release - Added 24/08/28, 0.4.3-dev.1 @@ -157,16 +162,20 @@ class CreateContext: context which should be handled by host. Args: - host(ModuleType): Host implementation which handles implementation and + host (PublishHost): Host implementation which handles implementation and global metadata. - headless(bool): Context is created out of UI (Current not used). - reset(bool): Reset context on initialization. - discover_publish_plugins(bool): Discover publish plugins during reset + headless (bool): Context is created out of UI (Current not used). + reset (bool): Reset context on initialization. + discover_publish_plugins (bool): Discover publish plugins during reset phase. """ def __init__( - self, host, headless=False, reset=True, discover_publish_plugins=True + self, + host: "PublishHost", + headless: bool = False, + reset: bool = True, + discover_publish_plugins: bool = True, ): self.host = host @@ -267,15 +276,15 @@ class CreateContext: self.reset(discover_publish_plugins) @property - def instances(self): + def instances(self) -> Iterable["CreatedInstance"]: return self._instances_by_id.values() @property - def instances_by_id(self): + def instances_by_id(self) -> dict[str, "CreatedInstance"]: return self._instances_by_id @property - def publish_attributes(self): + def publish_attributes(self) -> PublishAttributes: """Access to global publish attributes.""" return self._publish_attributes @@ -294,15 +303,17 @@ class CreateContext: """ return self._instances_by_id.get(instance_id) - def get_sorted_creators(self, identifiers=None): + def get_sorted_creators( + self, identifiers: Optional[Iterable[str]] = None + ) -> list["BaseCreator"]: """Sorted creators by 'order' attribute. Args: - identifiers (Iterable[str]): Filter creators by identifiers. All - creators are returned if 'None' is passed. + identifiers (Optional[Iterable[str]]): Filter creators by + identifiers. All creators are returned if 'None' is passed. Returns: - List[BaseCreator]: Sorted creator plugins by 'order' value. + list[BaseCreator]: Sorted creator plugins by 'order' value. """ if identifiers is not None: @@ -320,21 +331,21 @@ class CreateContext: ) @property - def sorted_creators(self): + def sorted_creators(self) -> list["BaseCreator"]: """Sorted creators by 'order' attribute. Returns: - List[BaseCreator]: Sorted creator plugins by 'order' value. + list[BaseCreator]: Sorted creator plugins by 'order' value. """ return self.get_sorted_creators() @property - def sorted_autocreators(self): + def sorted_autocreators(self) -> list["AutoCreator"]: """Sorted auto-creators by 'order' attribute. Returns: - List[AutoCreator]: Sorted plugins by 'order' value. + list[AutoCreator]: Sorted plugins by 'order' value. """ return sorted( @@ -365,38 +376,38 @@ class CreateContext: return self.host.name return os.environ["AYON_HOST_NAME"] - def get_current_project_name(self) -> Optional[str]: + def get_current_project_name(self) -> str: """Project name which was used as current context on context reset. Returns: - Union[str, None]: Project name. - """ + Optional[str]: Project name. + """ return self._current_project_name def get_current_folder_path(self) -> Optional[str]: """Folder path which was used as current context on context reset. Returns: - Union[str, None]: Folder path. - """ + Optional[str]: Folder path. + """ return self._current_folder_path def get_current_task_name(self) -> Optional[str]: """Task name which was used as current context on context reset. Returns: - Union[str, None]: Task name. - """ + Optional[str]: Task name. + """ return self._current_task_name def get_current_task_type(self) -> Optional[str]: """Task type which was used as current context on context reset. Returns: - Union[str, None]: Task type. + Optional[str]: Task type. """ if self._current_task_type is _NOT_SET: @@ -407,11 +418,11 @@ class CreateContext: self._current_task_type = task_type return self._current_task_type - def get_current_project_entity(self) -> Optional[Dict[str, Any]]: + def get_current_project_entity(self) -> Optional[dict[str, Any]]: """Project entity for current context project. Returns: - Union[dict[str, Any], None]: Folder entity. + Optional[dict[str, Any]]: Folder entity. """ if self._current_project_entity is not _NOT_SET: @@ -423,7 +434,7 @@ class CreateContext: self._current_project_entity = project_entity return copy.deepcopy(self._current_project_entity) - def get_current_folder_entity(self) -> Optional[Dict[str, Any]]: + def get_current_folder_entity(self) -> Optional[dict[str, Any]]: """Folder entity for current context folder. Returns: @@ -437,11 +448,11 @@ class CreateContext: self._current_folder_entity = self.get_folder_entity(folder_path) return copy.deepcopy(self._current_folder_entity) - def get_current_task_entity(self) -> Optional[Dict[str, Any]]: + def get_current_task_entity(self) -> Optional[dict[str, Any]]: """Task entity for current context task. Returns: - Union[dict[str, Any], None]: Task entity. + Optional[dict[str, Any]]: Task entity. """ if self._current_task_entity is not _NOT_SET: @@ -454,16 +465,16 @@ class CreateContext: ) return copy.deepcopy(self._current_task_entity) - def get_current_workfile_path(self): + def get_current_workfile_path(self) -> Optional[str]: """Workfile path which was opened on context reset. Returns: - Union[str, None]: Workfile path. - """ + Optional[str]: Workfile path. + """ return self._current_workfile_path - def get_current_project_anatomy(self): + def get_current_project_anatomy(self) -> Anatomy: """Project anatomy for current project. Returns: @@ -475,7 +486,7 @@ class CreateContext: self._current_project_name) return self._current_project_anatomy - def get_current_project_settings(self): + def get_current_project_settings(self) -> dict[str, Any]: if self._current_project_settings is None: self._current_project_settings = get_project_settings( self.get_current_project_name()) @@ -483,7 +494,7 @@ class CreateContext: def get_template_data( self, folder_path: Optional[str], task_name: Optional[str] - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Prepare template data for given context. Method is using cached entities and settings to prepare template data. @@ -512,7 +523,7 @@ class CreateContext: ) @property - def context_has_changed(self): + def context_has_changed(self) -> bool: """Host context has changed. As context is used project, folder, task name and workfile path if @@ -520,8 +531,8 @@ class CreateContext: Returns: bool: Context changed. - """ + """ project_name, folder_path, task_name, workfile_path = ( self._get_current_host_context() ) @@ -532,17 +543,17 @@ class CreateContext: or self._current_workfile_path != workfile_path ) - project_name = property(get_current_project_name) - project_anatomy = property(get_current_project_anatomy) + project_name: str = property(get_current_project_name) + project_anatomy: Anatomy = property(get_current_project_anatomy) @property - def log(self): + def log(self) -> logging.Logger: """Dynamic access to logger.""" if self._log is None: self._log = logging.getLogger(self.__class__.__name__) return self._log - def reset(self, discover_publish_plugins=True): + def reset(self, discover_publish_plugins: bool = True) -> None: """Reset context with all plugins and instances. All changes will be lost if were not saved explicitely. @@ -561,7 +572,7 @@ class CreateContext: self.reset_finalization() - def refresh_thumbnails(self): + def refresh_thumbnails(self) -> None: """Cleanup thumbnail paths. Remove all thumbnail filepaths that are empty or lead to files which @@ -584,7 +595,7 @@ class CreateContext: for instance_id in invalid: self.thumbnail_paths_by_instance_id.pop(instance_id) - def reset_preparation(self): + def reset_preparation(self) -> None: """Prepare attributes that must be prepared/cleaned before reset.""" # Give ability to store shared data for collection phase @@ -598,14 +609,16 @@ class CreateContext: self._event_hub.clear_callbacks() - def reset_finalization(self): + def reset_finalization(self) -> None: """Cleanup of attributes after reset.""" # Stop access to collection shared data self._collection_shared_data = None self.refresh_thumbnails() - def _get_current_host_context(self): + def _get_current_host_context( + self + ) -> tuple[str, Optional[str], Optional[str], Optional[str]]: project_name = folder_path = task_name = workfile_path = None if hasattr(self.host, "get_current_context"): host_context = self.host.get_current_context() @@ -619,7 +632,7 @@ class CreateContext: return project_name, folder_path, task_name, workfile_path - def reset_current_context(self): + def reset_current_context(self) -> None: """Refresh current context. Reset is based on optional host implementation of `get_current_context` @@ -653,7 +666,7 @@ class CreateContext: self._current_project_anatomy = None self._current_project_settings = None - def reset_plugins(self, discover_publish_plugins=True): + def reset_plugins(self, discover_publish_plugins: bool = True) -> None: """Reload plugins. Reloads creators from preregistered paths and can load publish plugins @@ -664,7 +677,7 @@ class CreateContext: self._reset_creator_plugins() self._reset_convertor_plugins() - def _reset_publish_plugins(self, discover_publish_plugins): + def _reset_publish_plugins(self, discover_publish_plugins: bool) -> None: from ayon_core.pipeline import AYONPyblishPluginMixin from ayon_core.pipeline.publish import ( publish_plugins_discover @@ -718,7 +731,7 @@ class CreateContext: self.publish_plugins = plugins_by_targets self.plugins_with_defs = plugins_with_defs - def _reset_creator_plugins(self): + def _reset_creator_plugins(self) -> None: # Prepare settings project_settings = self.get_current_project_settings() @@ -784,7 +797,7 @@ class CreateContext: self.creators = creators self.disabled_creators = disabled_creators - def _reset_convertor_plugins(self): + def _reset_convertor_plugins(self) -> None: convertors_plugins = {} report = discover_convertor_plugins(return_report=True) self.convertor_discover_result = report @@ -807,7 +820,7 @@ class CreateContext: self.convertors_plugins = convertors_plugins - def reset_context_data(self): + def reset_context_data(self) -> None: """Reload context data using host implementation. These data are not related to any instance but may be needed for whole @@ -846,7 +859,9 @@ class CreateContext: plugin.__name__, attr_defs ) - def add_instances_added_callback(self, callback): + def add_instances_added_callback( + self, callback: Callable + ) -> "EventCallback": """Register callback for added instances. Event is triggered when instances are already available in context @@ -872,7 +887,9 @@ class CreateContext: """ return self._event_hub.add_callback(INSTANCE_ADDED_TOPIC, callback) - def add_instances_removed_callback(self, callback): + def add_instances_removed_callback( + self, callback: Callable + ) -> "EventCallback": """Register callback for removed instances. Event is triggered when instances are already removed from context. @@ -895,9 +912,11 @@ class CreateContext: stop listening. """ - self._event_hub.add_callback(INSTANCE_REMOVED_TOPIC, callback) + return self._event_hub.add_callback(INSTANCE_REMOVED_TOPIC, callback) - def add_value_changed_callback(self, callback): + def add_value_changed_callback( + self, callback: Callable + ) -> "EventCallback": """Register callback to listen value changes. Event is triggered when any value changes on any instance or @@ -931,9 +950,11 @@ class CreateContext: stop listening. """ - self._event_hub.add_callback(VALUE_CHANGED_TOPIC, callback) + return self._event_hub.add_callback(VALUE_CHANGED_TOPIC, callback) - def add_pre_create_attr_defs_change_callback(self, callback): + def add_pre_create_attr_defs_change_callback( + self, callback: Callable + ) -> "EventCallback": """Register callback to listen pre-create attribute changes. Create plugin can trigger refresh of pre-create attributes. Usage of @@ -957,11 +978,13 @@ class CreateContext: stop listening. """ - self._event_hub.add_callback( + return self._event_hub.add_callback( PRE_CREATE_ATTR_DEFS_CHANGED_TOPIC, callback ) - def add_create_attr_defs_change_callback(self, callback): + def add_create_attr_defs_change_callback( + self, callback: Callable + ) -> "EventCallback": """Register callback to listen create attribute changes. Create plugin changed attribute definitions of instance. @@ -984,9 +1007,13 @@ class CreateContext: stop listening. """ - self._event_hub.add_callback(CREATE_ATTR_DEFS_CHANGED_TOPIC, callback) + return self._event_hub.add_callback( + CREATE_ATTR_DEFS_CHANGED_TOPIC, callback + ) - def add_publish_attr_defs_change_callback(self, callback): + def add_publish_attr_defs_change_callback( + self, callback: Callable + ) -> "EventCallback": """Register callback to listen publish attribute changes. Publish plugin changed attribute definitions of instance of context. @@ -1018,11 +1045,11 @@ class CreateContext: stop listening. """ - self._event_hub.add_callback( + return self._event_hub.add_callback( PUBLISH_ATTR_DEFS_CHANGED_TOPIC, callback ) - def context_data_to_store(self): + def context_data_to_store(self) -> dict[str, Any]: """Data that should be stored by host function. The same data should be returned on loading. @@ -1031,19 +1058,21 @@ class CreateContext: "publish_attributes": self._publish_attributes.data_to_store() } - def context_data_changes(self): + def context_data_changes(self) -> TrackChangesItem: """Changes of attributes.""" return TrackChangesItem( self._original_context_data, self.context_data_to_store() ) - def set_context_publish_plugin_attr_defs(self, plugin_name, attr_defs): + def set_context_publish_plugin_attr_defs( + self, plugin_name: str, attr_defs: list["AbstractAttrDef"] + ) -> None: """Set attribute definitions for CreateContext publish plugin. Args: plugin_name(str): Name of publish plugin. - attr_defs(List[AbstractAttrDef]): Attribute definitions. + attr_defs(list[AbstractAttrDef]): Attribute definitions. """ self.publish_attributes.set_publish_plugin_attr_defs( @@ -1053,7 +1082,7 @@ class CreateContext: None, plugin_name ) - def creator_adds_instance(self, instance: "CreatedInstance"): + def creator_adds_instance(self, instance: "CreatedInstance") -> None: """Creator adds new instance to context. Instances should be added only from creators. @@ -1078,7 +1107,7 @@ class CreateContext: with self.bulk_add_instances() as bulk_info: bulk_info.append(instance) - def _get_creator_in_create(self, identifier): + def _get_creator_in_create(self, identifier: str) -> "BaseCreator": """Creator by identifier with unified error. Helper method to get creator by identifier with same error when creator @@ -1104,13 +1133,13 @@ class CreateContext: def create( self, - creator_identifier, - variant, - folder_entity=None, - task_entity=None, - pre_create_data=None, - active=None - ): + creator_identifier: str, + variant: str, + folder_entity: Optional[dict[str, Any]] = None, + task_entity: Optional[dict[str, Any]] = None, + pre_create_data: Optional[dict[str, Any]] = None, + active: Optional[bool] = None, + ) -> Any: """Trigger create of plugins with standartized arguments. Arguments 'folder_entity' and 'task_name' use current context as @@ -1123,10 +1152,10 @@ class CreateContext: Args: creator_identifier (str): Identifier of creator plugin. variant (str): Variant used for product name. - folder_entity (Dict[str, Any]): Folder entity which define context + folder_entity (dict[str, Any]): Folder entity which define context of creation (possible context of created instance/s). - task_entity (Dict[str, Any]): Task entity. - pre_create_data (Dict[str, Any]): Pre-create attribute values. + task_entity (dict[str, Any]): Task entity. + pre_create_data (dict[str, Any]): Pre-create attribute values. active (Optional[bool]): Whether the created instance defaults to be active or not. @@ -1209,7 +1238,9 @@ class CreateContext: _pre_create_data ) - def create_with_unified_error(self, identifier, *args, **kwargs): + def create_with_unified_error( + self, identifier: str, *args, **kwargs + ) -> Any: """Trigger create but raise only one error if anything fails. Added to raise unified exception. Capture any possible issues and @@ -1217,8 +1248,8 @@ class CreateContext: Args: identifier (str): Identifier of creator. - *args (Tuple[Any]): Arguments for create method. - **kwargs (Dict[Any, Any]): Keyword argument for create method. + *args (tuple[Any]): Arguments for create method. + **kwargs (dict[Any, Any]): Keyword argument for create method. Raises: CreatorsCreateFailed: When creation fails due to any possible @@ -1233,7 +1264,7 @@ class CreateContext: raise CreatorsCreateFailed([fail_info]) return result - def creator_removed_instance(self, instance: "CreatedInstance"): + def creator_removed_instance(self, instance: "CreatedInstance") -> None: """When creator removes instance context should be acknowledged. If creator removes instance context should know about it to avoid @@ -1246,57 +1277,61 @@ class CreateContext: self._remove_instances([instance]) - def add_convertor_item(self, convertor_identifier, label): + def add_convertor_item( + self, convertor_identifier: str, label: str + ) -> None: self.convertor_items_by_id[convertor_identifier] = ConvertorItem( convertor_identifier, label ) - def remove_convertor_item(self, convertor_identifier): + def remove_convertor_item(self, convertor_identifier: str) -> None: self.convertor_items_by_id.pop(convertor_identifier, None) @contextmanager - def bulk_add_instances(self, sender=None): + def bulk_add_instances(self, sender: Optional[str] = None): with self._bulk_context("add", sender) as bulk_info: yield bulk_info @contextmanager - def bulk_instances_collection(self, sender=None): + def bulk_instances_collection(self, sender: Optional[str] = None): """DEPRECATED use 'bulk_add_instances' instead.""" # TODO add warning with self.bulk_add_instances(sender) as bulk_info: yield bulk_info @contextmanager - def bulk_remove_instances(self, sender=None): + def bulk_remove_instances(self, sender: Optional[str] = None): with self._bulk_context("remove", sender) as bulk_info: yield bulk_info @contextmanager - def bulk_value_changes(self, sender=None): + def bulk_value_changes(self, sender: Optional[str] = None): with self._bulk_context("change", sender) as bulk_info: yield bulk_info @contextmanager - def bulk_pre_create_attr_defs_change(self, sender=None): + def bulk_pre_create_attr_defs_change(self, sender: Optional[str] = None): with self._bulk_context( "pre_create_attrs_change", sender ) as bulk_info: yield bulk_info @contextmanager - def bulk_create_attr_defs_change(self, sender=None): + def bulk_create_attr_defs_change(self, sender: Optional[str] = None): with self._bulk_context( "create_attrs_change", sender ) as bulk_info: yield bulk_info @contextmanager - def bulk_publish_attr_defs_change(self, sender=None): + def bulk_publish_attr_defs_change(self, sender: Optional[str] = None): with self._bulk_context("publish_attrs_change", sender) as bulk_info: yield bulk_info # --- instance change callbacks --- - def create_plugin_pre_create_attr_defs_changed(self, identifier: str): + def create_plugin_pre_create_attr_defs_changed( + self, identifier: str + ) -> None: """Create plugin pre-create attributes changed. Triggered by 'Creator'. @@ -1308,7 +1343,7 @@ class CreateContext: with self.bulk_pre_create_attr_defs_change() as bulk_item: bulk_item.append(identifier) - def instance_create_attr_defs_changed(self, instance_id: str): + def instance_create_attr_defs_changed(self, instance_id: str) -> None: """Instance attribute definitions changed. Triggered by instance 'CreatorAttributeValues' on instance. @@ -1323,7 +1358,7 @@ class CreateContext: def instance_publish_attr_defs_changed( self, instance_id: Optional[str], plugin_name: str - ): + ) -> None: """Instance attribute definitions changed. Triggered by instance 'PublishAttributeValues' on instance. @@ -1339,8 +1374,8 @@ class CreateContext: bulk_item.append((instance_id, plugin_name)) def instance_values_changed( - self, instance_id: Optional[str], new_values: Dict[str, Any] - ): + self, instance_id: Optional[str], new_values: dict[str, Any] + ) -> None: """Instance value changed. Triggered by `CreatedInstance, 'CreatorAttributeValues' @@ -1348,7 +1383,7 @@ class CreateContext: Args: instance_id (Optional[str]): Instance id or None for context. - new_values (Dict[str, Any]): Changed values. + new_values (dict[str, Any]): Changed values. """ if self._is_instance_events_ready(instance_id): @@ -1357,15 +1392,15 @@ class CreateContext: # --- context change callbacks --- def publish_attribute_value_changed( - self, plugin_name: str, value: Dict[str, Any] - ): + self, plugin_name: str, value: dict[str, Any] + ) -> None: """Context publish attribute values changed. Triggered by instance 'PublishAttributeValues' on context. Args: plugin_name (str): Plugin name which changed value. - value (Dict[str, Any]): Changed values. + value (dict[str, Any]): Changed values. """ self.instance_values_changed( @@ -1377,7 +1412,7 @@ class CreateContext: }, ) - def reset_instances(self): + def reset_instances(self) -> None: """Reload instances""" self._instances_by_id = collections.OrderedDict() @@ -1417,7 +1452,7 @@ class CreateContext: if failed_info: raise CreatorsCollectionFailed(failed_info) - def find_convertor_items(self): + def find_convertor_items(self) -> None: """Go through convertor plugins to look for items to convert. Raises: @@ -1448,7 +1483,7 @@ class CreateContext: if failed_info: raise ConvertorsFindFailed(failed_info) - def execute_autocreators(self): + def execute_autocreators(self) -> None: """Execute discovered AutoCreator plugins. Reset instances if any autocreator executed properly. @@ -1464,14 +1499,16 @@ class CreateContext: if failed_info: raise CreatorsCreateFailed(failed_info) - def get_folder_entities(self, folder_paths: Iterable[str]): + def get_folder_entities( + self, folder_paths: Iterable[str] + ) -> dict[str, Optional[dict[str, Any]]]: """Get folder entities by paths. Args: folder_paths (Iterable[str]): Folder paths. Returns: - Dict[str, Optional[Dict[str, Any]]]: Folder entities by path. + dict[str, Optional[dict[str, Any]]]: Folder entities by path. """ output = { @@ -1511,18 +1548,18 @@ class CreateContext: def get_task_entities( self, - task_names_by_folder_paths: Dict[str, Set[str]] - ) -> Dict[str, Dict[str, Optional[Dict[str, Any]]]]: + task_names_by_folder_paths: dict[str, set[str]] + ) -> dict[str, dict[str, Optional[dict[str, Any]]]]: """Get task entities by folder path and task name. Entities are cached until reset. Args: - task_names_by_folder_paths (Dict[str, Set[str]]): Task names by + task_names_by_folder_paths (dict[str, set[str]]): Task names by folder path. Returns: - Dict[str, Dict[str, Dict[str, Any]]]: Task entities by folder path + dict[str, dict[str, dict[str, Any]]]: Task entities by folder path and task name. """ @@ -1609,7 +1646,7 @@ class CreateContext: def get_folder_entity( self, folder_path: Optional[str], - ) -> Optional[Dict[str, Any]]: + ) -> Optional[dict[str, Any]]: """Get folder entity by path. Entities are cached until reset. @@ -1618,7 +1655,7 @@ class CreateContext: folder_path (Optional[str]): Folder path. Returns: - Optional[Dict[str, Any]]: Folder entity. + Optional[dict[str, Any]]: Folder entity. """ if not folder_path: @@ -1629,7 +1666,7 @@ class CreateContext: self, folder_path: Optional[str], task_name: Optional[str], - ) -> Optional[Dict[str, Any]]: + ) -> Optional[dict[str, Any]]: """Get task entity by name and folder path. Entities are cached until reset. @@ -1639,7 +1676,7 @@ class CreateContext: task_name (Optional[str]): Task name. Returns: - Optional[Dict[str, Any]]: Task entity. + Optional[dict[str, Any]]: Task entity. """ if not folder_path or not task_name: @@ -1650,7 +1687,7 @@ class CreateContext: def get_instances_folder_entities( self, instances: Optional[Iterable["CreatedInstance"]] = None - ) -> Dict[str, Optional[Dict[str, Any]]]: + ) -> dict[str, Optional[dict[str, Any]]]: if instances is None: instances = self._instances_by_id.values() instances = list(instances) @@ -1674,7 +1711,7 @@ class CreateContext: def get_instances_task_entities( self, instances: Optional[Iterable["CreatedInstance"]] = None - ): + ) -> dict[str, Optional[dict[str, Any]]]: """Get task entities for instances. Args: @@ -1682,7 +1719,7 @@ class CreateContext: get task entities. If not provided all instances are used. Returns: - Dict[str, Optional[Dict[str, Any]]]: Task entity by instance id. + dict[str, Optional[dict[str, Any]]]: Task entity by instance id. """ if instances is None: @@ -1720,7 +1757,7 @@ class CreateContext: def get_instances_context_info( self, instances: Optional[Iterable["CreatedInstance"]] = None - ) -> Dict[str, InstanceContextInfo]: + ) -> dict[str, InstanceContextInfo]: """Validate 'folder' and 'task' instance context. Args: @@ -1728,7 +1765,7 @@ class CreateContext: validate. If not provided all instances are validated. Returns: - Dict[str, InstanceContextInfo]: Validation results by instance id. + dict[str, InstanceContextInfo]: Validation results by instance id. """ # Use all instances from context if 'instances' are not passed @@ -1862,7 +1899,7 @@ class CreateContext: context_info.task_is_valid = True return info_by_instance_id - def save_changes(self): + def save_changes(self) -> None: """Save changes. Update all changed values.""" if not self.host_is_valid: missing_methods = self.get_host_misssing_methods(self.host) @@ -1871,14 +1908,14 @@ class CreateContext: self._save_context_changes() self._save_instance_changes() - def _save_context_changes(self): + def _save_context_changes(self) -> None: """Save global context values.""" changes = self.context_data_changes() if changes: data = self.context_data_to_store() self.host.update_context_data(data, changes) - def _save_instance_changes(self): + def _save_instance_changes(self) -> None: """Save instance specific values.""" instances_by_identifier = collections.defaultdict(list) for instance in self._instances_by_id.values(): @@ -1938,14 +1975,18 @@ class CreateContext: if failed_info: raise CreatorsSaveFailed(failed_info) - def remove_instances(self, instances, sender=None): + def remove_instances( + self, + instances: list["CreatedInstances"], + sender: Optional[str] = None, + ) -> None: """Remove instances from context. All instances that don't have creator identifier leading to existing creator are just removed from context. Args: - instances (List[CreatedInstance]): Instances that should be + instances (list[CreatedInstance]): Instances that should be removed. Remove logic is done using creator, which may require to do other cleanup than just remove instance from context. sender (Optional[str]): Sender of the event. @@ -2013,11 +2054,11 @@ class CreateContext: raise CreatorsRemoveFailed(failed_info) @property - def collection_shared_data(self): + def collection_shared_data(self) -> dict[str, Any]: """Access to shared data that can be used during creator's collection. Returns: - Dict[str, Any]: Shared data. + dict[str, Any]: Shared data. Raises: UnavailableSharedData: When called out of collection phase. @@ -2029,7 +2070,7 @@ class CreateContext: ) return self._collection_shared_data - def run_convertor(self, convertor_identifier): + def run_convertor(self, convertor_identifier: str) -> None: """Run convertor plugin by identifier. Conversion is skipped if convertor is not available. @@ -2042,14 +2083,14 @@ class CreateContext: if convertor is not None: convertor.convert() - def run_convertors(self, convertor_identifiers): + def run_convertors(self, convertor_identifiers: Iterable[str]) -> None: """Run convertor plugins by identifiers. Conversion is skipped if convertor is not available. It is recommended to trigger reset after conversion to reload instances. Args: - convertor_identifiers (Iterator[str]): Identifiers of convertors + convertor_identifiers (Iterable[str]): Identifiers of convertors to run. Raises: @@ -2077,21 +2118,27 @@ class CreateContext: if failed_info: raise ConvertorsConversionFailed(failed_info) - def _register_event_callback(self, topic: str, callback: Callable): + def _register_event_callback( + self, topic: str, callback: Callable + ) -> "EventCallback": return self._event_hub.add_callback(topic, callback) def _emit_event( self, topic: str, - data: Optional[Dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, sender: Optional[str] = None, - ): + ) -> "Event": if data is None: data = {} data.setdefault("create_context", self) return self._event_hub.emit(topic, data, sender) - def _remove_instances(self, instances, sender=None): + def _remove_instances( + self, + instances: Iterable[CreatedInstance], + sender: Optional[str] = None, + ) -> None: with self.bulk_remove_instances(sender) as bulk_info: for instance in instances: obj = self._instances_by_id.pop(instance.id, None) @@ -2099,8 +2146,12 @@ class CreateContext: bulk_info.append(obj) def _create_with_unified_error( - self, identifier, creator, *args, **kwargs - ): + self, + identifier: str, + creator: Optional["BaseCreator"], + *args, + **kwargs + ) -> tuple[Optional[Any], Optional[dict[str, Any]]]: error_message = "Failed to run Creator with identifier \"{}\". {}" label = None @@ -2168,7 +2219,7 @@ class CreateContext: if bulk_info: self._bulk_finished(key) - def _bulk_finished(self, key: str): + def _bulk_finished(self, key: str) -> None: if self._bulk_order[0] != key: return @@ -2182,7 +2233,7 @@ class CreateContext: self._bulk_order.pop(0) self._bulk_finish(key) - def _bulk_finish(self, key: str): + def _bulk_finish(self, key: str) -> None: bulk_info = self._bulk_info[key] sender = bulk_info.get_sender() data = bulk_info.pop_data() @@ -2201,9 +2252,9 @@ class CreateContext: def _bulk_add_instances_finished( self, - instances_to_validate: List["CreatedInstance"], + instances_to_validate: list["CreatedInstance"], sender: Optional[str] - ): + ) -> None: if not instances_to_validate: return @@ -2264,9 +2315,9 @@ class CreateContext: def _bulk_remove_instances_finished( self, - instances_to_remove: List["CreatedInstance"], + instances_to_remove: list["CreatedInstance"], sender: Optional[str] - ): + ) -> None: if not instances_to_remove: return @@ -2280,9 +2331,9 @@ class CreateContext: def _bulk_values_change_finished( self, - changes: Tuple[Union[str, None], Dict[str, Any]], + changes: list[tuple[Optional[str], dict[str, Any]]], sender: Optional[str], - ): + ) -> None: if not changes: return item_data_by_id = {} @@ -2335,8 +2386,8 @@ class CreateContext: ) def _bulk_pre_create_attrs_change_finished( - self, identifiers: List[str], sender: Optional[str] - ): + self, identifiers: list[str], sender: Optional[str] + ) -> None: if not identifiers: return identifiers = list(set(identifiers)) @@ -2349,8 +2400,8 @@ class CreateContext: ) def _bulk_create_attrs_change_finished( - self, instance_ids: List[str], sender: Optional[str] - ): + self, instance_ids: list[str], sender: Optional[str] + ) -> None: if not instance_ids: return @@ -2368,9 +2419,9 @@ class CreateContext: def _bulk_publish_attrs_change_finished( self, - attr_info: Tuple[str, Union[str, None]], + attr_info: list[tuple[str, Optional[str]]], sender: Optional[str], - ): + ) -> None: if not attr_info: return From cef876b339551782f5d91752364c9821805b8980 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 30 Jun 2025 10:54:43 +0200 Subject: [PATCH 154/155] formatting fixes --- client/ayon_core/pipeline/create/context.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 1a838a3bd8..d997f464fb 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -51,7 +51,7 @@ from .creator_plugins import ( if typing.TYPE_CHECKING: from ayon_core.host import HostBase, IPublishHost from ayon_core.lib import AbstractAttrDef - from ayon_core.lib.events import EventCallback + from ayon_core.lib.events import EventCallback, Event from .structures import CreatedInstance from .creator_plugins import BaseCreator @@ -162,8 +162,8 @@ class CreateContext: context which should be handled by host. Args: - host (PublishHost): Host implementation which handles implementation and - global metadata. + host (PublishHost): Host implementation which handles implementation + and global metadata. headless (bool): Context is created out of UI (Current not used). reset (bool): Reset context on initialization. discover_publish_plugins (bool): Discover publish plugins during reset @@ -1977,7 +1977,7 @@ class CreateContext: def remove_instances( self, - instances: list["CreatedInstances"], + instances: list["CreatedInstance"], sender: Optional[str] = None, ) -> None: """Remove instances from context. From f26f69eae6a0afce08e0ae73e8a4b402d962ae80 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 1 Jul 2025 16:08:48 +0200 Subject: [PATCH 155/155] Fix `IPublishHost` import --- client/ayon_core/pipeline/create/context.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index d997f464fb..05531afd05 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -24,7 +24,7 @@ from ayon_core.settings import get_project_settings from ayon_core.lib import is_func_signature_supported from ayon_core.lib.events import QueuedEventSystem from ayon_core.lib.attribute_definitions import get_default_values -from ayon_core.host import IWorkfileHost +from ayon_core.host import IWorkfileHost, IPublishHost from ayon_core.pipeline import Anatomy from ayon_core.pipeline.template_data import get_template_data from ayon_core.pipeline.plugin_discover import DiscoverResult @@ -49,7 +49,7 @@ from .creator_plugins import ( discover_convertor_plugins, ) if typing.TYPE_CHECKING: - from ayon_core.host import HostBase, IPublishHost + from ayon_core.host import HostBase from ayon_core.lib import AbstractAttrDef from ayon_core.lib.events import EventCallback, Event