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 01/24] 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 02/24] 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 03/24] 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 04/24] 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 05/24] 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 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 06/24] 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 07/24] 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 08/24] [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 09/24] [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 10/24] 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 From eabd6b601f3ce8e980a053714548d9f9d3466bc5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 22 Sep 2025 19:11:02 +0200 Subject: [PATCH 11/24] small changes or logic order --- client/ayon_core/pipeline/load/utils.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 836fc5e096..de79ad4d52 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -1042,13 +1042,13 @@ def filter_containers(containers, project_name): hero=True, fields={"id", "productId", "version"} ) - verisons_by_id = {} + versions_by_id = {} versions_by_product_id = collections.defaultdict(list) hero_version_ids = set() for version_entity in version_entities: version_id = version_entity["id"] # Store versions by their ids - verisons_by_id[version_id] = version_entity + versions_by_id[version_id] = version_entity # There's no need to query products for hero versions # - they are considered as latest? if version_entity["version"] < 0: @@ -1083,24 +1083,23 @@ def filter_containers(containers, project_name): repre_entity = repre_entities_by_id.get(repre_id) if not repre_entity: - log.debug(( - "Container '{}' has an invalid representation." + log.debug( + f"Container '{container_name}' has an invalid representation." " It is missing in the database." - ).format(container_name)) + ) not_found_containers.append(container) continue version_id = repre_entity["versionId"] - if version_id in outdated_version_ids: - outdated_containers.append(container) - - elif version_id not in verisons_by_id: - log.debug(( - "Representation on container '{}' has an invalid version." - " It is missing in the database." - ).format(container_name)) + if version_id not in versions_by_id: + log.debug( + f"Representation on container '{container_name}' has an" + " invalid version. It is missing in the database." + ) not_found_containers.append(container) + elif version_id in outdated_version_ids: + outdated_containers.append(container) else: uptodate_containers.append(container) From 0748d659d71291f5ec326086201891640c4ec265 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 22 Sep 2025 19:11:33 +0200 Subject: [PATCH 12/24] do not consider locked containers in 'get_outdated_containers' as outdated --- client/ayon_core/pipeline/load/utils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index de79ad4d52..7dab889ec5 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -964,7 +964,12 @@ def get_outdated_containers(host=None, project_name=None): containers = host.get_containers() else: containers = host.ls() - return filter_containers(containers, project_name).outdated + outdated_containers = [] + for container in filter_containers(containers, project_name).outdated: + if container.get("locked_version") is True: + continue + outdated_containers.append(container) + return outdated_containers def _is_valid_representation_id(repre_id: Any) -> bool: From ace6a84f5e9759f83a7f915fafb0f5ee1830b4a1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 22 Sep 2025 19:12:03 +0200 Subject: [PATCH 13/24] look for locked version in container --- client/ayon_core/tools/sceneinventory/models/containers.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/models/containers.py b/client/ayon_core/tools/sceneinventory/models/containers.py index 47f74476de..0e19f381cd 100644 --- a/client/ayon_core/tools/sceneinventory/models/containers.py +++ b/client/ayon_core/tools/sceneinventory/models/containers.py @@ -95,7 +95,8 @@ class ContainerItem: namespace, object_name, item_id, - project_name + project_name, + version_locked, ): self.representation_id = representation_id self.loader_name = loader_name @@ -103,6 +104,7 @@ class ContainerItem: self.namespace = namespace self.item_id = item_id self.project_name = project_name + self.version_locked = version_locked @classmethod def from_container_data(cls, current_project_name, container): @@ -114,7 +116,8 @@ class ContainerItem: item_id=uuid.uuid4().hex, project_name=container.get( "project_name", current_project_name - ) + ), + version_locked=container.get("version_locked", False), ) From 1f41e03fe00c57fcb341d6c82677184e8e80a1a4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 22 Sep 2025 19:12:26 +0200 Subject: [PATCH 14/24] store the information to the model item --- client/ayon_core/tools/sceneinventory/model.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index 9977acea21..27211165bf 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -37,6 +37,7 @@ REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 23 # containers inbetween refresh. ITEM_UNIQUE_NAME_ROLE = QtCore.Qt.UserRole + 24 PROJECT_NAME_ROLE = QtCore.Qt.UserRole + 25 +CONTAINER_VERSION_LOCKED_ROLE = QtCore.Qt.UserRole + 26 class InventoryModel(QtGui.QStandardItemModel): @@ -291,6 +292,10 @@ class InventoryModel(QtGui.QStandardItemModel): item.setData(container_item.object_name, OBJECT_NAME_ROLE) item.setData(True, IS_CONTAINER_ITEM_ROLE) item.setData(unique_name, ITEM_UNIQUE_NAME_ROLE) + item.setData( + container_item.version_locked, + CONTAINER_VERSION_LOCKED_ROLE + ) container_model_items.append(item) progress = progress_by_id[repre_id] From 2fbb6c279be98a6d7c8c110db53d0c53a8f51b04 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 22 Sep 2025 19:13:11 +0200 Subject: [PATCH 15/24] allow more options for icons --- client/ayon_core/tools/sceneinventory/view.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index fdd1bdbe75..ead10f9e62 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -524,7 +524,15 @@ class SceneInventoryView(QtWidgets.QTreeView): submenu = QtWidgets.QMenu("Actions", self) for action in custom_actions: color = action.color or DEFAULT_COLOR - icon = qtawesome.icon("fa.%s" % action.icon, color=color) + icon_def = action.icon + if not isinstance(action.icon, dict): + icon_def = { + "type": "awesome-font", + "name": icon_def, + "color": color, + } + icon = get_qt_icon(icon_def) + # icon = qtawesome.icon("fa.%s" % action.icon, color=color) action_item = QtWidgets.QAction(icon, action.label, submenu) action_item.triggered.connect( partial( From d96e8087ec63676be751b8618d79c3ea7a5c2a03 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 22 Sep 2025 19:13:22 +0200 Subject: [PATCH 16/24] draw a lock next to version if is locked --- .../tools/sceneinventory/delegates.py | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/delegates.py b/client/ayon_core/tools/sceneinventory/delegates.py index 6f91587613..9bc4294fda 100644 --- a/client/ayon_core/tools/sceneinventory/delegates.py +++ b/client/ayon_core/tools/sceneinventory/delegates.py @@ -1,10 +1,14 @@ from qtpy import QtWidgets, QtCore, QtGui -from .model import VERSION_LABEL_ROLE +from ayon_core.tools.utils import get_qt_icon + +from .model import VERSION_LABEL_ROLE, CONTAINER_VERSION_LOCKED_ROLE class VersionDelegate(QtWidgets.QStyledItemDelegate): """A delegate that display version integer formatted as version string.""" + _locked_icon = None + def paint(self, painter, option, index): fg_color = index.data(QtCore.Qt.ForegroundRole) if fg_color: @@ -45,10 +49,35 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): QtWidgets.QStyle.PM_FocusFrameHMargin, option, option.widget ) + 1 + text_rect_f = text_rect.adjusted( + text_margin, 0, - text_margin, 0 + ) + painter.drawText( - text_rect.adjusted(text_margin, 0, - text_margin, 0), + text_rect_f, option.displayAlignment, text ) + if index.data(CONTAINER_VERSION_LOCKED_ROLE) is True: + icon = self._get_locked_icon() + size = max(text_rect_f.height() // 2, 16) + margin = (text_rect_f.height() - size) // 2 + + icon_rect = QtCore.QRect( + text_rect_f.right() - size, + text_rect_f.top() + margin, + size, + size + ) + icon.paint(painter, icon_rect) painter.restore() + + def _get_locked_icon(cls): + if cls._locked_icon is None: + cls._locked_icon = get_qt_icon({ + "type": "material-symbols", + "name": "lock", + "color": "white", + }) + return cls._locked_icon From bb64f3c2a5e9e2f36111d722b4e677334c388e26 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 22 Sep 2025 19:13:41 +0200 Subject: [PATCH 17/24] make sure 'data_changed' is triggered --- client/ayon_core/tools/sceneinventory/view.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index ead10f9e62..b1e378f343 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -17,6 +17,7 @@ from ayon_core.tools.utils.lib import ( format_version, preserve_expanded_rows, preserve_selection, + get_qt_icon, ) from ayon_core.tools.utils.delegates import StatusDelegate @@ -46,7 +47,7 @@ class SceneInventoryView(QtWidgets.QTreeView): hierarchy_view_changed = QtCore.Signal(bool) def __init__(self, controller, parent): - super(SceneInventoryView, self).__init__(parent=parent) + super().__init__(parent=parent) # view settings self.setIndentation(12) @@ -623,17 +624,20 @@ class SceneInventoryView(QtWidgets.QTreeView): containers_by_id = self._controller.get_containers_by_item_ids( item_ids ) - result = action.process(list(containers_by_id.values())) - if result: - self.data_changed.emit() + try: + result = action.process(list(containers_by_id.values())) + if not result: + pass - if isinstance(result, (list, set)): + elif isinstance(result, (list, set)): self._select_items_by_action(result) - if isinstance(result, dict): + elif isinstance(result, dict): self._select_items_by_action( result["objectNames"], result["options"] ) + finally: + self.data_changed.emit() def _select_items_by_action(self, object_names, options=None): """Select view items by the result of action From b6feefa19a7ebc50963d09637f57adf79d8fedbe Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:54:29 +0200 Subject: [PATCH 18/24] use Logger as log attribute for loader plugin --- client/ayon_core/pipeline/load/plugins.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 48e860e834..ed963110c6 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -2,10 +2,10 @@ from __future__ import annotations from abc import abstractmethod -import logging import os from typing import Any, Optional, Type +from ayon_core.lib import Logger from ayon_core.pipeline.plugin_discover import ( deregister_plugin, deregister_plugin_path, @@ -31,8 +31,7 @@ class LoaderPlugin(list): options = [] - log = logging.getLogger("ProductLoader") - log.propagate = True + log = Logger.get_logger("ProductLoader") @classmethod def apply_settings(cls, project_settings): From 2656e0c7d860a1468900c5bf2a528e39a11fbe90 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Sep 2025 12:11:41 +0200 Subject: [PATCH 19/24] remove commented line Co-authored-by: Roy Nieterau --- client/ayon_core/tools/sceneinventory/view.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index b1e378f343..6a825a2ca4 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -533,7 +533,6 @@ class SceneInventoryView(QtWidgets.QTreeView): "color": color, } icon = get_qt_icon(icon_def) - # icon = qtawesome.icon("fa.%s" % action.icon, color=color) action_item = QtWidgets.QAction(icon, action.label, submenu) action_item.triggered.connect( partial( From 0122686522aad4dfa8baf144bb8e06bc83fcf7be Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Sep 2025 12:39:32 +0200 Subject: [PATCH 20/24] allow to ignore locked versions --- client/ayon_core/pipeline/load/utils.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 7dab889ec5..6b751dec30 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -942,7 +942,11 @@ def any_outdated_containers(host=None, project_name=None): return False -def get_outdated_containers(host=None, project_name=None): +def get_outdated_containers( + host=None, + project_name=None, + ignore_locked_versions: bool = False, +): """Collect outdated containers from host scene. Currently registered host and project in global session are used if @@ -951,6 +955,8 @@ def get_outdated_containers(host=None, project_name=None): Args: host (ModuleType): Host implementation with 'ls' function available. project_name (str): Name of project in which context we are. + ignore_locked_versions (bool): Locked versions are ignored. + """ from ayon_core.pipeline import registered_host, get_current_project_name @@ -964,9 +970,13 @@ def get_outdated_containers(host=None, project_name=None): containers = host.get_containers() else: containers = host.ls() + outdated_containers = [] for container in filter_containers(containers, project_name).outdated: - if container.get("locked_version") is True: + if ( + not ignore_locked_versions + and container.get("locked_version") is True + ): continue outdated_containers.append(container) return outdated_containers From 88db0b46e83bbddb1a112af2947715172986d45a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Sep 2025 12:39:48 +0200 Subject: [PATCH 21/24] added typehints --- client/ayon_core/pipeline/load/utils.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 6b751dec30..0cfe004572 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -9,7 +9,7 @@ from typing import Optional, Union, Any import ayon_api -from ayon_core.host import ILoadHost +from ayon_core.host import ILoadHost, AbstractHost from ayon_core.lib import ( StringTemplate, TemplateUnsolved, @@ -943,8 +943,8 @@ def any_outdated_containers(host=None, project_name=None): def get_outdated_containers( - host=None, - project_name=None, + host: Optional[AbstractHost] = None, + project_name: Optional[str] = None, ignore_locked_versions: bool = False, ): """Collect outdated containers from host scene. @@ -953,8 +953,8 @@ def get_outdated_containers( arguments are not passed. Args: - host (ModuleType): Host implementation with 'ls' function available. - project_name (str): Name of project in which context we are. + host (Optional[AbstractHost]): Host implementation. + project_name (Optional[str]): Name of project in which context we are. ignore_locked_versions (bool): Locked versions are ignored. """ @@ -1008,8 +1008,8 @@ def filter_containers(containers, project_name): Returns: ContainersFilterResult: Named tuple with 'latest', 'outdated', 'invalid' and 'not_found' containers. - """ + """ # Make sure containers is list that won't change containers = list(containers) From 740f0276e25b3ec9130ad3346c04d3cb4bda54c9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Sep 2025 12:39:59 +0200 Subject: [PATCH 22/24] add a todo to 'filter_containers' --- client/ayon_core/pipeline/load/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 0cfe004572..a111444d48 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -1000,6 +1000,9 @@ def filter_containers(containers, project_name): 'invalid' are invalid containers (invalid content) and 'not_found' has some missing entity in database. + Todos: + Respect 'project_name' on containers if is available. + Args: containers (Iterable[dict]): List of containers referenced into scene. project_name (str): Name of project in which context shoud look for From 3a524844609d375656e5eed3e2fe71a9ce565203 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Sep 2025 12:42:06 +0200 Subject: [PATCH 23/24] revert back output handling --- client/ayon_core/tools/sceneinventory/view.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index 6a825a2ca4..22bc170230 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -623,20 +623,17 @@ class SceneInventoryView(QtWidgets.QTreeView): containers_by_id = self._controller.get_containers_by_item_ids( item_ids ) - try: - result = action.process(list(containers_by_id.values())) - if not result: - pass + result = action.process(list(containers_by_id.values())) + if result: + self.data_changed.emit() - elif isinstance(result, (list, set)): + if isinstance(result, (list, set)): self._select_items_by_action(result) elif isinstance(result, dict): self._select_items_by_action( result["objectNames"], result["options"] ) - finally: - self.data_changed.emit() def _select_items_by_action(self, object_names, options=None): """Select view items by the result of action From 0b6e171558ee3846b72f2e182de93a94d41aeaa8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 23 Sep 2025 13:27:21 +0200 Subject: [PATCH 24/24] Fix wrong key --- client/ayon_core/pipeline/load/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index a111444d48..d1731d4cf9 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -975,7 +975,7 @@ def get_outdated_containers( for container in filter_containers(containers, project_name).outdated: if ( not ignore_locked_versions - and container.get("locked_version") is True + and container.get("version_locked") is True ): continue outdated_containers.append(container)