From d1ef11defa7f193bede9cbeebfe03771085fee02 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:06:56 +0100 Subject: [PATCH 01/11] define helper widget for folders filtering --- client/ayon_core/tools/utils/__init__.py | 2 + .../ayon_core/tools/utils/folders_widget.py | 40 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/client/ayon_core/tools/utils/__init__.py b/client/ayon_core/tools/utils/__init__.py index 111b7c614b..56989927ee 100644 --- a/client/ayon_core/tools/utils/__init__.py +++ b/client/ayon_core/tools/utils/__init__.py @@ -76,6 +76,7 @@ from .folders_widget import ( FoldersQtModel, FOLDERS_MODEL_SENDER_NAME, SimpleFoldersWidget, + FoldersFiltersWidget, ) from .tasks_widget import ( @@ -160,6 +161,7 @@ __all__ = ( "FoldersQtModel", "FOLDERS_MODEL_SENDER_NAME", "SimpleFoldersWidget", + "FoldersFiltersWidget", "TasksWidget", "TasksQtModel", diff --git a/client/ayon_core/tools/utils/folders_widget.py b/client/ayon_core/tools/utils/folders_widget.py index 7b71dd087c..12f4bebdae 100644 --- a/client/ayon_core/tools/utils/folders_widget.py +++ b/client/ayon_core/tools/utils/folders_widget.py @@ -15,6 +15,8 @@ from ayon_core.tools.common_models import ( from .models import RecursiveSortFilterProxyModel from .views import TreeView from .lib import RefreshThread, get_qt_icon +from .widgets import PlaceholderLineEdit +from .nice_checkbox import NiceCheckbox FOLDERS_MODEL_SENDER_NAME = "qt_folders_model" @@ -794,3 +796,41 @@ class SimpleFoldersWidget(FoldersWidget): event (Event): Triggered event. """ pass + + +class FoldersFiltersWidget(QtWidgets.QWidget): + """Helper widget for most commonly used filters in context selection.""" + text_changed = QtCore.Signal(str) + my_tasks_changed = QtCore.Signal(bool) + + def __init__(self, parent: QtWidgets.QWidget) -> None: + super().__init__(parent) + + folders_filter_input = PlaceholderLineEdit(self) + folders_filter_input.setPlaceholderText("Folder name filter...") + + my_tasks_tooltip = ( + "Filter folders and task to only those you are assigned to." + ) + my_tasks_label = QtWidgets.QLabel("My tasks", self) + my_tasks_label.setToolTip(my_tasks_tooltip) + + my_tasks_checkbox = NiceCheckbox(self) + my_tasks_checkbox.setChecked(False) + my_tasks_checkbox.setToolTip(my_tasks_tooltip) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(5) + layout.addWidget(folders_filter_input, 1) + layout.addWidget(my_tasks_label, 0) + layout.addWidget(my_tasks_checkbox, 0) + + folders_filter_input.textChanged.connect(self.text_changed) + my_tasks_checkbox.stateChanged.connect(self._on_my_tasks_change) + + self._folders_filter_input = folders_filter_input + self._my_tasks_checkbox = my_tasks_checkbox + + def _on_my_tasks_change(self, _state: int) -> None: + self.my_tasks_changed.emit(self._my_tasks_checkbox.isChecked()) From cef3bc229a4508e0f19c2dd5fadcb2b0f96c2233 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:07:12 +0100 Subject: [PATCH 02/11] disable case sensitivity for folders proxy --- client/ayon_core/tools/utils/folders_widget.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/tools/utils/folders_widget.py b/client/ayon_core/tools/utils/folders_widget.py index 12f4bebdae..126363086b 100644 --- a/client/ayon_core/tools/utils/folders_widget.py +++ b/client/ayon_core/tools/utils/folders_widget.py @@ -345,6 +345,8 @@ class FoldersProxyModel(RecursiveSortFilterProxyModel): def __init__(self): super().__init__() + self.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) + self._folder_ids_filter = None def set_folder_ids_filter(self, folder_ids: Optional[list[str]]): From 0dd47211c54850066d6440599b6fb540b388949d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:13:24 +0100 Subject: [PATCH 03/11] add 'get_current_username' to UsersModel --- client/ayon_core/tools/common_models/users.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/common_models/users.py b/client/ayon_core/tools/common_models/users.py index f7939e5cd3..42a76d8d7d 100644 --- a/client/ayon_core/tools/common_models/users.py +++ b/client/ayon_core/tools/common_models/users.py @@ -1,10 +1,13 @@ import json import collections +from typing import Optional import ayon_api from ayon_api.graphql import FIELD_VALUE, GraphQlQuery, fields_to_dict -from ayon_core.lib import NestedCacheItem +from ayon_core.lib import NestedCacheItem, get_ayon_username + +NOT_SET = object() # --- Implementation that should be in ayon-python-api --- @@ -105,9 +108,18 @@ class UserItem: class UsersModel: def __init__(self, controller): + self._current_username = NOT_SET self._controller = controller self._users_cache = NestedCacheItem(default_factory=list) + def get_current_username(self) -> Optional[str]: + if self._current_username is NOT_SET: + self._current_username = get_ayon_username() + return self._current_username + + def reset(self) -> None: + self._users_cache.reset() + def get_user_items(self, project_name): """Get user items. From ad83d827e2a1dcb092d191360a1756086813f9cf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:14:32 +0100 Subject: [PATCH 04/11] move private methods below public one --- client/ayon_core/tools/loader/control.py | 28 ++++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index 9f159bfb21..9ec3e580d9 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -476,20 +476,6 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): def is_standard_projects_filter_enabled(self): return self._host is not None - def _get_project_anatomy(self, project_name): - if not project_name: - return None - cache = self._project_anatomy_cache[project_name] - if not cache.is_valid: - cache.update_data(Anatomy(project_name)) - return cache.get_data() - - def _create_event_system(self): - return QueuedEventSystem() - - def _emit_event(self, topic, data=None): - self._event_system.emit(topic, data or {}, "controller") - def get_product_types_filter(self): output = ProductTypesFilter( is_allow_list=False, @@ -545,3 +531,17 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): product_types=profile["filter_product_types"] ) return output + + def _create_event_system(self): + return QueuedEventSystem() + + def _emit_event(self, topic, data=None): + self._event_system.emit(topic, data or {}, "controller") + + def _get_project_anatomy(self, project_name): + if not project_name: + return None + cache = self._project_anatomy_cache[project_name] + if not cache.is_valid: + cache.update_data(Anatomy(project_name)) + return cache.get_data() From 91d44a833b7915b9bbbccb23ad81fd45df295db2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:22:44 +0100 Subject: [PATCH 05/11] implemented my tasks filter to browser --- client/ayon_core/tools/loader/abstract.py | 15 +++++++++ client/ayon_core/tools/loader/control.py | 20 +++++++++++- .../tools/loader/ui/folders_widget.py | 19 ++++++++--- .../ayon_core/tools/loader/ui/tasks_widget.py | 21 ++++++++++-- client/ayon_core/tools/loader/ui/window.py | 32 ++++++++++++++----- 5 files changed, 91 insertions(+), 16 deletions(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index 9c7934d2db..089d298b2c 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -666,6 +666,21 @@ class FrontendLoaderController(_BaseLoaderController): """ pass + @abstractmethod + def get_my_tasks_entity_ids( + self, project_name: str + ) -> dict[str, list[str]]: + """Get entity ids for my tasks. + + Args: + project_name (str): Project name. + + Returns: + dict[str, list[str]]: Folder and task ids. + + """ + pass + @abstractmethod def get_available_tags_by_entity_type( self, project_name: str diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index 9ec3e580d9..2a86a50b6d 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -8,7 +8,11 @@ import ayon_api from ayon_core.settings import get_project_settings from ayon_core.pipeline import get_current_host_name -from ayon_core.lib import NestedCacheItem, CacheItem, filter_profiles +from ayon_core.lib import ( + NestedCacheItem, + CacheItem, + filter_profiles, +) from ayon_core.lib.events import QueuedEventSystem from ayon_core.pipeline import Anatomy, get_current_context from ayon_core.host import ILoadHost @@ -18,6 +22,7 @@ from ayon_core.tools.common_models import ( ThumbnailsModel, TagItem, ProductTypeIconMapping, + UsersModel, ) from .abstract import ( @@ -32,6 +37,8 @@ from .models import ( SiteSyncModel ) +NOT_SET = object() + class ExpectedSelection: def __init__(self, controller): @@ -124,6 +131,7 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): self._loader_actions_model = LoaderActionsModel(self) self._thumbnails_model = ThumbnailsModel() self._sitesync_model = SiteSyncModel(self) + self._users_model = UsersModel(self) @property def log(self): @@ -160,6 +168,7 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): self._projects_model.reset() self._thumbnails_model.reset() self._sitesync_model.reset() + self._users_model.reset() self._projects_model.refresh() @@ -235,6 +244,15 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): output[folder_id] = label return output + def get_my_tasks_entity_ids(self, project_name: str): + username = self._users_model.get_current_username() + assignees = [] + if username: + assignees.append(username) + return self._hierarchy_model.get_entity_ids_for_assignees( + project_name, assignees + ) + def get_available_tags_by_entity_type( self, project_name: str ) -> dict[str, list[str]]: diff --git a/client/ayon_core/tools/loader/ui/folders_widget.py b/client/ayon_core/tools/loader/ui/folders_widget.py index f238eabcef..6de0b17ea2 100644 --- a/client/ayon_core/tools/loader/ui/folders_widget.py +++ b/client/ayon_core/tools/loader/ui/folders_widget.py @@ -1,11 +1,11 @@ +from typing import Optional + import qtpy from qtpy import QtWidgets, QtCore, QtGui -from ayon_core.tools.utils import ( - RecursiveSortFilterProxyModel, - DeselectableTreeView, -) from ayon_core.style import get_objected_colors +from ayon_core.tools.utils import DeselectableTreeView +from ayon_core.tools.utils.folders_widget import FoldersProxyModel from ayon_core.tools.utils import ( FoldersQtModel, @@ -260,7 +260,7 @@ class LoaderFoldersWidget(QtWidgets.QWidget): QtWidgets.QAbstractItemView.ExtendedSelection) folders_model = LoaderFoldersModel(controller) - folders_proxy_model = RecursiveSortFilterProxyModel() + folders_proxy_model = FoldersProxyModel() folders_proxy_model.setSourceModel(folders_model) folders_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) @@ -314,6 +314,15 @@ class LoaderFoldersWidget(QtWidgets.QWidget): if name: self._folders_view.expandAll() + def set_folder_ids_filter(self, folder_ids: Optional[list[str]]): + """Set filter of folder ids. + + Args: + folder_ids (list[str]): The list of folder ids. + + """ + self._folders_proxy_model.set_folder_ids_filter(folder_ids) + def set_merged_products_selection(self, items): """ diff --git a/client/ayon_core/tools/loader/ui/tasks_widget.py b/client/ayon_core/tools/loader/ui/tasks_widget.py index cc7e2e9c95..3a38739cf0 100644 --- a/client/ayon_core/tools/loader/ui/tasks_widget.py +++ b/client/ayon_core/tools/loader/ui/tasks_widget.py @@ -1,11 +1,11 @@ import collections import hashlib +from typing import Optional from qtpy import QtWidgets, QtCore, QtGui from ayon_core.style import get_default_entity_icon_color from ayon_core.tools.utils import ( - RecursiveSortFilterProxyModel, DeselectableTreeView, TasksQtModel, TASKS_MODEL_SENDER_NAME, @@ -15,9 +15,11 @@ from ayon_core.tools.utils.tasks_widget import ( ITEM_NAME_ROLE, PARENT_ID_ROLE, TASK_TYPE_ROLE, + TasksProxyModel, ) from ayon_core.tools.utils.lib import RefreshThread, get_qt_icon + # Role that can't clash with default 'tasks_widget' roles FOLDER_LABEL_ROLE = QtCore.Qt.UserRole + 100 NO_TASKS_ID = "--no-task--" @@ -295,7 +297,7 @@ class LoaderTasksQtModel(TasksQtModel): return super().data(index, role) -class LoaderTasksProxyModel(RecursiveSortFilterProxyModel): +class LoaderTasksProxyModel(TasksProxyModel): def lessThan(self, left, right): if left.data(ITEM_ID_ROLE) == NO_TASKS_ID: return False @@ -303,6 +305,12 @@ class LoaderTasksProxyModel(RecursiveSortFilterProxyModel): return True return super().lessThan(left, right) + def filterAcceptsRow(self, row, parent_index): + source_index = self.sourceModel().index(row, 0, parent_index) + if source_index.data(ITEM_ID_ROLE) == NO_TASKS_ID: + return True + return super().filterAcceptsRow(row, parent_index) + class LoaderTasksWidget(QtWidgets.QWidget): refreshed = QtCore.Signal() @@ -363,6 +371,15 @@ class LoaderTasksWidget(QtWidgets.QWidget): if name: self._tasks_view.expandAll() + def set_task_ids_filter(self, task_ids: Optional[list[str]]): + """Set filter of folder ids. + + Args: + task_ids (list[str]): The list of folder ids. + + """ + self._tasks_proxy_model.set_task_ids_filter(task_ids) + def refresh(self): self._tasks_model.refresh() diff --git a/client/ayon_core/tools/loader/ui/window.py b/client/ayon_core/tools/loader/ui/window.py index df5beb708f..d1d1222f51 100644 --- a/client/ayon_core/tools/loader/ui/window.py +++ b/client/ayon_core/tools/loader/ui/window.py @@ -5,14 +5,14 @@ from qtpy import QtWidgets, QtCore, QtGui from ayon_core.resources import get_ayon_icon_filepath from ayon_core.style import load_stylesheet from ayon_core.tools.utils import ( - PlaceholderLineEdit, ErrorMessageBox, ThumbnailPainterWidget, RefreshButton, GoToCurrentButton, + FoldersFiltersWidget, ) from ayon_core.tools.utils.lib import center_window -from ayon_core.tools.utils import ProjectsCombobox +from ayon_core.tools.utils import ProjectsCombobox, NiceCheckbox from ayon_core.tools.common_models import StatusItem from ayon_core.tools.loader.abstract import ProductTypeItem from ayon_core.tools.loader.control import LoaderController @@ -170,15 +170,14 @@ class LoaderWindow(QtWidgets.QWidget): context_top_layout.addWidget(go_to_current_btn, 0) context_top_layout.addWidget(refresh_btn, 0) - folders_filter_input = PlaceholderLineEdit(context_widget) - folders_filter_input.setPlaceholderText("Folder name filter...") + filters_widget = FoldersFiltersWidget(context_widget) folders_widget = LoaderFoldersWidget(controller, context_widget) context_layout = QtWidgets.QVBoxLayout(context_widget) context_layout.setContentsMargins(0, 0, 0, 0) context_layout.addWidget(context_top_widget, 0) - context_layout.addWidget(folders_filter_input, 0) + context_layout.addWidget(filters_widget, 0) context_layout.addWidget(folders_widget, 1) tasks_widget = LoaderTasksWidget(controller, context_widget) @@ -247,9 +246,12 @@ class LoaderWindow(QtWidgets.QWidget): projects_combobox.refreshed.connect(self._on_projects_refresh) folders_widget.refreshed.connect(self._on_folders_refresh) products_widget.refreshed.connect(self._on_products_refresh) - folders_filter_input.textChanged.connect( + filters_widget.text_changed.connect( self._on_folder_filter_change ) + filters_widget.my_tasks_changed.connect( + self._on_my_tasks_checkbox_state_changed + ) search_bar.filter_changed.connect(self._on_filter_change) product_group_checkbox.stateChanged.connect( self._on_product_group_change @@ -303,7 +305,7 @@ class LoaderWindow(QtWidgets.QWidget): self._refresh_btn = refresh_btn self._projects_combobox = projects_combobox - self._folders_filter_input = folders_filter_input + self._filters_widget = filters_widget self._folders_widget = folders_widget self._tasks_widget = tasks_widget @@ -421,9 +423,23 @@ class LoaderWindow(QtWidgets.QWidget): self._group_dialog.set_product_ids(project_name, product_ids) self._group_dialog.show() - def _on_folder_filter_change(self, text): + def _on_folder_filter_change(self, text: str) -> None: self._folders_widget.set_name_filter(text) + def _on_my_tasks_checkbox_state_changed(self, enabled: bool) -> None: + # self._folders_widget + folder_ids = None + task_ids = None + if enabled: + entity_ids = self._controller.get_my_tasks_entity_ids( + self._selected_project_name + ) + folder_ids = entity_ids["folder_ids"] + task_ids = entity_ids["task_ids"] + self._folders_widget.set_folder_ids_filter(folder_ids) + self._tasks_widget.set_task_ids_filter(task_ids) + + def _on_product_group_change(self): self._products_widget.set_enable_grouping( self._product_group_checkbox.isChecked() From e6325fa2e81580555cb07847e6e78cfac2c6376e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:27:49 +0100 Subject: [PATCH 06/11] use 'FoldersFiltersWidget' in launcher --- .../tools/launcher/ui/hierarchy_page.py | 37 +++---------------- 1 file changed, 6 insertions(+), 31 deletions(-) diff --git a/client/ayon_core/tools/launcher/ui/hierarchy_page.py b/client/ayon_core/tools/launcher/ui/hierarchy_page.py index dc535db5fc..8554a5af8c 100644 --- a/client/ayon_core/tools/launcher/ui/hierarchy_page.py +++ b/client/ayon_core/tools/launcher/ui/hierarchy_page.py @@ -2,14 +2,13 @@ import qtawesome from qtpy import QtWidgets, QtCore from ayon_core.tools.utils import ( - PlaceholderLineEdit, SquareButton, RefreshButton, ProjectsCombobox, FoldersWidget, TasksWidget, - NiceCheckbox, ) +from ayon_core.tools.utils.folders_widget import FoldersFiltersWidget from ayon_core.tools.utils.lib import checkstate_int_to_enum from .workfiles_page import WorkfilesPage @@ -76,26 +75,7 @@ class HierarchyPage(QtWidgets.QWidget): content_body.setOrientation(QtCore.Qt.Horizontal) # - filters - filters_widget = QtWidgets.QWidget(self) - - folders_filter_text = PlaceholderLineEdit(filters_widget) - folders_filter_text.setPlaceholderText("Filter folders...") - - my_tasks_tooltip = ( - "Filter folders and task to only those you are assigned to." - ) - my_tasks_label = QtWidgets.QLabel("My tasks", filters_widget) - my_tasks_label.setToolTip(my_tasks_tooltip) - - my_tasks_checkbox = NiceCheckbox(filters_widget) - my_tasks_checkbox.setChecked(False) - my_tasks_checkbox.setToolTip(my_tasks_tooltip) - - filters_layout = QtWidgets.QHBoxLayout(filters_widget) - filters_layout.setContentsMargins(0, 0, 0, 0) - filters_layout.addWidget(folders_filter_text, 1) - filters_layout.addWidget(my_tasks_label, 0) - filters_layout.addWidget(my_tasks_checkbox, 0) + filters_widget = FoldersFiltersWidget(self) # - Folders widget folders_widget = LauncherFoldersWidget(controller, content_body) @@ -123,8 +103,8 @@ class HierarchyPage(QtWidgets.QWidget): btn_back.clicked.connect(self._on_back_clicked) refresh_btn.clicked.connect(self._on_refresh_clicked) - folders_filter_text.textChanged.connect(self._on_filter_text_changed) - my_tasks_checkbox.stateChanged.connect( + filters_widget.text_changed.connect(self._on_filter_text_changed) + filters_widget.my_tasks_changed.connect( self._on_my_tasks_checkbox_state_changed ) folders_widget.focused_in.connect(self._on_folders_focus) @@ -135,7 +115,6 @@ class HierarchyPage(QtWidgets.QWidget): self._btn_back = btn_back self._projects_combobox = projects_combobox - self._my_tasks_checkbox = my_tasks_checkbox self._folders_widget = folders_widget self._tasks_widget = tasks_widget self._workfiles_page = workfiles_page @@ -158,9 +137,6 @@ class HierarchyPage(QtWidgets.QWidget): self._folders_widget.refresh() self._tasks_widget.refresh() self._workfiles_page.refresh() - self._on_my_tasks_checkbox_state_changed( - self._my_tasks_checkbox.checkState() - ) def _on_back_clicked(self): self._controller.set_selected_project(None) @@ -171,11 +147,10 @@ class HierarchyPage(QtWidgets.QWidget): def _on_filter_text_changed(self, text): self._folders_widget.set_name_filter(text) - def _on_my_tasks_checkbox_state_changed(self, state): + def _on_my_tasks_checkbox_state_changed(self, enabled: bool) -> None: folder_ids = None task_ids = None - state = checkstate_int_to_enum(state) - if state == QtCore.Qt.Checked: + if enabled: entity_ids = self._controller.get_my_tasks_entity_ids( self._project_name ) From 9c3dec09c9382aa210f59583081e7224b0e8ec22 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:53:36 +0100 Subject: [PATCH 07/11] small cleanup --- client/ayon_core/tools/launcher/control.py | 23 +++++++++++----------- client/ayon_core/tools/loader/control.py | 4 +++- client/ayon_core/tools/loader/ui/window.py | 2 -- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/client/ayon_core/tools/launcher/control.py b/client/ayon_core/tools/launcher/control.py index 85b362f9d7..f4656de787 100644 --- a/client/ayon_core/tools/launcher/control.py +++ b/client/ayon_core/tools/launcher/control.py @@ -1,10 +1,14 @@ from typing import Optional -from ayon_core.lib import Logger, get_ayon_username +from ayon_core.lib import Logger from ayon_core.lib.events import QueuedEventSystem from ayon_core.addon import AddonsManager from ayon_core.settings import get_project_settings, get_studio_settings -from ayon_core.tools.common_models import ProjectsModel, HierarchyModel +from ayon_core.tools.common_models import ( + ProjectsModel, + HierarchyModel, + UsersModel, +) from .abstract import ( AbstractLauncherFrontEnd, @@ -30,13 +34,12 @@ class BaseLauncherController( self._addons_manager = None - self._username = NOT_SET - self._selection_model = LauncherSelectionModel(self) self._projects_model = ProjectsModel(self) self._hierarchy_model = HierarchyModel(self) self._actions_model = ActionsModel(self) self._workfiles_model = WorkfilesModel(self) + self._users_model = UsersModel(self) @property def log(self): @@ -209,6 +212,7 @@ class BaseLauncherController( self._projects_model.reset() self._hierarchy_model.reset() + self._users_model.reset() self._actions_model.refresh() self._projects_model.refresh() @@ -229,8 +233,10 @@ class BaseLauncherController( self._emit_event("controller.refresh.actions.finished") - def get_my_tasks_entity_ids(self, project_name: str): - username = self._get_my_username() + def get_my_tasks_entity_ids( + self, project_name: str + ) -> dict[str, list[str]]: + username = self._users_model.get_current_username() assignees = [] if username: assignees.append(username) @@ -238,10 +244,5 @@ class BaseLauncherController( project_name, assignees ) - def _get_my_username(self): - if self._username is NOT_SET: - self._username = get_ayon_username() - return self._username - def _emit_event(self, topic, data=None): self.emit_event(topic, data, "controller") diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index 2a86a50b6d..d0cc9db2f5 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -244,7 +244,9 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): output[folder_id] = label return output - def get_my_tasks_entity_ids(self, project_name: str): + def get_my_tasks_entity_ids( + self, project_name: str + ) -> dict[str, list[str]]: username = self._users_model.get_current_username() assignees = [] if username: diff --git a/client/ayon_core/tools/loader/ui/window.py b/client/ayon_core/tools/loader/ui/window.py index d1d1222f51..48704e7481 100644 --- a/client/ayon_core/tools/loader/ui/window.py +++ b/client/ayon_core/tools/loader/ui/window.py @@ -427,7 +427,6 @@ class LoaderWindow(QtWidgets.QWidget): self._folders_widget.set_name_filter(text) def _on_my_tasks_checkbox_state_changed(self, enabled: bool) -> None: - # self._folders_widget folder_ids = None task_ids = None if enabled: @@ -439,7 +438,6 @@ class LoaderWindow(QtWidgets.QWidget): self._folders_widget.set_folder_ids_filter(folder_ids) self._tasks_widget.set_task_ids_filter(task_ids) - def _on_product_group_change(self): self._products_widget.set_enable_grouping( self._product_group_checkbox.isChecked() From 3a6ee43f22e94d9c3f304787820433b3277e3761 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Nov 2025 16:22:20 +0100 Subject: [PATCH 08/11] added doption to change filters --- client/ayon_core/tools/utils/folders_widget.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/ayon_core/tools/utils/folders_widget.py b/client/ayon_core/tools/utils/folders_widget.py index 126363086b..f506af5352 100644 --- a/client/ayon_core/tools/utils/folders_widget.py +++ b/client/ayon_core/tools/utils/folders_widget.py @@ -834,5 +834,11 @@ class FoldersFiltersWidget(QtWidgets.QWidget): self._folders_filter_input = folders_filter_input self._my_tasks_checkbox = my_tasks_checkbox + def set_text(self, text: str) -> None: + self._folders_filter_input.setText(text) + + def set_my_tasks_checked(self, checked: bool) -> None: + self._my_tasks_checkbox.setChecked(checked) + def _on_my_tasks_change(self, _state: int) -> None: self.my_tasks_changed.emit(self._my_tasks_checkbox.isChecked()) From f9f55b48b03fefa20cb2c361e676858afba1c78e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Nov 2025 16:23:19 +0100 Subject: [PATCH 09/11] added my tasks filtering to publisher --- client/ayon_core/tools/publisher/abstract.py | 15 +++ client/ayon_core/tools/publisher/control.py | 19 +++- .../widgets/create_context_widgets.py | 33 ++++-- .../tools/publisher/widgets/create_widget.py | 12 +- .../tools/publisher/widgets/folders_dialog.py | 107 ++++++++++-------- 5 files changed, 126 insertions(+), 60 deletions(-) diff --git a/client/ayon_core/tools/publisher/abstract.py b/client/ayon_core/tools/publisher/abstract.py index 14da15793d..bfd0948519 100644 --- a/client/ayon_core/tools/publisher/abstract.py +++ b/client/ayon_core/tools/publisher/abstract.py @@ -295,6 +295,21 @@ class AbstractPublisherFrontend(AbstractPublisherCommon): """Get folder id from folder path.""" pass + @abstractmethod + def get_my_tasks_entity_ids( + self, project_name: str + ) -> dict[str, list[str]]: + """Get entity ids for my tasks. + + Args: + project_name (str): Project name. + + Returns: + dict[str, list[str]]: Folder and task ids. + + """ + pass + # --- Create --- @abstractmethod def get_creator_items(self) -> Dict[str, "CreatorItem"]: diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index 038816c6fc..3d11131dc3 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -11,7 +11,11 @@ from ayon_core.pipeline import ( registered_host, get_process_id, ) -from ayon_core.tools.common_models import ProjectsModel, HierarchyModel +from ayon_core.tools.common_models import ( + ProjectsModel, + HierarchyModel, + UsersModel, +) from .models import ( PublishModel, @@ -101,6 +105,7 @@ class PublisherController( # Cacher of avalon documents self._projects_model = ProjectsModel(self) self._hierarchy_model = HierarchyModel(self) + self._users_model = UsersModel(self) @property def log(self): @@ -317,6 +322,17 @@ class PublisherController( return False return True + def get_my_tasks_entity_ids( + self, project_name: str + ) -> dict[str, list[str]]: + username = self._users_model.get_current_username() + assignees = [] + if username: + assignees.append(username) + return self._hierarchy_model.get_entity_ids_for_assignees( + project_name, assignees + ) + # --- Publish specific callbacks --- def get_context_title(self): """Get context title for artist shown at the top of main window.""" @@ -359,6 +375,7 @@ class PublisherController( self._emit_event("controller.reset.started") self._hierarchy_model.reset() + self._users_model.reset() # Publish part must be reset after plugins self._create_model.reset() diff --git a/client/ayon_core/tools/publisher/widgets/create_context_widgets.py b/client/ayon_core/tools/publisher/widgets/create_context_widgets.py index faf2248181..49d236353f 100644 --- a/client/ayon_core/tools/publisher/widgets/create_context_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/create_context_widgets.py @@ -1,10 +1,14 @@ from qtpy import QtWidgets, QtCore from ayon_core.lib.events import QueuedEventSystem -from ayon_core.tools.utils import PlaceholderLineEdit, GoToCurrentButton from ayon_core.tools.common_models import HierarchyExpectedSelection -from ayon_core.tools.utils import FoldersWidget, TasksWidget +from ayon_core.tools.utils import ( + FoldersWidget, + TasksWidget, + FoldersFiltersWidget, + GoToCurrentButton, +) from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend @@ -180,8 +184,7 @@ class CreateContextWidget(QtWidgets.QWidget): headers_widget = QtWidgets.QWidget(self) - folder_filter_input = PlaceholderLineEdit(headers_widget) - folder_filter_input.setPlaceholderText("Filter folders..") + filters_widget = FoldersFiltersWidget(headers_widget) current_context_btn = GoToCurrentButton(headers_widget) current_context_btn.setToolTip("Go to current context") @@ -189,7 +192,8 @@ class CreateContextWidget(QtWidgets.QWidget): headers_layout = QtWidgets.QHBoxLayout(headers_widget) headers_layout.setContentsMargins(0, 0, 0, 0) - headers_layout.addWidget(folder_filter_input, 1) + headers_layout.setSpacing(5) + headers_layout.addWidget(filters_widget, 1) headers_layout.addWidget(current_context_btn, 0) hierarchy_controller = CreateHierarchyController(controller) @@ -207,15 +211,16 @@ class CreateContextWidget(QtWidgets.QWidget): main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setSpacing(0) main_layout.addWidget(headers_widget, 0) + main_layout.addSpacing(5) main_layout.addWidget(folders_widget, 2) main_layout.addWidget(tasks_widget, 1) folders_widget.selection_changed.connect(self._on_folder_change) tasks_widget.selection_changed.connect(self._on_task_change) current_context_btn.clicked.connect(self._on_current_context_click) - folder_filter_input.textChanged.connect(self._on_folder_filter_change) + filters_widget.text_changed.connect(self._on_folder_filter_change) + filters_widget.my_tasks_changed.connect(self._on_my_tasks_change) - self._folder_filter_input = folder_filter_input self._current_context_btn = current_context_btn self._folders_widget = folders_widget self._tasks_widget = tasks_widget @@ -303,5 +308,17 @@ class CreateContextWidget(QtWidgets.QWidget): self._last_project_name, folder_id, task_name ) - def _on_folder_filter_change(self, text): + def _on_folder_filter_change(self, text: str) -> None: self._folders_widget.set_name_filter(text) + + def _on_my_tasks_change(self, enabled: bool) -> None: + folder_ids = None + task_ids = None + if enabled: + entity_ids = self._controller.get_my_tasks_entity_ids( + self._last_project_name + ) + folder_ids = entity_ids["folder_ids"] + task_ids = entity_ids["task_ids"] + self._folders_widget.set_folder_ids_filter(folder_ids) + self._tasks_widget.set_task_ids_filter(task_ids) diff --git a/client/ayon_core/tools/publisher/widgets/create_widget.py b/client/ayon_core/tools/publisher/widgets/create_widget.py index b9b3afd895..d98bc95eb2 100644 --- a/client/ayon_core/tools/publisher/widgets/create_widget.py +++ b/client/ayon_core/tools/publisher/widgets/create_widget.py @@ -710,11 +710,13 @@ class CreateWidget(QtWidgets.QWidget): def _on_first_show(self): width = self.width() - part = int(width / 4) - rem_width = width - part - self._main_splitter_widget.setSizes([part, rem_width]) - rem_width = rem_width - part - self._creators_splitter.setSizes([part, rem_width]) + part = int(width / 9) + context_width = part * 3 + create_sel_width = part * 2 + rem_width = width - context_width + self._main_splitter_widget.setSizes([context_width, rem_width]) + rem_width -= create_sel_width + self._creators_splitter.setSizes([create_sel_width, rem_width]) def showEvent(self, event): super().showEvent(event) diff --git a/client/ayon_core/tools/publisher/widgets/folders_dialog.py b/client/ayon_core/tools/publisher/widgets/folders_dialog.py index d2eb68310e..e0d9c098d8 100644 --- a/client/ayon_core/tools/publisher/widgets/folders_dialog.py +++ b/client/ayon_core/tools/publisher/widgets/folders_dialog.py @@ -1,7 +1,10 @@ from qtpy import QtWidgets from ayon_core.lib.events import QueuedEventSystem -from ayon_core.tools.utils import PlaceholderLineEdit, FoldersWidget +from ayon_core.tools.utils import ( + FoldersWidget, + FoldersFiltersWidget, +) from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend @@ -43,8 +46,7 @@ class FoldersDialog(QtWidgets.QDialog): super().__init__(parent) self.setWindowTitle("Select folder") - filter_input = PlaceholderLineEdit(self) - filter_input.setPlaceholderText("Filter folders..") + filters_widget = FoldersFiltersWidget(self) folders_controller = FoldersDialogController(controller) folders_widget = FoldersWidget(folders_controller, self) @@ -59,7 +61,8 @@ class FoldersDialog(QtWidgets.QDialog): btns_layout.addWidget(cancel_btn) layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(filter_input, 0) + layout.setSpacing(5) + layout.addWidget(filters_widget, 0) layout.addWidget(folders_widget, 1) layout.addLayout(btns_layout, 0) @@ -68,12 +71,13 @@ class FoldersDialog(QtWidgets.QDialog): ) folders_widget.double_clicked.connect(self._on_ok_clicked) - filter_input.textChanged.connect(self._on_filter_change) + filters_widget.text_changed.connect(self._on_filter_change) + filters_widget.my_tasks_changed.connect(self._on_my_tasks_change) ok_btn.clicked.connect(self._on_ok_clicked) cancel_btn.clicked.connect(self._on_cancel_clicked) self._controller = controller - self._filter_input = filter_input + self._filters_widget = filters_widget self._ok_btn = ok_btn self._cancel_btn = cancel_btn @@ -88,6 +92,49 @@ class FoldersDialog(QtWidgets.QDialog): self._first_show = True self._default_height = 500 + self._project_name = None + + def showEvent(self, event): + """Refresh folders widget on show.""" + super().showEvent(event) + if self._first_show: + self._first_show = False + self._on_first_show() + # Refresh on show + self.reset(False) + + def reset(self, force=True): + """Reset widget.""" + if not force and not self._soft_reset_enabled: + return + + self._project_name = self._controller.get_current_project_name() + if self._soft_reset_enabled: + self._soft_reset_enabled = False + + self._folders_widget.set_project_name(self._project_name) + + def get_selected_folder_path(self): + """Get selected folder path.""" + return self._selected_folder_path + + def set_selected_folders(self, folder_paths: list[str]) -> None: + """Change preselected folder before showing the dialog. + + This also resets model and clean filter. + """ + self.reset(False) + self._filters_widget.set_text("") + self._filters_widget.set_my_tasks_checked(False) + + folder_id = None + for folder_path in folder_paths: + folder_id = self._controller.get_folder_id_from_path(folder_path) + if folder_id: + break + if folder_id: + self._folders_widget.set_selected_folder(folder_id) + def _on_first_show(self): center = self.rect().center() size = self.size() @@ -103,27 +150,6 @@ class FoldersDialog(QtWidgets.QDialog): # Change reset enabled so model is reset on show event self._soft_reset_enabled = True - def showEvent(self, event): - """Refresh folders widget on show.""" - super().showEvent(event) - if self._first_show: - self._first_show = False - self._on_first_show() - # Refresh on show - self.reset(False) - - def reset(self, force=True): - """Reset widget.""" - if not force and not self._soft_reset_enabled: - return - - if self._soft_reset_enabled: - self._soft_reset_enabled = False - - self._folders_widget.set_project_name( - self._controller.get_current_project_name() - ) - def _on_filter_change(self, text): """Trigger change of filter of folders.""" self._folders_widget.set_name_filter(text) @@ -137,22 +163,11 @@ class FoldersDialog(QtWidgets.QDialog): ) self.done(1) - def set_selected_folders(self, folder_paths): - """Change preselected folder before showing the dialog. - - This also resets model and clean filter. - """ - self.reset(False) - self._filter_input.setText("") - - folder_id = None - for folder_path in folder_paths: - folder_id = self._controller.get_folder_id_from_path(folder_path) - if folder_id: - break - if folder_id: - self._folders_widget.set_selected_folder(folder_id) - - def get_selected_folder_path(self): - """Get selected folder path.""" - return self._selected_folder_path + def _on_my_tasks_change(self, enabled: bool) -> None: + folder_ids = None + if enabled: + entity_ids = self._controller.get_my_tasks_entity_ids( + self._project_name + ) + folder_ids = entity_ids["folder_ids"] + self._folders_widget.set_folder_ids_filter(folder_ids) From ba4ecc6f80b0894e3c9345924106b0eeae6a109b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Nov 2025 17:04:29 +0100 Subject: [PATCH 10/11] use filters widget in workfiles tool --- .../tools/workfiles/widgets/window.py | 46 ++++++------------- 1 file changed, 14 insertions(+), 32 deletions(-) diff --git a/client/ayon_core/tools/workfiles/widgets/window.py b/client/ayon_core/tools/workfiles/widgets/window.py index 00362ea866..811fe602d1 100644 --- a/client/ayon_core/tools/workfiles/widgets/window.py +++ b/client/ayon_core/tools/workfiles/widgets/window.py @@ -6,12 +6,11 @@ from ayon_core.tools.utils import ( FoldersWidget, GoToCurrentButton, MessageOverlayObject, - NiceCheckbox, PlaceholderLineEdit, RefreshButton, TasksWidget, + FoldersFiltersWidget, ) -from ayon_core.tools.utils.lib import checkstate_int_to_enum from ayon_core.tools.workfiles.control import BaseWorkfileController from .files_widget import FilesWidget @@ -69,7 +68,6 @@ class WorkfilesToolWindow(QtWidgets.QWidget): self._default_window_flags = flags self._folders_widget = None - self._folder_filter_input = None self._files_widget = None @@ -178,48 +176,33 @@ class WorkfilesToolWindow(QtWidgets.QWidget): col_widget = QtWidgets.QWidget(parent) header_widget = QtWidgets.QWidget(col_widget) - folder_filter_input = PlaceholderLineEdit(header_widget) - folder_filter_input.setPlaceholderText("Filter folders..") + filters_widget = FoldersFiltersWidget(header_widget) go_to_current_btn = GoToCurrentButton(header_widget) refresh_btn = RefreshButton(header_widget) + header_layout = QtWidgets.QHBoxLayout(header_widget) + header_layout.setContentsMargins(0, 0, 0, 0) + header_layout.addWidget(filters_widget, 1) + header_layout.addWidget(go_to_current_btn, 0) + header_layout.addWidget(refresh_btn, 0) + folder_widget = FoldersWidget( controller, col_widget, handle_expected_selection=True ) - my_tasks_tooltip = ( - "Filter folders and task to only those you are assigned to." - ) - - my_tasks_label = QtWidgets.QLabel("My tasks") - my_tasks_label.setToolTip(my_tasks_tooltip) - - my_tasks_checkbox = NiceCheckbox(folder_widget) - my_tasks_checkbox.setChecked(False) - my_tasks_checkbox.setToolTip(my_tasks_tooltip) - - header_layout = QtWidgets.QHBoxLayout(header_widget) - header_layout.setContentsMargins(0, 0, 0, 0) - header_layout.addWidget(folder_filter_input, 1) - header_layout.addWidget(go_to_current_btn, 0) - header_layout.addWidget(refresh_btn, 0) - header_layout.addWidget(my_tasks_label, 0) - header_layout.addWidget(my_tasks_checkbox, 0) - col_layout = QtWidgets.QVBoxLayout(col_widget) col_layout.setContentsMargins(0, 0, 0, 0) col_layout.addWidget(header_widget, 0) col_layout.addWidget(folder_widget, 1) - folder_filter_input.textChanged.connect(self._on_folder_filter_change) - go_to_current_btn.clicked.connect(self._on_go_to_current_clicked) - refresh_btn.clicked.connect(self._on_refresh_clicked) - my_tasks_checkbox.stateChanged.connect( + filters_widget.text_changed.connect(self._on_folder_filter_change) + filters_widget.my_tasks_changed.connect( self._on_my_tasks_checkbox_state_changed ) + go_to_current_btn.clicked.connect(self._on_go_to_current_clicked) + refresh_btn.clicked.connect(self._on_refresh_clicked) - self._folder_filter_input = folder_filter_input self._folders_widget = folder_widget return col_widget @@ -403,11 +386,10 @@ class WorkfilesToolWindow(QtWidgets.QWidget): else: self.close() - def _on_my_tasks_checkbox_state_changed(self, state): + def _on_my_tasks_checkbox_state_changed(self, enabled: bool) -> None: folder_ids = None task_ids = None - state = checkstate_int_to_enum(state) - if state == QtCore.Qt.Checked: + if enabled: entity_ids = self._controller.get_my_tasks_entity_ids( self._project_name ) From dc7f1556750fdcece3b6391ae83fd8bb7e95eeac Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Nov 2025 17:07:24 +0100 Subject: [PATCH 11/11] remove unused imports --- client/ayon_core/tools/launcher/ui/hierarchy_page.py | 1 - client/ayon_core/tools/loader/ui/window.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/tools/launcher/ui/hierarchy_page.py b/client/ayon_core/tools/launcher/ui/hierarchy_page.py index 8554a5af8c..3c8be4679e 100644 --- a/client/ayon_core/tools/launcher/ui/hierarchy_page.py +++ b/client/ayon_core/tools/launcher/ui/hierarchy_page.py @@ -9,7 +9,6 @@ from ayon_core.tools.utils import ( TasksWidget, ) from ayon_core.tools.utils.folders_widget import FoldersFiltersWidget -from ayon_core.tools.utils.lib import checkstate_int_to_enum from .workfiles_page import WorkfilesPage diff --git a/client/ayon_core/tools/loader/ui/window.py b/client/ayon_core/tools/loader/ui/window.py index 48704e7481..27e416b495 100644 --- a/client/ayon_core/tools/loader/ui/window.py +++ b/client/ayon_core/tools/loader/ui/window.py @@ -12,7 +12,7 @@ from ayon_core.tools.utils import ( FoldersFiltersWidget, ) from ayon_core.tools.utils.lib import center_window -from ayon_core.tools.utils import ProjectsCombobox, NiceCheckbox +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