Merge branch 'develop' into enhancement/1416-loader-actions

# Conflicts:
#	client/ayon_core/tools/loader/control.py
This commit is contained in:
Jakub Trllo 2025-09-18 10:14:44 +02:00
commit 291930b78d
22 changed files with 481 additions and 186 deletions

18
.github/workflows/deploy_mkdocs.yml vendored Normal file
View file

@ -0,0 +1,18 @@
name: Deploy MkDocs
on:
push:
tags:
- "*"
workflow_dispatch:
jobs:
build-mk-docs:
# FIXME: Update @develop to @main after `ops-repo-automation` release.
uses: ynput/ops-repo-automation/.github/workflows/deploy_mkdocs.yml@develop
with:
repo: ${{ github.repository }}
secrets:
YNPUT_BOT_TOKEN: ${{ secrets.YNPUT_BOT_TOKEN }}
CI_USER: ${{ secrets.CI_USER }}
CI_EMAIL: ${{ secrets.CI_EMAIL }}

View file

@ -6,6 +6,8 @@ import collections
import tempfile
import subprocess
import platform
import warnings
import functools
from typing import Optional
import xml.etree.ElementTree
@ -67,6 +69,47 @@ VIDEO_EXTENSIONS = {
}
def deprecated(new_destination):
"""Mark functions as deprecated.
It will result in a warning being emitted when the function is used.
"""
func = None
if callable(new_destination):
func = new_destination
new_destination = None
def _decorator(decorated_func):
if new_destination is None:
warning_message = (
" Please check content of deprecated function to figure out"
" possible replacement."
)
else:
warning_message = " Please replace your usage with '{}'.".format(
new_destination
)
@functools.wraps(decorated_func)
def wrapper(*args, **kwargs):
warnings.simplefilter("always", DeprecationWarning)
warnings.warn(
(
"Call to deprecated function '{}'"
"\nFunction was moved or removed.{}"
).format(decorated_func.__name__, warning_message),
category=DeprecationWarning,
stacklevel=4
)
return decorated_func(*args, **kwargs)
return wrapper
if func is None:
return _decorator
return _decorator(func)
def get_transcode_temp_directory():
"""Creates temporary folder for transcoding.
@ -966,6 +1009,8 @@ def convert_ffprobe_fps_to_float(value):
return dividend / divisor
# --- Deprecated functions ---
@deprecated("oiio_color_convert")
def convert_colorspace(
input_path,
output_path,
@ -977,7 +1022,62 @@ def convert_colorspace(
additional_command_args=None,
logger=None,
):
"""Convert source file from one color space to another.
"""DEPRECATED function use `oiio_color_convert` instead
Args:
input_path (str): Path to input file that should be converted.
output_path (str): Path to output file where result will be stored.
config_path (str): Path to OCIO config file.
source_colorspace (str): OCIO valid color space of source files.
target_colorspace (str, optional): OCIO valid target color space.
If filled, 'view' and 'display' must be empty.
view (str, optional): Name for target viewer space (OCIO valid).
Both 'view' and 'display' must be filled
(if not 'target_colorspace').
display (str, optional): Name for target display-referred
reference space. Both 'view' and 'display' must be filled
(if not 'target_colorspace').
additional_command_args (list, optional): Additional arguments
for oiiotool (like binary depth for .dpx).
logger (logging.Logger, optional): Logger used for logging.
Returns:
None: Function returns None.
Raises:
ValueError: If parameters are misconfigured.
"""
return oiio_color_convert(
input_path,
output_path,
config_path,
source_colorspace,
target_colorspace=target_colorspace,
target_display=display,
target_view=view,
additional_command_args=additional_command_args,
logger=logger,
)
def oiio_color_convert(
input_path,
output_path,
config_path,
source_colorspace,
source_display=None,
source_view=None,
target_colorspace=None,
target_display=None,
target_view=None,
additional_command_args=None,
logger=None,
):
"""Transcode source file to other with colormanagement.
Oiiotool also support additional arguments for transcoding.
For more information, see the official documentation:
https://openimageio.readthedocs.io/en/latest/oiiotool.html
Args:
input_path (str): Path that should be converted. It is expected that
@ -989,17 +1089,26 @@ def convert_colorspace(
sequence in 'file.FRAMESTART-FRAMEEND#.ext', `output.1-3#.tif`)
config_path (str): path to OCIO config file
source_colorspace (str): ocio valid color space of source files
source_display (str, optional): name for source display-referred
reference space (ocio valid). If provided, source_view must also be
provided, and source_colorspace will be ignored
source_view (str, optional): name for source viewer space (ocio valid)
If provided, source_display must also be provided, and
source_colorspace will be ignored
target_colorspace (str): ocio valid target color space
if filled, 'view' and 'display' must be empty
view (str): name for viewer space (ocio valid)
both 'view' and 'display' must be filled (if 'target_colorspace')
display (str): name for display-referred reference space (ocio valid)
target_display (str): name for target display-referred reference space
(ocio valid) both 'view' and 'display' must be filled (if
'target_colorspace')
target_view (str): name for target viewer space (ocio valid)
both 'view' and 'display' must be filled (if 'target_colorspace')
additional_command_args (list): arguments for oiiotool (like binary
depth for .dpx)
logger (logging.Logger): Logger used for logging.
Raises:
ValueError: if misconfigured
"""
if logger is None:
logger = logging.getLogger(__name__)
@ -1024,23 +1133,82 @@ def convert_colorspace(
"--ch", channels_arg
])
if all([target_colorspace, view, display]):
raise ValueError("Colorspace and both screen and display"
" cannot be set together."
"Choose colorspace or screen and display")
if not target_colorspace and not all([view, display]):
raise ValueError("Both screen and display must be set.")
# Validate input parameters
if target_colorspace and target_view and target_display:
raise ValueError(
"Colorspace and both view and display cannot be set together."
"Choose colorspace or screen and display"
)
if not target_colorspace and not target_view and not target_display:
raise ValueError(
"Both view and display must be set if target_colorspace is not "
"provided."
)
if (
(source_view and not source_display)
or (source_display and not source_view)
):
raise ValueError(
"Both source_view and source_display must be provided if using "
"display/view inputs."
)
if source_view and source_display and source_colorspace:
logger.warning(
"Both source display/view and source_colorspace provided. "
"Using source display/view pair and ignoring source_colorspace."
)
if additional_command_args:
oiio_cmd.extend(additional_command_args)
if target_colorspace:
oiio_cmd.extend(["--colorconvert:subimages=0",
source_colorspace,
target_colorspace])
if view and display:
oiio_cmd.extend(["--iscolorspace", source_colorspace])
oiio_cmd.extend(["--ociodisplay:subimages=0", display, view])
# Handle the different conversion cases
# Source view and display are known
if source_view and source_display:
if target_colorspace:
# This is a two-step conversion process since there's no direct
# display/view to colorspace command
# This could be a config parameter or determined from OCIO config
# Use temporarty role space 'scene_linear'
color_convert_args = ("scene_linear", target_colorspace)
elif source_display != target_display or source_view != target_view:
# Complete display/view pair conversion
# - go through a reference space
color_convert_args = (target_display, target_view)
else:
color_convert_args = None
logger.debug(
"Source and target display/view pairs are identical."
" No color conversion needed."
)
if color_convert_args:
oiio_cmd.extend([
"--ociodisplay:inverse=1:subimages=0",
source_display,
source_view,
"--colorconvert:subimages=0",
*color_convert_args
])
elif target_colorspace:
# Standard color space to color space conversion
oiio_cmd.extend([
"--colorconvert:subimages=0",
source_colorspace,
target_colorspace,
])
else:
# Standard conversion from colorspace to display/view
oiio_cmd.extend([
"--iscolorspace",
source_colorspace,
"--ociodisplay:subimages=0",
target_display,
target_view,
])
oiio_cmd.extend(["-o", output_path])

View file

@ -1404,7 +1404,7 @@ def _get_display_view_colorspace_name(config_path, display, view):
"""
config = _get_ocio_config(config_path)
colorspace = config.getDisplayViewColorSpaceName(display, view)
# Special token. See https://opencolorio.readthedocs.io/en/latest/guides/authoring/authoring.html#shared-views # noqa
# Special token. See https://opencolorio.readthedocs.io/en/latest/guides/authoring/authoring.html#shared-views # noqa
if colorspace == "<USE_DISPLAY_NAME>":
colorspace = display

View file

@ -11,7 +11,7 @@ from ayon_core.lib import (
is_oiio_supported,
)
from ayon_core.lib.transcoding import (
convert_colorspace,
oiio_color_convert,
)
from ayon_core.lib.profiles_filtering import filter_profiles
@ -87,6 +87,14 @@ class ExtractOIIOTranscode(publish.Extractor):
new_representations = []
repres = instance.data["representations"]
for idx, repre in enumerate(list(repres)):
# target space, display and view might be defined upstream
# TODO: address https://github.com/ynput/ayon-core/pull/1268#discussion_r2156555474
# Implement upstream logic to handle target_colorspace,
# target_display, target_view in other DCCs
target_colorspace = False
target_display = instance.data.get("colorspaceDisplay")
target_view = instance.data.get("colorspaceView")
self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"]))
if not self._repre_is_valid(repre):
continue
@ -96,6 +104,8 @@ class ExtractOIIOTranscode(publish.Extractor):
colorspace_data = repre["colorspaceData"]
source_colorspace = colorspace_data["colorspace"]
source_display = colorspace_data.get("display")
source_view = colorspace_data.get("view")
config_path = colorspace_data.get("config", {}).get("path")
if not config_path or not os.path.exists(config_path):
self.log.warning("Config file doesn't exist, skipping")
@ -126,7 +136,6 @@ class ExtractOIIOTranscode(publish.Extractor):
transcoding_type = output_def["transcoding_type"]
target_colorspace = view = display = None
# NOTE: we use colorspace_data as the fallback values for
# the target colorspace.
if transcoding_type == "colorspace":
@ -138,18 +147,20 @@ class ExtractOIIOTranscode(publish.Extractor):
colorspace_data.get("colorspace"))
elif transcoding_type == "display_view":
display_view = output_def["display_view"]
view = display_view["view"] or colorspace_data.get("view")
display = (
target_view = (
display_view["view"]
or colorspace_data.get("view"))
target_display = (
display_view["display"]
or colorspace_data.get("display")
)
# both could be already collected by DCC,
# but could be overwritten when transcoding
if view:
new_repre["colorspaceData"]["view"] = view
if display:
new_repre["colorspaceData"]["display"] = display
if target_view:
new_repre["colorspaceData"]["view"] = target_view
if target_display:
new_repre["colorspaceData"]["display"] = target_display
if target_colorspace:
new_repre["colorspaceData"]["colorspace"] = \
target_colorspace
@ -168,16 +179,18 @@ class ExtractOIIOTranscode(publish.Extractor):
new_staging_dir,
output_extension)
convert_colorspace(
input_path,
output_path,
config_path,
source_colorspace,
target_colorspace,
view,
display,
additional_command_args,
self.log
oiio_color_convert(
input_path=input_path,
output_path=output_path,
config_path=config_path,
source_colorspace=source_colorspace,
target_colorspace=target_colorspace,
target_display=target_display,
target_view=target_view,
source_display=source_display,
source_view=source_view,
additional_command_args=additional_command_args,
logger=self.log
)
# cleanup temporary transcoded files

View file

@ -15,7 +15,7 @@ from ayon_core.lib import (
path_to_subprocess_arg,
run_subprocess,
)
from ayon_core.lib.transcoding import convert_colorspace
from ayon_core.lib.transcoding import oiio_color_convert
from ayon_core.lib.transcoding import VIDEO_EXTENSIONS, IMAGE_EXTENSIONS
@ -433,13 +433,15 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
oiio_default_view = display_and_view["view"]
try:
convert_colorspace(
oiio_color_convert(
src_path,
dst_path,
colorspace_data["config"]["path"],
colorspace_data["colorspace"],
display=repre_display or oiio_default_display,
view=repre_view or oiio_default_view,
source_display=colorspace_data.get("display"),
source_view=colorspace_data.get("view"),
target_display=repre_display or oiio_default_display,
target_view=repre_view or oiio_default_view,
target_colorspace=oiio_default_colorspace,
additional_command_args=resolution_arg,
logger=self.log,

View file

@ -10,6 +10,7 @@ from .projects import (
PROJECTS_MODEL_SENDER,
FolderTypeItem,
TaskTypeItem,
ProductTypeIconMapping,
)
from .hierarchy import (
FolderItem,
@ -34,6 +35,7 @@ __all__ = (
"PROJECTS_MODEL_SENDER",
"FolderTypeItem",
"TaskTypeItem",
"ProductTypeIconMapping",
"FolderItem",
"TaskItem",

View file

@ -2,7 +2,7 @@ from __future__ import annotations
import contextlib
from abc import ABC, abstractmethod
from typing import Dict, Any
from typing import Any, Optional
from dataclasses import dataclass
import ayon_api
@ -51,7 +51,7 @@ class StatusItem:
self.icon: str = icon
self.state: str = state
def to_data(self) -> Dict[str, Any]:
def to_data(self) -> dict[str, Any]:
return {
"name": self.name,
"color": self.color,
@ -125,16 +125,24 @@ class TaskTypeItem:
icon (str): Icon name in MaterialIcons ("fiber_new").
"""
def __init__(self, name, short, icon):
def __init__(
self,
name: str,
short: str,
icon: str,
color: Optional[str],
):
self.name = name
self.short = short
self.icon = icon
self.color = color
def to_data(self):
return {
"name": self.name,
"short": self.short,
"icon": self.icon,
"color": self.color,
}
@classmethod
@ -147,6 +155,7 @@ class TaskTypeItem:
name=task_type_data["name"],
short=task_type_data["shortName"],
icon=task_type_data["icon"],
color=task_type_data.get("color"),
)
@ -218,6 +227,54 @@ class ProjectItem:
return cls(**data)
class ProductTypeIconMapping:
def __init__(
self,
default: Optional[dict[str, str]] = None,
definitions: Optional[list[dict[str, str]]] = None,
):
self._default = default or {}
self._definitions = definitions or []
self._default_def = None
self._definitions_by_name = None
def get_icon(
self,
product_base_type: Optional[str] = None,
product_type: Optional[str] = None,
) -> dict[str, str]:
defs = self._get_defs_by_name()
icon = defs.get(product_type)
if icon is None:
icon = defs.get(product_base_type)
if icon is None:
icon = self._get_default_def()
return icon.copy()
def _get_default_def(self) -> dict[str, str]:
if self._default_def is None:
self._default_def = {
"type": "material-symbols",
"name": self._default.get("icon", "deployed_code"),
"color": self._default.get("color", "#cccccc"),
}
return self._default_def
def _get_defs_by_name(self) -> dict[str, dict[str, str]]:
if self._definitions_by_name is None:
self._definitions_by_name = {
product_base_type_def["name"]: {
"type": "material-symbols",
"name": product_base_type_def.get("icon", "deployed_code"),
"color": product_base_type_def.get("color", "#cccccc"),
}
for product_base_type_def in self._definitions
}
return self._definitions_by_name
def _get_project_items_from_entitiy(
projects: list[dict[str, Any]]
) -> list[ProjectItem]:
@ -242,6 +299,9 @@ class ProjectsModel(object):
self._projects_by_name = NestedCacheItem(
levels=1, default_factory=list
)
self._product_type_icons_mapping = NestedCacheItem(
levels=1, default_factory=ProductTypeIconMapping
)
self._project_statuses_cache = {}
self._folder_types_cache = {}
self._task_types_cache = {}
@ -255,6 +315,7 @@ class ProjectsModel(object):
self._task_types_cache = {}
self._projects_cache.reset()
self._projects_by_name.reset()
self._product_type_icons_mapping.reset()
def refresh(self):
"""Refresh project items.
@ -390,6 +451,27 @@ class ProjectsModel(object):
self._task_type_items_getter,
)
def get_product_type_icons_mapping(
self, project_name: Optional[str]
) -> ProductTypeIconMapping:
cache = self._product_type_icons_mapping[project_name]
if cache.is_valid:
return cache.get_data()
project_entity = self.get_project_entity(project_name)
icons_mapping = ProductTypeIconMapping()
if project_entity:
product_base_types = (
project_entity["config"].get("productBaseTypes", {})
)
icons_mapping = ProductTypeIconMapping(
product_base_types.get("default"),
product_base_types.get("definitions")
)
cache.update_data(icons_mapping)
return icons_mapping
def _get_project_items(
self, project_name, sender, item_type, cache_obj, getter
):

View file

@ -9,7 +9,11 @@ from ayon_core.lib.attribute_definitions import (
deserialize_attr_defs,
serialize_attr_defs,
)
from ayon_core.tools.common_models import TaskItem, TagItem
from ayon_core.tools.common_models import (
TaskItem,
TagItem,
ProductTypeIconMapping,
)
class ProductTypeItem:
@ -78,7 +82,6 @@ class ProductItem:
product_type (str): Product type.
product_name (str): Product name.
product_icon (dict[str, Any]): Product icon definition.
product_type_icon (dict[str, Any]): Product type icon definition.
product_in_scene (bool): Is product in scene (only when used in DCC).
group_name (str): Group name.
folder_id (str): Folder id.
@ -93,8 +96,6 @@ class ProductItem:
product_base_type: str,
product_name: str,
product_icon: dict[str, Any],
product_type_icon: dict[str, Any],
product_base_type_icon: dict[str, Any],
group_name: str,
folder_id: str,
folder_label: str,
@ -106,8 +107,6 @@ class ProductItem:
self.product_base_type = product_base_type
self.product_name = product_name
self.product_icon = product_icon
self.product_type_icon = product_type_icon
self.product_base_type_icon = product_base_type_icon
self.product_in_scene = product_in_scene
self.group_name = group_name
self.folder_id = folder_id
@ -121,8 +120,6 @@ class ProductItem:
"product_base_type": self.product_base_type,
"product_name": self.product_name,
"product_icon": self.product_icon,
"product_type_icon": self.product_type_icon,
"product_base_type_icon": self.product_base_type_icon,
"product_in_scene": self.product_in_scene,
"group_name": self.group_name,
"folder_id": self.folder_id,
@ -495,8 +492,8 @@ class BackendLoaderController(_BaseLoaderController):
topic (str): Event topic name.
data (Optional[dict[str, Any]]): Event data.
source (Optional[str]): Event source.
"""
"""
pass
@abstractmethod
@ -505,8 +502,20 @@ class BackendLoaderController(_BaseLoaderController):
Returns:
set[str]: Set of loaded product ids.
"""
"""
pass
@abstractmethod
def get_product_type_icons_mapping(
self, project_name: Optional[str]
) -> ProductTypeIconMapping:
"""Product type icons mapping.
Returns:
ProductTypeIconMapping: Product type icons mapping.
"""
pass

View file

@ -2,7 +2,7 @@ from __future__ import annotations
import logging
import uuid
from typing import Any
from typing import Optional, Any
import ayon_api
@ -17,6 +17,7 @@ from ayon_core.tools.common_models import (
HierarchyModel,
ThumbnailsModel,
TagItem,
ProductTypeIconMapping,
)
from .abstract import (
@ -200,6 +201,13 @@ class LoaderController(BackendLoaderController, FrontendLoaderController):
project_name, sender
)
def get_product_type_icons_mapping(
self, project_name: Optional[str]
) -> ProductTypeIconMapping:
return self._projects_model.get_product_type_icons_mapping(
project_name
)
def get_folder_items(self, project_name, sender=None):
return self._hierarchy_model.get_folder_items(project_name, sender)

View file

@ -9,9 +9,9 @@ import arrow
import ayon_api
from ayon_api.operations import OperationsSession
from ayon_core.lib import NestedCacheItem
from ayon_core.style import get_default_entity_icon_color
from ayon_core.tools.common_models import ProductTypeIconMapping
from ayon_core.tools.loader.abstract import (
ProductTypeItem,
ProductBaseTypeItem,
@ -21,8 +21,11 @@ from ayon_core.tools.loader.abstract import (
)
if TYPE_CHECKING:
from ayon_api.typing import ProductBaseTypeDict, ProductDict, VersionDict
from ayon_api.typing import (
ProductBaseTypeDict,
ProductDict,
VersionDict,
)
PRODUCTS_MODEL_SENDER = "products.model"
@ -84,42 +87,18 @@ def version_item_from_entity(version):
def product_item_from_entity(
product_entity: ProductDict,
version_entities,
product_type_items_by_name: dict[str, ProductTypeItem],
product_base_type_items_by_name: dict[str, ProductBaseTypeItem],
folder_label,
icons_mapping,
product_in_scene,
):
product_attribs = product_entity["attrib"]
group = product_attribs.get("productGroup")
product_type = product_entity["productType"]
product_type_item = product_type_items_by_name.get(product_type)
# NOTE This is needed for cases when products were not created on server
# using api functions. In that case product type item may not be
# available and we need to create a default.
if product_type_item is None:
product_type_item = create_default_product_type_item(product_type)
# Cache the item for future use
product_type_items_by_name[product_type] = product_type_item
product_base_type = product_entity.get("productBaseType")
product_base_type_item = product_base_type_items_by_name.get(
product_base_type)
# Same as for product type item above. Not sure if this is still needed
# though.
if product_base_type_item is None:
product_base_type_item = create_default_product_base_type_item(
product_base_type)
# Cache the item for future use
product_base_type_items_by_name[product_base_type] = (
product_base_type_item)
product_type_icon = product_type_item.icon
product_base_type_icon = product_base_type_item.icon
product_icon = {
"type": "awesome-font",
"name": "fa.file-o",
"color": get_default_entity_icon_color(),
}
product_icon = icons_mapping.get_icon(
product_base_type, product_type
)
version_items = {
version_entity["id"]: version_item_from_entity(version_entity)
for version_entity in version_entities
@ -131,8 +110,6 @@ def product_item_from_entity(
product_base_type=product_base_type,
product_name=product_entity["name"],
product_icon=product_icon,
product_type_icon=product_type_icon,
product_base_type_icon=product_base_type_icon,
product_in_scene=product_in_scene,
group_name=group,
folder_id=product_entity["folderId"],
@ -141,22 +118,8 @@ def product_item_from_entity(
)
def product_type_item_from_data(
product_type_data: ProductDict) -> ProductTypeItem:
# TODO implement icon implementation
# icon = product_type_data["icon"]
# color = product_type_data["color"]
icon = {
"type": "awesome-font",
"name": "fa.folder",
"color": "#0091B2",
}
# TODO implement checked logic
return ProductTypeItem(product_type_data["name"], icon)
def product_base_type_item_from_data(
product_base_type_data: ProductBaseTypeDict
product_base_type_data: ProductBaseTypeDict
) -> ProductBaseTypeItem:
"""Create product base type item from data.
@ -174,34 +137,8 @@ def product_base_type_item_from_data(
}
return ProductBaseTypeItem(
name=product_base_type_data["name"],
icon=icon)
def create_default_product_type_item(product_type: str) -> ProductTypeItem:
icon = {
"type": "awesome-font",
"name": "fa.folder",
"color": "#0091B2",
}
return ProductTypeItem(product_type, icon)
def create_default_product_base_type_item(
product_base_type: str) -> ProductBaseTypeItem:
"""Create default product base type item.
Args:
product_base_type (str): Product base type name.
Returns:
ProductBaseTypeItem: Default product base type item.
"""
icon = {
"type": "awesome-font",
"name": "fa.folder",
"color": "#0091B2",
}
return ProductBaseTypeItem(product_base_type, icon)
icon=icon
)
class ProductsModel:
@ -247,7 +184,9 @@ class ProductsModel:
self._product_items_cache.reset()
self._repre_items_cache.reset()
def get_product_type_items(self, project_name):
def get_product_type_items(
self, project_name: Optional[str]
) -> list[ProductTypeItem]:
"""Product type items for project.
Args:
@ -255,25 +194,33 @@ class ProductsModel:
Returns:
list[ProductTypeItem]: Product type items.
"""
"""
if not project_name:
return []
cache = self._product_type_items_cache[project_name]
if not cache.is_valid:
icons_mapping = self._get_product_type_icons(project_name)
product_types = ayon_api.get_project_product_types(project_name)
cache.update_data([
product_type_item_from_data(product_type)
ProductTypeItem(
product_type["name"],
icons_mapping.get_icon(product_type=product_type["name"]),
)
for product_type in product_types
])
return cache.get_data()
def get_product_base_type_items(
self,
project_name: Optional[str]) -> list[ProductBaseTypeItem]:
self, project_name: Optional[str]
) -> list[ProductBaseTypeItem]:
"""Product base type items for the project.
Notes:
This will be used for filtering product types in UI when
product base types are fully implemented.
Args:
project_name (optional, str): Project name.
@ -286,6 +233,7 @@ class ProductsModel:
cache = self._product_base_type_items_cache[project_name]
if not cache.is_valid:
icons_mapping = self._get_product_type_icons(project_name)
product_base_types = []
# TODO add temp implementation here when it is actually
# implemented and available on server.
@ -294,7 +242,10 @@ class ProductsModel:
project_name
)
cache.update_data([
product_base_type_item_from_data(product_base_type)
ProductBaseTypeItem(
product_base_type["name"],
icons_mapping.get_icon(product_base_type["name"]),
)
for product_base_type in product_base_types
])
return cache.get_data()
@ -511,6 +462,11 @@ class ProductsModel:
PRODUCTS_MODEL_SENDER
)
def _get_product_type_icons(
self, project_name: Optional[str]
) -> ProductTypeIconMapping:
return self._controller.get_product_type_icons_mapping(project_name)
def _get_product_items_by_id(self, project_name, product_ids):
product_item_by_id = self._product_item_by_id[project_name]
missing_product_ids = set()
@ -524,7 +480,7 @@ class ProductsModel:
output.update(
self._query_product_items_by_ids(
project_name, missing_product_ids
project_name, product_ids=missing_product_ids
)
)
return output
@ -553,36 +509,18 @@ class ProductsModel:
products: Iterable[ProductDict],
versions: Iterable[VersionDict],
folder_items=None,
product_type_items=None,
product_base_type_items: Optional[Iterable[ProductBaseTypeItem]] = None
):
if folder_items is None:
folder_items = self._controller.get_folder_items(project_name)
if product_type_items is None:
product_type_items = self.get_product_type_items(project_name)
if product_base_type_items is None:
product_base_type_items = self.get_product_base_type_items(
project_name
)
loaded_product_ids = self._controller.get_loaded_product_ids()
versions_by_product_id = collections.defaultdict(list)
for version in versions:
versions_by_product_id[version["productId"]].append(version)
product_type_items_by_name = {
product_type_item.name: product_type_item
for product_type_item in product_type_items
}
product_base_type_items_by_name: dict[str, ProductBaseTypeItem] = {
product_base_type_item.name: product_base_type_item
for product_base_type_item in product_base_type_items
}
output: dict[str, ProductItem] = {}
icons_mapping = self._get_product_type_icons(project_name)
for product in products:
product_id = product["id"]
folder_id = product["folderId"]
@ -595,9 +533,8 @@ class ProductsModel:
product_item = product_item_from_entity(
product,
versions,
product_type_items_by_name,
product_base_type_items_by_name,
folder_item.label,
icons_mapping,
product_id in loaded_product_ids,
)
output[product_id] = product_item

View file

@ -17,7 +17,6 @@ PRODUCT_ID_ROLE = QtCore.Qt.UserRole + 6
PRODUCT_NAME_ROLE = QtCore.Qt.UserRole + 7
PRODUCT_TYPE_ROLE = QtCore.Qt.UserRole + 8
PRODUCT_BASE_TYPE_ROLE = QtCore.Qt.UserRole + 9
PRODUCT_TYPE_ICON_ROLE = QtCore.Qt.UserRole + 10
PRODUCT_IN_SCENE_ROLE = QtCore.Qt.UserRole + 11
VERSION_ID_ROLE = QtCore.Qt.UserRole + 12
VERSION_HERO_ROLE = QtCore.Qt.UserRole + 13
@ -228,10 +227,7 @@ class ProductsModel(QtGui.QStandardItemModel):
return super().data(index, role)
if role == QtCore.Qt.DecorationRole:
if col == 1:
role = PRODUCT_TYPE_ICON_ROLE
else:
return None
return None
if (
role == VERSION_NAME_EDIT_ROLE
@ -455,7 +451,6 @@ class ProductsModel(QtGui.QStandardItemModel):
model_item = QtGui.QStandardItem(product_item.product_name)
model_item.setEditable(False)
icon = get_qt_icon(product_item.product_icon)
product_type_icon = get_qt_icon(product_item.product_type_icon)
model_item.setColumnCount(self.columnCount())
model_item.setData(icon, QtCore.Qt.DecorationRole)
model_item.setData(product_id, PRODUCT_ID_ROLE)
@ -464,7 +459,6 @@ class ProductsModel(QtGui.QStandardItemModel):
product_item.product_base_type, PRODUCT_BASE_TYPE_ROLE
)
model_item.setData(product_item.product_type, PRODUCT_TYPE_ROLE)
model_item.setData(product_type_icon, PRODUCT_TYPE_ICON_ROLE)
model_item.setData(product_item.folder_id, FOLDER_ID_ROLE)
self._product_items_by_id[product_id] = product_item

View file

@ -1147,6 +1147,8 @@ class LogItemMessage(QtWidgets.QTextEdit):
QtWidgets.QSizePolicy.Preferred,
QtWidgets.QSizePolicy.Maximum
)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
document = self.document()
document.documentLayout().documentSizeChanged.connect(
self._adjust_minimum_size

View file

@ -146,19 +146,19 @@ class TasksModel(QtGui.QStandardItemModel):
self._controller.get_current_project_name()
)
}
icon_name_by_task_name = {}
type_item_by_task_name = {}
for task_items in task_items_by_folder_path.values():
for task_item in task_items:
task_name = task_item.name
if (
task_name not in new_task_names
or task_name in icon_name_by_task_name
or task_name in type_item_by_task_name
):
continue
task_type_name = task_item.task_type
task_type_item = task_type_items.get(task_type_name)
if task_type_item:
icon_name_by_task_name[task_name] = task_type_item.icon
type_item_by_task_name[task_name] = task_type_item
for task_name in new_task_names:
item = self._items_by_name.get(task_name)
@ -171,13 +171,18 @@ class TasksModel(QtGui.QStandardItemModel):
if not task_name:
continue
icon_name = icon_name_by_task_name.get(task_name)
icon = None
icon = icon_name = icon_color = None
task_type_item = type_item_by_task_name.get(task_name)
if task_type_item is not None:
icon_name = task_type_item.icon
icon_color = task_type_item.color
if icon_name:
if not icon_color:
icon_color = get_default_entity_icon_color()
icon = get_qt_icon({
"type": "material-symbols",
"name": icon_name,
"color": get_default_entity_icon_color(),
"color": icon_color,
})
if icon is None:
icon = default_icon

View file

@ -1,3 +1,5 @@
from typing import Optional
import ayon_api
from ayon_core.lib.events import QueuedEventSystem
@ -6,7 +8,11 @@ from ayon_core.pipeline import (
registered_host,
get_current_context,
)
from ayon_core.tools.common_models import HierarchyModel, ProjectsModel
from ayon_core.tools.common_models import (
HierarchyModel,
ProjectsModel,
ProductTypeIconMapping,
)
from .models import SiteSyncModel, ContainersModel
@ -93,6 +99,13 @@ class SceneInventoryController:
project_name, None
)
def get_product_type_icons_mapping(
self, project_name: Optional[str]
) -> ProductTypeIconMapping:
return self._projects_model.get_product_type_icons_mapping(
project_name
)
# Containers methods
def get_containers(self):
return self._containers_model.get_containers()

View file

@ -214,9 +214,6 @@ class InventoryModel(QtGui.QStandardItemModel):
group_icon = qtawesome.icon(
"fa.object-group", color=self._default_icon_color
)
product_type_icon = qtawesome.icon(
"fa.folder", color="#0091B2"
)
group_item_font = QtGui.QFont()
group_item_font.setBold(True)
@ -303,7 +300,7 @@ class InventoryModel(QtGui.QStandardItemModel):
remote_site_progress = "{}%".format(
max(progress["remote_site"], 0) * 100
)
product_type_icon = get_qt_icon(repre_info.product_type_icon)
group_item = QtGui.QStandardItem()
group_item.setColumnCount(root_item.columnCount())
group_item.setData(group_name, QtCore.Qt.DisplayRole)

View file

@ -126,6 +126,7 @@ class RepresentationInfo:
product_id,
product_name,
product_type,
product_type_icon,
product_group,
version_id,
representation_name,
@ -135,6 +136,7 @@ class RepresentationInfo:
self.product_id = product_id
self.product_name = product_name
self.product_type = product_type
self.product_type_icon = product_type_icon
self.product_group = product_group
self.version_id = version_id
self.representation_name = representation_name
@ -153,7 +155,17 @@ class RepresentationInfo:
@classmethod
def new_invalid(cls):
return cls(None, None, None, None, None, None, None, None)
return cls(
None,
None,
None,
None,
None,
None,
None,
None,
None,
)
class VersionItem:
@ -229,6 +241,9 @@ class ContainersModel:
def get_representation_info_items(self, project_name, representation_ids):
output = {}
missing_repre_ids = set()
icons_mapping = self._controller.get_product_type_icons_mapping(
project_name
)
for repre_id in representation_ids:
try:
uuid.UUID(repre_id)
@ -253,6 +268,7 @@ class ContainersModel:
"product_id": None,
"product_name": None,
"product_type": None,
"product_type_icon": None,
"product_group": None,
"version_id": None,
"representation_name": None,
@ -265,10 +281,17 @@ class ContainersModel:
kwargs["folder_id"] = folder["id"]
kwargs["folder_path"] = folder["path"]
if product:
product_type = product["productType"]
product_base_type = product.get("productBaseType")
icon = icons_mapping.get_icon(
product_base_type=product_base_type,
product_type=product_type,
)
group = product["attrib"]["productGroup"]
kwargs["product_id"] = product["id"]
kwargs["product_name"] = product["name"]
kwargs["product_type"] = product["productType"]
kwargs["product_type_icon"] = icon
kwargs["product_group"] = group
if version:
kwargs["version_id"] = version["id"]

