mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 12:54:40 +01:00
Merge branch 'develop' into enhancement/publisher_existing_variants_sorted
This commit is contained in:
commit
8eb0b7a748
31 changed files with 2437 additions and 706 deletions
|
|
@ -235,6 +235,30 @@ def version(build):
|
|||
print(os.environ["AYON_VERSION"])
|
||||
|
||||
|
||||
@main_cli.command()
|
||||
@click.option(
|
||||
"--project",
|
||||
type=str,
|
||||
help="Project name",
|
||||
required=True)
|
||||
def create_project_structure(
|
||||
project,
|
||||
):
|
||||
"""Create project folder structure as defined in setting
|
||||
`ayon+settings://core/project_folder_structure`
|
||||
|
||||
Args:
|
||||
project (str): The name of the project for which you
|
||||
want to create its additional folder structure.
|
||||
|
||||
"""
|
||||
|
||||
from ayon_core.pipeline.project_folders import create_project_folders
|
||||
|
||||
print(f">>> Creating project folder structure for project '{project}'.")
|
||||
create_project_folders(project)
|
||||
|
||||
|
||||
def _set_global_environments() -> None:
|
||||
"""Set global AYON environments."""
|
||||
# First resolve general environment
|
||||
|
|
|
|||
|
|
@ -1,24 +1,28 @@
|
|||
"""Plugins for loading representations and products into host applications."""
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import logging
|
||||
from typing import Any, Type, Optional
|
||||
from abc import abstractmethod
|
||||
|
||||
from ayon_core.settings import get_project_settings
|
||||
from abc import abstractmethod
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Optional, Type
|
||||
|
||||
from ayon_core.pipeline.plugin_discover import (
|
||||
deregister_plugin,
|
||||
deregister_plugin_path,
|
||||
discover,
|
||||
register_plugin,
|
||||
register_plugin_path,
|
||||
deregister_plugin,
|
||||
deregister_plugin_path
|
||||
)
|
||||
from ayon_core.settings import get_project_settings
|
||||
|
||||
from .utils import get_representation_path_from_context
|
||||
|
||||
|
||||
class LoaderPlugin(list):
|
||||
"""Load representation into host application"""
|
||||
|
||||
product_types = set()
|
||||
product_types: set[str] = set()
|
||||
product_base_types: Optional[set[str]] = None
|
||||
representations = set()
|
||||
extensions = {"*"}
|
||||
order = 0
|
||||
|
|
@ -61,12 +65,12 @@ class LoaderPlugin(list):
|
|||
if not plugin_settings:
|
||||
return
|
||||
|
||||
print(">>> We have preset for {}".format(plugin_name))
|
||||
print(f">>> We have preset for {plugin_name}")
|
||||
for option, value in plugin_settings.items():
|
||||
if option == "enabled" and value is False:
|
||||
print(" - is disabled by preset")
|
||||
else:
|
||||
print(" - setting `{}`: `{}`".format(option, value))
|
||||
print(f" - setting `{option}`: `{value}`")
|
||||
setattr(cls, option, value)
|
||||
|
||||
@classmethod
|
||||
|
|
@ -79,7 +83,6 @@ class LoaderPlugin(list):
|
|||
Returns:
|
||||
bool: Representation has valid extension
|
||||
"""
|
||||
|
||||
if "*" in cls.extensions:
|
||||
return True
|
||||
|
||||
|
|
@ -124,18 +127,34 @@ class LoaderPlugin(list):
|
|||
"""
|
||||
|
||||
plugin_repre_names = cls.get_representations()
|
||||
plugin_product_types = cls.product_types
|
||||
|
||||
# If the product base type isn't defined on the loader plugin,
|
||||
# then we will use the product types.
|
||||
plugin_product_filter = cls.product_base_types
|
||||
if plugin_product_filter is None:
|
||||
plugin_product_filter = cls.product_types
|
||||
|
||||
if plugin_product_filter:
|
||||
plugin_product_filter = set(plugin_product_filter)
|
||||
|
||||
repre_entity = context.get("representation")
|
||||
product_entity = context["product"]
|
||||
|
||||
# If no representation names, product types or extensions are defined
|
||||
# then loader is not compatible with any context.
|
||||
if (
|
||||
not plugin_repre_names
|
||||
or not plugin_product_types
|
||||
or not plugin_product_filter
|
||||
or not cls.extensions
|
||||
):
|
||||
return False
|
||||
|
||||
repre_entity = context.get("representation")
|
||||
# If no representation entity is provided then loader is not
|
||||
# compatible with context.
|
||||
if not repre_entity:
|
||||
return False
|
||||
|
||||
# Check the compatibility with the representation names.
|
||||
plugin_repre_names = set(plugin_repre_names)
|
||||
if (
|
||||
"*" not in plugin_repre_names
|
||||
|
|
@ -143,17 +162,34 @@ class LoaderPlugin(list):
|
|||
):
|
||||
return False
|
||||
|
||||
# Check the compatibility with the extension of the representation.
|
||||
if not cls.has_valid_extension(repre_entity):
|
||||
return False
|
||||
|
||||
plugin_product_types = set(plugin_product_types)
|
||||
if "*" in plugin_product_types:
|
||||
product_type = product_entity.get("productType")
|
||||
product_base_type = product_entity.get("productBaseType")
|
||||
|
||||
# Use product base type if defined, otherwise use product type.
|
||||
product_filter = product_base_type
|
||||
# If there is no product base type defined in the product entity,
|
||||
# then we will use the product type.
|
||||
if product_filter is None:
|
||||
product_filter = product_type
|
||||
|
||||
# If wildcard is used in product types or base types,
|
||||
# then we will consider the loader compatible with any product type.
|
||||
if "*" in plugin_product_filter:
|
||||
return True
|
||||
|
||||
product_entity = context["product"]
|
||||
product_type = product_entity["productType"]
|
||||
# compatibility with legacy loader
|
||||
if cls.product_base_types is None and product_base_type:
|
||||
cls.log.error(
|
||||
f"Loader {cls.__name__} is doesn't specify "
|
||||
"`product_base_types` but product entity has "
|
||||
f"`productBaseType` defined as `{product_base_type}`. "
|
||||
)
|
||||
|
||||
return product_type in plugin_product_types
|
||||
return product_filter in plugin_product_filter
|
||||
|
||||
@classmethod
|
||||
def get_representations(cls):
|
||||
|
|
@ -208,14 +244,12 @@ class LoaderPlugin(list):
|
|||
bool: Whether the container was deleted
|
||||
|
||||
"""
|
||||
|
||||
raise NotImplementedError("Loader.remove() must be "
|
||||
"implemented by subclass")
|
||||
|
||||
@classmethod
|
||||
def get_options(cls, contexts):
|
||||
"""
|
||||
Returns static (cls) options or could collect from 'contexts'.
|
||||
"""Returns static (cls) options or could collect from 'contexts'.
|
||||
|
||||
Args:
|
||||
contexts (list): of repre or product contexts
|
||||
|
|
@ -347,10 +381,8 @@ def discover_loader_plugins(project_name=None):
|
|||
plugin.apply_settings(project_settings)
|
||||
except Exception:
|
||||
log.warning(
|
||||
"Failed to apply settings to loader {}".format(
|
||||
plugin.__name__
|
||||
),
|
||||
exc_info=True,
|
||||
f"Failed to apply settings to loader {plugin.__name__}",
|
||||
exc_info=True
|
||||
)
|
||||
compatible_hooks = []
|
||||
for hook_cls in sorted_hooks:
|
||||
|
|
|
|||
|
|
@ -829,35 +829,47 @@ HintedLineEditButton {
|
|||
}
|
||||
|
||||
/* Launcher specific stylesheets */
|
||||
ActionsView[mode="icon"] {
|
||||
ActionsView {
|
||||
/* font size can't be set on items */
|
||||
font-size: 9pt;
|
||||
font-size: 8pt;
|
||||
border: 0px;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
ActionsView[mode="icon"]::item {
|
||||
ActionsView::item {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 4px;
|
||||
border: 0px;
|
||||
border-radius: 0.3em;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
ActionsView[mode="icon"]::item:hover {
|
||||
ActionsView::item:hover {
|
||||
color: {color:font-hover};
|
||||
background: #424A57;
|
||||
}
|
||||
|
||||
ActionsView[mode="icon"]::icon {}
|
||||
ActionsView::icon {}
|
||||
|
||||
ActionMenuPopup #GroupLabel {
|
||||
padding: 5px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
ActionMenuPopup #ShadowFrame {
|
||||
border-radius: 5px;
|
||||
background: rgba(12, 13, 24, 0.5);
|
||||
}
|
||||
|
||||
ActionMenuPopup #Wrapper {
|
||||
border-radius: 0.3em;
|
||||
border-radius: 5px;
|
||||
background: #353B46;
|
||||
}
|
||||
ActionMenuPopup ActionsView[mode="icon"] {
|
||||
|
||||
ActionMenuPopup ActionsView {
|
||||
background: transparent;
|
||||
border: none;
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
#IconView[mode="icon"] {
|
||||
|
|
@ -893,6 +905,70 @@ ActionMenuPopup ActionsView[mode="icon"] {
|
|||
border-radius: 0.1em;
|
||||
}
|
||||
|
||||
/* Launcher specific stylesheets */
|
||||
FiltersBar {
|
||||
background: {color:bg-inputs};
|
||||
border: 1px solid {color:border};
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
FiltersBar #ScrollArea {
|
||||
background: {color:bg-inputs};
|
||||
}
|
||||
FiltersBar #SearchButton {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
FiltersBar #BackButton {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
FiltersBar #BackButton:hover {
|
||||
background: {color:bg-buttons-hover};
|
||||
}
|
||||
|
||||
FiltersBar #ConfirmButton {
|
||||
background: #91CDFB;
|
||||
color: #03344D;
|
||||
}
|
||||
|
||||
FiltersPopup #PopupWrapper, FilterValuePopup #PopupWrapper {
|
||||
border-radius: 5px;
|
||||
background: {color:bg-inputs};
|
||||
}
|
||||
|
||||
FiltersPopup #ShadowFrame, FilterValuePopup #ShadowFrame {
|
||||
border-radius: 5px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
FilterItemButton, FilterValueItemButton {
|
||||
border-radius: 5px;
|
||||
background: transparent;
|
||||
}
|
||||
FilterItemButton:hover, FilterValueItemButton:hover {
|
||||
background: {color:bg-buttons-hover};
|
||||
}
|
||||
FilterValueItemButton[selected="1"] {
|
||||
background: {color:bg-view-selection};
|
||||
}
|
||||
FilterValueItemButton[selected="1"]:hover {
|
||||
background: {color:bg-view-selection-hover};
|
||||
}
|
||||
FilterValueItemsView #ContentWidget {
|
||||
background: {color:bg-inputs};
|
||||
}
|
||||
SearchItemDisplayWidget {
|
||||
border-radius: 5px;
|
||||
}
|
||||
SearchItemDisplayWidget:hover {
|
||||
background: {color:bg-buttons};
|
||||
}
|
||||
SearchItemDisplayWidget #ValueWidget {
|
||||
border-radius: 3px;
|
||||
background: {color:bg-buttons};
|
||||
}
|
||||
|
||||
/* Subset Manager */
|
||||
#SubsetManagerDetailsText {}
|
||||
#SubsetManagerDetailsText[state="invalid"] {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
from .cache import CacheItem, NestedCacheItem
|
||||
from .projects import (
|
||||
TagItem,
|
||||
StatusItem,
|
||||
StatusStates,
|
||||
ProjectItem,
|
||||
|
|
@ -25,6 +26,7 @@ __all__ = (
|
|||
"CacheItem",
|
||||
"NestedCacheItem",
|
||||
|
||||
"TagItem",
|
||||
"StatusItem",
|
||||
"StatusStates",
|
||||
"ProjectItem",
|
||||
|
|
|
|||
|
|
@ -100,12 +100,14 @@ class TaskItem:
|
|||
label: Union[str, None],
|
||||
task_type: str,
|
||||
parent_id: str,
|
||||
tags: list[str],
|
||||
):
|
||||
self.task_id = task_id
|
||||
self.name = name
|
||||
self.label = label
|
||||
self.task_type = task_type
|
||||
self.parent_id = parent_id
|
||||
self.tags = tags
|
||||
|
||||
self._full_label = None
|
||||
|
||||
|
|
@ -145,6 +147,7 @@ class TaskItem:
|
|||
"label": self.label,
|
||||
"parent_id": self.parent_id,
|
||||
"task_type": self.task_type,
|
||||
"tags": self.tags,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
|
@ -176,7 +179,8 @@ def _get_task_items_from_tasks(tasks):
|
|||
task["name"],
|
||||
task["label"],
|
||||
task["type"],
|
||||
folder_id
|
||||
folder_id,
|
||||
task["tags"],
|
||||
))
|
||||
return output
|
||||
|
||||
|
|
@ -217,6 +221,8 @@ class HierarchyModel(object):
|
|||
lifetime = 60 # A minute
|
||||
|
||||
def __init__(self, controller):
|
||||
self._tags_by_entity_type = NestedCacheItem(
|
||||
levels=1, default_factory=dict, lifetime=self.lifetime)
|
||||
self._folders_items = NestedCacheItem(
|
||||
levels=1, default_factory=dict, lifetime=self.lifetime)
|
||||
self._folders_by_id = NestedCacheItem(
|
||||
|
|
@ -235,6 +241,7 @@ class HierarchyModel(object):
|
|||
self._controller = controller
|
||||
|
||||
def reset(self):
|
||||
self._tags_by_entity_type.reset()
|
||||
self._folders_items.reset()
|
||||
self._folders_by_id.reset()
|
||||
|
||||
|
|
@ -514,6 +521,31 @@ class HierarchyModel(object):
|
|||
|
||||
return output
|
||||
|
||||
def get_available_tags_by_entity_type(
|
||||
self, project_name: str
|
||||
) -> dict[str, list[str]]:
|
||||
"""Get available tags for all entity types in a project."""
|
||||
cache = self._tags_by_entity_type.get(project_name)
|
||||
if not cache.is_valid:
|
||||
tags = None
|
||||
if project_name:
|
||||
response = ayon_api.get(f"projects/{project_name}/tags")
|
||||
if response.status_code == 200:
|
||||
tags = response.data
|
||||
|
||||
# Fake empty tags
|
||||
if tags is None:
|
||||
tags = {
|
||||
"folders": [],
|
||||
"tasks": [],
|
||||
"products": [],
|
||||
"versions": [],
|
||||
"representations": [],
|
||||
"workfiles": []
|
||||
}
|
||||
cache.update_data(tags)
|
||||
return cache.get_data()
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _folder_refresh_event_manager(self, project_name, sender):
|
||||
self._folders_refreshing.add(project_name)
|
||||
|
|
@ -617,6 +649,6 @@ class HierarchyModel(object):
|
|||
tasks = list(ayon_api.get_tasks(
|
||||
project_name,
|
||||
folder_ids=[folder_id],
|
||||
fields={"id", "name", "label", "folderId", "type"}
|
||||
fields={"id", "name", "label", "folderId", "type", "tags"}
|
||||
))
|
||||
return _get_task_items_from_tasks(tasks)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Any
|
||||
|
|
@ -74,6 +75,13 @@ class StatusItem:
|
|||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TagItem:
|
||||
"""Tag definition set on project anatomy."""
|
||||
name: str
|
||||
color: str
|
||||
|
||||
|
||||
class FolderTypeItem:
|
||||
"""Item representing folder type of project.
|
||||
|
||||
|
|
@ -292,6 +300,22 @@ class ProjectsModel(object):
|
|||
project_cache.update_data(entity)
|
||||
return project_cache.get_data()
|
||||
|
||||
def get_project_anatomy_tags(self, project_name: str) -> list[TagItem]:
|
||||
"""Get project anatomy tags.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
|
||||
Returns:
|
||||
list[TagItem]: Tag definitions.
|
||||
|
||||
"""
|
||||
project_entity = self.get_project_entity(project_name)
|
||||
return [
|
||||
TagItem(tag["name"], tag["color"])
|
||||
for tag in project_entity["tags"]
|
||||
]
|
||||
|
||||
def get_project_status_items(self, project_name, sender):
|
||||
"""Get project status items.
|
||||
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@ from ayon_core.lib.attribute_definitions import (
|
|||
from ayon_core.tools.flickcharm import FlickCharm
|
||||
from ayon_core.tools.utils import (
|
||||
get_qt_icon,
|
||||
PixmapLabel,
|
||||
)
|
||||
from ayon_core.tools.attribute_defs import AttributeDefinitionsDialog
|
||||
from ayon_core.tools.launcher.abstract import WebactionContext
|
||||
|
||||
ANIMATION_LEN = 7
|
||||
SHADOW_FRAME_MARGINS = (1, 1, 1, 1)
|
||||
|
||||
ACTION_ID_ROLE = QtCore.Qt.UserRole + 1
|
||||
ACTION_TYPE_ROLE = QtCore.Qt.UserRole + 2
|
||||
|
|
@ -51,13 +51,9 @@ def _variant_label_sort_getter(action_item):
|
|||
|
||||
|
||||
# --- Replacement for QAction for action variants ---
|
||||
class LauncherSettingsLabel(PixmapLabel):
|
||||
class LauncherSettingsLabel(QtWidgets.QWidget):
|
||||
_settings_icon = None
|
||||
|
||||
def __init__(self, parent):
|
||||
icon = self._get_settings_icon()
|
||||
super().__init__(icon.pixmap(64, 64), parent)
|
||||
|
||||
@classmethod
|
||||
def _get_settings_icon(cls):
|
||||
if cls._settings_icon is None:
|
||||
|
|
@ -67,24 +63,52 @@ class LauncherSettingsLabel(PixmapLabel):
|
|||
})
|
||||
return cls._settings_icon
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QtGui.QPainter(self)
|
||||
|
||||
painter.setRenderHints(
|
||||
QtGui.QPainter.Antialiasing
|
||||
| QtGui.QPainter.SmoothPixmapTransform
|
||||
)
|
||||
|
||||
rect = event.rect()
|
||||
size = min(rect.height(), rect.width())
|
||||
pix_rect = QtCore.QRect(
|
||||
rect.x(), rect.y(),
|
||||
size, size
|
||||
)
|
||||
pix = self._get_settings_icon().pixmap(size, size)
|
||||
painter.drawPixmap(pix_rect, pix)
|
||||
|
||||
painter.end()
|
||||
|
||||
|
||||
class ActionOverlayWidget(QtWidgets.QFrame):
|
||||
config_requested = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, item_id, parent):
|
||||
super().__init__(parent)
|
||||
self._item_id = item_id
|
||||
|
||||
settings_icon = LauncherSettingsLabel(self)
|
||||
settings_icon.setToolTip("Right click for options")
|
||||
settings_icon.setVisible(False)
|
||||
|
||||
main_layout = QtWidgets.QGridLayout(self)
|
||||
main_layout.setContentsMargins(5, 5, 0, 0)
|
||||
main_layout.addWidget(settings_icon, 0, 0)
|
||||
main_layout.setColumnStretch(1, 1)
|
||||
main_layout.setRowStretch(1, 1)
|
||||
main_layout.setColumnStretch(0, 1)
|
||||
main_layout.setColumnStretch(1, 5)
|
||||
|
||||
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True)
|
||||
|
||||
self._settings_icon = settings_icon
|
||||
|
||||
def enterEvent(self, event):
|
||||
super().enterEvent(event)
|
||||
self._settings_icon.setVisible(True)
|
||||
|
||||
def leaveEvent(self, event):
|
||||
super().leaveEvent(event)
|
||||
self._settings_icon.setVisible(False)
|
||||
|
||||
|
||||
class ActionsQtModel(QtGui.QStandardItemModel):
|
||||
|
|
@ -138,6 +162,12 @@ class ActionsQtModel(QtGui.QStandardItemModel):
|
|||
def get_item_by_id(self, action_id):
|
||||
return self._items_by_id.get(action_id)
|
||||
|
||||
def get_index_by_id(self, action_id):
|
||||
item = self.get_item_by_id(action_id)
|
||||
if item is not None:
|
||||
return self.indexFromItem(item)
|
||||
return QtCore.QModelIndex()
|
||||
|
||||
def get_group_item_by_action_id(self, action_id):
|
||||
item = self._items_by_id.get(action_id)
|
||||
if item is not None:
|
||||
|
|
@ -222,7 +252,7 @@ class ActionsQtModel(QtGui.QStandardItemModel):
|
|||
|
||||
item.setFlags(QtCore.Qt.ItemIsEnabled)
|
||||
item.setData(label, QtCore.Qt.DisplayRole)
|
||||
# item.setData(label, QtCore.Qt.ToolTipRole)
|
||||
item.setData(label, QtCore.Qt.ToolTipRole)
|
||||
item.setData(icon, QtCore.Qt.DecorationRole)
|
||||
item.setData(is_group, ACTION_IS_GROUP_ROLE)
|
||||
item.setData(has_configs, ACTION_HAS_CONFIGS_ROLE)
|
||||
|
|
@ -295,8 +325,8 @@ class ActionMenuPopupModel(QtGui.QStandardItemModel):
|
|||
|
||||
item = QtGui.QStandardItem()
|
||||
item.setFlags(QtCore.Qt.ItemIsEnabled)
|
||||
# item.setData(action_item.full_label, QtCore.Qt.ToolTipRole)
|
||||
item.setData(action_item.full_label, QtCore.Qt.DisplayRole)
|
||||
item.setData(action_item.variant_label, QtCore.Qt.DisplayRole)
|
||||
item.setData(action_item.full_label, QtCore.Qt.ToolTipRole)
|
||||
item.setData(icon, QtCore.Qt.DecorationRole)
|
||||
item.setData(action_item.identifier, ACTION_ID_ROLE)
|
||||
item.setData(
|
||||
|
|
@ -344,8 +374,24 @@ class ActionMenuPopupModel(QtGui.QStandardItemModel):
|
|||
|
||||
|
||||
class ActionMenuPopup(QtWidgets.QWidget):
|
||||
"""Popup widget for group varaints.
|
||||
|
||||
The popup is handling most of the layout and showing of the items
|
||||
manually.
|
||||
|
||||
There 4 parts:
|
||||
1. Shadow - semi transparent black widget used as shadow.
|
||||
2. Background - painted over the shadow with blur effect. All
|
||||
other items are painted over.
|
||||
3. Label - show group label and positioned manually at the top
|
||||
of the popup.
|
||||
4. View - View with variant action items. View is positioned
|
||||
and resized manually according to the items in the group and then
|
||||
animated using mask region.
|
||||
|
||||
"""
|
||||
action_triggered = QtCore.Signal(str)
|
||||
config_requested = QtCore.Signal(str)
|
||||
config_requested = QtCore.Signal(str, QtCore.QPoint)
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
|
|
@ -363,16 +409,34 @@ class ActionMenuPopup(QtWidgets.QWidget):
|
|||
expand_anim.setDuration(60)
|
||||
expand_anim.setEasingCurve(QtCore.QEasingCurve.InOutQuad)
|
||||
|
||||
sh_l, sh_t, sh_r, sh_b = SHADOW_FRAME_MARGINS
|
||||
|
||||
group_label = QtWidgets.QLabel("|", self)
|
||||
group_label.setObjectName("GroupLabel")
|
||||
|
||||
# View with actions
|
||||
view = ActionsView(self)
|
||||
view.setGridSize(QtCore.QSize(75, 80))
|
||||
view.setIconSize(QtCore.QSize(32, 32))
|
||||
view.move(QtCore.QPoint(3, 3))
|
||||
view.move(sh_l, sh_t)
|
||||
|
||||
view.stackUnder(group_label)
|
||||
|
||||
# Background draw
|
||||
bg_frame = QtWidgets.QFrame(self)
|
||||
bg_frame.setObjectName("ShadowFrame")
|
||||
bg_frame.stackUnder(view)
|
||||
|
||||
wrapper = QtWidgets.QFrame(self)
|
||||
wrapper.setObjectName("Wrapper")
|
||||
wrapper.stackUnder(view)
|
||||
|
||||
effect = QtWidgets.QGraphicsBlurEffect(wrapper)
|
||||
effect.setBlurRadius(3.0)
|
||||
wrapper.setGraphicsEffect(effect)
|
||||
|
||||
bg_layout = QtWidgets.QVBoxLayout(bg_frame)
|
||||
bg_layout.setContentsMargins(sh_l, sh_t, sh_r, sh_b)
|
||||
bg_layout.addWidget(wrapper)
|
||||
|
||||
model = ActionMenuPopupModel()
|
||||
proxy_model = ActionsProxyModel()
|
||||
|
|
@ -387,10 +451,12 @@ class ActionMenuPopup(QtWidgets.QWidget):
|
|||
expand_anim.finished.connect(self._on_expand_finish)
|
||||
|
||||
view.clicked.connect(self._on_clicked)
|
||||
view.config_requested.connect(self.config_requested)
|
||||
view.config_requested.connect(self._on_configs_trigger)
|
||||
|
||||
self._group_label = group_label
|
||||
self._view = view
|
||||
self._wrapper = wrapper
|
||||
self._bg_frame = bg_frame
|
||||
self._effect = effect
|
||||
self._model = model
|
||||
self._proxy_model = proxy_model
|
||||
|
||||
|
|
@ -417,7 +483,8 @@ class ActionMenuPopup(QtWidgets.QWidget):
|
|||
super().leaveEvent(event)
|
||||
self._close_timer.start()
|
||||
|
||||
def show_items(self, action_id, action_items, pos):
|
||||
def show_items(self, group_label, action_id, action_items, pos):
|
||||
self._group_label.setText(group_label)
|
||||
if not action_items:
|
||||
if self._showed:
|
||||
self._close_timer.start()
|
||||
|
|
@ -426,70 +493,82 @@ class ActionMenuPopup(QtWidgets.QWidget):
|
|||
|
||||
self._close_timer.stop()
|
||||
|
||||
update_position = False
|
||||
if action_id != self._current_id:
|
||||
update_position = True
|
||||
self.setGeometry(pos.x(), pos.y(), 1, 1)
|
||||
self._current_id = action_id
|
||||
self._update_items(action_items)
|
||||
|
||||
# Make sure is visible
|
||||
if not self._showed:
|
||||
update_position = True
|
||||
self.show()
|
||||
|
||||
if not update_position:
|
||||
self.raise_()
|
||||
return
|
||||
|
||||
# Set geometry to position
|
||||
# - first make sure widget changes from '_update_items'
|
||||
# are recalculated
|
||||
app = QtWidgets.QApplication.instance()
|
||||
app.processEvents()
|
||||
items_count, size, target_size = self._get_size_hint()
|
||||
items_count, start_size, target_size = self._get_size_hint()
|
||||
self._model.fill_to_count(items_count)
|
||||
|
||||
label_sh = self._group_label.sizeHint()
|
||||
label_width, label_height = label_sh.width(), label_sh.height()
|
||||
window = self.screen()
|
||||
window_geo = window.geometry()
|
||||
_target_x = pos.x() + target_size.width()
|
||||
_target_y = pos.y() + target_size.height() + label_height
|
||||
right_to_left = (
|
||||
pos.x() + target_size.width() > window_geo.right()
|
||||
or pos.y() + target_size.height() > window_geo.bottom()
|
||||
_target_x > window_geo.right()
|
||||
or _target_y > window_geo.bottom()
|
||||
)
|
||||
|
||||
pos_x = pos.x() - 5
|
||||
pos_y = pos.y() - 4
|
||||
|
||||
wrap_x = wrap_y = 0
|
||||
sh_l, sh_t, sh_r, sh_b = SHADOW_FRAME_MARGINS
|
||||
viewport_offset = self._view.viewport().geometry().topLeft()
|
||||
pos_x = pos.x() - (sh_l + viewport_offset.x() + 2)
|
||||
pos_y = pos.y() - (sh_t + viewport_offset.y() + 1)
|
||||
bg_x = bg_y = 0
|
||||
sort_order = QtCore.Qt.DescendingOrder
|
||||
if right_to_left:
|
||||
sort_order = QtCore.Qt.AscendingOrder
|
||||
size_diff = target_size - size
|
||||
size_diff = target_size - start_size
|
||||
pos_x -= size_diff.width()
|
||||
pos_y -= size_diff.height()
|
||||
wrap_x = size_diff.width()
|
||||
wrap_y = size_diff.height()
|
||||
bg_x = size_diff.width()
|
||||
bg_y = size_diff.height() - label_height
|
||||
|
||||
wrap_geo = QtCore.QRect(
|
||||
wrap_x, wrap_y, size.width(), size.height()
|
||||
bg_geo = QtCore.QRect(
|
||||
bg_x, bg_y,
|
||||
start_size.width(), start_size.height() + label_height
|
||||
)
|
||||
|
||||
label_pos_x = sh_l
|
||||
label_pos_y = bg_y + sh_t
|
||||
if label_width < start_size.width():
|
||||
label_pos_x = bg_x + (start_size.width() - label_width) // 2
|
||||
|
||||
if self._expand_anim.state() == QtCore.QAbstractAnimation.Running:
|
||||
self._expand_anim.stop()
|
||||
self._first_anim_frame = True
|
||||
|
||||
self._right_to_left = right_to_left
|
||||
|
||||
self._proxy_model.sort(0, sort_order)
|
||||
self.setUpdatesEnabled(False)
|
||||
self._view.setMask(wrap_geo)
|
||||
self._view.setMask(
|
||||
bg_geo.adjusted(sh_l, sh_t, -sh_r, -sh_b)
|
||||
)
|
||||
self._view.setMinimumWidth(target_size.width())
|
||||
self._view.setMaximumWidth(target_size.width())
|
||||
self._wrapper.setGeometry(wrap_geo)
|
||||
self._view.setMinimumHeight(target_size.height())
|
||||
self._view.move(sh_l, sh_t + label_height)
|
||||
self.setGeometry(
|
||||
pos_x, pos_y,
|
||||
target_size.width(), target_size.height()
|
||||
pos_x, pos_y - label_height,
|
||||
target_size.width(), target_size.height() + label_height
|
||||
)
|
||||
self._bg_frame.setGeometry(bg_geo)
|
||||
self._group_label.move(label_pos_x, label_pos_y)
|
||||
self.setUpdatesEnabled(True)
|
||||
|
||||
self._expand_anim.updateCurrentTime(0)
|
||||
self._expand_anim.setStartValue(size)
|
||||
self._expand_anim.setStartValue(start_size)
|
||||
self._expand_anim.setEndValue(target_size)
|
||||
self._expand_anim.start()
|
||||
|
||||
|
|
@ -511,20 +590,37 @@ class ActionMenuPopup(QtWidgets.QWidget):
|
|||
self._expand_anim.stop()
|
||||
return
|
||||
|
||||
wrapper_geo = self._wrapper.geometry()
|
||||
wrapper_geo.setWidth(value.width())
|
||||
wrapper_geo.setHeight(value.height())
|
||||
bg_geo = self._bg_frame.geometry()
|
||||
|
||||
label_sh = self._group_label.sizeHint()
|
||||
label_width, label_height = label_sh.width(), label_sh.height()
|
||||
if self._right_to_left:
|
||||
geo = self.geometry()
|
||||
popup_geo = self.geometry()
|
||||
diff_size = popup_geo.size() - value
|
||||
pos = QtCore.QPoint(
|
||||
geo.width() - value.width(),
|
||||
geo.height() - value.height(),
|
||||
diff_size.width(), diff_size.height() - label_height
|
||||
)
|
||||
wrapper_geo.setTopLeft(pos)
|
||||
|
||||
self._view.setMask(wrapper_geo)
|
||||
self._wrapper.setGeometry(wrapper_geo)
|
||||
bg_geo.moveTopLeft(pos)
|
||||
|
||||
bg_geo.setWidth(value.width())
|
||||
bg_geo.setHeight(value.height() + label_height)
|
||||
|
||||
label_width = self._group_label.sizeHint().width()
|
||||
bgeo_tl = bg_geo.topLeft()
|
||||
sh_l, sh_t, sh_r, sh_b = SHADOW_FRAME_MARGINS
|
||||
|
||||
label_pos_x = sh_l
|
||||
if label_width < value.width():
|
||||
label_pos_x = bgeo_tl.x() + (value.width() - label_width) // 2
|
||||
|
||||
self.setUpdatesEnabled(False)
|
||||
self._view.setMask(
|
||||
bg_geo.adjusted(sh_l, sh_t, -sh_r, -sh_b)
|
||||
)
|
||||
self._group_label.move(label_pos_x, sh_t)
|
||||
self._bg_frame.setGeometry(bg_geo)
|
||||
self.setUpdatesEnabled(True)
|
||||
|
||||
def _on_expand_finish(self):
|
||||
# Make sure that size is recalculated if src and targe size is same
|
||||
|
|
@ -547,16 +643,25 @@ class ActionMenuPopup(QtWidgets.QWidget):
|
|||
if rows == 1:
|
||||
cols = row_count
|
||||
|
||||
m_l, m_t, m_r, m_b = (3, 3, 1, 1)
|
||||
# QUESTION how to get the margins from Qt?
|
||||
border = 2 * 1
|
||||
viewport_geo = self._view.viewport().geometry()
|
||||
viewport_offset = viewport_geo.topLeft()
|
||||
# QUESTION how to get the bottom and right margins from Qt?
|
||||
vp_lr = viewport_offset.x()
|
||||
vp_tb = viewport_offset.y()
|
||||
m_l, m_t, m_r, m_b = (
|
||||
s_m + vp_m
|
||||
for s_m, vp_m in zip(
|
||||
SHADOW_FRAME_MARGINS,
|
||||
(vp_lr, vp_tb, vp_lr, vp_tb)
|
||||
)
|
||||
)
|
||||
single_width = (
|
||||
grid_size.width()
|
||||
+ self._view.horizontalOffset() + border + m_l + m_r + 1
|
||||
+ self._view.horizontalOffset() + m_l + m_r + 1
|
||||
)
|
||||
single_height = (
|
||||
grid_size.height()
|
||||
+ self._view.verticalOffset() + border + m_b + m_t + 1
|
||||
+ self._view.verticalOffset() + m_b + m_t + 1
|
||||
)
|
||||
total_width = single_width
|
||||
total_height = single_height
|
||||
|
|
@ -586,14 +691,13 @@ class ActionMenuPopup(QtWidgets.QWidget):
|
|||
self.action_triggered.emit(action_id)
|
||||
self.close()
|
||||
|
||||
def _on_configs_trigger(self, action_id):
|
||||
self.config_requested.emit(action_id)
|
||||
def _on_configs_trigger(self, action_id, center_pos):
|
||||
self.config_requested.emit(action_id, center_pos)
|
||||
self.close()
|
||||
|
||||
|
||||
class ActionDelegate(QtWidgets.QStyledItemDelegate):
|
||||
_cached_extender = {}
|
||||
_cached_extender_base_pix = None
|
||||
_extender_icon = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
|
@ -653,31 +757,18 @@ class ActionDelegate(QtWidgets.QStyledItemDelegate):
|
|||
painter.restore()
|
||||
|
||||
@classmethod
|
||||
def _get_extender_pixmap(cls, size):
|
||||
pix = cls._cached_extender.get(size)
|
||||
if pix is not None:
|
||||
return pix
|
||||
|
||||
base_pix = cls._cached_extender_base_pix
|
||||
if base_pix is None:
|
||||
icon = get_qt_icon({
|
||||
def _get_extender_pixmap(cls):
|
||||
if cls._extender_icon is None:
|
||||
cls._extender_icon = get_qt_icon({
|
||||
"type": "material-symbols",
|
||||
"name": "more_horiz",
|
||||
})
|
||||
base_pix = icon.pixmap(64, 64)
|
||||
cls._cached_extender_base_pix = base_pix
|
||||
|
||||
pix = base_pix.scaled(
|
||||
size, size,
|
||||
QtCore.Qt.KeepAspectRatio,
|
||||
QtCore.Qt.SmoothTransformation
|
||||
)
|
||||
cls._cached_extender[size] = pix
|
||||
return pix
|
||||
return cls._extender_icon
|
||||
|
||||
def paint(self, painter, option, index):
|
||||
painter.setRenderHints(
|
||||
QtGui.QPainter.Antialiasing
|
||||
| QtGui.QPainter.TextAntialiasing
|
||||
| QtGui.QPainter.SmoothPixmapTransform
|
||||
)
|
||||
|
||||
|
|
@ -690,20 +781,15 @@ class ActionDelegate(QtWidgets.QStyledItemDelegate):
|
|||
return
|
||||
|
||||
grid_size = option.widget.gridSize()
|
||||
x_offset = int(
|
||||
(grid_size.width() / 2)
|
||||
- (option.rect.width() / 2)
|
||||
)
|
||||
item_x = option.rect.x() - x_offset
|
||||
|
||||
tenth_size = int(grid_size.width() / 10)
|
||||
extender_size = int(tenth_size * 2.4)
|
||||
extender_rect = option.rect.adjusted(5, 5, 0, 0)
|
||||
extender_size = grid_size.width() // 6
|
||||
extender_rect.setWidth(extender_size)
|
||||
extender_rect.setHeight(extender_size)
|
||||
|
||||
extender_x = item_x + tenth_size
|
||||
extender_y = option.rect.y() + tenth_size
|
||||
|
||||
pix = self._get_extender_pixmap(extender_size)
|
||||
painter.drawPixmap(extender_x, extender_y, pix)
|
||||
icon = self._get_extender_pixmap()
|
||||
pix = icon.pixmap(extender_size, extender_size)
|
||||
painter.drawPixmap(extender_rect, pix)
|
||||
|
||||
|
||||
class ActionsProxyModel(QtCore.QSortFilterProxyModel):
|
||||
|
|
@ -739,12 +825,10 @@ class ActionsProxyModel(QtCore.QSortFilterProxyModel):
|
|||
|
||||
|
||||
class ActionsView(QtWidgets.QListView):
|
||||
action_triggered = QtCore.Signal(str)
|
||||
config_requested = QtCore.Signal(str)
|
||||
config_requested = QtCore.Signal(str, QtCore.QPoint)
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
self.setProperty("mode", "icon")
|
||||
self.setViewMode(QtWidgets.QListView.IconMode)
|
||||
self.setResizeMode(QtWidgets.QListView.Adjust)
|
||||
self.setSelectionMode(QtWidgets.QListView.NoSelection)
|
||||
|
|
@ -773,18 +857,6 @@ class ActionsView(QtWidgets.QListView):
|
|||
self._overlay_widgets = []
|
||||
self._flick = flick
|
||||
self._delegate = delegate
|
||||
self._popup_widget = None
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
"""Handle mouse move event."""
|
||||
super().mouseMoveEvent(event)
|
||||
# Update hover state for the item under mouse
|
||||
index = self.indexAt(event.pos())
|
||||
if index.isValid() and index.data(ACTION_IS_GROUP_ROLE):
|
||||
self._show_group_popup(index)
|
||||
|
||||
elif self._popup_widget is not None:
|
||||
self._popup_widget.close()
|
||||
|
||||
def _on_context_menu(self, point):
|
||||
"""Creates menu to force skip opening last workfile."""
|
||||
|
|
@ -792,34 +864,9 @@ class ActionsView(QtWidgets.QListView):
|
|||
if not index.isValid():
|
||||
return
|
||||
action_id = index.data(ACTION_ID_ROLE)
|
||||
self.config_requested.emit(action_id)
|
||||
|
||||
def _get_popup_widget(self):
|
||||
if self._popup_widget is None:
|
||||
popup_widget = ActionMenuPopup(self)
|
||||
|
||||
popup_widget.action_triggered.connect(self.action_triggered)
|
||||
popup_widget.config_requested.connect(self.config_requested)
|
||||
self._popup_widget = popup_widget
|
||||
return self._popup_widget
|
||||
|
||||
def _show_group_popup(self, index):
|
||||
action_id = index.data(ACTION_ID_ROLE)
|
||||
model = self.model()
|
||||
while hasattr(model, "sourceModel"):
|
||||
model = model.sourceModel()
|
||||
|
||||
if not hasattr(model, "get_group_items"):
|
||||
return
|
||||
|
||||
action_items = model.get_group_items(action_id)
|
||||
rect = self.visualRect(index)
|
||||
pos = self.mapToGlobal(rect.topLeft())
|
||||
|
||||
popup_widget = self._get_popup_widget()
|
||||
popup_widget.show_items(
|
||||
action_id, action_items, pos
|
||||
)
|
||||
global_center = self.mapToGlobal(rect.center())
|
||||
self.config_requested.emit(action_id, global_center)
|
||||
|
||||
def update_on_refresh(self):
|
||||
viewport = self.viewport()
|
||||
|
|
@ -837,9 +884,6 @@ class ActionsView(QtWidgets.QListView):
|
|||
if has_configs:
|
||||
item_id = index.data(ACTION_ID_ROLE)
|
||||
widget = ActionOverlayWidget(item_id, viewport)
|
||||
widget.config_requested.connect(
|
||||
self.config_requested
|
||||
)
|
||||
overlay_widgets.append(widget)
|
||||
self.setIndexWidget(index, widget)
|
||||
|
||||
|
|
@ -877,8 +921,7 @@ class ActionsWidget(QtWidgets.QWidget):
|
|||
animation_timer.timeout.connect(self._on_animation)
|
||||
|
||||
view.clicked.connect(self._on_clicked)
|
||||
view.action_triggered.connect(self._trigger_action)
|
||||
view.config_requested.connect(self._on_config_request)
|
||||
view.config_requested.connect(self._show_config_dialog)
|
||||
model.refreshed.connect(self._on_model_refresh)
|
||||
|
||||
self._animated_items = set()
|
||||
|
|
@ -888,6 +931,8 @@ class ActionsWidget(QtWidgets.QWidget):
|
|||
self._model = model
|
||||
self._proxy_model = proxy_model
|
||||
|
||||
self._popup_widget = None
|
||||
|
||||
self._set_row_height(1)
|
||||
|
||||
def refresh(self):
|
||||
|
|
@ -974,11 +1019,33 @@ class ActionsWidget(QtWidgets.QWidget):
|
|||
return
|
||||
|
||||
is_group = index.data(ACTION_IS_GROUP_ROLE)
|
||||
if is_group:
|
||||
return
|
||||
action_id = index.data(ACTION_ID_ROLE)
|
||||
if is_group:
|
||||
self._show_group_popup(index)
|
||||
else:
|
||||
self._trigger_action(action_id, index)
|
||||
|
||||
def _get_popup_widget(self):
|
||||
if self._popup_widget is None:
|
||||
popup_widget = ActionMenuPopup(self)
|
||||
|
||||
popup_widget.action_triggered.connect(self._trigger_action)
|
||||
popup_widget.config_requested.connect(self._show_config_dialog)
|
||||
self._popup_widget = popup_widget
|
||||
return self._popup_widget
|
||||
|
||||
def _show_group_popup(self, index):
|
||||
action_id = index.data(ACTION_ID_ROLE)
|
||||
group_label = index.data(QtCore.Qt.DisplayRole)
|
||||
action_items = self._model.get_group_items(action_id)
|
||||
rect = self._view.visualRect(index)
|
||||
pos = self.mapToGlobal(rect.topLeft())
|
||||
|
||||
popup_widget = self._get_popup_widget()
|
||||
popup_widget.show_items(
|
||||
group_label, action_id, action_items, pos
|
||||
)
|
||||
|
||||
def _trigger_action(self, action_id, index=None):
|
||||
project_name = self._model.get_selected_project_name()
|
||||
folder_id = self._model.get_selected_folder_id()
|
||||
|
|
@ -1011,10 +1078,7 @@ class ActionsWidget(QtWidgets.QWidget):
|
|||
if index is not None:
|
||||
self._start_animation(index)
|
||||
|
||||
def _on_config_request(self, action_id):
|
||||
self._show_config_dialog(action_id)
|
||||
|
||||
def _show_config_dialog(self, action_id):
|
||||
def _show_config_dialog(self, action_id, center_point):
|
||||
action_item = self._model.get_action_item_by_id(action_id)
|
||||
config_fields = self._model.get_action_config_fields(action_id)
|
||||
if not config_fields:
|
||||
|
|
@ -1040,11 +1104,31 @@ class ActionsWidget(QtWidgets.QWidget):
|
|||
"Cancel",
|
||||
)
|
||||
dialog.set_values(values)
|
||||
dialog.show()
|
||||
self._center_dialog(dialog, center_point)
|
||||
result = dialog.exec_()
|
||||
if result == QtWidgets.QDialog.Accepted:
|
||||
new_values = dialog.get_values()
|
||||
self._controller.set_action_config_values(context, new_values)
|
||||
|
||||
@staticmethod
|
||||
def _center_dialog(dialog, target_center_pos):
|
||||
dialog_geo = dialog.geometry()
|
||||
dialog_geo.moveCenter(target_center_pos)
|
||||
|
||||
screen = dialog.screen()
|
||||
screen_geo = screen.availableGeometry()
|
||||
if screen_geo.left() > dialog_geo.left():
|
||||
dialog_geo.moveLeft(screen_geo.left())
|
||||
elif screen_geo.right() < dialog_geo.right():
|
||||
dialog_geo.moveRight(screen_geo.right())
|
||||
|
||||
if screen_geo.top() > dialog_geo.top():
|
||||
dialog_geo.moveTop(screen_geo.top())
|
||||
elif screen_geo.bottom() < dialog_geo.bottom():
|
||||
dialog_geo.moveBottom(screen_geo.bottom())
|
||||
dialog.move(dialog_geo.topLeft())
|
||||
|
||||
def _create_attrs_dialog(
|
||||
self,
|
||||
config_fields,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
"""Abstract base classes for loader tool."""
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List
|
||||
from typing import Iterable, Any, Optional
|
||||
|
||||
from ayon_core.lib.attribute_definitions import (
|
||||
AbstractAttrDef,
|
||||
serialize_attr_defs,
|
||||
deserialize_attr_defs,
|
||||
serialize_attr_defs,
|
||||
)
|
||||
from ayon_core.tools.common_models import TaskItem, TagItem
|
||||
|
||||
|
||||
class ProductTypeItem:
|
||||
|
|
@ -16,7 +20,7 @@ class ProductTypeItem:
|
|||
icon (dict[str, Any]): Product type icon definition.
|
||||
"""
|
||||
|
||||
def __init__(self, name, icon):
|
||||
def __init__(self, name: str, icon: dict[str, Any]):
|
||||
self.name = name
|
||||
self.icon = icon
|
||||
|
||||
|
|
@ -31,6 +35,41 @@ class ProductTypeItem:
|
|||
return cls(**data)
|
||||
|
||||
|
||||
class ProductBaseTypeItem:
|
||||
"""Item representing the product base type."""
|
||||
|
||||
def __init__(self, name: str, icon: dict[str, Any]):
|
||||
"""Initialize product base type item."""
|
||||
self.name = name
|
||||
self.icon = icon
|
||||
|
||||
def to_data(self) -> dict[str, Any]:
|
||||
"""Convert item to data dictionary.
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: Data representation of the item.
|
||||
|
||||
"""
|
||||
return {
|
||||
"name": self.name,
|
||||
"icon": self.icon,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_data(
|
||||
cls, data: dict[str, Any]) -> ProductBaseTypeItem:
|
||||
"""Create item from data dictionary.
|
||||
|
||||
Args:
|
||||
data (dict[str, Any]): Data to create item from.
|
||||
|
||||
Returns:
|
||||
ProductBaseTypeItem: Item created from the provided data.
|
||||
|
||||
"""
|
||||
return cls(**data)
|
||||
|
||||
|
||||
class ProductItem:
|
||||
"""Product item with it versions.
|
||||
|
||||
|
|
@ -49,35 +88,41 @@ class ProductItem:
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
product_id,
|
||||
product_type,
|
||||
product_name,
|
||||
product_icon,
|
||||
product_type_icon,
|
||||
product_in_scene,
|
||||
group_name,
|
||||
folder_id,
|
||||
folder_label,
|
||||
version_items,
|
||||
product_id: str,
|
||||
product_type: str,
|
||||
product_base_type: str,
|
||||
product_name: str,
|
||||
product_icon: dict[str, Any],
|
||||
product_type_icon: dict[str, Any],
|
||||
product_base_type_icon: dict[str, Any],
|
||||
group_name: str,
|
||||
folder_id: str,
|
||||
folder_label: str,
|
||||
version_items: dict[str, VersionItem],
|
||||
product_in_scene: bool,
|
||||
):
|
||||
self.product_id = product_id
|
||||
self.product_type = product_type
|
||||
self.product_base_type = product_base_type
|
||||
self.product_name = product_name
|
||||
self.product_icon = product_icon
|
||||
self.product_type_icon = product_type_icon
|
||||
self.product_base_type_icon = product_base_type_icon
|
||||
self.product_in_scene = product_in_scene
|
||||
self.group_name = group_name
|
||||
self.folder_id = folder_id
|
||||
self.folder_label = folder_label
|
||||
self.version_items = version_items
|
||||
|
||||
def to_data(self):
|
||||
def to_data(self) -> dict[str, Any]:
|
||||
return {
|
||||
"product_id": self.product_id,
|
||||
"product_type": self.product_type,
|
||||
"product_base_type": self.product_base_type,
|
||||
"product_name": self.product_name,
|
||||
"product_icon": self.product_icon,
|
||||
"product_type_icon": self.product_type_icon,
|
||||
"product_base_type_icon": self.product_base_type_icon,
|
||||
"product_in_scene": self.product_in_scene,
|
||||
"group_name": self.group_name,
|
||||
"folder_id": self.folder_id,
|
||||
|
|
@ -113,6 +158,7 @@ class VersionItem:
|
|||
published_time (Union[str, None]): Published time in format
|
||||
'%Y%m%dT%H%M%SZ'.
|
||||
status (Union[str, None]): Status name.
|
||||
tags (Union[list[str], None]): Tags.
|
||||
author (Union[str, None]): Author.
|
||||
frame_range (Union[str, None]): Frame range.
|
||||
duration (Union[int, None]): Duration.
|
||||
|
|
@ -124,21 +170,22 @@ class VersionItem:
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
version_id,
|
||||
version,
|
||||
is_hero,
|
||||
product_id,
|
||||
task_id,
|
||||
thumbnail_id,
|
||||
published_time,
|
||||
author,
|
||||
status,
|
||||
frame_range,
|
||||
duration,
|
||||
handles,
|
||||
step,
|
||||
comment,
|
||||
source,
|
||||
version_id: str,
|
||||
version: int,
|
||||
is_hero: bool,
|
||||
product_id: str,
|
||||
task_id: Optional[str],
|
||||
thumbnail_id: Optional[str],
|
||||
published_time: Optional[str],
|
||||
tags: Optional[list[str]],
|
||||
author: Optional[str],
|
||||
status: Optional[str],
|
||||
frame_range: Optional[str],
|
||||
duration: Optional[int],
|
||||
handles: Optional[str],
|
||||
step: Optional[int],
|
||||
comment: Optional[str],
|
||||
source: Optional[str],
|
||||
):
|
||||
self.version_id = version_id
|
||||
self.product_id = product_id
|
||||
|
|
@ -148,6 +195,7 @@ class VersionItem:
|
|||
self.is_hero = is_hero
|
||||
self.published_time = published_time
|
||||
self.author = author
|
||||
self.tags = tags
|
||||
self.status = status
|
||||
self.frame_range = frame_range
|
||||
self.duration = duration
|
||||
|
|
@ -198,7 +246,7 @@ class VersionItem:
|
|||
def __le__(self, other):
|
||||
return self.__eq__(other) or self.__lt__(other)
|
||||
|
||||
def to_data(self):
|
||||
def to_data(self) -> dict[str, Any]:
|
||||
return {
|
||||
"version_id": self.version_id,
|
||||
"product_id": self.product_id,
|
||||
|
|
@ -208,6 +256,7 @@ class VersionItem:
|
|||
"is_hero": self.is_hero,
|
||||
"published_time": self.published_time,
|
||||
"author": self.author,
|
||||
"tags": self.tags,
|
||||
"status": self.status,
|
||||
"frame_range": self.frame_range,
|
||||
"duration": self.duration,
|
||||
|
|
@ -218,7 +267,7 @@ class VersionItem:
|
|||
}
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data):
|
||||
def from_data(cls, data: dict[str, Any]) -> VersionItem:
|
||||
return cls(**data)
|
||||
|
||||
|
||||
|
|
@ -354,8 +403,8 @@ class ProductTypesFilter:
|
|||
|
||||
Defines the filtering for product types.
|
||||
"""
|
||||
def __init__(self, product_types: List[str], is_allow_list: bool):
|
||||
self.product_types: List[str] = product_types
|
||||
def __init__(self, product_types: list[str], is_allow_list: bool):
|
||||
self.product_types: list[str] = product_types
|
||||
self.is_allow_list: bool = is_allow_list
|
||||
|
||||
|
||||
|
|
@ -517,8 +566,21 @@ class FrontendLoaderController(_BaseLoaderController):
|
|||
|
||||
Returns:
|
||||
list[ProjectItem]: List of project items.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_project_anatomy_tags(self, project_name: str) -> list[TagItem]:
|
||||
"""Tag items defined on project anatomy.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
|
||||
Returns:
|
||||
list[TagItem]: Tag definition items.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -542,7 +604,12 @@ class FrontendLoaderController(_BaseLoaderController):
|
|||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_task_items(self, project_name, folder_ids, sender=None):
|
||||
def get_task_items(
|
||||
self,
|
||||
project_name: str,
|
||||
folder_ids: Iterable[str],
|
||||
sender: Optional[str] = None,
|
||||
) -> list[TaskItem]:
|
||||
"""Task items for folder ids.
|
||||
|
||||
Args:
|
||||
|
|
@ -590,6 +657,21 @@ class FrontendLoaderController(_BaseLoaderController):
|
|||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_available_tags_by_entity_type(
|
||||
self, project_name: str
|
||||
) -> dict[str, list[str]]:
|
||||
"""Get available tags by entity type.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
|
||||
Returns:
|
||||
dict[str, list[str]]: Available tags by entity type.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_project_status_items(self, project_name, sender=None):
|
||||
"""Items for all projects available on server.
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from ayon_core.tools.common_models import (
|
|||
ProjectsModel,
|
||||
HierarchyModel,
|
||||
ThumbnailsModel,
|
||||
TagItem,
|
||||
)
|
||||
|
||||
from .abstract import (
|
||||
|
|
@ -223,6 +224,16 @@ class LoaderController(BackendLoaderController, FrontendLoaderController):
|
|||
output[folder_id] = label
|
||||
return output
|
||||
|
||||
def get_available_tags_by_entity_type(
|
||||
self, project_name: str
|
||||
) -> dict[str, list[str]]:
|
||||
return self._hierarchy_model.get_available_tags_by_entity_type(
|
||||
project_name
|
||||
)
|
||||
|
||||
def get_project_anatomy_tags(self, project_name: str) -> list[TagItem]:
|
||||
return self._projects_model.get_project_anatomy_tags(project_name)
|
||||
|
||||
def get_product_items(self, project_name, folder_ids, sender=None):
|
||||
return self._products_model.get_product_items(
|
||||
project_name, folder_ids, sender)
|
||||
|
|
|
|||
|
|
@ -1,24 +1,34 @@
|
|||
"""Products model for loader tools."""
|
||||
from __future__ import annotations
|
||||
import collections
|
||||
import contextlib
|
||||
from typing import TYPE_CHECKING, Iterable, Optional
|
||||
|
||||
import arrow
|
||||
import ayon_api
|
||||
from ayon_api.operations import OperationsSession
|
||||
|
||||
|
||||
from ayon_core.lib import NestedCacheItem
|
||||
from ayon_core.style import get_default_entity_icon_color
|
||||
from ayon_core.tools.loader.abstract import (
|
||||
ProductTypeItem,
|
||||
ProductBaseTypeItem,
|
||||
ProductItem,
|
||||
VersionItem,
|
||||
RepreItem,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ayon_api.typing import ProductBaseTypeDict, ProductDict, VersionDict
|
||||
|
||||
|
||||
PRODUCTS_MODEL_SENDER = "products.model"
|
||||
|
||||
|
||||
def version_item_from_entity(version):
|
||||
version_attribs = version["attrib"]
|
||||
tags = version["tags"]
|
||||
frame_start = version_attribs.get("frameStart")
|
||||
frame_end = version_attribs.get("frameEnd")
|
||||
handle_start = version_attribs.get("handleStart")
|
||||
|
|
@ -59,6 +69,7 @@ def version_item_from_entity(version):
|
|||
thumbnail_id=version["thumbnailId"],
|
||||
published_time=published_time,
|
||||
author=author,
|
||||
tags=tags,
|
||||
status=version["status"],
|
||||
frame_range=frame_range,
|
||||
duration=duration,
|
||||
|
|
@ -70,9 +81,10 @@ def version_item_from_entity(version):
|
|||
|
||||
|
||||
def product_item_from_entity(
|
||||
product_entity,
|
||||
product_entity: ProductDict,
|
||||
version_entities,
|
||||
product_type_items_by_name,
|
||||
product_type_items_by_name: dict[str, ProductTypeItem],
|
||||
product_base_type_items_by_name: dict[str, ProductBaseTypeItem],
|
||||
folder_label,
|
||||
product_in_scene,
|
||||
):
|
||||
|
|
@ -88,8 +100,20 @@ def product_item_from_entity(
|
|||
# Cache the item for future use
|
||||
product_type_items_by_name[product_type] = product_type_item
|
||||
|
||||
product_type_icon = product_type_item.icon
|
||||
product_base_type = product_entity.get("productBaseType")
|
||||
product_base_type_item = product_base_type_items_by_name.get(
|
||||
product_base_type)
|
||||
# Same as for product type item above. Not sure if this is still needed
|
||||
# though.
|
||||
if product_base_type_item is None:
|
||||
product_base_type_item = create_default_product_base_type_item(
|
||||
product_base_type)
|
||||
# Cache the item for future use
|
||||
product_base_type_items_by_name[product_base_type] = (
|
||||
product_base_type_item)
|
||||
|
||||
product_type_icon = product_type_item.icon
|
||||
product_base_type_icon = product_base_type_item.icon
|
||||
product_icon = {
|
||||
"type": "awesome-font",
|
||||
"name": "fa.file-o",
|
||||
|
|
@ -103,9 +127,11 @@ def product_item_from_entity(
|
|||
return ProductItem(
|
||||
product_id=product_entity["id"],
|
||||
product_type=product_type,
|
||||
product_base_type=product_base_type,
|
||||
product_name=product_entity["name"],
|
||||
product_icon=product_icon,
|
||||
product_type_icon=product_type_icon,
|
||||
product_base_type_icon=product_base_type_icon,
|
||||
product_in_scene=product_in_scene,
|
||||
group_name=group,
|
||||
folder_id=product_entity["folderId"],
|
||||
|
|
@ -114,7 +140,8 @@ def product_item_from_entity(
|
|||
)
|
||||
|
||||
|
||||
def product_type_item_from_data(product_type_data):
|
||||
def product_type_item_from_data(
|
||||
product_type_data: ProductDict) -> ProductTypeItem:
|
||||
# TODO implement icon implementation
|
||||
# icon = product_type_data["icon"]
|
||||
# color = product_type_data["color"]
|
||||
|
|
@ -127,7 +154,29 @@ def product_type_item_from_data(product_type_data):
|
|||
return ProductTypeItem(product_type_data["name"], icon)
|
||||
|
||||
|
||||
def create_default_product_type_item(product_type):
|
||||
def product_base_type_item_from_data(
|
||||
product_base_type_data: ProductBaseTypeDict
|
||||
) -> ProductBaseTypeItem:
|
||||
"""Create product base type item from data.
|
||||
|
||||
Args:
|
||||
product_base_type_data (ProductBaseTypeDict): Product base type data.
|
||||
|
||||
Returns:
|
||||
ProductBaseTypeDict: Product base type item.
|
||||
|
||||
"""
|
||||
icon = {
|
||||
"type": "awesome-font",
|
||||
"name": "fa.folder",
|
||||
"color": "#0091B2",
|
||||
}
|
||||
return ProductBaseTypeItem(
|
||||
name=product_base_type_data["name"],
|
||||
icon=icon)
|
||||
|
||||
|
||||
def create_default_product_type_item(product_type: str) -> ProductTypeItem:
|
||||
icon = {
|
||||
"type": "awesome-font",
|
||||
"name": "fa.folder",
|
||||
|
|
@ -136,10 +185,28 @@ def create_default_product_type_item(product_type):
|
|||
return ProductTypeItem(product_type, icon)
|
||||
|
||||
|
||||
def create_default_product_base_type_item(
|
||||
product_base_type: str) -> ProductBaseTypeItem:
|
||||
"""Create default product base type item.
|
||||
|
||||
Args:
|
||||
product_base_type (str): Product base type name.
|
||||
|
||||
Returns:
|
||||
ProductBaseTypeItem: Default product base type item.
|
||||
"""
|
||||
icon = {
|
||||
"type": "awesome-font",
|
||||
"name": "fa.folder",
|
||||
"color": "#0091B2",
|
||||
}
|
||||
return ProductBaseTypeItem(product_base_type, icon)
|
||||
|
||||
|
||||
class ProductsModel:
|
||||
"""Model for products, version and representation.
|
||||
|
||||
All of the entities are product based. This model prepares data for UI
|
||||
All the entities are product based. This model prepares data for UI
|
||||
and caches it for faster access.
|
||||
|
||||
Note:
|
||||
|
|
@ -161,6 +228,8 @@ class ProductsModel:
|
|||
# Cache helpers
|
||||
self._product_type_items_cache = NestedCacheItem(
|
||||
levels=1, default_factory=list, lifetime=self.lifetime)
|
||||
self._product_base_type_items_cache = NestedCacheItem(
|
||||
levels=1, default_factory=list, lifetime=self.lifetime)
|
||||
self._product_items_cache = NestedCacheItem(
|
||||
levels=2, default_factory=dict, lifetime=self.lifetime)
|
||||
self._repre_items_cache = NestedCacheItem(
|
||||
|
|
@ -199,6 +268,36 @@ class ProductsModel:
|
|||
])
|
||||
return cache.get_data()
|
||||
|
||||
def get_product_base_type_items(
|
||||
self,
|
||||
project_name: Optional[str]) -> list[ProductBaseTypeItem]:
|
||||
"""Product base type items for the project.
|
||||
|
||||
Args:
|
||||
project_name (optional, str): Project name.
|
||||
|
||||
Returns:
|
||||
list[ProductBaseTypeDict]: Product base type items.
|
||||
|
||||
"""
|
||||
if not project_name:
|
||||
return []
|
||||
|
||||
cache = self._product_base_type_items_cache[project_name]
|
||||
if not cache.is_valid:
|
||||
product_base_types = []
|
||||
# TODO add temp implementation here when it is actually
|
||||
# implemented and available on server.
|
||||
if hasattr(ayon_api, "get_project_product_base_types"):
|
||||
product_base_types = ayon_api.get_project_product_base_types(
|
||||
project_name
|
||||
)
|
||||
cache.update_data([
|
||||
product_base_type_item_from_data(product_base_type)
|
||||
for product_base_type in product_base_types
|
||||
])
|
||||
return cache.get_data()
|
||||
|
||||
def get_product_items(self, project_name, folder_ids, sender):
|
||||
"""Product items with versions for project and folder ids.
|
||||
|
||||
|
|
@ -449,11 +548,12 @@ class ProductsModel:
|
|||
|
||||
def _create_product_items(
|
||||
self,
|
||||
project_name,
|
||||
products,
|
||||
versions,
|
||||
project_name: str,
|
||||
products: Iterable[ProductDict],
|
||||
versions: Iterable[VersionDict],
|
||||
folder_items=None,
|
||||
product_type_items=None,
|
||||
product_base_type_items: Optional[Iterable[ProductBaseTypeItem]] = None
|
||||
):
|
||||
if folder_items is None:
|
||||
folder_items = self._controller.get_folder_items(project_name)
|
||||
|
|
@ -461,6 +561,11 @@ class ProductsModel:
|
|||
if product_type_items is None:
|
||||
product_type_items = self.get_product_type_items(project_name)
|
||||
|
||||
if product_base_type_items is None:
|
||||
product_base_type_items = self.get_product_base_type_items(
|
||||
project_name
|
||||
)
|
||||
|
||||
loaded_product_ids = self._controller.get_loaded_product_ids()
|
||||
|
||||
versions_by_product_id = collections.defaultdict(list)
|
||||
|
|
@ -470,7 +575,13 @@ class ProductsModel:
|
|||
product_type_item.name: product_type_item
|
||||
for product_type_item in product_type_items
|
||||
}
|
||||
output = {}
|
||||
|
||||
product_base_type_items_by_name: dict[str, ProductBaseTypeItem] = {
|
||||
product_base_type_item.name: product_base_type_item
|
||||
for product_base_type_item in product_base_type_items
|
||||
}
|
||||
|
||||
output: dict[str, ProductItem] = {}
|
||||
for product in products:
|
||||
product_id = product["id"]
|
||||
folder_id = product["folderId"]
|
||||
|
|
@ -484,6 +595,7 @@ class ProductsModel:
|
|||
product,
|
||||
versions,
|
||||
product_type_items_by_name,
|
||||
product_base_type_items_by_name,
|
||||
folder_item.label,
|
||||
product_id in loaded_product_ids,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,170 +0,0 @@
|
|||
from __future__ import annotations
|
||||
from qtpy import QtGui, QtCore
|
||||
|
||||
from ._multicombobox import (
|
||||
CustomPaintMultiselectComboBox,
|
||||
BaseQtModel,
|
||||
)
|
||||
|
||||
STATUS_ITEM_TYPE = 0
|
||||
SELECT_ALL_TYPE = 1
|
||||
DESELECT_ALL_TYPE = 2
|
||||
SWAP_STATE_TYPE = 3
|
||||
|
||||
PRODUCT_TYPE_ROLE = QtCore.Qt.UserRole + 1
|
||||
ITEM_TYPE_ROLE = QtCore.Qt.UserRole + 2
|
||||
ITEM_SUBTYPE_ROLE = QtCore.Qt.UserRole + 3
|
||||
|
||||
|
||||
class ProductTypesQtModel(BaseQtModel):
|
||||
refreshed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller):
|
||||
self._reset_filters_on_refresh = True
|
||||
self._refreshing = False
|
||||
self._bulk_change = False
|
||||
self._items_by_name = {}
|
||||
|
||||
super().__init__(
|
||||
item_type_role=ITEM_TYPE_ROLE,
|
||||
item_subtype_role=ITEM_SUBTYPE_ROLE,
|
||||
empty_values_label="No product types...",
|
||||
controller=controller,
|
||||
)
|
||||
|
||||
def is_refreshing(self):
|
||||
return self._refreshing
|
||||
|
||||
def refresh(self, project_name):
|
||||
self._refreshing = True
|
||||
super().refresh(project_name)
|
||||
|
||||
self._reset_filters_on_refresh = False
|
||||
self._refreshing = False
|
||||
self.refreshed.emit()
|
||||
|
||||
def reset_product_types_filter_on_refresh(self):
|
||||
self._reset_filters_on_refresh = True
|
||||
|
||||
def _get_standard_items(self) -> list[QtGui.QStandardItem]:
|
||||
return list(self._items_by_name.values())
|
||||
|
||||
def _clear_standard_items(self):
|
||||
self._items_by_name.clear()
|
||||
|
||||
def _prepare_new_value_items(self, project_name: str, _: bool) -> tuple[
|
||||
list[QtGui.QStandardItem], list[QtGui.QStandardItem]
|
||||
]:
|
||||
product_type_items = self._controller.get_product_type_items(
|
||||
project_name)
|
||||
self._last_project = project_name
|
||||
|
||||
names_to_remove = set(self._items_by_name.keys())
|
||||
items = []
|
||||
items_filter_required = {}
|
||||
for product_type_item in product_type_items:
|
||||
name = product_type_item.name
|
||||
names_to_remove.discard(name)
|
||||
item = self._items_by_name.get(name)
|
||||
# Apply filter to new items or if filters reset is requested
|
||||
filter_required = self._reset_filters_on_refresh
|
||||
if item is None:
|
||||
filter_required = True
|
||||
item = QtGui.QStandardItem(name)
|
||||
item.setData(name, PRODUCT_TYPE_ROLE)
|
||||
item.setEditable(False)
|
||||
item.setCheckable(True)
|
||||
self._items_by_name[name] = item
|
||||
|
||||
items.append(item)
|
||||
|
||||
if filter_required:
|
||||
items_filter_required[name] = item
|
||||
|
||||
if items_filter_required:
|
||||
product_types_filter = self._controller.get_product_types_filter()
|
||||
for product_type, item in items_filter_required.items():
|
||||
matching = (
|
||||
int(product_type in product_types_filter.product_types)
|
||||
+ int(product_types_filter.is_allow_list)
|
||||
)
|
||||
item.setCheckState(
|
||||
QtCore.Qt.Checked
|
||||
if matching % 2 == 0
|
||||
else QtCore.Qt.Unchecked
|
||||
)
|
||||
|
||||
items_to_remove = []
|
||||
for name in names_to_remove:
|
||||
items_to_remove.append(
|
||||
self._items_by_name.pop(name)
|
||||
)
|
||||
|
||||
# Uncheck all if all are checked (same result)
|
||||
if all(
|
||||
item.checkState() == QtCore.Qt.Checked
|
||||
for item in items
|
||||
):
|
||||
for item in items:
|
||||
item.setCheckState(QtCore.Qt.Unchecked)
|
||||
|
||||
return items, items_to_remove
|
||||
|
||||
|
||||
class ProductTypesCombobox(CustomPaintMultiselectComboBox):
|
||||
def __init__(self, controller, parent):
|
||||
self._controller = controller
|
||||
model = ProductTypesQtModel(controller)
|
||||
super().__init__(
|
||||
PRODUCT_TYPE_ROLE,
|
||||
PRODUCT_TYPE_ROLE,
|
||||
QtCore.Qt.ForegroundRole,
|
||||
QtCore.Qt.DecorationRole,
|
||||
item_type_role=ITEM_TYPE_ROLE,
|
||||
model=model,
|
||||
parent=parent
|
||||
)
|
||||
|
||||
model.refreshed.connect(self._on_model_refresh)
|
||||
|
||||
self.set_placeholder_text("Product types filter...")
|
||||
self._model = model
|
||||
self._last_project_name = None
|
||||
self._fully_disabled_filter = False
|
||||
|
||||
controller.register_event_callback(
|
||||
"selection.project.changed",
|
||||
self._on_project_change
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"projects.refresh.finished",
|
||||
self._on_projects_refresh
|
||||
)
|
||||
self.setToolTip("Product types filter")
|
||||
self.value_changed.connect(
|
||||
self._on_product_type_filter_change
|
||||
)
|
||||
|
||||
def reset_product_types_filter_on_refresh(self):
|
||||
self._model.reset_product_types_filter_on_refresh()
|
||||
|
||||
def _on_model_refresh(self):
|
||||
self.value_changed.emit()
|
||||
|
||||
def _on_product_type_filter_change(self):
|
||||
lines = ["Product types filter"]
|
||||
for item in self.get_value_info():
|
||||
status_name, enabled = item
|
||||
lines.append(f"{'✔' if enabled else '☐'} {status_name}")
|
||||
|
||||
self.setToolTip("\n".join(lines))
|
||||
|
||||
def _on_project_change(self, event):
|
||||
project_name = event["project_name"]
|
||||
self._last_project_name = project_name
|
||||
self._model.refresh(project_name)
|
||||
|
||||
def _on_projects_refresh(self):
|
||||
if self._last_project_name:
|
||||
self._model.refresh(self._last_project_name)
|
||||
self._on_product_type_filter_change()
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import numbers
|
||||
import uuid
|
||||
from typing import Dict
|
||||
|
|
@ -18,16 +20,19 @@ from .products_model import (
|
|||
SYNC_REMOTE_SITE_AVAILABILITY,
|
||||
)
|
||||
|
||||
STATUS_NAME_ROLE = QtCore.Qt.UserRole + 1
|
||||
TASK_ID_ROLE = QtCore.Qt.UserRole + 2
|
||||
COMBO_VERSION_ID_ROLE = QtCore.Qt.UserRole + 1
|
||||
COMBO_TASK_ID_ROLE = QtCore.Qt.UserRole + 2
|
||||
COMBO_STATUS_NAME_ROLE = QtCore.Qt.UserRole + 3
|
||||
COMBO_VERSION_TAGS_ROLE = QtCore.Qt.UserRole + 4
|
||||
COMBO_TASK_TAGS_ROLE = QtCore.Qt.UserRole + 5
|
||||
|
||||
|
||||
class VersionsModel(QtGui.QStandardItemModel):
|
||||
class ComboVersionsModel(QtGui.QStandardItemModel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._items_by_id = {}
|
||||
|
||||
def update_versions(self, version_items):
|
||||
def update_versions(self, version_items, task_tags_by_version_id):
|
||||
version_ids = {
|
||||
version_item.version_id
|
||||
for version_item in version_items
|
||||
|
|
@ -39,6 +44,7 @@ class VersionsModel(QtGui.QStandardItemModel):
|
|||
item = self._items_by_id.pop(item_id)
|
||||
root_item.removeRow(item.row())
|
||||
|
||||
version_tags_by_version_id = {}
|
||||
for idx, version_item in enumerate(version_items):
|
||||
version_id = version_item.version_id
|
||||
|
||||
|
|
@ -48,34 +54,74 @@ class VersionsModel(QtGui.QStandardItemModel):
|
|||
item = QtGui.QStandardItem(label)
|
||||
item.setData(version_id, QtCore.Qt.UserRole)
|
||||
self._items_by_id[version_id] = item
|
||||
item.setData(version_item.status, STATUS_NAME_ROLE)
|
||||
item.setData(version_item.task_id, TASK_ID_ROLE)
|
||||
version_tags = set(version_item.tags)
|
||||
task_tags = task_tags_by_version_id[version_id]
|
||||
item.setData(version_id, COMBO_VERSION_ID_ROLE)
|
||||
item.setData(version_item.status, COMBO_STATUS_NAME_ROLE)
|
||||
item.setData(version_item.task_id, COMBO_TASK_ID_ROLE)
|
||||
item.setData("|".join(version_tags), COMBO_VERSION_TAGS_ROLE)
|
||||
item.setData("|".join(task_tags), COMBO_TASK_TAGS_ROLE)
|
||||
version_tags_by_version_id[version_id] = set(version_item.tags)
|
||||
|
||||
if item.row() != idx:
|
||||
root_item.insertRow(idx, item)
|
||||
|
||||
|
||||
class VersionsFilterModel(QtCore.QSortFilterProxyModel):
|
||||
class ComboVersionsFilterModel(QtCore.QSortFilterProxyModel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._status_filter = None
|
||||
self._task_ids_filter = None
|
||||
self._version_tags_filter = None
|
||||
self._task_tags_filter = None
|
||||
|
||||
def filterAcceptsRow(self, row, parent):
|
||||
index = None
|
||||
if self._status_filter is not None:
|
||||
if not self._status_filter:
|
||||
return False
|
||||
|
||||
if index is None:
|
||||
index = self.sourceModel().index(row, 0, parent)
|
||||
status = index.data(STATUS_NAME_ROLE)
|
||||
status = index.data(COMBO_STATUS_NAME_ROLE)
|
||||
if status not in self._status_filter:
|
||||
return False
|
||||
|
||||
if self._task_ids_filter:
|
||||
if index is None:
|
||||
index = self.sourceModel().index(row, 0, parent)
|
||||
task_id = index.data(TASK_ID_ROLE)
|
||||
task_id = index.data(COMBO_TASK_ID_ROLE)
|
||||
if task_id not in self._task_ids_filter:
|
||||
return False
|
||||
|
||||
if self._version_tags_filter is not None:
|
||||
if not self._version_tags_filter:
|
||||
return False
|
||||
|
||||
if index is None:
|
||||
model = self.sourceModel()
|
||||
index = model.index(row, 0, parent)
|
||||
version_tags_s = index.data(COMBO_TASK_TAGS_ROLE)
|
||||
version_tags = set()
|
||||
if version_tags_s:
|
||||
version_tags = set(version_tags_s.split("|"))
|
||||
|
||||
if not version_tags & self._version_tags_filter:
|
||||
return False
|
||||
|
||||
if self._task_tags_filter is not None:
|
||||
if not self._task_tags_filter:
|
||||
return False
|
||||
|
||||
if index is None:
|
||||
model = self.sourceModel()
|
||||
index = model.index(row, 0, parent)
|
||||
task_tags_s = index.data(COMBO_TASK_TAGS_ROLE)
|
||||
task_tags = set()
|
||||
if task_tags_s:
|
||||
task_tags = set(task_tags_s.split("|"))
|
||||
if not (task_tags & self._task_tags_filter):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def set_tasks_filter(self, task_ids):
|
||||
|
|
@ -84,12 +130,24 @@ class VersionsFilterModel(QtCore.QSortFilterProxyModel):
|
|||
self._task_ids_filter = task_ids
|
||||
self.invalidateFilter()
|
||||
|
||||
def set_task_tags_filter(self, tags):
|
||||
if self._task_tags_filter == tags:
|
||||
return
|
||||
self._task_tags_filter = tags
|
||||
self.invalidateFilter()
|
||||
|
||||
def set_statuses_filter(self, status_names):
|
||||
if self._status_filter == status_names:
|
||||
return
|
||||
self._status_filter = status_names
|
||||
self.invalidateFilter()
|
||||
|
||||
def set_version_tags_filter(self, tags):
|
||||
if self._version_tags_filter == tags:
|
||||
return
|
||||
self._version_tags_filter = tags
|
||||
self.invalidateFilter()
|
||||
|
||||
|
||||
class VersionComboBox(QtWidgets.QComboBox):
|
||||
value_changed = QtCore.Signal(str, str)
|
||||
|
|
@ -97,8 +155,8 @@ class VersionComboBox(QtWidgets.QComboBox):
|
|||
def __init__(self, product_id, parent):
|
||||
super().__init__(parent)
|
||||
|
||||
versions_model = VersionsModel()
|
||||
proxy_model = VersionsFilterModel()
|
||||
versions_model = ComboVersionsModel()
|
||||
proxy_model = ComboVersionsFilterModel()
|
||||
proxy_model.setSourceModel(versions_model)
|
||||
|
||||
self.setModel(proxy_model)
|
||||
|
|
@ -123,6 +181,13 @@ class VersionComboBox(QtWidgets.QComboBox):
|
|||
if self.currentIndex() != 0:
|
||||
self.setCurrentIndex(0)
|
||||
|
||||
def set_task_tags_filter(self, tags):
|
||||
self._proxy_model.set_task_tags_filter(tags)
|
||||
if self.count() == 0:
|
||||
return
|
||||
if self.currentIndex() != 0:
|
||||
self.setCurrentIndex(0)
|
||||
|
||||
def set_statuses_filter(self, status_names):
|
||||
self._proxy_model.set_statuses_filter(status_names)
|
||||
if self.count() == 0:
|
||||
|
|
@ -130,12 +195,24 @@ class VersionComboBox(QtWidgets.QComboBox):
|
|||
if self.currentIndex() != 0:
|
||||
self.setCurrentIndex(0)
|
||||
|
||||
def set_version_tags_filter(self, tags):
|
||||
self._proxy_model.set_version_tags_filter(tags)
|
||||
if self.count() == 0:
|
||||
return
|
||||
if self.currentIndex() != 0:
|
||||
self.setCurrentIndex(0)
|
||||
|
||||
def all_versions_filtered_out(self):
|
||||
if self._items_by_id:
|
||||
return self.count() == 0
|
||||
return False
|
||||
|
||||
def update_versions(self, version_items, current_version_id):
|
||||
def update_versions(
|
||||
self,
|
||||
version_items,
|
||||
current_version_id,
|
||||
task_tags_by_version_id,
|
||||
):
|
||||
self.blockSignals(True)
|
||||
version_items = list(version_items)
|
||||
version_ids = [
|
||||
|
|
@ -146,7 +223,9 @@ class VersionComboBox(QtWidgets.QComboBox):
|
|||
current_version_id = version_ids[0]
|
||||
self._current_id = current_version_id
|
||||
|
||||
self._versions_model.update_versions(version_items)
|
||||
self._versions_model.update_versions(
|
||||
version_items, task_tags_by_version_id
|
||||
)
|
||||
|
||||
index = version_ids.index(current_version_id)
|
||||
if self.currentIndex() != index:
|
||||
|
|
@ -173,6 +252,8 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
|
|||
self._editor_by_id: Dict[str, VersionComboBox] = {}
|
||||
self._task_ids_filter = None
|
||||
self._statuses_filter = None
|
||||
self._version_tags_filter = None
|
||||
self._task_tags_filter = None
|
||||
|
||||
def displayText(self, value, locale):
|
||||
if not isinstance(value, numbers.Integral):
|
||||
|
|
@ -185,10 +266,26 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
|
|||
widget.set_tasks_filter(task_ids)
|
||||
|
||||
def set_statuses_filter(self, status_names):
|
||||
self._statuses_filter = set(status_names)
|
||||
if status_names is not None:
|
||||
status_names = set(status_names)
|
||||
self._statuses_filter = status_names
|
||||
for widget in self._editor_by_id.values():
|
||||
widget.set_statuses_filter(status_names)
|
||||
|
||||
def set_version_tags_filter(self, tags):
|
||||
if tags is not None:
|
||||
tags = set(tags)
|
||||
self._version_tags_filter = tags
|
||||
for widget in self._editor_by_id.values():
|
||||
widget.set_version_tags_filter(tags)
|
||||
|
||||
def set_task_tags_filter(self, tags):
|
||||
if tags is not None:
|
||||
tags = set(tags)
|
||||
self._task_tags_filter = tags
|
||||
for widget in self._editor_by_id.values():
|
||||
widget.set_task_tags_filter(tags)
|
||||
|
||||
def paint(self, painter, option, index):
|
||||
fg_color = index.data(QtCore.Qt.ForegroundRole)
|
||||
if fg_color:
|
||||
|
|
@ -200,7 +297,7 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
|
|||
fg_color = None
|
||||
|
||||
if not fg_color:
|
||||
return super(VersionDelegate, self).paint(painter, option, index)
|
||||
return super().paint(painter, option, index)
|
||||
|
||||
if option.widget:
|
||||
style = option.widget.style()
|
||||
|
|
@ -263,11 +360,22 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
|
|||
editor.clear()
|
||||
|
||||
# Current value of the index
|
||||
versions = index.data(VERSION_NAME_EDIT_ROLE) or []
|
||||
product_id = index.data(PRODUCT_ID_ROLE)
|
||||
version_id = index.data(VERSION_ID_ROLE)
|
||||
model = index.model()
|
||||
while hasattr(model, "sourceModel"):
|
||||
model = model.sourceModel()
|
||||
versions = model.get_version_items_by_product_id(product_id)
|
||||
task_tags_by_version_id = {
|
||||
version_item.version_id: model.get_task_tags_by_id(
|
||||
version_item.task_id
|
||||
)
|
||||
for version_item in versions
|
||||
}
|
||||
|
||||
editor.update_versions(versions, version_id)
|
||||
editor.update_versions(versions, version_id, task_tags_by_version_id)
|
||||
editor.set_tasks_filter(self._task_ids_filter)
|
||||
editor.set_task_tags_filter(self._task_tags_filter)
|
||||
editor.set_statuses_filter(self._statuses_filter)
|
||||
|
||||
def setModelData(self, editor, model, index):
|
||||
|
|
|
|||
|
|
@ -16,31 +16,34 @@ TASK_ID_ROLE = QtCore.Qt.UserRole + 5
|
|||
PRODUCT_ID_ROLE = QtCore.Qt.UserRole + 6
|
||||
PRODUCT_NAME_ROLE = QtCore.Qt.UserRole + 7
|
||||
PRODUCT_TYPE_ROLE = QtCore.Qt.UserRole + 8
|
||||
PRODUCT_TYPE_ICON_ROLE = QtCore.Qt.UserRole + 9
|
||||
PRODUCT_IN_SCENE_ROLE = QtCore.Qt.UserRole + 10
|
||||
VERSION_ID_ROLE = QtCore.Qt.UserRole + 11
|
||||
VERSION_HERO_ROLE = QtCore.Qt.UserRole + 12
|
||||
VERSION_NAME_ROLE = QtCore.Qt.UserRole + 13
|
||||
VERSION_NAME_EDIT_ROLE = QtCore.Qt.UserRole + 14
|
||||
VERSION_PUBLISH_TIME_ROLE = QtCore.Qt.UserRole + 15
|
||||
VERSION_STATUS_NAME_ROLE = QtCore.Qt.UserRole + 16
|
||||
VERSION_STATUS_SHORT_ROLE = QtCore.Qt.UserRole + 17
|
||||
VERSION_STATUS_COLOR_ROLE = QtCore.Qt.UserRole + 18
|
||||
VERSION_STATUS_ICON_ROLE = QtCore.Qt.UserRole + 19
|
||||
VERSION_AUTHOR_ROLE = QtCore.Qt.UserRole + 20
|
||||
VERSION_FRAME_RANGE_ROLE = QtCore.Qt.UserRole + 21
|
||||
VERSION_DURATION_ROLE = QtCore.Qt.UserRole + 22
|
||||
VERSION_HANDLES_ROLE = QtCore.Qt.UserRole + 23
|
||||
VERSION_STEP_ROLE = QtCore.Qt.UserRole + 24
|
||||
VERSION_AVAILABLE_ROLE = QtCore.Qt.UserRole + 25
|
||||
VERSION_THUMBNAIL_ID_ROLE = QtCore.Qt.UserRole + 26
|
||||
ACTIVE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 27
|
||||
REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 28
|
||||
REPRESENTATIONS_COUNT_ROLE = QtCore.Qt.UserRole + 29
|
||||
SYNC_ACTIVE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 30
|
||||
SYNC_REMOTE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 31
|
||||
PRODUCT_BASE_TYPE_ROLE = QtCore.Qt.UserRole + 9
|
||||
PRODUCT_TYPE_ICON_ROLE = QtCore.Qt.UserRole + 10
|
||||
PRODUCT_IN_SCENE_ROLE = QtCore.Qt.UserRole + 11
|
||||
VERSION_ID_ROLE = QtCore.Qt.UserRole + 12
|
||||
VERSION_HERO_ROLE = QtCore.Qt.UserRole + 13
|
||||
VERSION_NAME_ROLE = QtCore.Qt.UserRole + 14
|
||||
VERSION_NAME_EDIT_ROLE = QtCore.Qt.UserRole + 15
|
||||
VERSION_PUBLISH_TIME_ROLE = QtCore.Qt.UserRole + 16
|
||||
VERSION_STATUS_NAME_ROLE = QtCore.Qt.UserRole + 17
|
||||
VERSION_STATUS_SHORT_ROLE = QtCore.Qt.UserRole + 18
|
||||
VERSION_STATUS_COLOR_ROLE = QtCore.Qt.UserRole + 19
|
||||
VERSION_STATUS_ICON_ROLE = QtCore.Qt.UserRole + 20
|
||||
VERSION_AUTHOR_ROLE = QtCore.Qt.UserRole + 21
|
||||
VERSION_FRAME_RANGE_ROLE = QtCore.Qt.UserRole + 22
|
||||
VERSION_DURATION_ROLE = QtCore.Qt.UserRole + 23
|
||||
VERSION_HANDLES_ROLE = QtCore.Qt.UserRole + 24
|
||||
VERSION_STEP_ROLE = QtCore.Qt.UserRole + 25
|
||||
VERSION_AVAILABLE_ROLE = QtCore.Qt.UserRole + 26
|
||||
VERSION_THUMBNAIL_ID_ROLE = QtCore.Qt.UserRole + 27
|
||||
ACTIVE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 28
|
||||
REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 29
|
||||
REPRESENTATIONS_COUNT_ROLE = QtCore.Qt.UserRole + 30
|
||||
SYNC_ACTIVE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 31
|
||||
SYNC_REMOTE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 32
|
||||
|
||||
STATUS_NAME_FILTER_ROLE = QtCore.Qt.UserRole + 32
|
||||
STATUS_NAME_FILTER_ROLE = QtCore.Qt.UserRole + 33
|
||||
TASK_TAGS_FILTER_ROLE = QtCore.Qt.UserRole + 34
|
||||
VERSION_TAGS_FILTER_ROLE = QtCore.Qt.UserRole + 35
|
||||
|
||||
|
||||
class ProductsModel(QtGui.QStandardItemModel):
|
||||
|
|
@ -49,6 +52,7 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
column_labels = [
|
||||
"Product name",
|
||||
"Product type",
|
||||
"Product base type",
|
||||
"Folder",
|
||||
"Version",
|
||||
"Status",
|
||||
|
|
@ -79,6 +83,7 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
|
||||
product_name_col = column_labels.index("Product name")
|
||||
product_type_col = column_labels.index("Product type")
|
||||
product_base_type_col = column_labels.index("Product base type")
|
||||
folders_label_col = column_labels.index("Folder")
|
||||
version_col = column_labels.index("Version")
|
||||
status_col = column_labels.index("Status")
|
||||
|
|
@ -93,6 +98,7 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
_display_role_mapping = {
|
||||
product_name_col: QtCore.Qt.DisplayRole,
|
||||
product_type_col: PRODUCT_TYPE_ROLE,
|
||||
product_base_type_col: PRODUCT_BASE_TYPE_ROLE,
|
||||
folders_label_col: FOLDER_LABEL_ROLE,
|
||||
version_col: VERSION_NAME_ROLE,
|
||||
status_col: VERSION_STATUS_NAME_ROLE,
|
||||
|
|
@ -130,6 +136,7 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
self._last_folder_ids = []
|
||||
self._last_project_statuses = {}
|
||||
self._last_status_icons_by_name = {}
|
||||
self._last_task_tags_by_task_id = {}
|
||||
|
||||
def get_product_item_indexes(self):
|
||||
return [
|
||||
|
|
@ -170,6 +177,17 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
self._last_folder_ids
|
||||
)
|
||||
|
||||
def get_task_tags_by_id(self, task_id):
|
||||
return self._last_task_tags_by_task_id.get(task_id, set())
|
||||
|
||||
def get_version_items_by_product_id(self, product_id: str):
|
||||
product_item = self._product_items_by_id.get(product_id)
|
||||
if product_item is None:
|
||||
return None
|
||||
version_items = list(product_item.version_items.values())
|
||||
version_items.sort(reverse=True)
|
||||
return version_items
|
||||
|
||||
def flags(self, index):
|
||||
# Make the version column editable
|
||||
if index.column() == self.version_col and index.data(PRODUCT_ID_ROLE):
|
||||
|
|
@ -224,9 +242,9 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
product_item = self._product_items_by_id.get(product_id)
|
||||
if product_item is None:
|
||||
return None
|
||||
product_items = list(product_item.version_items.values())
|
||||
product_items.sort(reverse=True)
|
||||
return product_items
|
||||
version_items = list(product_item.version_items.values())
|
||||
version_items.sort(reverse=True)
|
||||
return version_items
|
||||
|
||||
if role == QtCore.Qt.EditRole:
|
||||
return None
|
||||
|
|
@ -422,6 +440,16 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
version_item.status
|
||||
for version_item in product_item.version_items.values()
|
||||
}
|
||||
version_tags = set()
|
||||
task_tags = set()
|
||||
for version_item in product_item.version_items.values():
|
||||
version_tags |= set(version_item.tags)
|
||||
_task_tags = self._last_task_tags_by_task_id.get(
|
||||
version_item.task_id
|
||||
)
|
||||
if _task_tags:
|
||||
task_tags |= set(_task_tags)
|
||||
|
||||
if model_item is None:
|
||||
product_id = product_item.product_id
|
||||
model_item = QtGui.QStandardItem(product_item.product_name)
|
||||
|
|
@ -432,6 +460,9 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
model_item.setData(icon, QtCore.Qt.DecorationRole)
|
||||
model_item.setData(product_id, PRODUCT_ID_ROLE)
|
||||
model_item.setData(product_item.product_name, PRODUCT_NAME_ROLE)
|
||||
model_item.setData(
|
||||
product_item.product_base_type, PRODUCT_BASE_TYPE_ROLE
|
||||
)
|
||||
model_item.setData(product_item.product_type, PRODUCT_TYPE_ROLE)
|
||||
model_item.setData(product_type_icon, PRODUCT_TYPE_ICON_ROLE)
|
||||
model_item.setData(product_item.folder_id, FOLDER_ID_ROLE)
|
||||
|
|
@ -440,6 +471,8 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
self._items_by_id[product_id] = model_item
|
||||
|
||||
model_item.setData("|".join(statuses), STATUS_NAME_FILTER_ROLE)
|
||||
model_item.setData("|".join(version_tags), VERSION_TAGS_FILTER_ROLE)
|
||||
model_item.setData("|".join(task_tags), TASK_TAGS_FILTER_ROLE)
|
||||
model_item.setData(product_item.folder_label, FOLDER_LABEL_ROLE)
|
||||
in_scene = 1 if product_item.product_in_scene else 0
|
||||
model_item.setData(in_scene, PRODUCT_IN_SCENE_ROLE)
|
||||
|
|
@ -470,6 +503,14 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
}
|
||||
self._last_status_icons_by_name = {}
|
||||
|
||||
task_items = self._controller.get_task_items(
|
||||
project_name, folder_ids, sender=PRODUCTS_MODEL_SENDER_NAME
|
||||
)
|
||||
self._last_task_tags_by_task_id = {
|
||||
task_item.task_id: task_item.tags
|
||||
for task_item in task_items
|
||||
}
|
||||
|
||||
active_site_icon_def = self._controller.get_active_site_icon_def(
|
||||
project_name
|
||||
)
|
||||
|
|
@ -484,6 +525,7 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
folder_ids,
|
||||
sender=PRODUCTS_MODEL_SENDER_NAME
|
||||
)
|
||||
|
||||
product_items_by_id = {
|
||||
product_item.product_id: product_item
|
||||
for product_item in product_items
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from typing import Optional
|
|||
|
||||
from qtpy import QtWidgets, QtCore
|
||||
|
||||
from ayon_core.pipeline.compatibility import is_product_base_type_supported
|
||||
from ayon_core.tools.utils import (
|
||||
RecursiveSortFilterProxyModel,
|
||||
DeselectableTreeView,
|
||||
|
|
@ -26,6 +27,8 @@ from .products_model import (
|
|||
VERSION_STATUS_ICON_ROLE,
|
||||
VERSION_THUMBNAIL_ID_ROLE,
|
||||
STATUS_NAME_FILTER_ROLE,
|
||||
VERSION_TAGS_FILTER_ROLE,
|
||||
TASK_TAGS_FILTER_ROLE,
|
||||
)
|
||||
from .products_delegates import (
|
||||
VersionDelegate,
|
||||
|
|
@ -41,6 +44,8 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel):
|
|||
|
||||
self._product_type_filters = None
|
||||
self._statuses_filter = None
|
||||
self._version_tags_filter = None
|
||||
self._task_tags_filter = None
|
||||
self._task_ids_filter = None
|
||||
self._ascending_sort = True
|
||||
|
||||
|
|
@ -67,6 +72,18 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel):
|
|||
self._statuses_filter = statuses_filter
|
||||
self.invalidateFilter()
|
||||
|
||||
def set_version_tags_filter(self, tags):
|
||||
if self._version_tags_filter == tags:
|
||||
return
|
||||
self._version_tags_filter = tags
|
||||
self.invalidateFilter()
|
||||
|
||||
def set_task_tags_filter(self, tags):
|
||||
if self._task_tags_filter == tags:
|
||||
return
|
||||
self._task_tags_filter = tags
|
||||
self.invalidateFilter()
|
||||
|
||||
def filterAcceptsRow(self, source_row, source_parent):
|
||||
source_model = self.sourceModel()
|
||||
index = source_model.index(source_row, 0, source_parent)
|
||||
|
|
@ -83,6 +100,16 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel):
|
|||
):
|
||||
return False
|
||||
|
||||
if not self._accept_row_by_role_value(
|
||||
index, self._version_tags_filter, VERSION_TAGS_FILTER_ROLE
|
||||
):
|
||||
return False
|
||||
|
||||
if not self._accept_row_by_role_value(
|
||||
index, self._task_tags_filter, TASK_TAGS_FILTER_ROLE
|
||||
):
|
||||
return False
|
||||
|
||||
return super().filterAcceptsRow(source_row, source_parent)
|
||||
|
||||
def _accept_task_ids_filter(self, index):
|
||||
|
|
@ -102,9 +129,10 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel):
|
|||
if not filter_value:
|
||||
return False
|
||||
|
||||
status_s = index.data(role)
|
||||
for status in status_s.split("|"):
|
||||
if status in filter_value:
|
||||
value_s = index.data(role)
|
||||
if value_s:
|
||||
for value in value_s.split("|"):
|
||||
if value in filter_value:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
|
@ -142,6 +170,7 @@ class ProductsWidget(QtWidgets.QWidget):
|
|||
default_widths = (
|
||||
200, # Product name
|
||||
90, # Product type
|
||||
90, # Product base type
|
||||
130, # Folder label
|
||||
60, # Version
|
||||
100, # Status
|
||||
|
|
@ -261,6 +290,12 @@ class ProductsWidget(QtWidgets.QWidget):
|
|||
self._controller.is_sitesync_enabled()
|
||||
)
|
||||
|
||||
if not is_product_base_type_supported():
|
||||
# Hide product base type column
|
||||
products_view.setColumnHidden(
|
||||
products_model.product_base_type_col, True
|
||||
)
|
||||
|
||||
def set_name_filter(self, name):
|
||||
"""Set filter of product name.
|
||||
|
||||
|
|
@ -290,6 +325,14 @@ class ProductsWidget(QtWidgets.QWidget):
|
|||
self._version_delegate.set_statuses_filter(status_names)
|
||||
self._products_proxy_model.set_statuses_filter(status_names)
|
||||
|
||||
def set_version_tags_filter(self, version_tags):
|
||||
self._version_delegate.set_version_tags_filter(version_tags)
|
||||
self._products_proxy_model.set_version_tags_filter(version_tags)
|
||||
|
||||
def set_task_tags_filter(self, task_tags):
|
||||
self._version_delegate.set_task_tags_filter(task_tags)
|
||||
self._products_proxy_model.set_task_tags_filter(task_tags)
|
||||
|
||||
def set_product_type_filter(self, product_type_filters):
|
||||
"""
|
||||
|
||||
|
|
|
|||
1122
client/ayon_core/tools/loader/ui/search_bar.py
Normal file
1122
client/ayon_core/tools/loader/ui/search_bar.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,157 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from qtpy import QtCore, QtGui
|
||||
|
||||
from ayon_core.tools.utils import get_qt_icon
|
||||
from ayon_core.tools.common_models import StatusItem
|
||||
|
||||
from ._multicombobox import (
|
||||
CustomPaintMultiselectComboBox,
|
||||
BaseQtModel,
|
||||
)
|
||||
|
||||
STATUS_ITEM_TYPE = 0
|
||||
SELECT_ALL_TYPE = 1
|
||||
DESELECT_ALL_TYPE = 2
|
||||
SWAP_STATE_TYPE = 3
|
||||
|
||||
STATUSES_FILTER_SENDER = "loader.statuses_filter"
|
||||
STATUS_NAME_ROLE = QtCore.Qt.UserRole + 1
|
||||
STATUS_SHORT_ROLE = QtCore.Qt.UserRole + 2
|
||||
STATUS_COLOR_ROLE = QtCore.Qt.UserRole + 3
|
||||
STATUS_ICON_ROLE = QtCore.Qt.UserRole + 4
|
||||
ITEM_TYPE_ROLE = QtCore.Qt.UserRole + 5
|
||||
ITEM_SUBTYPE_ROLE = QtCore.Qt.UserRole + 6
|
||||
|
||||
|
||||
class StatusesQtModel(BaseQtModel):
|
||||
def __init__(self, controller):
|
||||
self._items_by_name: dict[str, QtGui.QStandardItem] = {}
|
||||
self._icons_by_name_n_color: dict[str, QtGui.QIcon] = {}
|
||||
super().__init__(
|
||||
ITEM_TYPE_ROLE,
|
||||
ITEM_SUBTYPE_ROLE,
|
||||
"No statuses...",
|
||||
controller,
|
||||
)
|
||||
|
||||
def _get_standard_items(self) -> list[QtGui.QStandardItem]:
|
||||
return list(self._items_by_name.values())
|
||||
|
||||
def _clear_standard_items(self):
|
||||
self._items_by_name.clear()
|
||||
|
||||
def _prepare_new_value_items(
|
||||
self, project_name: str, project_changed: bool
|
||||
):
|
||||
status_items: list[StatusItem] = (
|
||||
self._controller.get_project_status_items(
|
||||
project_name, sender=STATUSES_FILTER_SENDER
|
||||
)
|
||||
)
|
||||
items = []
|
||||
items_to_remove = []
|
||||
if not status_items:
|
||||
return items, items_to_remove
|
||||
|
||||
names_to_remove = set(self._items_by_name)
|
||||
for row_idx, status_item in enumerate(status_items):
|
||||
name = status_item.name
|
||||
if name in self._items_by_name:
|
||||
item = self._items_by_name[name]
|
||||
names_to_remove.discard(name)
|
||||
else:
|
||||
item = QtGui.QStandardItem()
|
||||
item.setData(ITEM_SUBTYPE_ROLE, STATUS_ITEM_TYPE)
|
||||
item.setCheckState(QtCore.Qt.Unchecked)
|
||||
item.setFlags(
|
||||
QtCore.Qt.ItemIsEnabled
|
||||
| QtCore.Qt.ItemIsSelectable
|
||||
| QtCore.Qt.ItemIsUserCheckable
|
||||
)
|
||||
self._items_by_name[name] = item
|
||||
|
||||
icon = self._get_icon(status_item)
|
||||
for role, value in (
|
||||
(STATUS_NAME_ROLE, status_item.name),
|
||||
(STATUS_SHORT_ROLE, status_item.short),
|
||||
(STATUS_COLOR_ROLE, status_item.color),
|
||||
(STATUS_ICON_ROLE, icon),
|
||||
):
|
||||
if item.data(role) != value:
|
||||
item.setData(value, role)
|
||||
|
||||
if project_changed:
|
||||
item.setCheckState(QtCore.Qt.Unchecked)
|
||||
items.append(item)
|
||||
|
||||
for name in names_to_remove:
|
||||
items_to_remove.append(self._items_by_name.pop(name))
|
||||
|
||||
return items, items_to_remove
|
||||
|
||||
def _get_icon(self, status_item: StatusItem) -> QtGui.QIcon:
|
||||
name = status_item.name
|
||||
color = status_item.color
|
||||
unique_id = "|".join([name or "", color or ""])
|
||||
icon = self._icons_by_name_n_color.get(unique_id)
|
||||
if icon is not None:
|
||||
return icon
|
||||
|
||||
icon: QtGui.QIcon = get_qt_icon({
|
||||
"type": "material-symbols",
|
||||
"name": status_item.icon,
|
||||
"color": status_item.color
|
||||
})
|
||||
self._icons_by_name_n_color[unique_id] = icon
|
||||
return icon
|
||||
|
||||
|
||||
class StatusesCombobox(CustomPaintMultiselectComboBox):
|
||||
def __init__(self, controller, parent):
|
||||
self._controller = controller
|
||||
model = StatusesQtModel(controller)
|
||||
super().__init__(
|
||||
STATUS_NAME_ROLE,
|
||||
STATUS_SHORT_ROLE,
|
||||
STATUS_COLOR_ROLE,
|
||||
STATUS_ICON_ROLE,
|
||||
item_type_role=ITEM_TYPE_ROLE,
|
||||
model=model,
|
||||
parent=parent
|
||||
)
|
||||
self.set_placeholder_text("Version status filter...")
|
||||
self._model = model
|
||||
self._last_project_name = None
|
||||
self._fully_disabled_filter = False
|
||||
|
||||
controller.register_event_callback(
|
||||
"selection.project.changed",
|
||||
self._on_project_change
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"projects.refresh.finished",
|
||||
self._on_projects_refresh
|
||||
)
|
||||
self.setToolTip("Statuses filter")
|
||||
self.value_changed.connect(
|
||||
self._on_status_filter_change
|
||||
)
|
||||
|
||||
def _on_status_filter_change(self):
|
||||
lines = ["Statuses filter"]
|
||||
for item in self.get_value_info():
|
||||
status_name, enabled = item
|
||||
lines.append(f"{'✔' if enabled else '☐'} {status_name}")
|
||||
|
||||
self.setToolTip("\n".join(lines))
|
||||
|
||||
def _on_project_change(self, event):
|
||||
project_name = event["project_name"]
|
||||
self._last_project_name = project_name
|
||||
self._model.refresh(project_name)
|
||||
|
||||
def _on_projects_refresh(self):
|
||||
if self._last_project_name:
|
||||
self._model.refresh(self._last_project_name)
|
||||
self._on_status_filter_change()
|
||||
|
|
@ -332,10 +332,6 @@ class LoaderTasksWidget(QtWidgets.QWidget):
|
|||
"selection.folders.changed",
|
||||
self._on_folders_selection_changed,
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"tasks.refresh.finished",
|
||||
self._on_tasks_refresh_finished
|
||||
)
|
||||
|
||||
selection_model = tasks_view.selectionModel()
|
||||
selection_model.selectionChanged.connect(self._on_selection_change)
|
||||
|
|
@ -373,10 +369,6 @@ class LoaderTasksWidget(QtWidgets.QWidget):
|
|||
def _clear(self):
|
||||
self._tasks_model.clear()
|
||||
|
||||
def _on_tasks_refresh_finished(self, event):
|
||||
if event["sender"] != TASKS_MODEL_SENDER_NAME:
|
||||
self._set_project_name(event["project_name"])
|
||||
|
||||
def _on_folders_selection_changed(self, event):
|
||||
project_name = event["project_name"]
|
||||
folder_ids = event["folder_ids"]
|
||||
|
|
|
|||
|
|
@ -11,16 +11,24 @@ from ayon_core.tools.utils import (
|
|||
)
|
||||
from ayon_core.tools.utils.lib import center_window
|
||||
from ayon_core.tools.utils import ProjectsCombobox
|
||||
from ayon_core.tools.common_models import StatusItem
|
||||
from ayon_core.tools.loader.abstract import ProductTypeItem
|
||||
from ayon_core.tools.loader.control import LoaderController
|
||||
|
||||
from .folders_widget import LoaderFoldersWidget
|
||||
from .tasks_widget import LoaderTasksWidget
|
||||
from .products_widget import ProductsWidget
|
||||
from .product_types_combo import ProductTypesCombobox
|
||||
from .product_group_dialog import ProductGroupDialog
|
||||
from .info_widget import InfoWidget
|
||||
from .repres_widget import RepresentationsWidget
|
||||
from .statuses_combo import StatusesCombobox
|
||||
from .search_bar import FiltersBar, FilterDefinition
|
||||
|
||||
FIND_KEY_SEQUENCE = QtGui.QKeySequence(
|
||||
QtCore.Qt.Modifier.CTRL | QtCore.Qt.Key_F
|
||||
)
|
||||
GROUP_KEY_SEQUENCE = QtGui.QKeySequence(
|
||||
QtCore.Qt.Modifier.CTRL | QtCore.Qt.Key_G
|
||||
)
|
||||
|
||||
|
||||
class LoadErrorMessageBox(ErrorMessageBox):
|
||||
|
|
@ -182,29 +190,19 @@ class LoaderWindow(QtWidgets.QWidget):
|
|||
products_wrap_widget = QtWidgets.QWidget(main_splitter)
|
||||
|
||||
products_inputs_widget = QtWidgets.QWidget(products_wrap_widget)
|
||||
|
||||
products_filter_input = PlaceholderLineEdit(products_inputs_widget)
|
||||
products_filter_input.setPlaceholderText("Product name filter...")
|
||||
|
||||
product_types_filter_combo = ProductTypesCombobox(
|
||||
controller, products_inputs_widget
|
||||
)
|
||||
|
||||
product_status_filter_combo = StatusesCombobox(controller, self)
|
||||
search_bar = FiltersBar(products_inputs_widget)
|
||||
|
||||
product_group_checkbox = QtWidgets.QCheckBox(
|
||||
"Enable grouping", products_inputs_widget)
|
||||
product_group_checkbox.setChecked(True)
|
||||
|
||||
products_widget = ProductsWidget(controller, products_wrap_widget)
|
||||
|
||||
products_inputs_layout = QtWidgets.QHBoxLayout(products_inputs_widget)
|
||||
products_inputs_layout.setContentsMargins(0, 0, 0, 0)
|
||||
products_inputs_layout.addWidget(products_filter_input, 1)
|
||||
products_inputs_layout.addWidget(product_types_filter_combo, 1)
|
||||
products_inputs_layout.addWidget(product_status_filter_combo, 1)
|
||||
products_inputs_layout.addWidget(search_bar, 1)
|
||||
products_inputs_layout.addWidget(product_group_checkbox, 0)
|
||||
|
||||
products_widget = ProductsWidget(controller, products_wrap_widget)
|
||||
|
||||
products_wrap_layout = QtWidgets.QVBoxLayout(products_wrap_widget)
|
||||
products_wrap_layout.setContentsMargins(0, 0, 0, 0)
|
||||
products_wrap_layout.addWidget(products_inputs_widget, 0)
|
||||
|
|
@ -250,15 +248,7 @@ class LoaderWindow(QtWidgets.QWidget):
|
|||
folders_filter_input.textChanged.connect(
|
||||
self._on_folder_filter_change
|
||||
)
|
||||
products_filter_input.textChanged.connect(
|
||||
self._on_product_filter_change
|
||||
)
|
||||
product_types_filter_combo.value_changed.connect(
|
||||
self._on_product_type_filter_change
|
||||
)
|
||||
product_status_filter_combo.value_changed.connect(
|
||||
self._on_status_filter_change
|
||||
)
|
||||
search_bar.filter_changed.connect(self._on_filter_change)
|
||||
product_group_checkbox.stateChanged.connect(
|
||||
self._on_product_group_change
|
||||
)
|
||||
|
|
@ -316,9 +306,7 @@ class LoaderWindow(QtWidgets.QWidget):
|
|||
|
||||
self._tasks_widget = tasks_widget
|
||||
|
||||
self._products_filter_input = products_filter_input
|
||||
self._product_types_filter_combo = product_types_filter_combo
|
||||
self._product_status_filter_combo = product_status_filter_combo
|
||||
self._search_bar = search_bar
|
||||
self._product_group_checkbox = product_group_checkbox
|
||||
self._products_widget = products_widget
|
||||
|
||||
|
|
@ -337,6 +325,8 @@ class LoaderWindow(QtWidgets.QWidget):
|
|||
self._selected_folder_ids = set()
|
||||
self._selected_version_ids = set()
|
||||
|
||||
self._set_product_type_filters = True
|
||||
|
||||
self._products_widget.set_enable_grouping(
|
||||
self._product_group_checkbox.isChecked()
|
||||
)
|
||||
|
|
@ -356,22 +346,24 @@ class LoaderWindow(QtWidgets.QWidget):
|
|||
def closeEvent(self, event):
|
||||
super().closeEvent(event)
|
||||
|
||||
(
|
||||
self
|
||||
._product_types_filter_combo
|
||||
.reset_product_types_filter_on_refresh()
|
||||
)
|
||||
|
||||
self._reset_on_show = True
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
modifiers = event.modifiers()
|
||||
ctrl_pressed = QtCore.Qt.ControlModifier & modifiers
|
||||
if hasattr(event, "keyCombination"):
|
||||
combination = event.keyCombination()
|
||||
else:
|
||||
combination = QtGui.QKeySequence(event.modifiers() | event.key())
|
||||
if (
|
||||
FIND_KEY_SEQUENCE == combination
|
||||
and not event.isAutoRepeat()
|
||||
):
|
||||
self._search_bar.show_filters_popup()
|
||||
event.setAccepted(True)
|
||||
return
|
||||
|
||||
# Grouping products on pressing Ctrl + G
|
||||
if (
|
||||
ctrl_pressed
|
||||
and event.key() == QtCore.Qt.Key_G
|
||||
GROUP_KEY_SEQUENCE == combination
|
||||
and not event.isAutoRepeat()
|
||||
):
|
||||
self._show_group_dialog()
|
||||
|
|
@ -435,20 +427,30 @@ class LoaderWindow(QtWidgets.QWidget):
|
|||
self._product_group_checkbox.isChecked()
|
||||
)
|
||||
|
||||
def _on_product_filter_change(self, text):
|
||||
self._products_widget.set_name_filter(text)
|
||||
def _on_filter_change(self, filter_name):
|
||||
if filter_name == "product_name":
|
||||
self._products_widget.set_name_filter(
|
||||
self._search_bar.get_filter_value("product_name")
|
||||
)
|
||||
elif filter_name == "product_types":
|
||||
product_types = self._search_bar.get_filter_value("product_types")
|
||||
self._products_widget.set_product_type_filter(product_types)
|
||||
|
||||
elif filter_name == "statuses":
|
||||
status_names = self._search_bar.get_filter_value("statuses")
|
||||
self._products_widget.set_statuses_filter(status_names)
|
||||
|
||||
elif filter_name == "version_tags":
|
||||
version_tags = self._search_bar.get_filter_value("version_tags")
|
||||
self._products_widget.set_version_tags_filter(version_tags)
|
||||
|
||||
elif filter_name == "task_tags":
|
||||
task_tags = self._search_bar.get_filter_value("task_tags")
|
||||
self._products_widget.set_task_tags_filter(task_tags)
|
||||
|
||||
def _on_tasks_selection_change(self, event):
|
||||
self._products_widget.set_tasks_filter(event["task_ids"])
|
||||
|
||||
def _on_status_filter_change(self):
|
||||
status_names = self._product_status_filter_combo.get_value()
|
||||
self._products_widget.set_statuses_filter(status_names)
|
||||
|
||||
def _on_product_type_filter_change(self):
|
||||
product_types = self._product_types_filter_combo.get_value()
|
||||
self._products_widget.set_product_type_filter(product_types)
|
||||
|
||||
def _on_merged_products_selection_change(self):
|
||||
items = self._products_widget.get_selected_merged_products()
|
||||
self._folders_widget.set_merged_products_selection(items)
|
||||
|
|
@ -480,6 +482,7 @@ class LoaderWindow(QtWidgets.QWidget):
|
|||
self._projects_combobox.set_current_context_project(project_name)
|
||||
if not self._refresh_handler.project_refreshed:
|
||||
self._projects_combobox.refresh()
|
||||
self._update_filters()
|
||||
|
||||
def _on_load_finished(self, event):
|
||||
error_info = event["error_info"]
|
||||
|
|
@ -491,6 +494,124 @@ class LoaderWindow(QtWidgets.QWidget):
|
|||
|
||||
def _on_project_selection_changed(self, event):
|
||||
self._selected_project_name = event["project_name"]
|
||||
self._update_filters()
|
||||
|
||||
def _update_filters(self):
|
||||
project_name = self._selected_project_name
|
||||
if not project_name:
|
||||
self._search_bar.set_search_items([])
|
||||
return
|
||||
|
||||
product_type_items: list[ProductTypeItem] = (
|
||||
self._controller.get_product_type_items(project_name)
|
||||
)
|
||||
status_items: list[StatusItem] = (
|
||||
self._controller.get_project_status_items(project_name)
|
||||
)
|
||||
tags_by_entity_type = (
|
||||
self._controller.get_available_tags_by_entity_type(project_name)
|
||||
)
|
||||
tag_items = self._controller.get_project_anatomy_tags(project_name)
|
||||
tag_color_by_name = {
|
||||
tag_item.name: tag_item.color
|
||||
for tag_item in tag_items
|
||||
}
|
||||
|
||||
filter_product_type_items = [
|
||||
{
|
||||
"value": item.name,
|
||||
"icon": item.icon,
|
||||
}
|
||||
for item in product_type_items
|
||||
]
|
||||
filter_status_items = [
|
||||
{
|
||||
"icon": {
|
||||
"type": "material-symbols",
|
||||
"name": status_item.icon,
|
||||
"color": status_item.color
|
||||
},
|
||||
"color": status_item.color,
|
||||
"value": status_item.name,
|
||||
}
|
||||
for status_item in status_items
|
||||
]
|
||||
version_tags = [
|
||||
{
|
||||
"value": tag_name,
|
||||
"color": tag_color_by_name.get(tag_name),
|
||||
}
|
||||
for tag_name in tags_by_entity_type.get("versions") or []
|
||||
]
|
||||
task_tags = [
|
||||
{
|
||||
"value": tag_name,
|
||||
"color": tag_color_by_name.get(tag_name),
|
||||
}
|
||||
for tag_name in tags_by_entity_type.get("tasks") or []
|
||||
]
|
||||
|
||||
self._search_bar.set_search_items([
|
||||
FilterDefinition(
|
||||
name="product_name",
|
||||
title="Product name",
|
||||
filter_type="text",
|
||||
icon=None,
|
||||
placeholder="Product name filter...",
|
||||
items=None,
|
||||
),
|
||||
FilterDefinition(
|
||||
name="product_types",
|
||||
title="Product type",
|
||||
filter_type="list",
|
||||
icon=None,
|
||||
items=filter_product_type_items,
|
||||
),
|
||||
FilterDefinition(
|
||||
name="statuses",
|
||||
title="Statuses",
|
||||
filter_type="list",
|
||||
icon=None,
|
||||
items=filter_status_items,
|
||||
),
|
||||
FilterDefinition(
|
||||
name="version_tags",
|
||||
title="Version tags",
|
||||
filter_type="list",
|
||||
icon=None,
|
||||
items=version_tags,
|
||||
),
|
||||
FilterDefinition(
|
||||
name="task_tags",
|
||||
title="Task tags",
|
||||
filter_type="list",
|
||||
icon=None,
|
||||
items=task_tags,
|
||||
),
|
||||
])
|
||||
|
||||
# Set product types filter from settings
|
||||
if self._set_product_type_filters:
|
||||
self._set_product_type_filters = False
|
||||
product_types_filter = self._controller.get_product_types_filter()
|
||||
product_types = []
|
||||
for item in filter_product_type_items:
|
||||
product_type = item["value"]
|
||||
matching = (
|
||||
int(product_type in product_types_filter.product_types)
|
||||
+ int(product_types_filter.is_allow_list)
|
||||
)
|
||||
if matching % 2 == 0:
|
||||
product_types.append(product_type)
|
||||
|
||||
if (
|
||||
product_types
|
||||
and len(product_types) < len(filter_product_type_items)
|
||||
):
|
||||
self._search_bar.set_filter_value(
|
||||
"product_types",
|
||||
product_types
|
||||
)
|
||||
|
||||
def _on_folders_selection_changed(self, event):
|
||||
self._selected_folder_ids = set(event["folder_ids"])
|
||||
|
|
|
|||
|
|
@ -348,8 +348,6 @@ class ScreenMarquee(QtCore.QObject):
|
|||
# QtGui.QPainter.Antialiasing
|
||||
# | QtGui.QPainter.SmoothPixmapTransform
|
||||
# )
|
||||
# if hasattr(QtGui.QPainter, "HighQualityAntialiasing"):
|
||||
# render_hints |= QtGui.QPainter.HighQualityAntialiasing
|
||||
# pix_painter.setRenderHints(render_hints)
|
||||
# for item in screen_pixes:
|
||||
# (screen_pix, offset) = item
|
||||
|
|
|
|||
|
|
@ -135,8 +135,6 @@ class ThumbnailPainterWidget(QtWidgets.QWidget):
|
|||
QtGui.QPainter.Antialiasing
|
||||
| QtGui.QPainter.SmoothPixmapTransform
|
||||
)
|
||||
if hasattr(QtGui.QPainter, "HighQualityAntialiasing"):
|
||||
render_hints |= QtGui.QPainter.HighQualityAntialiasing
|
||||
|
||||
pix_painter.setRenderHints(render_hints)
|
||||
pix_painter.drawPixmap(pos_x, pos_y, scaled_pix)
|
||||
|
|
@ -171,8 +169,6 @@ class ThumbnailPainterWidget(QtWidgets.QWidget):
|
|||
QtGui.QPainter.Antialiasing
|
||||
| QtGui.QPainter.SmoothPixmapTransform
|
||||
)
|
||||
if hasattr(QtGui.QPainter, "HighQualityAntialiasing"):
|
||||
render_hints |= QtGui.QPainter.HighQualityAntialiasing
|
||||
pix_painter.setRenderHints(render_hints)
|
||||
|
||||
tiled_rect = QtCore.QRectF(
|
||||
|
|
@ -265,8 +261,6 @@ class ThumbnailPainterWidget(QtWidgets.QWidget):
|
|||
QtGui.QPainter.Antialiasing
|
||||
| QtGui.QPainter.SmoothPixmapTransform
|
||||
)
|
||||
if hasattr(QtGui.QPainter, "HighQualityAntialiasing"):
|
||||
render_hints |= QtGui.QPainter.HighQualityAntialiasing
|
||||
|
||||
final_painter.setRenderHints(render_hints)
|
||||
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ class AlphaSlider(QtWidgets.QSlider):
|
|||
|
||||
painter.fillRect(event.rect(), QtCore.Qt.transparent)
|
||||
|
||||
painter.setRenderHint(QtGui.QPainter.HighQualityAntialiasing)
|
||||
painter.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
|
||||
horizontal = self.orientation() == QtCore.Qt.Horizontal
|
||||
|
||||
|
|
|
|||
|
|
@ -261,7 +261,7 @@ class QtColorTriangle(QtWidgets.QWidget):
|
|||
pix = self.bg_image.copy()
|
||||
pix_painter = QtGui.QPainter(pix)
|
||||
|
||||
pix_painter.setRenderHint(QtGui.QPainter.HighQualityAntialiasing)
|
||||
pix_painter.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
|
||||
trigon_path = QtGui.QPainterPath()
|
||||
trigon_path.moveTo(self.point_a)
|
||||
|
|
|
|||
|
|
@ -118,9 +118,6 @@ def paint_image_with_color(image, color):
|
|||
QtGui.QPainter.Antialiasing
|
||||
| QtGui.QPainter.SmoothPixmapTransform
|
||||
)
|
||||
# Deprecated since 5.14
|
||||
if hasattr(QtGui.QPainter, "HighQualityAntialiasing"):
|
||||
render_hints |= QtGui.QPainter.HighQualityAntialiasing
|
||||
painter.setRenderHints(render_hints)
|
||||
|
||||
painter.setClipRegion(alpha_region)
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ class NiceSlider(QtWidgets.QSlider):
|
|||
|
||||
painter.fillRect(event.rect(), QtCore.Qt.transparent)
|
||||
|
||||
painter.setRenderHint(QtGui.QPainter.HighQualityAntialiasing)
|
||||
painter.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
|
||||
horizontal = self.orientation() == QtCore.Qt.Horizontal
|
||||
|
||||
|
|
|
|||
|
|
@ -205,8 +205,6 @@ class ThumbnailPainterWidget(QtWidgets.QWidget):
|
|||
QtGui.QPainter.Antialiasing
|
||||
| QtGui.QPainter.SmoothPixmapTransform
|
||||
)
|
||||
if hasattr(QtGui.QPainter, "HighQualityAntialiasing"):
|
||||
render_hints |= QtGui.QPainter.HighQualityAntialiasing
|
||||
|
||||
pix_painter.setRenderHints(render_hints)
|
||||
pix_painter.drawPixmap(pos_x, pos_y, scaled_pix)
|
||||
|
|
@ -241,8 +239,6 @@ class ThumbnailPainterWidget(QtWidgets.QWidget):
|
|||
QtGui.QPainter.Antialiasing
|
||||
| QtGui.QPainter.SmoothPixmapTransform
|
||||
)
|
||||
if hasattr(QtGui.QPainter, "HighQualityAntialiasing"):
|
||||
render_hints |= QtGui.QPainter.HighQualityAntialiasing
|
||||
pix_painter.setRenderHints(render_hints)
|
||||
|
||||
tiled_rect = QtCore.QRectF(
|
||||
|
|
@ -335,8 +331,6 @@ class ThumbnailPainterWidget(QtWidgets.QWidget):
|
|||
QtGui.QPainter.Antialiasing
|
||||
| QtGui.QPainter.SmoothPixmapTransform
|
||||
)
|
||||
if hasattr(QtGui.QPainter, "HighQualityAntialiasing"):
|
||||
render_hints |= QtGui.QPainter.HighQualityAntialiasing
|
||||
|
||||
final_painter.setRenderHints(render_hints)
|
||||
|
||||
|
|
|
|||
|
|
@ -462,7 +462,7 @@ class BaseClickableFrame(QtWidgets.QFrame):
|
|||
Callback is defined by overriding `_mouse_release_callback`.
|
||||
"""
|
||||
def __init__(self, parent):
|
||||
super(BaseClickableFrame, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
self._mouse_pressed = False
|
||||
|
||||
|
|
@ -470,17 +470,23 @@ class BaseClickableFrame(QtWidgets.QFrame):
|
|||
pass
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
super().mousePressEvent(event)
|
||||
if event.isAccepted():
|
||||
return
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
self._mouse_pressed = True
|
||||
super(BaseClickableFrame, self).mousePressEvent(event)
|
||||
event.accept()
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if self._mouse_pressed:
|
||||
self._mouse_pressed = False
|
||||
if self.rect().contains(event.pos()):
|
||||
self._mouse_release_callback()
|
||||
pressed, self._mouse_pressed = self._mouse_pressed, False
|
||||
super().mouseReleaseEvent(event)
|
||||
if event.isAccepted():
|
||||
return
|
||||
|
||||
super(BaseClickableFrame, self).mouseReleaseEvent(event)
|
||||
accepted = pressed and self.rect().contains(event.pos())
|
||||
if accepted:
|
||||
event.accept()
|
||||
self._mouse_release_callback()
|
||||
|
||||
|
||||
class ClickableFrame(BaseClickableFrame):
|
||||
|
|
@ -624,8 +630,6 @@ class ClassicExpandBtnLabel(ExpandBtnLabel):
|
|||
QtGui.QPainter.Antialiasing
|
||||
| QtGui.QPainter.SmoothPixmapTransform
|
||||
)
|
||||
if hasattr(QtGui.QPainter, "HighQualityAntialiasing"):
|
||||
render_hints |= QtGui.QPainter.HighQualityAntialiasing
|
||||
painter.setRenderHints(render_hints)
|
||||
painter.drawPixmap(QtCore.QPoint(pos_x, pos_y), pixmap)
|
||||
painter.end()
|
||||
|
|
@ -788,8 +792,6 @@ class PixmapButtonPainter(QtWidgets.QWidget):
|
|||
QtGui.QPainter.Antialiasing
|
||||
| QtGui.QPainter.SmoothPixmapTransform
|
||||
)
|
||||
if hasattr(QtGui.QPainter, "HighQualityAntialiasing"):
|
||||
render_hints |= QtGui.QPainter.HighQualityAntialiasing
|
||||
|
||||
painter.setRenderHints(render_hints)
|
||||
if self._cached_pixmap is None:
|
||||
|
|
@ -1189,7 +1191,7 @@ class SquareButton(QtWidgets.QPushButton):
|
|||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SquareButton, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
sp = self.sizePolicy()
|
||||
sp.setVerticalPolicy(QtWidgets.QSizePolicy.Minimum)
|
||||
|
|
@ -1198,17 +1200,17 @@ class SquareButton(QtWidgets.QPushButton):
|
|||
self._ideal_width = None
|
||||
|
||||
def showEvent(self, event):
|
||||
super(SquareButton, self).showEvent(event)
|
||||
super().showEvent(event)
|
||||
self._ideal_width = self.height()
|
||||
self.updateGeometry()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super(SquareButton, self).resizeEvent(event)
|
||||
super().resizeEvent(event)
|
||||
self._ideal_width = self.height()
|
||||
self.updateGeometry()
|
||||
|
||||
def sizeHint(self):
|
||||
sh = super(SquareButton, self).sizeHint()
|
||||
sh = super().sizeHint()
|
||||
ideal_width = self._ideal_width
|
||||
if ideal_width is None:
|
||||
ideal_width = sh.height()
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ mdx-gh-links = "^0.4"
|
|||
pymdown-extensions = "^10.14.3"
|
||||
mike = "^2.1.3"
|
||||
mkdocstrings-shell = "^1.0.2"
|
||||
nxtools = "^1.6"
|
||||
|
||||
[tool.poetry.group.test.dependencies]
|
||||
attrs = "^25.0.0"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,15 @@
|
|||
from typing import Any
|
||||
|
||||
from ayon_server.addons import BaseServerAddon
|
||||
from ayon_server.actions import (
|
||||
ActionExecutor,
|
||||
ExecuteResponseModel,
|
||||
SimpleActionManifest,
|
||||
)
|
||||
try:
|
||||
from ayon_server.logging import logger
|
||||
except ImportError:
|
||||
from nxtools import logging as logger
|
||||
|
||||
from .settings import (
|
||||
CoreSettings,
|
||||
|
|
@ -26,3 +35,67 @@ class CoreAddon(BaseServerAddon):
|
|||
return await super().convert_settings_overrides(
|
||||
source_version, overrides
|
||||
)
|
||||
|
||||
async def get_simple_actions(
|
||||
self,
|
||||
project_name: str | None = None,
|
||||
variant: str = "production",
|
||||
) -> list[SimpleActionManifest]:
|
||||
"""Return a list of simple actions provided by the addon"""
|
||||
output = []
|
||||
|
||||
if project_name:
|
||||
# Add 'Create Project Folder Structure' action to folders.
|
||||
output.append(
|
||||
SimpleActionManifest(
|
||||
identifier="core.createprojectstructure",
|
||||
label="Create Project Folder Structure",
|
||||
icon={
|
||||
"type": "material-symbols",
|
||||
"name": "create_new_folder",
|
||||
},
|
||||
order=100,
|
||||
entity_type="project",
|
||||
entity_subtypes=None,
|
||||
allow_multiselection=False,
|
||||
)
|
||||
)
|
||||
|
||||
return output
|
||||
|
||||
async def execute_action(
|
||||
self,
|
||||
executor: ActionExecutor,
|
||||
) -> ExecuteResponseModel:
|
||||
"""Execute webactions."""
|
||||
|
||||
project_name = executor.context.project_name
|
||||
|
||||
if executor.identifier == "core.createprojectstructure":
|
||||
if not project_name:
|
||||
logger.error(
|
||||
f"Can't execute {executor.identifier} because"
|
||||
" of missing project name."
|
||||
)
|
||||
# Works since AYON server 1.8.3
|
||||
if hasattr(executor, "get_simple_response"):
|
||||
return await executor.get_simple_response(
|
||||
"Missing project name", success=False
|
||||
)
|
||||
return
|
||||
|
||||
args = [
|
||||
"create-project-structure", "--project", project_name,
|
||||
]
|
||||
# Works since AYON server 1.8.3
|
||||
if hasattr(executor, "get_launcher_response"):
|
||||
return await executor.get_launcher_response(args)
|
||||
|
||||
return await executor.get_launcher_action_response(args)
|
||||
|
||||
logger.debug(f"Unknown action: {executor.identifier}")
|
||||
# Works since AYON server 1.8.3
|
||||
if hasattr(executor, "get_simple_response"):
|
||||
return await executor.get_simple_response(
|
||||
"Unknown action", success=False
|
||||
)
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ class FallbackProductModel(BaseSettingsModel):
|
|||
fallback_type: str = SettingsField(
|
||||
title="Fallback config type",
|
||||
enum_resolver=_fallback_ocio_config_profile_types,
|
||||
conditionalEnum=True,
|
||||
conditional_enum=True,
|
||||
default="builtin_path",
|
||||
description=(
|
||||
"Type of config which needs to be used in case published "
|
||||
|
|
@ -162,7 +162,7 @@ class CoreImageIOConfigProfilesModel(BaseSettingsModel):
|
|||
type: str = SettingsField(
|
||||
title="Profile type",
|
||||
enum_resolver=_ocio_config_profile_types,
|
||||
conditionalEnum=True,
|
||||
conditional_enum=True,
|
||||
default="builtin_path",
|
||||
section="---",
|
||||
)
|
||||
|
|
@ -319,6 +319,10 @@ class CoreSettings(BaseSettingsModel):
|
|||
"{}",
|
||||
widget="textarea",
|
||||
title="Project folder structure",
|
||||
description=(
|
||||
"Defines project folders to create on disk"
|
||||
" for 'Create project folders' action."
|
||||
),
|
||||
section="---"
|
||||
)
|
||||
project_environments: str = SettingsField(
|
||||
|
|
|
|||
|
|
@ -340,7 +340,7 @@ class ResizeModel(BaseSettingsModel):
|
|||
title="Type",
|
||||
description="Type of resizing",
|
||||
enum_resolver=lambda: _resize_types_enum,
|
||||
conditionalEnum=True,
|
||||
conditional_enum=True,
|
||||
default="source"
|
||||
)
|
||||
|
||||
|
|
@ -373,7 +373,7 @@ class ExtractThumbnailOIIODefaultsModel(BaseSettingsModel):
|
|||
title="Type",
|
||||
description="Transcoding type",
|
||||
enum_resolver=lambda: _thumbnail_oiio_transcoding_type,
|
||||
conditionalEnum=True,
|
||||
conditional_enum=True,
|
||||
default="colorspace"
|
||||
)
|
||||
|
||||
|
|
@ -476,7 +476,7 @@ class ExtractOIIOTranscodeOutputModel(BaseSettingsModel):
|
|||
"colorspace",
|
||||
title="Transcoding type",
|
||||
enum_resolver=_extract_oiio_transcoding_type,
|
||||
conditionalEnum=True,
|
||||
conditional_enum=True,
|
||||
description=(
|
||||
"Select the transcoding type for your output, choosing either "
|
||||
"*Colorspace* or *Display&View* transform."
|
||||
|
|
|
|||
88
tests/client/ayon_core/pipeline/load/test_loaders.py
Normal file
88
tests/client/ayon_core/pipeline/load/test_loaders.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
"""Test loaders in the pipeline module."""
|
||||
|
||||
from ayon_core.pipeline.load import LoaderPlugin
|
||||
|
||||
|
||||
def test_is_compatible_loader():
|
||||
"""Test if a loader is compatible with a given representation."""
|
||||
from ayon_core.pipeline.load import is_compatible_loader
|
||||
|
||||
# Create a mock representation context
|
||||
context = {
|
||||
"loader": "test_loader",
|
||||
"representation": {"name": "test_representation"},
|
||||
}
|
||||
|
||||
# Create a mock loader plugin
|
||||
class MockLoader(LoaderPlugin):
|
||||
name = "test_loader"
|
||||
version = "1.0.0"
|
||||
|
||||
def is_compatible_loader(self, context):
|
||||
return True
|
||||
|
||||
# Check compatibility
|
||||
assert is_compatible_loader(MockLoader(), context) is True
|
||||
|
||||
|
||||
def test_complex_is_compatible_loader():
|
||||
"""Test if a loader is compatible with a complex representation."""
|
||||
from ayon_core.pipeline.load import is_compatible_loader
|
||||
|
||||
# Create a mock complex representation context
|
||||
context = {
|
||||
"loader": "complex_loader",
|
||||
"representation": {
|
||||
"name": "complex_representation",
|
||||
"extension": "exr"
|
||||
},
|
||||
"additional_data": {"key": "value"},
|
||||
"product": {
|
||||
"name": "complex_product",
|
||||
"productType": "foo",
|
||||
"productBaseType": "bar",
|
||||
},
|
||||
}
|
||||
|
||||
# Create a mock loader plugin
|
||||
class ComplexLoaderA(LoaderPlugin):
|
||||
name = "complex_loaderA"
|
||||
|
||||
# False because the loader doesn't specify any compatibility (missing
|
||||
# wildcard for product type and product base type)
|
||||
assert is_compatible_loader(ComplexLoaderA(), context) is False
|
||||
|
||||
class ComplexLoaderB(LoaderPlugin):
|
||||
name = "complex_loaderB"
|
||||
product_types = {"*"}
|
||||
representations = {"*"}
|
||||
|
||||
# True, it is compatible with any product type
|
||||
assert is_compatible_loader(ComplexLoaderB(), context) is True
|
||||
|
||||
class ComplexLoaderC(LoaderPlugin):
|
||||
name = "complex_loaderC"
|
||||
product_base_types = {"*"}
|
||||
representations = {"*"}
|
||||
|
||||
# True, it is compatible with any product base type
|
||||
assert is_compatible_loader(ComplexLoaderC(), context) is True
|
||||
|
||||
class ComplexLoaderD(LoaderPlugin):
|
||||
name = "complex_loaderD"
|
||||
product_types = {"foo"}
|
||||
representations = {"*"}
|
||||
|
||||
# legacy loader defining compatibility only with product type
|
||||
# is compatible provided the same product type is defined in context
|
||||
assert is_compatible_loader(ComplexLoaderD(), context) is False
|
||||
|
||||
class ComplexLoaderE(LoaderPlugin):
|
||||
name = "complex_loaderE"
|
||||
product_types = {"foo"}
|
||||
representations = {"*"}
|
||||
|
||||
# remove productBaseType from context to simulate legacy behavior
|
||||
context["product"].pop("productBaseType", None)
|
||||
|
||||
assert is_compatible_loader(ComplexLoaderE(), context) is True
|
||||
Loading…
Add table
Add a link
Reference in a new issue