mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 12:54:40 +01:00
Merge branch 'develop' into enhancement/1416-loader-actions
This commit is contained in:
commit
ce3a59446c
27 changed files with 768 additions and 140 deletions
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from .constants import ContextChangeReason
|
||||
from .abstract import AbstractHost
|
||||
from .abstract import AbstractHost, ApplicationInformation
|
||||
from .host import (
|
||||
HostBase,
|
||||
ContextChangeData,
|
||||
|
|
@ -21,6 +21,7 @@ __all__ = (
|
|||
"ContextChangeReason",
|
||||
|
||||
"AbstractHost",
|
||||
"ApplicationInformation",
|
||||
|
||||
"HostBase",
|
||||
"ContextChangeData",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
|||
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
import typing
|
||||
from typing import Optional, Any
|
||||
|
||||
|
|
@ -13,6 +14,19 @@ if typing.TYPE_CHECKING:
|
|||
from .typing import HostContextData
|
||||
|
||||
|
||||
@dataclass
|
||||
class ApplicationInformation:
|
||||
"""Application information.
|
||||
|
||||
Attributes:
|
||||
app_name (Optional[str]): Application name. e.g. Maya, NukeX, Nuke
|
||||
app_version (Optional[str]): Application version. e.g. 15.2.1
|
||||
|
||||
"""
|
||||
app_name: Optional[str] = None
|
||||
app_version: Optional[str] = None
|
||||
|
||||
|
||||
class AbstractHost(ABC):
|
||||
"""Abstract definition of host implementation."""
|
||||
@property
|
||||
|
|
@ -26,6 +40,16 @@ class AbstractHost(ABC):
|
|||
"""Host name."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_app_information(self) -> ApplicationInformation:
|
||||
"""Information about the application where host is running.
|
||||
|
||||
Returns:
|
||||
ApplicationInformation: Application information.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_current_context(self) -> HostContextData:
|
||||
"""Get the current context of the host.
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import ayon_api
|
|||
from ayon_core.lib import emit_event
|
||||
|
||||
from .constants import ContextChangeReason
|
||||
from .abstract import AbstractHost
|
||||
from .abstract import AbstractHost, ApplicationInformation
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from ayon_core.pipeline import Anatomy
|
||||
|
|
@ -96,6 +96,18 @@ class HostBase(AbstractHost):
|
|||
|
||||
pass
|
||||
|
||||
def get_app_information(self) -> ApplicationInformation:
|
||||
"""Running application information.
|
||||
|
||||
Host integration should override this method and return correct
|
||||
information.
|
||||
|
||||
Returns:
|
||||
ApplicationInformation: Application information.
|
||||
|
||||
"""
|
||||
return ApplicationInformation()
|
||||
|
||||
def install(self):
|
||||
"""Install host specific functionality.
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ class _WorkfileOptionalData:
|
|||
):
|
||||
if kwargs:
|
||||
cls_name = self.__class__.__name__
|
||||
keys = ", ".join(['"{}"'.format(k) for k in kwargs.keys()])
|
||||
keys = ", ".join([f'"{k}"' for k in kwargs.keys()])
|
||||
warnings.warn(
|
||||
f"Unknown keywords passed to {cls_name}: {keys}",
|
||||
)
|
||||
|
|
@ -1554,6 +1554,27 @@ class IWorkfileHost(AbstractHost):
|
|||
if platform.system().lower() == "windows":
|
||||
rootless_path = rootless_path.replace("\\", "/")
|
||||
|
||||
# Get application information
|
||||
app_info = self.get_app_information()
|
||||
data = {}
|
||||
if app_info.app_name:
|
||||
data["app_name"] = app_info.app_name
|
||||
if app_info.app_version:
|
||||
data["app_version"] = app_info.app_version
|
||||
|
||||
# Use app group and app variant from applications addon (if available)
|
||||
app_addon_name = os.environ.get("AYON_APP_NAME")
|
||||
if not app_addon_name:
|
||||
app_addon_name = None
|
||||
|
||||
app_addon_tools_s = os.environ.get("AYON_APP_TOOLS")
|
||||
app_addon_tools = []
|
||||
if app_addon_tools_s:
|
||||
app_addon_tools = app_addon_tools_s.split(";")
|
||||
|
||||
data["ayon_app_name"] = app_addon_name
|
||||
data["ayon_app_tools"] = app_addon_tools
|
||||
|
||||
workfile_info = save_workfile_info(
|
||||
project_name,
|
||||
save_workfile_context.task_entity["id"],
|
||||
|
|
@ -1562,6 +1583,7 @@ class IWorkfileHost(AbstractHost):
|
|||
version,
|
||||
comment,
|
||||
description,
|
||||
data=data,
|
||||
workfile_entities=save_workfile_context.workfile_entities,
|
||||
)
|
||||
return workfile_info
|
||||
|
|
|
|||
|
|
@ -33,16 +33,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
|
||||
|
|
@ -50,6 +53,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
|
||||
|
||||
|
|
@ -209,6 +213,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.
|
||||
|
||||
|
|
@ -255,6 +268,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.
|
||||
|
||||
|
|
@ -301,15 +332,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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -942,15 +942,21 @@ 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: Optional[AbstractHost] = None,
|
||||
project_name: Optional[str] = None,
|
||||
ignore_locked_versions: bool = False,
|
||||
):
|
||||
"""Collect outdated containers from host scene.
|
||||
|
||||
Currently registered host and project in global session are used if
|
||||
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.
|
||||
|
||||
"""
|
||||
from ayon_core.pipeline import registered_host, get_current_project_name
|
||||
|
||||
|
|
@ -964,7 +970,16 @@ 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 (
|
||||
not ignore_locked_versions
|
||||
and container.get("version_locked") is True
|
||||
):
|
||||
continue
|
||||
outdated_containers.append(container)
|
||||
return outdated_containers
|
||||
|
||||
|
||||
def _is_valid_representation_id(repre_id: Any) -> bool:
|
||||
|
|
@ -985,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
|
||||
|
|
@ -993,8 +1011,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)
|
||||
|
||||
|
|
@ -1042,13 +1060,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 +1101,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)
|
||||
|
||||
|
|
|
|||
|
|
@ -207,6 +207,7 @@ def save_workfile_info(
|
|||
comment: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
username: Optional[str] = None,
|
||||
data: Optional[dict[str, Any]] = None,
|
||||
workfile_entities: Optional[list[dict[str, Any]]] = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Save workfile info entity for a workfile path.
|
||||
|
|
@ -221,6 +222,7 @@ def save_workfile_info(
|
|||
description (Optional[str]): Workfile description.
|
||||
username (Optional[str]): Username of user who saves the workfile.
|
||||
If not provided, current user is used.
|
||||
data (Optional[dict[str, Any]]): Additional workfile entity data.
|
||||
workfile_entities (Optional[list[dict[str, Any]]]): Pre-fetched
|
||||
workfile entities related to task.
|
||||
|
||||
|
|
@ -246,6 +248,18 @@ def save_workfile_info(
|
|||
if username is None:
|
||||
username = get_ayon_username()
|
||||
|
||||
attrib = {}
|
||||
extension = os.path.splitext(rootless_path)[1]
|
||||
for key, value in (
|
||||
("extension", extension),
|
||||
("description", description),
|
||||
):
|
||||
if value is not None:
|
||||
attrib[key] = value
|
||||
|
||||
if data is None:
|
||||
data = {}
|
||||
|
||||
if not workfile_entity:
|
||||
return _create_workfile_info_entity(
|
||||
project_name,
|
||||
|
|
@ -255,34 +269,38 @@ def save_workfile_info(
|
|||
username,
|
||||
version,
|
||||
comment,
|
||||
description,
|
||||
attrib,
|
||||
data,
|
||||
)
|
||||
|
||||
data = {
|
||||
key: value
|
||||
for key, value in (
|
||||
("host_name", host_name),
|
||||
("version", version),
|
||||
("comment", comment),
|
||||
)
|
||||
if value is not None
|
||||
}
|
||||
|
||||
old_data = workfile_entity["data"]
|
||||
for key, value in (
|
||||
("host_name", host_name),
|
||||
("version", version),
|
||||
("comment", comment),
|
||||
):
|
||||
if value is not None:
|
||||
data[key] = value
|
||||
|
||||
changed_data = {}
|
||||
old_data = workfile_entity["data"]
|
||||
for key, value in data.items():
|
||||
if key not in old_data or old_data[key] != value:
|
||||
changed_data[key] = value
|
||||
workfile_entity["data"][key] = value
|
||||
|
||||
changed_attrib = {}
|
||||
old_attrib = workfile_entity["attrib"]
|
||||
for key, value in attrib.items():
|
||||
if key not in old_attrib or old_attrib[key] != value:
|
||||
changed_attrib[key] = value
|
||||
workfile_entity["attrib"][key] = value
|
||||
|
||||
update_data = {}
|
||||
if changed_data:
|
||||
update_data["data"] = changed_data
|
||||
|
||||
old_description = workfile_entity["attrib"].get("description")
|
||||
if description is not None and old_description != description:
|
||||
update_data["attrib"] = {"description": description}
|
||||
workfile_entity["attrib"]["description"] = description
|
||||
if changed_attrib:
|
||||
update_data["attrib"] = changed_attrib
|
||||
|
||||
# Automatically fix 'createdBy' and 'updatedBy' fields
|
||||
# NOTE both fields were not automatically filled by server
|
||||
|
|
@ -749,7 +767,8 @@ def _create_workfile_info_entity(
|
|||
username: str,
|
||||
version: Optional[int],
|
||||
comment: Optional[str],
|
||||
description: Optional[str],
|
||||
attrib: dict[str, Any],
|
||||
data: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""Create workfile entity data.
|
||||
|
||||
|
|
@ -761,27 +780,18 @@ def _create_workfile_info_entity(
|
|||
username (str): Username.
|
||||
version (Optional[int]): Workfile version.
|
||||
comment (Optional[str]): Workfile comment.
|
||||
description (Optional[str]): Workfile description.
|
||||
attrib (dict[str, Any]): Workfile entity attributes.
|
||||
data (dict[str, Any]): Workfile entity data.
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: Created workfile entity data.
|
||||
|
||||
"""
|
||||
extension = os.path.splitext(rootless_path)[1]
|
||||
|
||||
attrib = {}
|
||||
for key, value in (
|
||||
("extension", extension),
|
||||
("description", description),
|
||||
):
|
||||
if value is not None:
|
||||
attrib[key] = value
|
||||
|
||||
data = {
|
||||
data.update({
|
||||
"host_name": host_name,
|
||||
"version": version,
|
||||
"comment": comment,
|
||||
}
|
||||
})
|
||||
|
||||
workfile_info = {
|
||||
"id": uuid.uuid4().hex,
|
||||
|
|
|
|||
|
|
@ -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,10 +986,11 @@ class ActionsWidget(QtWidgets.QWidget):
|
|||
event["project_name"],
|
||||
event["folder_id"],
|
||||
event["task_id"],
|
||||
event["workfile_id"],
|
||||
event["addon_name"],
|
||||
event["addon_version"],
|
||||
),
|
||||
event["action_label"],
|
||||
event["full_label"],
|
||||
form_data,
|
||||
)
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -524,7 +525,14 @@ 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)
|
||||
action_item = QtWidgets.QAction(icon, action.label, submenu)
|
||||
action_item.triggered.connect(
|
||||
partial(
|
||||
|
|
@ -622,7 +630,7 @@ class SceneInventoryView(QtWidgets.QTreeView):
|
|||
if 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"]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring AYON addon 'core' version."""
|
||||
__version__ = "1.6.0+dev"
|
||||
__version__ = "1.6.1+dev"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
name = "core"
|
||||
title = "Core"
|
||||
version = "1.6.0+dev"
|
||||
version = "1.6.1+dev"
|
||||
|
||||
client_dir = "ayon_core"
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
[tool.poetry]
|
||||
name = "ayon-core"
|
||||
version = "1.6.0+dev"
|
||||
version = "1.6.1+dev"
|
||||
description = ""
|
||||
authors = ["Ynput Team <team@ynput.io>"]
|
||||
readme = "README.md"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue