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",
"houdini",
"max",
"circuit",
]
audio_product_name = "audioMain"

View file

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

View file

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

View file

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

View file

@ -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)

View file

@ -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

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.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")

View file

@ -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)

View file

@ -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

View file

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

View file

@ -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.

View file

@ -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.