From 10c66f496e911b105c7ce34818d10bd471059a14 Mon Sep 17 00:00:00 2001 From: Philippe Leprince Date: Fri, 9 May 2025 14:39:26 +0200 Subject: [PATCH 01/57] first version Signed-off-by: Philippe Leprince --- .github/workflows/deploy_mkdocs.yml | 78 +++++++++++++++++++++++++++++ docs/mkdocs_requirements.txt | 9 ++++ 2 files changed, 87 insertions(+) create mode 100644 .github/workflows/deploy_mkdocs.yml create mode 100644 docs/mkdocs_requirements.txt diff --git a/.github/workflows/deploy_mkdocs.yml b/.github/workflows/deploy_mkdocs.yml new file mode 100644 index 0000000000..9fd97c1e82 --- /dev/null +++ b/.github/workflows/deploy_mkdocs.yml @@ -0,0 +1,78 @@ +name: Deploy MkDocs + +on: + workflow_dispatch: + workflow_call: + inputs: + repo: + type: string + required: true + branch_name: + type: string + required: true + default: "main" + tag: + type: string + required: true + secrets: + token: + required: true + user: + required: true + email: + required: true + +env: + GH_TOKEN: ${{ secrets.token || secrets.YNPUT_BOT_TOKEN }} + GH_USER: ${{ secrets.user || secrets.CI_USER }} + GH_EMAIL: ${{ secrets.email || secrets.CI_EMAIL }} + +jobs: + + verify-latest-release: + uses: ynput/ops-repo-automation/.github/workflows/verify_latest_release.yml@main + with: + repo: ${{ github.repository }} + expect_release: true + secrets: + gh_token: ${{ secrets.token }} + + verify-repo-secrets: + uses: ynput/ops-repo-automation/.github/workflows/verify_secrets.yml@main + with: + repo: ${{ github.repository }} + secrets: + gh_token: ${{ secrets.token }} + gh_user: ${{ secrets.user }} + gh_email: ${{ secrets.email }} + + verify-repo-vars: + uses: ynput/ops-repo-automation/.github/workflows/verify_variables.yml@main + with: + variables: "MAIN_BRANCH,MINOR_BUMP_LABEL,PATCH_BUMP_LABEL,PROJECT_NAME" + repo: ${{ github.repository }} + secrets: + gh_token: ${{ secrets.token }} + + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout ${{ inputs.branch_name }} + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch_name }} + fetch-depth: 0 + submodules: true + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.x" + + - name: Install dependencies + run: | + python3 -m pip install -r ./docs/mkdocs_requirements.txt + + - name: Mike deploy + run: mike deploy --update-aliases ${{ inputs.tag }} latest diff --git a/docs/mkdocs_requirements.txt b/docs/mkdocs_requirements.txt new file mode 100644 index 0000000000..829d02951a --- /dev/null +++ b/docs/mkdocs_requirements.txt @@ -0,0 +1,9 @@ +mkdocs-material >= 9.6.7 +mkdocs-autoapi >= 0.4.0 +mkdocstrings-python >= 1.16.2 +mkdocs-minify-plugin >= 0.8.0 +markdown-checklist >= 0.4.4 +mdx-gh-links >= 0.4 +pymdown-extensions >= 10.14.3 +mike >= 2.1.3 +mkdocstrings-shell >= 1.0.2 From bd1dd87d242ee3d637133bea0e84ee64c2b76172 Mon Sep 17 00:00:00 2001 From: Philippe Leprince Date: Fri, 9 May 2025 16:41:16 +0200 Subject: [PATCH 02/57] can I test this ? Signed-off-by: Philippe Leprince --- .github/workflows/deploy_mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy_mkdocs.yml b/.github/workflows/deploy_mkdocs.yml index 9fd97c1e82..fcee1dd3c2 100644 --- a/.github/workflows/deploy_mkdocs.yml +++ b/.github/workflows/deploy_mkdocs.yml @@ -1,6 +1,7 @@ name: Deploy MkDocs on: + pull_request: workflow_dispatch: workflow_call: inputs: From 4977e771224ed202d76d11c6bfd6187d37812240 Mon Sep 17 00:00:00 2001 From: Philippe Leprince Date: Fri, 9 May 2025 16:44:05 +0200 Subject: [PATCH 03/57] remove some tests Signed-off-by: Philippe Leprince --- .github/workflows/deploy_mkdocs.yml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/deploy_mkdocs.yml b/.github/workflows/deploy_mkdocs.yml index fcee1dd3c2..80e69b8fa1 100644 --- a/.github/workflows/deploy_mkdocs.yml +++ b/.github/workflows/deploy_mkdocs.yml @@ -30,13 +30,13 @@ env: jobs: - verify-latest-release: - uses: ynput/ops-repo-automation/.github/workflows/verify_latest_release.yml@main - with: - repo: ${{ github.repository }} - expect_release: true - secrets: - gh_token: ${{ secrets.token }} +# verify-latest-release: +# uses: ynput/ops-repo-automation/.github/workflows/verify_latest_release.yml@main +# with: +# repo: ${{ github.repository }} +# expect_release: true +# secrets: +# gh_token: ${{ secrets.token }} verify-repo-secrets: uses: ynput/ops-repo-automation/.github/workflows/verify_secrets.yml@main @@ -47,13 +47,13 @@ jobs: gh_user: ${{ secrets.user }} gh_email: ${{ secrets.email }} - verify-repo-vars: - uses: ynput/ops-repo-automation/.github/workflows/verify_variables.yml@main - with: - variables: "MAIN_BRANCH,MINOR_BUMP_LABEL,PATCH_BUMP_LABEL,PROJECT_NAME" - repo: ${{ github.repository }} - secrets: - gh_token: ${{ secrets.token }} +# verify-repo-vars: +# uses: ynput/ops-repo-automation/.github/workflows/verify_variables.yml@main +# with: +# variables: "MAIN_BRANCH,MINOR_BUMP_LABEL,PATCH_BUMP_LABEL,PROJECT_NAME" +# repo: ${{ github.repository }} +# secrets: +# gh_token: ${{ secrets.token }} deploy: runs-on: ubuntu-latest From dc9d65d9b85d6a4afe5eda2e3638facf2d7d2c6a Mon Sep 17 00:00:00 2001 From: Philippe Leprince Date: Fri, 9 May 2025 17:01:09 +0200 Subject: [PATCH 04/57] fix authentification error Signed-off-by: Philippe Leprince --- .github/workflows/deploy_mkdocs.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/deploy_mkdocs.yml b/.github/workflows/deploy_mkdocs.yml index 80e69b8fa1..98866eb43e 100644 --- a/.github/workflows/deploy_mkdocs.yml +++ b/.github/workflows/deploy_mkdocs.yml @@ -66,6 +66,11 @@ jobs: fetch-depth: 0 submodules: true + - name: πŸ”‘ Set Authentication + run: | + git config --global user.name "${{ secrets.user }}" + git config --global user.email "${{ secrets.email }}" + - name: Set up Python uses: actions/setup-python@v4 with: From 5899f484a5d242904b5ee21808b2c53249deadc7 Mon Sep 17 00:00:00 2001 From: Philippe Leprince Date: Fri, 9 May 2025 17:10:26 +0200 Subject: [PATCH 05/57] better authentification fix Signed-off-by: Philippe Leprince --- .github/workflows/deploy_mkdocs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy_mkdocs.yml b/.github/workflows/deploy_mkdocs.yml index 98866eb43e..8c05985613 100644 --- a/.github/workflows/deploy_mkdocs.yml +++ b/.github/workflows/deploy_mkdocs.yml @@ -68,8 +68,8 @@ jobs: - name: πŸ”‘ Set Authentication run: | - git config --global user.name "${{ secrets.user }}" - git config --global user.email "${{ secrets.email }}" + git config --global user.name "${{ secrets.user || secrets.CI_USER }}" + git config --global user.email "${{ secrets.email || secrets.CI_EMAIL }}" - name: Set up Python uses: actions/setup-python@v4 From b5006abc8b51bf18f7ba4fa083925df364877692 Mon Sep 17 00:00:00 2001 From: Philippe Leprince Date: Fri, 9 May 2025 17:15:09 +0200 Subject: [PATCH 06/57] display tag Signed-off-by: Philippe Leprince --- .github/workflows/deploy_mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy_mkdocs.yml b/.github/workflows/deploy_mkdocs.yml index 8c05985613..49f6cfafba 100644 --- a/.github/workflows/deploy_mkdocs.yml +++ b/.github/workflows/deploy_mkdocs.yml @@ -80,5 +80,5 @@ jobs: run: | python3 -m pip install -r ./docs/mkdocs_requirements.txt - - name: Mike deploy + - name: Mike deploy ${{ inputs.tag }} run: mike deploy --update-aliases ${{ inputs.tag }} latest From 71a5a4603020905dbd2a33751b4f24030bbabadb Mon Sep 17 00:00:00 2001 From: Philippe Leprince Date: Fri, 9 May 2025 17:38:41 +0200 Subject: [PATCH 07/57] Get the current tag with fallback to 1.0.0 Signed-off-by: Philippe Leprince --- .github/workflows/deploy_mkdocs.yml | 30 ++++++++--------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/.github/workflows/deploy_mkdocs.yml b/.github/workflows/deploy_mkdocs.yml index 49f6cfafba..2e6329f924 100644 --- a/.github/workflows/deploy_mkdocs.yml +++ b/.github/workflows/deploy_mkdocs.yml @@ -12,9 +12,6 @@ on: type: string required: true default: "main" - tag: - type: string - required: true secrets: token: required: true @@ -29,15 +26,6 @@ env: GH_EMAIL: ${{ secrets.email || secrets.CI_EMAIL }} jobs: - -# verify-latest-release: -# uses: ynput/ops-repo-automation/.github/workflows/verify_latest_release.yml@main -# with: -# repo: ${{ github.repository }} -# expect_release: true -# secrets: -# gh_token: ${{ secrets.token }} - verify-repo-secrets: uses: ynput/ops-repo-automation/.github/workflows/verify_secrets.yml@main with: @@ -47,14 +35,6 @@ jobs: gh_user: ${{ secrets.user }} gh_email: ${{ secrets.email }} -# verify-repo-vars: -# uses: ynput/ops-repo-automation/.github/workflows/verify_variables.yml@main -# with: -# variables: "MAIN_BRANCH,MINOR_BUMP_LABEL,PATCH_BUMP_LABEL,PROJECT_NAME" -# repo: ${{ github.repository }} -# secrets: -# gh_token: ${{ secrets.token }} - deploy: runs-on: ubuntu-latest @@ -71,6 +51,12 @@ jobs: git config --global user.name "${{ secrets.user || secrets.CI_USER }}" git config --global user.email "${{ secrets.email || secrets.CI_EMAIL }}" + - name: Get current tag + id: git_tag + uses: "devops-actions/action-get-tag@v1.0.3" + with: + default: 1.0.0 + - name: Set up Python uses: actions/setup-python@v4 with: @@ -80,5 +66,5 @@ jobs: run: | python3 -m pip install -r ./docs/mkdocs_requirements.txt - - name: Mike deploy ${{ inputs.tag }} - run: mike deploy --update-aliases ${{ inputs.tag }} latest + - name: Mike deploy ${{ steps.git_tag.outputs.tag }} + run: mike deploy --update-aliases ${{ steps.git_tag.outputs.tag }} latest From 17fbd5228cad7a867137751f2a0ceb125f10f0a0 Mon Sep 17 00:00:00 2001 From: Philippe Leprince Date: Fri, 9 May 2025 17:43:42 +0200 Subject: [PATCH 08/57] remove quotes Signed-off-by: Philippe Leprince --- .github/workflows/deploy_mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy_mkdocs.yml b/.github/workflows/deploy_mkdocs.yml index 2e6329f924..a8281a1de0 100644 --- a/.github/workflows/deploy_mkdocs.yml +++ b/.github/workflows/deploy_mkdocs.yml @@ -53,7 +53,7 @@ jobs: - name: Get current tag id: git_tag - uses: "devops-actions/action-get-tag@v1.0.3" + uses: devops-actions/action-get-tag@v1.0.3 with: default: 1.0.0 From 18c0299bfb9a1812a485d8c454f68f5d165f5271 Mon Sep 17 00:00:00 2001 From: Philippe Leprince Date: Fri, 9 May 2025 17:47:57 +0200 Subject: [PATCH 09/57] fix missing branch name Signed-off-by: Philippe Leprince --- .github/workflows/deploy_mkdocs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy_mkdocs.yml b/.github/workflows/deploy_mkdocs.yml index a8281a1de0..4d6fd9104e 100644 --- a/.github/workflows/deploy_mkdocs.yml +++ b/.github/workflows/deploy_mkdocs.yml @@ -39,10 +39,10 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout ${{ inputs.branch_name }} + - name: Checkout ${{ inputs.branch_name || 'main' }} uses: actions/checkout@v4 with: - ref: ${{ inputs.branch_name }} + ref: ${{ inputs.branch_name || 'main' }} fetch-depth: 0 submodules: true From af9fd17ee8108f4a13b9777fa9daf68e3a09fa74 Mon Sep 17 00:00:00 2001 From: Philippe Leprince Date: Fri, 9 May 2025 17:55:49 +0200 Subject: [PATCH 10/57] format Signed-off-by: Philippe Leprince --- .github/workflows/deploy_mkdocs.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/deploy_mkdocs.yml b/.github/workflows/deploy_mkdocs.yml index 4d6fd9104e..2f46bdc6ad 100644 --- a/.github/workflows/deploy_mkdocs.yml +++ b/.github/workflows/deploy_mkdocs.yml @@ -21,28 +21,28 @@ on: required: true env: - GH_TOKEN: ${{ secrets.token || secrets.YNPUT_BOT_TOKEN }} - GH_USER: ${{ secrets.user || secrets.CI_USER }} - GH_EMAIL: ${{ secrets.email || secrets.CI_EMAIL }} + GH_TOKEN: ${{ secrets.token || secrets.YNPUT_BOT_TOKEN }} + GH_USER: ${{ secrets.user || secrets.CI_USER }} + GH_EMAIL: ${{ secrets.email || secrets.CI_EMAIL }} jobs: verify-repo-secrets: - uses: ynput/ops-repo-automation/.github/workflows/verify_secrets.yml@main - with: - repo: ${{ github.repository }} - secrets: - gh_token: ${{ secrets.token }} - gh_user: ${{ secrets.user }} - gh_email: ${{ secrets.email }} + uses: ynput/ops-repo-automation/.github/workflows/verify_secrets.yml@main + with: + repo: ${{ github.repository }} + secrets: + gh_token: ${{ secrets.token }} + gh_user: ${{ secrets.user }} + gh_email: ${{ secrets.email }} deploy: runs-on: ubuntu-latest steps: - - name: Checkout ${{ inputs.branch_name || 'main' }} + - name: Checkout ${{ inputs.branch_name}} uses: actions/checkout@v4 with: - ref: ${{ inputs.branch_name || 'main' }} + ref: ${{ inputs.branch_name}} fetch-depth: 0 submodules: true From dba8d78a2abc79583d64016c46b6aa5df13916bb Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 20 May 2025 10:53:37 +0200 Subject: [PATCH 11/57] Refactors color space conversion with oiiotool Consolidates color space conversion logic into a dedicated `oiiotool_transcode` function for better flexibility and clarity. This change introduces support for display/view transformations, enhancing the tool's ability to handle complex color management workflows. It also fixes issues with conflicting color space parameters and improves handling of source and target display/view configurations. --- client/ayon_core/lib/transcoding.py | 170 ++++++++++++++++-- .../publish/extract_color_transcode.py | 42 +++-- .../plugins/publish/extract_thumbnail.py | 10 +- 3 files changed, 184 insertions(+), 38 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 8c84e1c4dc..6648f5219e 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -977,7 +977,60 @@ def convert_colorspace( additional_command_args=None, logger=None, ): - """Convert source file from one color space to another. + """Backward compatibility function for convert_colorspace. + + Args: + input_path (str): Path to input file that should be converted. + output_path (str): Path to output file where result will be stored. + config_path (str): Path to OCIO config file. + source_colorspace (str): OCIO valid color space of source files. + target_colorspace (str, optional): OCIO valid target color space. + If filled, 'view' and 'display' must be empty. + view (str, optional): Name for target viewer space (OCIO valid). + Both 'view' and 'display' must be filled (if not 'target_colorspace'). + display (str, optional): Name for target display-referred reference space. + Both 'view' and 'display' must be filled (if not 'target_colorspace'). + additional_command_args (list, optional): Additional arguments for oiiotool + (like binary depth for .dpx). + logger (logging.Logger, optional): Logger used for logging. + + Returns: + None: Function returns None. + + Raises: + ValueError: If parameters are misconfigured. + """ + return oiiotool_transcode( + input_path, + output_path, + config_path, + source_colorspace, + target_colorspace=target_colorspace, + target_display=display, + target_view=view, + additional_command_args=additional_command_args, + logger=logger, + ) + + +def oiiotool_transcode( + input_path, + output_path, + config_path, + source_colorspace, + source_display=None, + source_view=None, + target_colorspace=None, + target_display=None, + target_view=None, + additional_command_args=None, + logger=None, +): + """Transcode source file to other with colormanagement. + + Oiiotool also support additional arguments for transcoding. + For more information, see the official documentation: + https://openimageio.readthedocs.io/en/latest/oiiotool.html Args: input_path (str): Path that should be converted. It is expected that @@ -989,17 +1042,26 @@ def convert_colorspace( sequence in 'file.FRAMESTART-FRAMEEND#.ext', `output.1-3#.tif`) config_path (str): path to OCIO config file source_colorspace (str): ocio valid color space of source files + source_display (str, optional): name for source display-referred + reference space (ocio valid). If provided, source_view must also be + provided, and source_colorspace will be ignored + source_view (str, optional): name for source viewer space (ocio valid) + If provided, source_display must also be provided, and + source_colorspace will be ignored target_colorspace (str): ocio valid target color space if filled, 'view' and 'display' must be empty - view (str): name for viewer space (ocio valid) - both 'view' and 'display' must be filled (if 'target_colorspace') - display (str): name for display-referred reference space (ocio valid) + target_display (str): name for target display-referred reference space + (ocio valid) both 'view' and 'display' must be filled (if + 'target_colorspace') + target_view (str): name for target viewer space (ocio valid) both 'view' and 'display' must be filled (if 'target_colorspace') additional_command_args (list): arguments for oiiotool (like binary depth for .dpx) logger (logging.Logger): Logger used for logging. + Raises: ValueError: if misconfigured + """ if logger is None: logger = logging.getLogger(__name__) @@ -1024,23 +1086,99 @@ def convert_colorspace( "--ch", channels_arg ]) - if all([target_colorspace, view, display]): - raise ValueError("Colorspace and both screen and display" - " cannot be set together." - "Choose colorspace or screen and display") - if not target_colorspace and not all([view, display]): - raise ValueError("Both screen and display must be set.") + # Validate input parameters + if all([target_colorspace, target_view, target_display]): + raise ValueError( + "Colorspace and both screen and display cannot be set together." + "Choose colorspace or screen and display" + ) + + if all([source_view, source_display]) and source_colorspace: + logger.warning( + "Both source display/view and source_colorspace provided. " + "Using source display/view pair and ignoring source_colorspace." + ) + + if not target_colorspace and not all([target_view, target_display]): + raise ValueError( + "Both screen and display must be set if target_colorspace is not " + "provided." + ) + + if ((source_view and not source_display) or + (source_display and not source_view)): + raise ValueError( + "Both source_view and source_display must be provided if using " + "display/view inputs." + ) if additional_command_args: oiio_cmd.extend(additional_command_args) + # Handle the different conversion cases if target_colorspace: - oiio_cmd.extend(["--colorconvert:subimages=0", - source_colorspace, - target_colorspace]) - if view and display: - oiio_cmd.extend(["--iscolorspace", source_colorspace]) - oiio_cmd.extend(["--ociodisplay:subimages=0", display, view]) + # Case 1: Converting to a named colorspace + if all([source_view, source_display]): + # First convert from source display/view to a role/reference space + # that can be used with colorconvert + # For example, converting to "scene_linear" or an appropriate + # intermediate space + # This is a two-step conversion process since there's no direct + # display/view to colorspace command + # This could be a config parameter or determined from OCIO config + tmp_role_space = "scene_linear" + oiio_cmd.extend([ + "--ociodisplay:inverse=1:subimages=0", source_display, + source_view, "--colorconvert:subimages=0", tmp_role_space, + target_colorspace, + ]) + else: + # Standard color space to color space conversion + oiio_cmd.extend([ + "--colorconvert:subimages=0", source_colorspace, + target_colorspace, + ]) + else: # Using display/view target + if all([source_view, source_display]): + if source_display == target_display and source_view == target_view: + # No conversion needed if source and target display/view are + # the same + logger.debug( + "Source and target display/view pairs are identical. " + "No color conversion needed." + ) + elif source_display == target_display: + # When only the view changes but display stays the same + # First convert from source view to a reference space, then to + # target view + # This could be configured + tmp_role_space = "scene_linear" + oiio_cmd.extend([ + "--ociodisplay:inverse=1:subimages=0", + source_display, + source_view, + "--ociodisplay:subimages=0", + target_display, + target_view, + ]) + else: + # Complete display/view pair conversion + # Similar approach: go through a reference space + # This could be configured + tmp_role_space = "scene_linear" + oiio_cmd.extend([ + "--ociodisplay:inverse=1:subimages=0", + source_display, + source_view, "--ociodisplay:subimages=0", + target_display, + target_view, + ]) + else: + # Standard conversion from colorspace to display/view + oiio_cmd.extend([ + "--iscolorspace", source_colorspace, + "--ociodisplay:subimages=0", target_display, target_view, + ]) oiio_cmd.extend(["-o", output_path]) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 6cf30857a4..f61379189c 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -11,7 +11,7 @@ from ayon_core.lib import ( is_oiio_supported, ) from ayon_core.lib.transcoding import ( - convert_colorspace, + oiiotool_transcode, ) from ayon_core.lib.profiles_filtering import filter_profiles @@ -94,6 +94,8 @@ class ExtractOIIOTranscode(publish.Extractor): colorspace_data = repre["colorspaceData"] source_colorspace = colorspace_data["colorspace"] + source_display = colorspace_data.get("display") + source_view = colorspace_data.get("view") config_path = colorspace_data.get("config", {}).get("path") if not config_path or not os.path.exists(config_path): self.log.warning("Config file doesn't exist, skipping") @@ -124,7 +126,7 @@ class ExtractOIIOTranscode(publish.Extractor): transcoding_type = output_def["transcoding_type"] - target_colorspace = view = display = None + target_colorspace = target_view = target_display = None # NOTE: we use colorspace_data as the fallback values for # the target colorspace. if transcoding_type == "colorspace": @@ -136,18 +138,20 @@ class ExtractOIIOTranscode(publish.Extractor): colorspace_data.get("colorspace")) elif transcoding_type == "display_view": display_view = output_def["display_view"] - view = display_view["view"] or colorspace_data.get("view") - display = ( + target_view = ( + display_view["view"] + or colorspace_data.get("view")) + target_display = ( display_view["display"] or colorspace_data.get("display") ) # both could be already collected by DCC, # but could be overwritten when transcoding - if view: - new_repre["colorspaceData"]["view"] = view - if display: - new_repre["colorspaceData"]["display"] = display + if target_view: + new_repre["colorspaceData"]["view"] = target_view + if target_display: + new_repre["colorspaceData"]["display"] = target_display if target_colorspace: new_repre["colorspaceData"]["colorspace"] = \ target_colorspace @@ -166,16 +170,18 @@ class ExtractOIIOTranscode(publish.Extractor): new_staging_dir, output_extension) - convert_colorspace( - input_path, - output_path, - config_path, - source_colorspace, - target_colorspace, - view, - display, - additional_command_args, - self.log + oiiotool_transcode( + input_path=input_path, + output_path=output_path, + config_path=config_path, + source_colorspace=source_colorspace, + target_colorspace=target_colorspace, + target_display=target_display, + target_view=target_view, + source_display=source_display, + source_view=source_view, + additional_command_args=additional_command_args, + logger=self.log ) # cleanup temporary transcoded files diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index 3a428c46a7..f65308121b 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -15,7 +15,7 @@ from ayon_core.lib import ( path_to_subprocess_arg, run_subprocess, ) -from ayon_core.lib.transcoding import convert_colorspace +from ayon_core.lib.transcoding import oiiotool_transcode from ayon_core.lib.transcoding import VIDEO_EXTENSIONS, IMAGE_EXTENSIONS @@ -431,13 +431,15 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): oiio_default_view = display_and_view["view"] try: - convert_colorspace( + oiiotool_transcode( src_path, dst_path, colorspace_data["config"]["path"], colorspace_data["colorspace"], - display=repre_display or oiio_default_display, - view=repre_view or oiio_default_view, + source_display=colorspace_data.get("display"), + source_view=colorspace_data.get("view"), + target_display=repre_display or oiio_default_display, + target_view=repre_view or oiio_default_view, target_colorspace=oiio_default_colorspace, additional_command_args=resolution_arg, logger=self.log, From 08f6b61a3e5f4d91d8a4d7247024a14fe9067d27 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 20 May 2025 11:19:14 +0200 Subject: [PATCH 12/57] Adds deprecation decorator and marks function Introduces a `deprecated` decorator to mark functions as deprecated, issuing a warning when they are called. The `convert_colorspace` function is marked as deprecated, advising users to switch to `oiiotool_transcode`. --- client/ayon_core/lib/transcoding.py | 90 +++++++++++++++++++++-------- 1 file changed, 66 insertions(+), 24 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 6648f5219e..e62873184d 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -6,6 +6,8 @@ import collections import tempfile import subprocess import platform +import warnings +import functools from typing import Optional import xml.etree.ElementTree @@ -67,6 +69,48 @@ VIDEO_EXTENSIONS = { } + +def deprecated(new_destination): + """Mark functions as deprecated. + + It will result in a warning being emitted when the function is used. + """ + + func = None + if callable(new_destination): + func = new_destination + new_destination = None + + def _decorator(decorated_func): + if new_destination is None: + warning_message = ( + " Please check content of deprecated function to figure out" + " possible replacement." + ) + else: + warning_message = " Please replace your usage with '{}'.".format( + new_destination + ) + + @functools.wraps(decorated_func) + def wrapper(*args, **kwargs): + warnings.simplefilter("always", DeprecationWarning) + warnings.warn( + ( + "Call to deprecated function '{}'" + "\nFunction was moved or removed.{}" + ).format(decorated_func.__name__, warning_message), + category=DeprecationWarning, + stacklevel=4 + ) + return decorated_func(*args, **kwargs) + return wrapper + + if func is None: + return _decorator + return _decorator(func) + + def get_transcode_temp_directory(): """Creates temporary folder for transcoding. @@ -966,6 +1010,8 @@ def convert_ffprobe_fps_to_float(value): return dividend / divisor +# --- Deprecated functions --- +@deprecated("oiiotool_transcode") def convert_colorspace( input_path, output_path, @@ -977,7 +1023,7 @@ def convert_colorspace( additional_command_args=None, logger=None, ): - """Backward compatibility function for convert_colorspace. + """DEPRECATED function use `oiiotool_transcode` instead Args: input_path (str): Path to input file that should be converted. @@ -1105,8 +1151,10 @@ def oiiotool_transcode( "provided." ) - if ((source_view and not source_display) or - (source_display and not source_view)): + if ( + (source_view and not source_display) + or (source_display and not source_view) + ): raise ValueError( "Both source_view and source_display must be provided if using " "display/view inputs." @@ -1128,14 +1176,18 @@ def oiiotool_transcode( # This could be a config parameter or determined from OCIO config tmp_role_space = "scene_linear" oiio_cmd.extend([ - "--ociodisplay:inverse=1:subimages=0", source_display, - source_view, "--colorconvert:subimages=0", tmp_role_space, + "--ociodisplay:inverse=1:subimages=0", + source_display, + source_view, + "--colorconvert:subimages=0", + tmp_role_space, target_colorspace, ]) else: # Standard color space to color space conversion oiio_cmd.extend([ - "--colorconvert:subimages=0", source_colorspace, + "--colorconvert:subimages=0", + source_colorspace, target_colorspace, ]) else: # Using display/view target @@ -1147,10 +1199,9 @@ def oiiotool_transcode( "Source and target display/view pairs are identical. " "No color conversion needed." ) - elif source_display == target_display: - # When only the view changes but display stays the same - # First convert from source view to a reference space, then to - # target view + else: + # Complete display/view pair conversion + # Similar approach: go through a reference space # This could be configured tmp_role_space = "scene_linear" oiio_cmd.extend([ @@ -1161,23 +1212,14 @@ def oiiotool_transcode( target_display, target_view, ]) - else: - # Complete display/view pair conversion - # Similar approach: go through a reference space - # This could be configured - tmp_role_space = "scene_linear" - oiio_cmd.extend([ - "--ociodisplay:inverse=1:subimages=0", - source_display, - source_view, "--ociodisplay:subimages=0", - target_display, - target_view, - ]) else: # Standard conversion from colorspace to display/view oiio_cmd.extend([ - "--iscolorspace", source_colorspace, - "--ociodisplay:subimages=0", target_display, target_view, + "--iscolorspace", + source_colorspace, + "--ociodisplay:subimages=0", + target_display, + target_view, ]) oiio_cmd.extend(["-o", output_path]) From 4ebf35dd8b68ef10a66551ef47276516a291e360 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 20 May 2025 11:26:28 +0200 Subject: [PATCH 13/57] Refactors docstring formatting in transcoding Improves readability by adjusting docstring formatting in the `convert_colorspace` function. This change ensures consistent documentation style and enhances clarity. --- client/ayon_core/lib/transcoding.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index e62873184d..4f985d5128 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -69,7 +69,6 @@ VIDEO_EXTENSIONS = { } - def deprecated(new_destination): """Mark functions as deprecated. @@ -1033,11 +1032,13 @@ def convert_colorspace( target_colorspace (str, optional): OCIO valid target color space. If filled, 'view' and 'display' must be empty. view (str, optional): Name for target viewer space (OCIO valid). - Both 'view' and 'display' must be filled (if not 'target_colorspace'). - display (str, optional): Name for target display-referred reference space. - Both 'view' and 'display' must be filled (if not 'target_colorspace'). - additional_command_args (list, optional): Additional arguments for oiiotool - (like binary depth for .dpx). + Both 'view' and 'display' must be filled + (if not 'target_colorspace'). + display (str, optional): Name for target display-referred + reference space. Both 'view' and 'display' must be filled + (if not 'target_colorspace'). + additional_command_args (list, optional): Additional arguments + for oiiotool (like binary depth for .dpx). logger (logging.Logger, optional): Logger used for logging. Returns: From 0b20e49eda6eb85b3b3c32039ee24a3880fc12e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 6 Jun 2025 16:07:06 +0200 Subject: [PATCH 14/57] Update client/ayon_core/pipeline/colorspace.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/colorspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/colorspace.py b/client/ayon_core/pipeline/colorspace.py index a7d1d80b0a..41241e17ca 100644 --- a/client/ayon_core/pipeline/colorspace.py +++ b/client/ayon_core/pipeline/colorspace.py @@ -1404,7 +1404,7 @@ def _get_display_view_colorspace_name(config_path, display, view): """ config = _get_ocio_config(config_path) colorspace = config.getDisplayViewColorSpaceName(display, view) - # Special token. See https://opencolorio.readthedocs.io/en/latest/guides/authoring/authoring.html#shared-views # noqa + # Special token. See https://opencolorio.readthedocs.io/en/latest/guides/authoring/authoring.html#shared-views # noqa if colorspace == "": colorspace = display From 21737339d37432cc62f83d88839711f6dc5e32fd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 12 Jun 2025 15:46:23 +0200 Subject: [PATCH 15/57] rename 'oiiotool_transcode' to 'oiio_color_convert' --- client/ayon_core/lib/transcoding.py | 8 ++++---- .../ayon_core/plugins/publish/extract_color_transcode.py | 4 ++-- client/ayon_core/plugins/publish/extract_thumbnail.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 4f985d5128..97e9f417f0 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -1010,7 +1010,7 @@ def convert_ffprobe_fps_to_float(value): # --- Deprecated functions --- -@deprecated("oiiotool_transcode") +@deprecated("oiio_color_convert") def convert_colorspace( input_path, output_path, @@ -1022,7 +1022,7 @@ def convert_colorspace( additional_command_args=None, logger=None, ): - """DEPRECATED function use `oiiotool_transcode` instead + """DEPRECATED function use `oiio_color_convert` instead Args: input_path (str): Path to input file that should be converted. @@ -1047,7 +1047,7 @@ def convert_colorspace( Raises: ValueError: If parameters are misconfigured. """ - return oiiotool_transcode( + return oiio_color_convert( input_path, output_path, config_path, @@ -1060,7 +1060,7 @@ def convert_colorspace( ) -def oiiotool_transcode( +def oiio_color_convert( input_path, output_path, config_path, diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 9759b340c7..152e0a27ea 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -11,7 +11,7 @@ from ayon_core.lib import ( is_oiio_supported, ) from ayon_core.lib.transcoding import ( - oiiotool_transcode, + oiio_color_convert, ) from ayon_core.lib.profiles_filtering import filter_profiles @@ -170,7 +170,7 @@ class ExtractOIIOTranscode(publish.Extractor): new_staging_dir, output_extension) - oiiotool_transcode( + oiio_color_convert( input_path=input_path, output_path=output_path, config_path=config_path, diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index 4d51405f01..541082352b 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -15,7 +15,7 @@ from ayon_core.lib import ( path_to_subprocess_arg, run_subprocess, ) -from ayon_core.lib.transcoding import oiiotool_transcode +from ayon_core.lib.transcoding import oiio_color_convert from ayon_core.lib.transcoding import VIDEO_EXTENSIONS, IMAGE_EXTENSIONS @@ -431,7 +431,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): oiio_default_view = display_and_view["view"] try: - oiiotool_transcode( + oiio_color_convert( src_path, dst_path, colorspace_data["config"]["path"], From 28978f07c654446c3af3b64e54cc93bff879bd56 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 12 Jun 2025 15:50:21 +0200 Subject: [PATCH 16/57] use standard conditions --- client/ayon_core/lib/transcoding.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 97e9f417f0..01a21615b5 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -1134,19 +1134,13 @@ def oiio_color_convert( ]) # Validate input parameters - if all([target_colorspace, target_view, target_display]): + if target_colorspace and target_view and target_display: raise ValueError( "Colorspace and both screen and display cannot be set together." "Choose colorspace or screen and display" ) - if all([source_view, source_display]) and source_colorspace: - logger.warning( - "Both source display/view and source_colorspace provided. " - "Using source display/view pair and ignoring source_colorspace." - ) - - if not target_colorspace and not all([target_view, target_display]): + if not target_colorspace and not target_view and not target_display: raise ValueError( "Both screen and display must be set if target_colorspace is not " "provided." @@ -1161,6 +1155,12 @@ def oiio_color_convert( "display/view inputs." ) + if source_view and source_display and source_colorspace: + logger.warning( + "Both source display/view and source_colorspace provided. " + "Using source display/view pair and ignoring source_colorspace." + ) + if additional_command_args: oiio_cmd.extend(additional_command_args) From 3eb88ae8506eac6dbbd8fd8f94abf88703c7bc72 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 12 Jun 2025 15:50:34 +0200 Subject: [PATCH 17/57] use 'view' instead of 'screen' --- client/ayon_core/lib/transcoding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 01a21615b5..7a4fd65e7c 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -1136,7 +1136,7 @@ def oiio_color_convert( # Validate input parameters if target_colorspace and target_view and target_display: raise ValueError( - "Colorspace and both screen and display cannot be set together." + "Colorspace and both view and display cannot be set together." "Choose colorspace or screen and display" ) From 2b209044ed4861668b908ee08f2115805eb0fa77 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 12 Jun 2025 15:50:44 +0200 Subject: [PATCH 18/57] better conditions order --- client/ayon_core/lib/transcoding.py | 83 ++++++++++++----------------- 1 file changed, 35 insertions(+), 48 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 7a4fd65e7c..bf88e187ae 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -1165,64 +1165,51 @@ def oiio_color_convert( oiio_cmd.extend(additional_command_args) # Handle the different conversion cases - if target_colorspace: - # Case 1: Converting to a named colorspace - if all([source_view, source_display]): - # First convert from source display/view to a role/reference space - # that can be used with colorconvert - # For example, converting to "scene_linear" or an appropriate - # intermediate space + # Source view and display are known + if source_view and source_display: + if target_colorspace: # This is a two-step conversion process since there's no direct # display/view to colorspace command # This could be a config parameter or determined from OCIO config - tmp_role_space = "scene_linear" + # Use temporarty role space 'scene_linear' + color_convert_args = ("scene_linear", target_colorspace) + elif source_display != target_display or source_view != target_view: + # Complete display/view pair conversion + # - go through a reference space + color_convert_args = (target_display, target_view) + else: + color_convert_args = None + logger.debug( + "Source and target display/view pairs are identical." + " No color conversion needed." + ) + + if color_convert_args: oiio_cmd.extend([ "--ociodisplay:inverse=1:subimages=0", source_display, source_view, "--colorconvert:subimages=0", - tmp_role_space, - target_colorspace, - ]) - else: - # Standard color space to color space conversion - oiio_cmd.extend([ - "--colorconvert:subimages=0", - source_colorspace, - target_colorspace, - ]) - else: # Using display/view target - if all([source_view, source_display]): - if source_display == target_display and source_view == target_view: - # No conversion needed if source and target display/view are - # the same - logger.debug( - "Source and target display/view pairs are identical. " - "No color conversion needed." - ) - else: - # Complete display/view pair conversion - # Similar approach: go through a reference space - # This could be configured - tmp_role_space = "scene_linear" - oiio_cmd.extend([ - "--ociodisplay:inverse=1:subimages=0", - source_display, - source_view, - "--ociodisplay:subimages=0", - target_display, - target_view, - ]) - else: - # Standard conversion from colorspace to display/view - oiio_cmd.extend([ - "--iscolorspace", - source_colorspace, - "--ociodisplay:subimages=0", - target_display, - target_view, + *color_convert_args ]) + elif target_colorspace: + # Standard color space to color space conversion + oiio_cmd.extend([ + "--colorconvert:subimages=0", + source_colorspace, + target_colorspace, + ]) + else: + # Standard conversion from colorspace to display/view + oiio_cmd.extend([ + "--iscolorspace", + source_colorspace, + "--ociodisplay:subimages=0", + target_display, + target_view, + ]) + oiio_cmd.extend(["-o", output_path]) logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) From f673abebc8393c9fdce5faf18291d2015fa0b039 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 19 Jun 2025 10:29:45 +0200 Subject: [PATCH 19/57] use 'view' instead of 'screen' Co-authored-by: Roy Nieterau --- client/ayon_core/lib/transcoding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index bf88e187ae..a4edf0aa1c 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -1142,7 +1142,7 @@ def oiio_color_convert( if not target_colorspace and not target_view and not target_display: raise ValueError( - "Both screen and display must be set if target_colorspace is not " + "Both view and display must be set if target_colorspace is not " "provided." ) From 63c81970316e66153cb2e2d05f1899c62fac89fd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 2 Jul 2025 14:47:05 +0200 Subject: [PATCH 20/57] try to fix flickering issue in publisher --- client/ayon_core/tools/utils/widgets.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/utils/widgets.py b/client/ayon_core/tools/utils/widgets.py index de2c42c91f..941aa692f9 100644 --- a/client/ayon_core/tools/utils/widgets.py +++ b/client/ayon_core/tools/utils/widgets.py @@ -441,14 +441,18 @@ class ExpandingTextEdit(QtWidgets.QTextEdit): margins = self.contentsMargins() document_width = 0 - if width >= margins.left() + margins.right(): - document_width = width - margins.left() - margins.right() + margins_size = margins.left() + margins.right() + if width >= margins_size: + document_width = width - margins_size document = self.document().clone() document.setTextWidth(document_width) return math.ceil( - margins.top() + document.size().height() + margins.bottom() + margins.top() + + document.size().height() + + margins.bottom() + + 2 ) def sizeHint(self): From 2228037656b8eeaa400fcaab1329c6416dbc050f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 3 Jul 2025 09:57:20 +0200 Subject: [PATCH 21/57] Adds target color space data extraction to OIIO transcode processor. - Introduces extraction of `targetOCIOColorspace`, `targetOCIODisplay`, and `targetOCIOView` from instance data. - Removes redundant assignment of `target_colorspace`, `target_display`, and `target_view`. --- client/ayon_core/plugins/publish/extract_color_transcode.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index ef718b8d02..7083d5094c 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -85,6 +85,11 @@ class ExtractOIIOTranscode(publish.Extractor): new_representations = [] repres = instance.data["representations"] for idx, repre in enumerate(list(repres)): + # target space, display and view might be defined upstream + target_colorspace = instance.data.get("targetOCIOColorspace") + target_display = instance.data.get("targetOCIODisplay") + target_view = instance.data.get("targetOCIOView") + self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) if not self._repre_is_valid(repre): continue @@ -126,7 +131,6 @@ class ExtractOIIOTranscode(publish.Extractor): transcoding_type = output_def["transcoding_type"] - target_colorspace = target_view = target_display = None # NOTE: we use colorspace_data as the fallback values for # the target colorspace. if transcoding_type == "colorspace": From 00cfb962e4f748aaec7e68b25d6d9c89833ee39f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Mon, 14 Jul 2025 14:38:41 +0200 Subject: [PATCH 22/57] Update client/ayon_core/lib/transcoding.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: OndΕ™ej Samohel <33513211+antirotor@users.noreply.github.com> --- client/ayon_core/lib/transcoding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index a4edf0aa1c..b3958863fe 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -1136,7 +1136,7 @@ def oiio_color_convert( # Validate input parameters if target_colorspace and target_view and target_display: raise ValueError( - "Colorspace and both view and display cannot be set together." + "Colorspace and both view and display cannot be set together." "Choose colorspace or screen and display" ) From a6a32b49fc176cb5039d47980c0252073f800350 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 2 Sep 2025 11:36:36 +0200 Subject: [PATCH 23/57] update opentimelineio to 0.17.0 --- client/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/pyproject.toml b/client/pyproject.toml index 6416d9b8e1..bccc0b9872 100644 --- a/client/pyproject.toml +++ b/client/pyproject.toml @@ -15,7 +15,7 @@ qtawesome = "0.7.3" [ayon.runtimeDependencies] aiohttp-middlewares = "^2.0.0" Click = "^8" -OpenTimelineIO = "0.16.0" +OpenTimelineIO = "0.17.0" opencolorio = "^2.3.2,<2.4.0" Pillow = "9.5.0" websocket-client = ">=0.40.0,<2" From c3b8f76501f5327343f98f5a5f106e264d34b06b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 3 Sep 2025 20:18:44 +0800 Subject: [PATCH 24/57] max hosts for pre ocio hook so that the environment variable for ocio would be collected accuratly --- client/ayon_core/hooks/pre_ocio_hook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hooks/pre_ocio_hook.py b/client/ayon_core/hooks/pre_ocio_hook.py index 85fcef47f2..be086dae65 100644 --- a/client/ayon_core/hooks/pre_ocio_hook.py +++ b/client/ayon_core/hooks/pre_ocio_hook.py @@ -14,7 +14,7 @@ class OCIOEnvHook(PreLaunchHook): "fusion", "blender", "aftereffects", - "3dsmax", + "max", "houdini", "maya", "nuke", From 14ab55e7ee75f25a34ccf1aa54865ef4f5cbe37e Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 5 Sep 2025 13:40:16 +0000 Subject: [PATCH 25/57] [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 f2aa94020f..8eb2aa68f8 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.5.3+dev" +__version__ = "1.6.0" diff --git a/package.py b/package.py index 4393b7be40..37c3133eb0 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.5.3+dev" +version = "1.6.0" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index ee6c35b50b..302d249cca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.5.3+dev" +version = "1.6.0" description = "" authors = ["Ynput Team "] readme = "README.md" From 95a143ea4602b0fb12cefa9d0a1bd8911c799084 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 5 Sep 2025 13:40:54 +0000 Subject: [PATCH 26/57] [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 8eb2aa68f8..9ca5e1bc30 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.6.0" +__version__ = "1.6.0+dev" diff --git a/package.py b/package.py index 37c3133eb0..e430524dd5 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.6.0" +version = "1.6.0+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 302d249cca..9a62a408ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.6.0" +version = "1.6.0+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From a269244e78c56b049cfc381bd0a927f2373f7f1d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 5 Sep 2025 13:41:47 +0000 Subject: [PATCH 27/57] 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 ce5982969c..24c2b568b3 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.6.0 - 1.5.3 - 1.5.2 - 1.5.1 From 257dfe204a62dedf4a88d598bbba54cd133cf5eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 8 Sep 2025 14:35:57 +0200 Subject: [PATCH 28/57] :sparkles: resolve environments in dev bundle paths --- client/ayon_core/addon/base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index f7fee13dc7..9f1d60fe88 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -302,6 +302,9 @@ def _load_ayon_addons(log): milestone_version = MOVED_ADDON_MILESTONE_VERSIONS.get(addon_name) if use_dev_path: addon_dir = dev_addon_info["path"] + if addon_dir: + addon_dir.format(os.environ) + if not addon_dir or not os.path.exists(addon_dir): log.warning(( "Dev addon {} {} path does not exists. Path \"{}\"" From 8ac8352dcd14c7d720c8439961e014cff34fc884 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Sep 2025 14:47:00 +0200 Subject: [PATCH 29/57] added burnins adapter --- client/pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/client/pyproject.toml b/client/pyproject.toml index bccc0b9872..5acfdf439d 100644 --- a/client/pyproject.toml +++ b/client/pyproject.toml @@ -16,6 +16,7 @@ qtawesome = "0.7.3" aiohttp-middlewares = "^2.0.0" Click = "^8" OpenTimelineIO = "0.17.0" +otio-burnins-adapter = "1.0.0" opencolorio = "^2.3.2,<2.4.0" Pillow = "9.5.0" websocket-client = ">=0.40.0,<2" From 2e881902037299ed09553984553640c2320e1d33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 8 Sep 2025 15:48:30 +0200 Subject: [PATCH 30/57] :bug: fix handling of `os.environ` --- client/ayon_core/addon/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index 9f1d60fe88..3ec09f32a3 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -303,7 +303,7 @@ def _load_ayon_addons(log): if use_dev_path: addon_dir = dev_addon_info["path"] if addon_dir: - addon_dir.format(os.environ) + addon_dir = addon_dir.format(**os.environ) if not addon_dir or not os.path.exists(addon_dir): log.warning(( From db2c1858d5a0014d27a16ecf138075f437b096a6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 8 Sep 2025 16:10:49 +0200 Subject: [PATCH 31/57] Updates color space handling in OIIO transcode. Temporarily disables upstream OCIO color space logic. Uses 'colorspaceDisplay' and 'colorspaceView' instead. This is a temporary workaround. --- .../ayon_core/plugins/publish/extract_color_transcode.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 24820f7022..8b351c7f31 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -88,9 +88,12 @@ class ExtractOIIOTranscode(publish.Extractor): repres = instance.data["representations"] for idx, repre in enumerate(list(repres)): # target space, display and view might be defined upstream - target_colorspace = instance.data.get("targetOCIOColorspace") - target_display = instance.data.get("targetOCIODisplay") - target_view = instance.data.get("targetOCIOView") + # TODO: address https://github.com/ynput/ayon-core/pull/1268#discussion_r2156555474 + # Implement upstream logic to handle target_colorspace, + # target_display, target_view in other DCCs + target_colorspace = False + target_display = instance.data.get("colorspaceDisplay") + target_view = instance.data.get("colorspaceView") self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) if not self._repre_is_valid(repre): From 8e0585c50d06c13e17cfbc0f78ddc0efcae71932 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Mon, 8 Sep 2025 17:52:56 +0200 Subject: [PATCH 32/57] Update client/ayon_core/addon/base.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/addon/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index 3ec09f32a3..70bb9dca40 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -303,7 +303,9 @@ def _load_ayon_addons(log): if use_dev_path: addon_dir = dev_addon_info["path"] if addon_dir: - addon_dir = addon_dir.format(**os.environ) + addon_dir = os.path.expandvars( + addon_dir.format_map(os.environ) + ) if not addon_dir or not os.path.exists(addon_dir): log.warning(( From 6419e3ed749fe33c80100a5fe5716166d4f404e2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 11 Sep 2025 13:35:19 +0200 Subject: [PATCH 33/57] use python 3 super --- client/ayon_core/tools/utils/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/utils/widgets.py b/client/ayon_core/tools/utils/widgets.py index 941aa692f9..4b787ff830 100644 --- a/client/ayon_core/tools/utils/widgets.py +++ b/client/ayon_core/tools/utils/widgets.py @@ -418,7 +418,7 @@ class ExpandingTextEdit(QtWidgets.QTextEdit): """QTextEdit which does not have sroll area but expands height.""" def __init__(self, parent=None): - super(ExpandingTextEdit, self).__init__(parent) + super().__init__(parent) size_policy = self.sizePolicy() size_policy.setHeightForWidth(True) From 6b058fccf092717111444b89e168e4f678d9ca51 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 11 Sep 2025 13:35:50 +0200 Subject: [PATCH 34/57] disable scroll bards on message input --- client/ayon_core/tools/publisher/widgets/report_page.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/tools/publisher/widgets/report_page.py b/client/ayon_core/tools/publisher/widgets/report_page.py index 1e46e7e52c..033ddab0ef 100644 --- a/client/ayon_core/tools/publisher/widgets/report_page.py +++ b/client/ayon_core/tools/publisher/widgets/report_page.py @@ -1147,6 +1147,8 @@ class LogItemMessage(QtWidgets.QTextEdit): QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Maximum ) + self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) document = self.document() document.documentLayout().documentSizeChanged.connect( self._adjust_minimum_size From 23a905b837938da661e30d3cafca3f4e917f32b7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 11 Sep 2025 15:10:45 +0200 Subject: [PATCH 35/57] move addons manager to controller --- client/ayon_core/tools/launcher/abstract.py | 5 +++++ client/ayon_core/tools/launcher/control.py | 8 ++++++++ client/ayon_core/tools/launcher/models/actions.py | 10 +--------- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/tools/launcher/abstract.py b/client/ayon_core/tools/launcher/abstract.py index 1d7dafd62f..c0fc115f31 100644 --- a/client/ayon_core/tools/launcher/abstract.py +++ b/client/ayon_core/tools/launcher/abstract.py @@ -4,6 +4,7 @@ from abc import ABC, abstractmethod from dataclasses import dataclass from typing import Optional, Any +from ayon_core.addon import AddonsManager from ayon_core.tools.common_models import ( ProjectItem, FolderItem, @@ -85,6 +86,10 @@ class AbstractLauncherBackend(AbstractLauncherCommon): pass + @abstractmethod + def get_addons_manager(self) -> AddonsManager: + pass + @abstractmethod def get_project_settings(self, project_name): """Project settings for current project. diff --git a/client/ayon_core/tools/launcher/control.py b/client/ayon_core/tools/launcher/control.py index 58d22453be..ce23b0323f 100644 --- a/client/ayon_core/tools/launcher/control.py +++ b/client/ayon_core/tools/launcher/control.py @@ -1,5 +1,6 @@ from ayon_core.lib import Logger, get_ayon_username from ayon_core.lib.events import QueuedEventSystem +from ayon_core.addon import AddonsManager from ayon_core.settings import get_project_settings, get_studio_settings from ayon_core.tools.common_models import ProjectsModel, HierarchyModel @@ -17,6 +18,8 @@ class BaseLauncherController( self._event_system = None self._log = None + self._addons_manager = None + self._username = NOT_SET self._selection_model = LauncherSelectionModel(self) @@ -59,6 +62,11 @@ class BaseLauncherController( def register_event_callback(self, topic, callback): self.event_system.add_callback(topic, callback) + def get_addons_manager(self) -> AddonsManager: + if self._addons_manager is None: + self._addons_manager = AddonsManager() + return self._addons_manager + # Entity items for UI def get_project_items(self, sender=None): return self._projects_model.get_project_items(sender) diff --git a/client/ayon_core/tools/launcher/models/actions.py b/client/ayon_core/tools/launcher/models/actions.py index 51fbe72143..5f888effb5 100644 --- a/client/ayon_core/tools/launcher/models/actions.py +++ b/client/ayon_core/tools/launcher/models/actions.py @@ -15,7 +15,6 @@ from ayon_core.lib import ( get_settings_variant, run_detached_ayon_launcher_process, ) -from ayon_core.addon import AddonsManager from ayon_core.pipeline.actions import ( discover_launcher_actions, LauncherActionSelection, @@ -104,8 +103,6 @@ class ActionsModel: levels=2, default_factory=list, lifetime=20, ) - self._addons_manager = None - self._variant = get_settings_variant() @staticmethod @@ -333,11 +330,6 @@ class ActionsModel: exc_info=True ) - def _get_addons_manager(self): - if self._addons_manager is None: - self._addons_manager = AddonsManager() - return self._addons_manager - def _prepare_selection(self, project_name, folder_id, task_id): project_entity = None if project_name: @@ -542,7 +534,7 @@ class ActionsModel: # NOTE We don't need to register the paths, but that would # require to change discovery logic and deprecate all functions # related to registering and discovering launcher actions. - addons_manager = self._get_addons_manager() + addons_manager = self._controller.get_addons_manager() actions_paths = addons_manager.collect_launcher_action_paths() for path in actions_paths: if path and os.path.exists(path): From d960694f453bee2a50847649bfbc79f1eea97656 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 12 Sep 2025 12:58:15 +0200 Subject: [PATCH 36/57] added workfile items to launcher controller --- client/ayon_core/tools/launcher/abstract.py | 26 +++++ client/ayon_core/tools/launcher/control.py | 28 ++++- .../tools/launcher/models/__init__.py | 2 + .../tools/launcher/models/workfiles.py | 101 ++++++++++++++++++ 4 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 client/ayon_core/tools/launcher/models/workfiles.py diff --git a/client/ayon_core/tools/launcher/abstract.py b/client/ayon_core/tools/launcher/abstract.py index c0fc115f31..b0a7a8b213 100644 --- a/client/ayon_core/tools/launcher/abstract.py +++ b/client/ayon_core/tools/launcher/abstract.py @@ -57,6 +57,14 @@ class ActionItem: addon_version: Optional[str] = None +@dataclass +class WorkfileItem: + filename : str + exists: bool + icon: Optional[str] + version: Optional[int] + + class AbstractLauncherCommon(ABC): @abstractmethod def register_event_callback(self, topic, callback): @@ -470,3 +478,21 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): """ pass + + @abstractmethod + def get_workfile_items( + self, + project_name: Optional[str], + task_id: Optional[str], + ) -> list[WorkfileItem]: + """Get workfile items for a given context. + + Args: + project_name (Optional[str]): Project name. + task_id (Optional[str]): Task id. + + Returns: + list[WorkfileItem]: List of workfile items. + + """ + pass diff --git a/client/ayon_core/tools/launcher/control.py b/client/ayon_core/tools/launcher/control.py index ce23b0323f..66afebc247 100644 --- a/client/ayon_core/tools/launcher/control.py +++ b/client/ayon_core/tools/launcher/control.py @@ -1,11 +1,21 @@ +from typing import Optional + from ayon_core.lib import Logger, get_ayon_username from ayon_core.lib.events import QueuedEventSystem from ayon_core.addon import AddonsManager from ayon_core.settings import get_project_settings, get_studio_settings from ayon_core.tools.common_models import ProjectsModel, HierarchyModel -from .abstract import AbstractLauncherFrontEnd, AbstractLauncherBackend -from .models import LauncherSelectionModel, ActionsModel +from .abstract import ( + AbstractLauncherFrontEnd, + AbstractLauncherBackend, + WorkfileItem, +) +from .models import ( + LauncherSelectionModel, + ActionsModel, + WorkfilesModel, +) NOT_SET = object() @@ -26,6 +36,7 @@ class BaseLauncherController( self._projects_model = ProjectsModel(self) self._hierarchy_model = HierarchyModel(self) self._actions_model = ActionsModel(self) + self._workfiles_model = WorkfilesModel(self) @property def log(self): @@ -141,6 +152,17 @@ class BaseLauncherController( "task_name": self.get_selected_task_name(), } + # Workfiles + def get_workfile_items( + self, + project_name: Optional[str], + task_id: Optional[str], + ) -> list[WorkfileItem]: + return self._workfiles_model.get_workfile_items( + project_name, + task_id, + ) + # Actions def get_action_items(self, project_name, folder_id, task_id): return self._actions_model.get_action_items( @@ -194,6 +216,8 @@ class BaseLauncherController( self._projects_model.reset() # Refresh actions self._actions_model.refresh() + # Reset workfiles model + self._workfiles_model.reset() self._emit_event("controller.refresh.actions.finished") diff --git a/client/ayon_core/tools/launcher/models/__init__.py b/client/ayon_core/tools/launcher/models/__init__.py index 1bc60c85f0..efc0de96ca 100644 --- a/client/ayon_core/tools/launcher/models/__init__.py +++ b/client/ayon_core/tools/launcher/models/__init__.py @@ -1,8 +1,10 @@ from .actions import ActionsModel from .selection import LauncherSelectionModel +from .workfiles import WorkfilesModel __all__ = ( "ActionsModel", "LauncherSelectionModel", + "WorkfilesModel", ) diff --git a/client/ayon_core/tools/launcher/models/workfiles.py b/client/ayon_core/tools/launcher/models/workfiles.py new file mode 100644 index 0000000000..2ba15c1800 --- /dev/null +++ b/client/ayon_core/tools/launcher/models/workfiles.py @@ -0,0 +1,101 @@ +import os +from typing import Optional, Any + +import ayon_api + +from ayon_core.lib import ( + Logger, + NestedCacheItem, +) +from ayon_core.pipeline import Anatomy +from ayon_core.tools.launcher.abstract import ( + WorkfileItem, + AbstractLauncherBackend, +) + + +class WorkfilesModel: + def __init__(self, controller: AbstractLauncherBackend): + self._controller = controller + + self._log = Logger.get_logger(self.__class__.__name__) + + self._host_icons = None + self._workfile_items = NestedCacheItem( + levels=2, default_factory=list, lifetime=60, + ) + + def reset(self) -> None: + self._workfile_items.reset() + + def get_workfile_items( + self, + project_name: Optional[str], + task_id: Optional[str], + ) -> list[WorkfileItem]: + if not project_name or not task_id: + return [] + + cache = self._workfile_items[project_name][task_id] + if cache.is_valid: + return cache.get_data() + + project_entity = self._controller.get_project_entity(project_name) + anatomy = Anatomy(project_name, project_entity=project_entity) + items = [] + for workfile_entity in ayon_api.get_workfiles_info( + project_name, task_ids={task_id}, fields={"path", "data"} + ): + rootless_path = workfile_entity["path"] + exists = False + try: + path = anatomy.fill_root(rootless_path) + exists = os.path.exists(path) + except Exception: + self._log.warning( + "Failed to fill root for workfile path", + exc_info=True, + ) + workfile_data = workfile_entity["data"] + host_name = workfile_data.get("host_name") + version = workfile_data.get("version") + + items.append(WorkfileItem( + os.path.basename(rootless_path), + exists=exists, + icon=self._get_host_icon(host_name), + version=version, + )) + cache.update_data(items) + return items + + def _get_host_icon( + self, host_name: Optional[str] + ) -> Optional[dict[str, Any]]: + if self._host_icons is None: + host_icons = {} + try: + host_icons = self._get_host_icons() + except Exception: + self._log.warning( + "Failed to get host icons", + exc_info=True, + ) + self._host_icons = host_icons + return self._host_icons.get(host_name) + + def _get_host_icons(self) -> dict[str, Any]: + addons_manager = self._controller.get_addons_manager() + applications_addon = addons_manager["applications"] + apps_manager = applications_addon.get_applications_manager() + output = {} + for app_group in apps_manager.app_groups.values(): + host_name = app_group.host_name + icon_filename = app_group.icon + if not host_name or not icon_filename: + continue + icon_url = applications_addon.get_app_icon_url( + icon_filename, server=True + ) + output[host_name] = icon_url + return output From 0794db9ac2d6fc6165c12a90aba2084433fab4eb Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 12 Sep 2025 12:58:37 +0200 Subject: [PATCH 37/57] added workfiles page to launcher window --- .../tools/launcher/ui/hierarchy_page.py | 13 +- client/ayon_core/tools/launcher/ui/window.py | 2 +- .../tools/launcher/ui/workfiles_page.py | 178 ++++++++++++++++++ 3 files changed, 190 insertions(+), 3 deletions(-) create mode 100644 client/ayon_core/tools/launcher/ui/workfiles_page.py diff --git a/client/ayon_core/tools/launcher/ui/hierarchy_page.py b/client/ayon_core/tools/launcher/ui/hierarchy_page.py index 65efdc27ac..47388d9685 100644 --- a/client/ayon_core/tools/launcher/ui/hierarchy_page.py +++ b/client/ayon_core/tools/launcher/ui/hierarchy_page.py @@ -12,6 +12,8 @@ from ayon_core.tools.utils import ( ) from ayon_core.tools.utils.lib import checkstate_int_to_enum +from .workfiles_page import WorkfilesPage + class HierarchyPage(QtWidgets.QWidget): def __init__(self, controller, parent): @@ -73,10 +75,15 @@ class HierarchyPage(QtWidgets.QWidget): # - Tasks widget tasks_widget = TasksWidget(controller, content_body) + # - Third page - Workfiles + workfiles_page = WorkfilesPage(controller, content_body) + content_body.addWidget(folders_widget) content_body.addWidget(tasks_widget) - content_body.setStretchFactor(0, 100) - content_body.setStretchFactor(1, 65) + content_body.addWidget(workfiles_page) + content_body.setStretchFactor(0, 120) + content_body.setStretchFactor(1, 85) + content_body.setStretchFactor(2, 220) main_layout = QtWidgets.QVBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) @@ -99,6 +106,7 @@ class HierarchyPage(QtWidgets.QWidget): self._my_tasks_checkbox = my_tasks_checkbox self._folders_widget = folders_widget self._tasks_widget = tasks_widget + self._workfiles_page = workfiles_page self._project_name = None @@ -117,6 +125,7 @@ class HierarchyPage(QtWidgets.QWidget): def refresh(self): self._folders_widget.refresh() self._tasks_widget.refresh() + self._workfiles_page.refresh() self._on_my_tasks_checkbox_state_changed( self._my_tasks_checkbox.checkState() ) diff --git a/client/ayon_core/tools/launcher/ui/window.py b/client/ayon_core/tools/launcher/ui/window.py index 819e141d59..ad2fd2d3c2 100644 --- a/client/ayon_core/tools/launcher/ui/window.py +++ b/client/ayon_core/tools/launcher/ui/window.py @@ -177,7 +177,7 @@ class LauncherWindow(QtWidgets.QWidget): self._page_slide_anim = page_slide_anim hierarchy_page.setVisible(not self._is_on_projects_page) - self.resize(520, 740) + self.resize(920, 740) def showEvent(self, event): super().showEvent(event) diff --git a/client/ayon_core/tools/launcher/ui/workfiles_page.py b/client/ayon_core/tools/launcher/ui/workfiles_page.py new file mode 100644 index 0000000000..2f390f1bee --- /dev/null +++ b/client/ayon_core/tools/launcher/ui/workfiles_page.py @@ -0,0 +1,178 @@ +from typing import Optional + +import ayon_api +from qtpy import QtCore, QtWidgets, QtGui + +from ayon_core.tools.utils import get_qt_icon +from ayon_core.tools.launcher.abstract import AbstractLauncherFrontEnd + +VERSION_ROLE = QtCore.Qt.UserRole + 1 + + +class WorkfilesModel(QtGui.QStandardItemModel): + refreshed = QtCore.Signal() + + def __init__(self, controller: AbstractLauncherFrontEnd) -> None: + super().__init__() + + self.setColumnCount(1) + self.setHeaderData(0, QtCore.Qt.Horizontal, "Workfiles") + + controller.register_event_callback( + "selection.project.changed", + self._on_selection_project_changed, + ) + controller.register_event_callback( + "selection.folder.changed", + self._on_selection_folder_changed, + ) + controller.register_event_callback( + "selection.task.changed", + self._on_selection_task_changed, + ) + + self._controller = controller + self._selected_project_name = None + self._selected_folder_id = None + self._selected_task_id = None + + self._transparent_icon = None + + self._cached_icons = {} + + def refresh(self) -> None: + root_item = self.invisibleRootItem() + root_item.removeRows(0, root_item.rowCount()) + + workfile_items = self._controller.get_workfile_items( + self._selected_project_name, self._selected_task_id + ) + new_items = [] + for workfile_item in workfile_items: + icon = self._get_icon(workfile_item.icon) + item = QtGui.QStandardItem(workfile_item.filename) + item.setData(icon, QtCore.Qt.DecorationRole) + item.setData(workfile_item.version, VERSION_ROLE) + flags = QtCore.Qt.NoItemFlags + if workfile_item.exists: + flags = QtCore.Qt.ItemIsEnabled + item.setFlags(flags) + new_items.append(item) + + if not new_items: + title = "< No workfiles >" + if not self._selected_project_name: + title = "< Select a project >" + elif not self._selected_folder_id: + title = "< Select a folder >" + elif not self._selected_task_id: + title = "< Select a task >" + item = QtGui.QStandardItem(title) + item.setFlags(QtCore.Qt.NoItemFlags) + new_items.append(item) + root_item.appendRows(new_items) + + self.refreshed.emit() + + def _on_selection_project_changed(self, event) -> None: + self._selected_project_name = event["project_name"] + self._selected_folder_id = None + self._selected_task_id = None + self.refresh() + + def _on_selection_folder_changed(self, event) -> None: + self._selected_project_name = event["project_name"] + self._selected_folder_id = event["folder_id"] + self._selected_task_id = None + self.refresh() + + def _on_selection_task_changed(self, event) -> None: + self._selected_project_name = event["project_name"] + self._selected_folder_id = event["folder_id"] + self._selected_task_id = event["task_id"] + self.refresh() + + def _get_transparent_icon(self) -> QtGui.QIcon: + if self._transparent_icon is None: + self._transparent_icon = get_qt_icon({ + "type": "transparent", "size": 256 + }) + return self._transparent_icon + + def _get_icon(self, icon_url: Optional[str]) -> QtGui.QIcon: + if icon_url is None: + return self._get_transparent_icon() + icon = self._cached_icons.get(icon_url) + if icon is not None: + return icon + + base_url = ayon_api.get_base_url() + if icon_url.startswith(base_url): + icon_def = { + "type": "ayon_url", + "url": icon_url[len(base_url) + 1:], + } + else: + icon_def = { + "type": "url", + "url": icon_url, + } + + icon = get_qt_icon(icon_def) + if icon is None: + icon = self._get_transparent_icon() + self._cached_icons[icon_url] = icon + return icon + + +class WorkfilesProxyModel(QtCore.QSortFilterProxyModel): + def lessThan(self, left, right) -> bool: + # left_version = left.data(VERSION_ROLE) + # right_version = right.data(VERSION_ROLE) + # if left_version != right_version: + # if left_version is None: + # return False + # if right_version is None: + # return True + # + # return left_version > right_version + return not super().lessThan(left, right) + + +class WorkfilesView(QtWidgets.QTreeView): + def drawBranches(self, painter, rect, index): + return + + +class WorkfilesPage(QtWidgets.QWidget): + def __init__( + self, + controller: AbstractLauncherFrontEnd, + parent: QtWidgets.QWidget, + ) -> None: + super().__init__(parent) + + workfiles_view = WorkfilesView(self) + workfiles_view.setIndentation(0) + workfiles_model = WorkfilesModel(controller) + workfiles_proxy = WorkfilesProxyModel() + workfiles_proxy.setSourceModel(workfiles_model) + + workfiles_view.setModel(workfiles_proxy) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(workfiles_view, 1) + + workfiles_model.refreshed.connect(self._on_refresh) + + self._controller = controller + self._workfiles_view = workfiles_view + self._workfiles_model = workfiles_model + self._workfiles_proxy = workfiles_proxy + + def refresh(self) -> None: + self._workfiles_model.refresh() + + def _on_refresh(self) -> None: + self._workfiles_proxy.sort(0) From f19c6d0ad8f75fda4854e2343665de8405396bb3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 12 Sep 2025 15:30:53 +0200 Subject: [PATCH 38/57] use standard proxy model --- .../tools/launcher/ui/workfiles_page.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/client/ayon_core/tools/launcher/ui/workfiles_page.py b/client/ayon_core/tools/launcher/ui/workfiles_page.py index 2f390f1bee..9bfd474764 100644 --- a/client/ayon_core/tools/launcher/ui/workfiles_page.py +++ b/client/ayon_core/tools/launcher/ui/workfiles_page.py @@ -125,20 +125,6 @@ class WorkfilesModel(QtGui.QStandardItemModel): return icon -class WorkfilesProxyModel(QtCore.QSortFilterProxyModel): - def lessThan(self, left, right) -> bool: - # left_version = left.data(VERSION_ROLE) - # right_version = right.data(VERSION_ROLE) - # if left_version != right_version: - # if left_version is None: - # return False - # if right_version is None: - # return True - # - # return left_version > right_version - return not super().lessThan(left, right) - - class WorkfilesView(QtWidgets.QTreeView): def drawBranches(self, painter, rect, index): return @@ -155,7 +141,7 @@ class WorkfilesPage(QtWidgets.QWidget): workfiles_view = WorkfilesView(self) workfiles_view.setIndentation(0) workfiles_model = WorkfilesModel(controller) - workfiles_proxy = WorkfilesProxyModel() + workfiles_proxy = QtCore.QSortFilterProxyModel() workfiles_proxy.setSourceModel(workfiles_model) workfiles_view.setModel(workfiles_proxy) @@ -175,4 +161,4 @@ class WorkfilesPage(QtWidgets.QWidget): self._workfiles_model.refresh() def _on_refresh(self) -> None: - self._workfiles_proxy.sort(0) + self._workfiles_proxy.sort(0, QtCore.Qt.DescendingOrder ) From afe70c0f55d3d567ddf578a13a03f7172093522f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 12 Sep 2025 15:37:50 +0200 Subject: [PATCH 39/57] formatting fixes --- client/ayon_core/tools/launcher/abstract.py | 2 +- client/ayon_core/tools/launcher/ui/workfiles_page.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/launcher/abstract.py b/client/ayon_core/tools/launcher/abstract.py index b0a7a8b213..f312504d31 100644 --- a/client/ayon_core/tools/launcher/abstract.py +++ b/client/ayon_core/tools/launcher/abstract.py @@ -59,7 +59,7 @@ class ActionItem: @dataclass class WorkfileItem: - filename : str + filename: str exists: bool icon: Optional[str] version: Optional[int] diff --git a/client/ayon_core/tools/launcher/ui/workfiles_page.py b/client/ayon_core/tools/launcher/ui/workfiles_page.py index 9bfd474764..0401183080 100644 --- a/client/ayon_core/tools/launcher/ui/workfiles_page.py +++ b/client/ayon_core/tools/launcher/ui/workfiles_page.py @@ -161,4 +161,4 @@ class WorkfilesPage(QtWidgets.QWidget): self._workfiles_model.refresh() def _on_refresh(self) -> None: - self._workfiles_proxy.sort(0, QtCore.Qt.DescendingOrder ) + self._workfiles_proxy.sort(0, QtCore.Qt.DescendingOrder) From 8f2f10bce1efca04f6f0a22f663d98a99cce95c6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 12 Sep 2025 18:20:43 +0200 Subject: [PATCH 40/57] added 'get_project_entity' to controller --- client/ayon_core/tools/loader/abstract.py | 6 ++++++ client/ayon_core/tools/loader/control.py | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index 5ab7e78212..42e88c15d7 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -513,6 +513,12 @@ class BackendLoaderController(_BaseLoaderController): pass + @abstractmethod + def get_project_entity( + self, project_name: Optional[str] + ) -> Optional[dict[str, Any]]: + pass + class FrontendLoaderController(_BaseLoaderController): @abstractmethod diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index 7ba42a0981..492a05e82f 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging import uuid +from typing import Optional, Any import ayon_api @@ -188,6 +189,11 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): def get_project_items(self, sender=None): return self._projects_model.get_project_items(sender) + def get_project_entity( + self, project_name: Optional[str] + ) -> Optional[dict[str, Any]]: + return self._projects_model.get_project_entity(project_name) + def get_folder_type_items(self, project_name, sender=None): return self._projects_model.get_folder_type_items( project_name, sender From 7f8e62e8efc2efa016fe66cabc305d1a215fd5bf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 12 Sep 2025 18:29:55 +0200 Subject: [PATCH 41/57] add icons mapping to products --- client/ayon_core/tools/loader/abstract.py | 7 - .../ayon_core/tools/loader/models/products.py | 205 +++++++++--------- .../tools/loader/ui/products_model.py | 8 +- 3 files changed, 103 insertions(+), 117 deletions(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index 42e88c15d7..87cfb1f3f2 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -78,7 +78,6 @@ class ProductItem: 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_in_scene (bool): Is product in scene (only when used in DCC). group_name (str): Group name. folder_id (str): Folder id. @@ -93,8 +92,6 @@ class ProductItem: product_base_type: str, product_name: str, product_icon: dict[str, Any], - product_type_icon: dict[str, Any], - product_base_type_icon: dict[str, Any], group_name: str, folder_id: str, folder_label: str, @@ -106,8 +103,6 @@ class ProductItem: 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 @@ -121,8 +116,6 @@ class ProductItem: "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, diff --git a/client/ayon_core/tools/loader/models/products.py b/client/ayon_core/tools/loader/models/products.py index 87e2406c81..f5e0b2fa80 100644 --- a/client/ayon_core/tools/loader/models/products.py +++ b/client/ayon_core/tools/loader/models/products.py @@ -9,7 +9,6 @@ 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 ( @@ -21,12 +20,63 @@ from ayon_core.tools.loader.abstract import ( ) if TYPE_CHECKING: - from ayon_api.typing import ProductBaseTypeDict, ProductDict, VersionDict - + from ayon_api.typing import ( + ProductBaseTypeDict, + ProductDict, + VersionDict, + ) PRODUCTS_MODEL_SENDER = "products.model" +class ProductBaseTypeIconMapping: + def __init__( + self, + default: Optional[dict[str, str]] = None, + definitions: Optional[list[dict[str, str]]] = None, + ): + self._default = default or {} + self._definitions = definitions or [] + + self._default_def = None + self._definitions_by_name = None + + def get_icon( + self, + product_base_type: Optional[str] = None, + product_type: Optional[str] = None, + ) -> dict[str, str]: + defs = self._get_defs_by_name() + icon = defs.get(product_type) + if icon is None: + icon = defs.get(product_base_type) + if icon is None: + icon = self._get_default_def() + return icon.copy() + + def _get_default_def(self) -> dict[str, str]: + if self._default_def is None: + self._default_def = { + "type": "material-symbols", + "name": self._default.get("icon", "inventory_2"), + "color": self._default.get("color", "#cccccc"), + } + + return self._default_def + + def _get_defs_by_name(self) -> dict[str, dict[str, str]]: + if self._definitions_by_name is None: + self._definitions_by_name = { + product_base_type_def["name"]: { + "type": "material-symbols", + "name": product_base_type_def.get("icon", "inventory_2"), + "color": product_base_type_def.get("color", "#cccccc"), + } + for product_base_type_def in self._definitions + } + return self._definitions_by_name + + def version_item_from_entity(version): version_attribs = version["attrib"] tags = version["tags"] @@ -84,42 +134,18 @@ def version_item_from_entity(version): def product_item_from_entity( product_entity: ProductDict, version_entities, - product_type_items_by_name: dict[str, ProductTypeItem], - product_base_type_items_by_name: dict[str, ProductBaseTypeItem], folder_label, + icons_mapping, product_in_scene, ): product_attribs = product_entity["attrib"] group = product_attribs.get("productGroup") product_type = product_entity["productType"] - product_type_item = product_type_items_by_name.get(product_type) - # NOTE This is needed for cases when products were not created on server - # using api functions. In that case product type item may not be - # available and we need to create a default. - if product_type_item is None: - product_type_item = create_default_product_type_item(product_type) - # Cache the item for future use - product_type_items_by_name[product_type] = product_type_item - 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_type_icon = product_type_item.icon - product_base_type_icon = product_base_type_item.icon - product_icon = { - "type": "awesome-font", - "name": "fa.file-o", - "color": get_default_entity_icon_color(), - } + product_icon = icons_mapping.get_icon( + product_base_type, product_type + ) version_items = { version_entity["id"]: version_item_from_entity(version_entity) for version_entity in version_entities @@ -131,8 +157,6 @@ def product_item_from_entity( 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"], @@ -141,22 +165,8 @@ def product_item_from_entity( ) -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 = { - "type": "awesome-font", - "name": "fa.folder", - "color": "#0091B2", - } - # TODO implement checked logic - return ProductTypeItem(product_type_data["name"], icon) - - def product_base_type_item_from_data( - product_base_type_data: ProductBaseTypeDict + product_base_type_data: ProductBaseTypeDict ) -> ProductBaseTypeItem: """Create product base type item from data. @@ -174,34 +184,8 @@ def product_base_type_item_from_data( } return ProductBaseTypeItem( name=product_base_type_data["name"], - icon=icon) - - -def create_default_product_type_item(product_type: str) -> ProductTypeItem: - icon = { - "type": "awesome-font", - "name": "fa.folder", - "color": "#0091B2", - } - 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 = { - "type": "awesome-font", - "name": "fa.folder", - "color": "#0091B2", - } - return ProductBaseTypeItem(product_base_type, icon) + icon=icon + ) class ProductsModel: @@ -227,6 +211,8 @@ class ProductsModel: self._product_folder_ids_mapping = collections.defaultdict(dict) # Cache helpers + self._product_type_icons_mapping = NestedCacheItem( + levels=1, default_factory=list, lifetime=self.lifetime) self._product_type_items_cache = NestedCacheItem( levels=1, default_factory=list, lifetime=self.lifetime) self._product_base_type_items_cache = NestedCacheItem( @@ -243,11 +229,14 @@ class ProductsModel: self._version_item_by_id.clear() self._product_folder_ids_mapping.clear() + self._product_type_icons_mapping.reset() self._product_type_items_cache.reset() self._product_items_cache.reset() self._repre_items_cache.reset() - def get_product_type_items(self, project_name): + def get_product_type_items( + self, project_name: Optional[str] + ) -> list[ProductTypeItem]: """Product type items for project. Args: @@ -255,23 +244,27 @@ class ProductsModel: Returns: list[ProductTypeItem]: Product type items. - """ + """ if not project_name: return [] cache = self._product_type_items_cache[project_name] if not cache.is_valid: + icons_mapping = self._get_product_type_icons(project_name) product_types = ayon_api.get_project_product_types(project_name) cache.update_data([ - product_type_item_from_data(product_type) + ProductTypeItem( + product_type["name"], + icons_mapping.get_icon(product_type=product_type["name"]), + ) for product_type in product_types ]) return cache.get_data() def get_product_base_type_items( - self, - project_name: Optional[str]) -> list[ProductBaseTypeItem]: + self, project_name: Optional[str] + ) -> list[ProductBaseTypeItem]: """Product base type items for the project. Args: @@ -286,6 +279,7 @@ class ProductsModel: cache = self._product_base_type_items_cache[project_name] if not cache.is_valid: + icons_mapping = self._get_product_type_icons(project_name) product_base_types = [] # TODO add temp implementation here when it is actually # implemented and available on server. @@ -294,7 +288,10 @@ class ProductsModel: project_name ) cache.update_data([ - product_base_type_item_from_data(product_base_type) + ProductBaseTypeItem( + product_base_type["name"], + icons_mapping.get_icon(product_base_type["name"]), + ) for product_base_type in product_base_types ]) return cache.get_data() @@ -511,6 +508,27 @@ class ProductsModel: PRODUCTS_MODEL_SENDER ) + def _get_product_type_icons( + self, project_name: Optional[str] + ) -> ProductBaseTypeIconMapping: + cache = self._product_type_icons_mapping[project_name] + if cache.is_valid: + return cache.get_data() + + project_entity = self._controller.get_project_entity(project_name) + icons_mapping = ProductBaseTypeIconMapping() + if project_entity: + product_base_types = ( + project_entity["config"].get("productBaseTypes", {}) + ) + icons_mapping = ProductBaseTypeIconMapping( + product_base_types.get("default"), + product_base_types.get("definitions") + ) + + cache.update_data(icons_mapping) + return icons_mapping + def _get_product_items_by_id(self, project_name, product_ids): product_item_by_id = self._product_item_by_id[project_name] missing_product_ids = set() @@ -553,36 +571,18 @@ class ProductsModel: 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) - 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) for version in versions: versions_by_product_id[version["productId"]].append(version) - product_type_items_by_name = { - product_type_item.name: product_type_item - for product_type_item in product_type_items - } - - 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] = {} + icons_mapping = self._get_product_type_icons(project_name) for product in products: product_id = product["id"] folder_id = product["folderId"] @@ -595,9 +595,8 @@ class ProductsModel: product_item = product_item_from_entity( product, versions, - product_type_items_by_name, - product_base_type_items_by_name, folder_item.label, + icons_mapping, product_id in loaded_product_ids, ) output[product_id] = product_item diff --git a/client/ayon_core/tools/loader/ui/products_model.py b/client/ayon_core/tools/loader/ui/products_model.py index f3e5271f51..79ed197d83 100644 --- a/client/ayon_core/tools/loader/ui/products_model.py +++ b/client/ayon_core/tools/loader/ui/products_model.py @@ -17,7 +17,6 @@ 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_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 @@ -228,10 +227,7 @@ class ProductsModel(QtGui.QStandardItemModel): return super().data(index, role) if role == QtCore.Qt.DecorationRole: - if col == 1: - role = PRODUCT_TYPE_ICON_ROLE - else: - return None + return None if ( role == VERSION_NAME_EDIT_ROLE @@ -455,7 +451,6 @@ class ProductsModel(QtGui.QStandardItemModel): model_item = QtGui.QStandardItem(product_item.product_name) model_item.setEditable(False) icon = get_qt_icon(product_item.product_icon) - product_type_icon = get_qt_icon(product_item.product_type_icon) model_item.setColumnCount(self.columnCount()) model_item.setData(icon, QtCore.Qt.DecorationRole) model_item.setData(product_id, PRODUCT_ID_ROLE) @@ -464,7 +459,6 @@ class ProductsModel(QtGui.QStandardItemModel): 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) self._product_items_by_id[product_id] = product_item From d8045df9a7f6d82941f280d4ae9783b58557a7f7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 12 Sep 2025 18:33:03 +0200 Subject: [PATCH 42/57] use same deafult as backend has --- client/ayon_core/tools/loader/models/products.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/loader/models/products.py b/client/ayon_core/tools/loader/models/products.py index f5e0b2fa80..11468e4db0 100644 --- a/client/ayon_core/tools/loader/models/products.py +++ b/client/ayon_core/tools/loader/models/products.py @@ -58,7 +58,7 @@ class ProductBaseTypeIconMapping: if self._default_def is None: self._default_def = { "type": "material-symbols", - "name": self._default.get("icon", "inventory_2"), + "name": self._default.get("icon", "deployed_code"), "color": self._default.get("color", "#cccccc"), } @@ -69,7 +69,7 @@ class ProductBaseTypeIconMapping: self._definitions_by_name = { product_base_type_def["name"]: { "type": "material-symbols", - "name": product_base_type_def.get("icon", "inventory_2"), + "name": product_base_type_def.get("icon", "deployed_code"), "color": product_base_type_def.get("color", "#cccccc"), } for product_base_type_def in self._definitions From 7b92047a6bea0e10e3e3e8cb34d97129a75d72c7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Sep 2025 11:38:58 +0200 Subject: [PATCH 43/57] move 'ProductTypeIconMapping' to common project model --- .../ayon_core/tools/common_models/__init__.py | 2 + .../ayon_core/tools/common_models/projects.py | 77 +++++++++++++++++- client/ayon_core/tools/loader/abstract.py | 20 +++-- client/ayon_core/tools/loader/control.py | 13 ++-- .../ayon_core/tools/loader/models/products.py | 78 ++----------------- 5 files changed, 108 insertions(+), 82 deletions(-) diff --git a/client/ayon_core/tools/common_models/__init__.py b/client/ayon_core/tools/common_models/__init__.py index ec69e20b64..77cc2dfb0f 100644 --- a/client/ayon_core/tools/common_models/__init__.py +++ b/client/ayon_core/tools/common_models/__init__.py @@ -10,6 +10,7 @@ from .projects import ( PROJECTS_MODEL_SENDER, FolderTypeItem, TaskTypeItem, + ProductTypeIconMapping, ) from .hierarchy import ( FolderItem, @@ -34,6 +35,7 @@ __all__ = ( "PROJECTS_MODEL_SENDER", "FolderTypeItem", "TaskTypeItem", + "ProductTypeIconMapping", "FolderItem", "TaskItem", diff --git a/client/ayon_core/tools/common_models/projects.py b/client/ayon_core/tools/common_models/projects.py index 034947de3a..710482a871 100644 --- a/client/ayon_core/tools/common_models/projects.py +++ b/client/ayon_core/tools/common_models/projects.py @@ -2,7 +2,7 @@ from __future__ import annotations import contextlib from abc import ABC, abstractmethod -from typing import Dict, Any +from typing import Any, Optional from dataclasses import dataclass import ayon_api @@ -51,7 +51,7 @@ class StatusItem: self.icon: str = icon self.state: str = state - def to_data(self) -> Dict[str, Any]: + def to_data(self) -> dict[str, Any]: return { "name": self.name, "color": self.color, @@ -218,6 +218,54 @@ class ProjectItem: return cls(**data) +class ProductTypeIconMapping: + def __init__( + self, + default: Optional[dict[str, str]] = None, + definitions: Optional[list[dict[str, str]]] = None, + ): + self._default = default or {} + self._definitions = definitions or [] + + self._default_def = None + self._definitions_by_name = None + + def get_icon( + self, + product_base_type: Optional[str] = None, + product_type: Optional[str] = None, + ) -> dict[str, str]: + defs = self._get_defs_by_name() + icon = defs.get(product_type) + if icon is None: + icon = defs.get(product_base_type) + if icon is None: + icon = self._get_default_def() + return icon.copy() + + def _get_default_def(self) -> dict[str, str]: + if self._default_def is None: + self._default_def = { + "type": "material-symbols", + "name": self._default.get("icon", "deployed_code"), + "color": self._default.get("color", "#cccccc"), + } + + return self._default_def + + def _get_defs_by_name(self) -> dict[str, dict[str, str]]: + if self._definitions_by_name is None: + self._definitions_by_name = { + product_base_type_def["name"]: { + "type": "material-symbols", + "name": product_base_type_def.get("icon", "deployed_code"), + "color": product_base_type_def.get("color", "#cccccc"), + } + for product_base_type_def in self._definitions + } + return self._definitions_by_name + + def _get_project_items_from_entitiy( projects: list[dict[str, Any]] ) -> list[ProjectItem]: @@ -242,6 +290,9 @@ class ProjectsModel(object): self._projects_by_name = NestedCacheItem( levels=1, default_factory=list ) + self._product_type_icons_mapping = NestedCacheItem( + levels=1, default_factory=ProductTypeIconMapping + ) self._project_statuses_cache = {} self._folder_types_cache = {} self._task_types_cache = {} @@ -255,6 +306,7 @@ class ProjectsModel(object): self._task_types_cache = {} self._projects_cache.reset() self._projects_by_name.reset() + self._product_type_icons_mapping.reset() def refresh(self): """Refresh project items. @@ -390,6 +442,27 @@ class ProjectsModel(object): self._task_type_items_getter, ) + def get_product_type_icons_mapping( + self, project_name: Optional[str] + ) -> ProductTypeIconMapping: + cache = self._product_type_icons_mapping[project_name] + if cache.is_valid: + return cache.get_data() + + project_entity = self.get_project_entity(project_name) + icons_mapping = ProductTypeIconMapping() + if project_entity: + product_base_types = ( + project_entity["config"].get("productBaseTypes", {}) + ) + icons_mapping = ProductTypeIconMapping( + product_base_types.get("default"), + product_base_types.get("definitions") + ) + + cache.update_data(icons_mapping) + return icons_mapping + def _get_project_items( self, project_name, sender, item_type, cache_obj, getter ): diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index 87cfb1f3f2..9c7934d2db 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -9,7 +9,11 @@ from ayon_core.lib.attribute_definitions import ( deserialize_attr_defs, serialize_attr_defs, ) -from ayon_core.tools.common_models import TaskItem, TagItem +from ayon_core.tools.common_models import ( + TaskItem, + TagItem, + ProductTypeIconMapping, +) class ProductTypeItem: @@ -492,8 +496,8 @@ class BackendLoaderController(_BaseLoaderController): topic (str): Event topic name. data (Optional[dict[str, Any]]): Event data. source (Optional[str]): Event source. - """ + """ pass @abstractmethod @@ -502,14 +506,20 @@ class BackendLoaderController(_BaseLoaderController): Returns: set[str]: Set of loaded product ids. - """ + """ pass @abstractmethod - def get_project_entity( + def get_product_type_icons_mapping( self, project_name: Optional[str] - ) -> Optional[dict[str, Any]]: + ) -> ProductTypeIconMapping: + """Product type icons mapping. + + Returns: + ProductTypeIconMapping: Product type icons mapping. + + """ pass diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index 492a05e82f..c7c182b192 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -17,6 +17,7 @@ from ayon_core.tools.common_models import ( HierarchyModel, ThumbnailsModel, TagItem, + ProductTypeIconMapping, ) from .abstract import ( @@ -189,11 +190,6 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): def get_project_items(self, sender=None): return self._projects_model.get_project_items(sender) - def get_project_entity( - self, project_name: Optional[str] - ) -> Optional[dict[str, Any]]: - return self._projects_model.get_project_entity(project_name) - def get_folder_type_items(self, project_name, sender=None): return self._projects_model.get_folder_type_items( project_name, sender @@ -204,6 +200,13 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): project_name, sender ) + def get_product_type_icons_mapping( + self, project_name: Optional[str] + ) -> ProductTypeIconMapping: + return self._projects_model.get_product_type_icons_mapping( + project_name + ) + def get_folder_items(self, project_name, sender=None): return self._hierarchy_model.get_folder_items(project_name, sender) diff --git a/client/ayon_core/tools/loader/models/products.py b/client/ayon_core/tools/loader/models/products.py index 11468e4db0..7915a75bcf 100644 --- a/client/ayon_core/tools/loader/models/products.py +++ b/client/ayon_core/tools/loader/models/products.py @@ -11,6 +11,7 @@ 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.common_models import ProductTypeIconMapping from ayon_core.tools.loader.abstract import ( ProductTypeItem, ProductBaseTypeItem, @@ -29,54 +30,6 @@ if TYPE_CHECKING: PRODUCTS_MODEL_SENDER = "products.model" -class ProductBaseTypeIconMapping: - def __init__( - self, - default: Optional[dict[str, str]] = None, - definitions: Optional[list[dict[str, str]]] = None, - ): - self._default = default or {} - self._definitions = definitions or [] - - self._default_def = None - self._definitions_by_name = None - - def get_icon( - self, - product_base_type: Optional[str] = None, - product_type: Optional[str] = None, - ) -> dict[str, str]: - defs = self._get_defs_by_name() - icon = defs.get(product_type) - if icon is None: - icon = defs.get(product_base_type) - if icon is None: - icon = self._get_default_def() - return icon.copy() - - def _get_default_def(self) -> dict[str, str]: - if self._default_def is None: - self._default_def = { - "type": "material-symbols", - "name": self._default.get("icon", "deployed_code"), - "color": self._default.get("color", "#cccccc"), - } - - return self._default_def - - def _get_defs_by_name(self) -> dict[str, dict[str, str]]: - if self._definitions_by_name is None: - self._definitions_by_name = { - product_base_type_def["name"]: { - "type": "material-symbols", - "name": product_base_type_def.get("icon", "deployed_code"), - "color": product_base_type_def.get("color", "#cccccc"), - } - for product_base_type_def in self._definitions - } - return self._definitions_by_name - - def version_item_from_entity(version): version_attribs = version["attrib"] tags = version["tags"] @@ -211,8 +164,6 @@ class ProductsModel: self._product_folder_ids_mapping = collections.defaultdict(dict) # Cache helpers - self._product_type_icons_mapping = NestedCacheItem( - levels=1, default_factory=list, lifetime=self.lifetime) self._product_type_items_cache = NestedCacheItem( levels=1, default_factory=list, lifetime=self.lifetime) self._product_base_type_items_cache = NestedCacheItem( @@ -229,7 +180,6 @@ class ProductsModel: self._version_item_by_id.clear() self._product_folder_ids_mapping.clear() - self._product_type_icons_mapping.reset() self._product_type_items_cache.reset() self._product_items_cache.reset() self._repre_items_cache.reset() @@ -267,6 +217,10 @@ class ProductsModel: ) -> list[ProductBaseTypeItem]: """Product base type items for the project. + Notes: + This will be used for filtering product types in UI when + product base types are fully implemented. + Args: project_name (optional, str): Project name. @@ -510,24 +464,8 @@ class ProductsModel: def _get_product_type_icons( self, project_name: Optional[str] - ) -> ProductBaseTypeIconMapping: - cache = self._product_type_icons_mapping[project_name] - if cache.is_valid: - return cache.get_data() - - project_entity = self._controller.get_project_entity(project_name) - icons_mapping = ProductBaseTypeIconMapping() - if project_entity: - product_base_types = ( - project_entity["config"].get("productBaseTypes", {}) - ) - icons_mapping = ProductBaseTypeIconMapping( - product_base_types.get("default"), - product_base_types.get("definitions") - ) - - cache.update_data(icons_mapping) - return icons_mapping + ) -> ProductTypeIconMapping: + return self._controller.get_product_type_icons_mapping(project_name) def _get_product_items_by_id(self, project_name, product_ids): product_item_by_id = self._product_item_by_id[project_name] @@ -542,7 +480,7 @@ class ProductsModel: output.update( self._query_product_items_by_ids( - project_name, missing_product_ids + project_name, product_ids=missing_product_ids ) ) return output From cf874e8f0a274b8a38ff8221d7f3d1a567095286 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Sep 2025 11:46:28 +0200 Subject: [PATCH 44/57] use product type icon in scene inventory --- .../ayon_core/tools/sceneinventory/control.py | 15 ++++++++++- .../ayon_core/tools/sceneinventory/model.py | 5 +--- .../tools/sceneinventory/models/containers.py | 25 ++++++++++++++++++- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/control.py b/client/ayon_core/tools/sceneinventory/control.py index 45f76a54ac..606c9e7298 100644 --- a/client/ayon_core/tools/sceneinventory/control.py +++ b/client/ayon_core/tools/sceneinventory/control.py @@ -1,3 +1,5 @@ +from typing import Optional + import ayon_api from ayon_core.lib.events import QueuedEventSystem @@ -6,7 +8,11 @@ from ayon_core.pipeline import ( registered_host, get_current_context, ) -from ayon_core.tools.common_models import HierarchyModel, ProjectsModel +from ayon_core.tools.common_models import ( + HierarchyModel, + ProjectsModel, + ProductTypeIconMapping, +) from .models import SiteSyncModel, ContainersModel @@ -93,6 +99,13 @@ class SceneInventoryController: project_name, None ) + def get_product_type_icons_mapping( + self, project_name: Optional[str] + ) -> ProductTypeIconMapping: + return self._projects_model.get_product_type_icons_mapping( + project_name + ) + # Containers methods def get_containers(self): return self._containers_model.get_containers() diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index 885553acaf..9977acea21 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -214,9 +214,6 @@ class InventoryModel(QtGui.QStandardItemModel): group_icon = qtawesome.icon( "fa.object-group", color=self._default_icon_color ) - product_type_icon = qtawesome.icon( - "fa.folder", color="#0091B2" - ) group_item_font = QtGui.QFont() group_item_font.setBold(True) @@ -303,7 +300,7 @@ class InventoryModel(QtGui.QStandardItemModel): remote_site_progress = "{}%".format( max(progress["remote_site"], 0) * 100 ) - + product_type_icon = get_qt_icon(repre_info.product_type_icon) group_item = QtGui.QStandardItem() group_item.setColumnCount(root_item.columnCount()) group_item.setData(group_name, QtCore.Qt.DisplayRole) diff --git a/client/ayon_core/tools/sceneinventory/models/containers.py b/client/ayon_core/tools/sceneinventory/models/containers.py index f841f87c8e..47f74476de 100644 --- a/client/ayon_core/tools/sceneinventory/models/containers.py +++ b/client/ayon_core/tools/sceneinventory/models/containers.py @@ -126,6 +126,7 @@ class RepresentationInfo: product_id, product_name, product_type, + product_type_icon, product_group, version_id, representation_name, @@ -135,6 +136,7 @@ class RepresentationInfo: self.product_id = product_id self.product_name = product_name self.product_type = product_type + self.product_type_icon = product_type_icon self.product_group = product_group self.version_id = version_id self.representation_name = representation_name @@ -153,7 +155,17 @@ class RepresentationInfo: @classmethod def new_invalid(cls): - return cls(None, None, None, None, None, None, None, None) + return cls( + None, + None, + None, + None, + None, + None, + None, + None, + None, + ) class VersionItem: @@ -229,6 +241,9 @@ class ContainersModel: def get_representation_info_items(self, project_name, representation_ids): output = {} missing_repre_ids = set() + icons_mapping = self._controller.get_product_type_icons_mapping( + project_name + ) for repre_id in representation_ids: try: uuid.UUID(repre_id) @@ -253,6 +268,7 @@ class ContainersModel: "product_id": None, "product_name": None, "product_type": None, + "product_type_icon": None, "product_group": None, "version_id": None, "representation_name": None, @@ -265,10 +281,17 @@ class ContainersModel: kwargs["folder_id"] = folder["id"] kwargs["folder_path"] = folder["path"] if product: + product_type = product["productType"] + product_base_type = product.get("productBaseType") + icon = icons_mapping.get_icon( + product_base_type=product_base_type, + product_type=product_type, + ) group = product["attrib"]["productGroup"] kwargs["product_id"] = product["id"] kwargs["product_name"] = product["name"] kwargs["product_type"] = product["productType"] + kwargs["product_type_icon"] = icon kwargs["product_group"] = group if version: kwargs["version_id"] = version["id"] From 31469bfd9a0b7d29b88ce9162cfb297fc1c4aef5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Sep 2025 11:52:37 +0200 Subject: [PATCH 45/57] remove unused import --- client/ayon_core/tools/loader/control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index c7c182b192..9f159bfb21 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging import uuid -from typing import Optional, Any +from typing import Optional import ayon_api From 7082905e5e25524c16d0bd5a7376eb010d025975 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Sep 2025 12:04:45 +0200 Subject: [PATCH 46/57] use icon color from project anatomy for tasks --- client/ayon_core/tools/common_models/projects.py | 15 ++++++++++++--- client/ayon_core/tools/utils/tasks_widget.py | 3 ++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/common_models/projects.py b/client/ayon_core/tools/common_models/projects.py index 034947de3a..7a5620b033 100644 --- a/client/ayon_core/tools/common_models/projects.py +++ b/client/ayon_core/tools/common_models/projects.py @@ -2,7 +2,7 @@ from __future__ import annotations import contextlib from abc import ABC, abstractmethod -from typing import Dict, Any +from typing import Any, Optional from dataclasses import dataclass import ayon_api @@ -51,7 +51,7 @@ class StatusItem: self.icon: str = icon self.state: str = state - def to_data(self) -> Dict[str, Any]: + def to_data(self) -> dict[str, Any]: return { "name": self.name, "color": self.color, @@ -125,16 +125,24 @@ class TaskTypeItem: icon (str): Icon name in MaterialIcons ("fiber_new"). """ - def __init__(self, name, short, icon): + def __init__( + self, + name: str, + short: str, + icon: str, + color: Optional[str], + ): self.name = name self.short = short self.icon = icon + self.color = color def to_data(self): return { "name": self.name, "short": self.short, "icon": self.icon, + "color": self.color, } @classmethod @@ -147,6 +155,7 @@ class TaskTypeItem: name=task_type_data["name"], short=task_type_data["shortName"], icon=task_type_data["icon"], + color=task_type_data.get("color"), ) diff --git a/client/ayon_core/tools/utils/tasks_widget.py b/client/ayon_core/tools/utils/tasks_widget.py index 744eb6060a..d77ce1e1f4 100644 --- a/client/ayon_core/tools/utils/tasks_widget.py +++ b/client/ayon_core/tools/utils/tasks_widget.py @@ -234,10 +234,11 @@ class TasksQtModel(QtGui.QStandardItemModel): ) icon = None if task_type_item is not None: + color = task_type_item.color or get_default_entity_icon_color() icon = get_qt_icon({ "type": "material-symbols", "name": task_type_item.icon, - "color": get_default_entity_icon_color() + "color": color, }) if icon is None: From 8979fc7608b81db11740738c17c6f581ae1dca1e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Sep 2025 13:54:22 +0200 Subject: [PATCH 47/57] elite status name --- client/ayon_core/tools/utils/delegates.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/utils/delegates.py b/client/ayon_core/tools/utils/delegates.py index 1cc18b5722..059fc1da0e 100644 --- a/client/ayon_core/tools/utils/delegates.py +++ b/client/ayon_core/tools/utils/delegates.py @@ -186,8 +186,15 @@ class StatusDelegate(QtWidgets.QStyledItemDelegate): ) fm = QtGui.QFontMetrics(option.font) if text_rect.width() < fm.width(text): - text = self._get_status_short_name(index) - if text_rect.width() < fm.width(text): + short_text = self._get_status_short_name(index) + if short_text: + text = short_text + + text = fm.elidedText( + text, QtCore.Qt.ElideRight, text_rect.width() + ) + # Allow at least one character + if len(text) < 2: text = "" fg_color = self._get_status_color(index) From 00603d05a4014c1871e742d8dee5c12dae1f7166 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Sep 2025 15:17:27 +0200 Subject: [PATCH 48/57] fill task type color in publisher's tasks enum --- .../tools/publisher/widgets/tasks_model.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/tasks_model.py b/client/ayon_core/tools/publisher/widgets/tasks_model.py index 8bfa81116a..1803e46c5f 100644 --- a/client/ayon_core/tools/publisher/widgets/tasks_model.py +++ b/client/ayon_core/tools/publisher/widgets/tasks_model.py @@ -146,19 +146,19 @@ class TasksModel(QtGui.QStandardItemModel): self._controller.get_current_project_name() ) } - icon_name_by_task_name = {} + type_item_by_task_name = {} for task_items in task_items_by_folder_path.values(): for task_item in task_items: task_name = task_item.name if ( task_name not in new_task_names - or task_name in icon_name_by_task_name + or task_name in type_item_by_task_name ): continue task_type_name = task_item.task_type task_type_item = task_type_items.get(task_type_name) if task_type_item: - icon_name_by_task_name[task_name] = task_type_item.icon + type_item_by_task_name[task_name] = task_type_item for task_name in new_task_names: item = self._items_by_name.get(task_name) @@ -171,13 +171,18 @@ class TasksModel(QtGui.QStandardItemModel): if not task_name: continue - icon_name = icon_name_by_task_name.get(task_name) - icon = None + icon = icon_name = icon_color = None + task_type_item = type_item_by_task_name.get(task_name) + if task_type_item is not None: + icon_name = task_type_item.icon + icon_color = task_type_item.color if icon_name: + if not icon_color: + icon_color = get_default_entity_icon_color() icon = get_qt_icon({ "type": "material-symbols", "name": icon_name, - "color": get_default_entity_icon_color(), + "color": icon_color, }) if icon is None: icon = default_icon From 216c06437fe2ea50d58a56f5d917f6d962818f28 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 15 Sep 2025 19:20:10 +0300 Subject: [PATCH 49/57] MK Docs: Update light/dark modes icons --- mkdocs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 8e4c2663bc..a3b89b5455 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -11,12 +11,12 @@ theme: - media: "(prefers-color-scheme: dark)" scheme: slate toggle: - icon: material/toggle-switch-off-outline + icon: material/weather-sunny name: Switch to light mode - media: "(prefers-color-scheme: light)" scheme: default toggle: - icon: material/toggle-switch + icon: material/weather-night name: Switch to dark mode logo: img/ay-symbol-blackw-full.png favicon: img/favicon.ico From 99c50c3a14a96a5b227a1f27c17191ce8ade0d73 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Tue, 16 Sep 2025 11:54:01 +0300 Subject: [PATCH 50/57] move mkdocs_requirements to root location. --- docs/mkdocs_requirements.txt => mkdocs_requirements.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/mkdocs_requirements.txt => mkdocs_requirements.txt (100%) diff --git a/docs/mkdocs_requirements.txt b/mkdocs_requirements.txt similarity index 100% rename from docs/mkdocs_requirements.txt rename to mkdocs_requirements.txt From eaa6727a9d0ae5289961b5b46d9fdf9245e6c93a Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Tue, 16 Sep 2025 11:56:00 +0300 Subject: [PATCH 51/57] CI: Re-use `deploy_mkdocs` from ynput/ops-repo-automation --- .github/workflows/deploy_mkdocs.yml | 69 ++++------------------------- 1 file changed, 8 insertions(+), 61 deletions(-) diff --git a/.github/workflows/deploy_mkdocs.yml b/.github/workflows/deploy_mkdocs.yml index 2f46bdc6ad..1e02643a3d 100644 --- a/.github/workflows/deploy_mkdocs.yml +++ b/.github/workflows/deploy_mkdocs.yml @@ -1,70 +1,17 @@ name: Deploy MkDocs on: - pull_request: + push: + tags: + - "*" workflow_dispatch: - workflow_call: - inputs: - repo: - type: string - required: true - branch_name: - type: string - required: true - default: "main" - secrets: - token: - required: true - user: - required: true - email: - required: true - -env: - GH_TOKEN: ${{ secrets.token || secrets.YNPUT_BOT_TOKEN }} - GH_USER: ${{ secrets.user || secrets.CI_USER }} - GH_EMAIL: ${{ secrets.email || secrets.CI_EMAIL }} jobs: - verify-repo-secrets: - uses: ynput/ops-repo-automation/.github/workflows/verify_secrets.yml@main + build-mk-docs: + uses: ynput/ops-repo-automation/.github/workflows/deploy_mkdocs.yml@feature/34-add-mk-docs-reusable-workflow with: repo: ${{ github.repository }} secrets: - gh_token: ${{ secrets.token }} - gh_user: ${{ secrets.user }} - gh_email: ${{ secrets.email }} - - deploy: - runs-on: ubuntu-latest - - steps: - - name: Checkout ${{ inputs.branch_name}} - uses: actions/checkout@v4 - with: - ref: ${{ inputs.branch_name}} - fetch-depth: 0 - submodules: true - - - name: πŸ”‘ Set Authentication - run: | - git config --global user.name "${{ secrets.user || secrets.CI_USER }}" - git config --global user.email "${{ secrets.email || secrets.CI_EMAIL }}" - - - name: Get current tag - id: git_tag - uses: devops-actions/action-get-tag@v1.0.3 - with: - default: 1.0.0 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: "3.x" - - - name: Install dependencies - run: | - python3 -m pip install -r ./docs/mkdocs_requirements.txt - - - name: Mike deploy ${{ steps.git_tag.outputs.tag }} - run: mike deploy --update-aliases ${{ steps.git_tag.outputs.tag }} latest + YNPUT_BOT_TOKEN: ${{ secrets.YNPUT_BOT_TOKEN }} + CI_USER: ${{ secrets.CI_USER }} + CI_EMAIL: ${{ secrets.CI_EMAIL }} From a46a9e3143998c93d04824d3aa795e8f53b580d8 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Tue, 16 Sep 2025 13:04:54 +0300 Subject: [PATCH 52/57] CI: Update Deploy MkDocs to use action from develop branch from ynput/ops-repo-automation --- .github/workflows/deploy_mkdocs.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy_mkdocs.yml b/.github/workflows/deploy_mkdocs.yml index 1e02643a3d..deafc7b850 100644 --- a/.github/workflows/deploy_mkdocs.yml +++ b/.github/workflows/deploy_mkdocs.yml @@ -8,7 +8,8 @@ on: jobs: build-mk-docs: - uses: ynput/ops-repo-automation/.github/workflows/deploy_mkdocs.yml@feature/34-add-mk-docs-reusable-workflow + # FIXME: Update @develop to @main after `ops-repo-automation` release. + uses: ynput/ops-repo-automation/.github/workflows/deploy_mkdocs.yml@develop with: repo: ${{ github.repository }} secrets: From e9e9461415e21c4e87729df3a45d011c408179f2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 16 Sep 2025 12:36:51 +0200 Subject: [PATCH 53/57] allow explicit workfile path to be defined --- .../hooks/pre_add_last_workfile_arg.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/hooks/pre_add_last_workfile_arg.py b/client/ayon_core/hooks/pre_add_last_workfile_arg.py index c6afaaa083..752302bb20 100644 --- a/client/ayon_core/hooks/pre_add_last_workfile_arg.py +++ b/client/ayon_core/hooks/pre_add_last_workfile_arg.py @@ -38,18 +38,20 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook): launch_types = {LaunchTypes.local} def execute(self): - if not self.data.get("start_last_workfile"): - self.log.info("It is set to not start last workfile on start.") - return + workfile_path = self.data.get("workfile_path") + if not workfile_path: + if not self.data.get("start_last_workfile"): + self.log.info("It is set to not start last workfile on start.") + return - last_workfile = self.data.get("last_workfile_path") - if not last_workfile: - self.log.warning("Last workfile was not collected.") - return + workfile_path = self.data.get("last_workfile_path") + if not workfile_path: + self.log.warning("Last workfile was not collected.") + return - if not os.path.exists(last_workfile): + if not os.path.exists(workfile_path): self.log.info("Current context does not have any workfile yet.") return # Add path to workfile to arguments - self.launch_context.launch_args.append(last_workfile) + self.launch_context.launch_args.append(workfile_path) From 12bf78b3cb20fb35eb4d1c61441896d13dd994d0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 16 Sep 2025 13:45:35 +0200 Subject: [PATCH 54/57] added workfile selection and actions --- client/ayon_core/pipeline/actions.py | 43 +++++++++++++++ client/ayon_core/tools/launcher/abstract.py | 50 +++++++++++------ client/ayon_core/tools/launcher/control.py | 12 +++- .../tools/launcher/models/actions.py | 55 +++++++++++++++---- .../tools/launcher/models/selection.py | 51 ++++++++++++++--- .../tools/launcher/models/workfiles.py | 5 +- .../tools/launcher/ui/actions_widget.py | 43 +++++++++++---- .../tools/launcher/ui/workfiles_page.py | 13 ++++- 8 files changed, 218 insertions(+), 54 deletions(-) diff --git a/client/ayon_core/pipeline/actions.py b/client/ayon_core/pipeline/actions.py index 860fed5e8b..6892af4252 100644 --- a/client/ayon_core/pipeline/actions.py +++ b/client/ayon_core/pipeline/actions.py @@ -37,16 +37,19 @@ class LauncherActionSelection: project_name, folder_id, task_id, + workfile_id, folder_path=None, task_name=None, project_entity=None, folder_entity=None, task_entity=None, + workfile_entity=None, project_settings=None, ): self._project_name = project_name self._folder_id = folder_id self._task_id = task_id + self._workfile_id = workfile_id self._folder_path = folder_path self._task_name = task_name @@ -54,6 +57,7 @@ class LauncherActionSelection: self._project_entity = project_entity self._folder_entity = folder_entity self._task_entity = task_entity + self._workfile_entity = workfile_entity self._project_settings = project_settings @@ -213,6 +217,15 @@ class LauncherActionSelection: self._task_name = self.task_entity["name"] return self._task_name + def get_workfile_id(self): + """Selected workfile id. + + Returns: + Union[str, None]: Selected workfile id. + + """ + return self._workfile_id + def get_project_entity(self): """Project entity for the selection. @@ -259,6 +272,24 @@ class LauncherActionSelection: ) return self._task_entity + def get_workfile_entity(self): + """Workfile entity for the selection. + + Returns: + Union[dict[str, Any], None]: Workfile entity. + + """ + if ( + self._project_name is None + or self._workfile_id is None + ): + return None + if self._workfile_entity is None: + self._workfile_entity = ayon_api.get_workfile_info_by_id( + self._project_name, self._workfile_id + ) + return self._workfile_entity + def get_project_settings(self): """Project settings for the selection. @@ -305,15 +336,27 @@ class LauncherActionSelection: """ return self._task_id is not None + @property + def is_workfile_selected(self): + """Return whether a task is selected. + + Returns: + bool: Whether a task is selected. + + """ + return self._workfile_id is not None + project_name = property(get_project_name) folder_id = property(get_folder_id) task_id = property(get_task_id) + workfile_id = property(get_workfile_id) folder_path = property(get_folder_path) task_name = property(get_task_name) project_entity = property(get_project_entity) folder_entity = property(get_folder_entity) task_entity = property(get_task_entity) + workfile_entity = property(get_workfile_entity) class LauncherAction(object): diff --git a/client/ayon_core/tools/launcher/abstract.py b/client/ayon_core/tools/launcher/abstract.py index f312504d31..a94500116b 100644 --- a/client/ayon_core/tools/launcher/abstract.py +++ b/client/ayon_core/tools/launcher/abstract.py @@ -21,6 +21,7 @@ class WebactionContext: project_name: str folder_id: str task_id: str + workfile_id: str addon_name: str addon_version: str @@ -34,7 +35,7 @@ class ActionItem: identifier (str): Unique identifier of action item. order (int): Action ordering. label (str): Action label. - variant_label (Union[str, None]): Variant label, full label is + variant_label (Optional[str]): Variant label, full label is concatenated with space. Actions are grouped under single action if it has same 'label' and have set 'variant_label'. full_label (str): Full label, if not set it is generated @@ -59,6 +60,7 @@ class ActionItem: @dataclass class WorkfileItem: + workfile_id: str filename: str exists: bool icon: Optional[str] @@ -103,7 +105,7 @@ class AbstractLauncherBackend(AbstractLauncherCommon): """Project settings for current project. Args: - project_name (Union[str, None]): Project name. + project_name (Optional[str]): Project name. Returns: dict[str, Any]: Project settings. @@ -267,7 +269,7 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): """Selected project name. Returns: - Union[str, None]: Selected project name. + Optional[str]: Selected project name. """ pass @@ -277,7 +279,7 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): """Selected folder id. Returns: - Union[str, None]: Selected folder id. + Optional[str]: Selected folder id. """ pass @@ -287,7 +289,7 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): """Selected task id. Returns: - Union[str, None]: Selected task id. + Optional[str]: Selected task id. """ pass @@ -297,7 +299,7 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): """Selected task name. Returns: - Union[str, None]: Selected task name. + Optional[str]: Selected task name. """ pass @@ -315,7 +317,7 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): } Returns: - dict[str, Union[str, None]]: Selected context. + dict[str, Optional[str]]: Selected context. """ pass @@ -325,7 +327,7 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): """Change selected folder. Args: - project_name (Union[str, None]): Project nameor None if no project + project_name (Optional[str]): Project nameor None if no project is selected. """ @@ -336,7 +338,7 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): """Change selected folder. Args: - folder_id (Union[str, None]): Folder id or None if no folder + folder_id (Optional[str]): Folder id or None if no folder is selected. """ @@ -349,14 +351,24 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): """Change selected task. Args: - task_id (Union[str, None]): Task id or None if no task + task_id (Optional[str]): Task id or None if no task is selected. - task_name (Union[str, None]): Task name or None if no task + task_name (Optional[str]): Task name or None if no task is selected. """ pass + @abstractmethod + def set_selected_workfile(self, workfile_id: Optional[str]): + """Change selected workfile. + + Args: + workfile_id (Optional[str]): Workfile id or None. + + """ + pass + # Actions @abstractmethod def get_action_items( @@ -364,13 +376,15 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): project_name: Optional[str], folder_id: Optional[str], task_id: Optional[str], + workfile_id: Optional[str], ) -> list[ActionItem]: """Get action items for given context. Args: - project_name (Union[str, None]): Project name. - folder_id (Union[str, None]): Folder id. - task_id (Union[str, None]): Task id. + project_name (Optional[str]): Project name. + folder_id (Optional[str]): Folder id. + task_id (Optional[str]): Task id. + workfile_id (Optional[str]): Workfile id. Returns: list[ActionItem]: List of action items that should be shown @@ -386,14 +400,16 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): project_name: Optional[str], folder_id: Optional[str], task_id: Optional[str], + workfile_id: Optional[str], ): """Trigger action on given context. Args: action_id (str): Action identifier. - project_name (Union[str, None]): Project name. - folder_id (Union[str, None]): Folder id. - task_id (Union[str, None]): Task id. + project_name (Optional[str]): Project name. + folder_id (Optional[str]): Folder id. + task_id (Optional[str]): Task id. + workfile_id (Optional[str]): Task id. """ pass diff --git a/client/ayon_core/tools/launcher/control.py b/client/ayon_core/tools/launcher/control.py index 66afebc247..85b362f9d7 100644 --- a/client/ayon_core/tools/launcher/control.py +++ b/client/ayon_core/tools/launcher/control.py @@ -144,6 +144,9 @@ class BaseLauncherController( def set_selected_task(self, task_id, task_name): self._selection_model.set_selected_task(task_id, task_name) + def set_selected_workfile(self, workfile_id): + self._selection_model.set_selected_workfile(workfile_id) + def get_selected_context(self): return { "project_name": self.get_selected_project_name(), @@ -164,9 +167,12 @@ class BaseLauncherController( ) # Actions - def get_action_items(self, project_name, folder_id, task_id): + def get_action_items( + self, project_name, folder_id, task_id, workfile_id + ): return self._actions_model.get_action_items( - project_name, folder_id, task_id) + project_name, folder_id, task_id, workfile_id + ) def trigger_action( self, @@ -174,12 +180,14 @@ class BaseLauncherController( project_name, folder_id, task_id, + workfile_id, ): self._actions_model.trigger_action( identifier, project_name, folder_id, task_id, + workfile_id, ) def trigger_webaction(self, context, action_label, form_data=None): diff --git a/client/ayon_core/tools/launcher/models/actions.py b/client/ayon_core/tools/launcher/models/actions.py index 5f888effb5..709ae2e9a8 100644 --- a/client/ayon_core/tools/launcher/models/actions.py +++ b/client/ayon_core/tools/launcher/models/actions.py @@ -128,19 +128,28 @@ class ActionsModel: self._get_action_objects() self._controller.emit_event("actions.refresh.finished") - def get_action_items(self, project_name, folder_id, task_id): + def get_action_items( + self, + project_name: Optional[str], + folder_id: Optional[str], + task_id: Optional[str], + workfile_id: Optional[str], + ) -> list[ActionItem]: """Get actions for project. Args: - project_name (Union[str, None]): Project name. - folder_id (Union[str, None]): Folder id. - task_id (Union[str, None]): Task id. + project_name (Optional[str]): Project name. + folder_id (Optional[str]): Folder id. + task_id (Optional[str]): Task id. + workfile_id (Optional[str]): Workfile id. Returns: list[ActionItem]: List of actions. """ - selection = self._prepare_selection(project_name, folder_id, task_id) + selection = self._prepare_selection( + project_name, folder_id, task_id, workfile_id + ) output = [] action_items = self._get_action_items(project_name) for identifier, action in self._get_action_objects().items(): @@ -156,8 +165,11 @@ class ActionsModel: project_name, folder_id, task_id, + workfile_id, ): - selection = self._prepare_selection(project_name, folder_id, task_id) + selection = self._prepare_selection( + project_name, folder_id, task_id, workfile_id + ) failed = False error_message = None action_label = identifier @@ -199,11 +211,15 @@ class ActionsModel: identifier = context.identifier folder_id = context.folder_id task_id = context.task_id + workfile_id = context.workfile_id project_name = context.project_name addon_name = context.addon_name addon_version = context.addon_version - if task_id: + if workfile_id: + entity_type = "workfile" + entity_ids.append(workfile_id) + elif task_id: entity_type = "task" entity_ids.append(task_id) elif folder_id: @@ -269,6 +285,7 @@ class ActionsModel: "project_name": project_name, "folder_id": folder_id, "task_id": task_id, + "workfile_id": workfile_id, "addon_name": addon_name, "addon_version": addon_version, }) @@ -279,7 +296,10 @@ class ActionsModel: def get_action_config_values(self, context: WebactionContext): selection = self._prepare_selection( - context.project_name, context.folder_id, context.task_id + context.project_name, + context.folder_id, + context.task_id, + context.workfile_id, ) if not selection.is_project_selected: return {} @@ -306,7 +326,10 @@ class ActionsModel: def set_action_config_values(self, context, values): selection = self._prepare_selection( - context.project_name, context.folder_id, context.task_id + context.project_name, + context.folder_id, + context.task_id, + context.workfile_id, ) if not selection.is_project_selected: return {} @@ -330,7 +353,9 @@ class ActionsModel: exc_info=True ) - def _prepare_selection(self, project_name, folder_id, task_id): + def _prepare_selection( + self, project_name, folder_id, task_id, workfile_id + ): project_entity = None if project_name: project_entity = self._controller.get_project_entity(project_name) @@ -339,6 +364,7 @@ class ActionsModel: project_name, folder_id, task_id, + workfile_id, project_entity=project_entity, project_settings=project_settings, ) @@ -347,7 +373,12 @@ class ActionsModel: entity_type = None entity_id = None entity_subtypes = [] - if selection.is_task_selected: + if selection.is_workfile_selected: + entity_type = "workfile" + entity_id = selection.workfile_id + entity_subtypes = [] + + elif selection.is_task_selected: entity_type = "task" entity_id = selection.task_entity["id"] entity_subtypes = [selection.task_entity["taskType"]] @@ -392,7 +423,7 @@ class ActionsModel: try: # 'variant' query is supported since AYON backend 1.10.4 - query = urlencode({"variant": self._variant}) + query = urlencode({"variant": self._variant, "mode": "all"}) response = ayon_api.post( f"actions/list?{query}", **request_data ) diff --git a/client/ayon_core/tools/launcher/models/selection.py b/client/ayon_core/tools/launcher/models/selection.py index b156d2084c..9d5ad47d89 100644 --- a/client/ayon_core/tools/launcher/models/selection.py +++ b/client/ayon_core/tools/launcher/models/selection.py @@ -1,26 +1,37 @@ -class LauncherSelectionModel(object): +from __future__ import annotations + +import typing +from typing import Optional + +if typing.TYPE_CHECKING: + from ayon_core.tools.launcher.abstract import AbstractLauncherBackend + + +class LauncherSelectionModel: """Model handling selection changes. Triggering events: - "selection.project.changed" - "selection.folder.changed" - "selection.task.changed" + - "selection.workfile.changed" """ event_source = "launcher.selection.model" - def __init__(self, controller): + def __init__(self, controller: AbstractLauncherBackend) -> None: self._controller = controller self._project_name = None self._folder_id = None self._task_name = None self._task_id = None + self._workfile_id = None - def get_selected_project_name(self): + def get_selected_project_name(self) -> Optional[str]: return self._project_name - def set_selected_project(self, project_name): + def set_selected_project(self, project_name: Optional[str]) -> None: if project_name == self._project_name: return @@ -31,10 +42,10 @@ class LauncherSelectionModel(object): self.event_source ) - def get_selected_folder_id(self): + def get_selected_folder_id(self) -> Optional[str]: return self._folder_id - def set_selected_folder(self, folder_id): + def set_selected_folder(self, folder_id: Optional[str]) -> None: if folder_id == self._folder_id: return @@ -48,13 +59,15 @@ class LauncherSelectionModel(object): self.event_source ) - def get_selected_task_name(self): + def get_selected_task_name(self) -> Optional[str]: return self._task_name - def get_selected_task_id(self): + def get_selected_task_id(self) -> Optional[str]: return self._task_id - def set_selected_task(self, task_id, task_name): + def set_selected_task( + self, task_id: Optional[str], task_name: Optional[str] + ) -> None: if task_id == self._task_id: return @@ -70,3 +83,23 @@ class LauncherSelectionModel(object): }, self.event_source ) + + def get_selected_workfile(self) -> Optional[str]: + return self._workfile_id + + def set_selected_workfile(self, workfile_id: Optional[str]) -> None: + if workfile_id == self._workfile_id: + return + + self._workfile_id = workfile_id + self._controller.emit_event( + "selection.workfile.changed", + { + "project_name": self._project_name, + "folder_id": self._folder_id, + "task_name": self._task_name, + "task_id": self._task_id, + "workfile_id": workfile_id, + }, + self.event_source + ) diff --git a/client/ayon_core/tools/launcher/models/workfiles.py b/client/ayon_core/tools/launcher/models/workfiles.py index 2ba15c1800..649a87353c 100644 --- a/client/ayon_core/tools/launcher/models/workfiles.py +++ b/client/ayon_core/tools/launcher/models/workfiles.py @@ -44,7 +44,7 @@ class WorkfilesModel: anatomy = Anatomy(project_name, project_entity=project_entity) items = [] for workfile_entity in ayon_api.get_workfiles_info( - project_name, task_ids={task_id}, fields={"path", "data"} + project_name, task_ids={task_id}, fields={"id", "path", "data"} ): rootless_path = workfile_entity["path"] exists = False @@ -61,7 +61,8 @@ class WorkfilesModel: version = workfile_data.get("version") items.append(WorkfileItem( - os.path.basename(rootless_path), + workfile_id=workfile_entity["id"], + filename=os.path.basename(rootless_path), exists=exists, icon=self._get_host_icon(host_name), version=version, diff --git a/client/ayon_core/tools/launcher/ui/actions_widget.py b/client/ayon_core/tools/launcher/ui/actions_widget.py index 51cb8e73bc..67a8bca787 100644 --- a/client/ayon_core/tools/launcher/ui/actions_widget.py +++ b/client/ayon_core/tools/launcher/ui/actions_widget.py @@ -136,6 +136,10 @@ class ActionsQtModel(QtGui.QStandardItemModel): "selection.task.changed", self._on_selection_task_changed, ) + controller.register_event_callback( + "selection.workfile.changed", + self._on_selection_workfile_changed, + ) self._controller = controller @@ -146,6 +150,7 @@ class ActionsQtModel(QtGui.QStandardItemModel): self._selected_project_name = None self._selected_folder_id = None self._selected_task_id = None + self._selected_workfile_id = None def get_selected_project_name(self): return self._selected_project_name @@ -156,6 +161,9 @@ class ActionsQtModel(QtGui.QStandardItemModel): def get_selected_task_id(self): return self._selected_task_id + def get_selected_workfile_id(self): + return self._selected_workfile_id + def get_group_items(self, action_id): return self._groups_by_id[action_id] @@ -194,6 +202,7 @@ class ActionsQtModel(QtGui.QStandardItemModel): self._selected_project_name, self._selected_folder_id, self._selected_task_id, + self._selected_workfile_id, ) if not items: self._clear_items() @@ -286,18 +295,28 @@ class ActionsQtModel(QtGui.QStandardItemModel): self._selected_project_name = event["project_name"] self._selected_folder_id = None self._selected_task_id = None + self._selected_workfile_id = None self.refresh() def _on_selection_folder_changed(self, event): self._selected_project_name = event["project_name"] self._selected_folder_id = event["folder_id"] self._selected_task_id = None + self._selected_workfile_id = None self.refresh() def _on_selection_task_changed(self, event): self._selected_project_name = event["project_name"] self._selected_folder_id = event["folder_id"] self._selected_task_id = event["task_id"] + self._selected_workfile_id = None + self.refresh() + + def _on_selection_workfile_changed(self, event): + self._selected_project_name = event["project_name"] + self._selected_folder_id = event["folder_id"] + self._selected_task_id = event["task_id"] + self._selected_workfile_id = event["workfile_id"] self.refresh() @@ -578,9 +597,6 @@ class ActionMenuPopup(QtWidgets.QWidget): if not index or not index.isValid(): return - if not index.data(ACTION_HAS_CONFIGS_ROLE): - return - action_id = index.data(ACTION_ID_ROLE) self.action_triggered.emit(action_id) @@ -970,6 +986,7 @@ class ActionsWidget(QtWidgets.QWidget): event["project_name"], event["folder_id"], event["task_id"], + event["workfile_id"], event["addon_name"], event["addon_version"], ), @@ -1050,24 +1067,26 @@ class ActionsWidget(QtWidgets.QWidget): project_name = self._model.get_selected_project_name() folder_id = self._model.get_selected_folder_id() task_id = self._model.get_selected_task_id() + workfile_id = self._model.get_selected_workfile_id() action_item = self._model.get_action_item_by_id(action_id) if action_item.action_type == "webaction": action_item = self._model.get_action_item_by_id(action_id) context = WebactionContext( - action_id, - project_name, - folder_id, - task_id, - action_item.addon_name, - action_item.addon_version + identifier=action_id, + project_name=project_name, + folder_id=folder_id, + task_id=task_id, + workfile_id=workfile_id, + addon_name=action_item.addon_name, + addon_version=action_item.addon_version, ) self._controller.trigger_webaction( context, action_item.full_label ) else: self._controller.trigger_action( - action_id, project_name, folder_id, task_id + action_id, project_name, folder_id, task_id, workfile_id ) if index is None: @@ -1087,11 +1106,13 @@ class ActionsWidget(QtWidgets.QWidget): project_name = self._model.get_selected_project_name() folder_id = self._model.get_selected_folder_id() task_id = self._model.get_selected_task_id() + workfile_id = self._model.get_selected_workfile_id() context = WebactionContext( - action_id, + identifier=action_id, project_name=project_name, folder_id=folder_id, task_id=task_id, + workfile_id=workfile_id, addon_name=action_item.addon_name, addon_version=action_item.addon_version, ) diff --git a/client/ayon_core/tools/launcher/ui/workfiles_page.py b/client/ayon_core/tools/launcher/ui/workfiles_page.py index 0401183080..1ea223031e 100644 --- a/client/ayon_core/tools/launcher/ui/workfiles_page.py +++ b/client/ayon_core/tools/launcher/ui/workfiles_page.py @@ -7,6 +7,7 @@ from ayon_core.tools.utils import get_qt_icon from ayon_core.tools.launcher.abstract import AbstractLauncherFrontEnd VERSION_ROLE = QtCore.Qt.UserRole + 1 +WORKFILE_ID_ROLE = QtCore.Qt.UserRole + 2 class WorkfilesModel(QtGui.QStandardItemModel): @@ -53,9 +54,10 @@ class WorkfilesModel(QtGui.QStandardItemModel): item = QtGui.QStandardItem(workfile_item.filename) item.setData(icon, QtCore.Qt.DecorationRole) item.setData(workfile_item.version, VERSION_ROLE) + item.setData(workfile_item.workfile_id, WORKFILE_ID_ROLE) flags = QtCore.Qt.NoItemFlags if workfile_item.exists: - flags = QtCore.Qt.ItemIsEnabled + flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable item.setFlags(flags) new_items.append(item) @@ -150,6 +152,9 @@ class WorkfilesPage(QtWidgets.QWidget): layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(workfiles_view, 1) + workfiles_view.selectionModel().selectionChanged.connect( + self._on_selection_changed + ) workfiles_model.refreshed.connect(self._on_refresh) self._controller = controller @@ -162,3 +167,9 @@ class WorkfilesPage(QtWidgets.QWidget): def _on_refresh(self) -> None: self._workfiles_proxy.sort(0, QtCore.Qt.DescendingOrder) + + def _on_selection_changed(self, selected, _deselected) -> None: + workfile_id = None + for index in selected.indexes(): + workfile_id = index.data(WORKFILE_ID_ROLE) + self._controller.set_selected_workfile(workfile_id) From ff269b7bd056de13b42db6d2b2b279244244dd0a Mon Sep 17 00:00:00 2001 From: Ynbot Date: Mon, 22 Sep 2025 12:23:41 +0000 Subject: [PATCH 55/57] [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 9ca5e1bc30..9224326169 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.6.0+dev" +__version__ = "1.6.1" diff --git a/package.py b/package.py index e430524dd5..91e56f0838 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.6.0+dev" +version = "1.6.1" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 9a62a408ba..6ba1dcb8f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.6.0+dev" +version = "1.6.1" description = "" authors = ["Ynput Team "] readme = "README.md" From c0e6772097040224e79e01c88f126c3f6a0a5f7d Mon Sep 17 00:00:00 2001 From: Ynbot Date: Mon, 22 Sep 2025 12:24:14 +0000 Subject: [PATCH 56/57] [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 9224326169..c7a72e0b43 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.6.1" +__version__ = "1.6.1+dev" diff --git a/package.py b/package.py index 91e56f0838..f6853d8816 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.6.1" +version = "1.6.1+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 6ba1dcb8f3..18f2047a92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.6.1" +version = "1.6.1+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From c5037123481fa9949f543cb6f6a14a5e8c456656 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 22 Sep 2025 12:25:06 +0000 Subject: [PATCH 57/57] 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 24c2b568b3..6b75179e7b 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.6.1 - 1.6.0 - 1.5.3 - 1.5.2