Merge pull request #1323 from ynput/enhancement/1309-loader-tool-add-tags-filtering

Loader tool: Add filtering bar with more filtering options
This commit is contained in:
Jakub Trllo 2025-06-24 12:22:34 +02:00 committed by GitHub
commit 915813815b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 1701 additions and 436 deletions

View file

@ -893,6 +893,70 @@ ActionMenuPopup ActionsView[mode="icon"] {
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

@ -2,13 +2,14 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Any, List, Optional
from typing import Iterable, Any, Optional
from ayon_core.lib.attribute_definitions import (
AbstractAttrDef,
deserialize_attr_defs,
serialize_attr_defs,
)
from ayon_core.tools.common_models import TaskItem, TagItem
class ProductTypeItem:
@ -16,10 +17,10 @@ class ProductTypeItem:
Args:
name (str): Product type name.
icon (dict[str, str]): Product type icon definition.
icon (dict[str, Any]): Product type icon definition.
"""
def __init__(self, name: str, icon: dict[str, str]):
def __init__(self, name: str, icon: dict[str, Any]):
self.name = name
self.icon = icon
@ -37,7 +38,7 @@ class ProductTypeItem:
class ProductBaseTypeItem:
"""Item representing the product base type."""
def __init__(self, name: str, icon: dict[str, str]):
def __init__(self, name: str, icon: dict[str, Any]):
"""Initialize product base type item."""
self.name = name
self.icon = icon
@ -76,8 +77,8 @@ class ProductItem:
product_id (str): Product id.
product_type (str): Product type.
product_name (str): Product name.
product_icon (dict[str, str]): Product icon definition.
product_type_icon (dict[str, str]): Product type icon definition.
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.
@ -91,9 +92,9 @@ class ProductItem:
product_type: str,
product_base_type: str,
product_name: str,
product_icon: dict[str, str],
product_type_icon: dict[str, str],
product_base_type_icon: dict[str, 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,
@ -157,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.
@ -175,6 +177,7 @@ class VersionItem:
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],
@ -192,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
@ -252,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,
@ -398,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
@ -561,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
@ -586,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:
@ -634,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

@ -28,6 +28,7 @@ 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")
@ -68,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,

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

@ -42,6 +42,8 @@ SYNC_ACTIVE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 31
SYNC_REMOTE_SITE_AVAILABILITY = 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):
@ -134,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 [
@ -174,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):
@ -228,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
@ -426,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)
@ -447,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)
@ -477,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
)
@ -491,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

@ -27,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,
@ -42,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
@ -68,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)
@ -84,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):
@ -103,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):
@ -298,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

@ -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):
@ -1185,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)
@ -1194,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()