View file

@ -186,8 +186,15 @@ class StatusDelegate(QtWidgets.QStyledItemDelegate):
)
fm = QtGui.QFontMetrics(option.font)
if text_rect.width() < fm.width(text):
text = self._get_status_short_name(index)
if text_rect.width() < fm.width(text):
short_text = self._get_status_short_name(index)
if short_text:
text = short_text
text = fm.elidedText(
text, QtCore.Qt.ElideRight, text_rect.width()
)
# Allow at least one character
if len(text) < 2:
text = ""
fg_color = self._get_status_color(index)

View file

@ -234,10 +234,11 @@ class TasksQtModel(QtGui.QStandardItemModel):
)
icon = None
if task_type_item is not None:
color = task_type_item.color or get_default_entity_icon_color()
icon = get_qt_icon({
"type": "material-symbols",
"name": task_type_item.icon,
"color": get_default_entity_icon_color()
"color": color,
})
if icon is None:

View file

@ -418,7 +418,7 @@ class ExpandingTextEdit(QtWidgets.QTextEdit):
"""QTextEdit which does not have sroll area but expands height."""
def __init__(self, parent=None):
super(ExpandingTextEdit, self).__init__(parent)
super().__init__(parent)
size_policy = self.sizePolicy()
size_policy.setHeightForWidth(True)
@ -441,14 +441,18 @@ class ExpandingTextEdit(QtWidgets.QTextEdit):
margins = self.contentsMargins()
document_width = 0
if width >= margins.left() + margins.right():
document_width = width - margins.left() - margins.right()
margins_size = margins.left() + margins.right()
if width >= margins_size:
document_width = width - margins_size
document = self.document().clone()
document.setTextWidth(document_width)
return math.ceil(
margins.top() + document.size().height() + margins.bottom()
margins.top()
+ document.size().height()
+ margins.bottom()
+ 2
)
def sizeHint(self):

View file

@ -16,6 +16,7 @@ qtawesome = "0.7.3"
aiohttp-middlewares = "^2.0.0"
Click = "^8"
OpenTimelineIO = "0.17.0"
otio-burnins-adapter = "1.0.0"
opencolorio = "^2.3.2,<2.4.0"
Pillow = "9.5.0"
websocket-client = ">=0.40.0,<2"

View file

@ -11,12 +11,12 @@ theme:
- media: "(prefers-color-scheme: dark)"
scheme: slate
toggle:
icon: material/toggle-switch-off-outline
icon: material/weather-sunny
name: Switch to light mode
- media: "(prefers-color-scheme: light)"
scheme: default
toggle:
icon: material/toggle-switch
icon: material/weather-night
name: Switch to dark mode
logo: img/ay-symbol-blackw-full.png
favicon: img/favicon.ico

9
mkdocs_requirements.txt Normal file
View file

@ -0,0 +1,9 @@
mkdocs-material >= 9.6.7
mkdocs-autoapi >= 0.4.0
mkdocstrings-python >= 1.16.2
mkdocs-minify-plugin >= 0.8.0
markdown-checklist >= 0.4.4
mdx-gh-links >= 0.4
pymdown-extensions >= 10.14.3
mike >= 2.1.3
mkdocstrings-shell >= 1.0.2