mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 12:54:40 +01:00
Merge remote-tracking branch 'origin/develop' into feature/909-define-basic-trait-type-using-dataclasses
This commit is contained in:
commit
a5228b057d
38 changed files with 4294 additions and 885 deletions
|
|
@ -8,7 +8,6 @@ from pathlib import Path
|
|||
import warnings
|
||||
|
||||
import click
|
||||
import acre
|
||||
|
||||
from ayon_core import AYON_CORE_ROOT
|
||||
from ayon_core.addon import AddonsManager
|
||||
|
|
@ -18,6 +17,11 @@ from ayon_core.lib import (
|
|||
is_running_from_build,
|
||||
Logger,
|
||||
)
|
||||
from ayon_core.lib.env_tools import (
|
||||
parse_env_variables_structure,
|
||||
compute_env_variables_structure,
|
||||
merge_env_variables,
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
|
@ -235,19 +239,15 @@ def version(build):
|
|||
|
||||
def _set_global_environments() -> None:
|
||||
"""Set global AYON environments."""
|
||||
general_env = get_general_environments()
|
||||
# First resolve general environment
|
||||
general_env = parse_env_variables_structure(get_general_environments())
|
||||
|
||||
# first resolve general environment because merge doesn't expect
|
||||
# values to be list.
|
||||
# TODO: switch to AYON environment functions
|
||||
merged_env = acre.merge(
|
||||
acre.compute(acre.parse(general_env), cleanup=False),
|
||||
# Merge environments with current environments and update values
|
||||
merged_env = merge_env_variables(
|
||||
compute_env_variables_structure(general_env),
|
||||
dict(os.environ)
|
||||
)
|
||||
env = acre.compute(
|
||||
merged_env,
|
||||
cleanup=False
|
||||
)
|
||||
env = compute_env_variables_structure(merged_env)
|
||||
os.environ.clear()
|
||||
os.environ.update(env)
|
||||
|
||||
|
|
@ -263,8 +263,8 @@ def _set_addons_environments(addons_manager):
|
|||
|
||||
# Merge environments with current environments and update values
|
||||
if module_envs := addons_manager.collect_global_environments():
|
||||
parsed_envs = acre.parse(module_envs)
|
||||
env = acre.merge(parsed_envs, dict(os.environ))
|
||||
parsed_envs = parse_env_variables_structure(module_envs)
|
||||
env = merge_env_variables(parsed_envs, dict(os.environ))
|
||||
os.environ.clear()
|
||||
os.environ.update(env)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,34 @@
|
|||
from __future__ import annotations
|
||||
import os
|
||||
import re
|
||||
import platform
|
||||
import typing
|
||||
import collections
|
||||
from string import Formatter
|
||||
from typing import Optional
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from typing import Union, Literal
|
||||
|
||||
PlatformName = Literal["windows", "linux", "darwin"]
|
||||
EnvValue = Union[str, list[str], dict[str, str], dict[str, list[str]]]
|
||||
|
||||
|
||||
def env_value_to_bool(env_key=None, value=None, default=False):
|
||||
class CycleError(ValueError):
|
||||
"""Raised when a cycle is detected in dynamic env variables compute."""
|
||||
pass
|
||||
|
||||
|
||||
class DynamicKeyClashError(Exception):
|
||||
"""Raised when dynamic key clashes with an existing key."""
|
||||
pass
|
||||
|
||||
|
||||
def env_value_to_bool(
|
||||
env_key: Optional[str] = None,
|
||||
value: Optional[str] = None,
|
||||
default: bool = False,
|
||||
) -> bool:
|
||||
"""Convert environment variable value to boolean.
|
||||
|
||||
Function is based on value of the environemt variable. Value is lowered
|
||||
|
|
@ -11,6 +38,7 @@ def env_value_to_bool(env_key=None, value=None, default=False):
|
|||
bool: If value match to one of ["true", "yes", "1"] result if True
|
||||
but if value match to ["false", "no", "0"] result is False else
|
||||
default value is returned.
|
||||
|
||||
"""
|
||||
if value is None and env_key is None:
|
||||
return default
|
||||
|
|
@ -27,18 +55,23 @@ def env_value_to_bool(env_key=None, value=None, default=False):
|
|||
return default
|
||||
|
||||
|
||||
def get_paths_from_environ(env_key=None, env_value=None, return_first=False):
|
||||
def get_paths_from_environ(
|
||||
env_key: Optional[str] = None,
|
||||
env_value: Optional[str] = None,
|
||||
return_first: bool = False,
|
||||
) -> Optional[Union[str, list[str]]]:
|
||||
"""Return existing paths from specific environment variable.
|
||||
|
||||
Args:
|
||||
env_key (str): Environment key where should look for paths.
|
||||
env_value (str): Value of environment variable. Argument `env_key` is
|
||||
skipped if this argument is entered.
|
||||
env_key (Optional[str]): Environment key where should look for paths.
|
||||
env_value (Optional[str]): Value of environment variable.
|
||||
Argument `env_key` is skipped if this argument is entered.
|
||||
return_first (bool): Return first found value or return list of found
|
||||
paths. `None` or empty list returned if nothing found.
|
||||
|
||||
Returns:
|
||||
str, list, None: Result of found path/s.
|
||||
Optional[Union[str, list[str]]]: Result of found path/s.
|
||||
|
||||
"""
|
||||
existing_paths = []
|
||||
if not env_key and not env_value:
|
||||
|
|
@ -69,3 +102,225 @@ def get_paths_from_environ(env_key=None, env_value=None, return_first=False):
|
|||
return None
|
||||
# Return all existing paths from environment variable
|
||||
return existing_paths
|
||||
|
||||
|
||||
def parse_env_variables_structure(
|
||||
env: dict[str, EnvValue],
|
||||
platform_name: Optional[PlatformName] = None
|
||||
) -> dict[str, str]:
|
||||
"""Parse environment for platform-specific values and paths as lists.
|
||||
|
||||
Args:
|
||||
env (dict): The source environment to read.
|
||||
platform_name (Optional[PlatformName]): Name of platform to parse for.
|
||||
Defaults to current platform.
|
||||
|
||||
Returns:
|
||||
dict: The flattened environment for a platform.
|
||||
|
||||
"""
|
||||
if platform_name is None:
|
||||
platform_name = platform.system().lower()
|
||||
|
||||
# Separator based on OS 'os.pathsep' is ';' on Windows and ':' on Unix
|
||||
sep = ";" if platform_name == "windows" else ":"
|
||||
|
||||
result = {}
|
||||
for variable, value in env.items():
|
||||
# Platform specific values
|
||||
if isinstance(value, dict):
|
||||
value = value.get(platform_name)
|
||||
|
||||
# Allow to have lists as values in the tool data
|
||||
if isinstance(value, (list, tuple)):
|
||||
value = sep.join(value)
|
||||
|
||||
if not value:
|
||||
continue
|
||||
|
||||
if not isinstance(value, str):
|
||||
raise TypeError(f"Expected 'str' got '{type(value)}'")
|
||||
|
||||
result[variable] = value
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _topological_sort(
|
||||
dependencies: dict[str, set[str]]
|
||||
) -> tuple[list[str], list[str]]:
|
||||
"""Sort values subject to dependency constraints.
|
||||
|
||||
Args:
|
||||
dependencies (dict[str, set[str]): Mapping of environment variable
|
||||
keys to a set of keys they depend on.
|
||||
|
||||
Returns:
|
||||
tuple[list[str], list[str]]: A tuple of two lists. The first list
|
||||
contains the ordered keys in which order should be environment
|
||||
keys filled, the second list contains the keys that would cause
|
||||
cyclic fill of values.
|
||||
|
||||
"""
|
||||
num_heads = collections.defaultdict(int) # num arrows pointing in
|
||||
tails = collections.defaultdict(list) # list of arrows going out
|
||||
heads = [] # unique list of heads in order first seen
|
||||
for head, tail_values in dependencies.items():
|
||||
for tail_value in tail_values:
|
||||
num_heads[tail_value] += 1
|
||||
if head not in tails:
|
||||
heads.append(head)
|
||||
tails[head].append(tail_value)
|
||||
|
||||
ordered = [head for head in heads if head not in num_heads]
|
||||
for head in ordered:
|
||||
for tail in tails[head]:
|
||||
num_heads[tail] -= 1
|
||||
if not num_heads[tail]:
|
||||
ordered.append(tail)
|
||||
cyclic = [tail for tail, heads in num_heads.items() if heads]
|
||||
return ordered, cyclic
|
||||
|
||||
|
||||
class _PartialFormatDict(dict):
|
||||
"""This supports partial formatting.
|
||||
|
||||
Missing keys are replaced with the return value of __missing__.
|
||||
|
||||
"""
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._missing_template: str = "{{{key}}}"
|
||||
|
||||
def set_missing_template(self, template: str):
|
||||
self._missing_template = template
|
||||
|
||||
def __missing__(self, key: str) -> str:
|
||||
return self._missing_template.format(key=key)
|
||||
|
||||
|
||||
def _partial_format(
|
||||
value: str,
|
||||
data: dict[str, str],
|
||||
missing_template: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Return string `s` formatted by `data` allowing a partial format
|
||||
|
||||
Arguments:
|
||||
value (str): The string that will be formatted
|
||||
data (dict): The dictionary used to format with.
|
||||
missing_template (Optional[str]): The template to use when a key is
|
||||
missing from the data. If `None`, the key will remain unformatted.
|
||||
|
||||
Example:
|
||||
>>> _partial_format("{d} {a} {b} {c} {d}", {'b': "and", 'd': "left"})
|
||||
'left {a} and {c} left'
|
||||
|
||||
"""
|
||||
|
||||
mapping = _PartialFormatDict(**data)
|
||||
if missing_template is not None:
|
||||
mapping.set_missing_template(missing_template)
|
||||
|
||||
formatter = Formatter()
|
||||
try:
|
||||
output = formatter.vformat(value, (), mapping)
|
||||
except Exception:
|
||||
r_token = re.compile(r"({.*?})")
|
||||
output = value
|
||||
for match in re.findall(r_token, value):
|
||||
try:
|
||||
output = re.sub(match, match.format(**data), output)
|
||||
except (KeyError, ValueError, IndexError):
|
||||
continue
|
||||
return output
|
||||
|
||||
|
||||
def compute_env_variables_structure(
|
||||
env: dict[str, str],
|
||||
fill_dynamic_keys: bool = True,
|
||||
) -> dict[str, str]:
|
||||
"""Compute the result from recursive dynamic environment.
|
||||
|
||||
Note: Keys that are not present in the data will remain unformatted as the
|
||||
original keys. So they can be formatted against the current user
|
||||
environment when merging. So {"A": "{key}"} will remain {key} if not
|
||||
present in the dynamic environment.
|
||||
|
||||
"""
|
||||
env = env.copy()
|
||||
|
||||
# Collect dependencies
|
||||
dependencies = collections.defaultdict(set)
|
||||
for key, value in env.items():
|
||||
dependent_keys = re.findall("{(.+?)}", value)
|
||||
for dependent_key in dependent_keys:
|
||||
# Ignore reference to itself or key is not in env
|
||||
if dependent_key != key and dependent_key in env:
|
||||
dependencies[key].add(dependent_key)
|
||||
|
||||
ordered, cyclic = _topological_sort(dependencies)
|
||||
|
||||
# Check cycle
|
||||
if cyclic:
|
||||
raise CycleError(f"A cycle is detected on: {cyclic}")
|
||||
|
||||
# Format dynamic values
|
||||
for key in reversed(ordered):
|
||||
if key in env:
|
||||
if not isinstance(env[key], str):
|
||||
continue
|
||||
data = env.copy()
|
||||
data.pop(key) # format without itself
|
||||
env[key] = _partial_format(env[key], data=data)
|
||||
|
||||
# Format dynamic keys
|
||||
if fill_dynamic_keys:
|
||||
formatted = {}
|
||||
for key, value in env.items():
|
||||
if not isinstance(value, str):
|
||||
formatted[key] = value
|
||||
continue
|
||||
|
||||
new_key = _partial_format(key, data=env)
|
||||
if new_key in formatted:
|
||||
raise DynamicKeyClashError(
|
||||
f"Key clashes on: {new_key} (source: {key})"
|
||||
)
|
||||
|
||||
formatted[new_key] = value
|
||||
env = formatted
|
||||
|
||||
return env
|
||||
|
||||
|
||||
def merge_env_variables(
|
||||
src_env: dict[str, str],
|
||||
dst_env: dict[str, str],
|
||||
missing_template: Optional[str] = None,
|
||||
) -> dict[str, str]:
|
||||
"""Merge the tools environment with the 'current_env'.
|
||||
|
||||
This finalizes the join with a current environment by formatting the
|
||||
remainder of dynamic variables with that from the current environment.
|
||||
|
||||
Remaining missing variables result in an empty value.
|
||||
|
||||
Args:
|
||||
src_env (dict): The dynamic environment
|
||||
dst_env (dict): The target environment variables mapping to merge
|
||||
the dynamic environment into.
|
||||
missing_template (str): Argument passed to '_partial_format' during
|
||||
merging. `None` should keep missing keys unchanged.
|
||||
|
||||
Returns:
|
||||
dict[str, str]: The resulting environment after the merge.
|
||||
|
||||
"""
|
||||
result = dst_env.copy()
|
||||
for key, value in src_env.items():
|
||||
result[key] = _partial_format(
|
||||
str(value), dst_env, missing_template
|
||||
)
|
||||
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from datetime import datetime
|
|||
from abc import ABC, abstractmethod
|
||||
from functools import lru_cache
|
||||
|
||||
import appdirs
|
||||
import platformdirs
|
||||
import ayon_api
|
||||
|
||||
_PLACEHOLDER = object()
|
||||
|
|
@ -17,7 +17,7 @@ _PLACEHOLDER = object()
|
|||
|
||||
def _get_ayon_appdirs(*args):
|
||||
return os.path.join(
|
||||
appdirs.user_data_dir("AYON", "Ynput"),
|
||||
platformdirs.user_data_dir("AYON", "Ynput"),
|
||||
*args
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -755,11 +755,19 @@ class CreateContext:
|
|||
).format(creator_class.host_name, self.host_name))
|
||||
continue
|
||||
|
||||
# TODO report initialization error
|
||||
try:
|
||||
creator = creator_class(
|
||||
project_settings,
|
||||
self,
|
||||
self.headless
|
||||
)
|
||||
except Exception:
|
||||
self.log.error(
|
||||
f"Failed to initialize plugin: {creator_class}",
|
||||
exc_info=True
|
||||
)
|
||||
continue
|
||||
|
||||
if not creator.enabled:
|
||||
disabled_creators[creator_identifier] = creator
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ from ayon_core.pipeline.plugin_discover import (
|
|||
from ayon_core.pipeline.create import (
|
||||
discover_legacy_creator_plugins,
|
||||
CreateContext,
|
||||
HiddenCreator,
|
||||
)
|
||||
|
||||
_NOT_SET = object()
|
||||
|
|
@ -309,7 +310,13 @@ class AbstractTemplateBuilder(ABC):
|
|||
self._creators_by_name = creators_by_name
|
||||
|
||||
def _collect_creators(self):
|
||||
self._creators_by_name = dict(self.create_context.creators)
|
||||
self._creators_by_name = {
|
||||
identifier: creator
|
||||
for identifier, creator
|
||||
in self.create_context.manual_creators.items()
|
||||
# Do not list HiddenCreator even though it is a 'manual creator'
|
||||
if not isinstance(creator, HiddenCreator)
|
||||
}
|
||||
|
||||
def get_creators_by_name(self):
|
||||
if self._creators_by_name is None:
|
||||
|
|
|
|||
|
|
@ -116,11 +116,11 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
|
|||
|
||||
if not_found_folder_paths:
|
||||
joined_folder_paths = ", ".join(
|
||||
["\"{}\"".format(path) for path in not_found_folder_paths]
|
||||
[f"\"{path}\"" for path in not_found_folder_paths]
|
||||
)
|
||||
self.log.warning(
|
||||
f"Not found folder entities with paths {joined_folder_paths}."
|
||||
)
|
||||
self.log.warning((
|
||||
"Not found folder entities with paths \"{}\"."
|
||||
).format(joined_folder_paths))
|
||||
|
||||
def fill_missing_task_entities(self, context, project_name):
|
||||
self.log.debug("Querying task entities for instances.")
|
||||
|
|
|
|||
|
|
@ -1,78 +1,120 @@
|
|||
"""Plugin for collecting OTIO frame ranges and related timing information.
|
||||
|
||||
This module contains a unified plugin that handles:
|
||||
- Basic timeline frame ranges
|
||||
- Source media frame ranges
|
||||
- Retimed clip frame ranges
|
||||
"""
|
||||
Requires:
|
||||
otioTimeline -> context data attribute
|
||||
review -> instance data attribute
|
||||
masterLayer -> instance data attribute
|
||||
otioClipRange -> instance data attribute
|
||||
"""
|
||||
|
||||
from pprint import pformat
|
||||
|
||||
import opentimelineio as otio
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class CollectOtioFrameRanges(pyblish.api.InstancePlugin):
|
||||
"""Getting otio ranges from otio_clip
|
||||
|
||||
Adding timeline and source ranges to instance data"""
|
||||
|
||||
label = "Collect OTIO Frame Ranges"
|
||||
order = pyblish.api.CollectorOrder - 0.08
|
||||
families = ["shot", "clip"]
|
||||
hosts = ["resolve", "hiero", "flame", "traypublisher"]
|
||||
|
||||
def process(self, instance):
|
||||
# Not all hosts can import these modules.
|
||||
import opentimelineio as otio
|
||||
from ayon_core.pipeline.editorial import (
|
||||
from ayon_core.pipeline.editorial import (
|
||||
get_media_range_with_retimes,
|
||||
otio_range_to_frame_range,
|
||||
otio_range_with_handles
|
||||
)
|
||||
otio_range_with_handles,
|
||||
)
|
||||
|
||||
|
||||
def validate_otio_clip(instance, logger):
|
||||
"""Validate if instance has required OTIO clip data.
|
||||
|
||||
Args:
|
||||
instance: The instance to validate
|
||||
logger: Logger object to use for debug messages
|
||||
|
||||
Returns:
|
||||
bool: True if valid, False otherwise
|
||||
"""
|
||||
if not instance.data.get("otioClip"):
|
||||
self.log.debug("Skipping collect OTIO frame range.")
|
||||
logger.debug("Skipping collect OTIO range - no clip found.")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class CollectOtioRanges(pyblish.api.InstancePlugin):
|
||||
"""Collect all OTIO-related frame ranges and timing information.
|
||||
|
||||
This plugin handles collection of:
|
||||
- Basic timeline frame ranges with handles
|
||||
- Source media frame ranges with handles
|
||||
- Retimed clip frame ranges
|
||||
|
||||
Requires:
|
||||
otioClip (otio.schema.Clip): OTIO clip object
|
||||
workfileFrameStart (int): Starting frame of work file
|
||||
|
||||
Optional:
|
||||
shotDurationFromSource (int): Duration from source if retimed
|
||||
|
||||
Provides:
|
||||
frameStart (int): Start frame in timeline
|
||||
frameEnd (int): End frame in timeline
|
||||
clipIn (int): Clip in point
|
||||
clipOut (int): Clip out point
|
||||
clipInH (int): Clip in point with handles
|
||||
clipOutH (int): Clip out point with handles
|
||||
sourceStart (int): Source media start frame
|
||||
sourceEnd (int): Source media end frame
|
||||
sourceStartH (int): Source media start frame with handles
|
||||
sourceEndH (int): Source media end frame with handles
|
||||
"""
|
||||
|
||||
label = "Collect OTIO Ranges"
|
||||
order = pyblish.api.CollectorOrder - 0.08
|
||||
families = ["shot", "clip"]
|
||||
|
||||
def process(self, instance):
|
||||
"""Process the instance to collect all frame ranges.
|
||||
|
||||
Args:
|
||||
instance: The instance to process
|
||||
"""
|
||||
if not validate_otio_clip(instance, self.log):
|
||||
return
|
||||
|
||||
# get basic variables
|
||||
otio_clip = instance.data["otioClip"]
|
||||
|
||||
# Collect timeline ranges if workfile start frame is available
|
||||
if "workfileFrameStart" in instance.data:
|
||||
self._collect_timeline_ranges(instance, otio_clip)
|
||||
|
||||
# Traypublisher Simple or Advanced editorial publishing is
|
||||
# working with otio clips which are having no available range
|
||||
# because they are not having any media references.
|
||||
try:
|
||||
otio_clip.available_range()
|
||||
has_available_range = True
|
||||
except otio._otio.CannotComputeAvailableRangeError:
|
||||
self.log.info("Clip has no available range")
|
||||
has_available_range = False
|
||||
|
||||
# Collect source ranges if clip has available range
|
||||
if has_available_range:
|
||||
self._collect_source_ranges(instance, otio_clip)
|
||||
|
||||
# Handle retimed ranges if source duration is available
|
||||
if "shotDurationFromSource" in instance.data:
|
||||
self._collect_retimed_ranges(instance, otio_clip)
|
||||
|
||||
def _collect_timeline_ranges(self, instance, otio_clip):
|
||||
"""Collect basic timeline frame ranges."""
|
||||
workfile_start = instance.data["workfileFrameStart"]
|
||||
workfile_source_duration = instance.data.get("shotDurationFromSource")
|
||||
|
||||
# get ranges
|
||||
# Get timeline ranges
|
||||
otio_tl_range = otio_clip.range_in_parent()
|
||||
otio_src_range = otio_clip.source_range
|
||||
otio_avalable_range = otio_clip.available_range()
|
||||
otio_tl_range_handles = otio_range_with_handles(
|
||||
otio_tl_range, instance)
|
||||
otio_src_range_handles = otio_range_with_handles(
|
||||
otio_src_range, instance)
|
||||
otio_tl_range,
|
||||
instance
|
||||
)
|
||||
|
||||
# get source avalable start frame
|
||||
src_starting_from = otio.opentime.to_frames(
|
||||
otio_avalable_range.start_time,
|
||||
otio_avalable_range.start_time.rate)
|
||||
# Convert to frames
|
||||
tl_start, tl_end = otio_range_to_frame_range(otio_tl_range)
|
||||
tl_start_h, tl_end_h = otio_range_to_frame_range(otio_tl_range_handles)
|
||||
|
||||
# convert to frames
|
||||
range_convert = otio_range_to_frame_range
|
||||
tl_start, tl_end = range_convert(otio_tl_range)
|
||||
tl_start_h, tl_end_h = range_convert(otio_tl_range_handles)
|
||||
src_start, src_end = range_convert(otio_src_range)
|
||||
src_start_h, src_end_h = range_convert(otio_src_range_handles)
|
||||
frame_start = workfile_start
|
||||
frame_end = frame_start + otio.opentime.to_frames(
|
||||
otio_tl_range.duration, otio_tl_range.duration.rate) - 1
|
||||
|
||||
# in case of retimed clip and frame range should not be retimed
|
||||
if workfile_source_duration:
|
||||
# get available range trimmed with processed retimes
|
||||
retimed_attributes = get_media_range_with_retimes(
|
||||
otio_clip, 0, 0)
|
||||
self.log.debug(
|
||||
">> retimed_attributes: {}".format(retimed_attributes))
|
||||
media_in = int(retimed_attributes["mediaIn"])
|
||||
media_out = int(retimed_attributes["mediaOut"])
|
||||
frame_end = frame_start + (media_out - media_in) + 1
|
||||
self.log.debug(frame_end)
|
||||
frame_end = frame_start + otio_tl_range.duration.to_frames() - 1
|
||||
|
||||
data = {
|
||||
"frameStart": frame_start,
|
||||
|
|
@ -81,13 +123,77 @@ class CollectOtioFrameRanges(pyblish.api.InstancePlugin):
|
|||
"clipOut": tl_end - 1,
|
||||
"clipInH": tl_start_h,
|
||||
"clipOutH": tl_end_h - 1,
|
||||
"sourceStart": src_starting_from + src_start,
|
||||
"sourceEnd": src_starting_from + src_end - 1,
|
||||
"sourceStartH": src_starting_from + src_start_h,
|
||||
"sourceEndH": src_starting_from + src_end_h - 1,
|
||||
}
|
||||
instance.data.update(data)
|
||||
self.log.debug(
|
||||
"_ data: {}".format(pformat(data)))
|
||||
self.log.debug(
|
||||
"_ instance.data: {}".format(pformat(instance.data)))
|
||||
self.log.debug(f"Added frame ranges: {pformat(data)}")
|
||||
|
||||
def _collect_source_ranges(self, instance, otio_clip):
|
||||
"""Collect source media frame ranges."""
|
||||
# Get source ranges
|
||||
otio_src_range = otio_clip.source_range
|
||||
otio_available_range = otio_clip.available_range()
|
||||
|
||||
# Backward-compatibility for Hiero OTIO exporter.
|
||||
# NTSC compatibility might introduce floating rates, when these are
|
||||
# not exactly the same (23.976 vs 23.976024627685547)
|
||||
# this will cause precision issue in computation.
|
||||
# Currently round to 2 decimals for comparison,
|
||||
# but this should always rescale after that.
|
||||
rounded_av_rate = round(otio_available_range.start_time.rate, 2)
|
||||
rounded_src_rate = round(otio_src_range.start_time.rate, 2)
|
||||
if rounded_av_rate != rounded_src_rate:
|
||||
conformed_src_in = otio_src_range.start_time.rescaled_to(
|
||||
otio_available_range.start_time.rate
|
||||
)
|
||||
conformed_src_duration = otio_src_range.duration.rescaled_to(
|
||||
otio_available_range.duration.rate
|
||||
)
|
||||
conformed_source_range = otio.opentime.TimeRange(
|
||||
start_time=conformed_src_in,
|
||||
duration=conformed_src_duration
|
||||
)
|
||||
else:
|
||||
conformed_source_range = otio_src_range
|
||||
|
||||
source_start = conformed_source_range.start_time
|
||||
source_end = source_start + conformed_source_range.duration
|
||||
handle_start = otio.opentime.RationalTime(
|
||||
instance.data.get("handleStart", 0),
|
||||
source_start.rate
|
||||
)
|
||||
handle_end = otio.opentime.RationalTime(
|
||||
instance.data.get("handleEnd", 0),
|
||||
source_start.rate
|
||||
)
|
||||
source_start_h = source_start - handle_start
|
||||
source_end_h = source_end + handle_end
|
||||
data = {
|
||||
"sourceStart": source_start.to_frames(),
|
||||
"sourceEnd": source_end.to_frames() - 1,
|
||||
"sourceStartH": source_start_h.to_frames(),
|
||||
"sourceEndH": source_end_h.to_frames() - 1,
|
||||
}
|
||||
instance.data.update(data)
|
||||
self.log.debug(f"Added source ranges: {pformat(data)}")
|
||||
|
||||
def _collect_retimed_ranges(self, instance, otio_clip):
|
||||
"""Handle retimed clip frame ranges."""
|
||||
retimed_attributes = get_media_range_with_retimes(otio_clip, 0, 0)
|
||||
self.log.debug(f"Retimed attributes: {retimed_attributes}")
|
||||
|
||||
frame_start = instance.data["frameStart"]
|
||||
media_in = int(retimed_attributes["mediaIn"])
|
||||
media_out = int(retimed_attributes["mediaOut"])
|
||||
frame_end = frame_start + (media_out - media_in)
|
||||
|
||||
data = {
|
||||
"frameStart": frame_start,
|
||||
"frameEnd": frame_end,
|
||||
"sourceStart": media_in,
|
||||
"sourceEnd": media_out,
|
||||
"sourceStartH": media_in - int(retimed_attributes["handleStart"]),
|
||||
"sourceEndH": media_out + int(retimed_attributes["handleEnd"]),
|
||||
}
|
||||
|
||||
instance.data.update(data)
|
||||
self.log.debug(f"Updated retimed values: {data}")
|
||||
|
|
|
|||
|
|
@ -286,7 +286,7 @@ class ExtractOTIOReview(
|
|||
)
|
||||
|
||||
instance.data["representations"].append(representation)
|
||||
self.log.info("Adding representation: {}".format(representation))
|
||||
self.log.debug("Adding representation: {}".format(representation))
|
||||
|
||||
def _create_representation(self, start, duration):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from operator import attrgetter
|
||||
import dataclasses
|
||||
import os
|
||||
from typing import Dict
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import pyblish.api
|
||||
try:
|
||||
|
|
@ -14,7 +14,8 @@ from ayon_core.lib import (
|
|||
BoolDef,
|
||||
UISeparatorDef,
|
||||
UILabelDef,
|
||||
EnumDef
|
||||
EnumDef,
|
||||
filter_profiles
|
||||
)
|
||||
try:
|
||||
from ayon_core.pipeline.usdlib import (
|
||||
|
|
@ -281,6 +282,9 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
|
|||
"fx": 500,
|
||||
"lighting": 600,
|
||||
}
|
||||
# Default profiles to set certain instance attribute defaults based on
|
||||
# profiles in settings
|
||||
profiles: List[Dict[str, Any]] = []
|
||||
|
||||
@classmethod
|
||||
def apply_settings(cls, project_settings):
|
||||
|
|
@ -298,6 +302,8 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
|
|||
if contribution_layers:
|
||||
cls.contribution_layers = contribution_layers
|
||||
|
||||
cls.profiles = plugin_settings.get("profiles", [])
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
attr_values = self.get_attr_values_from_data(instance.data)
|
||||
|
|
@ -463,6 +469,28 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
|
|||
if not cls.instance_matches_plugin_families(instance):
|
||||
return []
|
||||
|
||||
# Set default target layer based on product type
|
||||
current_context_task_type = create_context.get_current_task_type()
|
||||
profile = filter_profiles(cls.profiles, {
|
||||
"product_types": instance.data["productType"],
|
||||
"task_types": current_context_task_type
|
||||
})
|
||||
if not profile:
|
||||
profile = {}
|
||||
|
||||
# Define defaults
|
||||
default_contribution_layer = profile.get(
|
||||
"contribution_layer", None)
|
||||
default_apply_as_variant = profile.get(
|
||||
"contribution_apply_as_variant", False)
|
||||
default_target_product = profile.get(
|
||||
"contribution_target_product", "usdAsset")
|
||||
default_init_as = (
|
||||
"asset"
|
||||
if profile.get("contribution_target_product") == "usdAsset"
|
||||
else "shot")
|
||||
init_as_visible = False
|
||||
|
||||
# Attributes logic
|
||||
publish_attributes = instance["publish_attributes"].get(
|
||||
cls.__name__, {})
|
||||
|
|
@ -495,7 +523,7 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
|
|||
"the contribution itself will be added to the "
|
||||
"department layer."
|
||||
),
|
||||
default="usdAsset",
|
||||
default=default_target_product,
|
||||
visible=visible),
|
||||
EnumDef("contribution_target_product_init",
|
||||
label="Initialize as",
|
||||
|
|
@ -507,8 +535,8 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
|
|||
"setting will do nothing."
|
||||
),
|
||||
items=["asset", "shot"],
|
||||
default="asset",
|
||||
visible=visible),
|
||||
default=default_init_as,
|
||||
visible=visible and init_as_visible),
|
||||
|
||||
# Asset layer, e.g. model.usd, look.usd, rig.usd
|
||||
EnumDef("contribution_layer",
|
||||
|
|
@ -520,7 +548,7 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
|
|||
"the list) will contribute as a stronger opinion."
|
||||
),
|
||||
items=list(cls.contribution_layers.keys()),
|
||||
default="model",
|
||||
default=default_contribution_layer,
|
||||
visible=visible),
|
||||
BoolDef("contribution_apply_as_variant",
|
||||
label="Add as variant",
|
||||
|
|
@ -532,7 +560,7 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
|
|||
"appended to as a sublayer to the department layer "
|
||||
"instead."
|
||||
),
|
||||
default=True,
|
||||
default=default_apply_as_variant,
|
||||
visible=visible),
|
||||
TextDef("contribution_variant_set_name",
|
||||
label="Variant Set Name",
|
||||
|
|
@ -588,31 +616,6 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
|
|||
instance.set_publish_plugin_attr_defs(cls.__name__, new_attrs)
|
||||
|
||||
|
||||
class CollectUSDLayerContributionsHoudiniLook(CollectUSDLayerContributions):
|
||||
"""
|
||||
This is solely here to expose the attribute definitions for the
|
||||
Houdini "look" family.
|
||||
"""
|
||||
# TODO: Improve how this is built for the look family
|
||||
hosts = ["houdini"]
|
||||
families = ["look"]
|
||||
label = CollectUSDLayerContributions.label + " (Look)"
|
||||
|
||||
@classmethod
|
||||
def get_attr_defs_for_instance(cls, create_context, instance):
|
||||
# Filtering of instance, if needed, can be customized
|
||||
if not cls.instance_matches_plugin_families(instance):
|
||||
return []
|
||||
|
||||
defs = super().get_attr_defs_for_instance(create_context, instance)
|
||||
|
||||
# Update default for department layer to look
|
||||
layer_def = next(d for d in defs if d.key == "contribution_layer")
|
||||
layer_def.default = "look"
|
||||
|
||||
return defs
|
||||
|
||||
|
||||
class ValidateUSDDependencies(pyblish.api.InstancePlugin):
|
||||
families = ["usdLayer"]
|
||||
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ class VersionItem:
|
|||
version (int): Version. Can be negative when is hero version.
|
||||
is_hero (bool): Is hero version.
|
||||
product_id (str): Product id.
|
||||
task_id (Union[str, None]): Task id.
|
||||
thumbnail_id (Union[str, None]): Thumbnail id.
|
||||
published_time (Union[str, None]): Published time in format
|
||||
'%Y%m%dT%H%M%SZ'.
|
||||
|
|
@ -127,6 +128,7 @@ class VersionItem:
|
|||
version,
|
||||
is_hero,
|
||||
product_id,
|
||||
task_id,
|
||||
thumbnail_id,
|
||||
published_time,
|
||||
author,
|
||||
|
|
@ -140,6 +142,7 @@ class VersionItem:
|
|||
):
|
||||
self.version_id = version_id
|
||||
self.product_id = product_id
|
||||
self.task_id = task_id
|
||||
self.thumbnail_id = thumbnail_id
|
||||
self.version = version
|
||||
self.is_hero = is_hero
|
||||
|
|
@ -161,6 +164,7 @@ class VersionItem:
|
|||
and self.version == other.version
|
||||
and self.version_id == other.version_id
|
||||
and self.product_id == other.product_id
|
||||
and self.task_id == other.task_id
|
||||
)
|
||||
|
||||
def __ne__(self, other):
|
||||
|
|
@ -198,6 +202,7 @@ class VersionItem:
|
|||
return {
|
||||
"version_id": self.version_id,
|
||||
"product_id": self.product_id,
|
||||
"task_id": self.task_id,
|
||||
"thumbnail_id": self.thumbnail_id,
|
||||
"version": self.version,
|
||||
"is_hero": self.is_hero,
|
||||
|
|
@ -536,6 +541,55 @@ class FrontendLoaderController(_BaseLoaderController):
|
|||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_task_items(self, project_name, folder_ids, sender=None):
|
||||
"""Task items for folder ids.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
folder_ids (Iterable[str]): Folder ids.
|
||||
sender (Optional[str]): Sender who requested the items.
|
||||
|
||||
Returns:
|
||||
list[TaskItem]: List of task items.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_task_type_items(self, project_name, sender=None):
|
||||
"""Task type items for a project.
|
||||
|
||||
This function may trigger events with topics
|
||||
'projects.task_types.refresh.started' and
|
||||
'projects.task_types.refresh.finished' which will contain 'sender'
|
||||
value in data.
|
||||
That may help to avoid re-refresh of items in UI elements.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
sender (str): Who requested task type items.
|
||||
|
||||
Returns:
|
||||
list[TaskTypeItem]: Task type information.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_folder_labels(self, project_name, folder_ids):
|
||||
"""Get folder labels for folder ids.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
folder_ids (Iterable[str]): Folder ids.
|
||||
|
||||
Returns:
|
||||
dict[str, Optional[str]]: Folder labels by folder id.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_project_status_items(self, project_name, sender=None):
|
||||
"""Items for all projects available on server.
|
||||
|
|
@ -717,8 +771,30 @@ class FrontendLoaderController(_BaseLoaderController):
|
|||
|
||||
Returns:
|
||||
list[str]: Selected folder ids.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_selected_task_ids(self):
|
||||
"""Get selected task ids.
|
||||
|
||||
The information is based on last selection from UI.
|
||||
|
||||
Returns:
|
||||
list[str]: Selected folder ids.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_selected_tasks(self, task_ids):
|
||||
"""Set selected tasks.
|
||||
|
||||
Args:
|
||||
task_ids (Iterable[str]): Selected task ids.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -729,8 +805,8 @@ class FrontendLoaderController(_BaseLoaderController):
|
|||
|
||||
Returns:
|
||||
list[str]: Selected version ids.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
|
|||
|
|
@ -198,6 +198,31 @@ class LoaderController(BackendLoaderController, FrontendLoaderController):
|
|||
def get_folder_items(self, project_name, sender=None):
|
||||
return self._hierarchy_model.get_folder_items(project_name, sender)
|
||||
|
||||
def get_task_items(self, project_name, folder_ids, sender=None):
|
||||
output = []
|
||||
for folder_id in folder_ids:
|
||||
output.extend(self._hierarchy_model.get_task_items(
|
||||
project_name, folder_id, sender
|
||||
))
|
||||
return output
|
||||
|
||||
def get_task_type_items(self, project_name, sender=None):
|
||||
return self._projects_model.get_task_type_items(
|
||||
project_name, sender
|
||||
)
|
||||
|
||||
def get_folder_labels(self, project_name, folder_ids):
|
||||
folder_items_by_id = self._hierarchy_model.get_folder_items_by_id(
|
||||
project_name, folder_ids
|
||||
)
|
||||
output = {}
|
||||
for folder_id, folder_item in folder_items_by_id.items():
|
||||
label = None
|
||||
if folder_item is not None:
|
||||
label = folder_item.label
|
||||
output[folder_id] = label
|
||||
return output
|
||||
|
||||
def get_product_items(self, project_name, folder_ids, sender=None):
|
||||
return self._products_model.get_product_items(
|
||||
project_name, folder_ids, sender)
|
||||
|
|
@ -299,6 +324,12 @@ class LoaderController(BackendLoaderController, FrontendLoaderController):
|
|||
def set_selected_folders(self, folder_ids):
|
||||
self._selection_model.set_selected_folders(folder_ids)
|
||||
|
||||
def get_selected_task_ids(self):
|
||||
return self._selection_model.get_selected_task_ids()
|
||||
|
||||
def set_selected_tasks(self, task_ids):
|
||||
self._selection_model.set_selected_tasks(task_ids)
|
||||
|
||||
def get_selected_version_ids(self):
|
||||
return self._selection_model.get_selected_version_ids()
|
||||
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ def version_item_from_entity(version):
|
|||
version=version_num,
|
||||
is_hero=is_hero,
|
||||
product_id=version["productId"],
|
||||
task_id=version["taskId"],
|
||||
thumbnail_id=version["thumbnailId"],
|
||||
published_time=published_time,
|
||||
author=author,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ class SelectionModel(object):
|
|||
|
||||
self._project_name = None
|
||||
self._folder_ids = set()
|
||||
self._task_ids = set()
|
||||
self._version_ids = set()
|
||||
self._representation_ids = set()
|
||||
|
||||
|
|
@ -48,6 +49,23 @@ class SelectionModel(object):
|
|||
self.event_source
|
||||
)
|
||||
|
||||
def get_selected_task_ids(self):
|
||||
return self._task_ids
|
||||
|
||||
def set_selected_tasks(self, task_ids):
|
||||
if task_ids == self._task_ids:
|
||||
return
|
||||
|
||||
self._task_ids = task_ids
|
||||
self._controller.emit_event(
|
||||
"selection.tasks.changed",
|
||||
{
|
||||
"project_name": self._project_name,
|
||||
"task_ids": task_ids,
|
||||
},
|
||||
self.event_source
|
||||
)
|
||||
|
||||
def get_selected_version_ids(self):
|
||||
return self._version_ids
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
from __future__ import annotations
|
||||
import typing
|
||||
from typing import List, Tuple, Optional, Iterable, Any
|
||||
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
||||
from ayon_core.tools.utils import get_qt_icon
|
||||
from ayon_core.tools.utils.lib import (
|
||||
checkstate_int_to_enum,
|
||||
checkstate_enum_to_int,
|
||||
|
|
@ -11,14 +14,269 @@ from ayon_core.tools.utils.constants import (
|
|||
UNCHECKED_INT,
|
||||
ITEM_IS_USER_TRISTATE,
|
||||
)
|
||||
if typing.TYPE_CHECKING:
|
||||
from ayon_core.tools.loader.abstract import FrontendLoaderController
|
||||
|
||||
VALUE_ITEM_TYPE = 0
|
||||
STANDARD_ITEM_TYPE = 1
|
||||
SEPARATOR_ITEM_TYPE = 2
|
||||
|
||||
VALUE_ITEM_SUBTYPE = 0
|
||||
SELECT_ALL_SUBTYPE = 1
|
||||
DESELECT_ALL_SUBTYPE = 2
|
||||
SWAP_STATE_SUBTYPE = 3
|
||||
|
||||
|
||||
class BaseQtModel(QtGui.QStandardItemModel):
|
||||
_empty_icon = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
item_type_role: int,
|
||||
item_subtype_role: int,
|
||||
empty_values_label: str,
|
||||
controller: FrontendLoaderController,
|
||||
):
|
||||
self._item_type_role = item_type_role
|
||||
self._item_subtype_role = item_subtype_role
|
||||
self._empty_values_label = empty_values_label
|
||||
self._controller = controller
|
||||
|
||||
self._last_project = None
|
||||
|
||||
self._select_project_item = None
|
||||
self._empty_values_item = None
|
||||
|
||||
self._select_all_item = None
|
||||
self._deselect_all_item = None
|
||||
self._swap_states_item = None
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.refresh(None)
|
||||
|
||||
def _get_standard_items(self) -> list[QtGui.QStandardItem]:
|
||||
raise NotImplementedError(
|
||||
"'_get_standard_items' is not implemented"
|
||||
f" for {self.__class__}"
|
||||
)
|
||||
|
||||
def _clear_standard_items(self):
|
||||
raise NotImplementedError(
|
||||
"'_clear_standard_items' is not implemented"
|
||||
f" for {self.__class__}"
|
||||
)
|
||||
|
||||
def _prepare_new_value_items(
|
||||
self, project_name: str, project_changed: bool
|
||||
) -> tuple[
|
||||
list[QtGui.QStandardItem], list[QtGui.QStandardItem]
|
||||
]:
|
||||
raise NotImplementedError(
|
||||
"'_prepare_new_value_items' is not implemented"
|
||||
f" for {self.__class__}"
|
||||
)
|
||||
|
||||
def refresh(self, project_name: Optional[str]):
|
||||
# New project was selected
|
||||
project_changed = False
|
||||
if project_name != self._last_project:
|
||||
self._last_project = project_name
|
||||
project_changed = True
|
||||
|
||||
if project_name is None:
|
||||
self._add_select_project_item()
|
||||
return
|
||||
|
||||
value_items, items_to_remove = self._prepare_new_value_items(
|
||||
project_name, project_changed
|
||||
)
|
||||
if not value_items:
|
||||
self._add_empty_values_item()
|
||||
return
|
||||
|
||||
self._remove_empty_items()
|
||||
|
||||
root_item = self.invisibleRootItem()
|
||||
for row_idx, value_item in enumerate(value_items):
|
||||
if value_item.row() == row_idx:
|
||||
continue
|
||||
if value_item.row() >= 0:
|
||||
root_item.takeRow(value_item.row())
|
||||
root_item.insertRow(row_idx, value_item)
|
||||
|
||||
for item in items_to_remove:
|
||||
root_item.removeRow(item.row())
|
||||
|
||||
self._add_selection_items()
|
||||
|
||||
def setData(self, index, value, role):
|
||||
if role == QtCore.Qt.CheckStateRole and index.isValid():
|
||||
item_subtype = index.data(self._item_subtype_role)
|
||||
if item_subtype == SELECT_ALL_SUBTYPE:
|
||||
for item in self._get_standard_items():
|
||||
item.setCheckState(QtCore.Qt.Checked)
|
||||
return True
|
||||
if item_subtype == DESELECT_ALL_SUBTYPE:
|
||||
for item in self._get_standard_items():
|
||||
item.setCheckState(QtCore.Qt.Unchecked)
|
||||
return True
|
||||
if item_subtype == SWAP_STATE_SUBTYPE:
|
||||
for item in self._get_standard_items():
|
||||
current_state = item.checkState()
|
||||
item.setCheckState(
|
||||
QtCore.Qt.Checked
|
||||
if current_state == QtCore.Qt.Unchecked
|
||||
else QtCore.Qt.Unchecked
|
||||
)
|
||||
return True
|
||||
return super().setData(index, value, role)
|
||||
|
||||
@classmethod
|
||||
def _get_empty_icon(cls):
|
||||
if cls._empty_icon is None:
|
||||
pix = QtGui.QPixmap(1, 1)
|
||||
pix.fill(QtCore.Qt.transparent)
|
||||
cls._empty_icon = QtGui.QIcon(pix)
|
||||
return cls._empty_icon
|
||||
|
||||
def _init_default_items(self):
|
||||
if self._empty_values_item is not None:
|
||||
return
|
||||
|
||||
empty_values_item = QtGui.QStandardItem(self._empty_values_label)
|
||||
select_project_item = QtGui.QStandardItem("Select project...")
|
||||
|
||||
select_all_item = QtGui.QStandardItem("Select all")
|
||||
deselect_all_item = QtGui.QStandardItem("Deselect all")
|
||||
swap_states_item = QtGui.QStandardItem("Swap")
|
||||
|
||||
for item in (
|
||||
empty_values_item,
|
||||
select_project_item,
|
||||
select_all_item,
|
||||
deselect_all_item,
|
||||
swap_states_item,
|
||||
):
|
||||
item.setData(STANDARD_ITEM_TYPE, self._item_type_role)
|
||||
|
||||
select_all_item.setIcon(get_qt_icon({
|
||||
"type": "material-symbols",
|
||||
"name": "done_all",
|
||||
"color": "white"
|
||||
}))
|
||||
deselect_all_item.setIcon(get_qt_icon({
|
||||
"type": "material-symbols",
|
||||
"name": "remove_done",
|
||||
"color": "white"
|
||||
}))
|
||||
swap_states_item.setIcon(get_qt_icon({
|
||||
"type": "material-symbols",
|
||||
"name": "swap_horiz",
|
||||
"color": "white"
|
||||
}))
|
||||
|
||||
for item in (
|
||||
empty_values_item,
|
||||
select_project_item,
|
||||
):
|
||||
item.setFlags(QtCore.Qt.NoItemFlags)
|
||||
|
||||
for item, item_type in (
|
||||
(select_all_item, SELECT_ALL_SUBTYPE),
|
||||
(deselect_all_item, DESELECT_ALL_SUBTYPE),
|
||||
(swap_states_item, SWAP_STATE_SUBTYPE),
|
||||
):
|
||||
item.setData(item_type, self._item_subtype_role)
|
||||
|
||||
for item in (
|
||||
select_all_item,
|
||||
deselect_all_item,
|
||||
swap_states_item,
|
||||
):
|
||||
item.setFlags(
|
||||
QtCore.Qt.ItemIsEnabled
|
||||
| QtCore.Qt.ItemIsSelectable
|
||||
| QtCore.Qt.ItemIsUserCheckable
|
||||
)
|
||||
|
||||
self._empty_values_item = empty_values_item
|
||||
self._select_project_item = select_project_item
|
||||
|
||||
self._select_all_item = select_all_item
|
||||
self._deselect_all_item = deselect_all_item
|
||||
self._swap_states_item = swap_states_item
|
||||
|
||||
def _get_empty_values_item(self):
|
||||
self._init_default_items()
|
||||
return self._empty_values_item
|
||||
|
||||
def _get_select_project_item(self):
|
||||
self._init_default_items()
|
||||
return self._select_project_item
|
||||
|
||||
def _get_empty_items(self):
|
||||
self._init_default_items()
|
||||
return [
|
||||
self._empty_values_item,
|
||||
self._select_project_item,
|
||||
]
|
||||
|
||||
def _get_selection_items(self):
|
||||
self._init_default_items()
|
||||
return [
|
||||
self._select_all_item,
|
||||
self._deselect_all_item,
|
||||
self._swap_states_item,
|
||||
]
|
||||
|
||||
def _get_default_items(self):
|
||||
return self._get_empty_items() + self._get_selection_items()
|
||||
|
||||
def _add_select_project_item(self):
|
||||
item = self._get_select_project_item()
|
||||
if item.row() < 0:
|
||||
self._remove_items()
|
||||
root_item = self.invisibleRootItem()
|
||||
root_item.appendRow(item)
|
||||
|
||||
def _add_empty_values_item(self):
|
||||
item = self._get_empty_values_item()
|
||||
if item.row() < 0:
|
||||
self._remove_items()
|
||||
root_item = self.invisibleRootItem()
|
||||
root_item.appendRow(item)
|
||||
|
||||
def _add_selection_items(self):
|
||||
root_item = self.invisibleRootItem()
|
||||
items = self._get_selection_items()
|
||||
for item in self._get_selection_items():
|
||||
row = item.row()
|
||||
if row >= 0:
|
||||
root_item.takeRow(row)
|
||||
root_item.appendRows(items)
|
||||
|
||||
def _remove_items(self):
|
||||
root_item = self.invisibleRootItem()
|
||||
for item in self._get_default_items():
|
||||
if item.row() < 0:
|
||||
continue
|
||||
root_item.takeRow(item.row())
|
||||
|
||||
root_item.removeRows(0, root_item.rowCount())
|
||||
self._clear_standard_items()
|
||||
|
||||
def _remove_empty_items(self):
|
||||
root_item = self.invisibleRootItem()
|
||||
for item in self._get_empty_items():
|
||||
if item.row() < 0:
|
||||
continue
|
||||
root_item.takeRow(item.row())
|
||||
|
||||
|
||||
class CustomPaintDelegate(QtWidgets.QStyledItemDelegate):
|
||||
"""Delegate showing status name and short name."""
|
||||
_empty_icon = None
|
||||
_checked_value = checkstate_enum_to_int(QtCore.Qt.Checked)
|
||||
_checked_bg_color = QtGui.QColor("#2C3B4C")
|
||||
|
||||
|
|
@ -38,6 +296,14 @@ class CustomPaintDelegate(QtWidgets.QStyledItemDelegate):
|
|||
self._icon_role = icon_role
|
||||
self._item_type_role = item_type_role
|
||||
|
||||
@classmethod
|
||||
def _get_empty_icon(cls):
|
||||
if cls._empty_icon is None:
|
||||
pix = QtGui.QPixmap(1, 1)
|
||||
pix.fill(QtCore.Qt.transparent)
|
||||
cls._empty_icon = QtGui.QIcon(pix)
|
||||
return cls._empty_icon
|
||||
|
||||
def paint(self, painter, option, index):
|
||||
item_type = None
|
||||
if self._item_type_role is not None:
|
||||
|
|
@ -70,6 +336,9 @@ class CustomPaintDelegate(QtWidgets.QStyledItemDelegate):
|
|||
if option.state & QtWidgets.QStyle.State_Open:
|
||||
state = QtGui.QIcon.On
|
||||
icon = self._get_index_icon(index)
|
||||
if icon is None or icon.isNull():
|
||||
icon = self._get_empty_icon()
|
||||
|
||||
option.features |= QtWidgets.QStyleOptionViewItem.HasDecoration
|
||||
|
||||
# Disable visible check indicator
|
||||
|
|
@ -241,6 +510,10 @@ class CustomPaintMultiselectComboBox(QtWidgets.QComboBox):
|
|||
QtCore.Qt.Key_Home,
|
||||
QtCore.Qt.Key_End,
|
||||
}
|
||||
_top_bottom_margins = 1
|
||||
_top_bottom_padding = 2
|
||||
_left_right_padding = 3
|
||||
_item_bg_color = QtGui.QColor("#31424e")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -433,14 +706,14 @@ class CustomPaintMultiselectComboBox(QtWidgets.QComboBox):
|
|||
|
||||
idxs = self._get_checked_idx()
|
||||
# draw the icon and text
|
||||
draw_text = True
|
||||
draw_items = False
|
||||
combotext = None
|
||||
if self._custom_text is not None:
|
||||
combotext = self._custom_text
|
||||
elif not idxs:
|
||||
combotext = self._placeholder_text
|
||||
else:
|
||||
draw_text = False
|
||||
draw_items = True
|
||||
|
||||
content_field_rect = self.style().subControlRect(
|
||||
QtWidgets.QStyle.CC_ComboBox,
|
||||
|
|
@ -448,7 +721,9 @@ class CustomPaintMultiselectComboBox(QtWidgets.QComboBox):
|
|||
QtWidgets.QStyle.SC_ComboBoxEditField
|
||||
).adjusted(1, 0, -1, 0)
|
||||
|
||||
if draw_text:
|
||||
if draw_items:
|
||||
self._paint_items(painter, idxs, content_field_rect)
|
||||
else:
|
||||
color = option.palette.color(QtGui.QPalette.Text)
|
||||
color.setAlpha(67)
|
||||
pen = painter.pen()
|
||||
|
|
@ -459,15 +734,12 @@ class CustomPaintMultiselectComboBox(QtWidgets.QComboBox):
|
|||
QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter,
|
||||
combotext
|
||||
)
|
||||
else:
|
||||
self._paint_items(painter, idxs, content_field_rect)
|
||||
|
||||
painter.end()
|
||||
|
||||
def _paint_items(self, painter, indexes, content_rect):
|
||||
origin_rect = QtCore.QRect(content_rect)
|
||||
|
||||
metrics = self.fontMetrics()
|
||||
model = self.model()
|
||||
available_width = content_rect.width()
|
||||
total_used_width = 0
|
||||
|
|
@ -482,10 +754,15 @@ class CustomPaintMultiselectComboBox(QtWidgets.QComboBox):
|
|||
continue
|
||||
|
||||
icon = index.data(self._icon_role)
|
||||
# TODO handle this case
|
||||
if icon is None or icon.isNull():
|
||||
continue
|
||||
text = index.data(self._text_role)
|
||||
valid_icon = icon is not None and not icon.isNull()
|
||||
if valid_icon:
|
||||
sizes = icon.availableSizes()
|
||||
if sizes:
|
||||
valid_icon = any(size.width() > 1 for size in sizes)
|
||||
|
||||
if valid_icon:
|
||||
metrics = self.fontMetrics()
|
||||
icon_rect = QtCore.QRect(content_rect)
|
||||
diff = icon_rect.height() - metrics.height()
|
||||
if diff < 0:
|
||||
|
|
@ -493,7 +770,14 @@ class CustomPaintMultiselectComboBox(QtWidgets.QComboBox):
|
|||
top_offset = diff // 2
|
||||
bottom_offset = diff - top_offset
|
||||
icon_rect.adjust(0, top_offset, 0, -bottom_offset)
|
||||
icon_rect.setWidth(metrics.height())
|
||||
used_width = metrics.height()
|
||||
if total_used_width > 0:
|
||||
total_used_width += spacing
|
||||
total_used_width += used_width
|
||||
if total_used_width > available_width:
|
||||
break
|
||||
|
||||
icon_rect.setWidth(used_width)
|
||||
icon.paint(
|
||||
painter,
|
||||
icon_rect,
|
||||
|
|
@ -502,12 +786,49 @@ class CustomPaintMultiselectComboBox(QtWidgets.QComboBox):
|
|||
QtGui.QIcon.On
|
||||
)
|
||||
content_rect.setLeft(icon_rect.right() + spacing)
|
||||
|
||||
elif text:
|
||||
bg_height = (
|
||||
content_rect.height()
|
||||
- (2 * self._top_bottom_margins)
|
||||
)
|
||||
font_height = bg_height - (2 * self._top_bottom_padding)
|
||||
|
||||
bg_top_y = content_rect.y() + self._top_bottom_margins
|
||||
|
||||
font = self.font()
|
||||
font.setPixelSize(font_height)
|
||||
metrics = QtGui.QFontMetrics(font)
|
||||
painter.setFont(font)
|
||||
|
||||
label_rect = metrics.boundingRect(text)
|
||||
|
||||
bg_width = label_rect.width() + (2 * self._left_right_padding)
|
||||
if total_used_width > 0:
|
||||
total_used_width += spacing
|
||||
total_used_width += icon_rect.width()
|
||||
total_used_width += bg_width
|
||||
if total_used_width > available_width:
|
||||
break
|
||||
|
||||
bg_rect = QtCore.QRectF(label_rect)
|
||||
bg_rect.moveTop(bg_top_y)
|
||||
bg_rect.moveLeft(content_rect.left())
|
||||
bg_rect.setWidth(bg_width)
|
||||
bg_rect.setHeight(bg_height)
|
||||
|
||||
label_rect.moveTop(bg_top_y)
|
||||
label_rect.moveLeft(
|
||||
content_rect.left() + self._left_right_padding
|
||||
)
|
||||
|
||||
path = QtGui.QPainterPath()
|
||||
path.addRoundedRect(bg_rect, 5, 5)
|
||||
|
||||
painter.fillPath(path, self._item_bg_color)
|
||||
painter.drawText(label_rect, QtCore.Qt.AlignCenter, text)
|
||||
|
||||
content_rect.setLeft(bg_rect.right() + spacing)
|
||||
|
||||
painter.restore()
|
||||
|
||||
if total_used_width > available_width:
|
||||
|
|
|
|||
169
client/ayon_core/tools/loader/ui/product_types_combo.py
Normal file
169
client/ayon_core/tools/loader/ui/product_types_combo.py
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
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,256 +0,0 @@
|
|||
from qtpy import QtWidgets, QtGui, QtCore
|
||||
|
||||
from ayon_core.tools.utils import get_qt_icon
|
||||
|
||||
PRODUCT_TYPE_ROLE = QtCore.Qt.UserRole + 1
|
||||
|
||||
|
||||
class ProductTypesQtModel(QtGui.QStandardItemModel):
|
||||
refreshed = QtCore.Signal()
|
||||
filter_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller):
|
||||
super(ProductTypesQtModel, self).__init__()
|
||||
self._controller = controller
|
||||
|
||||
self._reset_filters_on_refresh = True
|
||||
self._refreshing = False
|
||||
self._bulk_change = False
|
||||
self._last_project = None
|
||||
self._items_by_name = {}
|
||||
|
||||
controller.register_event_callback(
|
||||
"controller.reset.finished",
|
||||
self._on_controller_reset_finish,
|
||||
)
|
||||
|
||||
def is_refreshing(self):
|
||||
return self._refreshing
|
||||
|
||||
def get_filter_info(self):
|
||||
"""Product types filtering info.
|
||||
|
||||
Returns:
|
||||
dict[str, bool]: Filtering value by product type name. False value
|
||||
means to hide product type.
|
||||
"""
|
||||
|
||||
return {
|
||||
name: item.checkState() == QtCore.Qt.Checked
|
||||
for name, item in self._items_by_name.items()
|
||||
}
|
||||
|
||||
def refresh(self, project_name):
|
||||
self._refreshing = True
|
||||
product_type_items = self._controller.get_product_type_items(
|
||||
project_name)
|
||||
self._last_project = project_name
|
||||
|
||||
items_to_remove = set(self._items_by_name.keys())
|
||||
new_items = []
|
||||
items_filter_required = {}
|
||||
for product_type_item in product_type_items:
|
||||
name = product_type_item.name
|
||||
items_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)
|
||||
new_items.append(item)
|
||||
self._items_by_name[name] = item
|
||||
|
||||
if filter_required:
|
||||
items_filter_required[name] = item
|
||||
|
||||
icon = get_qt_icon(product_type_item.icon)
|
||||
item.setData(icon, QtCore.Qt.DecorationRole)
|
||||
|
||||
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)
|
||||
)
|
||||
state = (
|
||||
QtCore.Qt.Checked
|
||||
if matching % 2 == 0
|
||||
else QtCore.Qt.Unchecked
|
||||
)
|
||||
item.setCheckState(state)
|
||||
|
||||
root_item = self.invisibleRootItem()
|
||||
if new_items:
|
||||
root_item.appendRows(new_items)
|
||||
|
||||
for name in items_to_remove:
|
||||
item = self._items_by_name.pop(name)
|
||||
root_item.removeRow(item.row())
|
||||
|
||||
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 setData(self, index, value, role=None):
|
||||
checkstate_changed = False
|
||||
if role is None:
|
||||
role = QtCore.Qt.EditRole
|
||||
elif role == QtCore.Qt.CheckStateRole:
|
||||
checkstate_changed = True
|
||||
output = super(ProductTypesQtModel, self).setData(index, value, role)
|
||||
if checkstate_changed and not self._bulk_change:
|
||||
self.filter_changed.emit()
|
||||
return output
|
||||
|
||||
def change_state_for_all(self, checked):
|
||||
if self._items_by_name:
|
||||
self.change_states(checked, self._items_by_name.keys())
|
||||
|
||||
def change_states(self, checked, product_types):
|
||||
product_types = set(product_types)
|
||||
if not product_types:
|
||||
return
|
||||
|
||||
if checked is None:
|
||||
state = None
|
||||
elif checked:
|
||||
state = QtCore.Qt.Checked
|
||||
else:
|
||||
state = QtCore.Qt.Unchecked
|
||||
|
||||
self._bulk_change = True
|
||||
|
||||
changed = False
|
||||
for product_type in product_types:
|
||||
item = self._items_by_name.get(product_type)
|
||||
if item is None:
|
||||
continue
|
||||
new_state = state
|
||||
item_checkstate = item.checkState()
|
||||
if new_state is None:
|
||||
if item_checkstate == QtCore.Qt.Checked:
|
||||
new_state = QtCore.Qt.Unchecked
|
||||
else:
|
||||
new_state = QtCore.Qt.Checked
|
||||
elif item_checkstate == new_state:
|
||||
continue
|
||||
changed = True
|
||||
item.setCheckState(new_state)
|
||||
|
||||
self._bulk_change = False
|
||||
|
||||
if changed:
|
||||
self.filter_changed.emit()
|
||||
|
||||
def _on_controller_reset_finish(self):
|
||||
self.refresh(self._last_project)
|
||||
|
||||
|
||||
class ProductTypesView(QtWidgets.QListView):
|
||||
filter_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(ProductTypesView, self).__init__(parent)
|
||||
|
||||
self.setSelectionMode(
|
||||
QtWidgets.QAbstractItemView.ExtendedSelection
|
||||
)
|
||||
self.setAlternatingRowColors(True)
|
||||
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
|
||||
product_types_model = ProductTypesQtModel(controller)
|
||||
product_types_proxy_model = QtCore.QSortFilterProxyModel()
|
||||
product_types_proxy_model.setSourceModel(product_types_model)
|
||||
|
||||
self.setModel(product_types_proxy_model)
|
||||
|
||||
product_types_model.refreshed.connect(self._on_refresh_finished)
|
||||
product_types_model.filter_changed.connect(self._on_filter_change)
|
||||
self.customContextMenuRequested.connect(self._on_context_menu)
|
||||
|
||||
controller.register_event_callback(
|
||||
"selection.project.changed",
|
||||
self._on_project_change
|
||||
)
|
||||
|
||||
self._controller = controller
|
||||
self._refresh_product_types_filter = False
|
||||
|
||||
self._product_types_model = product_types_model
|
||||
self._product_types_proxy_model = product_types_proxy_model
|
||||
|
||||
def get_filter_info(self):
|
||||
return self._product_types_model.get_filter_info()
|
||||
|
||||
def reset_product_types_filter_on_refresh(self):
|
||||
self._product_types_model.reset_product_types_filter_on_refresh()
|
||||
|
||||
def _on_project_change(self, event):
|
||||
project_name = event["project_name"]
|
||||
self._product_types_model.refresh(project_name)
|
||||
|
||||
def _on_refresh_finished(self):
|
||||
# Apply product types filter on first show
|
||||
self.filter_changed.emit()
|
||||
|
||||
def _on_filter_change(self):
|
||||
if not self._product_types_model.is_refreshing():
|
||||
self.filter_changed.emit()
|
||||
|
||||
def _change_selection_state(self, checkstate):
|
||||
selection_model = self.selectionModel()
|
||||
product_types = {
|
||||
index.data(PRODUCT_TYPE_ROLE)
|
||||
for index in selection_model.selectedIndexes()
|
||||
}
|
||||
product_types.discard(None)
|
||||
self._product_types_model.change_states(checkstate, product_types)
|
||||
|
||||
def _on_enable_all(self):
|
||||
self._product_types_model.change_state_for_all(True)
|
||||
|
||||
def _on_disable_all(self):
|
||||
self._product_types_model.change_state_for_all(False)
|
||||
|
||||
def _on_context_menu(self, pos):
|
||||
menu = QtWidgets.QMenu(self)
|
||||
|
||||
# Add enable all action
|
||||
action_check_all = QtWidgets.QAction(menu)
|
||||
action_check_all.setText("Enable All")
|
||||
action_check_all.triggered.connect(self._on_enable_all)
|
||||
# Add disable all action
|
||||
action_uncheck_all = QtWidgets.QAction(menu)
|
||||
action_uncheck_all.setText("Disable All")
|
||||
action_uncheck_all.triggered.connect(self._on_disable_all)
|
||||
|
||||
menu.addAction(action_check_all)
|
||||
menu.addAction(action_uncheck_all)
|
||||
|
||||
# Get mouse position
|
||||
global_pos = self.viewport().mapToGlobal(pos)
|
||||
menu.exec_(global_pos)
|
||||
|
||||
def event(self, event):
|
||||
if event.type() == QtCore.QEvent.KeyPress:
|
||||
if event.key() == QtCore.Qt.Key_Space:
|
||||
self._change_selection_state(None)
|
||||
return True
|
||||
|
||||
if event.key() == QtCore.Qt.Key_Backspace:
|
||||
self._change_selection_state(False)
|
||||
return True
|
||||
|
||||
if event.key() == QtCore.Qt.Key_Return:
|
||||
self._change_selection_state(True)
|
||||
return True
|
||||
|
||||
return super(ProductTypesView, self).event(event)
|
||||
|
|
@ -19,6 +19,7 @@ from .products_model import (
|
|||
)
|
||||
|
||||
STATUS_NAME_ROLE = QtCore.Qt.UserRole + 1
|
||||
TASK_ID_ROLE = QtCore.Qt.UserRole + 2
|
||||
|
||||
|
||||
class VersionsModel(QtGui.QStandardItemModel):
|
||||
|
|
@ -48,6 +49,7 @@ class VersionsModel(QtGui.QStandardItemModel):
|
|||
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)
|
||||
|
||||
if item.row() != idx:
|
||||
root_item.insertRow(idx, item)
|
||||
|
|
@ -57,17 +59,30 @@ class VersionsFilterModel(QtCore.QSortFilterProxyModel):
|
|||
def __init__(self):
|
||||
super().__init__()
|
||||
self._status_filter = None
|
||||
self._task_ids_filter = None
|
||||
|
||||
def filterAcceptsRow(self, row, parent):
|
||||
if self._status_filter is None:
|
||||
return True
|
||||
|
||||
if self._status_filter is not None:
|
||||
if not self._status_filter:
|
||||
return False
|
||||
|
||||
index = self.sourceModel().index(row, 0, parent)
|
||||
status = index.data(STATUS_NAME_ROLE)
|
||||
return status in self._status_filter
|
||||
if status not in self._status_filter:
|
||||
return False
|
||||
|
||||
if self._task_ids_filter:
|
||||
index = self.sourceModel().index(row, 0, parent)
|
||||
task_id = index.data(TASK_ID_ROLE)
|
||||
if task_id not in self._task_ids_filter:
|
||||
return False
|
||||
return True
|
||||
|
||||
def set_tasks_filter(self, task_ids):
|
||||
if self._task_ids_filter == task_ids:
|
||||
return
|
||||
self._task_ids_filter = task_ids
|
||||
self.invalidateFilter()
|
||||
|
||||
def set_statuses_filter(self, status_names):
|
||||
if self._status_filter == status_names:
|
||||
|
|
@ -101,6 +116,13 @@ class VersionComboBox(QtWidgets.QComboBox):
|
|||
def get_product_id(self):
|
||||
return self._product_id
|
||||
|
||||
def set_tasks_filter(self, task_ids):
|
||||
self._proxy_model.set_tasks_filter(task_ids)
|
||||
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:
|
||||
|
|
@ -149,6 +171,7 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
|
|||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._editor_by_id: Dict[str, VersionComboBox] = {}
|
||||
self._task_ids_filter = None
|
||||
self._statuses_filter = None
|
||||
|
||||
def displayText(self, value, locale):
|
||||
|
|
@ -156,6 +179,11 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
|
|||
return "N/A"
|
||||
return format_version(value)
|
||||
|
||||
def set_tasks_filter(self, task_ids):
|
||||
self._task_ids_filter = set(task_ids)
|
||||
for widget in self._editor_by_id.values():
|
||||
widget.set_tasks_filter(task_ids)
|
||||
|
||||
def set_statuses_filter(self, status_names):
|
||||
self._statuses_filter = set(status_names)
|
||||
for widget in self._editor_by_id.values():
|
||||
|
|
@ -239,6 +267,7 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
|
|||
version_id = index.data(VERSION_ID_ROLE)
|
||||
|
||||
editor.update_versions(versions, version_id)
|
||||
editor.set_tasks_filter(self._task_ids_filter)
|
||||
editor.set_statuses_filter(self._statuses_filter)
|
||||
|
||||
def setModelData(self, editor, model, index):
|
||||
|
|
|
|||
|
|
@ -12,34 +12,35 @@ GROUP_TYPE_ROLE = QtCore.Qt.UserRole + 1
|
|||
MERGED_COLOR_ROLE = QtCore.Qt.UserRole + 2
|
||||
FOLDER_LABEL_ROLE = QtCore.Qt.UserRole + 3
|
||||
FOLDER_ID_ROLE = QtCore.Qt.UserRole + 4
|
||||
PRODUCT_ID_ROLE = QtCore.Qt.UserRole + 5
|
||||
PRODUCT_NAME_ROLE = QtCore.Qt.UserRole + 6
|
||||
PRODUCT_TYPE_ROLE = QtCore.Qt.UserRole + 7
|
||||
PRODUCT_TYPE_ICON_ROLE = QtCore.Qt.UserRole + 8
|
||||
PRODUCT_IN_SCENE_ROLE = QtCore.Qt.UserRole + 9
|
||||
VERSION_ID_ROLE = QtCore.Qt.UserRole + 10
|
||||
VERSION_HERO_ROLE = QtCore.Qt.UserRole + 11
|
||||
VERSION_NAME_ROLE = QtCore.Qt.UserRole + 12
|
||||
VERSION_NAME_EDIT_ROLE = QtCore.Qt.UserRole + 13
|
||||
VERSION_PUBLISH_TIME_ROLE = QtCore.Qt.UserRole + 14
|
||||
VERSION_STATUS_NAME_ROLE = QtCore.Qt.UserRole + 15
|
||||
VERSION_STATUS_SHORT_ROLE = QtCore.Qt.UserRole + 16
|
||||
VERSION_STATUS_COLOR_ROLE = QtCore.Qt.UserRole + 17
|
||||
VERSION_STATUS_ICON_ROLE = QtCore.Qt.UserRole + 18
|
||||
VERSION_AUTHOR_ROLE = QtCore.Qt.UserRole + 19
|
||||
VERSION_FRAME_RANGE_ROLE = QtCore.Qt.UserRole + 20
|
||||
VERSION_DURATION_ROLE = QtCore.Qt.UserRole + 21
|
||||
VERSION_HANDLES_ROLE = QtCore.Qt.UserRole + 22
|
||||
VERSION_STEP_ROLE = QtCore.Qt.UserRole + 23
|
||||
VERSION_AVAILABLE_ROLE = QtCore.Qt.UserRole + 24
|
||||
VERSION_THUMBNAIL_ID_ROLE = QtCore.Qt.UserRole + 25
|
||||
ACTIVE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 26
|
||||
REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 27
|
||||
REPRESENTATIONS_COUNT_ROLE = QtCore.Qt.UserRole + 28
|
||||
SYNC_ACTIVE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 29
|
||||
SYNC_REMOTE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 30
|
||||
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
|
||||
|
||||
STATUS_NAME_FILTER_ROLE = QtCore.Qt.UserRole + 31
|
||||
STATUS_NAME_FILTER_ROLE = QtCore.Qt.UserRole + 32
|
||||
|
||||
|
||||
class ProductsModel(QtGui.QStandardItemModel):
|
||||
|
|
@ -368,6 +369,7 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
|
||||
"""
|
||||
model_item.setData(version_item.version_id, VERSION_ID_ROLE)
|
||||
model_item.setData(version_item.task_id, TASK_ID_ROLE)
|
||||
model_item.setData(version_item.version, VERSION_NAME_ROLE)
|
||||
model_item.setData(version_item.is_hero, VERSION_HERO_ROLE)
|
||||
model_item.setData(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
from __future__ import annotations
|
||||
import collections
|
||||
from typing import Optional
|
||||
|
||||
from qtpy import QtWidgets, QtCore
|
||||
|
||||
|
|
@ -15,6 +17,7 @@ from .products_model import (
|
|||
GROUP_TYPE_ROLE,
|
||||
MERGED_COLOR_ROLE,
|
||||
FOLDER_ID_ROLE,
|
||||
TASK_ID_ROLE,
|
||||
PRODUCT_ID_ROLE,
|
||||
VERSION_ID_ROLE,
|
||||
VERSION_STATUS_NAME_ROLE,
|
||||
|
|
@ -36,8 +39,9 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel):
|
|||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self._product_type_filters = {}
|
||||
self._product_type_filters = None
|
||||
self._statuses_filter = None
|
||||
self._task_ids_filter = None
|
||||
self._ascending_sort = True
|
||||
|
||||
def get_statuses_filter(self):
|
||||
|
|
@ -45,7 +49,15 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel):
|
|||
return None
|
||||
return set(self._statuses_filter)
|
||||
|
||||
def set_tasks_filter(self, task_ids_filter):
|
||||
if self._task_ids_filter == task_ids_filter:
|
||||
return
|
||||
self._task_ids_filter = task_ids_filter
|
||||
self.invalidateFilter()
|
||||
|
||||
def set_product_type_filters(self, product_type_filters):
|
||||
if self._product_type_filters == product_type_filters:
|
||||
return
|
||||
self._product_type_filters = product_type_filters
|
||||
self.invalidateFilter()
|
||||
|
||||
|
|
@ -58,29 +70,41 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel):
|
|||
def filterAcceptsRow(self, source_row, source_parent):
|
||||
source_model = self.sourceModel()
|
||||
index = source_model.index(source_row, 0, source_parent)
|
||||
|
||||
product_types_s = source_model.data(index, PRODUCT_TYPE_ROLE)
|
||||
product_types = []
|
||||
if product_types_s:
|
||||
product_types = product_types_s.split("|")
|
||||
|
||||
for product_type in product_types:
|
||||
if not self._product_type_filters.get(product_type, True):
|
||||
if not self._accept_task_ids_filter(index):
|
||||
return False
|
||||
|
||||
if not self._accept_row_by_statuses(index):
|
||||
if not self._accept_row_by_role_value(
|
||||
index, self._product_type_filters, PRODUCT_TYPE_ROLE
|
||||
):
|
||||
return False
|
||||
|
||||
if not self._accept_row_by_role_value(
|
||||
index, self._statuses_filter, STATUS_NAME_FILTER_ROLE
|
||||
):
|
||||
return False
|
||||
|
||||
return super().filterAcceptsRow(source_row, source_parent)
|
||||
|
||||
def _accept_row_by_statuses(self, index):
|
||||
if self._statuses_filter is None:
|
||||
def _accept_task_ids_filter(self, index):
|
||||
if not self._task_ids_filter:
|
||||
return True
|
||||
if not self._statuses_filter:
|
||||
task_id = index.data(TASK_ID_ROLE)
|
||||
return task_id in self._task_ids_filter
|
||||
|
||||
def _accept_row_by_role_value(
|
||||
self,
|
||||
index: QtCore.QModelIndex,
|
||||
filter_value: Optional[set[str]],
|
||||
role: int
|
||||
):
|
||||
if filter_value is None:
|
||||
return True
|
||||
if not filter_value:
|
||||
return False
|
||||
|
||||
status_s = index.data(STATUS_NAME_FILTER_ROLE)
|
||||
status_s = index.data(role)
|
||||
for status in status_s.split("|"):
|
||||
if status in self._statuses_filter:
|
||||
if status in filter_value:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
|
@ -246,6 +270,16 @@ class ProductsWidget(QtWidgets.QWidget):
|
|||
"""
|
||||
self._products_proxy_model.setFilterFixedString(name)
|
||||
|
||||
def set_tasks_filter(self, task_ids):
|
||||
"""Set filter of version tasks.
|
||||
|
||||
Args:
|
||||
task_ids (set[str]): Task ids.
|
||||
|
||||
"""
|
||||
self._version_delegate.set_tasks_filter(task_ids)
|
||||
self._products_proxy_model.set_tasks_filter(task_ids)
|
||||
|
||||
def set_statuses_filter(self, status_names):
|
||||
"""Set filter of version statuses.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from typing import List, Dict
|
||||
from __future__ import annotations
|
||||
|
||||
from qtpy import QtCore, QtGui
|
||||
|
||||
|
|
@ -7,7 +7,7 @@ from ayon_core.tools.common_models import StatusItem
|
|||
|
||||
from ._multicombobox import (
|
||||
CustomPaintMultiselectComboBox,
|
||||
STANDARD_ITEM_TYPE,
|
||||
BaseQtModel,
|
||||
)
|
||||
|
||||
STATUS_ITEM_TYPE = 0
|
||||
|
|
@ -24,62 +24,43 @@ ITEM_TYPE_ROLE = QtCore.Qt.UserRole + 5
|
|||
ITEM_SUBTYPE_ROLE = QtCore.Qt.UserRole + 6
|
||||
|
||||
|
||||
class StatusesQtModel(QtGui.QStandardItemModel):
|
||||
class StatusesQtModel(BaseQtModel):
|
||||
def __init__(self, controller):
|
||||
self._controller = controller
|
||||
self._items_by_name: Dict[str, QtGui.QStandardItem] = {}
|
||||
self._icons_by_name_n_color: Dict[str, QtGui.QIcon] = {}
|
||||
self._last_project = None
|
||||
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,
|
||||
)
|
||||
|
||||
self._select_project_item = None
|
||||
self._empty_statuses_item = None
|
||||
def _get_standard_items(self) -> list[QtGui.QStandardItem]:
|
||||
return list(self._items_by_name.values())
|
||||
|
||||
self._select_all_item = None
|
||||
self._deselect_all_item = None
|
||||
self._swap_states_item = None
|
||||
def _clear_standard_items(self):
|
||||
self._items_by_name.clear()
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.refresh(None)
|
||||
|
||||
def get_placeholder_text(self):
|
||||
return self._placeholder
|
||||
|
||||
def refresh(self, project_name):
|
||||
# New project was selected
|
||||
# status filter is reset to show all statuses
|
||||
uncheck_all = False
|
||||
if project_name != self._last_project:
|
||||
self._last_project = project_name
|
||||
uncheck_all = True
|
||||
|
||||
if project_name is None:
|
||||
self._add_select_project_item()
|
||||
return
|
||||
|
||||
status_items: List[StatusItem] = (
|
||||
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:
|
||||
self._add_empty_statuses_item()
|
||||
return
|
||||
return items, items_to_remove
|
||||
|
||||
self._remove_empty_items()
|
||||
|
||||
items_to_remove = set(self._items_by_name)
|
||||
root_item = self.invisibleRootItem()
|
||||
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:
|
||||
is_new = False
|
||||
item = self._items_by_name[name]
|
||||
if uncheck_all:
|
||||
item.setCheckState(QtCore.Qt.Unchecked)
|
||||
items_to_remove.discard(name)
|
||||
names_to_remove.discard(name)
|
||||
else:
|
||||
is_new = True
|
||||
item = QtGui.QStandardItem()
|
||||
item.setData(ITEM_SUBTYPE_ROLE, STATUS_ITEM_TYPE)
|
||||
item.setCheckState(QtCore.Qt.Unchecked)
|
||||
|
|
@ -100,36 +81,14 @@ class StatusesQtModel(QtGui.QStandardItemModel):
|
|||
if item.data(role) != value:
|
||||
item.setData(value, role)
|
||||
|
||||
if is_new:
|
||||
root_item.insertRow(row_idx, item)
|
||||
|
||||
for name in items_to_remove:
|
||||
item = self._items_by_name.pop(name)
|
||||
root_item.removeRow(item.row())
|
||||
|
||||
self._add_selection_items()
|
||||
|
||||
def setData(self, index, value, role):
|
||||
if role == QtCore.Qt.CheckStateRole and index.isValid():
|
||||
item_type = index.data(ITEM_SUBTYPE_ROLE)
|
||||
if item_type == SELECT_ALL_TYPE:
|
||||
for item in self._items_by_name.values():
|
||||
item.setCheckState(QtCore.Qt.Checked)
|
||||
return True
|
||||
if item_type == DESELECT_ALL_TYPE:
|
||||
for item in self._items_by_name.values():
|
||||
if project_changed:
|
||||
item.setCheckState(QtCore.Qt.Unchecked)
|
||||
return True
|
||||
if item_type == SWAP_STATE_TYPE:
|
||||
for item in self._items_by_name.values():
|
||||
current_state = item.checkState()
|
||||
item.setCheckState(
|
||||
QtCore.Qt.Checked
|
||||
if current_state == QtCore.Qt.Unchecked
|
||||
else QtCore.Qt.Unchecked
|
||||
)
|
||||
return True
|
||||
return super().setData(index, value, role)
|
||||
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
|
||||
|
|
@ -147,139 +106,6 @@ class StatusesQtModel(QtGui.QStandardItemModel):
|
|||
self._icons_by_name_n_color[unique_id] = icon
|
||||
return icon
|
||||
|
||||
def _init_default_items(self):
|
||||
if self._empty_statuses_item is not None:
|
||||
return
|
||||
|
||||
empty_statuses_item = QtGui.QStandardItem("No statuses...")
|
||||
select_project_item = QtGui.QStandardItem("Select project...")
|
||||
|
||||
select_all_item = QtGui.QStandardItem("Select all")
|
||||
deselect_all_item = QtGui.QStandardItem("Deselect all")
|
||||
swap_states_item = QtGui.QStandardItem("Swap")
|
||||
|
||||
for item in (
|
||||
empty_statuses_item,
|
||||
select_project_item,
|
||||
select_all_item,
|
||||
deselect_all_item,
|
||||
swap_states_item,
|
||||
):
|
||||
item.setData(STANDARD_ITEM_TYPE, ITEM_TYPE_ROLE)
|
||||
|
||||
select_all_item.setIcon(get_qt_icon({
|
||||
"type": "material-symbols",
|
||||
"name": "done_all",
|
||||
"color": "white"
|
||||
}))
|
||||
deselect_all_item.setIcon(get_qt_icon({
|
||||
"type": "material-symbols",
|
||||
"name": "remove_done",
|
||||
"color": "white"
|
||||
}))
|
||||
swap_states_item.setIcon(get_qt_icon({
|
||||
"type": "material-symbols",
|
||||
"name": "swap_horiz",
|
||||
"color": "white"
|
||||
}))
|
||||
|
||||
for item in (
|
||||
empty_statuses_item,
|
||||
select_project_item,
|
||||
):
|
||||
item.setFlags(QtCore.Qt.NoItemFlags)
|
||||
|
||||
for item, item_type in (
|
||||
(select_all_item, SELECT_ALL_TYPE),
|
||||
(deselect_all_item, DESELECT_ALL_TYPE),
|
||||
(swap_states_item, SWAP_STATE_TYPE),
|
||||
):
|
||||
item.setData(item_type, ITEM_SUBTYPE_ROLE)
|
||||
|
||||
for item in (
|
||||
select_all_item,
|
||||
deselect_all_item,
|
||||
swap_states_item,
|
||||
):
|
||||
item.setFlags(
|
||||
QtCore.Qt.ItemIsEnabled
|
||||
| QtCore.Qt.ItemIsSelectable
|
||||
| QtCore.Qt.ItemIsUserCheckable
|
||||
)
|
||||
|
||||
self._empty_statuses_item = empty_statuses_item
|
||||
self._select_project_item = select_project_item
|
||||
|
||||
self._select_all_item = select_all_item
|
||||
self._deselect_all_item = deselect_all_item
|
||||
self._swap_states_item = swap_states_item
|
||||
|
||||
def _get_empty_statuses_item(self):
|
||||
self._init_default_items()
|
||||
return self._empty_statuses_item
|
||||
|
||||
def _get_select_project_item(self):
|
||||
self._init_default_items()
|
||||
return self._select_project_item
|
||||
|
||||
def _get_empty_items(self):
|
||||
self._init_default_items()
|
||||
return [
|
||||
self._empty_statuses_item,
|
||||
self._select_project_item,
|
||||
]
|
||||
|
||||
def _get_selection_items(self):
|
||||
self._init_default_items()
|
||||
return [
|
||||
self._select_all_item,
|
||||
self._deselect_all_item,
|
||||
self._swap_states_item,
|
||||
]
|
||||
|
||||
def _get_default_items(self):
|
||||
return self._get_empty_items() + self._get_selection_items()
|
||||
|
||||
def _add_select_project_item(self):
|
||||
item = self._get_select_project_item()
|
||||
if item.row() < 0:
|
||||
self._remove_items()
|
||||
root_item = self.invisibleRootItem()
|
||||
root_item.appendRow(item)
|
||||
|
||||
def _add_empty_statuses_item(self):
|
||||
item = self._get_empty_statuses_item()
|
||||
if item.row() < 0:
|
||||
self._remove_items()
|
||||
root_item = self.invisibleRootItem()
|
||||
root_item.appendRow(item)
|
||||
|
||||
def _add_selection_items(self):
|
||||
root_item = self.invisibleRootItem()
|
||||
items = self._get_selection_items()
|
||||
for item in self._get_selection_items():
|
||||
row = item.row()
|
||||
if row >= 0:
|
||||
root_item.takeRow(row)
|
||||
root_item.appendRows(items)
|
||||
|
||||
def _remove_items(self):
|
||||
root_item = self.invisibleRootItem()
|
||||
for item in self._get_default_items():
|
||||
if item.row() < 0:
|
||||
continue
|
||||
root_item.takeRow(item.row())
|
||||
|
||||
root_item.removeRows(0, root_item.rowCount())
|
||||
self._items_by_name.clear()
|
||||
|
||||
def _remove_empty_items(self):
|
||||
root_item = self.invisibleRootItem()
|
||||
for item in self._get_empty_items():
|
||||
if item.row() < 0:
|
||||
continue
|
||||
root_item.takeRow(item.row())
|
||||
|
||||
|
||||
class StatusesCombobox(CustomPaintMultiselectComboBox):
|
||||
def __init__(self, controller, parent):
|
||||
|
|
|
|||
405
client/ayon_core/tools/loader/ui/tasks_widget.py
Normal file
405
client/ayon_core/tools/loader/ui/tasks_widget.py
Normal file
|
|
@ -0,0 +1,405 @@
|
|||
import collections
|
||||
import hashlib
|
||||
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
||||
from ayon_core.style import get_default_entity_icon_color
|
||||
from ayon_core.tools.utils import (
|
||||
RecursiveSortFilterProxyModel,
|
||||
DeselectableTreeView,
|
||||
TasksQtModel,
|
||||
TASKS_MODEL_SENDER_NAME,
|
||||
)
|
||||
from ayon_core.tools.utils.tasks_widget import (
|
||||
ITEM_ID_ROLE,
|
||||
ITEM_NAME_ROLE,
|
||||
PARENT_ID_ROLE,
|
||||
TASK_TYPE_ROLE,
|
||||
)
|
||||
from ayon_core.tools.utils.lib import RefreshThread, get_qt_icon
|
||||
|
||||
# Role that can't clash with default 'tasks_widget' roles
|
||||
FOLDER_LABEL_ROLE = QtCore.Qt.UserRole + 100
|
||||
NO_TASKS_ID = "--no-task--"
|
||||
|
||||
|
||||
class LoaderTasksQtModel(TasksQtModel):
|
||||
column_labels = [
|
||||
"Task name",
|
||||
"Task type",
|
||||
"Folder"
|
||||
]
|
||||
|
||||
def __init__(self, controller):
|
||||
super().__init__(controller)
|
||||
|
||||
self._items_by_id = {}
|
||||
self._groups_by_name = {}
|
||||
self._last_folder_ids = set()
|
||||
# This item is used to be able filter versions without any task
|
||||
# - do not mismatch with '_empty_tasks_item' item from 'TasksQtModel'
|
||||
self._no_tasks_item = None
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh tasks for selected folders."""
|
||||
|
||||
self._refresh(self._last_project_name, self._last_folder_ids)
|
||||
|
||||
def set_context(self, project_name, folder_ids):
|
||||
self._refresh(project_name, folder_ids)
|
||||
|
||||
# Mark some functions from 'TasksQtModel' as not implemented
|
||||
def get_index_by_name(self, task_name):
|
||||
raise NotImplementedError(
|
||||
"Method 'get_index_by_name' is not implemented."
|
||||
)
|
||||
|
||||
def get_last_folder_id(self):
|
||||
raise NotImplementedError(
|
||||
"Method 'get_last_folder_id' is not implemented."
|
||||
)
|
||||
|
||||
def flags(self, index):
|
||||
if index.column() != 0:
|
||||
index = self.index(index.row(), 0, index.parent())
|
||||
return super().flags(index)
|
||||
|
||||
def _get_no_tasks_item(self):
|
||||
if self._no_tasks_item is None:
|
||||
item = QtGui.QStandardItem("No task")
|
||||
icon = get_qt_icon({
|
||||
"type": "material-symbols",
|
||||
"name": "indeterminate_check_box",
|
||||
"color": get_default_entity_icon_color(),
|
||||
})
|
||||
item.setData(icon, QtCore.Qt.DecorationRole)
|
||||
item.setData(NO_TASKS_ID, ITEM_ID_ROLE)
|
||||
item.setEditable(False)
|
||||
self._no_tasks_item = item
|
||||
return self._no_tasks_item
|
||||
|
||||
def _refresh(self, project_name, folder_ids):
|
||||
self._is_refreshing = True
|
||||
self._last_project_name = project_name
|
||||
self._last_folder_ids = folder_ids
|
||||
if not folder_ids:
|
||||
self._add_invalid_selection_item()
|
||||
self._current_refresh_thread = None
|
||||
self._is_refreshing = False
|
||||
self.refreshed.emit()
|
||||
return
|
||||
|
||||
thread_id = hashlib.sha256(
|
||||
"|".join(sorted(folder_ids)).encode()
|
||||
).hexdigest()
|
||||
thread = self._refresh_threads.get(thread_id)
|
||||
if thread is not None:
|
||||
self._current_refresh_thread = thread
|
||||
return
|
||||
thread = RefreshThread(
|
||||
thread_id,
|
||||
self._thread_getter,
|
||||
project_name,
|
||||
folder_ids
|
||||
)
|
||||
self._current_refresh_thread = thread
|
||||
self._refresh_threads[thread.id] = thread
|
||||
thread.refresh_finished.connect(self._on_refresh_thread)
|
||||
thread.start()
|
||||
|
||||
def _thread_getter(self, project_name, folder_ids):
|
||||
task_items = self._controller.get_task_items(
|
||||
project_name, folder_ids, sender=TASKS_MODEL_SENDER_NAME
|
||||
)
|
||||
task_type_items = {}
|
||||
if hasattr(self._controller, "get_task_type_items"):
|
||||
task_type_items = self._controller.get_task_type_items(
|
||||
project_name, sender=TASKS_MODEL_SENDER_NAME
|
||||
)
|
||||
folder_ids = {
|
||||
task_item.parent_id
|
||||
for task_item in task_items
|
||||
}
|
||||
folder_labels_by_id = self._controller.get_folder_labels(
|
||||
project_name, folder_ids
|
||||
)
|
||||
return task_items, task_type_items, folder_labels_by_id
|
||||
|
||||
def _on_refresh_thread(self, thread_id):
|
||||
"""Callback when refresh thread is finished.
|
||||
|
||||
Technically can be running multiple refresh threads at the same time,
|
||||
to avoid using values from wrong thread, we check if thread id is
|
||||
current refresh thread id.
|
||||
|
||||
Tasks are stored by name, so if a folder has same task name as
|
||||
previously selected folder it keeps the selection.
|
||||
|
||||
Args:
|
||||
thread_id (str): Thread id.
|
||||
"""
|
||||
|
||||
# Make sure to remove thread from '_refresh_threads' dict
|
||||
thread = self._refresh_threads.pop(thread_id)
|
||||
if (
|
||||
self._current_refresh_thread is None
|
||||
or thread_id != self._current_refresh_thread.id
|
||||
):
|
||||
return
|
||||
|
||||
self._fill_data_from_thread(thread)
|
||||
|
||||
root_item = self.invisibleRootItem()
|
||||
self._has_content = root_item.rowCount() > 0
|
||||
self._current_refresh_thread = None
|
||||
self._is_refreshing = False
|
||||
self.refreshed.emit()
|
||||
|
||||
def _clear_items(self):
|
||||
self._items_by_id = {}
|
||||
self._groups_by_name = {}
|
||||
super()._clear_items()
|
||||
|
||||
def _fill_data_from_thread(self, thread):
|
||||
task_items, task_type_items, folder_labels_by_id = thread.get_result()
|
||||
# Task items are refreshed
|
||||
if task_items is None:
|
||||
return
|
||||
|
||||
# No tasks are available on folder
|
||||
if not task_items:
|
||||
self._add_empty_task_item()
|
||||
return
|
||||
self._remove_invalid_items()
|
||||
|
||||
task_type_item_by_name = {
|
||||
task_type_item.name: task_type_item
|
||||
for task_type_item in task_type_items
|
||||
}
|
||||
task_type_icon_cache = {}
|
||||
current_ids = set()
|
||||
items_by_name = collections.defaultdict(list)
|
||||
for task_item in task_items:
|
||||
task_id = task_item.task_id
|
||||
current_ids.add(task_id)
|
||||
item = self._items_by_id.get(task_id)
|
||||
if item is None:
|
||||
item = QtGui.QStandardItem()
|
||||
item.setColumnCount(self.columnCount())
|
||||
item.setEditable(False)
|
||||
self._items_by_id[task_id] = item
|
||||
|
||||
icon = self._get_task_item_icon(
|
||||
task_item,
|
||||
task_type_item_by_name,
|
||||
task_type_icon_cache
|
||||
)
|
||||
name = task_item.name
|
||||
folder_id = task_item.parent_id
|
||||
folder_label = folder_labels_by_id.get(folder_id)
|
||||
|
||||
item.setData(name, QtCore.Qt.DisplayRole)
|
||||
item.setData(name, ITEM_NAME_ROLE)
|
||||
item.setData(task_item.id, ITEM_ID_ROLE)
|
||||
item.setData(task_item.task_type, TASK_TYPE_ROLE)
|
||||
item.setData(folder_id, PARENT_ID_ROLE)
|
||||
item.setData(folder_label, FOLDER_LABEL_ROLE)
|
||||
item.setData(icon, QtCore.Qt.DecorationRole)
|
||||
|
||||
items_by_name[name].append(item)
|
||||
|
||||
root_item = self.invisibleRootItem()
|
||||
|
||||
# Make sure item is not parented
|
||||
# - this is laziness to avoid re-parenting items which does
|
||||
# complicate the code with no benefit
|
||||
queue = collections.deque()
|
||||
queue.append((None, root_item))
|
||||
while queue:
|
||||
(parent, item) = queue.popleft()
|
||||
if not item.hasChildren():
|
||||
if parent:
|
||||
parent.takeRow(item.row())
|
||||
continue
|
||||
|
||||
for row in range(item.rowCount()):
|
||||
queue.append((item, item.child(row, 0)))
|
||||
|
||||
queue.append((parent, item))
|
||||
|
||||
used_group_names = set()
|
||||
new_root_items = [
|
||||
self._get_no_tasks_item()
|
||||
]
|
||||
for name, items in items_by_name.items():
|
||||
if len(items) == 1:
|
||||
new_root_items.extend(items)
|
||||
continue
|
||||
|
||||
used_group_names.add(name)
|
||||
group_item = self._groups_by_name.get(name)
|
||||
if group_item is None:
|
||||
group_item = QtGui.QStandardItem()
|
||||
group_item.setData(name, QtCore.Qt.DisplayRole)
|
||||
group_item.setEditable(False)
|
||||
group_item.setColumnCount(self.columnCount())
|
||||
self._groups_by_name[name] = group_item
|
||||
|
||||
# Use icon from first item
|
||||
first_item_icon = items[0].data(QtCore.Qt.DecorationRole)
|
||||
task_ids = [
|
||||
item.data(ITEM_ID_ROLE)
|
||||
for item in items
|
||||
]
|
||||
|
||||
group_item.setData(first_item_icon, QtCore.Qt.DecorationRole)
|
||||
group_item.setData("|".join(task_ids), ITEM_ID_ROLE)
|
||||
|
||||
group_item.appendRows(items)
|
||||
|
||||
new_root_items.append(group_item)
|
||||
|
||||
# Remove unused caches
|
||||
for task_id in set(self._items_by_id) - current_ids:
|
||||
self._items_by_id.pop(task_id)
|
||||
|
||||
for name in set(self._groups_by_name) - used_group_names:
|
||||
self._groups_by_name.pop(name)
|
||||
|
||||
if new_root_items:
|
||||
root_item.appendRows(new_root_items)
|
||||
|
||||
def data(self, index, role=None):
|
||||
if not index.isValid():
|
||||
return None
|
||||
|
||||
if role is None:
|
||||
role = QtCore.Qt.DisplayRole
|
||||
|
||||
col = index.column()
|
||||
if col != 0:
|
||||
index = self.index(index.row(), 0, index.parent())
|
||||
|
||||
if col == 1:
|
||||
if role == QtCore.Qt.DisplayRole:
|
||||
role = TASK_TYPE_ROLE
|
||||
else:
|
||||
return None
|
||||
|
||||
if col == 2:
|
||||
if role == QtCore.Qt.DisplayRole:
|
||||
role = FOLDER_LABEL_ROLE
|
||||
else:
|
||||
return None
|
||||
|
||||
return super().data(index, role)
|
||||
|
||||
|
||||
class LoaderTasksProxyModel(RecursiveSortFilterProxyModel):
|
||||
def lessThan(self, left, right):
|
||||
if left.data(ITEM_ID_ROLE) == NO_TASKS_ID:
|
||||
return False
|
||||
if right.data(ITEM_ID_ROLE) == NO_TASKS_ID:
|
||||
return True
|
||||
return super().lessThan(left, right)
|
||||
|
||||
|
||||
class LoaderTasksWidget(QtWidgets.QWidget):
|
||||
refreshed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super().__init__(parent)
|
||||
|
||||
tasks_view = DeselectableTreeView(self)
|
||||
tasks_view.setSelectionMode(
|
||||
QtWidgets.QAbstractItemView.ExtendedSelection
|
||||
)
|
||||
|
||||
tasks_model = LoaderTasksQtModel(controller)
|
||||
tasks_proxy_model = LoaderTasksProxyModel()
|
||||
tasks_proxy_model.setSourceModel(tasks_model)
|
||||
tasks_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
|
||||
tasks_view.setModel(tasks_proxy_model)
|
||||
# Hide folder column by default
|
||||
tasks_view.setColumnHidden(2, True)
|
||||
|
||||
main_layout = QtWidgets.QHBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.addWidget(tasks_view, 1)
|
||||
|
||||
controller.register_event_callback(
|
||||
"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)
|
||||
|
||||
tasks_model.refreshed.connect(self._on_model_refresh)
|
||||
|
||||
self._controller = controller
|
||||
self._tasks_view = tasks_view
|
||||
self._tasks_model = tasks_model
|
||||
self._tasks_proxy_model = tasks_proxy_model
|
||||
|
||||
self._fisrt_show = True
|
||||
|
||||
def showEvent(self, event):
|
||||
super().showEvent(event)
|
||||
if self._fisrt_show:
|
||||
self._fisrt_show = False
|
||||
header_widget = self._tasks_view.header()
|
||||
header_widget.resizeSection(0, 200)
|
||||
|
||||
def set_name_filter(self, name):
|
||||
"""Set filter of folder name.
|
||||
|
||||
Args:
|
||||
name (str): The string filter.
|
||||
|
||||
"""
|
||||
self._tasks_proxy_model.setFilterFixedString(name)
|
||||
if name:
|
||||
self._tasks_view.expandAll()
|
||||
|
||||
def refresh(self):
|
||||
self._tasks_model.refresh()
|
||||
|
||||
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"]
|
||||
self._tasks_view.setColumnHidden(2, len(folder_ids) == 1)
|
||||
self._tasks_model.set_context(project_name, folder_ids)
|
||||
|
||||
def _on_model_refresh(self):
|
||||
self._tasks_proxy_model.sort(0)
|
||||
self.refreshed.emit()
|
||||
|
||||
def _get_selected_item_ids(self):
|
||||
selection_model = self._tasks_view.selectionModel()
|
||||
item_ids = set()
|
||||
for index in selection_model.selectedIndexes():
|
||||
item_id = index.data(ITEM_ID_ROLE)
|
||||
if item_id is None:
|
||||
continue
|
||||
if item_id == NO_TASKS_ID:
|
||||
item_ids.add(None)
|
||||
else:
|
||||
item_ids |= set(item_id.split("|"))
|
||||
return item_ids
|
||||
|
||||
def _on_selection_change(self):
|
||||
item_ids = self._get_selected_item_ids()
|
||||
self._controller.set_selected_tasks(item_ids)
|
||||
|
|
@ -14,8 +14,9 @@ from ayon_core.tools.utils import ProjectsCombobox
|
|||
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_widget import ProductTypesView
|
||||
from .product_types_combo import ProductTypesCombobox
|
||||
from .product_group_dialog import ProductGroupDialog
|
||||
from .info_widget import InfoWidget
|
||||
from .repres_widget import RepresentationsWidget
|
||||
|
|
@ -164,16 +165,16 @@ class LoaderWindow(QtWidgets.QWidget):
|
|||
|
||||
folders_widget = LoaderFoldersWidget(controller, context_widget)
|
||||
|
||||
product_types_widget = ProductTypesView(controller, context_splitter)
|
||||
|
||||
context_layout = QtWidgets.QVBoxLayout(context_widget)
|
||||
context_layout.setContentsMargins(0, 0, 0, 0)
|
||||
context_layout.addWidget(context_top_widget, 0)
|
||||
context_layout.addWidget(folders_filter_input, 0)
|
||||
context_layout.addWidget(folders_widget, 1)
|
||||
|
||||
tasks_widget = LoaderTasksWidget(controller, context_widget)
|
||||
|
||||
context_splitter.addWidget(context_widget)
|
||||
context_splitter.addWidget(product_types_widget)
|
||||
context_splitter.addWidget(tasks_widget)
|
||||
context_splitter.setStretchFactor(0, 65)
|
||||
context_splitter.setStretchFactor(1, 35)
|
||||
|
||||
|
|
@ -185,6 +186,10 @@ class LoaderWindow(QtWidgets.QWidget):
|
|||
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)
|
||||
|
||||
product_group_checkbox = QtWidgets.QCheckBox(
|
||||
|
|
@ -196,6 +201,7 @@ class LoaderWindow(QtWidgets.QWidget):
|
|||
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(product_group_checkbox, 0)
|
||||
|
||||
|
|
@ -244,12 +250,12 @@ class LoaderWindow(QtWidgets.QWidget):
|
|||
folders_filter_input.textChanged.connect(
|
||||
self._on_folder_filter_change
|
||||
)
|
||||
product_types_widget.filter_changed.connect(
|
||||
self._on_product_type_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
|
||||
)
|
||||
|
|
@ -280,6 +286,10 @@ class LoaderWindow(QtWidgets.QWidget):
|
|||
"selection.folders.changed",
|
||||
self._on_folders_selection_changed,
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"selection.tasks.changed",
|
||||
self._on_tasks_selection_change,
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"selection.versions.changed",
|
||||
self._on_versions_selection_changed,
|
||||
|
|
@ -304,9 +314,10 @@ class LoaderWindow(QtWidgets.QWidget):
|
|||
self._folders_filter_input = folders_filter_input
|
||||
self._folders_widget = folders_widget
|
||||
|
||||
self._product_types_widget = product_types_widget
|
||||
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._product_group_checkbox = product_group_checkbox
|
||||
self._products_widget = products_widget
|
||||
|
|
@ -335,7 +346,7 @@ class LoaderWindow(QtWidgets.QWidget):
|
|||
self._controller.reset()
|
||||
|
||||
def showEvent(self, event):
|
||||
super(LoaderWindow, self).showEvent(event)
|
||||
super().showEvent(event)
|
||||
|
||||
if self._first_show:
|
||||
self._on_first_show()
|
||||
|
|
@ -343,9 +354,13 @@ class LoaderWindow(QtWidgets.QWidget):
|
|||
self._show_timer.start()
|
||||
|
||||
def closeEvent(self, event):
|
||||
super(LoaderWindow, self).closeEvent(event)
|
||||
super().closeEvent(event)
|
||||
|
||||
self._product_types_widget.reset_product_types_filter_on_refresh()
|
||||
(
|
||||
self
|
||||
._product_types_filter_combo
|
||||
.reset_product_types_filter_on_refresh()
|
||||
)
|
||||
|
||||
self._reset_on_show = True
|
||||
|
||||
|
|
@ -363,7 +378,7 @@ class LoaderWindow(QtWidgets.QWidget):
|
|||
event.setAccepted(True)
|
||||
return
|
||||
|
||||
super(LoaderWindow, self).keyPressEvent(event)
|
||||
super().keyPressEvent(event)
|
||||
|
||||
def _on_first_show(self):
|
||||
self._first_show = False
|
||||
|
|
@ -423,14 +438,16 @@ class LoaderWindow(QtWidgets.QWidget):
|
|||
def _on_product_filter_change(self, text):
|
||||
self._products_widget.set_name_filter(text)
|
||||
|
||||
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):
|
||||
self._products_widget.set_product_type_filter(
|
||||
self._product_types_widget.get_filter_info()
|
||||
)
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ from . import settings, util
|
|||
from .awesome import tags as awesome
|
||||
from qtpy import QtCore, QtGui
|
||||
import qtawesome
|
||||
from six import text_type
|
||||
from .constants import PluginStates, InstanceStates, GroupStates, Roles
|
||||
|
||||
|
||||
|
|
@ -985,7 +984,7 @@ class TerminalModel(QtGui.QStandardItemModel):
|
|||
record_item = record
|
||||
else:
|
||||
record_item = {
|
||||
"label": text_type(record.msg),
|
||||
"label": str(record.msg),
|
||||
"type": "record",
|
||||
"levelno": record.levelno,
|
||||
"threadName": record.threadName,
|
||||
|
|
@ -993,7 +992,7 @@ class TerminalModel(QtGui.QStandardItemModel):
|
|||
"filename": record.filename,
|
||||
"pathname": record.pathname,
|
||||
"lineno": record.lineno,
|
||||
"msg": text_type(record.msg),
|
||||
"msg": str(record.msg),
|
||||
"msecs": record.msecs,
|
||||
"levelname": record.levelname
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import sys
|
|||
import collections
|
||||
|
||||
from qtpy import QtCore
|
||||
from six import text_type
|
||||
import pyblish.api
|
||||
|
||||
root = os.path.dirname(__file__)
|
||||
|
|
@ -64,7 +63,7 @@ def u_print(msg, **kwargs):
|
|||
**kwargs: Keyword argument for `print` function.
|
||||
"""
|
||||
|
||||
if isinstance(msg, text_type):
|
||||
if isinstance(msg, str):
|
||||
encoding = None
|
||||
try:
|
||||
encoding = os.getenv('PYTHONIOENCODING', sys.stdout.encoding)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ from __future__ import print_function
|
|||
import json
|
||||
import os
|
||||
|
||||
import six
|
||||
from qtpy import QtCore, QtGui
|
||||
|
||||
|
||||
|
|
@ -152,7 +151,7 @@ class IconicFont(QtCore.QObject):
|
|||
def hook(obj):
|
||||
result = {}
|
||||
for key in obj:
|
||||
result[key] = six.unichr(int(obj[key], 16))
|
||||
result[key] = chr(int(obj[key], 16))
|
||||
return result
|
||||
|
||||
if directory is None:
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import os
|
|||
|
||||
import pyblish.api
|
||||
|
||||
from ayon_core.host import ILoadHost
|
||||
from ayon_core.host import ILoadHost, IPublishHost
|
||||
from ayon_core.lib import Logger
|
||||
from ayon_core.pipeline import registered_host
|
||||
|
||||
|
|
@ -236,7 +236,7 @@ class HostToolsHelper:
|
|||
from ayon_core.tools.publisher.window import PublisherWindow
|
||||
|
||||
host = registered_host()
|
||||
ILoadHost.validate_load_methods(host)
|
||||
IPublishHost.validate_publish_methods(host)
|
||||
|
||||
publisher_window = PublisherWindow(
|
||||
controller=controller,
|
||||
|
|
|
|||
|
|
@ -24,9 +24,14 @@ class TasksQtModel(QtGui.QStandardItemModel):
|
|||
"""
|
||||
_default_task_icon = None
|
||||
refreshed = QtCore.Signal()
|
||||
column_labels = ["Tasks"]
|
||||
|
||||
def __init__(self, controller):
|
||||
super(TasksQtModel, self).__init__()
|
||||
super().__init__()
|
||||
|
||||
self.setColumnCount(len(self.column_labels))
|
||||
for idx, label in enumerate(self.column_labels):
|
||||
self.setHeaderData(idx, QtCore.Qt.Horizontal, label)
|
||||
|
||||
self._controller = controller
|
||||
|
||||
|
|
@ -53,7 +58,8 @@ class TasksQtModel(QtGui.QStandardItemModel):
|
|||
self._has_content = False
|
||||
self._remove_invalid_items()
|
||||
root_item = self.invisibleRootItem()
|
||||
root_item.removeRows(0, root_item.rowCount())
|
||||
while root_item.rowCount() != 0:
|
||||
root_item.takeRow(0)
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh tasks for last project and folder."""
|
||||
|
|
@ -336,19 +342,6 @@ class TasksQtModel(QtGui.QStandardItemModel):
|
|||
|
||||
return self._has_content
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
# Show nice labels in the header
|
||||
if (
|
||||
role == QtCore.Qt.DisplayRole
|
||||
and orientation == QtCore.Qt.Horizontal
|
||||
):
|
||||
if section == 0:
|
||||
return "Tasks"
|
||||
|
||||
return super(TasksQtModel, self).headerData(
|
||||
section, orientation, role
|
||||
)
|
||||
|
||||
|
||||
class TasksWidget(QtWidgets.QWidget):
|
||||
"""Tasks widget.
|
||||
|
|
@ -365,7 +358,7 @@ class TasksWidget(QtWidgets.QWidget):
|
|||
selection_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent, handle_expected_selection=False):
|
||||
super(TasksWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
tasks_view = DeselectableTreeView(self)
|
||||
tasks_view.setIndentation(0)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ class DeselectableTreeView(QtWidgets.QTreeView):
|
|||
"""A tree view that deselects on clicking on an empty area in the view"""
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
|
||||
index = self.indexAt(event.pos())
|
||||
if not index.isValid():
|
||||
# clear the selection
|
||||
|
|
@ -15,7 +14,14 @@ class DeselectableTreeView(QtWidgets.QTreeView):
|
|||
# clear the current index
|
||||
self.setCurrentIndex(QtCore.QModelIndex())
|
||||
|
||||
QtWidgets.QTreeView.mousePressEvent(self, event)
|
||||
elif (
|
||||
self.selectionModel().isSelected(index)
|
||||
and len(self.selectionModel().selectedRows()) == 1
|
||||
and event.modifiers() == QtCore.Qt.NoModifier
|
||||
):
|
||||
event.setModifiers(QtCore.Qt.ControlModifier)
|
||||
|
||||
super().mousePressEvent(event)
|
||||
|
||||
|
||||
class TreeView(QtWidgets.QTreeView):
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring AYON addon 'core' version."""
|
||||
__version__ = "1.1.0+dev"
|
||||
__version__ = "1.1.3+dev"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
name = "core"
|
||||
title = "Core"
|
||||
version = "1.1.0+dev"
|
||||
version = "1.1.3+dev"
|
||||
|
||||
client_dir = "ayon_core"
|
||||
|
||||
|
|
|
|||
255
poetry.lock
generated
255
poetry.lock
generated
|
|
@ -1,15 +1,4 @@
|
|||
# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
description = "Reusable constraint types to use with typing.Annotated"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
|
||||
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
|
||||
]
|
||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "appdirs"
|
||||
|
|
@ -24,13 +13,13 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "24.3.0"
|
||||
version = "25.1.0"
|
||||
description = "Classes Without Boilerplate"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"},
|
||||
{file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"},
|
||||
{file = "attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a"},
|
||||
{file = "attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
|
|
@ -317,6 +306,22 @@ files = [
|
|||
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mock"
|
||||
version = "5.2.0"
|
||||
description = "Rolling backport of unittest.mock for all Pythons"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "mock-5.2.0-py3-none-any.whl", hash = "sha256:7ba87f72ca0e915175596069dbbcc7c75af7b5e9b9bc107ad6349ede0819982f"},
|
||||
{file = "mock-5.2.0.tar.gz", hash = "sha256:4e460e818629b4b173f32d08bf30d3af8123afbb8e04bb5707a1fd4799e503f0"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
build = ["blurb", "twine", "wheel"]
|
||||
docs = ["sphinx"]
|
||||
test = ["pytest", "pytest-cov"]
|
||||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
version = "1.14.0"
|
||||
|
|
@ -392,6 +397,55 @@ files = [
|
|||
{file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentimelineio"
|
||||
version = "0.17.0"
|
||||
description = "Editorial interchange format and API"
|
||||
optional = false
|
||||
python-versions = "!=3.9.0,>=3.7"
|
||||
files = [
|
||||
{file = "OpenTimelineIO-0.17.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:2dd31a570cabfd6227c1b1dd0cc038da10787492c26c55de058326e21fe8a313"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5a1da5d4803d1ba5e846b181a9e0f4a392c76b9acc5e08947772bc086f2ebfc0"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3527977aec8202789a42d60e1e0dc11b4154f585ef72921760445f43e7967a00"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b3aafb4c50455832ed2627c2cac654b896473a5c1f8348ddc07c10be5cfbd59"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp310-cp310-win32.whl", hash = "sha256:fee45af9f6330773893cd0858e92f8256bb5bde4229b44a76f03e59a9fb1b1b6"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:d51887619689c21d67cc4b11b1088f99ae44094513315e7a144be00f1393bfa8"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:cbf05c3e8c0187969f79e91f7495d1f0dc3609557874d8e601ba2e072c70ddb1"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d3430c3f4e88c5365d7b6afbee920b0815b62ecf141abe44cd739c9eedc04284"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1912345227b0bd1654c7153863eadbcee60362aa46340678e576e5d2aa3106a"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51e06eb11a868d970c1534e39faf916228d5163bf3598076d408d8f393ab0bd4"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp311-cp311-win32.whl", hash = "sha256:5c3a3f4780b25a8c1a80d788becba691d12b629069ad8783d0db21027639276f"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:43c8726b33af30ba42928972192311ea0f986edbbd5f74651bada182d4fe805c"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:9a9af4105a088c0ab131780e49db268db7e37871aac33db842de6b2b16f14e39"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8e653ad1dd3b85f5c312a742dc24b61b330964aa391dc5bc072fe8b9c85adff1"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a77823c27a1b93c6b87682372c3734ac5fddc10bfe53875e657d43c60fb885"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4f4efcf3ddd81b62c4feb49a0bcc309b50ffeb6a8c48ab173d169a029006f4d"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp312-cp312-win32.whl", hash = "sha256:9872ab74a20bb2bb3a50af04e80fe9238998d67d6be4e30e45aebe25d3eefac6"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:c83b78be3312d3152d7e07ab32b0086fe220acc2a5b035b70ad69a787c0ece62"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:0e671a6f2a1f772445bb326c7640dc977cfc3db589fe108a783a0311939cfac8"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b931a3189b4ce064f06f15a89fe08ef4de01f7dcf0abc441fe2e02ef2a3311bb"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:923cb54d806c981cf1e91916c3e57fba5664c22f37763dd012bad5a5a7bd4db4"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp37-cp37m-win32.whl", hash = "sha256:8e16598c5084dcb21df3d83978b0e5f72300af9edd4cdcb85e3b0ba5da0df4e8"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp37-cp37m-win_amd64.whl", hash = "sha256:7eed5033494888fb3f802af50e60559e279b2f398802748872903c2f54efd2c9"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:118baa22b9227da5003bee653601a68686ae2823682dcd7d13c88178c63081c3"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:43389eacdee2169de454e1c79ecfea82f54a9e73b67151427a9b621349a22b7f"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:17659b1e6aa42ed617a942f7a2bfc6ecc375d0464ec127ce9edf896278ecaee9"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36d5ea8cfbebf3c9013cc680eef5be48bffb515aafa9dc31e99bf66052a4ca3d"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp38-cp38-win32.whl", hash = "sha256:cc67c74eb4b73bc0f7d135d3ff3dbbd86b2d451a9b142690a8d1631ad79c46f2"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:69b39079bee6fa4aff34c6ad6544df394bc7388483fa5ce958ecd16e243a53ad"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:a33554894dea17c22feec0201991e705c2c90a679ba2a012a0c558a7130df711"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6b1ad3b3155370245b851b2f7b60006b2ebbb5bb76dd0fdc49bb4dce73fa7d96"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:030454a9c0e9e82e5a153119f9afb8f3f4e64a3b27f80ac0dcde44b029fd3f3f"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bce64376a28919533bd4f744ff8885118abefa73f78fd408f95fa7a9489855b6"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp39-cp39-win32.whl", hash = "sha256:fa8cdceb25f9003c3c0b5b32baef2c764949d88b867161ddc6f44f48f6bbfa4a"},
|
||||
{file = "OpenTimelineIO-0.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:fbcf8a000cd688633c8dc5d22e91912013c67c674329eba603358e3b54da32bf"},
|
||||
{file = "opentimelineio-0.17.0.tar.gz", hash = "sha256:10ef324e710457e9977387cd9ef91eb24a9837bfb370aec3330f9c0f146cea85"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["check-manifest", "coverage (>=4.5)", "flake8 (>=3.5)", "urllib3 (>=1.24.3)"]
|
||||
view = ["PySide2 (>=5.11,<6.0)", "PySide6 (>=6.2,<7.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "24.2"
|
||||
|
|
@ -463,138 +517,6 @@ files = [
|
|||
{file = "pyblish_base-1.8.12-py2.py3-none-any.whl", hash = "sha256:2cbe956bfbd4175a2d7d22b344cd345800f4d4437153434ab658fc12646a11e8"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.10.4"
|
||||
description = "Data validation using Python type hints"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pydantic-2.10.4-py3-none-any.whl", hash = "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d"},
|
||||
{file = "pydantic-2.10.4.tar.gz", hash = "sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
annotated-types = ">=0.6.0"
|
||||
pydantic-core = "2.27.2"
|
||||
typing-extensions = ">=4.12.2"
|
||||
|
||||
[package.extras]
|
||||
email = ["email-validator (>=2.0.0)"]
|
||||
timezone = ["tzdata"]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.27.2"
|
||||
description = "Core functionality for Pydantic validation and serialization"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"},
|
||||
{file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"},
|
||||
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"},
|
||||
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"},
|
||||
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"},
|
||||
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"},
|
||||
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"},
|
||||
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"},
|
||||
{file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"},
|
||||
{file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"},
|
||||
{file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"},
|
||||
{file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"},
|
||||
{file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"},
|
||||
{file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"},
|
||||
{file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"},
|
||||
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"},
|
||||
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"},
|
||||
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"},
|
||||
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"},
|
||||
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"},
|
||||
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"},
|
||||
{file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"},
|
||||
{file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"},
|
||||
{file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"},
|
||||
{file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"},
|
||||
{file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"},
|
||||
{file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"},
|
||||
{file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"},
|
||||
{file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"},
|
||||
{file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"},
|
||||
{file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"},
|
||||
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"},
|
||||
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"},
|
||||
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"},
|
||||
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"},
|
||||
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"},
|
||||
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"},
|
||||
{file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"},
|
||||
{file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"},
|
||||
{file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"},
|
||||
{file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"},
|
||||
{file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"},
|
||||
{file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"},
|
||||
{file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"},
|
||||
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"},
|
||||
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"},
|
||||
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"},
|
||||
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"},
|
||||
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"},
|
||||
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"},
|
||||
{file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"},
|
||||
{file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"},
|
||||
{file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"},
|
||||
{file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"},
|
||||
{file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"},
|
||||
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"},
|
||||
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"},
|
||||
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"},
|
||||
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"},
|
||||
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"},
|
||||
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"},
|
||||
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"},
|
||||
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"},
|
||||
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"},
|
||||
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"},
|
||||
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"},
|
||||
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"},
|
||||
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"},
|
||||
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"},
|
||||
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"},
|
||||
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"},
|
||||
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"},
|
||||
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"},
|
||||
{file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.3.4"
|
||||
|
|
@ -719,28 +641,29 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
|||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.3.7"
|
||||
version = "0.9.9"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0e8377cccb2f07abd25e84fc5b2cbe48eeb0fea9f1719cad7caedb061d70e5ce"},
|
||||
{file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:15a4d1cc1e64e556fa0d67bfd388fed416b7f3b26d5d1c3e7d192c897e39ba4b"},
|
||||
{file = "ruff-0.3.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d28bdf3d7dc71dd46929fafeec98ba89b7c3550c3f0978e36389b5631b793663"},
|
||||
{file = "ruff-0.3.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:379b67d4f49774ba679593b232dcd90d9e10f04d96e3c8ce4a28037ae473f7bb"},
|
||||
{file = "ruff-0.3.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c060aea8ad5ef21cdfbbe05475ab5104ce7827b639a78dd55383a6e9895b7c51"},
|
||||
{file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ebf8f615dde968272d70502c083ebf963b6781aacd3079081e03b32adfe4d58a"},
|
||||
{file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d48098bd8f5c38897b03604f5428901b65e3c97d40b3952e38637b5404b739a2"},
|
||||
{file = "ruff-0.3.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da8a4fda219bf9024692b1bc68c9cff4b80507879ada8769dc7e985755d662ea"},
|
||||
{file = "ruff-0.3.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c44e0149f1d8b48c4d5c33d88c677a4aa22fd09b1683d6a7ff55b816b5d074f"},
|
||||
{file = "ruff-0.3.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3050ec0af72b709a62ecc2aca941b9cd479a7bf2b36cc4562f0033d688e44fa1"},
|
||||
{file = "ruff-0.3.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a29cc38e4c1ab00da18a3f6777f8b50099d73326981bb7d182e54a9a21bb4ff7"},
|
||||
{file = "ruff-0.3.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b15cc59c19edca917f51b1956637db47e200b0fc5e6e1878233d3a938384b0b"},
|
||||
{file = "ruff-0.3.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e491045781b1e38b72c91247cf4634f040f8d0cb3e6d3d64d38dcf43616650b4"},
|
||||
{file = "ruff-0.3.7-py3-none-win32.whl", hash = "sha256:bc931de87593d64fad3a22e201e55ad76271f1d5bfc44e1a1887edd0903c7d9f"},
|
||||
{file = "ruff-0.3.7-py3-none-win_amd64.whl", hash = "sha256:5ef0e501e1e39f35e03c2acb1d1238c595b8bb36cf7a170e7c1df1b73da00e74"},
|
||||
{file = "ruff-0.3.7-py3-none-win_arm64.whl", hash = "sha256:789e144f6dc7019d1f92a812891c645274ed08af6037d11fc65fcbc183b7d59f"},
|
||||
{file = "ruff-0.3.7.tar.gz", hash = "sha256:d5c1aebee5162c2226784800ae031f660c350e7a3402c4d1f8ea4e97e232e3ba"},
|
||||
{file = "ruff-0.9.9-py3-none-linux_armv6l.whl", hash = "sha256:628abb5ea10345e53dff55b167595a159d3e174d6720bf19761f5e467e68d367"},
|
||||
{file = "ruff-0.9.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6cd1428e834b35d7493354723543b28cc11dc14d1ce19b685f6e68e07c05ec7"},
|
||||
{file = "ruff-0.9.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5ee162652869120ad260670706f3cd36cd3f32b0c651f02b6da142652c54941d"},
|
||||
{file = "ruff-0.9.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3aa0f6b75082c9be1ec5a1db78c6d4b02e2375c3068438241dc19c7c306cc61a"},
|
||||
{file = "ruff-0.9.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:584cc66e89fb5f80f84b05133dd677a17cdd86901d6479712c96597a3f28e7fe"},
|
||||
{file = "ruff-0.9.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abf3369325761a35aba75cd5c55ba1b5eb17d772f12ab168fbfac54be85cf18c"},
|
||||
{file = "ruff-0.9.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3403a53a32a90ce929aa2f758542aca9234befa133e29f4933dcef28a24317be"},
|
||||
{file = "ruff-0.9.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:18454e7fa4e4d72cffe28a37cf6a73cb2594f81ec9f4eca31a0aaa9ccdfb1590"},
|
||||
{file = "ruff-0.9.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fadfe2c88724c9617339f62319ed40dcdadadf2888d5afb88bf3adee7b35bfb"},
|
||||
{file = "ruff-0.9.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6df104d08c442a1aabcfd254279b8cc1e2cbf41a605aa3e26610ba1ec4acf0b0"},
|
||||
{file = "ruff-0.9.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d7c62939daf5b2a15af48abbd23bea1efdd38c312d6e7c4cedf5a24e03207e17"},
|
||||
{file = "ruff-0.9.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9494ba82a37a4b81b6a798076e4a3251c13243fc37967e998efe4cce58c8a8d1"},
|
||||
{file = "ruff-0.9.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4efd7a96ed6d36ef011ae798bf794c5501a514be369296c672dab7921087fa57"},
|
||||
{file = "ruff-0.9.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ab90a7944c5a1296f3ecb08d1cbf8c2da34c7e68114b1271a431a3ad30cb660e"},
|
||||
{file = "ruff-0.9.9-py3-none-win32.whl", hash = "sha256:6b4c376d929c25ecd6d87e182a230fa4377b8e5125a4ff52d506ee8c087153c1"},
|
||||
{file = "ruff-0.9.9-py3-none-win_amd64.whl", hash = "sha256:837982ea24091d4c1700ddb2f63b7070e5baec508e43b01de013dc7eff974ff1"},
|
||||
{file = "ruff-0.9.9-py3-none-win_arm64.whl", hash = "sha256:3ac78f127517209fe6d96ab00f3ba97cafe38718b23b1db3e96d8b2d39e37ddf"},
|
||||
{file = "ruff-0.9.9.tar.gz", hash = "sha256:0062ed13f22173e85f8f7056f9a24016e692efeea8704d1a5e8011b8aa850933"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -857,4 +780,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.9.1,<3.10"
|
||||
content-hash = "5fb5a45697502e537b9f6cf618d744a4cece4803ef65f6315186e83d1cd90f3a"
|
||||
content-hash = "8e5b1f886eb608198752e1ce84f6bc89d1c8d53a5eeabff84a4272538f81403f"
|
||||
|
|
|
|||
|
|
@ -5,20 +5,16 @@
|
|||
|
||||
[tool.poetry]
|
||||
name = "ayon-core"
|
||||
version = "1.1.0+dev"
|
||||
version = "1.1.3+dev"
|
||||
description = ""
|
||||
authors = ["Ynput Team <team@ynput.io>"]
|
||||
readme = "README.md"
|
||||
package-mode = false
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.9.1,<3.10"
|
||||
pre-commit = "^4.0.0"
|
||||
clique = "^2"
|
||||
pyblish-base = "^1.8"
|
||||
attrs = "^24.2.0"
|
||||
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
# test dependencies
|
||||
pytest = "^8.0"
|
||||
pytest-print = "^1.0"
|
||||
|
|
@ -29,6 +25,11 @@ pre-commit = "^4"
|
|||
codespell = "^2.2.6"
|
||||
semver = "^3.0.2"
|
||||
mypy = "^1.14.0"
|
||||
mock = "^5.0.0"
|
||||
attrs = "^25.0.0"
|
||||
pyblish-base = "^1.8.7"
|
||||
clique = "^2.0.0"
|
||||
opentimelineio = "^0.17.0"
|
||||
|
||||
|
||||
[tool.ruff]
|
||||
|
|
|
|||
|
|
@ -68,6 +68,59 @@ class ContributionLayersModel(BaseSettingsModel):
|
|||
"layer on top.")
|
||||
|
||||
|
||||
class CollectUSDLayerContributionsProfileModel(BaseSettingsModel):
|
||||
"""Profiles to define instance attribute defaults for USD contribution."""
|
||||
_layout = "expanded"
|
||||
product_types: list[str] = SettingsField(
|
||||
default_factory=list,
|
||||
title="Product types",
|
||||
description=(
|
||||
"The product types to match this profile to. When matched, the"
|
||||
" settings below would apply to the instance as default"
|
||||
" attributes."
|
||||
),
|
||||
section="Filter"
|
||||
)
|
||||
task_types: list[str] = SettingsField(
|
||||
default_factory=list,
|
||||
title="Task Types",
|
||||
enum_resolver=task_types_enum,
|
||||
description=(
|
||||
"The current create context task type to filter against. This"
|
||||
" allows to filter the profile to only be valid if currently "
|
||||
" creating from within that task type."
|
||||
),
|
||||
)
|
||||
contribution_layer: str = SettingsField(
|
||||
"",
|
||||
title="Contribution Department Layer",
|
||||
description=(
|
||||
"The default contribution layer to apply the contribution to when"
|
||||
" matching this profile. The layer name should be in the"
|
||||
" 'Department Layer Orders' list to get a sensible order."
|
||||
),
|
||||
section="Instance attribute defaults",
|
||||
)
|
||||
contribution_apply_as_variant: bool = SettingsField(
|
||||
True,
|
||||
title="Apply as variant",
|
||||
description=(
|
||||
"The default 'Apply as variant' state for instances matching this"
|
||||
" profile. Usually enabled for asset contributions and disabled"
|
||||
" for shot contributions."
|
||||
),
|
||||
)
|
||||
contribution_target_product: str = SettingsField(
|
||||
"usdAsset",
|
||||
title="Target Product",
|
||||
description=(
|
||||
"The default destination product name to apply the contribution to"
|
||||
" when matching this profile."
|
||||
" Usually e.g. 'usdAsset' or 'usdShot'."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class CollectUSDLayerContributionsModel(BaseSettingsModel):
|
||||
enabled: bool = SettingsField(True, title="Enabled")
|
||||
contribution_layers: list[ContributionLayersModel] = SettingsField(
|
||||
|
|
@ -77,6 +130,14 @@ class CollectUSDLayerContributionsModel(BaseSettingsModel):
|
|||
"ordering inside the USD contribution workflow."
|
||||
)
|
||||
)
|
||||
profiles: list[CollectUSDLayerContributionsProfileModel] = SettingsField(
|
||||
default_factory=list,
|
||||
title="Profiles",
|
||||
description=(
|
||||
"Define attribute defaults for USD Contributions on publish"
|
||||
" instances."
|
||||
)
|
||||
)
|
||||
|
||||
@validator("contribution_layers")
|
||||
def validate_unique_outputs(cls, value):
|
||||
|
|
@ -1017,6 +1078,43 @@ DEFAULT_PUBLISH_VALUES = {
|
|||
{"name": "fx", "order": 500},
|
||||
{"name": "lighting", "order": 600},
|
||||
],
|
||||
"profiles": [
|
||||
{
|
||||
"product_types": ["model"],
|
||||
"task_types": [],
|
||||
"contribution_layer": "model",
|
||||
"contribution_apply_as_variant": True,
|
||||
"contribution_target_product": "usdAsset"
|
||||
},
|
||||
{
|
||||
"product_types": ["look"],
|
||||
"task_types": [],
|
||||
"contribution_layer": "look",
|
||||
"contribution_apply_as_variant": True,
|
||||
"contribution_target_product": "usdAsset"
|
||||
},
|
||||
{
|
||||
"product_types": ["groom"],
|
||||
"task_types": [],
|
||||
"contribution_layer": "groom",
|
||||
"contribution_apply_as_variant": True,
|
||||
"contribution_target_product": "usdAsset"
|
||||
},
|
||||
{
|
||||
"product_types": ["rig"],
|
||||
"task_types": [],
|
||||
"contribution_layer": "rig",
|
||||
"contribution_apply_as_variant": True,
|
||||
"contribution_target_product": "usdAsset"
|
||||
},
|
||||
{
|
||||
"product_types": ["usd"],
|
||||
"task_types": [],
|
||||
"contribution_layer": "assembly",
|
||||
"contribution_apply_as_variant": False,
|
||||
"contribution_target_product": "usdShot"
|
||||
},
|
||||
]
|
||||
},
|
||||
"ValidateEditorialAssetName": {
|
||||
"enabled": True,
|
||||
|
|
|
|||
135
tests/client/ayon_core/lib/test_env_tools.py
Normal file
135
tests/client/ayon_core/lib/test_env_tools.py
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from ayon_core.lib.env_tools import (
|
||||
CycleError,
|
||||
DynamicKeyClashError,
|
||||
parse_env_variables_structure,
|
||||
compute_env_variables_structure,
|
||||
)
|
||||
|
||||
# --- Test data ---
|
||||
COMPUTE_SRC_ENV = {
|
||||
"COMPUTE_VERSION": "1.0.0",
|
||||
# Will be available only for darwin
|
||||
"COMPUTE_ONE_PLATFORM": {
|
||||
"darwin": "Compute macOs",
|
||||
},
|
||||
"COMPUTE_LOCATION": {
|
||||
"darwin": "/compute-app-{COMPUTE_VERSION}",
|
||||
"linux": "/usr/compute-app-{COMPUTE_VERSION}",
|
||||
"windows": "C:/Program Files/compute-app-{COMPUTE_VERSION}"
|
||||
},
|
||||
"PATH_LIST": {
|
||||
"darwin": ["{COMPUTE_LOCATION}/bin", "{COMPUTE_LOCATION}/bin2"],
|
||||
"linux": ["{COMPUTE_LOCATION}/bin", "{COMPUTE_LOCATION}/bin2"],
|
||||
"windows": ["{COMPUTE_LOCATION}/bin", "{COMPUTE_LOCATION}/bin2"],
|
||||
},
|
||||
"PATH_STR": {
|
||||
"darwin": "{COMPUTE_LOCATION}/bin:{COMPUTE_LOCATION}/bin2",
|
||||
"linux": "{COMPUTE_LOCATION}/bin:{COMPUTE_LOCATION}/bin2",
|
||||
"windows": "{COMPUTE_LOCATION}/bin;{COMPUTE_LOCATION}/bin2",
|
||||
},
|
||||
}
|
||||
|
||||
# --- RESULTS ---
|
||||
# --- Parse results ---
|
||||
PARSE_RESULT_WINDOWS = {
|
||||
"COMPUTE_VERSION": "1.0.0",
|
||||
"COMPUTE_LOCATION": "C:/Program Files/compute-app-{COMPUTE_VERSION}",
|
||||
"PATH_LIST": "{COMPUTE_LOCATION}/bin;{COMPUTE_LOCATION}/bin2",
|
||||
"PATH_STR": "{COMPUTE_LOCATION}/bin;{COMPUTE_LOCATION}/bin2",
|
||||
}
|
||||
|
||||
PARSE_RESULT_LINUX = {
|
||||
"COMPUTE_VERSION": "1.0.0",
|
||||
"COMPUTE_LOCATION": "/usr/compute-app-{COMPUTE_VERSION}",
|
||||
"PATH_LIST": "{COMPUTE_LOCATION}/bin:{COMPUTE_LOCATION}/bin2",
|
||||
"PATH_STR": "{COMPUTE_LOCATION}/bin:{COMPUTE_LOCATION}/bin2",
|
||||
}
|
||||
|
||||
PARSE_RESULT_DARWIN = {
|
||||
"COMPUTE_VERSION": "1.0.0",
|
||||
"COMPUTE_ONE_PLATFORM": "Compute macOs",
|
||||
"COMPUTE_LOCATION": "/compute-app-{COMPUTE_VERSION}",
|
||||
"PATH_LIST": "{COMPUTE_LOCATION}/bin:{COMPUTE_LOCATION}/bin2",
|
||||
"PATH_STR": "{COMPUTE_LOCATION}/bin:{COMPUTE_LOCATION}/bin2",
|
||||
}
|
||||
|
||||
# --- Compute results ---
|
||||
COMPUTE_RESULT_WINDOWS = {
|
||||
"COMPUTE_VERSION": "1.0.0",
|
||||
"COMPUTE_LOCATION": "C:/Program Files/compute-app-1.0.0",
|
||||
"PATH_LIST": (
|
||||
"C:/Program Files/compute-app-1.0.0/bin"
|
||||
";C:/Program Files/compute-app-1.0.0/bin2"
|
||||
),
|
||||
"PATH_STR": (
|
||||
"C:/Program Files/compute-app-1.0.0/bin"
|
||||
";C:/Program Files/compute-app-1.0.0/bin2"
|
||||
)
|
||||
}
|
||||
|
||||
COMPUTE_RESULT_LINUX = {
|
||||
"COMPUTE_VERSION": "1.0.0",
|
||||
"COMPUTE_LOCATION": "/usr/compute-app-1.0.0",
|
||||
"PATH_LIST": "/usr/compute-app-1.0.0/bin:/usr/compute-app-1.0.0/bin2",
|
||||
"PATH_STR": "/usr/compute-app-1.0.0/bin:/usr/compute-app-1.0.0/bin2"
|
||||
}
|
||||
|
||||
COMPUTE_RESULT_DARWIN = {
|
||||
"COMPUTE_VERSION": "1.0.0",
|
||||
"COMPUTE_ONE_PLATFORM": "Compute macOs",
|
||||
"COMPUTE_LOCATION": "/compute-app-1.0.0",
|
||||
"PATH_LIST": "/compute-app-1.0.0/bin:/compute-app-1.0.0/bin2",
|
||||
"PATH_STR": "/compute-app-1.0.0/bin:/compute-app-1.0.0/bin2"
|
||||
}
|
||||
|
||||
|
||||
class EnvParseCompute(unittest.TestCase):
|
||||
def test_parse_env(self):
|
||||
with patch("platform.system", return_value="windows"):
|
||||
result = parse_env_variables_structure(COMPUTE_SRC_ENV)
|
||||
assert result == PARSE_RESULT_WINDOWS
|
||||
|
||||
with patch("platform.system", return_value="linux"):
|
||||
result = parse_env_variables_structure(COMPUTE_SRC_ENV)
|
||||
assert result == PARSE_RESULT_LINUX
|
||||
|
||||
with patch("platform.system", return_value="darwin"):
|
||||
result = parse_env_variables_structure(COMPUTE_SRC_ENV)
|
||||
assert result == PARSE_RESULT_DARWIN
|
||||
|
||||
def test_compute_env(self):
|
||||
with patch("platform.system", return_value="windows"):
|
||||
result = compute_env_variables_structure(
|
||||
parse_env_variables_structure(COMPUTE_SRC_ENV)
|
||||
)
|
||||
assert result == COMPUTE_RESULT_WINDOWS
|
||||
|
||||
with patch("platform.system", return_value="linux"):
|
||||
result = compute_env_variables_structure(
|
||||
parse_env_variables_structure(COMPUTE_SRC_ENV)
|
||||
)
|
||||
assert result == COMPUTE_RESULT_LINUX
|
||||
|
||||
with patch("platform.system", return_value="darwin"):
|
||||
result = compute_env_variables_structure(
|
||||
parse_env_variables_structure(COMPUTE_SRC_ENV)
|
||||
)
|
||||
assert result == COMPUTE_RESULT_DARWIN
|
||||
|
||||
def test_cycle_error(self):
|
||||
with self.assertRaises(CycleError):
|
||||
compute_env_variables_structure({
|
||||
"KEY_1": "{KEY_2}",
|
||||
"KEY_2": "{KEY_1}",
|
||||
})
|
||||
|
||||
def test_dynamic_key_error(self):
|
||||
with self.assertRaises(DynamicKeyClashError):
|
||||
compute_env_variables_structure({
|
||||
"KEY_A": "Occupied",
|
||||
"SUBKEY": "A",
|
||||
"KEY_{SUBKEY}": "Resolves as occupied key",
|
||||
})
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,128 @@
|
|||
import os
|
||||
|
||||
import opentimelineio as otio
|
||||
|
||||
from ayon_core.plugins.publish import collect_otio_frame_ranges
|
||||
|
||||
|
||||
_RESOURCE_DIR = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"resources",
|
||||
"timeline"
|
||||
)
|
||||
|
||||
|
||||
class MockInstance():
|
||||
""" Mock pyblish instance for testing purpose.
|
||||
"""
|
||||
def __init__(self, data: dict):
|
||||
self.data = data
|
||||
self.context = self
|
||||
|
||||
|
||||
def _check_expected_frame_range_values(
|
||||
clip_name: str,
|
||||
expected_data: dict,
|
||||
handle_start: int = 10,
|
||||
handle_end: int = 10,
|
||||
retimed: bool = False,
|
||||
):
|
||||
file_path = os.path.join(_RESOURCE_DIR, "timeline.json")
|
||||
otio_timeline = otio.schema.Timeline.from_json_file(file_path)
|
||||
|
||||
for otio_clip in otio_timeline.find_clips():
|
||||
if otio_clip.name == clip_name:
|
||||
break
|
||||
|
||||
instance_data = {
|
||||
"otioClip": otio_clip,
|
||||
"handleStart": handle_start,
|
||||
"handleEnd": handle_end,
|
||||
"workfileFrameStart": 1001,
|
||||
}
|
||||
if retimed:
|
||||
instance_data["shotDurationFromSource"] = True
|
||||
|
||||
instance = MockInstance(instance_data)
|
||||
|
||||
processor = collect_otio_frame_ranges.CollectOtioRanges()
|
||||
processor.process(instance)
|
||||
|
||||
# Assert expected data is subset of edited instance.
|
||||
assert expected_data.items() <= instance.data.items()
|
||||
|
||||
|
||||
def test_movie_with_timecode():
|
||||
"""
|
||||
Movie clip (with embedded timecode)
|
||||
available_range = 86531-86590 23.976fps
|
||||
source_range = 86535-86586 23.976fps
|
||||
"""
|
||||
expected_data = {
|
||||
'frameStart': 1001,
|
||||
'frameEnd': 1052,
|
||||
'clipIn': 24,
|
||||
'clipOut': 75,
|
||||
'clipInH': 14,
|
||||
'clipOutH': 85,
|
||||
'sourceStart': 86535,
|
||||
'sourceStartH': 86525,
|
||||
'sourceEnd': 86586,
|
||||
'sourceEndH': 86596,
|
||||
}
|
||||
|
||||
_check_expected_frame_range_values(
|
||||
"sh010",
|
||||
expected_data,
|
||||
)
|
||||
|
||||
|
||||
def test_image_sequence():
|
||||
"""
|
||||
EXR image sequence.
|
||||
available_range = 87399-87482 24fps
|
||||
source_range = 87311-87336 23.976fps
|
||||
"""
|
||||
expected_data = {
|
||||
'frameStart': 1001,
|
||||
'frameEnd': 1026,
|
||||
'clipIn': 76,
|
||||
'clipOut': 101,
|
||||
'clipInH': 66,
|
||||
'clipOutH': 111,
|
||||
'sourceStart': 87399,
|
||||
'sourceStartH': 87389,
|
||||
'sourceEnd': 87424,
|
||||
'sourceEndH': 87434,
|
||||
}
|
||||
|
||||
_check_expected_frame_range_values(
|
||||
"img_sequence_exr",
|
||||
expected_data,
|
||||
)
|
||||
|
||||
def test_media_retimed():
|
||||
"""
|
||||
EXR image sequence.
|
||||
available_range = 345619-345691 23.976fps
|
||||
source_range = 345623-345687 23.976fps
|
||||
TimeWarp = frozen frame.
|
||||
"""
|
||||
expected_data = {
|
||||
'frameStart': 1001,
|
||||
'frameEnd': 1065,
|
||||
'clipIn': 127,
|
||||
'clipOut': 191,
|
||||
'clipInH': 117,
|
||||
'clipOutH': 201,
|
||||
'sourceStart': 1001,
|
||||
'sourceStartH': 1001,
|
||||
'sourceEnd': 1065,
|
||||
'sourceEndH': 1065,
|
||||
}
|
||||
|
||||
_check_expected_frame_range_values(
|
||||
"P01default_twsh010",
|
||||
expected_data,
|
||||
retimed=True,
|
||||
)
|
||||
|
|
@ -240,6 +240,13 @@ function Run-From-Code {
|
|||
& $Poetry $RunArgs @arguments
|
||||
}
|
||||
|
||||
function Run-Tests {
|
||||
$Poetry = "$RepoRoot\.poetry\bin\poetry.exe"
|
||||
$RunArgs = @( "run", "pytest", "$($RepoRoot)/tests")
|
||||
|
||||
& $Poetry $RunArgs @arguments
|
||||
}
|
||||
|
||||
function Write-Help {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
|
|
@ -256,6 +263,7 @@ function Write-Help {
|
|||
Write-Info -Text " ruff-fix ", "Run Ruff fix for the repository" -Color White, Cyan
|
||||
Write-Info -Text " codespell ", "Run codespell check for the repository" -Color White, Cyan
|
||||
Write-Info -Text " run ", "Run a poetry command in the repository environment" -Color White, Cyan
|
||||
Write-Info -Text " run-tests ", "Run ayon-core tests" -Color White, Cyan
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
|
|
@ -280,6 +288,9 @@ function Resolve-Function {
|
|||
} elseif ($FunctionName -eq "run") {
|
||||
Set-Cwd
|
||||
Run-From-Code
|
||||
} elseif ($FunctionName -eq "runtests") {
|
||||
Set-Cwd
|
||||
Run-Tests
|
||||
} else {
|
||||
Write-Host "Unknown function ""$FunctionName"""
|
||||
Write-Help
|
||||
|
|
|
|||
|
|
@ -158,6 +158,7 @@ default_help() {
|
|||
echo -e " ${BWhite}ruff-fix${RST} ${BCyan}Run Ruff fix for the repository${RST}"
|
||||
echo -e " ${BWhite}codespell${RST} ${BCyan}Run codespell check for the repository${RST}"
|
||||
echo -e " ${BWhite}run${RST} ${BCyan}Run a poetry command in the repository environment${RST}"
|
||||
echo -e " ${BWhite}run-tests${RST} ${BCyan}Run ayon-core tests${RST}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
|
|
@ -182,6 +183,12 @@ run_command () {
|
|||
"$POETRY_HOME/bin/poetry" run "$@"
|
||||
}
|
||||
|
||||
run_tests () {
|
||||
echo -e "${BIGreen}>>>${RST} Running tests..."
|
||||
shift; # will remove first arg ("run-tests") from the "$@"
|
||||
"$POETRY_HOME/bin/poetry" run pytest ./tests
|
||||
}
|
||||
|
||||
main () {
|
||||
detect_python || return 1
|
||||
|
||||
|
|
@ -218,6 +225,10 @@ main () {
|
|||
run_command "$@" || return_code=$?
|
||||
exit $return_code
|
||||
;;
|
||||
"runtests")
|
||||
run_tests "$@" || return_code=$?
|
||||
exit $return_code
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$function_name" != "" ]; then
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue