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; 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 */ /* Subset Manager */
#SubsetManagerDetailsText {} #SubsetManagerDetailsText {}
#SubsetManagerDetailsText[state="invalid"] { #SubsetManagerDetailsText[state="invalid"] {

View file

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

View file

@ -100,12 +100,14 @@ class TaskItem:
label: Union[str, None], label: Union[str, None],
task_type: str, task_type: str,
parent_id: str, parent_id: str,
tags: list[str],
): ):
self.task_id = task_id self.task_id = task_id
self.name = name self.name = name
self.label = label self.label = label
self.task_type = task_type self.task_type = task_type
self.parent_id = parent_id self.parent_id = parent_id
self.tags = tags
self._full_label = None self._full_label = None
@ -145,6 +147,7 @@ class TaskItem:
"label": self.label, "label": self.label,
"parent_id": self.parent_id, "parent_id": self.parent_id,
"task_type": self.task_type, "task_type": self.task_type,
"tags": self.tags,
} }
@classmethod @classmethod
@ -176,7 +179,8 @@ def _get_task_items_from_tasks(tasks):
task["name"], task["name"],
task["label"], task["label"],
task["type"], task["type"],
folder_id folder_id,
task["tags"],
)) ))
return output return output
@ -217,6 +221,8 @@ class HierarchyModel(object):
lifetime = 60 # A minute lifetime = 60 # A minute
def __init__(self, controller): def __init__(self, controller):
self._tags_by_entity_type = NestedCacheItem(
levels=1, default_factory=dict, lifetime=self.lifetime)
self._folders_items = NestedCacheItem( self._folders_items = NestedCacheItem(
levels=1, default_factory=dict, lifetime=self.lifetime) levels=1, default_factory=dict, lifetime=self.lifetime)
self._folders_by_id = NestedCacheItem( self._folders_by_id = NestedCacheItem(
@ -235,6 +241,7 @@ class HierarchyModel(object):
self._controller = controller self._controller = controller
def reset(self): def reset(self):
self._tags_by_entity_type.reset()
self._folders_items.reset() self._folders_items.reset()
self._folders_by_id.reset() self._folders_by_id.reset()
@ -514,6 +521,31 @@ class HierarchyModel(object):
return output 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 @contextlib.contextmanager
def _folder_refresh_event_manager(self, project_name, sender): def _folder_refresh_event_manager(self, project_name, sender):
self._folders_refreshing.add(project_name) self._folders_refreshing.add(project_name)
@ -617,6 +649,6 @@ class HierarchyModel(object):
tasks = list(ayon_api.get_tasks( tasks = list(ayon_api.get_tasks(
project_name, project_name,
folder_ids=[folder_id], folder_ids=[folder_id],
fields={"id", "name", "label", "folderId", "type"} fields={"id", "name", "label", "folderId", "type", "tags"}
)) ))
return _get_task_items_from_tasks(tasks) return _get_task_items_from_tasks(tasks)

View file

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import contextlib import contextlib
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Dict, Any 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: class FolderTypeItem:
"""Item representing folder type of project. """Item representing folder type of project.
@ -292,6 +300,22 @@ class ProjectsModel(object):
project_cache.update_data(entity) project_cache.update_data(entity)
return project_cache.get_data() 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): def get_project_status_items(self, project_name, sender):
"""Get project status items. """Get project status items.

View file

@ -2,13 +2,14 @@
from __future__ import annotations from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Any, List, Optional from typing import Iterable, Any, Optional
from ayon_core.lib.attribute_definitions import ( from ayon_core.lib.attribute_definitions import (
AbstractAttrDef, AbstractAttrDef,
deserialize_attr_defs, deserialize_attr_defs,
serialize_attr_defs, serialize_attr_defs,
) )
from ayon_core.tools.common_models import TaskItem, TagItem
class ProductTypeItem: class ProductTypeItem:
@ -16,10 +17,10 @@ class ProductTypeItem:
Args: Args:
name (str): Product type name. 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.name = name
self.icon = icon self.icon = icon
@ -37,7 +38,7 @@ class ProductTypeItem:
class ProductBaseTypeItem: class ProductBaseTypeItem:
"""Item representing the product base type.""" """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.""" """Initialize product base type item."""
self.name = name self.name = name
self.icon = icon self.icon = icon
@ -76,8 +77,8 @@ class ProductItem:
product_id (str): Product id. product_id (str): Product id.
product_type (str): Product type. product_type (str): Product type.
product_name (str): Product name. product_name (str): Product name.
product_icon (dict[str, str]): Product icon definition. product_icon (dict[str, Any]): Product icon definition.
product_type_icon (dict[str, str]): Product type 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). product_in_scene (bool): Is product in scene (only when used in DCC).
group_name (str): Group name. group_name (str): Group name.
folder_id (str): Folder id. folder_id (str): Folder id.
@ -91,9 +92,9 @@ class ProductItem:
product_type: str, product_type: str,
product_base_type: str, product_base_type: str,
product_name: str, product_name: str,
product_icon: dict[str, str], product_icon: dict[str, Any],
product_type_icon: dict[str, str], product_type_icon: dict[str, Any],
product_base_type_icon: dict[str, str], product_base_type_icon: dict[str, Any],
group_name: str, group_name: str,
folder_id: str, folder_id: str,
folder_label: str, folder_label: str,
@ -157,6 +158,7 @@ class VersionItem:
published_time (Union[str, None]): Published time in format published_time (Union[str, None]): Published time in format
'%Y%m%dT%H%M%SZ'. '%Y%m%dT%H%M%SZ'.
status (Union[str, None]): Status name. status (Union[str, None]): Status name.
tags (Union[list[str], None]): Tags.
author (Union[str, None]): Author. author (Union[str, None]): Author.
frame_range (Union[str, None]): Frame range. frame_range (Union[str, None]): Frame range.
duration (Union[int, None]): Duration. duration (Union[int, None]): Duration.
@ -175,6 +177,7 @@ class VersionItem:
task_id: Optional[str], task_id: Optional[str],
thumbnail_id: Optional[str], thumbnail_id: Optional[str],
published_time: Optional[str], published_time: Optional[str],
tags: Optional[list[str]],
author: Optional[str], author: Optional[str],
status: Optional[str], status: Optional[str],
frame_range: Optional[str], frame_range: Optional[str],
@ -192,6 +195,7 @@ class VersionItem:
self.is_hero = is_hero self.is_hero = is_hero
self.published_time = published_time self.published_time = published_time
self.author = author self.author = author
self.tags = tags
self.status = status self.status = status
self.frame_range = frame_range self.frame_range = frame_range
self.duration = duration self.duration = duration
@ -252,6 +256,7 @@ class VersionItem:
"is_hero": self.is_hero, "is_hero": self.is_hero,
"published_time": self.published_time, "published_time": self.published_time,
"author": self.author, "author": self.author,
"tags": self.tags,
"status": self.status, "status": self.status,
"frame_range": self.frame_range, "frame_range": self.frame_range,
"duration": self.duration, "duration": self.duration,
@ -398,8 +403,8 @@ class ProductTypesFilter:
Defines the filtering for product types. Defines the filtering for product types.
""" """
def __init__(self, product_types: List[str], is_allow_list: bool): def __init__(self, product_types: list[str], is_allow_list: bool):
self.product_types: List[str] = product_types self.product_types: list[str] = product_types
self.is_allow_list: bool = is_allow_list self.is_allow_list: bool = is_allow_list
@ -561,8 +566,21 @@ class FrontendLoaderController(_BaseLoaderController):
Returns: Returns:
list[ProjectItem]: List of project items. 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 pass
@abstractmethod @abstractmethod
@ -586,7 +604,12 @@ class FrontendLoaderController(_BaseLoaderController):
pass pass
@abstractmethod @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. """Task items for folder ids.
Args: Args:
@ -634,6 +657,21 @@ class FrontendLoaderController(_BaseLoaderController):
""" """
pass 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 @abstractmethod
def get_project_status_items(self, project_name, sender=None): def get_project_status_items(self, project_name, sender=None):
"""Items for all projects available on server. """Items for all projects available on server.

View file

@ -13,6 +13,7 @@ from ayon_core.tools.common_models import (
ProjectsModel, ProjectsModel,
HierarchyModel, HierarchyModel,
ThumbnailsModel, ThumbnailsModel,
TagItem,
) )
from .abstract import ( from .abstract import (
@ -223,6 +224,16 @@ class LoaderController(BackendLoaderController, FrontendLoaderController):
output[folder_id] = label output[folder_id] = label
return output 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): def get_product_items(self, project_name, folder_ids, sender=None):
return self._products_model.get_product_items( return self._products_model.get_product_items(
project_name, folder_ids, sender) project_name, folder_ids, sender)

View file

@ -28,6 +28,7 @@ PRODUCTS_MODEL_SENDER = "products.model"
def version_item_from_entity(version): def version_item_from_entity(version):
version_attribs = version["attrib"] version_attribs = version["attrib"]
tags = version["tags"]
frame_start = version_attribs.get("frameStart") frame_start = version_attribs.get("frameStart")
frame_end = version_attribs.get("frameEnd") frame_end = version_attribs.get("frameEnd")
handle_start = version_attribs.get("handleStart") handle_start = version_attribs.get("handleStart")
@ -68,6 +69,7 @@ def version_item_from_entity(version):
thumbnail_id=version["thumbnailId"], thumbnail_id=version["thumbnailId"],
published_time=published_time, published_time=published_time,
author=author, author=author,
tags=tags,
status=version["status"], status=version["status"],
frame_range=frame_range, frame_range=frame_range,
duration=duration, 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 numbers
import uuid import uuid
from typing import Dict from typing import Dict
@ -18,16 +20,19 @@ from .products_model import (
SYNC_REMOTE_SITE_AVAILABILITY, SYNC_REMOTE_SITE_AVAILABILITY,
) )
STATUS_NAME_ROLE = QtCore.Qt.UserRole + 1 COMBO_VERSION_ID_ROLE = QtCore.Qt.UserRole + 1
TASK_ID_ROLE = QtCore.Qt.UserRole + 2 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): def __init__(self):
super().__init__() super().__init__()
self._items_by_id = {} self._items_by_id = {}
def update_versions(self, version_items): def update_versions(self, version_items, task_tags_by_version_id):
version_ids = { version_ids = {
version_item.version_id version_item.version_id
for version_item in version_items for version_item in version_items
@ -39,6 +44,7 @@ class VersionsModel(QtGui.QStandardItemModel):
item = self._items_by_id.pop(item_id) item = self._items_by_id.pop(item_id)
root_item.removeRow(item.row()) root_item.removeRow(item.row())
version_tags_by_version_id = {}
for idx, version_item in enumerate(version_items): for idx, version_item in enumerate(version_items):
version_id = version_item.version_id version_id = version_item.version_id
@ -48,34 +54,74 @@ class VersionsModel(QtGui.QStandardItemModel):
item = QtGui.QStandardItem(label) item = QtGui.QStandardItem(label)
item.setData(version_id, QtCore.Qt.UserRole) item.setData(version_id, QtCore.Qt.UserRole)
self._items_by_id[version_id] = item self._items_by_id[version_id] = item
item.setData(version_item.status, STATUS_NAME_ROLE) version_tags = set(version_item.tags)
item.setData(version_item.task_id, TASK_ID_ROLE) 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: if item.row() != idx:
root_item.insertRow(idx, item) root_item.insertRow(idx, item)
class VersionsFilterModel(QtCore.QSortFilterProxyModel): class ComboVersionsFilterModel(QtCore.QSortFilterProxyModel):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._status_filter = None self._status_filter = None
self._task_ids_filter = None self._task_ids_filter = None
self._version_tags_filter = None
self._task_tags_filter = None
def filterAcceptsRow(self, row, parent): def filterAcceptsRow(self, row, parent):
index = None
if self._status_filter is not None: if self._status_filter is not None:
if not self._status_filter: if not self._status_filter:
return False return False
if index is None:
index = self.sourceModel().index(row, 0, parent) index = self.sourceModel().index(row, 0, parent)
status = index.data(STATUS_NAME_ROLE) status = index.data(COMBO_STATUS_NAME_ROLE)
if status not in self._status_filter: if status not in self._status_filter:
return False return False
if self._task_ids_filter: if self._task_ids_filter:
index = self.sourceModel().index(row, 0, parent) if index is None:
task_id = index.data(TASK_ID_ROLE) index = self.sourceModel().index(row, 0, parent)
task_id = index.data(COMBO_TASK_ID_ROLE)
if task_id not in self._task_ids_filter: if task_id not in self._task_ids_filter:
return False 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 return True
def set_tasks_filter(self, task_ids): def set_tasks_filter(self, task_ids):
@ -84,12 +130,24 @@ class VersionsFilterModel(QtCore.QSortFilterProxyModel):
self._task_ids_filter = task_ids self._task_ids_filter = task_ids
self.invalidateFilter() 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): def set_statuses_filter(self, status_names):
if self._status_filter == status_names: if self._status_filter == status_names:
return return
self._status_filter = status_names self._status_filter = status_names
self.invalidateFilter() 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): class VersionComboBox(QtWidgets.QComboBox):
value_changed = QtCore.Signal(str, str) value_changed = QtCore.Signal(str, str)
@ -97,8 +155,8 @@ class VersionComboBox(QtWidgets.QComboBox):
def __init__(self, product_id, parent): def __init__(self, product_id, parent):
super().__init__(parent) super().__init__(parent)
versions_model = VersionsModel() versions_model = ComboVersionsModel()
proxy_model = VersionsFilterModel() proxy_model = ComboVersionsFilterModel()
proxy_model.setSourceModel(versions_model) proxy_model.setSourceModel(versions_model)
self.setModel(proxy_model) self.setModel(proxy_model)
@ -123,6 +181,13 @@ class VersionComboBox(QtWidgets.QComboBox):
if self.currentIndex() != 0: if self.currentIndex() != 0:
self.setCurrentIndex(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): def set_statuses_filter(self, status_names):
self._proxy_model.set_statuses_filter(status_names) self._proxy_model.set_statuses_filter(status_names)
if self.count() == 0: if self.count() == 0:
@ -130,12 +195,24 @@ class VersionComboBox(QtWidgets.QComboBox):
if self.currentIndex() != 0: if self.currentIndex() != 0:
self.setCurrentIndex(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): def all_versions_filtered_out(self):
if self._items_by_id: if self._items_by_id:
return self.count() == 0 return self.count() == 0
return False 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) self.blockSignals(True)
version_items = list(version_items) version_items = list(version_items)
version_ids = [ version_ids = [
@ -146,7 +223,9 @@ class VersionComboBox(QtWidgets.QComboBox):
current_version_id = version_ids[0] current_version_id = version_ids[0]
self._current_id = current_version_id 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) index = version_ids.index(current_version_id)
if self.currentIndex() != index: if self.currentIndex() != index:
@ -173,6 +252,8 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
self._editor_by_id: Dict[str, VersionComboBox] = {} self._editor_by_id: Dict[str, VersionComboBox] = {}
self._task_ids_filter = None self._task_ids_filter = None
self._statuses_filter = None self._statuses_filter = None
self._version_tags_filter = None
self._task_tags_filter = None
def displayText(self, value, locale): def displayText(self, value, locale):
if not isinstance(value, numbers.Integral): if not isinstance(value, numbers.Integral):
@ -185,10 +266,26 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
widget.set_tasks_filter(task_ids) widget.set_tasks_filter(task_ids)
def set_statuses_filter(self, status_names): 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(): for widget in self._editor_by_id.values():
widget.set_statuses_filter(status_names) 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): def paint(self, painter, option, index):
fg_color = index.data(QtCore.Qt.ForegroundRole) fg_color = index.data(QtCore.Qt.ForegroundRole)
if fg_color: if fg_color:
@ -200,7 +297,7 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
fg_color = None fg_color = None
if not fg_color: if not fg_color:
return super(VersionDelegate, self).paint(painter, option, index) return super().paint(painter, option, index)
if option.widget: if option.widget:
style = option.widget.style() style = option.widget.style()
@ -263,11 +360,22 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
editor.clear() editor.clear()
# Current value of the index # 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) 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_tasks_filter(self._task_ids_filter)
editor.set_task_tags_filter(self._task_tags_filter)
editor.set_statuses_filter(self._statuses_filter) editor.set_statuses_filter(self._statuses_filter)
def setModelData(self, editor, model, index): 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 SYNC_REMOTE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 32
STATUS_NAME_FILTER_ROLE = QtCore.Qt.UserRole + 33 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): class ProductsModel(QtGui.QStandardItemModel):
@ -134,6 +136,7 @@ class ProductsModel(QtGui.QStandardItemModel):
self._last_folder_ids = [] self._last_folder_ids = []
self._last_project_statuses = {} self._last_project_statuses = {}
self._last_status_icons_by_name = {} self._last_status_icons_by_name = {}
self._last_task_tags_by_task_id = {}
def get_product_item_indexes(self): def get_product_item_indexes(self):
return [ return [
@ -174,6 +177,17 @@ class ProductsModel(QtGui.QStandardItemModel):
self._last_folder_ids 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): def flags(self, index):
# Make the version column editable # Make the version column editable
if index.column() == self.version_col and index.data(PRODUCT_ID_ROLE): 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) product_item = self._product_items_by_id.get(product_id)
if product_item is None: if product_item is None:
return None return None
product_items = list(product_item.version_items.values()) version_items = list(product_item.version_items.values())
product_items.sort(reverse=True) version_items.sort(reverse=True)
return product_items return version_items
if role == QtCore.Qt.EditRole: if role == QtCore.Qt.EditRole:
return None return None
@ -426,6 +440,16 @@ class ProductsModel(QtGui.QStandardItemModel):
version_item.status version_item.status
for version_item in product_item.version_items.values() 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: if model_item is None:
product_id = product_item.product_id product_id = product_item.product_id
model_item = QtGui.QStandardItem(product_item.product_name) model_item = QtGui.QStandardItem(product_item.product_name)
@ -447,6 +471,8 @@ class ProductsModel(QtGui.QStandardItemModel):
self._items_by_id[product_id] = model_item self._items_by_id[product_id] = model_item
model_item.setData("|".join(statuses), STATUS_NAME_FILTER_ROLE) 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) model_item.setData(product_item.folder_label, FOLDER_LABEL_ROLE)
in_scene = 1 if product_item.product_in_scene else 0 in_scene = 1 if product_item.product_in_scene else 0
model_item.setData(in_scene, PRODUCT_IN_SCENE_ROLE) model_item.setData(in_scene, PRODUCT_IN_SCENE_ROLE)
@ -477,6 +503,14 @@ class ProductsModel(QtGui.QStandardItemModel):
} }
self._last_status_icons_by_name = {} 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( active_site_icon_def = self._controller.get_active_site_icon_def(
project_name project_name
) )
@ -491,6 +525,7 @@ class ProductsModel(QtGui.QStandardItemModel):
folder_ids, folder_ids,
sender=PRODUCTS_MODEL_SENDER_NAME sender=PRODUCTS_MODEL_SENDER_NAME
) )
product_items_by_id = { product_items_by_id = {
product_item.product_id: product_item product_item.product_id: product_item
for product_item in product_items for product_item in product_items

View file

@ -27,6 +27,8 @@ from .products_model import (
VERSION_STATUS_ICON_ROLE, VERSION_STATUS_ICON_ROLE,
VERSION_THUMBNAIL_ID_ROLE, VERSION_THUMBNAIL_ID_ROLE,
STATUS_NAME_FILTER_ROLE, STATUS_NAME_FILTER_ROLE,
VERSION_TAGS_FILTER_ROLE,
TASK_TAGS_FILTER_ROLE,
) )
from .products_delegates import ( from .products_delegates import (
VersionDelegate, VersionDelegate,
@ -42,6 +44,8 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel):
self._product_type_filters = None self._product_type_filters = None
self._statuses_filter = None self._statuses_filter = None
self._version_tags_filter = None
self._task_tags_filter = None
self._task_ids_filter = None self._task_ids_filter = None
self._ascending_sort = True self._ascending_sort = True
@ -68,6 +72,18 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel):
self._statuses_filter = statuses_filter self._statuses_filter = statuses_filter
self.invalidateFilter() 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): def filterAcceptsRow(self, source_row, source_parent):
source_model = self.sourceModel() source_model = self.sourceModel()
index = source_model.index(source_row, 0, source_parent) index = source_model.index(source_row, 0, source_parent)
@ -84,6 +100,16 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel):
): ):
return False 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) return super().filterAcceptsRow(source_row, source_parent)
def _accept_task_ids_filter(self, index): def _accept_task_ids_filter(self, index):
@ -103,10 +129,11 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel):
if not filter_value: if not filter_value:
return False return False
status_s = index.data(role) value_s = index.data(role)
for status in status_s.split("|"): if value_s:
if status in filter_value: for value in value_s.split("|"):
return True if value in filter_value:
return True
return False return False
def lessThan(self, left, right): def lessThan(self, left, right):
@ -298,6 +325,14 @@ class ProductsWidget(QtWidgets.QWidget):
self._version_delegate.set_statuses_filter(status_names) self._version_delegate.set_statuses_filter(status_names)
self._products_proxy_model.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): 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", "selection.folders.changed",
self._on_folders_selection_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 = tasks_view.selectionModel()
selection_model.selectionChanged.connect(self._on_selection_change) selection_model.selectionChanged.connect(self._on_selection_change)
@ -373,10 +369,6 @@ class LoaderTasksWidget(QtWidgets.QWidget):
def _clear(self): def _clear(self):
self._tasks_model.clear() 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): def _on_folders_selection_changed(self, event):
project_name = event["project_name"] project_name = event["project_name"]
folder_ids = event["folder_ids"] 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.lib import center_window
from ayon_core.tools.utils import ProjectsCombobox 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 ayon_core.tools.loader.control import LoaderController
from .folders_widget import LoaderFoldersWidget from .folders_widget import LoaderFoldersWidget
from .tasks_widget import LoaderTasksWidget from .tasks_widget import LoaderTasksWidget
from .products_widget import ProductsWidget from .products_widget import ProductsWidget
from .product_types_combo import ProductTypesCombobox
from .product_group_dialog import ProductGroupDialog from .product_group_dialog import ProductGroupDialog
from .info_widget import InfoWidget from .info_widget import InfoWidget
from .repres_widget import RepresentationsWidget 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): class LoadErrorMessageBox(ErrorMessageBox):
@ -182,29 +190,19 @@ class LoaderWindow(QtWidgets.QWidget):
products_wrap_widget = QtWidgets.QWidget(main_splitter) products_wrap_widget = QtWidgets.QWidget(main_splitter)
products_inputs_widget = QtWidgets.QWidget(products_wrap_widget) products_inputs_widget = QtWidgets.QWidget(products_wrap_widget)
search_bar = FiltersBar(products_inputs_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)
product_group_checkbox = QtWidgets.QCheckBox( product_group_checkbox = QtWidgets.QCheckBox(
"Enable grouping", products_inputs_widget) "Enable grouping", products_inputs_widget)
product_group_checkbox.setChecked(True) product_group_checkbox.setChecked(True)
products_widget = ProductsWidget(controller, products_wrap_widget)
products_inputs_layout = QtWidgets.QHBoxLayout(products_inputs_widget) products_inputs_layout = QtWidgets.QHBoxLayout(products_inputs_widget)
products_inputs_layout.setContentsMargins(0, 0, 0, 0) products_inputs_layout.setContentsMargins(0, 0, 0, 0)
products_inputs_layout.addWidget(products_filter_input, 1) products_inputs_layout.addWidget(search_bar, 1)
products_inputs_layout.addWidget(product_types_filter_combo, 1)
products_inputs_layout.addWidget(product_status_filter_combo, 1)
products_inputs_layout.addWidget(product_group_checkbox, 0) 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 = QtWidgets.QVBoxLayout(products_wrap_widget)
products_wrap_layout.setContentsMargins(0, 0, 0, 0) products_wrap_layout.setContentsMargins(0, 0, 0, 0)
products_wrap_layout.addWidget(products_inputs_widget, 0) products_wrap_layout.addWidget(products_inputs_widget, 0)
@ -250,15 +248,7 @@ class LoaderWindow(QtWidgets.QWidget):
folders_filter_input.textChanged.connect( folders_filter_input.textChanged.connect(
self._on_folder_filter_change self._on_folder_filter_change
) )
products_filter_input.textChanged.connect( search_bar.filter_changed.connect(self._on_filter_change)
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
)
product_group_checkbox.stateChanged.connect( product_group_checkbox.stateChanged.connect(
self._on_product_group_change self._on_product_group_change
) )
@ -316,9 +306,7 @@ class LoaderWindow(QtWidgets.QWidget):
self._tasks_widget = tasks_widget self._tasks_widget = tasks_widget
self._products_filter_input = products_filter_input self._search_bar = search_bar
self._product_types_filter_combo = product_types_filter_combo
self._product_status_filter_combo = product_status_filter_combo
self._product_group_checkbox = product_group_checkbox self._product_group_checkbox = product_group_checkbox
self._products_widget = products_widget self._products_widget = products_widget
@ -337,6 +325,8 @@ class LoaderWindow(QtWidgets.QWidget):
self._selected_folder_ids = set() self._selected_folder_ids = set()
self._selected_version_ids = set() self._selected_version_ids = set()
self._set_product_type_filters = True
self._products_widget.set_enable_grouping( self._products_widget.set_enable_grouping(
self._product_group_checkbox.isChecked() self._product_group_checkbox.isChecked()
) )
@ -356,22 +346,24 @@ class LoaderWindow(QtWidgets.QWidget):
def closeEvent(self, event): def closeEvent(self, event):
super().closeEvent(event) super().closeEvent(event)
(
self
._product_types_filter_combo
.reset_product_types_filter_on_refresh()
)
self._reset_on_show = True self._reset_on_show = True
def keyPressEvent(self, event): def keyPressEvent(self, event):
modifiers = event.modifiers() if hasattr(event, "keyCombination"):
ctrl_pressed = QtCore.Qt.ControlModifier & modifiers 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 # Grouping products on pressing Ctrl + G
if ( if (
ctrl_pressed GROUP_KEY_SEQUENCE == combination
and event.key() == QtCore.Qt.Key_G
and not event.isAutoRepeat() and not event.isAutoRepeat()
): ):
self._show_group_dialog() self._show_group_dialog()
@ -435,20 +427,30 @@ class LoaderWindow(QtWidgets.QWidget):
self._product_group_checkbox.isChecked() self._product_group_checkbox.isChecked()
) )
def _on_product_filter_change(self, text): def _on_filter_change(self, filter_name):
self._products_widget.set_name_filter(text) 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): def _on_tasks_selection_change(self, event):
self._products_widget.set_tasks_filter(event["task_ids"]) 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): def _on_merged_products_selection_change(self):
items = self._products_widget.get_selected_merged_products() items = self._products_widget.get_selected_merged_products()
self._folders_widget.set_merged_products_selection(items) 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) self._projects_combobox.set_current_context_project(project_name)
if not self._refresh_handler.project_refreshed: if not self._refresh_handler.project_refreshed:
self._projects_combobox.refresh() self._projects_combobox.refresh()
self._update_filters()
def _on_load_finished(self, event): def _on_load_finished(self, event):
error_info = event["error_info"] error_info = event["error_info"]
@ -491,6 +494,124 @@ class LoaderWindow(QtWidgets.QWidget):
def _on_project_selection_changed(self, event): def _on_project_selection_changed(self, event):
self._selected_project_name = event["project_name"] 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): def _on_folders_selection_changed(self, event):
self._selected_folder_ids = set(event["folder_ids"]) 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`. Callback is defined by overriding `_mouse_release_callback`.
""" """
def __init__(self, parent): def __init__(self, parent):
super(BaseClickableFrame, self).__init__(parent) super().__init__(parent)
self._mouse_pressed = False self._mouse_pressed = False
@ -470,17 +470,23 @@ class BaseClickableFrame(QtWidgets.QFrame):
pass pass
def mousePressEvent(self, event): def mousePressEvent(self, event):
super().mousePressEvent(event)
if event.isAccepted():
return
if event.button() == QtCore.Qt.LeftButton: if event.button() == QtCore.Qt.LeftButton:
self._mouse_pressed = True self._mouse_pressed = True
super(BaseClickableFrame, self).mousePressEvent(event) event.accept()
def mouseReleaseEvent(self, event): def mouseReleaseEvent(self, event):
if self._mouse_pressed: pressed, self._mouse_pressed = self._mouse_pressed, False
self._mouse_pressed = False super().mouseReleaseEvent(event)
if self.rect().contains(event.pos()): if event.isAccepted():
self._mouse_release_callback() 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): class ClickableFrame(BaseClickableFrame):
@ -1185,7 +1191,7 @@ class SquareButton(QtWidgets.QPushButton):
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(SquareButton, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
sp = self.sizePolicy() sp = self.sizePolicy()
sp.setVerticalPolicy(QtWidgets.QSizePolicy.Minimum) sp.setVerticalPolicy(QtWidgets.QSizePolicy.Minimum)
@ -1194,17 +1200,17 @@ class SquareButton(QtWidgets.QPushButton):
self._ideal_width = None self._ideal_width = None
def showEvent(self, event): def showEvent(self, event):
super(SquareButton, self).showEvent(event) super().showEvent(event)
self._ideal_width = self.height() self._ideal_width = self.height()
self.updateGeometry() self.updateGeometry()
def resizeEvent(self, event): def resizeEvent(self, event):
super(SquareButton, self).resizeEvent(event) super().resizeEvent(event)
self._ideal_width = self.height() self._ideal_width = self.height()
self.updateGeometry() self.updateGeometry()
def sizeHint(self): def sizeHint(self):
sh = super(SquareButton, self).sizeHint() sh = super().sizeHint()
ideal_width = self._ideal_width ideal_width = self._ideal_width
if ideal_width is None: if ideal_width is None:
ideal_width = sh.height() ideal_width = sh.height()