mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 12:54:40 +01:00
Merge remote-tracking branch 'origin/develop' into enhancement/1297-product-base-types-creation-and-creator-plugins
This commit is contained in:
commit
8edd6c583d
11 changed files with 192 additions and 62 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue