diff --git a/client/ayon_core/plugins/load/create_hero_version.py b/client/ayon_core/plugins/load/create_hero_version.py
index aef0cf8863..d01a97e2ff 100644
--- a/client/ayon_core/plugins/load/create_hero_version.py
+++ b/client/ayon_core/plugins/load/create_hero_version.py
@@ -75,6 +75,7 @@ class CreateHeroVersion(load.ProductLoaderPlugin):
msgBox.setStyleSheet(style.load_stylesheet())
msgBox.setWindowFlags(
msgBox.windowFlags() | QtCore.Qt.WindowType.FramelessWindowHint
+ | QtCore.Qt.WindowType.WindowStaysOnTopHint
)
msgBox.exec_()
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.
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/launcher/ui/hierarchy_page.py b/client/ayon_core/tools/launcher/ui/hierarchy_page.py
index 47388d9685..3c8be4679e 100644
--- a/client/ayon_core/tools/launcher/ui/hierarchy_page.py
+++ b/client/ayon_core/tools/launcher/ui/hierarchy_page.py
@@ -2,19 +2,47 @@ 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.lib import checkstate_int_to_enum
+from ayon_core.tools.utils.folders_widget import FoldersFiltersWidget
from .workfiles_page import WorkfilesPage
+class LauncherFoldersWidget(FoldersWidget):
+ focused_in = QtCore.Signal()
+
+ def __init__(self, *args, **kwargs) -> None:
+ super().__init__(*args, **kwargs)
+ self._folders_view.installEventFilter(self)
+
+ def eventFilter(self, obj, event):
+ if event.type() == QtCore.QEvent.FocusIn:
+ self.focused_in.emit()
+ return False
+
+
+class LauncherTasksWidget(TasksWidget):
+ focused_in = QtCore.Signal()
+
+ def __init__(self, *args, **kwargs) -> None:
+ super().__init__(*args, **kwargs)
+ self._tasks_view.installEventFilter(self)
+
+ def deselect(self):
+ sel_model = self._tasks_view.selectionModel()
+ sel_model.clearSelection()
+
+ def eventFilter(self, obj, event):
+ if event.type() == QtCore.QEvent.FocusIn:
+ self.focused_in.emit()
+ return False
+
+
class HierarchyPage(QtWidgets.QWidget):
def __init__(self, controller, parent):
super().__init__(parent)
@@ -46,34 +74,15 @@ 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 = FoldersWidget(controller, content_body)
+ folders_widget = LauncherFoldersWidget(controller, content_body)
folders_widget.set_header_visible(True)
folders_widget.set_deselectable(True)
# - Tasks widget
- tasks_widget = TasksWidget(controller, content_body)
+ tasks_widget = LauncherTasksWidget(controller, content_body)
# - Third page - Workfiles
workfiles_page = WorkfilesPage(controller, content_body)
@@ -93,17 +102,18 @@ 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)
+ tasks_widget.focused_in.connect(self._on_tasks_focus)
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._workfiles_page = workfiles_page
@@ -126,9 +136,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)
@@ -139,11 +146,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
)
@@ -151,3 +157,9 @@ class HierarchyPage(QtWidgets.QWidget):
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_folders_focus(self):
+ self._workfiles_page.deselect()
+
+ def _on_tasks_focus(self):
+ self._workfiles_page.deselect()
diff --git a/client/ayon_core/tools/launcher/ui/workfiles_page.py b/client/ayon_core/tools/launcher/ui/workfiles_page.py
index 1ea223031e..d81221f38d 100644
--- a/client/ayon_core/tools/launcher/ui/workfiles_page.py
+++ b/client/ayon_core/tools/launcher/ui/workfiles_page.py
@@ -3,7 +3,7 @@ from typing import Optional
import ayon_api
from qtpy import QtCore, QtWidgets, QtGui
-from ayon_core.tools.utils import get_qt_icon
+from ayon_core.tools.utils import get_qt_icon, DeselectableTreeView
from ayon_core.tools.launcher.abstract import AbstractLauncherFrontEnd
VERSION_ROLE = QtCore.Qt.UserRole + 1
@@ -127,7 +127,7 @@ class WorkfilesModel(QtGui.QStandardItemModel):
return icon
-class WorkfilesView(QtWidgets.QTreeView):
+class WorkfilesView(DeselectableTreeView):
def drawBranches(self, painter, rect, index):
return
@@ -165,6 +165,10 @@ class WorkfilesPage(QtWidgets.QWidget):
def refresh(self) -> None:
self._workfiles_model.refresh()
+ def deselect(self):
+ sel_model = self._workfiles_view.selectionModel()
+ sel_model.clearSelection()
+
def _on_refresh(self) -> None:
self._workfiles_proxy.sort(0, QtCore.Qt.DescendingOrder)
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 9f159bfb21..d0cc9db2f5 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,17 @@ class LoaderController(BackendLoaderController, FrontendLoaderController):
output[folder_id] = label
return output
+ 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
+ )
+
def get_available_tags_by_entity_type(
self, project_name: str
) -> dict[str, list[str]]:
@@ -476,20 +496,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 +551,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()
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..27e416b495 100644
--- a/client/ayon_core/tools/loader/ui/window.py
+++ b/client/ayon_core/tools/loader/ui/window.py
@@ -5,11 +5,11 @@ 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
@@ -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,21 @@ 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:
+ 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()
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/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py
index 84786a671e..ca95b1ff1a 100644
--- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py
+++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py
@@ -202,7 +202,7 @@ class ContextCardWidget(CardWidget):
Is not visually under group widget and is always at the top of card view.
"""
- def __init__(self, parent):
+ def __init__(self, parent: QtWidgets.QWidget):
super().__init__(parent)
self._id = CONTEXT_ID
@@ -211,7 +211,7 @@ class ContextCardWidget(CardWidget):
icon_widget = PublishPixmapLabel(None, self)
icon_widget.setObjectName("ProductTypeIconLabel")
- label_widget = QtWidgets.QLabel(CONTEXT_LABEL, self)
+ label_widget = QtWidgets.QLabel(f"{CONTEXT_LABEL}", self)
icon_layout = QtWidgets.QHBoxLayout()
icon_layout.setContentsMargins(5, 5, 5, 5)
@@ -288,6 +288,8 @@ class InstanceCardWidget(CardWidget):
self._last_product_name = None
self._last_variant = None
self._last_label = None
+ self._last_folder_path = None
+ self._last_task_name = None
icon_widget = IconValuePixmapLabel(group_icon, self)
icon_widget.setObjectName("ProductTypeIconLabel")
@@ -383,29 +385,54 @@ class InstanceCardWidget(CardWidget):
self._icon_widget.setVisible(valid)
self._context_warning.setVisible(not valid)
+ @staticmethod
+ def _get_card_widget_sub_label(
+ folder_path: Optional[str],
+ task_name: Optional[str],
+ ) -> str:
+ sublabel = ""
+ if folder_path:
+ folder_name = folder_path.rsplit("/", 1)[-1]
+ sublabel = f"{folder_name}"
+ if task_name:
+ sublabel += f" - {task_name}"
+ return sublabel
+
def _update_product_name(self):
variant = self.instance.variant
product_name = self.instance.product_name
label = self.instance.label
+ folder_path = self.instance.folder_path
+ task_name = self.instance.task_name
if (
variant == self._last_variant
and product_name == self._last_product_name
and label == self._last_label
+ and folder_path == self._last_folder_path
+ and task_name == self._last_task_name
):
return
self._last_variant = variant
self._last_product_name = product_name
self._last_label = label
+ self._last_folder_path = folder_path
+ self._last_task_name = task_name
+
# Make `variant` bold
label = html_escape(self.instance.label)
found_parts = set(re.findall(variant, label, re.IGNORECASE))
if found_parts:
for part in found_parts:
- replacement = "{}".format(part)
+ replacement = f"{part}"
label = label.replace(part, replacement)
+ label = f"{label}"
+ sublabel = self._get_card_widget_sub_label(folder_path, task_name)
+ if sublabel:
+ label += f"
{sublabel}"
+
self._label_widget.setText(label)
# HTML text will cause that label start catch mouse clicks
# - disabling with changing interaction flag
@@ -702,11 +729,9 @@ class InstanceCardView(AbstractInstanceView):
def refresh(self):
"""Refresh instances in view based on CreatedContext."""
-
self._make_sure_context_widget_exists()
self._update_convertors_group()
-
context_info_by_id = self._controller.get_instances_context_info()
# Prepare instances by group and identifiers by group
@@ -814,6 +839,8 @@ class InstanceCardView(AbstractInstanceView):
widget.setVisible(False)
widget.deleteLater()
+ sorted_group_names.insert(0, CONTEXT_GROUP)
+
self._parent_id_by_id = parent_id_by_id
self._instance_ids_by_parent_id = instance_ids_by_parent_id
self._group_name_by_instance_id = group_by_instance_id
@@ -881,7 +908,7 @@ class InstanceCardView(AbstractInstanceView):
context_info,
is_parent_active,
group_icon,
- group_widget
+ group_widget,
)
widget.selected.connect(self._on_widget_selection)
widget.active_changed.connect(self._on_active_changed)
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)
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..f506af5352 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"
@@ -343,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]]):
@@ -794,3 +798,47 @@ 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 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())
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
)