From 67dd1d07216b33dbb3188aa1d10459e9005012e8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Jun 2024 14:53:01 +0200 Subject: [PATCH] implemented launcher action class --- .../client/ayon_applications/action.py | 147 ++++++++++++++++++ .../client/ayon_applications/addon.py | 41 +++++ .../client/ayon_applications/manager.py | 2 +- 3 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 server_addon/applications/client/ayon_applications/action.py diff --git a/server_addon/applications/client/ayon_applications/action.py b/server_addon/applications/client/ayon_applications/action.py new file mode 100644 index 0000000000..e5942c7008 --- /dev/null +++ b/server_addon/applications/client/ayon_applications/action.py @@ -0,0 +1,147 @@ +import copy + +import ayon_api + +from ayon_core import resources +from ayon_core.lib import Logger, NestedCacheItem +from ayon_core.settings import get_studio_settings, get_project_settings +from ayon_core.pipeline.actions import LauncherAction + +from .exceptions import ( + ApplicationExecutableNotFound, + ApplicationLaunchFailed, +) + + +class ApplicationAction(LauncherAction): + """Action to launch an application. + + Application action based on 'ApplicationManager' system. + + Handling of applications in launcher is not ideal and should be completely + redone from scratch. This is just a temporary solution to keep backwards + compatibility with AYON launcher. + + Todos: + Move handling of errors to frontend. + """ + + # Application object + application = None + # Action attributes + name = None + label = None + label_variant = None + group = None + icon = None + color = None + order = 0 + data = {} + project_settings = {} + project_entities = {} + + _log = None + + # --- For compatibility for combinations of new and old ayon-core --- + project_settings_cache = NestedCacheItem( + levels=1, default_factory=dict, lifetime=20 + ) + project_entities_cache = NestedCacheItem( + levels=1, default_factory=dict, lifetime=20 + ) + + @classmethod + def _app_get_project_settings(cls, selection): + project_name = selection.project_name + if project_name in ApplicationAction.project_settings: + return ApplicationAction.project_settings[project_name] + + if hasattr(selection, "get_project_settings"): + return selection.get_project_settings() + + cache = ApplicationAction.project_settings_cache[project_name] + if not cache.is_valid: + if project_name: + settings = get_project_settings(project_name) + else: + settings = get_studio_settings() + cache.update_data(settings) + return copy.deepcopy(cache.get_data()) + + @classmethod + def _app_get_project_entity(cls, selection): + project_name = selection.project_name + if project_name in ApplicationAction.project_entities: + return ApplicationAction.project_entities[project_name] + + if hasattr(selection, "get_project_settings"): + return selection.get_project_entity() + + cache = ApplicationAction.project_entities_cache[project_name] + if not cache.is_valid: + project_entity = None + if project_name: + project_entity = ayon_api.get_project(project_name) + cache.update_data(project_entity) + return copy.deepcopy(cache.get_data()) + + @property + def log(self): + if self._log is None: + self._log = Logger.get_logger(self.__class__.__name__) + return self._log + + def is_compatible(self, selection): + if not selection.is_task_selected: + return False + + project_entity = self._app_get_project_entity(selection) + apps = project_entity["attrib"].get("applications") + if not apps or self.application.full_name not in apps: + return False + + project_settings = self._app_get_project_settings(selection) + only_available = project_settings["applications"]["only_available"] + if only_available and not self.application.find_executable(): + return False + return True + + def _show_message_box(self, title, message, details=None): + from qtpy import QtWidgets, QtGui + from ayon_core import style + + dialog = QtWidgets.QMessageBox() + icon = QtGui.QIcon(resources.get_ayon_icon_filepath()) + dialog.setWindowIcon(icon) + dialog.setStyleSheet(style.load_stylesheet()) + dialog.setWindowTitle(title) + dialog.setText(message) + if details: + dialog.setDetailedText(details) + dialog.exec_() + + def process(self, selection, **kwargs): + """Process the full Application action""" + try: + self.application.launch( + project_name=selection.project_name, + folder_path=selection.folder_path, + task_name=selection.task_name, + **self.data + ) + + except ApplicationExecutableNotFound as exc: + details = exc.details + msg = exc.msg + log_msg = str(msg) + if details: + log_msg += "\n" + details + self.log.warning(log_msg) + self._show_message_box( + "Application executable not found", msg, details + ) + + except ApplicationLaunchFailed as exc: + msg = str(exc) + self.log.warning(msg, exc_info=True) + self._show_message_box("Application launch failed", msg) diff --git a/server_addon/applications/client/ayon_applications/addon.py b/server_addon/applications/client/ayon_applications/addon.py index 5b8e86777d..26374ad0cd 100644 --- a/server_addon/applications/client/ayon_applications/addon.py +++ b/server_addon/applications/client/ayon_applications/addon.py @@ -162,6 +162,47 @@ class ApplicationsAddon(AYONAddon, IPluginPaths): server_url, "addons", self.name, self.version, "icons", icon_name ]) + def get_applications_action_classes(self): + """Get application action classes for launcher tool. + + This method should be used only by launcher tool. Please do not use it + in other places as its implementation is not optimal, and might + change or be removed. + + Returns: + list[ApplicationAction]: List of application action classes. + + """ + from .action import ApplicationAction + + actions = [] + + manager = self.get_applications_manager() + for full_name, application in manager.applications.items(): + if not application.enabled: + continue + + icon = self.get_app_icon_path(application.icon) + + action = type( + "app_{}".format(full_name), + (ApplicationAction,), + { + "identifier": "application.{}".format(full_name), + "application": application, + "name": application.name, + "label": application.group.label, + "label_variant": application.label, + "group": None, + "icon": icon, + "color": getattr(application, "color", None), + "order": getattr(application, "order", None) or 0, + "data": {} + } + ) + actions.append(action) + return actions + def launch_application( self, app_name, project_name, folder_path, task_name ): diff --git a/server_addon/applications/client/ayon_applications/manager.py b/server_addon/applications/client/ayon_applications/manager.py index dca2ff4491..f32bc20ef8 100644 --- a/server_addon/applications/client/ayon_applications/manager.py +++ b/server_addon/applications/client/ayon_applications/manager.py @@ -156,7 +156,7 @@ class ApplicationManager: Args: app_name (str): Name of application that should be launched. - **data (dict): Any additional data. Data may be used during + **data (Any): Any additional data. Data may be used during preparation to store objects usable in multiple places. Raises: