Merge branch 'develop' into feature/envs-in-roots

This commit is contained in:
Jakub Trllo 2025-04-07 15:13:11 +02:00 committed by GitHub
commit f450942ccd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 239 additions and 39 deletions

View file

@ -39,6 +39,7 @@ class CollectAudio(pyblish.api.ContextPlugin):
"blender", "blender",
"houdini", "houdini",
"max", "max",
"circuit",
] ]
audio_product_name = "audioMain" audio_product_name = "audioMain"

View file

@ -54,7 +54,8 @@ class ExtractBurnin(publish.Extractor):
"houdini", "houdini",
"max", "max",
"blender", "blender",
"unreal" "unreal",
"circuit",
] ]
optional = True optional = True

View file

@ -91,7 +91,8 @@ class ExtractReview(pyblish.api.InstancePlugin):
"webpublisher", "webpublisher",
"aftereffects", "aftereffects",
"flame", "flame",
"unreal" "unreal",
"circuit",
] ]
# Supported extensions # Supported extensions

View file

@ -39,7 +39,8 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
"nuke", "nuke",
"aftereffects", "aftereffects",
"unreal", "unreal",
"houdini" "houdini",
"circuit",
] ]
enabled = False enabled = False

View file

@ -227,6 +227,9 @@ class HierarchyModel(object):
self._tasks_by_id = NestedCacheItem( self._tasks_by_id = NestedCacheItem(
levels=2, default_factory=dict, lifetime=self.lifetime) 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._folders_refreshing = set()
self._tasks_refreshing = set() self._tasks_refreshing = set()
self._controller = controller self._controller = controller
@ -238,6 +241,8 @@ class HierarchyModel(object):
self._task_items.reset() self._task_items.reset()
self._tasks_by_id.reset() self._tasks_by_id.reset()
self._entity_ids_by_assignee.reset()
def refresh_project(self, project_name): def refresh_project(self, project_name):
"""Force to refresh folder items for a project. """Force to refresh folder items for a project.
@ -461,6 +466,54 @@ class HierarchyModel(object):
output = self.get_task_entities(project_name, {task_id}) output = self.get_task_entities(project_name, {task_id})
return output[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 @contextlib.contextmanager
def _folder_refresh_event_manager(self, project_name, sender): def _folder_refresh_event_manager(self, project_name, sender):
self._folders_refreshing.add(project_name) self._folders_refreshing.add(project_name)

View file

@ -160,8 +160,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
Returns: Returns:
list[FolderItem]: Minimum possible information needed list[FolderItem]: Minimum possible information needed
for visualisation of folder hierarchy. for visualisation of folder hierarchy.
"""
"""
pass pass
@abstractmethod @abstractmethod
@ -180,8 +180,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
Returns: Returns:
list[TaskItem]: Minimum possible information needed list[TaskItem]: Minimum possible information needed
for visualisation of tasks. for visualisation of tasks.
"""
"""
pass pass
@abstractmethod @abstractmethod
@ -190,8 +190,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
Returns: Returns:
Union[str, None]: Selected project name. Union[str, None]: Selected project name.
"""
"""
pass pass
@abstractmethod @abstractmethod
@ -200,8 +200,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
Returns: Returns:
Union[str, None]: Selected folder id. Union[str, None]: Selected folder id.
"""
"""
pass pass
@abstractmethod @abstractmethod
@ -210,8 +210,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
Returns: Returns:
Union[str, None]: Selected task id. Union[str, None]: Selected task id.
"""
"""
pass pass
@abstractmethod @abstractmethod
@ -220,8 +220,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
Returns: Returns:
Union[str, None]: Selected task name. Union[str, None]: Selected task name.
"""
"""
pass pass
@abstractmethod @abstractmethod
@ -238,8 +238,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
Returns: Returns:
dict[str, Union[str, None]]: Selected context. dict[str, Union[str, None]]: Selected context.
"""
"""
pass pass
@abstractmethod @abstractmethod
@ -249,8 +249,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
Args: Args:
project_name (Union[str, None]): Project nameor None if no project project_name (Union[str, None]): Project nameor None if no project
is selected. is selected.
"""
"""
pass pass
@abstractmethod @abstractmethod
@ -260,8 +260,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
Args: Args:
folder_id (Union[str, None]): Folder id or None if no folder folder_id (Union[str, None]): Folder id or None if no folder
is selected. is selected.
"""
"""
pass pass
@abstractmethod @abstractmethod
@ -273,8 +273,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
is selected. is selected.
task_name (Union[str, None]): Task name or None if no task task_name (Union[str, None]): Task name or None if no task
is selected. is selected.
"""
"""
pass pass
# Actions # Actions
@ -290,8 +290,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
Returns: Returns:
list[ActionItem]: List of action items that should be shown list[ActionItem]: List of action items that should be shown
for given context. for given context.
"""
"""
pass pass
@abstractmethod @abstractmethod
@ -303,8 +303,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
folder_id (Union[str, None]): Folder id. folder_id (Union[str, None]): Folder id.
task_id (Union[str, None]): Task id. task_id (Union[str, None]): Task id.
action_id (str): Action identifier. action_id (str): Action identifier.
"""
"""
pass pass
@abstractmethod @abstractmethod
@ -317,10 +317,10 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
project_name (Union[str, None]): Project name. project_name (Union[str, None]): Project name.
folder_id (Union[str, None]): Folder id. folder_id (Union[str, None]): Folder id.
task_id (Union[str, None]): Task 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. enabled (bool): New value of force not open workfile.
"""
"""
pass pass
@abstractmethod @abstractmethod
@ -340,5 +340,17 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
Triggers 'controller.refresh.actions.started' event at the beginning Triggers 'controller.refresh.actions.started' event at the beginning
and 'controller.refresh.actions.finished' at the end. 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 pass

View file

@ -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.lib.events import QueuedEventSystem
from ayon_core.settings import get_project_settings from ayon_core.settings import get_project_settings
from ayon_core.tools.common_models import ProjectsModel, HierarchyModel 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 .abstract import AbstractLauncherFrontEnd, AbstractLauncherBackend
from .models import LauncherSelectionModel, ActionsModel from .models import LauncherSelectionModel, ActionsModel
NOT_SET = object()
class BaseLauncherController( class BaseLauncherController(
AbstractLauncherFrontEnd, AbstractLauncherBackend AbstractLauncherFrontEnd, AbstractLauncherBackend
@ -15,6 +17,8 @@ class BaseLauncherController(
self._event_system = None self._event_system = None
self._log = None self._log = None
self._username = NOT_SET
self._selection_model = LauncherSelectionModel(self) self._selection_model = LauncherSelectionModel(self)
self._projects_model = ProjectsModel(self) self._projects_model = ProjectsModel(self)
self._hierarchy_model = HierarchyModel(self) self._hierarchy_model = HierarchyModel(self)
@ -168,5 +172,19 @@ class BaseLauncherController(
self._emit_event("controller.refresh.actions.finished") 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): def _emit_event(self, topic, data=None):
self.emit_event(topic, data, "controller") self.emit_event(topic, data, "controller")

View file

@ -5,17 +5,17 @@ from ayon_core.tools.utils import (
PlaceholderLineEdit, PlaceholderLineEdit,
SquareButton, SquareButton,
RefreshButton, RefreshButton,
)
from ayon_core.tools.utils import (
ProjectsCombobox, ProjectsCombobox,
FoldersWidget, FoldersWidget,
TasksWidget, TasksWidget,
NiceCheckbox,
) )
from ayon_core.tools.utils.lib import checkstate_int_to_enum
class HierarchyPage(QtWidgets.QWidget): class HierarchyPage(QtWidgets.QWidget):
def __init__(self, controller, parent): def __init__(self, controller, parent):
super(HierarchyPage, self).__init__(parent) super().__init__(parent)
# Header # Header
header_widget = QtWidgets.QWidget(self) header_widget = QtWidgets.QWidget(self)
@ -43,23 +43,36 @@ class HierarchyPage(QtWidgets.QWidget):
) )
content_body.setOrientation(QtCore.Qt.Horizontal) content_body.setOrientation(QtCore.Qt.Horizontal)
# - Folders widget with filter # - filters
folders_wrapper = QtWidgets.QWidget(content_body) filters_widget = QtWidgets.QWidget(self)
folders_filter_text = PlaceholderLineEdit(folders_wrapper) folders_filter_text = PlaceholderLineEdit(filters_widget)
folders_filter_text.setPlaceholderText("Filter folders...") 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) my_tasks_checkbox = NiceCheckbox(filters_widget)
folders_wrapper_layout.setContentsMargins(0, 0, 0, 0) my_tasks_checkbox.setChecked(False)
folders_wrapper_layout.addWidget(folders_filter_text, 0) my_tasks_checkbox.setToolTip(my_tasks_tooltip)
folders_wrapper_layout.addWidget(folders_widget, 1)
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
tasks_widget = TasksWidget(controller, content_body) tasks_widget = TasksWidget(controller, content_body)
content_body.addWidget(folders_wrapper) content_body.addWidget(folders_widget)
content_body.addWidget(tasks_widget) content_body.addWidget(tasks_widget)
content_body.setStretchFactor(0, 100) content_body.setStretchFactor(0, 100)
content_body.setStretchFactor(1, 65) content_body.setStretchFactor(1, 65)
@ -67,20 +80,27 @@ class HierarchyPage(QtWidgets.QWidget):
main_layout = QtWidgets.QVBoxLayout(self) main_layout = QtWidgets.QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.addWidget(header_widget, 0) main_layout.addWidget(header_widget, 0)
main_layout.addWidget(filters_widget, 0)
main_layout.addWidget(content_body, 1) main_layout.addWidget(content_body, 1)
btn_back.clicked.connect(self._on_back_clicked) btn_back.clicked.connect(self._on_back_clicked)
refresh_btn.clicked.connect(self._on_refresh_clicked) refresh_btn.clicked.connect(self._on_refresh_clicked)
folders_filter_text.textChanged.connect(self._on_filter_text_changed) 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._is_visible = False
self._controller = controller self._controller = controller
self._btn_back = btn_back self._btn_back = btn_back
self._projects_combobox = projects_combobox self._projects_combobox = projects_combobox
self._my_tasks_checkbox = my_tasks_checkbox
self._folders_widget = folders_widget self._folders_widget = folders_widget
self._tasks_widget = tasks_widget self._tasks_widget = tasks_widget
self._project_name = None
# Post init # Post init
projects_combobox.set_listen_to_selection_change(self._is_visible) 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) self._projects_combobox.set_listen_to_selection_change(visible)
if visible and project_name: if visible and project_name:
self._projects_combobox.set_selection(project_name) self._projects_combobox.set_selection(project_name)
self._project_name = project_name
def refresh(self): def refresh(self):
self._folders_widget.refresh() self._folders_widget.refresh()
self._tasks_widget.refresh() self._tasks_widget.refresh()
self._on_my_tasks_checkbox_state_changed(
self._my_tasks_checkbox.checkState()
)
def _on_back_clicked(self): def _on_back_clicked(self):
self._controller.set_selected_project(None) self._controller.set_selected_project(None)
@ -104,3 +128,16 @@ class HierarchyPage(QtWidgets.QWidget):
def _on_filter_text_changed(self, text): def _on_filter_text_changed(self, text):
self._folders_widget.set_name_filter(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)

View file

@ -17,7 +17,7 @@ class LauncherWindow(QtWidgets.QWidget):
page_side_anim_interval = 250 page_side_anim_interval = 250
def __init__(self, controller=None, parent=None): def __init__(self, controller=None, parent=None):
super(LauncherWindow, self).__init__(parent) super().__init__(parent)
if controller is None: if controller is None:
controller = BaseLauncherController() controller = BaseLauncherController()
@ -153,14 +153,14 @@ class LauncherWindow(QtWidgets.QWidget):
self.resize(520, 740) self.resize(520, 740)
def showEvent(self, event): def showEvent(self, event):
super(LauncherWindow, self).showEvent(event) super().showEvent(event)
self._window_is_active = True self._window_is_active = True
if not self._actions_refresh_timer.isActive(): if not self._actions_refresh_timer.isActive():
self._actions_refresh_timer.start() self._actions_refresh_timer.start()
self._controller.refresh() self._controller.refresh()
def closeEvent(self, event): def closeEvent(self, event):
super(LauncherWindow, self).closeEvent(event) super().closeEvent(event)
self._window_is_active = False self._window_is_active = False
self._actions_refresh_timer.stop() self._actions_refresh_timer.stop()
@ -176,7 +176,7 @@ class LauncherWindow(QtWidgets.QWidget):
self._on_actions_refresh_timeout() self._on_actions_refresh_timeout()
self._actions_refresh_timer.start() self._actions_refresh_timer.start()
super(LauncherWindow, self).changeEvent(event) super().changeEvent(event)
def _on_actions_refresh_timeout(self): def _on_actions_refresh_timeout(self):
# Stop timer if widget is not visible # Stop timer if widget is not visible

View file

@ -1,3 +1,4 @@
from __future__ import annotations
from qtpy import QtGui, QtCore from qtpy import QtGui, QtCore
from ._multicombobox import ( from ._multicombobox import (

View file

@ -1,4 +1,6 @@
from __future__ import annotations
import collections import collections
from typing import Optional
from qtpy import QtWidgets, QtGui, QtCore from qtpy import QtWidgets, QtGui, QtCore
@ -33,7 +35,10 @@ class FoldersQtModel(QtGui.QStandardItemModel):
refreshed = QtCore.Signal() refreshed = QtCore.Signal()
def __init__(self, controller): def __init__(self, controller):
super(FoldersQtModel, self).__init__() super().__init__()
self.setColumnCount(1)
self.setHeaderData(0, QtCore.Qt.Horizontal, "Folders")
self._controller = controller self._controller = controller
self._items_by_id = {} self._items_by_id = {}
@ -334,6 +339,29 @@ class FoldersQtModel(QtGui.QStandardItemModel):
self.refreshed.emit() 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): class FoldersWidget(QtWidgets.QWidget):
"""Folders widget. """Folders widget.
@ -369,13 +397,13 @@ class FoldersWidget(QtWidgets.QWidget):
refreshed = QtCore.Signal() refreshed = QtCore.Signal()
def __init__(self, controller, parent, handle_expected_selection=False): def __init__(self, controller, parent, handle_expected_selection=False):
super(FoldersWidget, self).__init__(parent) super().__init__(parent)
folders_view = TreeView(self) folders_view = TreeView(self)
folders_view.setHeaderHidden(True) folders_view.setHeaderHidden(True)
folders_model = FoldersQtModel(controller) folders_model = FoldersQtModel(controller)
folders_proxy_model = RecursiveSortFilterProxyModel() folders_proxy_model = FoldersProxyModel()
folders_proxy_model.setSourceModel(folders_model) folders_proxy_model.setSourceModel(folders_model)
folders_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) folders_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
@ -446,6 +474,18 @@ class FoldersWidget(QtWidgets.QWidget):
if name: if name:
self._folders_view.expandAll() 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): def refresh(self):
"""Refresh folders model. """Refresh folders model.

View file

@ -1,3 +1,6 @@
from __future__ import annotations
from typing import Optional
from qtpy import QtWidgets, QtGui, QtCore from qtpy import QtWidgets, QtGui, QtCore
from ayon_core.style import ( from ayon_core.style import (
@ -343,6 +346,29 @@ class TasksQtModel(QtGui.QStandardItemModel):
return self._has_content 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): class TasksWidget(QtWidgets.QWidget):
"""Tasks widget. """Tasks widget.
@ -364,7 +390,7 @@ class TasksWidget(QtWidgets.QWidget):
tasks_view.setIndentation(0) tasks_view.setIndentation(0)
tasks_model = TasksQtModel(controller) tasks_model = TasksQtModel(controller)
tasks_proxy_model = QtCore.QSortFilterProxyModel() tasks_proxy_model = TasksProxyModel()
tasks_proxy_model.setSourceModel(tasks_model) tasks_proxy_model.setSourceModel(tasks_model)
tasks_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) tasks_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
@ -490,6 +516,15 @@ class TasksWidget(QtWidgets.QWidget):
) )
return True 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): def _on_tasks_refresh_finished(self, event):
"""Tasks were refreshed in controller. """Tasks were refreshed in controller.