Merge branch 'develop' into enhancement/1322-tray-launcher-comments-for-new-launcher-action-design

This commit is contained in:
Jakub Trllo 2025-06-24 18:13:20 +02:00 committed by GitHub
commit 0fc0216551
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 2079 additions and 546 deletions

View file

@ -1,24 +1,28 @@
"""Plugins for loading representations and products into host applications."""
from __future__ import annotations
import os
import logging
from typing import Any, Type, Optional
from abc import abstractmethod
from ayon_core.settings import get_project_settings
from abc import abstractmethod
import logging
import os
from typing import Any, Optional, Type
from ayon_core.pipeline.plugin_discover import (
deregister_plugin,
deregister_plugin_path,
discover,
register_plugin,
register_plugin_path,
deregister_plugin,
deregister_plugin_path
)
from ayon_core.settings import get_project_settings
from .utils import get_representation_path_from_context
class LoaderPlugin(list):
"""Load representation into host application"""
product_types = set()
product_types: set[str] = set()
product_base_types: Optional[set[str]] = None
representations = set()
extensions = {"*"}
order = 0
@ -61,12 +65,12 @@ class LoaderPlugin(list):
if not plugin_settings:
return
print(">>> We have preset for {}".format(plugin_name))
print(f">>> We have preset for {plugin_name}")
for option, value in plugin_settings.items():
if option == "enabled" and value is False:
print(" - is disabled by preset")
else:
print(" - setting `{}`: `{}`".format(option, value))
print(f" - setting `{option}`: `{value}`")
setattr(cls, option, value)
@classmethod
@ -79,7 +83,6 @@ class LoaderPlugin(list):
Returns:
bool: Representation has valid extension
"""
if "*" in cls.extensions:
return True
@ -124,18 +127,34 @@ class LoaderPlugin(list):
"""
plugin_repre_names = cls.get_representations()
plugin_product_types = cls.product_types
# If the product base type isn't defined on the loader plugin,
# then we will use the product types.
plugin_product_filter = cls.product_base_types
if plugin_product_filter is None:
plugin_product_filter = cls.product_types
if plugin_product_filter:
plugin_product_filter = set(plugin_product_filter)
repre_entity = context.get("representation")
product_entity = context["product"]
# If no representation names, product types or extensions are defined
# then loader is not compatible with any context.
if (
not plugin_repre_names
or not plugin_product_types
or not plugin_product_filter
or not cls.extensions
):
return False
repre_entity = context.get("representation")
# If no representation entity is provided then loader is not
# compatible with context.
if not repre_entity:
return False
# Check the compatibility with the representation names.
plugin_repre_names = set(plugin_repre_names)
if (
"*" not in plugin_repre_names
@ -143,17 +162,34 @@ class LoaderPlugin(list):
):
return False
# Check the compatibility with the extension of the representation.
if not cls.has_valid_extension(repre_entity):
return False
plugin_product_types = set(plugin_product_types)
if "*" in plugin_product_types:
product_type = product_entity.get("productType")
product_base_type = product_entity.get("productBaseType")
# Use product base type if defined, otherwise use product type.
product_filter = product_base_type
# If there is no product base type defined in the product entity,
# then we will use the product type.
if product_filter is None:
product_filter = product_type
# If wildcard is used in product types or base types,
# then we will consider the loader compatible with any product type.
if "*" in plugin_product_filter:
return True
product_entity = context["product"]
product_type = product_entity["productType"]
# compatibility with legacy loader
if cls.product_base_types is None and product_base_type:
cls.log.error(
f"Loader {cls.__name__} is doesn't specify "
"`product_base_types` but product entity has "
f"`productBaseType` defined as `{product_base_type}`. "
)
return product_type in plugin_product_types
return product_filter in plugin_product_filter
@classmethod
def get_representations(cls):
@ -208,19 +244,17 @@ class LoaderPlugin(list):
bool: Whether the container was deleted
"""
raise NotImplementedError("Loader.remove() must be "
"implemented by subclass")
@classmethod
def get_options(cls, contexts):
"""
Returns static (cls) options or could collect from 'contexts'.
"""Returns static (cls) options or could collect from 'contexts'.
Args:
contexts (list): of repre or product contexts
Returns:
(list)
Args:
contexts (list): of repre or product contexts
Returns:
(list)
"""
return cls.options or []
@ -347,10 +381,8 @@ def discover_loader_plugins(project_name=None):
plugin.apply_settings(project_settings)
except Exception:
log.warning(
"Failed to apply settings to loader {}".format(
plugin.__name__
),
exc_info=True,
f"Failed to apply settings to loader {plugin.__name__}",
exc_info=True
)
compatible_hooks = []
for hook_cls in sorted_hooks:

View file

@ -905,6 +905,70 @@ ActionMenuPopup ActionsView {
border-radius: 0.1em;
}
/* Launcher specific stylesheets */
FiltersBar {
background: {color:bg-inputs};
border: 1px solid {color:border};
border-radius: 5px;
}
FiltersBar #ScrollArea {
background: {color:bg-inputs};
}
FiltersBar #SearchButton {
background: transparent;
}
FiltersBar #BackButton {
background: transparent;
}
FiltersBar #BackButton:hover {
background: {color:bg-buttons-hover};
}
FiltersBar #ConfirmButton {
background: #91CDFB;
color: #03344D;
}
FiltersPopup #PopupWrapper, FilterValuePopup #PopupWrapper {
border-radius: 5px;
background: {color:bg-inputs};
}
FiltersPopup #ShadowFrame, FilterValuePopup #ShadowFrame {
border-radius: 5px;
background: rgba(0, 0, 0, 0.5);
}
FilterItemButton, FilterValueItemButton {
border-radius: 5px;
background: transparent;
}
FilterItemButton:hover, FilterValueItemButton:hover {
background: {color:bg-buttons-hover};
}
FilterValueItemButton[selected="1"] {
background: {color:bg-view-selection};
}
FilterValueItemButton[selected="1"]:hover {
background: {color:bg-view-selection-hover};
}
FilterValueItemsView #ContentWidget {
background: {color:bg-inputs};
}
SearchItemDisplayWidget {
border-radius: 5px;
}
SearchItemDisplayWidget:hover {
background: {color:bg-buttons};
}
SearchItemDisplayWidget #ValueWidget {
border-radius: 3px;
background: {color:bg-buttons};
}
/* Subset Manager */
#SubsetManagerDetailsText {}
#SubsetManagerDetailsText[state="invalid"] {

View file

@ -2,6 +2,7 @@
from .cache import CacheItem, NestedCacheItem
from .projects import (
TagItem,
StatusItem,
StatusStates,
ProjectItem,
@ -25,6 +26,7 @@ __all__ = (
"CacheItem",
"NestedCacheItem",
"TagItem",
"StatusItem",
"StatusStates",
"ProjectItem",

View file

@ -100,12 +100,14 @@ class TaskItem:
label: Union[str, None],
task_type: str,
parent_id: str,
tags: list[str],
):
self.task_id = task_id
self.name = name
self.label = label
self.task_type = task_type
self.parent_id = parent_id
self.tags = tags
self._full_label = None
@ -145,6 +147,7 @@ class TaskItem:
"label": self.label,
"parent_id": self.parent_id,
"task_type": self.task_type,
"tags": self.tags,
}
@classmethod
@ -176,7 +179,8 @@ def _get_task_items_from_tasks(tasks):
task["name"],
task["label"],
task["type"],
folder_id
folder_id,
task["tags"],
))
return output
@ -217,6 +221,8 @@ class HierarchyModel(object):
lifetime = 60 # A minute
def __init__(self, controller):
self._tags_by_entity_type = NestedCacheItem(
levels=1, default_factory=dict, lifetime=self.lifetime)
self._folders_items = NestedCacheItem(
levels=1, default_factory=dict, lifetime=self.lifetime)
self._folders_by_id = NestedCacheItem(
@ -235,6 +241,7 @@ class HierarchyModel(object):
self._controller = controller
def reset(self):
self._tags_by_entity_type.reset()
self._folders_items.reset()
self._folders_by_id.reset()
@ -514,6 +521,31 @@ class HierarchyModel(object):
return output
def get_available_tags_by_entity_type(
self, project_name: str
) -> dict[str, list[str]]:
"""Get available tags for all entity types in a project."""
cache = self._tags_by_entity_type.get(project_name)
if not cache.is_valid:
tags = None
if project_name:
response = ayon_api.get(f"projects/{project_name}/tags")
if response.status_code == 200:
tags = response.data
# Fake empty tags
if tags is None:
tags = {
"folders": [],
"tasks": [],
"products": [],
"versions": [],
"representations": [],
"workfiles": []
}
cache.update_data(tags)
return cache.get_data()
@contextlib.contextmanager
def _folder_refresh_event_manager(self, project_name, sender):
self._folders_refreshing.add(project_name)
@ -617,6 +649,6 @@ class HierarchyModel(object):
tasks = list(ayon_api.get_tasks(
project_name,
folder_ids=[folder_id],
fields={"id", "name", "label", "folderId", "type"}
fields={"id", "name", "label", "folderId", "type", "tags"}
))
return _get_task_items_from_tasks(tasks)

View file

@ -1,4 +1,5 @@
from __future__ import annotations
import contextlib
from abc import ABC, abstractmethod
from typing import Dict, Any
@ -74,6 +75,13 @@ class StatusItem:
)
@dataclass
class TagItem:
"""Tag definition set on project anatomy."""
name: str
color: str
class FolderTypeItem:
"""Item representing folder type of project.
@ -292,6 +300,22 @@ class ProjectsModel(object):
project_cache.update_data(entity)
return project_cache.get_data()
def get_project_anatomy_tags(self, project_name: str) -> list[TagItem]:
"""Get project anatomy tags.
Args:
project_name (str): Project name.
Returns:
list[TagItem]: Tag definitions.
"""
project_entity = self.get_project_entity(project_name)
return [
TagItem(tag["name"], tag["color"])
for tag in project_entity["tags"]
]
def get_project_status_items(self, project_name, sender):
"""Get project status items.

View file

@ -1,11 +1,15 @@
"""Abstract base classes for loader tool."""
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import List
from typing import Iterable, Any, Optional
from ayon_core.lib.attribute_definitions import (
AbstractAttrDef,
serialize_attr_defs,
deserialize_attr_defs,
serialize_attr_defs,
)
from ayon_core.tools.common_models import TaskItem, TagItem
class ProductTypeItem:
@ -16,7 +20,7 @@ class ProductTypeItem:
icon (dict[str, Any]): Product type icon definition.
"""
def __init__(self, name, icon):
def __init__(self, name: str, icon: dict[str, Any]):
self.name = name
self.icon = icon
@ -31,6 +35,41 @@ class ProductTypeItem:
return cls(**data)
class ProductBaseTypeItem:
"""Item representing the product base type."""
def __init__(self, name: str, icon: dict[str, Any]):
"""Initialize product base type item."""
self.name = name
self.icon = icon
def to_data(self) -> dict[str, Any]:
"""Convert item to data dictionary.
Returns:
dict[str, Any]: Data representation of the item.
"""
return {
"name": self.name,
"icon": self.icon,
}
@classmethod
def from_data(
cls, data: dict[str, Any]) -> ProductBaseTypeItem:
"""Create item from data dictionary.
Args:
data (dict[str, Any]): Data to create item from.
Returns:
ProductBaseTypeItem: Item created from the provided data.
"""
return cls(**data)
class ProductItem:
"""Product item with it versions.
@ -49,35 +88,41 @@ class ProductItem:
def __init__(
self,
product_id,
product_type,
product_name,
product_icon,
product_type_icon,
product_in_scene,
group_name,
folder_id,
folder_label,
version_items,
product_id: str,
product_type: str,
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,
version_items: dict[str, VersionItem],
product_in_scene: bool,
):
self.product_id = product_id
self.product_type = product_type
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
self.folder_label = folder_label
self.version_items = version_items
def to_data(self):
def to_data(self) -> dict[str, Any]:
return {
"product_id": self.product_id,
"product_type": self.product_type,
"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,
@ -113,6 +158,7 @@ class VersionItem:
published_time (Union[str, None]): Published time in format
'%Y%m%dT%H%M%SZ'.
status (Union[str, None]): Status name.
tags (Union[list[str], None]): Tags.
author (Union[str, None]): Author.
frame_range (Union[str, None]): Frame range.
duration (Union[int, None]): Duration.
@ -124,21 +170,22 @@ class VersionItem:
def __init__(
self,
version_id,
version,
is_hero,
product_id,
task_id,
thumbnail_id,
published_time,
author,
status,
frame_range,
duration,
handles,
step,
comment,
source,
version_id: str,
version: int,
is_hero: bool,
product_id: str,
task_id: Optional[str],
thumbnail_id: Optional[str],
published_time: Optional[str],
tags: Optional[list[str]],
author: Optional[str],
status: Optional[str],
frame_range: Optional[str],
duration: Optional[int],
handles: Optional[str],
step: Optional[int],
comment: Optional[str],
source: Optional[str],
):
self.version_id = version_id
self.product_id = product_id
@ -148,6 +195,7 @@ class VersionItem:
self.is_hero = is_hero
self.published_time = published_time
self.author = author
self.tags = tags
self.status = status
self.frame_range = frame_range
self.duration = duration
@ -198,7 +246,7 @@ class VersionItem:
def __le__(self, other):
return self.__eq__(other) or self.__lt__(other)
def to_data(self):
def to_data(self) -> dict[str, Any]:
return {
"version_id": self.version_id,
"product_id": self.product_id,
@ -208,6 +256,7 @@ class VersionItem:
"is_hero": self.is_hero,
"published_time": self.published_time,
"author": self.author,
"tags": self.tags,
"status": self.status,
"frame_range": self.frame_range,
"duration": self.duration,
@ -218,7 +267,7 @@ class VersionItem:
}
@classmethod
def from_data(cls, data):
def from_data(cls, data: dict[str, Any]) -> VersionItem:
return cls(**data)
@ -354,8 +403,8 @@ class ProductTypesFilter:
Defines the filtering for product types.
"""
def __init__(self, product_types: List[str], is_allow_list: bool):
self.product_types: List[str] = product_types
def __init__(self, product_types: list[str], is_allow_list: bool):
self.product_types: list[str] = product_types
self.is_allow_list: bool = is_allow_list
@ -517,8 +566,21 @@ class FrontendLoaderController(_BaseLoaderController):
Returns:
list[ProjectItem]: List of project items.
"""
"""
pass
@abstractmethod
def get_project_anatomy_tags(self, project_name: str) -> list[TagItem]:
"""Tag items defined on project anatomy.
Args:
project_name (str): Project name.
Returns:
list[TagItem]: Tag definition items.
"""
pass
@abstractmethod
@ -542,7 +604,12 @@ class FrontendLoaderController(_BaseLoaderController):
pass
@abstractmethod
def get_task_items(self, project_name, folder_ids, sender=None):
def get_task_items(
self,
project_name: str,
folder_ids: Iterable[str],
sender: Optional[str] = None,
) -> list[TaskItem]:
"""Task items for folder ids.
Args:
@ -590,6 +657,21 @@ class FrontendLoaderController(_BaseLoaderController):
"""
pass
@abstractmethod
def get_available_tags_by_entity_type(
self, project_name: str
) -> dict[str, list[str]]:
"""Get available tags by entity type.
Args:
project_name (str): Project name.
Returns:
dict[str, list[str]]: Available tags by entity type.
"""
pass
@abstractmethod
def get_project_status_items(self, project_name, sender=None):
"""Items for all projects available on server.

View file

@ -13,6 +13,7 @@ from ayon_core.tools.common_models import (
ProjectsModel,
HierarchyModel,
ThumbnailsModel,
TagItem,
)
from .abstract import (
@ -223,6 +224,16 @@ class LoaderController(BackendLoaderController, FrontendLoaderController):
output[folder_id] = label
return output
def get_available_tags_by_entity_type(
self, project_name: str
) -> dict[str, list[str]]:
return self._hierarchy_model.get_available_tags_by_entity_type(
project_name
)
def get_project_anatomy_tags(self, project_name: str) -> list[TagItem]:
return self._projects_model.get_project_anatomy_tags(project_name)
def get_product_items(self, project_name, folder_ids, sender=None):
return self._products_model.get_product_items(
project_name, folder_ids, sender)

View file

@ -1,24 +1,34 @@
"""Products model for loader tools."""
from __future__ import annotations
import collections
import contextlib
from typing import TYPE_CHECKING, Iterable, Optional
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.loader.abstract import (
ProductTypeItem,
ProductBaseTypeItem,
ProductItem,
VersionItem,
RepreItem,
)
if TYPE_CHECKING:
from ayon_api.typing import ProductBaseTypeDict, ProductDict, VersionDict
PRODUCTS_MODEL_SENDER = "products.model"
def version_item_from_entity(version):
version_attribs = version["attrib"]
tags = version["tags"]
frame_start = version_attribs.get("frameStart")
frame_end = version_attribs.get("frameEnd")
handle_start = version_attribs.get("handleStart")
@ -59,6 +69,7 @@ def version_item_from_entity(version):
thumbnail_id=version["thumbnailId"],
published_time=published_time,
author=author,
tags=tags,
status=version["status"],
frame_range=frame_range,
duration=duration,
@ -70,9 +81,10 @@ def version_item_from_entity(version):
def product_item_from_entity(
product_entity,
product_entity: ProductDict,
version_entities,
product_type_items_by_name,
product_type_items_by_name: dict[str, ProductTypeItem],
product_base_type_items_by_name: dict[str, ProductBaseTypeItem],
folder_label,
product_in_scene,
):
@ -88,8 +100,20 @@ def product_item_from_entity(
# Cache the item for future use
product_type_items_by_name[product_type] = product_type_item
product_type_icon = product_type_item.icon
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",
@ -103,9 +127,11 @@ def product_item_from_entity(
return ProductItem(
product_id=product_entity["id"],
product_type=product_type,
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"],
@ -114,7 +140,8 @@ def product_item_from_entity(
)
def product_type_item_from_data(product_type_data):
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"]
@ -127,7 +154,29 @@ def product_type_item_from_data(product_type_data):
return ProductTypeItem(product_type_data["name"], icon)
def create_default_product_type_item(product_type):
def product_base_type_item_from_data(
product_base_type_data: ProductBaseTypeDict
) -> ProductBaseTypeItem:
"""Create product base type item from data.
Args:
product_base_type_data (ProductBaseTypeDict): Product base type data.
Returns:
ProductBaseTypeDict: Product base type item.
"""
icon = {
"type": "awesome-font",
"name": "fa.folder",
"color": "#0091B2",
}
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",
@ -136,10 +185,28 @@ def create_default_product_type_item(product_type):
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)
class ProductsModel:
"""Model for products, version and representation.
All of the entities are product based. This model prepares data for UI
All the entities are product based. This model prepares data for UI
and caches it for faster access.
Note:
@ -161,6 +228,8 @@ class ProductsModel:
# Cache helpers
self._product_type_items_cache = NestedCacheItem(
levels=1, default_factory=list, lifetime=self.lifetime)
self._product_base_type_items_cache = NestedCacheItem(
levels=1, default_factory=list, lifetime=self.lifetime)
self._product_items_cache = NestedCacheItem(
levels=2, default_factory=dict, lifetime=self.lifetime)
self._repre_items_cache = NestedCacheItem(
@ -199,6 +268,36 @@ class ProductsModel:
])
return cache.get_data()
def get_product_base_type_items(
self,
project_name: Optional[str]) -> list[ProductBaseTypeItem]:
"""Product base type items for the project.
Args:
project_name (optional, str): Project name.
Returns:
list[ProductBaseTypeDict]: Product base type items.
"""
if not project_name:
return []
cache = self._product_base_type_items_cache[project_name]
if not cache.is_valid:
product_base_types = []
# TODO add temp implementation here when it is actually
# implemented and available on server.
if hasattr(ayon_api, "get_project_product_base_types"):
product_base_types = ayon_api.get_project_product_base_types(
project_name
)
cache.update_data([
product_base_type_item_from_data(product_base_type)
for product_base_type in product_base_types
])
return cache.get_data()
def get_product_items(self, project_name, folder_ids, sender):
"""Product items with versions for project and folder ids.
@ -449,11 +548,12 @@ class ProductsModel:
def _create_product_items(
self,
project_name,
products,
versions,
project_name: str,
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)
@ -461,6 +561,11 @@ class ProductsModel:
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)
@ -470,7 +575,13 @@ class ProductsModel:
product_type_item.name: product_type_item
for product_type_item in product_type_items
}
output = {}
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] = {}
for product in products:
product_id = product["id"]
folder_id = product["folderId"]
@ -484,6 +595,7 @@ class ProductsModel:
product,
versions,
product_type_items_by_name,
product_base_type_items_by_name,
folder_item.label,
product_id in loaded_product_ids,
)

View file

@ -1,170 +0,0 @@
from __future__ import annotations
from qtpy import QtGui, QtCore
from ._multicombobox import (
CustomPaintMultiselectComboBox,
BaseQtModel,
)
STATUS_ITEM_TYPE = 0
SELECT_ALL_TYPE = 1
DESELECT_ALL_TYPE = 2
SWAP_STATE_TYPE = 3
PRODUCT_TYPE_ROLE = QtCore.Qt.UserRole + 1
ITEM_TYPE_ROLE = QtCore.Qt.UserRole + 2
ITEM_SUBTYPE_ROLE = QtCore.Qt.UserRole + 3
class ProductTypesQtModel(BaseQtModel):
refreshed = QtCore.Signal()
def __init__(self, controller):
self._reset_filters_on_refresh = True
self._refreshing = False
self._bulk_change = False
self._items_by_name = {}
super().__init__(
item_type_role=ITEM_TYPE_ROLE,
item_subtype_role=ITEM_SUBTYPE_ROLE,
empty_values_label="No product types...",
controller=controller,
)
def is_refreshing(self):
return self._refreshing
def refresh(self, project_name):
self._refreshing = True
super().refresh(project_name)
self._reset_filters_on_refresh = False
self._refreshing = False
self.refreshed.emit()
def reset_product_types_filter_on_refresh(self):
self._reset_filters_on_refresh = True
def _get_standard_items(self) -> list[QtGui.QStandardItem]:
return list(self._items_by_name.values())
def _clear_standard_items(self):
self._items_by_name.clear()
def _prepare_new_value_items(self, project_name: str, _: bool) -> tuple[
list[QtGui.QStandardItem], list[QtGui.QStandardItem]
]:
product_type_items = self._controller.get_product_type_items(
project_name)
self._last_project = project_name
names_to_remove = set(self._items_by_name.keys())
items = []
items_filter_required = {}
for product_type_item in product_type_items:
name = product_type_item.name
names_to_remove.discard(name)
item = self._items_by_name.get(name)
# Apply filter to new items or if filters reset is requested
filter_required = self._reset_filters_on_refresh
if item is None:
filter_required = True
item = QtGui.QStandardItem(name)
item.setData(name, PRODUCT_TYPE_ROLE)
item.setEditable(False)
item.setCheckable(True)
self._items_by_name[name] = item
items.append(item)
if filter_required:
items_filter_required[name] = item
if items_filter_required:
product_types_filter = self._controller.get_product_types_filter()
for product_type, item in items_filter_required.items():
matching = (
int(product_type in product_types_filter.product_types)
+ int(product_types_filter.is_allow_list)
)
item.setCheckState(
QtCore.Qt.Checked
if matching % 2 == 0
else QtCore.Qt.Unchecked
)
items_to_remove = []
for name in names_to_remove:
items_to_remove.append(
self._items_by_name.pop(name)
)
# Uncheck all if all are checked (same result)
if all(
item.checkState() == QtCore.Qt.Checked
for item in items
):
for item in items:
item.setCheckState(QtCore.Qt.Unchecked)
return items, items_to_remove
class ProductTypesCombobox(CustomPaintMultiselectComboBox):
def __init__(self, controller, parent):
self._controller = controller
model = ProductTypesQtModel(controller)
super().__init__(
PRODUCT_TYPE_ROLE,
PRODUCT_TYPE_ROLE,
QtCore.Qt.ForegroundRole,
QtCore.Qt.DecorationRole,
item_type_role=ITEM_TYPE_ROLE,
model=model,
parent=parent
)
model.refreshed.connect(self._on_model_refresh)
self.set_placeholder_text("Product types filter...")
self._model = model
self._last_project_name = None
self._fully_disabled_filter = False
controller.register_event_callback(
"selection.project.changed",
self._on_project_change
)
controller.register_event_callback(
"projects.refresh.finished",
self._on_projects_refresh
)
self.setToolTip("Product types filter")
self.value_changed.connect(
self._on_product_type_filter_change
)
def reset_product_types_filter_on_refresh(self):
self._model.reset_product_types_filter_on_refresh()
def _on_model_refresh(self):
self.value_changed.emit()
def _on_product_type_filter_change(self):
lines = ["Product types filter"]
for item in self.get_value_info():
status_name, enabled = item
lines.append(f"{'' if enabled else ''} {status_name}")
self.setToolTip("\n".join(lines))
def _on_project_change(self, event):
project_name = event["project_name"]
self._last_project_name = project_name
self._model.refresh(project_name)
def _on_projects_refresh(self):
if self._last_project_name:
self._model.refresh(self._last_project_name)
self._on_product_type_filter_change()

View file

@ -1,3 +1,5 @@
from __future__ import annotations
import numbers
import uuid
from typing import Dict
@ -18,16 +20,19 @@ from .products_model import (
SYNC_REMOTE_SITE_AVAILABILITY,
)
STATUS_NAME_ROLE = QtCore.Qt.UserRole + 1
TASK_ID_ROLE = QtCore.Qt.UserRole + 2
COMBO_VERSION_ID_ROLE = QtCore.Qt.UserRole + 1
COMBO_TASK_ID_ROLE = QtCore.Qt.UserRole + 2
COMBO_STATUS_NAME_ROLE = QtCore.Qt.UserRole + 3
COMBO_VERSION_TAGS_ROLE = QtCore.Qt.UserRole + 4
COMBO_TASK_TAGS_ROLE = QtCore.Qt.UserRole + 5
class VersionsModel(QtGui.QStandardItemModel):
class ComboVersionsModel(QtGui.QStandardItemModel):
def __init__(self):
super().__init__()
self._items_by_id = {}
def update_versions(self, version_items):
def update_versions(self, version_items, task_tags_by_version_id):
version_ids = {
version_item.version_id
for version_item in version_items
@ -39,6 +44,7 @@ class VersionsModel(QtGui.QStandardItemModel):
item = self._items_by_id.pop(item_id)
root_item.removeRow(item.row())
version_tags_by_version_id = {}
for idx, version_item in enumerate(version_items):
version_id = version_item.version_id
@ -48,34 +54,74 @@ class VersionsModel(QtGui.QStandardItemModel):
item = QtGui.QStandardItem(label)
item.setData(version_id, QtCore.Qt.UserRole)
self._items_by_id[version_id] = item
item.setData(version_item.status, STATUS_NAME_ROLE)
item.setData(version_item.task_id, TASK_ID_ROLE)
version_tags = set(version_item.tags)
task_tags = task_tags_by_version_id[version_id]
item.setData(version_id, COMBO_VERSION_ID_ROLE)
item.setData(version_item.status, COMBO_STATUS_NAME_ROLE)
item.setData(version_item.task_id, COMBO_TASK_ID_ROLE)
item.setData("|".join(version_tags), COMBO_VERSION_TAGS_ROLE)
item.setData("|".join(task_tags), COMBO_TASK_TAGS_ROLE)
version_tags_by_version_id[version_id] = set(version_item.tags)
if item.row() != idx:
root_item.insertRow(idx, item)
class VersionsFilterModel(QtCore.QSortFilterProxyModel):
class ComboVersionsFilterModel(QtCore.QSortFilterProxyModel):
def __init__(self):
super().__init__()
self._status_filter = None
self._task_ids_filter = None
self._version_tags_filter = None
self._task_tags_filter = None
def filterAcceptsRow(self, row, parent):
index = None
if self._status_filter is not None:
if not self._status_filter:
return False
index = self.sourceModel().index(row, 0, parent)
status = index.data(STATUS_NAME_ROLE)
if index is None:
index = self.sourceModel().index(row, 0, parent)
status = index.data(COMBO_STATUS_NAME_ROLE)
if status not in self._status_filter:
return False
if self._task_ids_filter:
index = self.sourceModel().index(row, 0, parent)
task_id = index.data(TASK_ID_ROLE)
if index is None:
index = self.sourceModel().index(row, 0, parent)
task_id = index.data(COMBO_TASK_ID_ROLE)
if task_id not in self._task_ids_filter:
return False
if self._version_tags_filter is not None:
if not self._version_tags_filter:
return False
if index is None:
model = self.sourceModel()
index = model.index(row, 0, parent)
version_tags_s = index.data(COMBO_TASK_TAGS_ROLE)
version_tags = set()
if version_tags_s:
version_tags = set(version_tags_s.split("|"))
if not version_tags & self._version_tags_filter:
return False
if self._task_tags_filter is not None:
if not self._task_tags_filter:
return False
if index is None:
model = self.sourceModel()
index = model.index(row, 0, parent)
task_tags_s = index.data(COMBO_TASK_TAGS_ROLE)
task_tags = set()
if task_tags_s:
task_tags = set(task_tags_s.split("|"))
if not (task_tags & self._task_tags_filter):
return False
return True
def set_tasks_filter(self, task_ids):
@ -84,12 +130,24 @@ class VersionsFilterModel(QtCore.QSortFilterProxyModel):
self._task_ids_filter = task_ids
self.invalidateFilter()
def set_task_tags_filter(self, tags):
if self._task_tags_filter == tags:
return
self._task_tags_filter = tags
self.invalidateFilter()
def set_statuses_filter(self, status_names):
if self._status_filter == status_names:
return
self._status_filter = status_names
self.invalidateFilter()
def set_version_tags_filter(self, tags):
if self._version_tags_filter == tags:
return
self._version_tags_filter = tags
self.invalidateFilter()
class VersionComboBox(QtWidgets.QComboBox):
value_changed = QtCore.Signal(str, str)
@ -97,8 +155,8 @@ class VersionComboBox(QtWidgets.QComboBox):
def __init__(self, product_id, parent):
super().__init__(parent)
versions_model = VersionsModel()
proxy_model = VersionsFilterModel()
versions_model = ComboVersionsModel()
proxy_model = ComboVersionsFilterModel()
proxy_model.setSourceModel(versions_model)
self.setModel(proxy_model)
@ -123,6 +181,13 @@ class VersionComboBox(QtWidgets.QComboBox):
if self.currentIndex() != 0:
self.setCurrentIndex(0)
def set_task_tags_filter(self, tags):
self._proxy_model.set_task_tags_filter(tags)
if self.count() == 0:
return
if self.currentIndex() != 0:
self.setCurrentIndex(0)
def set_statuses_filter(self, status_names):
self._proxy_model.set_statuses_filter(status_names)
if self.count() == 0:
@ -130,12 +195,24 @@ class VersionComboBox(QtWidgets.QComboBox):
if self.currentIndex() != 0:
self.setCurrentIndex(0)
def set_version_tags_filter(self, tags):
self._proxy_model.set_version_tags_filter(tags)
if self.count() == 0:
return
if self.currentIndex() != 0:
self.setCurrentIndex(0)
def all_versions_filtered_out(self):
if self._items_by_id:
return self.count() == 0
return False
def update_versions(self, version_items, current_version_id):
def update_versions(
self,
version_items,
current_version_id,
task_tags_by_version_id,
):
self.blockSignals(True)
version_items = list(version_items)
version_ids = [
@ -146,7 +223,9 @@ class VersionComboBox(QtWidgets.QComboBox):
current_version_id = version_ids[0]
self._current_id = current_version_id
self._versions_model.update_versions(version_items)
self._versions_model.update_versions(
version_items, task_tags_by_version_id
)
index = version_ids.index(current_version_id)
if self.currentIndex() != index:
@ -173,6 +252,8 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
self._editor_by_id: Dict[str, VersionComboBox] = {}
self._task_ids_filter = None
self._statuses_filter = None
self._version_tags_filter = None
self._task_tags_filter = None
def displayText(self, value, locale):
if not isinstance(value, numbers.Integral):
@ -185,10 +266,26 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
widget.set_tasks_filter(task_ids)
def set_statuses_filter(self, status_names):
self._statuses_filter = set(status_names)
if status_names is not None:
status_names = set(status_names)
self._statuses_filter = status_names
for widget in self._editor_by_id.values():
widget.set_statuses_filter(status_names)
def set_version_tags_filter(self, tags):
if tags is not None:
tags = set(tags)
self._version_tags_filter = tags
for widget in self._editor_by_id.values():
widget.set_version_tags_filter(tags)
def set_task_tags_filter(self, tags):
if tags is not None:
tags = set(tags)
self._task_tags_filter = tags
for widget in self._editor_by_id.values():
widget.set_task_tags_filter(tags)
def paint(self, painter, option, index):
fg_color = index.data(QtCore.Qt.ForegroundRole)
if fg_color:
@ -200,7 +297,7 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
fg_color = None
if not fg_color:
return super(VersionDelegate, self).paint(painter, option, index)
return super().paint(painter, option, index)
if option.widget:
style = option.widget.style()
@ -263,11 +360,22 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
editor.clear()
# Current value of the index
versions = index.data(VERSION_NAME_EDIT_ROLE) or []
product_id = index.data(PRODUCT_ID_ROLE)
version_id = index.data(VERSION_ID_ROLE)
model = index.model()
while hasattr(model, "sourceModel"):
model = model.sourceModel()
versions = model.get_version_items_by_product_id(product_id)
task_tags_by_version_id = {
version_item.version_id: model.get_task_tags_by_id(
version_item.task_id
)
for version_item in versions
}
editor.update_versions(versions, version_id)
editor.update_versions(versions, version_id, task_tags_by_version_id)
editor.set_tasks_filter(self._task_ids_filter)
editor.set_task_tags_filter(self._task_tags_filter)
editor.set_statuses_filter(self._statuses_filter)
def setModelData(self, editor, model, index):

View file

@ -16,31 +16,34 @@ TASK_ID_ROLE = QtCore.Qt.UserRole + 5
PRODUCT_ID_ROLE = QtCore.Qt.UserRole + 6
PRODUCT_NAME_ROLE = QtCore.Qt.UserRole + 7
PRODUCT_TYPE_ROLE = QtCore.Qt.UserRole + 8
PRODUCT_TYPE_ICON_ROLE = QtCore.Qt.UserRole + 9
PRODUCT_IN_SCENE_ROLE = QtCore.Qt.UserRole + 10
VERSION_ID_ROLE = QtCore.Qt.UserRole + 11
VERSION_HERO_ROLE = QtCore.Qt.UserRole + 12
VERSION_NAME_ROLE = QtCore.Qt.UserRole + 13
VERSION_NAME_EDIT_ROLE = QtCore.Qt.UserRole + 14
VERSION_PUBLISH_TIME_ROLE = QtCore.Qt.UserRole + 15
VERSION_STATUS_NAME_ROLE = QtCore.Qt.UserRole + 16
VERSION_STATUS_SHORT_ROLE = QtCore.Qt.UserRole + 17
VERSION_STATUS_COLOR_ROLE = QtCore.Qt.UserRole + 18
VERSION_STATUS_ICON_ROLE = QtCore.Qt.UserRole + 19
VERSION_AUTHOR_ROLE = QtCore.Qt.UserRole + 20
VERSION_FRAME_RANGE_ROLE = QtCore.Qt.UserRole + 21
VERSION_DURATION_ROLE = QtCore.Qt.UserRole + 22
VERSION_HANDLES_ROLE = QtCore.Qt.UserRole + 23
VERSION_STEP_ROLE = QtCore.Qt.UserRole + 24
VERSION_AVAILABLE_ROLE = QtCore.Qt.UserRole + 25
VERSION_THUMBNAIL_ID_ROLE = QtCore.Qt.UserRole + 26
ACTIVE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 27
REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 28
REPRESENTATIONS_COUNT_ROLE = QtCore.Qt.UserRole + 29
SYNC_ACTIVE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 30
SYNC_REMOTE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 31
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
VERSION_NAME_ROLE = QtCore.Qt.UserRole + 14
VERSION_NAME_EDIT_ROLE = QtCore.Qt.UserRole + 15
VERSION_PUBLISH_TIME_ROLE = QtCore.Qt.UserRole + 16
VERSION_STATUS_NAME_ROLE = QtCore.Qt.UserRole + 17
VERSION_STATUS_SHORT_ROLE = QtCore.Qt.UserRole + 18
VERSION_STATUS_COLOR_ROLE = QtCore.Qt.UserRole + 19
VERSION_STATUS_ICON_ROLE = QtCore.Qt.UserRole + 20
VERSION_AUTHOR_ROLE = QtCore.Qt.UserRole + 21
VERSION_FRAME_RANGE_ROLE = QtCore.Qt.UserRole + 22
VERSION_DURATION_ROLE = QtCore.Qt.UserRole + 23
VERSION_HANDLES_ROLE = QtCore.Qt.UserRole + 24
VERSION_STEP_ROLE = QtCore.Qt.UserRole + 25
VERSION_AVAILABLE_ROLE = QtCore.Qt.UserRole + 26
VERSION_THUMBNAIL_ID_ROLE = QtCore.Qt.UserRole + 27
ACTIVE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 28
REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 29
REPRESENTATIONS_COUNT_ROLE = QtCore.Qt.UserRole + 30
SYNC_ACTIVE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 31
SYNC_REMOTE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 32
STATUS_NAME_FILTER_ROLE = QtCore.Qt.UserRole + 32
STATUS_NAME_FILTER_ROLE = QtCore.Qt.UserRole + 33
TASK_TAGS_FILTER_ROLE = QtCore.Qt.UserRole + 34
VERSION_TAGS_FILTER_ROLE = QtCore.Qt.UserRole + 35
class ProductsModel(QtGui.QStandardItemModel):
@ -49,6 +52,7 @@ class ProductsModel(QtGui.QStandardItemModel):
column_labels = [
"Product name",
"Product type",
"Product base type",
"Folder",
"Version",
"Status",
@ -79,6 +83,7 @@ class ProductsModel(QtGui.QStandardItemModel):
product_name_col = column_labels.index("Product name")
product_type_col = column_labels.index("Product type")
product_base_type_col = column_labels.index("Product base type")
folders_label_col = column_labels.index("Folder")
version_col = column_labels.index("Version")
status_col = column_labels.index("Status")
@ -93,6 +98,7 @@ class ProductsModel(QtGui.QStandardItemModel):
_display_role_mapping = {
product_name_col: QtCore.Qt.DisplayRole,
product_type_col: PRODUCT_TYPE_ROLE,
product_base_type_col: PRODUCT_BASE_TYPE_ROLE,
folders_label_col: FOLDER_LABEL_ROLE,
version_col: VERSION_NAME_ROLE,
status_col: VERSION_STATUS_NAME_ROLE,
@ -130,6 +136,7 @@ class ProductsModel(QtGui.QStandardItemModel):
self._last_folder_ids = []
self._last_project_statuses = {}
self._last_status_icons_by_name = {}
self._last_task_tags_by_task_id = {}
def get_product_item_indexes(self):
return [
@ -170,6 +177,17 @@ class ProductsModel(QtGui.QStandardItemModel):
self._last_folder_ids
)
def get_task_tags_by_id(self, task_id):
return self._last_task_tags_by_task_id.get(task_id, set())
def get_version_items_by_product_id(self, product_id: str):
product_item = self._product_items_by_id.get(product_id)
if product_item is None:
return None
version_items = list(product_item.version_items.values())
version_items.sort(reverse=True)
return version_items
def flags(self, index):
# Make the version column editable
if index.column() == self.version_col and index.data(PRODUCT_ID_ROLE):
@ -224,9 +242,9 @@ class ProductsModel(QtGui.QStandardItemModel):
product_item = self._product_items_by_id.get(product_id)
if product_item is None:
return None
product_items = list(product_item.version_items.values())
product_items.sort(reverse=True)
return product_items
version_items = list(product_item.version_items.values())
version_items.sort(reverse=True)
return version_items
if role == QtCore.Qt.EditRole:
return None
@ -422,6 +440,16 @@ class ProductsModel(QtGui.QStandardItemModel):
version_item.status
for version_item in product_item.version_items.values()
}
version_tags = set()
task_tags = set()
for version_item in product_item.version_items.values():
version_tags |= set(version_item.tags)
_task_tags = self._last_task_tags_by_task_id.get(
version_item.task_id
)
if _task_tags:
task_tags |= set(_task_tags)
if model_item is None:
product_id = product_item.product_id
model_item = QtGui.QStandardItem(product_item.product_name)
@ -432,6 +460,9 @@ class ProductsModel(QtGui.QStandardItemModel):
model_item.setData(icon, QtCore.Qt.DecorationRole)
model_item.setData(product_id, PRODUCT_ID_ROLE)
model_item.setData(product_item.product_name, PRODUCT_NAME_ROLE)
model_item.setData(
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)
@ -440,6 +471,8 @@ class ProductsModel(QtGui.QStandardItemModel):
self._items_by_id[product_id] = model_item
model_item.setData("|".join(statuses), STATUS_NAME_FILTER_ROLE)
model_item.setData("|".join(version_tags), VERSION_TAGS_FILTER_ROLE)
model_item.setData("|".join(task_tags), TASK_TAGS_FILTER_ROLE)
model_item.setData(product_item.folder_label, FOLDER_LABEL_ROLE)
in_scene = 1 if product_item.product_in_scene else 0
model_item.setData(in_scene, PRODUCT_IN_SCENE_ROLE)
@ -470,6 +503,14 @@ class ProductsModel(QtGui.QStandardItemModel):
}
self._last_status_icons_by_name = {}
task_items = self._controller.get_task_items(
project_name, folder_ids, sender=PRODUCTS_MODEL_SENDER_NAME
)
self._last_task_tags_by_task_id = {
task_item.task_id: task_item.tags
for task_item in task_items
}
active_site_icon_def = self._controller.get_active_site_icon_def(
project_name
)
@ -484,6 +525,7 @@ class ProductsModel(QtGui.QStandardItemModel):
folder_ids,
sender=PRODUCTS_MODEL_SENDER_NAME
)
product_items_by_id = {
product_item.product_id: product_item
for product_item in product_items

View file

@ -4,6 +4,7 @@ from typing import Optional
from qtpy import QtWidgets, QtCore
from ayon_core.pipeline.compatibility import is_product_base_type_supported
from ayon_core.tools.utils import (
RecursiveSortFilterProxyModel,
DeselectableTreeView,
@ -26,6 +27,8 @@ from .products_model import (
VERSION_STATUS_ICON_ROLE,
VERSION_THUMBNAIL_ID_ROLE,
STATUS_NAME_FILTER_ROLE,
VERSION_TAGS_FILTER_ROLE,
TASK_TAGS_FILTER_ROLE,
)
from .products_delegates import (
VersionDelegate,
@ -41,6 +44,8 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel):
self._product_type_filters = None
self._statuses_filter = None
self._version_tags_filter = None
self._task_tags_filter = None
self._task_ids_filter = None
self._ascending_sort = True
@ -67,6 +72,18 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel):
self._statuses_filter = statuses_filter
self.invalidateFilter()
def set_version_tags_filter(self, tags):
if self._version_tags_filter == tags:
return
self._version_tags_filter = tags
self.invalidateFilter()
def set_task_tags_filter(self, tags):
if self._task_tags_filter == tags:
return
self._task_tags_filter = tags
self.invalidateFilter()
def filterAcceptsRow(self, source_row, source_parent):
source_model = self.sourceModel()
index = source_model.index(source_row, 0, source_parent)
@ -83,6 +100,16 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel):
):
return False
if not self._accept_row_by_role_value(
index, self._version_tags_filter, VERSION_TAGS_FILTER_ROLE
):
return False
if not self._accept_row_by_role_value(
index, self._task_tags_filter, TASK_TAGS_FILTER_ROLE
):
return False
return super().filterAcceptsRow(source_row, source_parent)
def _accept_task_ids_filter(self, index):
@ -102,10 +129,11 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel):
if not filter_value:
return False
status_s = index.data(role)
for status in status_s.split("|"):
if status in filter_value:
return True
value_s = index.data(role)
if value_s:
for value in value_s.split("|"):
if value in filter_value:
return True
return False
def lessThan(self, left, right):
@ -142,6 +170,7 @@ class ProductsWidget(QtWidgets.QWidget):
default_widths = (
200, # Product name
90, # Product type
90, # Product base type
130, # Folder label
60, # Version
100, # Status
@ -261,6 +290,12 @@ class ProductsWidget(QtWidgets.QWidget):
self._controller.is_sitesync_enabled()
)
if not is_product_base_type_supported():
# Hide product base type column
products_view.setColumnHidden(
products_model.product_base_type_col, True
)
def set_name_filter(self, name):
"""Set filter of product name.
@ -290,6 +325,14 @@ class ProductsWidget(QtWidgets.QWidget):
self._version_delegate.set_statuses_filter(status_names)
self._products_proxy_model.set_statuses_filter(status_names)
def set_version_tags_filter(self, version_tags):
self._version_delegate.set_version_tags_filter(version_tags)
self._products_proxy_model.set_version_tags_filter(version_tags)
def set_task_tags_filter(self, task_tags):
self._version_delegate.set_task_tags_filter(task_tags)
self._products_proxy_model.set_task_tags_filter(task_tags)
def set_product_type_filter(self, product_type_filters):
"""

File diff suppressed because it is too large Load diff

View file

@ -1,157 +0,0 @@
from __future__ import annotations
from qtpy import QtCore, QtGui
from ayon_core.tools.utils import get_qt_icon
from ayon_core.tools.common_models import StatusItem
from ._multicombobox import (
CustomPaintMultiselectComboBox,
BaseQtModel,
)
STATUS_ITEM_TYPE = 0
SELECT_ALL_TYPE = 1
DESELECT_ALL_TYPE = 2
SWAP_STATE_TYPE = 3
STATUSES_FILTER_SENDER = "loader.statuses_filter"
STATUS_NAME_ROLE = QtCore.Qt.UserRole + 1
STATUS_SHORT_ROLE = QtCore.Qt.UserRole + 2
STATUS_COLOR_ROLE = QtCore.Qt.UserRole + 3
STATUS_ICON_ROLE = QtCore.Qt.UserRole + 4
ITEM_TYPE_ROLE = QtCore.Qt.UserRole + 5
ITEM_SUBTYPE_ROLE = QtCore.Qt.UserRole + 6
class StatusesQtModel(BaseQtModel):
def __init__(self, controller):
self._items_by_name: dict[str, QtGui.QStandardItem] = {}
self._icons_by_name_n_color: dict[str, QtGui.QIcon] = {}
super().__init__(
ITEM_TYPE_ROLE,
ITEM_SUBTYPE_ROLE,
"No statuses...",
controller,
)
def _get_standard_items(self) -> list[QtGui.QStandardItem]:
return list(self._items_by_name.values())
def _clear_standard_items(self):
self._items_by_name.clear()
def _prepare_new_value_items(
self, project_name: str, project_changed: bool
):
status_items: list[StatusItem] = (
self._controller.get_project_status_items(
project_name, sender=STATUSES_FILTER_SENDER
)
)
items = []
items_to_remove = []
if not status_items:
return items, items_to_remove
names_to_remove = set(self._items_by_name)
for row_idx, status_item in enumerate(status_items):
name = status_item.name
if name in self._items_by_name:
item = self._items_by_name[name]
names_to_remove.discard(name)
else:
item = QtGui.QStandardItem()
item.setData(ITEM_SUBTYPE_ROLE, STATUS_ITEM_TYPE)
item.setCheckState(QtCore.Qt.Unchecked)
item.setFlags(
QtCore.Qt.ItemIsEnabled
| QtCore.Qt.ItemIsSelectable
| QtCore.Qt.ItemIsUserCheckable
)
self._items_by_name[name] = item
icon = self._get_icon(status_item)
for role, value in (
(STATUS_NAME_ROLE, status_item.name),
(STATUS_SHORT_ROLE, status_item.short),
(STATUS_COLOR_ROLE, status_item.color),
(STATUS_ICON_ROLE, icon),
):
if item.data(role) != value:
item.setData(value, role)
if project_changed:
item.setCheckState(QtCore.Qt.Unchecked)
items.append(item)
for name in names_to_remove:
items_to_remove.append(self._items_by_name.pop(name))
return items, items_to_remove
def _get_icon(self, status_item: StatusItem) -> QtGui.QIcon:
name = status_item.name
color = status_item.color
unique_id = "|".join([name or "", color or ""])
icon = self._icons_by_name_n_color.get(unique_id)
if icon is not None:
return icon
icon: QtGui.QIcon = get_qt_icon({
"type": "material-symbols",
"name": status_item.icon,
"color": status_item.color
})
self._icons_by_name_n_color[unique_id] = icon
return icon
class StatusesCombobox(CustomPaintMultiselectComboBox):
def __init__(self, controller, parent):
self._controller = controller
model = StatusesQtModel(controller)
super().__init__(
STATUS_NAME_ROLE,
STATUS_SHORT_ROLE,
STATUS_COLOR_ROLE,
STATUS_ICON_ROLE,
item_type_role=ITEM_TYPE_ROLE,
model=model,
parent=parent
)
self.set_placeholder_text("Version status filter...")
self._model = model
self._last_project_name = None
self._fully_disabled_filter = False
controller.register_event_callback(
"selection.project.changed",
self._on_project_change
)
controller.register_event_callback(
"projects.refresh.finished",
self._on_projects_refresh
)
self.setToolTip("Statuses filter")
self.value_changed.connect(
self._on_status_filter_change
)
def _on_status_filter_change(self):
lines = ["Statuses filter"]
for item in self.get_value_info():
status_name, enabled = item
lines.append(f"{'' if enabled else ''} {status_name}")
self.setToolTip("\n".join(lines))
def _on_project_change(self, event):
project_name = event["project_name"]
self._last_project_name = project_name
self._model.refresh(project_name)
def _on_projects_refresh(self):
if self._last_project_name:
self._model.refresh(self._last_project_name)
self._on_status_filter_change()

View file

@ -332,10 +332,6 @@ class LoaderTasksWidget(QtWidgets.QWidget):
"selection.folders.changed",
self._on_folders_selection_changed,
)
controller.register_event_callback(
"tasks.refresh.finished",
self._on_tasks_refresh_finished
)
selection_model = tasks_view.selectionModel()
selection_model.selectionChanged.connect(self._on_selection_change)
@ -373,10 +369,6 @@ class LoaderTasksWidget(QtWidgets.QWidget):
def _clear(self):
self._tasks_model.clear()
def _on_tasks_refresh_finished(self, event):
if event["sender"] != TASKS_MODEL_SENDER_NAME:
self._set_project_name(event["project_name"])
def _on_folders_selection_changed(self, event):
project_name = event["project_name"]
folder_ids = event["folder_ids"]

View file

@ -11,16 +11,24 @@ from ayon_core.tools.utils import (
)
from ayon_core.tools.utils.lib import center_window
from ayon_core.tools.utils import ProjectsCombobox
from ayon_core.tools.common_models import StatusItem
from ayon_core.tools.loader.abstract import ProductTypeItem
from ayon_core.tools.loader.control import LoaderController
from .folders_widget import LoaderFoldersWidget
from .tasks_widget import LoaderTasksWidget
from .products_widget import ProductsWidget
from .product_types_combo import ProductTypesCombobox
from .product_group_dialog import ProductGroupDialog
from .info_widget import InfoWidget
from .repres_widget import RepresentationsWidget
from .statuses_combo import StatusesCombobox
from .search_bar import FiltersBar, FilterDefinition
FIND_KEY_SEQUENCE = QtGui.QKeySequence(
QtCore.Qt.Modifier.CTRL | QtCore.Qt.Key_F
)
GROUP_KEY_SEQUENCE = QtGui.QKeySequence(
QtCore.Qt.Modifier.CTRL | QtCore.Qt.Key_G
)
class LoadErrorMessageBox(ErrorMessageBox):
@ -182,29 +190,19 @@ class LoaderWindow(QtWidgets.QWidget):
products_wrap_widget = QtWidgets.QWidget(main_splitter)
products_inputs_widget = QtWidgets.QWidget(products_wrap_widget)
products_filter_input = PlaceholderLineEdit(products_inputs_widget)
products_filter_input.setPlaceholderText("Product name filter...")
product_types_filter_combo = ProductTypesCombobox(
controller, products_inputs_widget
)
product_status_filter_combo = StatusesCombobox(controller, self)
search_bar = FiltersBar(products_inputs_widget)
product_group_checkbox = QtWidgets.QCheckBox(
"Enable grouping", products_inputs_widget)
product_group_checkbox.setChecked(True)
products_widget = ProductsWidget(controller, products_wrap_widget)
products_inputs_layout = QtWidgets.QHBoxLayout(products_inputs_widget)
products_inputs_layout.setContentsMargins(0, 0, 0, 0)
products_inputs_layout.addWidget(products_filter_input, 1)
products_inputs_layout.addWidget(product_types_filter_combo, 1)
products_inputs_layout.addWidget(product_status_filter_combo, 1)
products_inputs_layout.addWidget(search_bar, 1)
products_inputs_layout.addWidget(product_group_checkbox, 0)
products_widget = ProductsWidget(controller, products_wrap_widget)
products_wrap_layout = QtWidgets.QVBoxLayout(products_wrap_widget)
products_wrap_layout.setContentsMargins(0, 0, 0, 0)
products_wrap_layout.addWidget(products_inputs_widget, 0)
@ -250,15 +248,7 @@ class LoaderWindow(QtWidgets.QWidget):
folders_filter_input.textChanged.connect(
self._on_folder_filter_change
)
products_filter_input.textChanged.connect(
self._on_product_filter_change
)
product_types_filter_combo.value_changed.connect(
self._on_product_type_filter_change
)
product_status_filter_combo.value_changed.connect(
self._on_status_filter_change
)
search_bar.filter_changed.connect(self._on_filter_change)
product_group_checkbox.stateChanged.connect(
self._on_product_group_change
)
@ -316,9 +306,7 @@ class LoaderWindow(QtWidgets.QWidget):
self._tasks_widget = tasks_widget
self._products_filter_input = products_filter_input
self._product_types_filter_combo = product_types_filter_combo
self._product_status_filter_combo = product_status_filter_combo
self._search_bar = search_bar
self._product_group_checkbox = product_group_checkbox
self._products_widget = products_widget
@ -337,6 +325,8 @@ class LoaderWindow(QtWidgets.QWidget):
self._selected_folder_ids = set()
self._selected_version_ids = set()
self._set_product_type_filters = True
self._products_widget.set_enable_grouping(
self._product_group_checkbox.isChecked()
)
@ -356,22 +346,24 @@ class LoaderWindow(QtWidgets.QWidget):
def closeEvent(self, event):
super().closeEvent(event)
(
self
._product_types_filter_combo
.reset_product_types_filter_on_refresh()
)
self._reset_on_show = True
def keyPressEvent(self, event):
modifiers = event.modifiers()
ctrl_pressed = QtCore.Qt.ControlModifier & modifiers
if hasattr(event, "keyCombination"):
combination = event.keyCombination()
else:
combination = QtGui.QKeySequence(event.modifiers() | event.key())
if (
FIND_KEY_SEQUENCE == combination
and not event.isAutoRepeat()
):
self._search_bar.show_filters_popup()
event.setAccepted(True)
return
# Grouping products on pressing Ctrl + G
if (
ctrl_pressed
and event.key() == QtCore.Qt.Key_G
GROUP_KEY_SEQUENCE == combination
and not event.isAutoRepeat()
):
self._show_group_dialog()
@ -435,20 +427,30 @@ class LoaderWindow(QtWidgets.QWidget):
self._product_group_checkbox.isChecked()
)
def _on_product_filter_change(self, text):
self._products_widget.set_name_filter(text)
def _on_filter_change(self, filter_name):
if filter_name == "product_name":
self._products_widget.set_name_filter(
self._search_bar.get_filter_value("product_name")
)
elif filter_name == "product_types":
product_types = self._search_bar.get_filter_value("product_types")
self._products_widget.set_product_type_filter(product_types)
elif filter_name == "statuses":
status_names = self._search_bar.get_filter_value("statuses")
self._products_widget.set_statuses_filter(status_names)
elif filter_name == "version_tags":
version_tags = self._search_bar.get_filter_value("version_tags")
self._products_widget.set_version_tags_filter(version_tags)
elif filter_name == "task_tags":
task_tags = self._search_bar.get_filter_value("task_tags")
self._products_widget.set_task_tags_filter(task_tags)
def _on_tasks_selection_change(self, event):
self._products_widget.set_tasks_filter(event["task_ids"])
def _on_status_filter_change(self):
status_names = self._product_status_filter_combo.get_value()
self._products_widget.set_statuses_filter(status_names)
def _on_product_type_filter_change(self):
product_types = self._product_types_filter_combo.get_value()
self._products_widget.set_product_type_filter(product_types)
def _on_merged_products_selection_change(self):
items = self._products_widget.get_selected_merged_products()
self._folders_widget.set_merged_products_selection(items)
@ -480,6 +482,7 @@ class LoaderWindow(QtWidgets.QWidget):
self._projects_combobox.set_current_context_project(project_name)
if not self._refresh_handler.project_refreshed:
self._projects_combobox.refresh()
self._update_filters()
def _on_load_finished(self, event):
error_info = event["error_info"]
@ -491,6 +494,124 @@ class LoaderWindow(QtWidgets.QWidget):
def _on_project_selection_changed(self, event):
self._selected_project_name = event["project_name"]
self._update_filters()
def _update_filters(self):
project_name = self._selected_project_name
if not project_name:
self._search_bar.set_search_items([])
return
product_type_items: list[ProductTypeItem] = (
self._controller.get_product_type_items(project_name)
)
status_items: list[StatusItem] = (
self._controller.get_project_status_items(project_name)
)
tags_by_entity_type = (
self._controller.get_available_tags_by_entity_type(project_name)
)
tag_items = self._controller.get_project_anatomy_tags(project_name)
tag_color_by_name = {
tag_item.name: tag_item.color
for tag_item in tag_items
}
filter_product_type_items = [
{
"value": item.name,
"icon": item.icon,
}
for item in product_type_items
]
filter_status_items = [
{
"icon": {
"type": "material-symbols",
"name": status_item.icon,
"color": status_item.color
},
"color": status_item.color,
"value": status_item.name,
}
for status_item in status_items
]
version_tags = [
{
"value": tag_name,
"color": tag_color_by_name.get(tag_name),
}
for tag_name in tags_by_entity_type.get("versions") or []
]
task_tags = [
{
"value": tag_name,
"color": tag_color_by_name.get(tag_name),
}
for tag_name in tags_by_entity_type.get("tasks") or []
]
self._search_bar.set_search_items([
FilterDefinition(
name="product_name",
title="Product name",
filter_type="text",
icon=None,
placeholder="Product name filter...",
items=None,
),
FilterDefinition(
name="product_types",
title="Product type",
filter_type="list",
icon=None,
items=filter_product_type_items,
),
FilterDefinition(
name="statuses",
title="Statuses",
filter_type="list",
icon=None,
items=filter_status_items,
),
FilterDefinition(
name="version_tags",
title="Version tags",
filter_type="list",
icon=None,
items=version_tags,
),
FilterDefinition(
name="task_tags",
title="Task tags",
filter_type="list",
icon=None,
items=task_tags,
),
])
# Set product types filter from settings
if self._set_product_type_filters:
self._set_product_type_filters = False
product_types_filter = self._controller.get_product_types_filter()
product_types = []
for item in filter_product_type_items:
product_type = item["value"]
matching = (
int(product_type in product_types_filter.product_types)
+ int(product_types_filter.is_allow_list)
)
if matching % 2 == 0:
product_types.append(product_type)
if (
product_types
and len(product_types) < len(filter_product_type_items)
):
self._search_bar.set_filter_value(
"product_types",
product_types
)
def _on_folders_selection_changed(self, event):
self._selected_folder_ids = set(event["folder_ids"])

View file

@ -348,8 +348,6 @@ class ScreenMarquee(QtCore.QObject):
# QtGui.QPainter.Antialiasing
# | QtGui.QPainter.SmoothPixmapTransform
# )
# if hasattr(QtGui.QPainter, "HighQualityAntialiasing"):
# render_hints |= QtGui.QPainter.HighQualityAntialiasing
# pix_painter.setRenderHints(render_hints)
# for item in screen_pixes:
# (screen_pix, offset) = item

View file

@ -135,8 +135,6 @@ class ThumbnailPainterWidget(QtWidgets.QWidget):
QtGui.QPainter.Antialiasing
| QtGui.QPainter.SmoothPixmapTransform
)
if hasattr(QtGui.QPainter, "HighQualityAntialiasing"):
render_hints |= QtGui.QPainter.HighQualityAntialiasing
pix_painter.setRenderHints(render_hints)
pix_painter.drawPixmap(pos_x, pos_y, scaled_pix)
@ -171,8 +169,6 @@ class ThumbnailPainterWidget(QtWidgets.QWidget):
QtGui.QPainter.Antialiasing
| QtGui.QPainter.SmoothPixmapTransform
)
if hasattr(QtGui.QPainter, "HighQualityAntialiasing"):
render_hints |= QtGui.QPainter.HighQualityAntialiasing
pix_painter.setRenderHints(render_hints)
tiled_rect = QtCore.QRectF(
@ -265,8 +261,6 @@ class ThumbnailPainterWidget(QtWidgets.QWidget):
QtGui.QPainter.Antialiasing
| QtGui.QPainter.SmoothPixmapTransform
)
if hasattr(QtGui.QPainter, "HighQualityAntialiasing"):
render_hints |= QtGui.QPainter.HighQualityAntialiasing
final_painter.setRenderHints(render_hints)

View file

@ -65,7 +65,7 @@ class AlphaSlider(QtWidgets.QSlider):
painter.fillRect(event.rect(), QtCore.Qt.transparent)
painter.setRenderHint(QtGui.QPainter.HighQualityAntialiasing)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
horizontal = self.orientation() == QtCore.Qt.Horizontal

View file

@ -261,7 +261,7 @@ class QtColorTriangle(QtWidgets.QWidget):
pix = self.bg_image.copy()
pix_painter = QtGui.QPainter(pix)
pix_painter.setRenderHint(QtGui.QPainter.HighQualityAntialiasing)
pix_painter.setRenderHint(QtGui.QPainter.Antialiasing)
trigon_path = QtGui.QPainterPath()
trigon_path.moveTo(self.point_a)

View file

@ -118,9 +118,6 @@ def paint_image_with_color(image, color):
QtGui.QPainter.Antialiasing
| QtGui.QPainter.SmoothPixmapTransform
)
# Deprecated since 5.14
if hasattr(QtGui.QPainter, "HighQualityAntialiasing"):
render_hints |= QtGui.QPainter.HighQualityAntialiasing
painter.setRenderHints(render_hints)
painter.setClipRegion(alpha_region)

View file

@ -58,7 +58,7 @@ class NiceSlider(QtWidgets.QSlider):
painter.fillRect(event.rect(), QtCore.Qt.transparent)
painter.setRenderHint(QtGui.QPainter.HighQualityAntialiasing)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
horizontal = self.orientation() == QtCore.Qt.Horizontal

View file

@ -205,8 +205,6 @@ class ThumbnailPainterWidget(QtWidgets.QWidget):
QtGui.QPainter.Antialiasing
| QtGui.QPainter.SmoothPixmapTransform
)
if hasattr(QtGui.QPainter, "HighQualityAntialiasing"):
render_hints |= QtGui.QPainter.HighQualityAntialiasing
pix_painter.setRenderHints(render_hints)
pix_painter.drawPixmap(pos_x, pos_y, scaled_pix)
@ -241,8 +239,6 @@ class ThumbnailPainterWidget(QtWidgets.QWidget):
QtGui.QPainter.Antialiasing
| QtGui.QPainter.SmoothPixmapTransform
)
if hasattr(QtGui.QPainter, "HighQualityAntialiasing"):
render_hints |= QtGui.QPainter.HighQualityAntialiasing
pix_painter.setRenderHints(render_hints)
tiled_rect = QtCore.QRectF(
@ -335,8 +331,6 @@ class ThumbnailPainterWidget(QtWidgets.QWidget):
QtGui.QPainter.Antialiasing
| QtGui.QPainter.SmoothPixmapTransform
)
if hasattr(QtGui.QPainter, "HighQualityAntialiasing"):
render_hints |= QtGui.QPainter.HighQualityAntialiasing
final_painter.setRenderHints(render_hints)

View file

@ -462,7 +462,7 @@ class BaseClickableFrame(QtWidgets.QFrame):
Callback is defined by overriding `_mouse_release_callback`.
"""
def __init__(self, parent):
super(BaseClickableFrame, self).__init__(parent)
super().__init__(parent)
self._mouse_pressed = False
@ -470,17 +470,23 @@ class BaseClickableFrame(QtWidgets.QFrame):
pass
def mousePressEvent(self, event):
super().mousePressEvent(event)
if event.isAccepted():
return
if event.button() == QtCore.Qt.LeftButton:
self._mouse_pressed = True
super(BaseClickableFrame, self).mousePressEvent(event)
event.accept()
def mouseReleaseEvent(self, event):
if self._mouse_pressed:
self._mouse_pressed = False
if self.rect().contains(event.pos()):
self._mouse_release_callback()
pressed, self._mouse_pressed = self._mouse_pressed, False
super().mouseReleaseEvent(event)
if event.isAccepted():
return
super(BaseClickableFrame, self).mouseReleaseEvent(event)
accepted = pressed and self.rect().contains(event.pos())
if accepted:
event.accept()
self._mouse_release_callback()
class ClickableFrame(BaseClickableFrame):
@ -624,8 +630,6 @@ class ClassicExpandBtnLabel(ExpandBtnLabel):
QtGui.QPainter.Antialiasing
| QtGui.QPainter.SmoothPixmapTransform
)
if hasattr(QtGui.QPainter, "HighQualityAntialiasing"):
render_hints |= QtGui.QPainter.HighQualityAntialiasing
painter.setRenderHints(render_hints)
painter.drawPixmap(QtCore.QPoint(pos_x, pos_y), pixmap)
painter.end()
@ -788,8 +792,6 @@ class PixmapButtonPainter(QtWidgets.QWidget):
QtGui.QPainter.Antialiasing
| QtGui.QPainter.SmoothPixmapTransform
)
if hasattr(QtGui.QPainter, "HighQualityAntialiasing"):
render_hints |= QtGui.QPainter.HighQualityAntialiasing
painter.setRenderHints(render_hints)
if self._cached_pixmap is None:
@ -1189,7 +1191,7 @@ class SquareButton(QtWidgets.QPushButton):
"""
def __init__(self, *args, **kwargs):
super(SquareButton, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
sp = self.sizePolicy()
sp.setVerticalPolicy(QtWidgets.QSizePolicy.Minimum)
@ -1198,17 +1200,17 @@ class SquareButton(QtWidgets.QPushButton):
self._ideal_width = None
def showEvent(self, event):
super(SquareButton, self).showEvent(event)
super().showEvent(event)
self._ideal_width = self.height()
self.updateGeometry()
def resizeEvent(self, event):
super(SquareButton, self).resizeEvent(event)
super().resizeEvent(event)
self._ideal_width = self.height()
self.updateGeometry()
def sizeHint(self):
sh = super(SquareButton, self).sizeHint()
sh = super().sizeHint()
ideal_width = self._ideal_width
if ideal_width is None:
ideal_width = sh.height()

View file

@ -0,0 +1,88 @@
"""Test loaders in the pipeline module."""
from ayon_core.pipeline.load import LoaderPlugin
def test_is_compatible_loader():
"""Test if a loader is compatible with a given representation."""
from ayon_core.pipeline.load import is_compatible_loader
# Create a mock representation context
context = {
"loader": "test_loader",
"representation": {"name": "test_representation"},
}
# Create a mock loader plugin
class MockLoader(LoaderPlugin):
name = "test_loader"
version = "1.0.0"
def is_compatible_loader(self, context):
return True
# Check compatibility
assert is_compatible_loader(MockLoader(), context) is True
def test_complex_is_compatible_loader():
"""Test if a loader is compatible with a complex representation."""
from ayon_core.pipeline.load import is_compatible_loader
# Create a mock complex representation context
context = {
"loader": "complex_loader",
"representation": {
"name": "complex_representation",
"extension": "exr"
},
"additional_data": {"key": "value"},
"product": {
"name": "complex_product",
"productType": "foo",
"productBaseType": "bar",
},
}
# Create a mock loader plugin
class ComplexLoaderA(LoaderPlugin):
name = "complex_loaderA"
# False because the loader doesn't specify any compatibility (missing
# wildcard for product type and product base type)
assert is_compatible_loader(ComplexLoaderA(), context) is False
class ComplexLoaderB(LoaderPlugin):
name = "complex_loaderB"
product_types = {"*"}
representations = {"*"}
# True, it is compatible with any product type
assert is_compatible_loader(ComplexLoaderB(), context) is True
class ComplexLoaderC(LoaderPlugin):
name = "complex_loaderC"
product_base_types = {"*"}
representations = {"*"}
# True, it is compatible with any product base type
assert is_compatible_loader(ComplexLoaderC(), context) is True
class ComplexLoaderD(LoaderPlugin):
name = "complex_loaderD"
product_types = {"foo"}
representations = {"*"}
# legacy loader defining compatibility only with product type
# is compatible provided the same product type is defined in context
assert is_compatible_loader(ComplexLoaderD(), context) is False
class ComplexLoaderE(LoaderPlugin):
name = "complex_loaderE"
product_types = {"foo"}
representations = {"*"}
# remove productBaseType from context to simulate legacy behavior
context["product"].pop("productBaseType", None)
assert is_compatible_loader(ComplexLoaderE(), context) is True