mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 12:54:40 +01:00
Merge branch 'develop' into enhancement/1309-loader-tool-add-tags-filtering
# Conflicts: # client/ayon_core/tools/common_models/projects.py # client/ayon_core/tools/loader/abstract.py # client/ayon_core/tools/loader/ui/products_model.py
This commit is contained in:
commit
ee761d074a
28 changed files with 1192 additions and 414 deletions
|
|
@ -32,8 +32,8 @@ class GlobalHostDataHook(PreLaunchHook):
|
|||
"app": app,
|
||||
|
||||
"project_entity": self.data["project_entity"],
|
||||
"folder_entity": self.data["folder_entity"],
|
||||
"task_entity": self.data["task_entity"],
|
||||
"folder_entity": self.data.get("folder_entity"),
|
||||
"task_entity": self.data.get("task_entity"),
|
||||
|
||||
"anatomy": self.data["anatomy"],
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,11 @@ from .plugins import (
|
|||
deregister_loader_plugin_path,
|
||||
register_loader_plugin_path,
|
||||
deregister_loader_plugin,
|
||||
|
||||
register_loader_hook_plugin,
|
||||
deregister_loader_hook_plugin,
|
||||
register_loader_hook_plugin_path,
|
||||
deregister_loader_hook_plugin_path,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -103,4 +108,10 @@ __all__ = (
|
|||
"deregister_loader_plugin_path",
|
||||
"register_loader_plugin_path",
|
||||
"deregister_loader_plugin",
|
||||
|
||||
"register_loader_hook_plugin",
|
||||
"deregister_loader_hook_plugin",
|
||||
"register_loader_hook_plugin_path",
|
||||
"deregister_loader_hook_plugin_path",
|
||||
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,21 +1,28 @@
|
|||
import os
|
||||
import logging
|
||||
"""Plugins for loading representations and products into host applications."""
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Optional, Type
|
||||
|
||||
from ayon_core.settings import get_project_settings
|
||||
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
|
||||
|
|
@ -58,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
|
||||
|
|
@ -76,7 +83,6 @@ class LoaderPlugin(list):
|
|||
Returns:
|
||||
bool: Representation has valid extension
|
||||
"""
|
||||
|
||||
if "*" in cls.extensions:
|
||||
return True
|
||||
|
||||
|
|
@ -121,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
|
||||
|
|
@ -140,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):
|
||||
|
|
@ -205,19 +244,17 @@ 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
|
||||
Returns:
|
||||
(list)
|
||||
Args:
|
||||
contexts (list): of repre or product contexts
|
||||
Returns:
|
||||
(list)
|
||||
"""
|
||||
return cls.options or []
|
||||
|
||||
|
|
@ -251,28 +288,152 @@ class ProductLoaderPlugin(LoaderPlugin):
|
|||
"""
|
||||
|
||||
|
||||
class LoaderHookPlugin:
|
||||
"""Plugin that runs before and post specific Loader in 'loaders'
|
||||
|
||||
Should be used as non-invasive method to enrich core loading process.
|
||||
Any studio might want to modify loaded data before or after
|
||||
they are loaded without need to override existing core plugins.
|
||||
|
||||
The post methods are called after the loader's methods and receive the
|
||||
return value of the loader's method as `result` argument.
|
||||
"""
|
||||
order = 0
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def is_compatible(cls, Loader: Type[LoaderPlugin]) -> bool:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def pre_load(
|
||||
self,
|
||||
plugin: LoaderPlugin,
|
||||
context: dict,
|
||||
name: Optional[str],
|
||||
namespace: Optional[str],
|
||||
options: Optional[dict],
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def post_load(
|
||||
self,
|
||||
plugin: LoaderPlugin,
|
||||
result: Any,
|
||||
context: dict,
|
||||
name: Optional[str],
|
||||
namespace: Optional[str],
|
||||
options: Optional[dict],
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def pre_update(
|
||||
self,
|
||||
plugin: LoaderPlugin,
|
||||
container: dict, # (ayon:container-3.0)
|
||||
context: dict,
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def post_update(
|
||||
self,
|
||||
plugin: LoaderPlugin,
|
||||
result: Any,
|
||||
container: dict, # (ayon:container-3.0)
|
||||
context: dict,
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def pre_remove(
|
||||
self,
|
||||
plugin: LoaderPlugin,
|
||||
container: dict, # (ayon:container-3.0)
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def post_remove(
|
||||
self,
|
||||
plugin: LoaderPlugin,
|
||||
result: Any,
|
||||
container: dict, # (ayon:container-3.0)
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
def discover_loader_plugins(project_name=None):
|
||||
from ayon_core.lib import Logger
|
||||
from ayon_core.pipeline import get_current_project_name
|
||||
|
||||
log = Logger.get_logger("LoaderDiscover")
|
||||
plugins = discover(LoaderPlugin)
|
||||
if not project_name:
|
||||
project_name = get_current_project_name()
|
||||
project_settings = get_project_settings(project_name)
|
||||
plugins = discover(LoaderPlugin)
|
||||
hooks = discover(LoaderHookPlugin)
|
||||
sorted_hooks = sorted(hooks, key=lambda hook: hook.order)
|
||||
for plugin in plugins:
|
||||
try:
|
||||
plugin.apply_settings(project_settings)
|
||||
except Exception:
|
||||
log.warning(
|
||||
"Failed to apply settings to loader {}".format(
|
||||
plugin.__name__
|
||||
),
|
||||
f"Failed to apply settings to loader {plugin.__name__}",
|
||||
exc_info=True
|
||||
)
|
||||
compatible_hooks = []
|
||||
for hook_cls in sorted_hooks:
|
||||
if hook_cls.is_compatible(plugin):
|
||||
compatible_hooks.append(hook_cls)
|
||||
add_hooks_to_loader(plugin, compatible_hooks)
|
||||
return plugins
|
||||
|
||||
|
||||
def add_hooks_to_loader(
|
||||
loader_class: LoaderPlugin, compatible_hooks: list[Type[LoaderHookPlugin]]
|
||||
) -> None:
|
||||
"""Monkey patch method replacing Loader.load|update|remove methods
|
||||
|
||||
It wraps applicable loaders with pre/post hooks. Discovery is called only
|
||||
once per loaders discovery.
|
||||
"""
|
||||
loader_class._load_hooks = compatible_hooks
|
||||
|
||||
def wrap_method(method_name: str):
|
||||
original_method = getattr(loader_class, method_name)
|
||||
|
||||
def wrapped_method(self, *args, **kwargs):
|
||||
# Call pre_<method_name> on all hooks
|
||||
pre_hook_name = f"pre_{method_name}"
|
||||
|
||||
hooks: list[LoaderHookPlugin] = []
|
||||
for cls in loader_class._load_hooks:
|
||||
hook = cls() # Instantiate the hook
|
||||
hooks.append(hook)
|
||||
pre_hook = getattr(hook, pre_hook_name, None)
|
||||
if callable(pre_hook):
|
||||
pre_hook(self, *args, **kwargs)
|
||||
# Call original method
|
||||
result = original_method(self, *args, **kwargs)
|
||||
# Call post_<method_name> on all hooks
|
||||
post_hook_name = f"post_{method_name}"
|
||||
for hook in hooks:
|
||||
post_hook = getattr(hook, post_hook_name, None)
|
||||
if callable(post_hook):
|
||||
post_hook(self, result, *args, **kwargs)
|
||||
|
||||
return result
|
||||
|
||||
setattr(loader_class, method_name, wrapped_method)
|
||||
|
||||
for method in ("load", "update", "remove"):
|
||||
if hasattr(loader_class, method):
|
||||
wrap_method(method)
|
||||
|
||||
|
||||
def register_loader_plugin(plugin):
|
||||
return register_plugin(LoaderPlugin, plugin)
|
||||
|
||||
|
|
@ -287,3 +448,19 @@ def deregister_loader_plugin_path(path):
|
|||
|
||||
def register_loader_plugin_path(path):
|
||||
return register_plugin_path(LoaderPlugin, path)
|
||||
|
||||
|
||||
def register_loader_hook_plugin(plugin):
|
||||
return register_plugin(LoaderHookPlugin, plugin)
|
||||
|
||||
|
||||
def deregister_loader_hook_plugin(plugin):
|
||||
deregister_plugin(LoaderHookPlugin, plugin)
|
||||
|
||||
|
||||
def register_loader_hook_plugin_path(path):
|
||||
return register_plugin_path(LoaderHookPlugin, path)
|
||||
|
||||
|
||||
def deregister_loader_hook_plugin_path(path):
|
||||
deregister_plugin_path(LoaderHookPlugin, path)
|
||||
|
|
|
|||
|
|
@ -288,7 +288,12 @@ def get_representation_context(project_name, representation):
|
|||
|
||||
|
||||
def load_with_repre_context(
|
||||
Loader, repre_context, namespace=None, name=None, options=None, **kwargs
|
||||
Loader,
|
||||
repre_context,
|
||||
namespace=None,
|
||||
name=None,
|
||||
options=None,
|
||||
**kwargs
|
||||
):
|
||||
|
||||
# Ensure the Loader is compatible for the representation
|
||||
|
|
@ -320,7 +325,12 @@ def load_with_repre_context(
|
|||
|
||||
|
||||
def load_with_product_context(
|
||||
Loader, product_context, namespace=None, name=None, options=None, **kwargs
|
||||
Loader,
|
||||
product_context,
|
||||
namespace=None,
|
||||
name=None,
|
||||
options=None,
|
||||
**kwargs
|
||||
):
|
||||
|
||||
# Ensure options is a dictionary when no explicit options provided
|
||||
|
|
@ -343,7 +353,12 @@ def load_with_product_context(
|
|||
|
||||
|
||||
def load_with_product_contexts(
|
||||
Loader, product_contexts, namespace=None, name=None, options=None, **kwargs
|
||||
Loader,
|
||||
product_contexts,
|
||||
namespace=None,
|
||||
name=None,
|
||||
options=None,
|
||||
**kwargs
|
||||
):
|
||||
|
||||
# Ensure options is a dictionary when no explicit options provided
|
||||
|
|
@ -553,15 +568,20 @@ def update_container(container, version=-1):
|
|||
return Loader().update(container, context)
|
||||
|
||||
|
||||
def switch_container(container, representation, loader_plugin=None):
|
||||
def switch_container(
|
||||
container,
|
||||
representation,
|
||||
loader_plugin=None,
|
||||
):
|
||||
"""Switch a container to representation
|
||||
|
||||
Args:
|
||||
container (dict): container information
|
||||
representation (dict): representation entity
|
||||
loader_plugin (LoaderPlugin)
|
||||
|
||||
Returns:
|
||||
function call
|
||||
return from function call
|
||||
"""
|
||||
from ayon_core.pipeline import get_current_project_name
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ targeted by task types and names.
|
|||
|
||||
Placeholders are created using placeholder plugins which should care about
|
||||
logic and data of placeholder items. 'PlaceholderItem' is used to keep track
|
||||
about it's progress.
|
||||
about its progress.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
|
@ -17,6 +17,7 @@ import collections
|
|||
import copy
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
import ayon_api
|
||||
from ayon_api import (
|
||||
get_folders,
|
||||
get_folder_by_path,
|
||||
|
|
@ -60,6 +61,32 @@ from ayon_core.pipeline.create import (
|
|||
_NOT_SET = object()
|
||||
|
||||
|
||||
class EntityResolutionError(Exception):
|
||||
"""Exception raised when entity URI resolution fails."""
|
||||
|
||||
|
||||
def resolve_entity_uri(entity_uri: str) -> str:
|
||||
"""Resolve AYON entity URI to a filesystem path for local system."""
|
||||
response = ayon_api.post(
|
||||
"resolve",
|
||||
resolveRoots=True,
|
||||
uris=[entity_uri]
|
||||
)
|
||||
if response.status_code != 200:
|
||||
raise RuntimeError(
|
||||
f"Unable to resolve AYON entity URI filepath for "
|
||||
f"'{entity_uri}': {response.text}"
|
||||
)
|
||||
|
||||
entities = response.data[0]["entities"]
|
||||
if len(entities) != 1:
|
||||
raise EntityResolutionError(
|
||||
f"Unable to resolve AYON entity URI '{entity_uri}' to a "
|
||||
f"single filepath. Received data: {response.data}"
|
||||
)
|
||||
return entities[0]["filePath"]
|
||||
|
||||
|
||||
class TemplateNotFound(Exception):
|
||||
"""Exception raised when template does not exist."""
|
||||
pass
|
||||
|
|
@ -823,7 +850,6 @@ class AbstractTemplateBuilder(ABC):
|
|||
"""
|
||||
|
||||
host_name = self.host_name
|
||||
project_name = self.project_name
|
||||
task_name = self.current_task_name
|
||||
task_type = self.current_task_type
|
||||
|
||||
|
|
@ -835,7 +861,6 @@ class AbstractTemplateBuilder(ABC):
|
|||
"task_names": task_name
|
||||
}
|
||||
)
|
||||
|
||||
if not profile:
|
||||
raise TemplateProfileNotFound((
|
||||
"No matching profile found for task '{}' of type '{}' "
|
||||
|
|
@ -843,6 +868,22 @@ class AbstractTemplateBuilder(ABC):
|
|||
).format(task_name, task_type, host_name))
|
||||
|
||||
path = profile["path"]
|
||||
if not path:
|
||||
raise TemplateLoadFailed((
|
||||
"Template path is not set.\n"
|
||||
"Path need to be set in {}\\Template Workfile Build "
|
||||
"Settings\\Profiles"
|
||||
).format(host_name.title()))
|
||||
|
||||
resolved_path = self.resolve_template_path(path)
|
||||
if not resolved_path or not os.path.exists(resolved_path):
|
||||
raise TemplateNotFound(
|
||||
"Template file found in AYON settings for task '{}' with host "
|
||||
"'{}' does not exists. (Not found : {})".format(
|
||||
task_name, host_name, resolved_path)
|
||||
)
|
||||
|
||||
self.log.info(f"Found template at: '{resolved_path}'")
|
||||
|
||||
# switch to remove placeholders after they are used
|
||||
keep_placeholder = profile.get("keep_placeholder")
|
||||
|
|
@ -852,44 +893,86 @@ class AbstractTemplateBuilder(ABC):
|
|||
if keep_placeholder is None:
|
||||
keep_placeholder = True
|
||||
|
||||
if not path:
|
||||
raise TemplateLoadFailed((
|
||||
"Template path is not set.\n"
|
||||
"Path need to be set in {}\\Template Workfile Build "
|
||||
"Settings\\Profiles"
|
||||
).format(host_name.title()))
|
||||
|
||||
# Try to fill path with environments and anatomy roots
|
||||
anatomy = Anatomy(project_name)
|
||||
fill_data = {
|
||||
key: value
|
||||
for key, value in os.environ.items()
|
||||
return {
|
||||
"path": resolved_path,
|
||||
"keep_placeholder": keep_placeholder,
|
||||
"create_first_version": create_first_version
|
||||
}
|
||||
|
||||
fill_data["root"] = anatomy.roots
|
||||
fill_data["project"] = {
|
||||
"name": project_name,
|
||||
"code": anatomy.project_code,
|
||||
}
|
||||
def resolve_template_path(self, path, fill_data=None) -> str:
|
||||
"""Resolve the template path.
|
||||
|
||||
path = self.resolve_template_path(path, fill_data)
|
||||
By default, this:
|
||||
- Resolves AYON entity URI to a filesystem path
|
||||
- Returns path directly if it exists on disk.
|
||||
- Resolves template keys through anatomy and environment variables.
|
||||
|
||||
This can be overridden in host integrations to perform additional
|
||||
resolving over the template. Like, `hou.text.expandString` in Houdini.
|
||||
It's recommended to still call the super().resolve_template_path()
|
||||
to ensure the basic resolving is done across all integrations.
|
||||
|
||||
Arguments:
|
||||
path (str): The input path.
|
||||
fill_data (dict[str, str]): Deprecated. This is computed inside
|
||||
the method using the current environment and project settings.
|
||||
Used to be the data to use for template formatting.
|
||||
|
||||
Returns:
|
||||
str: The resolved path.
|
||||
|
||||
"""
|
||||
|
||||
# If the path is an AYON entity URI, then resolve the filepath
|
||||
# through the backend
|
||||
if path.startswith("ayon+entity://") or path.startswith("ayon://"):
|
||||
# This is a special case where the path is an AYON entity URI
|
||||
# We need to resolve it to a filesystem path
|
||||
resolved_path = resolve_entity_uri(path)
|
||||
return resolved_path
|
||||
|
||||
# If the path is set and it's found on disk, return it directly
|
||||
if path and os.path.exists(path):
|
||||
self.log.info("Found template at: '{}'".format(path))
|
||||
return {
|
||||
"path": path,
|
||||
"keep_placeholder": keep_placeholder,
|
||||
"create_first_version": create_first_version
|
||||
return path
|
||||
|
||||
# We may have path for another platform, like C:/path/to/file
|
||||
# or a path with template keys, like {project[code]} or both.
|
||||
# Try to fill path with environments and anatomy roots
|
||||
project_name = self.project_name
|
||||
anatomy = Anatomy(project_name)
|
||||
|
||||
# Simple check whether the path contains any template keys
|
||||
if "{" in path:
|
||||
fill_data = {
|
||||
key: value
|
||||
for key, value in os.environ.items()
|
||||
}
|
||||
fill_data["root"] = anatomy.roots
|
||||
fill_data["project"] = {
|
||||
"name": project_name,
|
||||
"code": anatomy.project_code,
|
||||
}
|
||||
|
||||
solved_path = None
|
||||
# Format the template using local fill data
|
||||
result = StringTemplate.format_template(path, fill_data)
|
||||
if not result.solved:
|
||||
return path
|
||||
|
||||
path = result.normalized()
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
|
||||
# If the path were set in settings using a Windows path and we
|
||||
# are now on a Linux system, we try to convert the solved path to
|
||||
# the current platform.
|
||||
while True:
|
||||
try:
|
||||
solved_path = anatomy.path_remapper(path)
|
||||
except KeyError as missing_key:
|
||||
raise KeyError(
|
||||
"Could not solve key '{}' in template path '{}'".format(
|
||||
missing_key, path))
|
||||
f"Could not solve key '{missing_key}'"
|
||||
f" in template path '{path}'"
|
||||
)
|
||||
|
||||
if solved_path is None:
|
||||
solved_path = path
|
||||
|
|
@ -898,40 +981,7 @@ class AbstractTemplateBuilder(ABC):
|
|||
path = solved_path
|
||||
|
||||
solved_path = os.path.normpath(solved_path)
|
||||
if not os.path.exists(solved_path):
|
||||
raise TemplateNotFound(
|
||||
"Template found in AYON settings for task '{}' with host "
|
||||
"'{}' does not exists. (Not found : {})".format(
|
||||
task_name, host_name, solved_path))
|
||||
|
||||
self.log.info("Found template at: '{}'".format(solved_path))
|
||||
|
||||
return {
|
||||
"path": solved_path,
|
||||
"keep_placeholder": keep_placeholder,
|
||||
"create_first_version": create_first_version
|
||||
}
|
||||
|
||||
def resolve_template_path(self, path, fill_data) -> str:
|
||||
"""Resolve the template path.
|
||||
|
||||
By default, this does nothing except returning the path directly.
|
||||
|
||||
This can be overridden in host integrations to perform additional
|
||||
resolving over the template. Like, `hou.text.expandString` in Houdini.
|
||||
|
||||
Arguments:
|
||||
path (str): The input path.
|
||||
fill_data (dict[str, str]): Data to use for template formatting.
|
||||
|
||||
Returns:
|
||||
str: The resolved path.
|
||||
|
||||
"""
|
||||
result = StringTemplate.format_template(path, fill_data)
|
||||
if result.solved:
|
||||
path = result.normalized()
|
||||
return path
|
||||
return solved_path
|
||||
|
||||
def emit_event(self, topic, data=None, source=None) -> Event:
|
||||
return self._event_system.emit(topic, data, source)
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ class ExtractOIIOTranscode(publish.Extractor):
|
|||
optional = True
|
||||
|
||||
# Supported extensions
|
||||
supported_exts = ["exr", "jpg", "jpeg", "png", "dpx"]
|
||||
supported_exts = {"exr", "jpg", "jpeg", "png", "dpx"}
|
||||
|
||||
# Configurable by Settings
|
||||
profiles = None
|
||||
|
|
|
|||
|
|
@ -135,11 +135,11 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
]
|
||||
|
||||
# Supported extensions
|
||||
image_exts = ["exr", "jpg", "jpeg", "png", "dpx", "tga", "tiff", "tif"]
|
||||
video_exts = ["mov", "mp4"]
|
||||
supported_exts = image_exts + video_exts
|
||||
image_exts = {"exr", "jpg", "jpeg", "png", "dpx", "tga", "tiff", "tif"}
|
||||
video_exts = {"mov", "mp4"}
|
||||
supported_exts = image_exts | video_exts
|
||||
|
||||
alpha_exts = ["exr", "png", "dpx"]
|
||||
alpha_exts = {"exr", "png", "dpx"}
|
||||
|
||||
# Preset attributes
|
||||
profiles = []
|
||||
|
|
|
|||
|
|
@ -150,6 +150,7 @@ class TaskTypeItem:
|
|||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProjectItem:
|
||||
"""Item representing folder entity on a server.
|
||||
|
||||
|
|
@ -160,21 +161,14 @@ class ProjectItem:
|
|||
active (Union[str, None]): Parent folder id. If 'None' then project
|
||||
is parent.
|
||||
"""
|
||||
|
||||
def __init__(self, name, active, is_library, icon=None):
|
||||
self.name = name
|
||||
self.active = active
|
||||
self.is_library = is_library
|
||||
if icon is None:
|
||||
icon = {
|
||||
"type": "awesome-font",
|
||||
"name": "fa.book" if is_library else "fa.map",
|
||||
"color": get_default_entity_icon_color(),
|
||||
}
|
||||
self.icon = icon
|
||||
name: str
|
||||
active: bool
|
||||
is_library: bool
|
||||
icon: dict[str, Any]
|
||||
is_pinned: bool = False
|
||||
|
||||
@classmethod
|
||||
def from_entity(cls, project_entity):
|
||||
def from_entity(cls, project_entity: dict[str, Any]) -> "ProjectItem":
|
||||
"""Creates folder item from entity.
|
||||
|
||||
Args:
|
||||
|
|
@ -184,10 +178,16 @@ class ProjectItem:
|
|||
ProjectItem: Project item.
|
||||
|
||||
"""
|
||||
icon = {
|
||||
"type": "awesome-font",
|
||||
"name": "fa.book" if project_entity["library"] else "fa.map",
|
||||
"color": get_default_entity_icon_color(),
|
||||
}
|
||||
return cls(
|
||||
project_entity["name"],
|
||||
project_entity["active"],
|
||||
project_entity["library"],
|
||||
icon
|
||||
)
|
||||
|
||||
def to_data(self):
|
||||
|
|
@ -218,16 +218,18 @@ class ProjectItem:
|
|||
return cls(**data)
|
||||
|
||||
|
||||
def _get_project_items_from_entitiy(projects):
|
||||
def _get_project_items_from_entitiy(
|
||||
projects: list[dict[str, Any]]
|
||||
) -> list[ProjectItem]:
|
||||
"""
|
||||
|
||||
Args:
|
||||
projects (list[dict[str, Any]]): List of projects.
|
||||
|
||||
Returns:
|
||||
ProjectItem: Project item.
|
||||
"""
|
||||
list[ProjectItem]: Project item.
|
||||
|
||||
"""
|
||||
return [
|
||||
ProjectItem.from_entity(project)
|
||||
for project in projects
|
||||
|
|
@ -454,9 +456,20 @@ class ProjectsModel(object):
|
|||
self._projects_cache.update_data(project_items)
|
||||
return self._projects_cache.get_data()
|
||||
|
||||
def _query_projects(self):
|
||||
def _query_projects(self) -> list[ProjectItem]:
|
||||
projects = ayon_api.get_projects(fields=["name", "active", "library"])
|
||||
return _get_project_items_from_entitiy(projects)
|
||||
user = ayon_api.get_user()
|
||||
pinned_projects = (
|
||||
user
|
||||
.get("data", {})
|
||||
.get("frontendPreferences", {})
|
||||
.get("pinnedProjects")
|
||||
) or []
|
||||
pinned_projects = set(pinned_projects)
|
||||
project_items = _get_project_items_from_entitiy(list(projects))
|
||||
for project in project_items:
|
||||
project.is_pinned = project.name in pinned_projects
|
||||
return project_items
|
||||
|
||||
def _status_items_getter(self, project_entity):
|
||||
if not project_entity:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from ayon_core.lib import Logger, get_ayon_username
|
||||
from ayon_core.lib.events import QueuedEventSystem
|
||||
from ayon_core.settings import get_project_settings
|
||||
from ayon_core.settings import get_project_settings, get_studio_settings
|
||||
from ayon_core.tools.common_models import ProjectsModel, HierarchyModel
|
||||
|
||||
from .abstract import AbstractLauncherFrontEnd, AbstractLauncherBackend
|
||||
|
|
@ -85,7 +85,10 @@ class BaseLauncherController(
|
|||
def get_project_settings(self, project_name):
|
||||
if project_name in self._project_settings:
|
||||
return self._project_settings[project_name]
|
||||
settings = get_project_settings(project_name)
|
||||
if project_name:
|
||||
settings = get_project_settings(project_name)
|
||||
else:
|
||||
settings = get_studio_settings()
|
||||
self._project_settings[project_name] = settings
|
||||
return settings
|
||||
|
||||
|
|
|
|||
|
|
@ -1,154 +0,0 @@
|
|||
from qtpy import QtWidgets, QtCore
|
||||
|
||||
from ayon_core.tools.flickcharm import FlickCharm
|
||||
from ayon_core.tools.utils import (
|
||||
PlaceholderLineEdit,
|
||||
RefreshButton,
|
||||
ProjectsQtModel,
|
||||
ProjectSortFilterProxy,
|
||||
)
|
||||
from ayon_core.tools.common_models import PROJECTS_MODEL_SENDER
|
||||
|
||||
|
||||
class ProjectIconView(QtWidgets.QListView):
|
||||
"""Styled ListView that allows to toggle between icon and list mode.
|
||||
|
||||
Toggling between the two modes is done by Right Mouse Click.
|
||||
"""
|
||||
|
||||
IconMode = 0
|
||||
ListMode = 1
|
||||
|
||||
def __init__(self, parent=None, mode=ListMode):
|
||||
super(ProjectIconView, self).__init__(parent=parent)
|
||||
|
||||
# Workaround for scrolling being super slow or fast when
|
||||
# toggling between the two visual modes
|
||||
self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
|
||||
self.setObjectName("IconView")
|
||||
|
||||
self._mode = None
|
||||
self.set_mode(mode)
|
||||
|
||||
def set_mode(self, mode):
|
||||
if mode == self._mode:
|
||||
return
|
||||
|
||||
self._mode = mode
|
||||
|
||||
if mode == self.IconMode:
|
||||
self.setViewMode(QtWidgets.QListView.IconMode)
|
||||
self.setResizeMode(QtWidgets.QListView.Adjust)
|
||||
self.setWrapping(True)
|
||||
self.setWordWrap(True)
|
||||
self.setGridSize(QtCore.QSize(151, 90))
|
||||
self.setIconSize(QtCore.QSize(50, 50))
|
||||
self.setSpacing(0)
|
||||
self.setAlternatingRowColors(False)
|
||||
|
||||
self.setProperty("mode", "icon")
|
||||
self.style().polish(self)
|
||||
|
||||
self.verticalScrollBar().setSingleStep(30)
|
||||
|
||||
elif self.ListMode:
|
||||
self.setProperty("mode", "list")
|
||||
self.style().polish(self)
|
||||
|
||||
self.setViewMode(QtWidgets.QListView.ListMode)
|
||||
self.setResizeMode(QtWidgets.QListView.Adjust)
|
||||
self.setWrapping(False)
|
||||
self.setWordWrap(False)
|
||||
self.setIconSize(QtCore.QSize(20, 20))
|
||||
self.setGridSize(QtCore.QSize(100, 25))
|
||||
self.setSpacing(0)
|
||||
self.setAlternatingRowColors(False)
|
||||
|
||||
self.verticalScrollBar().setSingleStep(34)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == QtCore.Qt.RightButton:
|
||||
self.set_mode(int(not self._mode))
|
||||
return super(ProjectIconView, self).mousePressEvent(event)
|
||||
|
||||
|
||||
class ProjectsWidget(QtWidgets.QWidget):
|
||||
"""Projects Page"""
|
||||
|
||||
refreshed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent=None):
|
||||
super(ProjectsWidget, self).__init__(parent=parent)
|
||||
|
||||
header_widget = QtWidgets.QWidget(self)
|
||||
|
||||
projects_filter_text = PlaceholderLineEdit(header_widget)
|
||||
projects_filter_text.setPlaceholderText("Filter projects...")
|
||||
|
||||
refresh_btn = RefreshButton(header_widget)
|
||||
|
||||
header_layout = QtWidgets.QHBoxLayout(header_widget)
|
||||
header_layout.setContentsMargins(0, 0, 0, 0)
|
||||
header_layout.addWidget(projects_filter_text, 1)
|
||||
header_layout.addWidget(refresh_btn, 0)
|
||||
|
||||
projects_view = ProjectIconView(parent=self)
|
||||
projects_view.setSelectionMode(QtWidgets.QListView.NoSelection)
|
||||
flick = FlickCharm(parent=self)
|
||||
flick.activateOn(projects_view)
|
||||
projects_model = ProjectsQtModel(controller)
|
||||
projects_proxy_model = ProjectSortFilterProxy()
|
||||
projects_proxy_model.setSourceModel(projects_model)
|
||||
|
||||
projects_view.setModel(projects_proxy_model)
|
||||
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.addWidget(header_widget, 0)
|
||||
main_layout.addWidget(projects_view, 1)
|
||||
|
||||
projects_view.clicked.connect(self._on_view_clicked)
|
||||
projects_model.refreshed.connect(self.refreshed)
|
||||
projects_filter_text.textChanged.connect(
|
||||
self._on_project_filter_change)
|
||||
refresh_btn.clicked.connect(self._on_refresh_clicked)
|
||||
|
||||
controller.register_event_callback(
|
||||
"projects.refresh.finished",
|
||||
self._on_projects_refresh_finished
|
||||
)
|
||||
|
||||
self._controller = controller
|
||||
|
||||
self._projects_view = projects_view
|
||||
self._projects_model = projects_model
|
||||
self._projects_proxy_model = projects_proxy_model
|
||||
|
||||
def has_content(self):
|
||||
"""Model has at least one project.
|
||||
|
||||
Returns:
|
||||
bool: True if there is any content in the model.
|
||||
"""
|
||||
|
||||
return self._projects_model.has_content()
|
||||
|
||||
def _on_view_clicked(self, index):
|
||||
if not index.isValid():
|
||||
return
|
||||
model = index.model()
|
||||
flags = model.flags(index)
|
||||
if not flags & QtCore.Qt.ItemIsEnabled:
|
||||
return
|
||||
project_name = index.data(QtCore.Qt.DisplayRole)
|
||||
self._controller.set_selected_project(project_name)
|
||||
|
||||
def _on_project_filter_change(self, text):
|
||||
self._projects_proxy_model.setFilterFixedString(text)
|
||||
|
||||
def _on_refresh_clicked(self):
|
||||
self._controller.refresh()
|
||||
|
||||
def _on_projects_refresh_finished(self, event):
|
||||
if event["sender"] != PROJECTS_MODEL_SENDER:
|
||||
self._projects_model.refresh()
|
||||
|
|
@ -3,9 +3,13 @@ from qtpy import QtWidgets, QtCore, QtGui
|
|||
from ayon_core import style, resources
|
||||
|
||||
from ayon_core.tools.launcher.control import BaseLauncherController
|
||||
from ayon_core.tools.utils import MessageOverlayObject
|
||||
from ayon_core.tools.utils import (
|
||||
MessageOverlayObject,
|
||||
PlaceholderLineEdit,
|
||||
RefreshButton,
|
||||
ProjectsWidget,
|
||||
)
|
||||
|
||||
from .projects_widget import ProjectsWidget
|
||||
from .hierarchy_page import HierarchyPage
|
||||
from .actions_widget import ActionsWidget
|
||||
|
||||
|
|
@ -50,7 +54,25 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
pages_widget = QtWidgets.QWidget(content_body)
|
||||
|
||||
# - First page - Projects
|
||||
projects_page = ProjectsWidget(controller, pages_widget)
|
||||
projects_page = QtWidgets.QWidget(pages_widget)
|
||||
projects_header_widget = QtWidgets.QWidget(projects_page)
|
||||
|
||||
projects_filter_text = PlaceholderLineEdit(projects_header_widget)
|
||||
projects_filter_text.setPlaceholderText("Filter projects...")
|
||||
|
||||
refresh_btn = RefreshButton(projects_header_widget)
|
||||
|
||||
projects_header_layout = QtWidgets.QHBoxLayout(projects_header_widget)
|
||||
projects_header_layout.setContentsMargins(0, 0, 0, 0)
|
||||
projects_header_layout.addWidget(projects_filter_text, 1)
|
||||
projects_header_layout.addWidget(refresh_btn, 0)
|
||||
|
||||
projects_widget = ProjectsWidget(controller, pages_widget)
|
||||
|
||||
projects_layout = QtWidgets.QVBoxLayout(projects_page)
|
||||
projects_layout.setContentsMargins(0, 0, 0, 0)
|
||||
projects_layout.addWidget(projects_header_widget, 0)
|
||||
projects_layout.addWidget(projects_widget, 1)
|
||||
|
||||
# - Second page - Hierarchy (folders & tasks)
|
||||
hierarchy_page = HierarchyPage(controller, pages_widget)
|
||||
|
|
@ -102,12 +124,16 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
page_slide_anim.setEndValue(1.0)
|
||||
page_slide_anim.setEasingCurve(QtCore.QEasingCurve.OutQuad)
|
||||
|
||||
projects_page.refreshed.connect(self._on_projects_refresh)
|
||||
refresh_btn.clicked.connect(self._on_refresh_request)
|
||||
projects_widget.refreshed.connect(self._on_projects_refresh)
|
||||
|
||||
actions_refresh_timer.timeout.connect(
|
||||
self._on_actions_refresh_timeout)
|
||||
page_slide_anim.valueChanged.connect(
|
||||
self._on_page_slide_value_changed)
|
||||
page_slide_anim.finished.connect(self._on_page_slide_finished)
|
||||
projects_filter_text.textChanged.connect(
|
||||
self._on_project_filter_change)
|
||||
|
||||
controller.register_event_callback(
|
||||
"selection.project.changed",
|
||||
|
|
@ -142,6 +168,7 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
self._pages_widget = pages_widget
|
||||
self._pages_layout = pages_layout
|
||||
self._projects_page = projects_page
|
||||
self._projects_widget = projects_widget
|
||||
self._hierarchy_page = hierarchy_page
|
||||
self._actions_widget = actions_widget
|
||||
# self._action_history = action_history
|
||||
|
|
@ -194,6 +221,12 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
elif self._is_on_projects_page:
|
||||
self._go_to_hierarchy_page(project_name)
|
||||
|
||||
def _on_project_filter_change(self, text):
|
||||
self._projects_widget.set_name_filter(text)
|
||||
|
||||
def _on_refresh_request(self):
|
||||
self._controller.refresh()
|
||||
|
||||
def _on_projects_refresh(self):
|
||||
# Refresh only actions on projects page
|
||||
if self._is_on_projects_page:
|
||||
|
|
@ -201,7 +234,7 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
return
|
||||
|
||||
# No projects were found -> go back to projects page
|
||||
if not self._projects_page.has_content():
|
||||
if not self._projects_widget.has_content():
|
||||
self._go_to_projects_page()
|
||||
return
|
||||
|
||||
|
|
@ -280,6 +313,9 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
def _go_to_projects_page(self):
|
||||
if self._is_on_projects_page:
|
||||
return
|
||||
|
||||
# Deselect project in projects widget
|
||||
self._projects_widget.set_selected_project(None)
|
||||
self._is_on_projects_page = True
|
||||
self._hierarchy_page.set_page_visible(False)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
"""Abstract base classes for loader tool."""
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Iterable, Optional
|
||||
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
|
||||
|
||||
|
|
@ -18,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
|
||||
|
||||
|
|
@ -33,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.
|
||||
|
||||
|
|
@ -51,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,
|
||||
|
|
@ -127,22 +170,22 @@ class VersionItem:
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
version_id,
|
||||
version,
|
||||
is_hero,
|
||||
product_id,
|
||||
task_id,
|
||||
thumbnail_id,
|
||||
published_time,
|
||||
tags,
|
||||
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
|
||||
|
|
@ -203,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,
|
||||
|
|
@ -224,7 +267,7 @@ class VersionItem:
|
|||
}
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data):
|
||||
def from_data(cls, data: dict[str, Any]) -> VersionItem:
|
||||
return cls(**data)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -322,7 +322,6 @@ class LoaderActionsModel:
|
|||
available_loaders = self._filter_loaders_by_tool_name(
|
||||
project_name, discover_loader_plugins(project_name)
|
||||
)
|
||||
|
||||
repre_loaders = []
|
||||
product_loaders = []
|
||||
loaders_by_identifier = {}
|
||||
|
|
@ -340,6 +339,7 @@ class LoaderActionsModel:
|
|||
loaders_by_identifier_c.update_data(loaders_by_identifier)
|
||||
product_loaders_c.update_data(product_loaders)
|
||||
repre_loaders_c.update_data(repre_loaders)
|
||||
|
||||
return product_loaders, repre_loaders
|
||||
|
||||
def _get_loader_by_identifier(self, project_name, identifier):
|
||||
|
|
@ -719,7 +719,12 @@ class LoaderActionsModel:
|
|||
loader, repre_contexts, options
|
||||
)
|
||||
|
||||
def _load_representations_by_loader(self, loader, repre_contexts, options):
|
||||
def _load_representations_by_loader(
|
||||
self,
|
||||
loader,
|
||||
repre_contexts,
|
||||
options
|
||||
):
|
||||
"""Loops through list of repre_contexts and loads them with one loader
|
||||
|
||||
Args:
|
||||
|
|
@ -770,7 +775,12 @@ class LoaderActionsModel:
|
|||
))
|
||||
return error_info
|
||||
|
||||
def _load_products_by_loader(self, loader, version_contexts, options):
|
||||
def _load_products_by_loader(
|
||||
self,
|
||||
loader,
|
||||
version_contexts,
|
||||
options
|
||||
):
|
||||
"""Triggers load with ProductLoader type of loaders.
|
||||
|
||||
Warning:
|
||||
|
|
@ -796,7 +806,6 @@ class LoaderActionsModel:
|
|||
version_contexts,
|
||||
options=options
|
||||
)
|
||||
|
||||
except Exception as exc:
|
||||
formatted_traceback = None
|
||||
if not isinstance(exc, LoadError):
|
||||
|
|
|
|||
|
|
@ -1,19 +1,29 @@
|
|||
"""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 (
|
||||
IconData,
|
||||
ProductTypeItem,
|
||||
ProductBaseTypeItem,
|
||||
ProductItem,
|
||||
VersionItem,
|
||||
RepreItem,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ayon_api.typing import ProductBaseTypeDict, ProductDict, VersionDict
|
||||
|
||||
|
||||
PRODUCTS_MODEL_SENDER = "products.model"
|
||||
|
||||
|
||||
|
|
@ -72,9 +82,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,
|
||||
):
|
||||
|
|
@ -90,9 +101,21 @@ 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_icon = {
|
||||
product_type_icon = product_type_item.icon
|
||||
product_base_type_icon = product_base_type_item.icon
|
||||
product_icon: IconData = {
|
||||
"type": "awesome-font",
|
||||
"name": "fa.file-o",
|
||||
"color": get_default_entity_icon_color(),
|
||||
|
|
@ -105,9 +128,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"],
|
||||
|
|
@ -116,11 +141,12 @@ 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"]
|
||||
icon = {
|
||||
icon: IconData = {
|
||||
"type": "awesome-font",
|
||||
"name": "fa.folder",
|
||||
"color": "#0091B2",
|
||||
|
|
@ -129,8 +155,30 @@ def product_type_item_from_data(product_type_data):
|
|||
return ProductTypeItem(product_type_data["name"], icon)
|
||||
|
||||
|
||||
def create_default_product_type_item(product_type):
|
||||
icon = {
|
||||
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: IconData = {
|
||||
"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: IconData = {
|
||||
"type": "awesome-font",
|
||||
"name": "fa.folder",
|
||||
"color": "#0091B2",
|
||||
|
|
@ -138,10 +186,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: IconData = {
|
||||
"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:
|
||||
|
|
@ -163,6 +229,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(
|
||||
|
|
@ -201,6 +269,31 @@ 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 = 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.
|
||||
|
||||
|
|
@ -451,11 +544,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)
|
||||
|
|
@ -463,6 +557,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)
|
||||
|
|
@ -472,7 +571,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"]
|
||||
|
|
@ -486,6 +591,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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,33 +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
|
||||
TASK_TAGS_FILTER_ROLE = QtCore.Qt.UserRole + 33
|
||||
VERSION_TAGS_FILTER_ROLE = QtCore.Qt.UserRole + 34
|
||||
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):
|
||||
|
|
@ -51,6 +52,7 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
column_labels = [
|
||||
"Product name",
|
||||
"Product type",
|
||||
"Product base type",
|
||||
"Folder",
|
||||
"Version",
|
||||
"Status",
|
||||
|
|
@ -81,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")
|
||||
|
|
@ -95,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,
|
||||
|
|
@ -456,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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -169,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
|
||||
|
|
@ -288,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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ from .widgets import (
|
|||
from .views import (
|
||||
DeselectableTreeView,
|
||||
TreeView,
|
||||
ListView,
|
||||
)
|
||||
from .error_dialog import ErrorMessageBox
|
||||
from .lib import (
|
||||
|
|
@ -61,6 +62,7 @@ from .dialogs import (
|
|||
)
|
||||
from .projects_widget import (
|
||||
ProjectsCombobox,
|
||||
ProjectsWidget,
|
||||
ProjectsQtModel,
|
||||
ProjectSortFilterProxy,
|
||||
PROJECT_NAME_ROLE,
|
||||
|
|
@ -114,6 +116,7 @@ __all__ = (
|
|||
|
||||
"DeselectableTreeView",
|
||||
"TreeView",
|
||||
"ListView",
|
||||
|
||||
"ErrorMessageBox",
|
||||
|
||||
|
|
@ -145,6 +148,7 @@ __all__ = (
|
|||
"PopupUpdateKeys",
|
||||
|
||||
"ProjectsCombobox",
|
||||
"ProjectsWidget",
|
||||
"ProjectsQtModel",
|
||||
"ProjectSortFilterProxy",
|
||||
"PROJECT_NAME_ROLE",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,21 +1,69 @@
|
|||
from __future__ import annotations
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Callable
|
||||
import typing
|
||||
from typing import Optional
|
||||
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
||||
from ayon_core.tools.common_models import PROJECTS_MODEL_SENDER
|
||||
from ayon_core.tools.common_models import (
|
||||
ProjectItem,
|
||||
PROJECTS_MODEL_SENDER,
|
||||
)
|
||||
|
||||
from .views import ListView
|
||||
from .lib import RefreshThread, get_qt_icon
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from typing import TypedDict
|
||||
|
||||
class ExpectedProjectSelectionData(TypedDict):
|
||||
name: Optional[str]
|
||||
current: Optional[str]
|
||||
selected: Optional[str]
|
||||
|
||||
class ExpectedSelectionData(TypedDict):
|
||||
project: ExpectedProjectSelectionData
|
||||
|
||||
|
||||
PROJECT_NAME_ROLE = QtCore.Qt.UserRole + 1
|
||||
PROJECT_IS_ACTIVE_ROLE = QtCore.Qt.UserRole + 2
|
||||
PROJECT_IS_LIBRARY_ROLE = QtCore.Qt.UserRole + 3
|
||||
PROJECT_IS_CURRENT_ROLE = QtCore.Qt.UserRole + 4
|
||||
LIBRARY_PROJECT_SEPARATOR_ROLE = QtCore.Qt.UserRole + 5
|
||||
PROJECT_IS_PINNED_ROLE = QtCore.Qt.UserRole + 5
|
||||
LIBRARY_PROJECT_SEPARATOR_ROLE = QtCore.Qt.UserRole + 6
|
||||
|
||||
|
||||
class AbstractProjectController(ABC):
|
||||
@abstractmethod
|
||||
def register_event_callback(self, topic: str, callback: Callable):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_project_items(
|
||||
self, sender: Optional[str] = None
|
||||
) -> list[str]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_selected_project(self, project_name: str):
|
||||
pass
|
||||
|
||||
# These are required only if widget should handle expected selection
|
||||
@abstractmethod
|
||||
def expected_project_selected(self, project_name: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_expected_selection_data(self) -> "ExpectedSelectionData":
|
||||
pass
|
||||
|
||||
|
||||
class ProjectsQtModel(QtGui.QStandardItemModel):
|
||||
refreshed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller):
|
||||
super(ProjectsQtModel, self).__init__()
|
||||
def __init__(self, controller: AbstractProjectController):
|
||||
super().__init__()
|
||||
self._controller = controller
|
||||
|
||||
self._project_items = {}
|
||||
|
|
@ -213,7 +261,7 @@ class ProjectsQtModel(QtGui.QStandardItemModel):
|
|||
else:
|
||||
self.refreshed.emit()
|
||||
|
||||
def _fill_items(self, project_items):
|
||||
def _fill_items(self, project_items: list[ProjectItem]):
|
||||
new_project_names = {
|
||||
project_item.name
|
||||
for project_item in project_items
|
||||
|
|
@ -252,6 +300,7 @@ class ProjectsQtModel(QtGui.QStandardItemModel):
|
|||
item.setData(project_name, PROJECT_NAME_ROLE)
|
||||
item.setData(project_item.active, PROJECT_IS_ACTIVE_ROLE)
|
||||
item.setData(project_item.is_library, PROJECT_IS_LIBRARY_ROLE)
|
||||
item.setData(project_item.is_pinned, PROJECT_IS_PINNED_ROLE)
|
||||
is_current = project_name == self._current_context_project
|
||||
item.setData(is_current, PROJECT_IS_CURRENT_ROLE)
|
||||
self._project_items[project_name] = item
|
||||
|
|
@ -279,7 +328,7 @@ class ProjectsQtModel(QtGui.QStandardItemModel):
|
|||
|
||||
class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ProjectSortFilterProxy, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self._filter_inactive = True
|
||||
self._filter_standard = False
|
||||
self._filter_library = False
|
||||
|
|
@ -323,26 +372,51 @@ class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel):
|
|||
return False
|
||||
|
||||
# Library separator should be before library projects
|
||||
result = self._type_sort(left_index, right_index)
|
||||
if result is not None:
|
||||
return result
|
||||
l_is_library = left_index.data(PROJECT_IS_LIBRARY_ROLE)
|
||||
r_is_library = right_index.data(PROJECT_IS_LIBRARY_ROLE)
|
||||
l_is_sep = left_index.data(LIBRARY_PROJECT_SEPARATOR_ROLE)
|
||||
r_is_sep = right_index.data(LIBRARY_PROJECT_SEPARATOR_ROLE)
|
||||
if l_is_sep:
|
||||
return bool(r_is_library)
|
||||
|
||||
if left_index.data(PROJECT_NAME_ROLE) is None:
|
||||
if r_is_sep:
|
||||
return not l_is_library
|
||||
|
||||
# Non project items should be on top
|
||||
l_project_name = left_index.data(PROJECT_NAME_ROLE)
|
||||
r_project_name = right_index.data(PROJECT_NAME_ROLE)
|
||||
if l_project_name is None:
|
||||
return True
|
||||
|
||||
if right_index.data(PROJECT_NAME_ROLE) is None:
|
||||
if r_project_name is None:
|
||||
return False
|
||||
|
||||
left_is_active = left_index.data(PROJECT_IS_ACTIVE_ROLE)
|
||||
right_is_active = right_index.data(PROJECT_IS_ACTIVE_ROLE)
|
||||
if right_is_active == left_is_active:
|
||||
return super(ProjectSortFilterProxy, self).lessThan(
|
||||
left_index, right_index
|
||||
)
|
||||
if right_is_active != left_is_active:
|
||||
return left_is_active
|
||||
|
||||
if left_is_active:
|
||||
l_is_pinned = left_index.data(PROJECT_IS_PINNED_ROLE)
|
||||
r_is_pinned = right_index.data(PROJECT_IS_PINNED_ROLE)
|
||||
if l_is_pinned is True and not r_is_pinned:
|
||||
return True
|
||||
return False
|
||||
|
||||
if r_is_pinned is True and not l_is_pinned:
|
||||
return False
|
||||
|
||||
# Move inactive projects to the end
|
||||
left_is_active = left_index.data(PROJECT_IS_ACTIVE_ROLE)
|
||||
right_is_active = right_index.data(PROJECT_IS_ACTIVE_ROLE)
|
||||
if right_is_active != left_is_active:
|
||||
return left_is_active
|
||||
|
||||
# Move library projects after standard projects
|
||||
if (
|
||||
l_is_library is not None
|
||||
and r_is_library is not None
|
||||
and l_is_library != r_is_library
|
||||
):
|
||||
return r_is_library
|
||||
return super().lessThan(left_index, right_index)
|
||||
|
||||
def filterAcceptsRow(self, source_row, source_parent):
|
||||
index = self.sourceModel().index(source_row, 0, source_parent)
|
||||
|
|
@ -415,15 +489,153 @@ class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel):
|
|||
self.invalidate()
|
||||
|
||||
|
||||
class ProjectsDelegate(QtWidgets.QStyledItemDelegate):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._pin_icon = None
|
||||
|
||||
def paint(self, painter, option, index):
|
||||
is_pinned = index.data(PROJECT_IS_PINNED_ROLE)
|
||||
if not is_pinned:
|
||||
super().paint(painter, option, index)
|
||||
return
|
||||
opt = QtWidgets.QStyleOptionViewItem(option)
|
||||
self.initStyleOption(opt, index)
|
||||
widget = option.widget
|
||||
if widget is None:
|
||||
style = QtWidgets.QApplication.style()
|
||||
else:
|
||||
style = widget.style()
|
||||
# CE_ItemViewItem
|
||||
proxy = style.proxy()
|
||||
painter.save()
|
||||
painter.setClipRect(option.rect)
|
||||
decor_rect = proxy.subElementRect(
|
||||
QtWidgets.QStyle.SE_ItemViewItemDecoration, opt, widget
|
||||
)
|
||||
text_rect = proxy.subElementRect(
|
||||
QtWidgets.QStyle.SE_ItemViewItemText, opt, widget
|
||||
)
|
||||
proxy.drawPrimitive(
|
||||
QtWidgets.QStyle.PE_PanelItemViewItem, opt, painter, widget
|
||||
)
|
||||
mode = QtGui.QIcon.Normal
|
||||
if not opt.state & QtWidgets.QStyle.State_Enabled:
|
||||
mode = QtGui.QIcon.Disabled
|
||||
elif opt.state & QtWidgets.QStyle.State_Selected:
|
||||
mode = QtGui.QIcon.Selected
|
||||
state = QtGui.QIcon.Off
|
||||
if opt.state & QtWidgets.QStyle.State_Open:
|
||||
state = QtGui.QIcon.On
|
||||
|
||||
# Draw project icon
|
||||
opt.icon.paint(
|
||||
painter, decor_rect, opt.decorationAlignment, mode, state
|
||||
)
|
||||
|
||||
# Draw pin icon
|
||||
if index.data(PROJECT_IS_PINNED_ROLE):
|
||||
pin_icon = self._get_pin_icon()
|
||||
pin_rect = QtCore.QRect(decor_rect)
|
||||
diff = option.rect.width() - pin_rect.width()
|
||||
pin_rect.moveLeft(diff)
|
||||
pin_icon.paint(
|
||||
painter, pin_rect, opt.decorationAlignment, mode, state
|
||||
)
|
||||
|
||||
# Draw text
|
||||
if opt.text:
|
||||
if not opt.state & QtWidgets.QStyle.State_Enabled:
|
||||
cg = QtGui.QPalette.Disabled
|
||||
elif not (opt.state & QtWidgets.QStyle.State_Active):
|
||||
cg = QtGui.QPalette.Inactive
|
||||
else:
|
||||
cg = QtGui.QPalette.Normal
|
||||
|
||||
if opt.state & QtWidgets.QStyle.State_Selected:
|
||||
painter.setPen(
|
||||
opt.palette.color(cg, QtGui.QPalette.HighlightedText)
|
||||
)
|
||||
else:
|
||||
painter.setPen(opt.palette.color(cg, QtGui.QPalette.Text))
|
||||
|
||||
if opt.state & QtWidgets.QStyle.State_Editing:
|
||||
painter.setPen(opt.palette.color(cg, QtGui.QPalette.Text))
|
||||
painter.drawRect(text_rect.adjusted(0, 0, -1, -1))
|
||||
|
||||
margin = proxy.pixelMetric(
|
||||
QtWidgets.QStyle.PM_FocusFrameHMargin, None, widget
|
||||
) + 1
|
||||
text_rect.adjust(margin, 0, -margin, 0)
|
||||
# NOTE skipping some steps e.g. word wrapping and elided
|
||||
# text (adding '...' when too long).
|
||||
painter.drawText(
|
||||
text_rect,
|
||||
opt.displayAlignment,
|
||||
opt.text
|
||||
)
|
||||
|
||||
# Draw focus rect
|
||||
if opt.state & QtWidgets.QStyle.State_HasFocus:
|
||||
focus_opt = QtWidgets.QStyleOptionFocusRect()
|
||||
focus_opt.state = option.state
|
||||
focus_opt.direction = option.direction
|
||||
focus_opt.rect = option.rect
|
||||
focus_opt.fontMetrics = option.fontMetrics
|
||||
focus_opt.palette = option.palette
|
||||
|
||||
focus_opt.rect = style.subElementRect(
|
||||
QtWidgets.QCommonStyle.SE_ItemViewItemFocusRect,
|
||||
option,
|
||||
option.widget
|
||||
)
|
||||
focus_opt.state |= (
|
||||
QtWidgets.QStyle.State_KeyboardFocusChange
|
||||
| QtWidgets.QStyle.State_Item
|
||||
)
|
||||
focus_opt.backgroundColor = option.palette.color(
|
||||
(
|
||||
QtGui.QPalette.Normal
|
||||
if option.state & QtWidgets.QStyle.State_Enabled
|
||||
else QtGui.QPalette.Disabled
|
||||
),
|
||||
(
|
||||
QtGui.QPalette.Highlight
|
||||
if option.state & QtWidgets.QStyle.State_Selected
|
||||
else QtGui.QPalette.Window
|
||||
)
|
||||
)
|
||||
style.drawPrimitive(
|
||||
QtWidgets.QCommonStyle.PE_FrameFocusRect,
|
||||
focus_opt,
|
||||
painter,
|
||||
option.widget
|
||||
)
|
||||
painter.restore()
|
||||
|
||||
def _get_pin_icon(self):
|
||||
if self._pin_icon is None:
|
||||
self._pin_icon = get_qt_icon({
|
||||
"type": "material-symbols",
|
||||
"name": "keep",
|
||||
})
|
||||
return self._pin_icon
|
||||
|
||||
|
||||
class ProjectsCombobox(QtWidgets.QWidget):
|
||||
refreshed = QtCore.Signal()
|
||||
selection_changed = QtCore.Signal()
|
||||
selection_changed = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, controller, parent, handle_expected_selection=False):
|
||||
super(ProjectsCombobox, self).__init__(parent)
|
||||
def __init__(
|
||||
self,
|
||||
controller: AbstractProjectController,
|
||||
parent: QtWidgets.QWidget,
|
||||
handle_expected_selection: bool = False,
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
projects_combobox = QtWidgets.QComboBox(self)
|
||||
combobox_delegate = QtWidgets.QStyledItemDelegate(projects_combobox)
|
||||
combobox_delegate = ProjectsDelegate(projects_combobox)
|
||||
projects_combobox.setItemDelegate(combobox_delegate)
|
||||
projects_model = ProjectsQtModel(controller)
|
||||
projects_proxy_model = ProjectSortFilterProxy()
|
||||
|
|
@ -468,7 +680,7 @@ class ProjectsCombobox(QtWidgets.QWidget):
|
|||
def refresh(self):
|
||||
self._projects_model.refresh()
|
||||
|
||||
def set_selection(self, project_name):
|
||||
def set_selection(self, project_name: str):
|
||||
"""Set selection to a given project.
|
||||
|
||||
Selection change is ignored if project is not found.
|
||||
|
|
@ -480,8 +692,8 @@ class ProjectsCombobox(QtWidgets.QWidget):
|
|||
bool: True if selection was changed, False otherwise. NOTE:
|
||||
Selection may not be changed if project is not found, or if
|
||||
project is already selected.
|
||||
"""
|
||||
|
||||
"""
|
||||
idx = self._projects_combobox.findData(
|
||||
project_name, PROJECT_NAME_ROLE)
|
||||
if idx < 0:
|
||||
|
|
@ -491,7 +703,7 @@ class ProjectsCombobox(QtWidgets.QWidget):
|
|||
return True
|
||||
return False
|
||||
|
||||
def set_listen_to_selection_change(self, listen):
|
||||
def set_listen_to_selection_change(self, listen: bool):
|
||||
"""Disable listening to changes of the selection.
|
||||
|
||||
Because combobox is triggering selection change when it's model
|
||||
|
|
@ -517,11 +729,11 @@ class ProjectsCombobox(QtWidgets.QWidget):
|
|||
return None
|
||||
return self._projects_combobox.itemData(idx, PROJECT_NAME_ROLE)
|
||||
|
||||
def set_current_context_project(self, project_name):
|
||||
def set_current_context_project(self, project_name: str):
|
||||
self._projects_model.set_current_context_project(project_name)
|
||||
self._projects_proxy_model.invalidateFilter()
|
||||
|
||||
def set_select_item_visible(self, visible):
|
||||
def set_select_item_visible(self, visible: bool):
|
||||
self._select_item_visible = visible
|
||||
self._projects_model.set_select_item_visible(visible)
|
||||
self._update_select_item_visiblity()
|
||||
|
|
@ -559,7 +771,7 @@ class ProjectsCombobox(QtWidgets.QWidget):
|
|||
idx, PROJECT_NAME_ROLE)
|
||||
self._update_select_item_visiblity(project_name=project_name)
|
||||
self._controller.set_selected_project(project_name)
|
||||
self.selection_changed.emit()
|
||||
self.selection_changed.emit(project_name or "")
|
||||
|
||||
def _on_model_refresh(self):
|
||||
self._projects_proxy_model.sort(0)
|
||||
|
|
@ -614,5 +826,119 @@ class ProjectsCombobox(QtWidgets.QWidget):
|
|||
|
||||
|
||||
class ProjectsWidget(QtWidgets.QWidget):
|
||||
# TODO implement
|
||||
pass
|
||||
"""Projects widget showing projects in list.
|
||||
|
||||
Warnings:
|
||||
This widget does not support expected selection handling.
|
||||
|
||||
"""
|
||||
refreshed = QtCore.Signal()
|
||||
selection_changed = QtCore.Signal(str)
|
||||
double_clicked = QtCore.Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
controller: AbstractProjectController,
|
||||
parent: Optional[QtWidgets.QWidget] = None
|
||||
):
|
||||
super().__init__(parent=parent)
|
||||
|
||||
projects_view = ListView(parent=self)
|
||||
projects_view.setResizeMode(QtWidgets.QListView.Adjust)
|
||||
projects_view.setVerticalScrollMode(
|
||||
QtWidgets.QAbstractItemView.ScrollPerPixel
|
||||
)
|
||||
projects_view.setAlternatingRowColors(False)
|
||||
projects_view.setWrapping(False)
|
||||
projects_view.setWordWrap(False)
|
||||
projects_view.setSpacing(0)
|
||||
projects_delegate = ProjectsDelegate(projects_view)
|
||||
projects_view.setItemDelegate(projects_delegate)
|
||||
projects_view.activate_flick_charm()
|
||||
projects_view.set_deselectable(True)
|
||||
|
||||
projects_model = ProjectsQtModel(controller)
|
||||
projects_proxy_model = ProjectSortFilterProxy()
|
||||
projects_proxy_model.setSourceModel(projects_model)
|
||||
projects_view.setModel(projects_proxy_model)
|
||||
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.addWidget(projects_view, 1)
|
||||
|
||||
projects_view.selectionModel().selectionChanged.connect(
|
||||
self._on_selection_change
|
||||
)
|
||||
projects_view.double_clicked.connect(self.double_clicked)
|
||||
projects_model.refreshed.connect(self._on_model_refresh)
|
||||
|
||||
controller.register_event_callback(
|
||||
"projects.refresh.finished",
|
||||
self._on_projects_refresh_finished
|
||||
)
|
||||
|
||||
self._controller = controller
|
||||
|
||||
self._projects_view = projects_view
|
||||
self._projects_model = projects_model
|
||||
self._projects_proxy_model = projects_proxy_model
|
||||
self._projects_delegate = projects_delegate
|
||||
|
||||
def refresh(self):
|
||||
self._projects_model.refresh()
|
||||
|
||||
def has_content(self) -> bool:
|
||||
"""Model has at least one project.
|
||||
|
||||
Returns:
|
||||
bool: True if there is any content in the model.
|
||||
|
||||
"""
|
||||
return self._projects_model.has_content()
|
||||
|
||||
def set_name_filter(self, text: str):
|
||||
self._projects_proxy_model.setFilterFixedString(text)
|
||||
|
||||
def get_selected_project(self) -> Optional[str]:
|
||||
selection_model = self._projects_view.selectionModel()
|
||||
for index in selection_model.selectedIndexes():
|
||||
project_name = index.data(PROJECT_NAME_ROLE)
|
||||
if project_name:
|
||||
return project_name
|
||||
return None
|
||||
|
||||
def set_selected_project(self, project_name: Optional[str]):
|
||||
if project_name is None:
|
||||
self._projects_view.clearSelection()
|
||||
self._projects_view.setCurrentIndex(QtCore.QModelIndex())
|
||||
return
|
||||
|
||||
index = self._projects_model.get_index_by_project_name(project_name)
|
||||
if not index.isValid():
|
||||
return
|
||||
proxy_index = self._projects_proxy_model.mapFromSource(index)
|
||||
if proxy_index.isValid():
|
||||
selection_model = self._projects_view.selectionModel()
|
||||
selection_model.select(
|
||||
proxy_index,
|
||||
QtCore.QItemSelectionModel.ClearAndSelect
|
||||
)
|
||||
|
||||
def _on_model_refresh(self):
|
||||
self._projects_proxy_model.sort(0)
|
||||
self._projects_proxy_model.invalidateFilter()
|
||||
self.refreshed.emit()
|
||||
|
||||
def _on_selection_change(self, new_selection, _old_selection):
|
||||
project_name = None
|
||||
for index in new_selection.indexes():
|
||||
name = index.data(PROJECT_NAME_ROLE)
|
||||
if name:
|
||||
project_name = name
|
||||
break
|
||||
self.selection_changed.emit(project_name or "")
|
||||
self._controller.set_selected_project(project_name)
|
||||
|
||||
def _on_projects_refresh_finished(self, event):
|
||||
if event["sender"] != PROJECTS_MODEL_SENDER:
|
||||
self._projects_model.refresh()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class TreeView(QtWidgets.QTreeView):
|
|||
double_clicked = QtCore.Signal(QtGui.QMouseEvent)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TreeView, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self._deselectable = False
|
||||
|
||||
self._flick_charm_activated = False
|
||||
|
|
@ -60,12 +60,64 @@ class TreeView(QtWidgets.QTreeView):
|
|||
self.clearSelection()
|
||||
# clear the current index
|
||||
self.setCurrentIndex(QtCore.QModelIndex())
|
||||
super(TreeView, self).mousePressEvent(event)
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
self.double_clicked.emit(event)
|
||||
|
||||
return super(TreeView, self).mouseDoubleClickEvent(event)
|
||||
return super().mouseDoubleClickEvent(event)
|
||||
|
||||
def activate_flick_charm(self):
|
||||
if self._flick_charm_activated:
|
||||
return
|
||||
self._flick_charm_activated = True
|
||||
self._before_flick_scroll_mode = self.verticalScrollMode()
|
||||
self._flick_charm.activateOn(self)
|
||||
self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
|
||||
|
||||
def deactivate_flick_charm(self):
|
||||
if not self._flick_charm_activated:
|
||||
return
|
||||
self._flick_charm_activated = False
|
||||
self._flick_charm.deactivateFrom(self)
|
||||
if self._before_flick_scroll_mode is not None:
|
||||
self.setVerticalScrollMode(self._before_flick_scroll_mode)
|
||||
|
||||
|
||||
class ListView(QtWidgets.QListView):
|
||||
"""A tree view that deselects on clicking on an empty area in the view"""
|
||||
double_clicked = QtCore.Signal(QtGui.QMouseEvent)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._deselectable = False
|
||||
|
||||
self._flick_charm_activated = False
|
||||
self._flick_charm = FlickCharm(parent=self)
|
||||
self._before_flick_scroll_mode = None
|
||||
|
||||
def is_deselectable(self):
|
||||
return self._deselectable
|
||||
|
||||
def set_deselectable(self, deselectable):
|
||||
self._deselectable = deselectable
|
||||
|
||||
deselectable = property(is_deselectable, set_deselectable)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if self._deselectable:
|
||||
index = self.indexAt(event.pos())
|
||||
if not index.isValid():
|
||||
# clear the selection
|
||||
self.clearSelection()
|
||||
# clear the current index
|
||||
self.setCurrentIndex(QtCore.QModelIndex())
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
self.double_clicked.emit(event)
|
||||
|
||||
return super().mouseDoubleClickEvent(event)
|
||||
|
||||
def activate_flick_charm(self):
|
||||
if self._flick_charm_activated:
|
||||
|
|
|
|||
|
|
@ -630,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()
|
||||
|
|
@ -794,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:
|
||||
|
|
|
|||
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