diff --git a/client/ayon_core/plugins/publish/collect_audio.py b/client/ayon_core/plugins/publish/collect_audio.py index c1633e414e..57c69ef2b2 100644 --- a/client/ayon_core/plugins/publish/collect_audio.py +++ b/client/ayon_core/plugins/publish/collect_audio.py @@ -39,6 +39,7 @@ class CollectAudio(pyblish.api.ContextPlugin): "blender", "houdini", "max", + "circuit", ] audio_product_name = "audioMain" diff --git a/client/ayon_core/plugins/publish/extract_burnin.py b/client/ayon_core/plugins/publish/extract_burnin.py index 8e8764fc33..3f7c2f4cba 100644 --- a/client/ayon_core/plugins/publish/extract_burnin.py +++ b/client/ayon_core/plugins/publish/extract_burnin.py @@ -54,7 +54,8 @@ class ExtractBurnin(publish.Extractor): "houdini", "max", "blender", - "unreal" + "unreal", + "circuit", ] optional = True diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index a3db16c898..df87abba91 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -91,7 +91,8 @@ class ExtractReview(pyblish.api.InstancePlugin): "webpublisher", "aftereffects", "flame", - "unreal" + "unreal", + "circuit", ] # Supported extensions diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index da429c1cd2..7062a0a591 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -39,7 +39,8 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): "nuke", "aftereffects", "unreal", - "houdini" + "houdini", + "circuit", ] enabled = False diff --git a/client/ayon_core/tools/common_models/hierarchy.py b/client/ayon_core/tools/common_models/hierarchy.py index edff8471b0..891eb80960 100644 --- a/client/ayon_core/tools/common_models/hierarchy.py +++ b/client/ayon_core/tools/common_models/hierarchy.py @@ -227,6 +227,9 @@ class HierarchyModel(object): self._tasks_by_id = NestedCacheItem( levels=2, default_factory=dict, lifetime=self.lifetime) + self._entity_ids_by_assignee = NestedCacheItem( + levels=2, default_factory=dict, lifetime=self.lifetime) + self._folders_refreshing = set() self._tasks_refreshing = set() self._controller = controller @@ -238,6 +241,8 @@ class HierarchyModel(object): self._task_items.reset() self._tasks_by_id.reset() + self._entity_ids_by_assignee.reset() + def refresh_project(self, project_name): """Force to refresh folder items for a project. @@ -461,6 +466,54 @@ class HierarchyModel(object): output = self.get_task_entities(project_name, {task_id}) return output[task_id] + def get_entity_ids_for_assignees( + self, project_name: str, assignees: list[str] + ): + folder_ids = set() + task_ids = set() + output = { + "folder_ids": folder_ids, + "task_ids": task_ids, + } + assignees = set(assignees) + for assignee in tuple(assignees): + cache = self._entity_ids_by_assignee[project_name][assignee] + if cache.is_valid: + assignees.discard(assignee) + assignee_data = cache.get_data() + folder_ids.update(assignee_data["folder_ids"]) + task_ids.update(assignee_data["task_ids"]) + + if not assignees: + return output + + tasks = ayon_api.get_tasks( + project_name, + assignees_all=assignees, + fields={"id", "folderId", "assignees"}, + ) + tasks_assignee = {} + for task in tasks: + folder_ids.add(task["folderId"]) + task_ids.add(task["id"]) + for assignee in task["assignees"]: + tasks_assignee.setdefault(assignee, []).append(task) + + for assignee, tasks in tasks_assignee.items(): + cache = self._entity_ids_by_assignee[project_name][assignee] + assignee_folder_ids = set() + assignee_task_ids = set() + assignee_data = { + "folder_ids": assignee_folder_ids, + "task_ids": assignee_task_ids, + } + for task in tasks: + assignee_folder_ids.add(task["folderId"]) + assignee_task_ids.add(task["id"]) + cache.update_data(assignee_data) + + return output + @contextlib.contextmanager def _folder_refresh_event_manager(self, project_name, sender): self._folders_refreshing.add(project_name) diff --git a/client/ayon_core/tools/launcher/abstract.py b/client/ayon_core/tools/launcher/abstract.py index 63ba4cd717..ea0842f24d 100644 --- a/client/ayon_core/tools/launcher/abstract.py +++ b/client/ayon_core/tools/launcher/abstract.py @@ -160,8 +160,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): Returns: list[FolderItem]: Minimum possible information needed for visualisation of folder hierarchy. - """ + """ pass @abstractmethod @@ -180,8 +180,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): Returns: list[TaskItem]: Minimum possible information needed for visualisation of tasks. - """ + """ pass @abstractmethod @@ -190,8 +190,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): Returns: Union[str, None]: Selected project name. - """ + """ pass @abstractmethod @@ -200,8 +200,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): Returns: Union[str, None]: Selected folder id. - """ + """ pass @abstractmethod @@ -210,8 +210,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): Returns: Union[str, None]: Selected task id. - """ + """ pass @abstractmethod @@ -220,8 +220,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): Returns: Union[str, None]: Selected task name. - """ + """ pass @abstractmethod @@ -238,8 +238,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): Returns: dict[str, Union[str, None]]: Selected context. - """ + """ pass @abstractmethod @@ -249,8 +249,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): Args: project_name (Union[str, None]): Project nameor None if no project is selected. - """ + """ pass @abstractmethod @@ -260,8 +260,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): Args: folder_id (Union[str, None]): Folder id or None if no folder is selected. - """ + """ pass @abstractmethod @@ -273,8 +273,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): is selected. task_name (Union[str, None]): Task name or None if no task is selected. - """ + """ pass # Actions @@ -290,8 +290,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): Returns: list[ActionItem]: List of action items that should be shown for given context. - """ + """ pass @abstractmethod @@ -303,8 +303,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): folder_id (Union[str, None]): Folder id. task_id (Union[str, None]): Task id. action_id (str): Action identifier. - """ + """ pass @abstractmethod @@ -317,10 +317,10 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): project_name (Union[str, None]): Project name. folder_id (Union[str, None]): Folder id. task_id (Union[str, None]): Task id. - action_id (Iterable[str]): Action identifiers. + action_ids (Iterable[str]): Action identifiers. enabled (bool): New value of force not open workfile. - """ + """ pass @abstractmethod @@ -340,5 +340,17 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): Triggers 'controller.refresh.actions.started' event at the beginning and 'controller.refresh.actions.finished' at the end. """ - + pass + + @abstractmethod + def get_my_tasks_entity_ids(self, project_name: str): + """Get entity ids for my tasks. + + Args: + project_name (str): Project name. + + Returns: + dict[str, Union[list[str]]]: Folder and task ids. + + """ pass diff --git a/client/ayon_core/tools/launcher/control.py b/client/ayon_core/tools/launcher/control.py index d60e4747e3..45cb2b7945 100644 --- a/client/ayon_core/tools/launcher/control.py +++ b/client/ayon_core/tools/launcher/control.py @@ -1,4 +1,4 @@ -from ayon_core.lib import Logger +from ayon_core.lib import Logger, get_ayon_username from ayon_core.lib.events import QueuedEventSystem from ayon_core.settings import get_project_settings from ayon_core.tools.common_models import ProjectsModel, HierarchyModel @@ -6,6 +6,8 @@ from ayon_core.tools.common_models import ProjectsModel, HierarchyModel from .abstract import AbstractLauncherFrontEnd, AbstractLauncherBackend from .models import LauncherSelectionModel, ActionsModel +NOT_SET = object() + class BaseLauncherController( AbstractLauncherFrontEnd, AbstractLauncherBackend @@ -15,6 +17,8 @@ class BaseLauncherController( self._event_system = None self._log = None + self._username = NOT_SET + self._selection_model = LauncherSelectionModel(self) self._projects_model = ProjectsModel(self) self._hierarchy_model = HierarchyModel(self) @@ -168,5 +172,19 @@ class BaseLauncherController( self._emit_event("controller.refresh.actions.finished") + def get_my_tasks_entity_ids(self, project_name: str): + username = self._get_my_username() + assignees = [] + if username: + assignees.append(username) + return self._hierarchy_model.get_entity_ids_for_assignees( + 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/launcher/ui/hierarchy_page.py b/client/ayon_core/tools/launcher/ui/hierarchy_page.py index ad48e8ac77..7c34989947 100644 --- a/client/ayon_core/tools/launcher/ui/hierarchy_page.py +++ b/client/ayon_core/tools/launcher/ui/hierarchy_page.py @@ -5,17 +5,17 @@ from ayon_core.tools.utils import ( PlaceholderLineEdit, SquareButton, RefreshButton, -) -from ayon_core.tools.utils import ( ProjectsCombobox, FoldersWidget, TasksWidget, + NiceCheckbox, ) +from ayon_core.tools.utils.lib import checkstate_int_to_enum class HierarchyPage(QtWidgets.QWidget): def __init__(self, controller, parent): - super(HierarchyPage, self).__init__(parent) + super().__init__(parent) # Header header_widget = QtWidgets.QWidget(self) @@ -43,23 +43,36 @@ class HierarchyPage(QtWidgets.QWidget): ) content_body.setOrientation(QtCore.Qt.Horizontal) - # - Folders widget with filter - folders_wrapper = QtWidgets.QWidget(content_body) + # - filters + filters_widget = QtWidgets.QWidget(self) - folders_filter_text = PlaceholderLineEdit(folders_wrapper) + folders_filter_text = PlaceholderLineEdit(filters_widget) folders_filter_text.setPlaceholderText("Filter folders...") - folders_widget = FoldersWidget(controller, folders_wrapper) + 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) - folders_wrapper_layout = QtWidgets.QVBoxLayout(folders_wrapper) - folders_wrapper_layout.setContentsMargins(0, 0, 0, 0) - folders_wrapper_layout.addWidget(folders_filter_text, 0) - folders_wrapper_layout.addWidget(folders_widget, 1) + 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) + + # - Folders widget + folders_widget = FoldersWidget(controller, content_body) + folders_widget.set_header_visible(True) # - Tasks widget tasks_widget = TasksWidget(controller, content_body) - content_body.addWidget(folders_wrapper) + content_body.addWidget(folders_widget) content_body.addWidget(tasks_widget) content_body.setStretchFactor(0, 100) content_body.setStretchFactor(1, 65) @@ -67,20 +80,27 @@ class HierarchyPage(QtWidgets.QWidget): main_layout = QtWidgets.QVBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) main_layout.addWidget(header_widget, 0) + main_layout.addWidget(filters_widget, 0) main_layout.addWidget(content_body, 1) 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( + self._on_my_tasks_checkbox_state_changed + ) self._is_visible = False self._controller = controller 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._project_name = None + # Post init projects_combobox.set_listen_to_selection_change(self._is_visible) @@ -91,10 +111,14 @@ class HierarchyPage(QtWidgets.QWidget): self._projects_combobox.set_listen_to_selection_change(visible) if visible and project_name: self._projects_combobox.set_selection(project_name) + self._project_name = project_name def refresh(self): self._folders_widget.refresh() self._tasks_widget.refresh() + self._on_my_tasks_checkbox_state_changed( + self._my_tasks_checkbox.checkState() + ) def _on_back_clicked(self): self._controller.set_selected_project(None) @@ -104,3 +128,16 @@ 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): + folder_ids = None + task_ids = None + state = checkstate_int_to_enum(state) + if state == QtCore.Qt.Checked: + entity_ids = self._controller.get_my_tasks_entity_ids( + self._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/launcher/ui/window.py b/client/ayon_core/tools/launcher/ui/window.py index 2d52a73c38..aa336108ed 100644 --- a/client/ayon_core/tools/launcher/ui/window.py +++ b/client/ayon_core/tools/launcher/ui/window.py @@ -17,7 +17,7 @@ class LauncherWindow(QtWidgets.QWidget): page_side_anim_interval = 250 def __init__(self, controller=None, parent=None): - super(LauncherWindow, self).__init__(parent) + super().__init__(parent) if controller is None: controller = BaseLauncherController() @@ -153,14 +153,14 @@ class LauncherWindow(QtWidgets.QWidget): self.resize(520, 740) def showEvent(self, event): - super(LauncherWindow, self).showEvent(event) + super().showEvent(event) self._window_is_active = True if not self._actions_refresh_timer.isActive(): self._actions_refresh_timer.start() self._controller.refresh() def closeEvent(self, event): - super(LauncherWindow, self).closeEvent(event) + super().closeEvent(event) self._window_is_active = False self._actions_refresh_timer.stop() @@ -176,7 +176,7 @@ class LauncherWindow(QtWidgets.QWidget): self._on_actions_refresh_timeout() self._actions_refresh_timer.start() - super(LauncherWindow, self).changeEvent(event) + super().changeEvent(event) def _on_actions_refresh_timeout(self): # Stop timer if widget is not visible diff --git a/client/ayon_core/tools/loader/ui/product_types_combo.py b/client/ayon_core/tools/loader/ui/product_types_combo.py index 91fa52b0e9..525f1cae1b 100644 --- a/client/ayon_core/tools/loader/ui/product_types_combo.py +++ b/client/ayon_core/tools/loader/ui/product_types_combo.py @@ -1,3 +1,4 @@ +from __future__ import annotations from qtpy import QtGui, QtCore from ._multicombobox import ( diff --git a/client/ayon_core/tools/utils/folders_widget.py b/client/ayon_core/tools/utils/folders_widget.py index b3b5cebb0e..7b71dd087c 100644 --- a/client/ayon_core/tools/utils/folders_widget.py +++ b/client/ayon_core/tools/utils/folders_widget.py @@ -1,4 +1,6 @@ +from __future__ import annotations import collections +from typing import Optional from qtpy import QtWidgets, QtGui, QtCore @@ -33,7 +35,10 @@ class FoldersQtModel(QtGui.QStandardItemModel): refreshed = QtCore.Signal() def __init__(self, controller): - super(FoldersQtModel, self).__init__() + super().__init__() + + self.setColumnCount(1) + self.setHeaderData(0, QtCore.Qt.Horizontal, "Folders") self._controller = controller self._items_by_id = {} @@ -334,6 +339,29 @@ class FoldersQtModel(QtGui.QStandardItemModel): self.refreshed.emit() +class FoldersProxyModel(RecursiveSortFilterProxyModel): + def __init__(self): + super().__init__() + + self._folder_ids_filter = None + + def set_folder_ids_filter(self, folder_ids: Optional[list[str]]): + if self._folder_ids_filter == folder_ids: + return + self._folder_ids_filter = folder_ids + self.invalidateFilter() + + def filterAcceptsRow(self, row, parent_index): + if self._folder_ids_filter is not None: + if not self._folder_ids_filter: + return False + source_index = self.sourceModel().index(row, 0, parent_index) + folder_id = source_index.data(FOLDER_ID_ROLE) + if folder_id not in self._folder_ids_filter: + return False + return super().filterAcceptsRow(row, parent_index) + + class FoldersWidget(QtWidgets.QWidget): """Folders widget. @@ -369,13 +397,13 @@ class FoldersWidget(QtWidgets.QWidget): refreshed = QtCore.Signal() def __init__(self, controller, parent, handle_expected_selection=False): - super(FoldersWidget, self).__init__(parent) + super().__init__(parent) folders_view = TreeView(self) folders_view.setHeaderHidden(True) folders_model = FoldersQtModel(controller) - folders_proxy_model = RecursiveSortFilterProxyModel() + folders_proxy_model = FoldersProxyModel() folders_proxy_model.setSourceModel(folders_model) folders_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) @@ -446,6 +474,18 @@ class FoldersWidget(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_header_visible(self, visible: bool): + self._folders_view.setHeaderHidden(not visible) + def refresh(self): """Refresh folders model. diff --git a/client/ayon_core/tools/utils/tasks_widget.py b/client/ayon_core/tools/utils/tasks_widget.py index 30846e6cda..9118611c23 100644 --- a/client/ayon_core/tools/utils/tasks_widget.py +++ b/client/ayon_core/tools/utils/tasks_widget.py @@ -1,3 +1,6 @@ +from __future__ import annotations +from typing import Optional + from qtpy import QtWidgets, QtGui, QtCore from ayon_core.style import ( @@ -343,6 +346,29 @@ class TasksQtModel(QtGui.QStandardItemModel): return self._has_content +class TasksProxyModel(QtCore.QSortFilterProxyModel): + def __init__(self): + super().__init__() + + self._task_ids_filter: Optional[set[str]] = None + + def set_task_ids_filter(self, task_ids: Optional[set[str]]): + if self._task_ids_filter == task_ids: + return + self._task_ids_filter = task_ids + self.invalidateFilter() + + def filterAcceptsRow(self, row, parent_index): + if self._task_ids_filter is not None: + if not self._task_ids_filter: + return False + source_index = self.sourceModel().index(row, 0, parent_index) + task_id = source_index.data(ITEM_ID_ROLE) + if task_id is not None and task_id not in self._task_ids_filter: + return False + return super().filterAcceptsRow(row, parent_index) + + class TasksWidget(QtWidgets.QWidget): """Tasks widget. @@ -364,7 +390,7 @@ class TasksWidget(QtWidgets.QWidget): tasks_view.setIndentation(0) tasks_model = TasksQtModel(controller) - tasks_proxy_model = QtCore.QSortFilterProxyModel() + tasks_proxy_model = TasksProxyModel() tasks_proxy_model.setSourceModel(tasks_model) tasks_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) @@ -490,6 +516,15 @@ class TasksWidget(QtWidgets.QWidget): ) return True + 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 _on_tasks_refresh_finished(self, event): """Tasks were refreshed in controller.