mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge pull request #1434 from ynput/feature/1229-ay-1284_show-application-used-on-task
Launcher: Show task workfiles
This commit is contained in:
commit
93f6ed27a1
12 changed files with 571 additions and 74 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -20,6 +21,7 @@ class WebactionContext:
|
|||
project_name: str
|
||||
folder_id: str
|
||||
task_id: str
|
||||
workfile_id: str
|
||||
addon_name: str
|
||||
addon_version: str
|
||||
|
||||
|
|
@ -33,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
|
||||
|
|
@ -56,6 +58,15 @@ class ActionItem:
|
|||
addon_version: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorkfileItem:
|
||||
workfile_id: str
|
||||
filename: str
|
||||
exists: bool
|
||||
icon: Optional[str]
|
||||
version: Optional[int]
|
||||
|
||||
|
||||
class AbstractLauncherCommon(ABC):
|
||||
@abstractmethod
|
||||
def register_event_callback(self, topic, callback):
|
||||
|
|
@ -85,12 +96,16 @@ 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.
|
||||
|
||||
Args:
|
||||
project_name (Union[str, None]): Project name.
|
||||
project_name (Optional[str]): Project name.
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: Project settings.
|
||||
|
|
@ -254,7 +269,7 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
"""Selected project name.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Selected project name.
|
||||
Optional[str]: Selected project name.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
|
@ -264,7 +279,7 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
"""Selected folder id.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Selected folder id.
|
||||
Optional[str]: Selected folder id.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
|
@ -274,7 +289,7 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
"""Selected task id.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Selected task id.
|
||||
Optional[str]: Selected task id.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
|
@ -284,7 +299,7 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
"""Selected task name.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Selected task name.
|
||||
Optional[str]: Selected task name.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
|
@ -302,7 +317,7 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
}
|
||||
|
||||
Returns:
|
||||
dict[str, Union[str, None]]: Selected context.
|
||||
dict[str, Optional[str]]: Selected context.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
|
@ -312,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.
|
||||
|
||||
"""
|
||||
|
|
@ -323,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.
|
||||
|
||||
"""
|
||||
|
|
@ -336,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(
|
||||
|
|
@ -351,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
|
||||
|
|
@ -373,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
|
||||
|
|
@ -465,3 +494,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
|
||||
|
|
|
|||
|
|
@ -1,10 +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()
|
||||
|
||||
|
|
@ -17,12 +28,15 @@ class BaseLauncherController(
|
|||
self._event_system = None
|
||||
self._log = None
|
||||
|
||||
self._addons_manager = None
|
||||
|
||||
self._username = NOT_SET
|
||||
|
||||
self._selection_model = LauncherSelectionModel(self)
|
||||
self._projects_model = ProjectsModel(self)
|
||||
self._hierarchy_model = HierarchyModel(self)
|
||||
self._actions_model = ActionsModel(self)
|
||||
self._workfiles_model = WorkfilesModel(self)
|
||||
|
||||
@property
|
||||
def log(self):
|
||||
|
|
@ -59,6 +73,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)
|
||||
|
|
@ -125,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(),
|
||||
|
|
@ -133,10 +155,24 @@ 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):
|
||||
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,
|
||||
|
|
@ -144,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):
|
||||
|
|
@ -186,6 +224,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")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
from .actions import ActionsModel
|
||||
from .selection import LauncherSelectionModel
|
||||
from .workfiles import WorkfilesModel
|
||||
|
||||
|
||||
__all__ = (
|
||||
"ActionsModel",
|
||||
"LauncherSelectionModel",
|
||||
"WorkfilesModel",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -131,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():
|
||||
|
|
@ -159,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
|
||||
|
|
@ -202,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:
|
||||
|
|
@ -272,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,
|
||||
})
|
||||
|
|
@ -282,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 {}
|
||||
|
|
@ -309,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 {}
|
||||
|
|
@ -333,12 +353,9 @@ 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):
|
||||
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)
|
||||
|
|
@ -347,6 +364,7 @@ class ActionsModel:
|
|||
project_name,
|
||||
folder_id,
|
||||
task_id,
|
||||
workfile_id,
|
||||
project_entity=project_entity,
|
||||
project_settings=project_settings,
|
||||
)
|
||||
|
|
@ -355,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"]]
|
||||
|
|
@ -400,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
|
||||
)
|
||||
|
|
@ -542,7 +565,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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
102
client/ayon_core/tools/launcher/models/workfiles.py
Normal file
102
client/ayon_core/tools/launcher/models/workfiles.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
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={"id", "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(
|
||||
workfile_id=workfile_entity["id"],
|
||||
filename=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
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
175
client/ayon_core/tools/launcher/ui/workfiles_page.py
Normal file
175
client/ayon_core/tools/launcher/ui/workfiles_page.py
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
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
|
||||
WORKFILE_ID_ROLE = QtCore.Qt.UserRole + 2
|
||||
|
||||
|
||||
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)
|
||||
item.setData(workfile_item.workfile_id, WORKFILE_ID_ROLE)
|
||||
flags = QtCore.Qt.NoItemFlags
|
||||
if workfile_item.exists:
|
||||
flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
|
||||
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 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 = QtCore.QSortFilterProxyModel()
|
||||
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_view.selectionModel().selectionChanged.connect(
|
||||
self._on_selection_changed
|
||||
)
|
||||
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, 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue