added workfile selection and actions

This commit is contained in:
Jakub Trllo 2025-09-16 13:45:35 +02:00
parent e9e9461415
commit 12bf78b3cb
8 changed files with 218 additions and 54 deletions

View file

@ -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):

View file

@ -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

View file

@ -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):

View file

@ -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
)

View file

@ -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
)

View file

@ -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,

View file

@ -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,
)

View file

@ -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)