mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge branch 'develop' into feature/AY-971_Use-custom-staging-dir-functions
This commit is contained in:
commit
acaae18966
37 changed files with 1477 additions and 502 deletions
13
.github/workflows/release_trigger.yml
vendored
13
.github/workflows/release_trigger.yml
vendored
|
|
@ -2,10 +2,23 @@ name: 🚀 Release Trigger
|
|||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
draft:
|
||||
type: boolean
|
||||
description: "Create Release Draft"
|
||||
required: false
|
||||
default: false
|
||||
release_overwrite:
|
||||
type: string
|
||||
description: "Set Version Release Tag"
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
call-release-trigger:
|
||||
uses: ynput/ops-repo-automation/.github/workflows/release_trigger.yml@main
|
||||
with:
|
||||
draft: ${{ inputs.draft }}
|
||||
release_overwrite: ${{ inputs.release_overwrite }}
|
||||
secrets:
|
||||
token: ${{ secrets.YNPUT_BOT_TOKEN }}
|
||||
email: ${{ secrets.CI_EMAIL }}
|
||||
|
|
|
|||
16
.github/workflows/upload_to_ynput_cloud.yml
vendored
Normal file
16
.github/workflows/upload_to_ynput_cloud.yml
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
name: 📤 Upload to Ynput Cloud
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
call-upload-to-ynput-cloud:
|
||||
uses: ynput/ops-repo-automation/.github/workflows/upload_to_ynput_cloud.yml@main
|
||||
secrets:
|
||||
CI_EMAIL: ${{ secrets.CI_EMAIL }}
|
||||
CI_USER: ${{ secrets.CI_USER }}
|
||||
YNPUT_BOT_TOKEN: ${{ secrets.YNPUT_BOT_TOKEN }}
|
||||
YNPUT_CLOUD_URL: ${{ secrets.YNPUT_CLOUD_URL }}
|
||||
YNPUT_CLOUD_TOKEN: ${{ secrets.YNPUT_CLOUD_TOKEN }}
|
||||
|
|
@ -6,82 +6,58 @@ import json
|
|||
import copy
|
||||
import warnings
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from typing import Any, Optional
|
||||
import typing
|
||||
from typing import (
|
||||
Any,
|
||||
Optional,
|
||||
List,
|
||||
Set,
|
||||
Dict,
|
||||
Iterable,
|
||||
TypeVar,
|
||||
)
|
||||
|
||||
import clique
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from typing import Self, Tuple, Union, TypedDict, Pattern
|
||||
|
||||
|
||||
class EnumItemDict(TypedDict):
|
||||
label: str
|
||||
value: Any
|
||||
|
||||
|
||||
EnumItemsInputType = Union[
|
||||
Dict[Any, str],
|
||||
List[Tuple[Any, str]],
|
||||
List[Any],
|
||||
List[EnumItemDict]
|
||||
]
|
||||
|
||||
|
||||
class FileDefItemDict(TypedDict):
|
||||
directory: str
|
||||
filenames: List[str]
|
||||
frames: Optional[List[int]]
|
||||
template: Optional[str]
|
||||
is_sequence: Optional[bool]
|
||||
|
||||
|
||||
# Global variable which store attribute definitions by type
|
||||
# - default types are registered on import
|
||||
_attr_defs_by_type = {}
|
||||
|
||||
|
||||
def register_attr_def_class(cls):
|
||||
"""Register attribute definition.
|
||||
|
||||
Currently registered definitions are used to deserialize data to objects.
|
||||
|
||||
Attrs:
|
||||
cls (AbstractAttrDef): Non-abstract class to be registered with unique
|
||||
'type' attribute.
|
||||
|
||||
Raises:
|
||||
KeyError: When type was already registered.
|
||||
"""
|
||||
|
||||
if cls.type in _attr_defs_by_type:
|
||||
raise KeyError("Type \"{}\" was already registered".format(cls.type))
|
||||
_attr_defs_by_type[cls.type] = cls
|
||||
|
||||
|
||||
def get_attributes_keys(attribute_definitions):
|
||||
"""Collect keys from list of attribute definitions.
|
||||
|
||||
Args:
|
||||
attribute_definitions (List[AbstractAttrDef]): Objects of attribute
|
||||
definitions.
|
||||
|
||||
Returns:
|
||||
Set[str]: Keys that will be created using passed attribute definitions.
|
||||
"""
|
||||
|
||||
keys = set()
|
||||
if not attribute_definitions:
|
||||
return keys
|
||||
|
||||
for attribute_def in attribute_definitions:
|
||||
if not isinstance(attribute_def, UIDef):
|
||||
keys.add(attribute_def.key)
|
||||
return keys
|
||||
|
||||
|
||||
def get_default_values(attribute_definitions):
|
||||
"""Receive default values for attribute definitions.
|
||||
|
||||
Args:
|
||||
attribute_definitions (List[AbstractAttrDef]): Attribute definitions
|
||||
for which default values should be collected.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Default values for passed attribute definitions.
|
||||
"""
|
||||
|
||||
output = {}
|
||||
if not attribute_definitions:
|
||||
return output
|
||||
|
||||
for attr_def in attribute_definitions:
|
||||
# Skip UI definitions
|
||||
if not isinstance(attr_def, UIDef):
|
||||
output[attr_def.key] = attr_def.default
|
||||
return output
|
||||
# Type hint helpers
|
||||
IntFloatType = "Union[int, float]"
|
||||
|
||||
|
||||
class AbstractAttrDefMeta(ABCMeta):
|
||||
"""Metaclass to validate the existence of 'key' attribute.
|
||||
|
||||
Each object of `AbstractAttrDef` must have defined 'key' attribute.
|
||||
"""
|
||||
|
||||
"""
|
||||
def __call__(cls, *args, **kwargs):
|
||||
obj = super(AbstractAttrDefMeta, cls).__call__(*args, **kwargs)
|
||||
init_class = getattr(obj, "__init__class__", None)
|
||||
|
|
@ -93,8 +69,12 @@ class AbstractAttrDefMeta(ABCMeta):
|
|||
|
||||
|
||||
def _convert_reversed_attr(
|
||||
main_value, depr_value, main_label, depr_label, default
|
||||
):
|
||||
main_value: Any,
|
||||
depr_value: Any,
|
||||
main_label: str,
|
||||
depr_label: str,
|
||||
default: Any,
|
||||
) -> Any:
|
||||
if main_value is not None and depr_value is not None:
|
||||
if main_value == depr_value:
|
||||
print(
|
||||
|
|
@ -140,8 +120,8 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta):
|
|||
enabled (Optional[bool]): Item is enabled (for UI purposes).
|
||||
hidden (Optional[bool]): DEPRECATED: Use 'visible' instead.
|
||||
disabled (Optional[bool]): DEPRECATED: Use 'enabled' instead.
|
||||
"""
|
||||
|
||||
"""
|
||||
type_attributes = []
|
||||
|
||||
is_value_def = True
|
||||
|
|
@ -183,7 +163,7 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta):
|
|||
def id(self) -> str:
|
||||
return self._id
|
||||
|
||||
def clone(self):
|
||||
def clone(self) -> "Self":
|
||||
data = self.serialize()
|
||||
data.pop("type")
|
||||
return self.deserialize(data)
|
||||
|
|
@ -251,28 +231,28 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta):
|
|||
|
||||
Returns:
|
||||
str: Type of attribute definition.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def convert_value(self, value):
|
||||
def convert_value(self, value: Any) -> Any:
|
||||
"""Convert value to a valid one.
|
||||
|
||||
Convert passed value to a valid type. Use default if value can't be
|
||||
converted.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def serialize(self):
|
||||
def serialize(self) -> Dict[str, Any]:
|
||||
"""Serialize object to data so it's possible to recreate it.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Serialized object that can be passed to
|
||||
'deserialize' method.
|
||||
"""
|
||||
|
||||
"""
|
||||
data = {
|
||||
"type": self.type,
|
||||
"key": self.key,
|
||||
|
|
@ -288,7 +268,7 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta):
|
|||
return data
|
||||
|
||||
@classmethod
|
||||
def deserialize(cls, data):
|
||||
def deserialize(cls, data: Dict[str, Any]) -> "Self":
|
||||
"""Recreate object from data.
|
||||
|
||||
Data can be received using 'serialize' method.
|
||||
|
|
@ -299,10 +279,12 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta):
|
|||
|
||||
return cls(**data)
|
||||
|
||||
def _def_type_compare(self, other: "AbstractAttrDef") -> bool:
|
||||
def _def_type_compare(self, other: "Self") -> bool:
|
||||
return True
|
||||
|
||||
|
||||
AttrDefType = TypeVar("AttrDefType", bound=AbstractAttrDef)
|
||||
|
||||
# -----------------------------------------
|
||||
# UI attribute definitions won't hold value
|
||||
# -----------------------------------------
|
||||
|
|
@ -310,13 +292,19 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta):
|
|||
class UIDef(AbstractAttrDef):
|
||||
is_value_def = False
|
||||
|
||||
def __init__(self, key=None, default=None, *args, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
key: Optional[str] = None,
|
||||
default: Optional[Any] = None,
|
||||
*args,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(key, default, *args, **kwargs)
|
||||
|
||||
def is_value_valid(self, value: Any) -> bool:
|
||||
return True
|
||||
|
||||
def convert_value(self, value):
|
||||
def convert_value(self, value: Any) -> Any:
|
||||
return value
|
||||
|
||||
|
||||
|
|
@ -327,8 +315,8 @@ class UISeparatorDef(UIDef):
|
|||
class UILabelDef(UIDef):
|
||||
type = "label"
|
||||
|
||||
def __init__(self, label, key=None):
|
||||
super().__init__(label=label, key=key)
|
||||
def __init__(self, label, key=None, *args, **kwargs):
|
||||
super().__init__(label=label, key=key, *args, **kwargs)
|
||||
|
||||
def _def_type_compare(self, other: "UILabelDef") -> bool:
|
||||
return self.label == other.label
|
||||
|
|
@ -343,18 +331,18 @@ class UnknownDef(AbstractAttrDef):
|
|||
|
||||
This attribute can be used to keep existing data unchanged but does not
|
||||
have known definition of type.
|
||||
"""
|
||||
|
||||
"""
|
||||
type = "unknown"
|
||||
|
||||
def __init__(self, key, default=None, **kwargs):
|
||||
def __init__(self, key: str, default: Optional[Any] = None, **kwargs):
|
||||
kwargs["default"] = default
|
||||
super().__init__(key, **kwargs)
|
||||
|
||||
def is_value_valid(self, value: Any) -> bool:
|
||||
return True
|
||||
|
||||
def convert_value(self, value):
|
||||
def convert_value(self, value: Any) -> Any:
|
||||
return value
|
||||
|
||||
|
||||
|
|
@ -365,11 +353,11 @@ class HiddenDef(AbstractAttrDef):
|
|||
to other attributes (e.g. in multi-page UIs).
|
||||
|
||||
Keep in mind the value should be possible to parse by json parser.
|
||||
"""
|
||||
|
||||
"""
|
||||
type = "hidden"
|
||||
|
||||
def __init__(self, key, default=None, **kwargs):
|
||||
def __init__(self, key: str, default: Optional[Any] = None, **kwargs):
|
||||
kwargs["default"] = default
|
||||
kwargs["visible"] = False
|
||||
super().__init__(key, **kwargs)
|
||||
|
|
@ -377,7 +365,7 @@ class HiddenDef(AbstractAttrDef):
|
|||
def is_value_valid(self, value: Any) -> bool:
|
||||
return True
|
||||
|
||||
def convert_value(self, value):
|
||||
def convert_value(self, value: Any) -> Any:
|
||||
return value
|
||||
|
||||
|
||||
|
|
@ -392,8 +380,8 @@ class NumberDef(AbstractAttrDef):
|
|||
maximum(int, float): Maximum possible value.
|
||||
decimals(int): Maximum decimal points of value.
|
||||
default(int, float): Default value for conversion.
|
||||
"""
|
||||
|
||||
"""
|
||||
type = "number"
|
||||
type_attributes = [
|
||||
"minimum",
|
||||
|
|
@ -402,7 +390,12 @@ class NumberDef(AbstractAttrDef):
|
|||
]
|
||||
|
||||
def __init__(
|
||||
self, key, minimum=None, maximum=None, decimals=None, default=None,
|
||||
self,
|
||||
key: str,
|
||||
minimum: Optional[IntFloatType] = None,
|
||||
maximum: Optional[IntFloatType] = None,
|
||||
decimals: Optional[int] = None,
|
||||
default: Optional[IntFloatType] = None,
|
||||
**kwargs
|
||||
):
|
||||
minimum = 0 if minimum is None else minimum
|
||||
|
|
@ -428,9 +421,9 @@ class NumberDef(AbstractAttrDef):
|
|||
|
||||
super().__init__(key, default=default, **kwargs)
|
||||
|
||||
self.minimum = minimum
|
||||
self.maximum = maximum
|
||||
self.decimals = 0 if decimals is None else decimals
|
||||
self.minimum: IntFloatType = minimum
|
||||
self.maximum: IntFloatType = maximum
|
||||
self.decimals: int = 0 if decimals is None else decimals
|
||||
|
||||
def is_value_valid(self, value: Any) -> bool:
|
||||
if self.decimals == 0:
|
||||
|
|
@ -442,7 +435,7 @@ class NumberDef(AbstractAttrDef):
|
|||
return False
|
||||
return True
|
||||
|
||||
def convert_value(self, value):
|
||||
def convert_value(self, value: Any) -> IntFloatType:
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
value = float(value)
|
||||
|
|
@ -477,8 +470,8 @@ class TextDef(AbstractAttrDef):
|
|||
regex(str, re.Pattern): Regex validation.
|
||||
placeholder(str): UI placeholder for attribute.
|
||||
default(str, None): Default value. Empty string used when not defined.
|
||||
"""
|
||||
|
||||
"""
|
||||
type = "text"
|
||||
type_attributes = [
|
||||
"multiline",
|
||||
|
|
@ -486,7 +479,12 @@ class TextDef(AbstractAttrDef):
|
|||
]
|
||||
|
||||
def __init__(
|
||||
self, key, multiline=None, regex=None, placeholder=None, default=None,
|
||||
self,
|
||||
key: str,
|
||||
multiline: Optional[bool] = None,
|
||||
regex: Optional[str] = None,
|
||||
placeholder: Optional[str] = None,
|
||||
default: Optional[str] = None,
|
||||
**kwargs
|
||||
):
|
||||
if default is None:
|
||||
|
|
@ -505,9 +503,9 @@ class TextDef(AbstractAttrDef):
|
|||
if isinstance(regex, str):
|
||||
regex = re.compile(regex)
|
||||
|
||||
self.multiline = multiline
|
||||
self.placeholder = placeholder
|
||||
self.regex = regex
|
||||
self.multiline: bool = multiline
|
||||
self.placeholder: Optional[str] = placeholder
|
||||
self.regex: Optional["Pattern"] = regex
|
||||
|
||||
def is_value_valid(self, value: Any) -> bool:
|
||||
if not isinstance(value, str):
|
||||
|
|
@ -516,14 +514,17 @@ class TextDef(AbstractAttrDef):
|
|||
return False
|
||||
return True
|
||||
|
||||
def convert_value(self, value):
|
||||
def convert_value(self, value: Any) -> str:
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
return self.default
|
||||
|
||||
def serialize(self):
|
||||
def serialize(self) -> Dict[str, Any]:
|
||||
data = super().serialize()
|
||||
data["regex"] = self.regex.pattern
|
||||
regex = None
|
||||
if self.regex is not None:
|
||||
regex = self.regex.pattern
|
||||
data["regex"] = regex
|
||||
data["multiline"] = self.multiline
|
||||
data["placeholder"] = self.placeholder
|
||||
return data
|
||||
|
|
@ -542,18 +543,24 @@ class EnumDef(AbstractAttrDef):
|
|||
is enabled.
|
||||
|
||||
Args:
|
||||
items (Union[list[str], list[dict[str, Any]]): Items definition that
|
||||
can be converted using 'prepare_enum_items'.
|
||||
key (str): Key under which value is stored.
|
||||
items (EnumItemsInputType): Items definition that can be converted
|
||||
using 'prepare_enum_items'.
|
||||
default (Optional[Any]): Default value. Must be one key(value) from
|
||||
passed items or list of values for multiselection.
|
||||
multiselection (Optional[bool]): If True, multiselection is allowed.
|
||||
Output is list of selected items.
|
||||
"""
|
||||
|
||||
"""
|
||||
type = "enum"
|
||||
|
||||
def __init__(
|
||||
self, key, items, default=None, multiselection=False, **kwargs
|
||||
self,
|
||||
key: str,
|
||||
items: "EnumItemsInputType",
|
||||
default: "Union[str, List[Any]]" = None,
|
||||
multiselection: Optional[bool] = False,
|
||||
**kwargs
|
||||
):
|
||||
if not items:
|
||||
raise ValueError((
|
||||
|
|
@ -564,6 +571,9 @@ class EnumDef(AbstractAttrDef):
|
|||
items = self.prepare_enum_items(items)
|
||||
item_values = [item["value"] for item in items]
|
||||
item_values_set = set(item_values)
|
||||
if multiselection is None:
|
||||
multiselection = False
|
||||
|
||||
if multiselection:
|
||||
if default is None:
|
||||
default = []
|
||||
|
|
@ -574,9 +584,9 @@ class EnumDef(AbstractAttrDef):
|
|||
|
||||
super().__init__(key, default=default, **kwargs)
|
||||
|
||||
self.items = items
|
||||
self._item_values = item_values_set
|
||||
self.multiselection = multiselection
|
||||
self.items: List["EnumItemDict"] = items
|
||||
self._item_values: Set[Any] = item_values_set
|
||||
self.multiselection: bool = multiselection
|
||||
|
||||
def convert_value(self, value):
|
||||
if not self.multiselection:
|
||||
|
|
@ -606,7 +616,7 @@ class EnumDef(AbstractAttrDef):
|
|||
return data
|
||||
|
||||
@staticmethod
|
||||
def prepare_enum_items(items):
|
||||
def prepare_enum_items(items: "EnumItemsInputType") -> List["EnumItemDict"]:
|
||||
"""Convert items to unified structure.
|
||||
|
||||
Output is a list where each item is dictionary with 'value'
|
||||
|
|
@ -622,13 +632,12 @@ class EnumDef(AbstractAttrDef):
|
|||
```
|
||||
|
||||
Args:
|
||||
items (Union[Dict[str, Any], List[Any], List[Dict[str, Any]]): The
|
||||
items to convert.
|
||||
items (EnumItemsInputType): The items to convert.
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: Unified structure of items.
|
||||
"""
|
||||
List[EnumItemDict]: Unified structure of items.
|
||||
|
||||
"""
|
||||
output = []
|
||||
if isinstance(items, dict):
|
||||
for value, label in items.items():
|
||||
|
|
@ -679,11 +688,11 @@ class BoolDef(AbstractAttrDef):
|
|||
|
||||
Args:
|
||||
default(bool): Default value. Set to `False` if not defined.
|
||||
"""
|
||||
|
||||
"""
|
||||
type = "bool"
|
||||
|
||||
def __init__(self, key, default=None, **kwargs):
|
||||
def __init__(self, key: str, default: Optional[bool] = None, **kwargs):
|
||||
if default is None:
|
||||
default = False
|
||||
super().__init__(key, default=default, **kwargs)
|
||||
|
|
@ -691,7 +700,7 @@ class BoolDef(AbstractAttrDef):
|
|||
def is_value_valid(self, value: Any) -> bool:
|
||||
return isinstance(value, bool)
|
||||
|
||||
def convert_value(self, value):
|
||||
def convert_value(self, value: Any) -> bool:
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
return self.default
|
||||
|
|
@ -699,7 +708,11 @@ class BoolDef(AbstractAttrDef):
|
|||
|
||||
class FileDefItem:
|
||||
def __init__(
|
||||
self, directory, filenames, frames=None, template=None
|
||||
self,
|
||||
directory: str,
|
||||
filenames: List[str],
|
||||
frames: Optional[List[int]] = None,
|
||||
template: Optional[str] = None,
|
||||
):
|
||||
self.directory = directory
|
||||
|
||||
|
|
@ -728,7 +741,7 @@ class FileDefItem:
|
|||
)
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
def label(self) -> Optional[str]:
|
||||
if self.is_empty:
|
||||
return None
|
||||
|
||||
|
|
@ -771,7 +784,7 @@ class FileDefItem:
|
|||
filename_template, ",".join(ranges)
|
||||
)
|
||||
|
||||
def split_sequence(self):
|
||||
def split_sequence(self) -> List["Self"]:
|
||||
if not self.is_sequence:
|
||||
raise ValueError("Cannot split single file item")
|
||||
|
||||
|
|
@ -782,7 +795,7 @@ class FileDefItem:
|
|||
return self.from_paths(paths, False)
|
||||
|
||||
@property
|
||||
def ext(self):
|
||||
def ext(self) -> Optional[str]:
|
||||
if self.is_empty:
|
||||
return None
|
||||
_, ext = os.path.splitext(self.filenames[0])
|
||||
|
|
@ -791,14 +804,14 @@ class FileDefItem:
|
|||
return None
|
||||
|
||||
@property
|
||||
def lower_ext(self):
|
||||
def lower_ext(self) -> Optional[str]:
|
||||
ext = self.ext
|
||||
if ext is not None:
|
||||
return ext.lower()
|
||||
return ext
|
||||
|
||||
@property
|
||||
def is_dir(self):
|
||||
def is_dir(self) -> bool:
|
||||
if self.is_empty:
|
||||
return False
|
||||
|
||||
|
|
@ -807,10 +820,15 @@ class FileDefItem:
|
|||
return False
|
||||
return True
|
||||
|
||||
def set_directory(self, directory):
|
||||
def set_directory(self, directory: str):
|
||||
self.directory = directory
|
||||
|
||||
def set_filenames(self, filenames, frames=None, template=None):
|
||||
def set_filenames(
|
||||
self,
|
||||
filenames: List[str],
|
||||
frames: Optional[List[int]] = None,
|
||||
template: Optional[str] = None,
|
||||
):
|
||||
if frames is None:
|
||||
frames = []
|
||||
is_sequence = False
|
||||
|
|
@ -827,17 +845,21 @@ class FileDefItem:
|
|||
self.is_sequence = is_sequence
|
||||
|
||||
@classmethod
|
||||
def create_empty_item(cls):
|
||||
def create_empty_item(cls) -> "Self":
|
||||
return cls("", "")
|
||||
|
||||
@classmethod
|
||||
def from_value(cls, value, allow_sequences):
|
||||
def from_value(
|
||||
cls,
|
||||
value: "Union[List[FileDefItemDict], FileDefItemDict]",
|
||||
allow_sequences: bool,
|
||||
) -> List["Self"]:
|
||||
"""Convert passed value to FileDefItem objects.
|
||||
|
||||
Returns:
|
||||
list: Created FileDefItem objects.
|
||||
"""
|
||||
|
||||
"""
|
||||
# Convert single item to iterable
|
||||
if not isinstance(value, (list, tuple, set)):
|
||||
value = [value]
|
||||
|
|
@ -869,7 +891,7 @@ class FileDefItem:
|
|||
return output
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data):
|
||||
def from_dict(cls, data: "FileDefItemDict") -> "Self":
|
||||
return cls(
|
||||
data["directory"],
|
||||
data["filenames"],
|
||||
|
|
@ -878,7 +900,11 @@ class FileDefItem:
|
|||
)
|
||||
|
||||
@classmethod
|
||||
def from_paths(cls, paths, allow_sequences):
|
||||
def from_paths(
|
||||
cls,
|
||||
paths: List[str],
|
||||
allow_sequences: bool,
|
||||
) -> List["Self"]:
|
||||
filenames_by_dir = collections.defaultdict(list)
|
||||
for path in paths:
|
||||
normalized = os.path.normpath(path)
|
||||
|
|
@ -907,7 +933,7 @@ class FileDefItem:
|
|||
|
||||
return output
|
||||
|
||||
def to_dict(self):
|
||||
def to_dict(self) -> "FileDefItemDict":
|
||||
output = {
|
||||
"is_sequence": self.is_sequence,
|
||||
"directory": self.directory,
|
||||
|
|
@ -945,8 +971,15 @@ class FileDef(AbstractAttrDef):
|
|||
]
|
||||
|
||||
def __init__(
|
||||
self, key, single_item=True, folders=None, extensions=None,
|
||||
allow_sequences=True, extensions_label=None, default=None, **kwargs
|
||||
self,
|
||||
key: str,
|
||||
single_item: Optional[bool] = True,
|
||||
folders: Optional[bool] = None,
|
||||
extensions: Optional[Iterable[str]] = None,
|
||||
allow_sequences: Optional[bool] = True,
|
||||
extensions_label: Optional[str] = None,
|
||||
default: Optional["Union[FileDefItemDict, List[str]]"] = None,
|
||||
**kwargs
|
||||
):
|
||||
if folders is None and extensions is None:
|
||||
folders = True
|
||||
|
|
@ -963,7 +996,9 @@ class FileDef(AbstractAttrDef):
|
|||
FileDefItem.from_dict(default)
|
||||
|
||||
elif isinstance(default, str):
|
||||
default = FileDefItem.from_paths([default.strip()])[0]
|
||||
default = FileDefItem.from_paths(
|
||||
[default.strip()], allow_sequences
|
||||
)[0]
|
||||
|
||||
else:
|
||||
raise TypeError((
|
||||
|
|
@ -982,14 +1017,14 @@ class FileDef(AbstractAttrDef):
|
|||
if is_label_horizontal is None:
|
||||
kwargs["is_label_horizontal"] = False
|
||||
|
||||
self.single_item = single_item
|
||||
self.folders = folders
|
||||
self.extensions = set(extensions)
|
||||
self.allow_sequences = allow_sequences
|
||||
self.extensions_label = extensions_label
|
||||
self.single_item: bool = single_item
|
||||
self.folders: bool = folders
|
||||
self.extensions: Set[str] = set(extensions)
|
||||
self.allow_sequences: bool = allow_sequences
|
||||
self.extensions_label: Optional[str] = extensions_label
|
||||
super().__init__(key, default=default, **kwargs)
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
if not super().__eq__(other):
|
||||
return False
|
||||
|
||||
|
|
@ -1023,7 +1058,9 @@ class FileDef(AbstractAttrDef):
|
|||
return False
|
||||
return True
|
||||
|
||||
def convert_value(self, value):
|
||||
def convert_value(
|
||||
self, value: Any
|
||||
) -> "Union[FileDefItemDict, List[FileDefItemDict]]":
|
||||
if isinstance(value, (str, dict)):
|
||||
value = [value]
|
||||
|
||||
|
|
@ -1041,7 +1078,9 @@ class FileDef(AbstractAttrDef):
|
|||
pass
|
||||
|
||||
if string_paths:
|
||||
file_items = FileDefItem.from_paths(string_paths)
|
||||
file_items = FileDefItem.from_paths(
|
||||
string_paths, self.allow_sequences
|
||||
)
|
||||
dict_items.extend([
|
||||
file_item.to_dict()
|
||||
for file_item in file_items
|
||||
|
|
@ -1059,55 +1098,124 @@ class FileDef(AbstractAttrDef):
|
|||
return []
|
||||
|
||||
|
||||
def serialize_attr_def(attr_def):
|
||||
def register_attr_def_class(cls: AttrDefType):
|
||||
"""Register attribute definition.
|
||||
|
||||
Currently registered definitions are used to deserialize data to objects.
|
||||
|
||||
Attrs:
|
||||
cls (AttrDefType): Non-abstract class to be registered with unique
|
||||
'type' attribute.
|
||||
|
||||
Raises:
|
||||
KeyError: When type was already registered.
|
||||
|
||||
"""
|
||||
if cls.type in _attr_defs_by_type:
|
||||
raise KeyError("Type \"{}\" was already registered".format(cls.type))
|
||||
_attr_defs_by_type[cls.type] = cls
|
||||
|
||||
|
||||
def get_attributes_keys(
|
||||
attribute_definitions: List[AttrDefType]
|
||||
) -> Set[str]:
|
||||
"""Collect keys from list of attribute definitions.
|
||||
|
||||
Args:
|
||||
attribute_definitions (List[AttrDefType]): Objects of attribute
|
||||
definitions.
|
||||
|
||||
Returns:
|
||||
Set[str]: Keys that will be created using passed attribute definitions.
|
||||
|
||||
"""
|
||||
keys = set()
|
||||
if not attribute_definitions:
|
||||
return keys
|
||||
|
||||
for attribute_def in attribute_definitions:
|
||||
if not isinstance(attribute_def, UIDef):
|
||||
keys.add(attribute_def.key)
|
||||
return keys
|
||||
|
||||
|
||||
def get_default_values(
|
||||
attribute_definitions: List[AttrDefType]
|
||||
) -> Dict[str, Any]:
|
||||
"""Receive default values for attribute definitions.
|
||||
|
||||
Args:
|
||||
attribute_definitions (List[AttrDefType]): Attribute definitions
|
||||
for which default values should be collected.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Default values for passed attribute definitions.
|
||||
|
||||
"""
|
||||
output = {}
|
||||
if not attribute_definitions:
|
||||
return output
|
||||
|
||||
for attr_def in attribute_definitions:
|
||||
# Skip UI definitions
|
||||
if not isinstance(attr_def, UIDef):
|
||||
output[attr_def.key] = attr_def.default
|
||||
return output
|
||||
|
||||
|
||||
def serialize_attr_def(attr_def: AttrDefType) -> Dict[str, Any]:
|
||||
"""Serialize attribute definition to data.
|
||||
|
||||
Args:
|
||||
attr_def (AbstractAttrDef): Attribute definition to serialize.
|
||||
attr_def (AttrDefType): Attribute definition to serialize.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Serialized data.
|
||||
"""
|
||||
|
||||
"""
|
||||
return attr_def.serialize()
|
||||
|
||||
|
||||
def serialize_attr_defs(attr_defs):
|
||||
def serialize_attr_defs(
|
||||
attr_defs: List[AttrDefType]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Serialize attribute definitions to data.
|
||||
|
||||
Args:
|
||||
attr_defs (List[AbstractAttrDef]): Attribute definitions to serialize.
|
||||
attr_defs (List[AttrDefType]): Attribute definitions to serialize.
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: Serialized data.
|
||||
"""
|
||||
|
||||
"""
|
||||
return [
|
||||
serialize_attr_def(attr_def)
|
||||
for attr_def in attr_defs
|
||||
]
|
||||
|
||||
|
||||
def deserialize_attr_def(attr_def_data):
|
||||
def deserialize_attr_def(attr_def_data: Dict[str, Any]) -> AttrDefType:
|
||||
"""Deserialize attribute definition from data.
|
||||
|
||||
Args:
|
||||
attr_def_data (Dict[str, Any]): Attribute definition data to
|
||||
deserialize.
|
||||
"""
|
||||
|
||||
"""
|
||||
attr_type = attr_def_data.pop("type")
|
||||
cls = _attr_defs_by_type[attr_type]
|
||||
return cls.deserialize(attr_def_data)
|
||||
|
||||
|
||||
def deserialize_attr_defs(attr_defs_data):
|
||||
def deserialize_attr_defs(
|
||||
attr_defs_data: List[Dict[str, Any]]
|
||||
) -> List[AttrDefType]:
|
||||
"""Deserialize attribute definitions.
|
||||
|
||||
Args:
|
||||
List[Dict[str, Any]]: List of attribute definitions.
|
||||
"""
|
||||
|
||||
"""
|
||||
return [
|
||||
deserialize_attr_def(attr_def_data)
|
||||
for attr_def_data in attr_defs_data
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import os
|
||||
import sys
|
||||
import uuid
|
||||
import getpass
|
||||
import logging
|
||||
import platform
|
||||
|
|
@ -11,12 +10,12 @@ import copy
|
|||
|
||||
from . import Terminal
|
||||
|
||||
# Check for `unicode` in builtins
|
||||
USE_UNICODE = hasattr(__builtins__, "unicode")
|
||||
|
||||
|
||||
class LogStreamHandler(logging.StreamHandler):
|
||||
""" StreamHandler class designed to handle utf errors in python 2.x hosts.
|
||||
"""StreamHandler class.
|
||||
|
||||
This was originally designed to handle UTF errors in python 2.x hosts,
|
||||
however currently solely remains for backwards compatibility.
|
||||
|
||||
"""
|
||||
|
||||
|
|
@ -25,49 +24,27 @@ class LogStreamHandler(logging.StreamHandler):
|
|||
self.enabled = True
|
||||
|
||||
def enable(self):
|
||||
""" Enable StreamHandler
|
||||
"""Enable StreamHandler
|
||||
|
||||
Used to silence output
|
||||
Make StreamHandler output again
|
||||
"""
|
||||
self.enabled = True
|
||||
|
||||
def disable(self):
|
||||
""" Disable StreamHandler
|
||||
"""Disable StreamHandler
|
||||
|
||||
Make StreamHandler output again
|
||||
Used to silence output
|
||||
"""
|
||||
self.enabled = False
|
||||
|
||||
def emit(self, record):
|
||||
if not self.enable:
|
||||
if not self.enabled or self.stream is None:
|
||||
return
|
||||
try:
|
||||
msg = self.format(record)
|
||||
msg = Terminal.log(msg)
|
||||
stream = self.stream
|
||||
if stream is None:
|
||||
return
|
||||
fs = "%s\n"
|
||||
# if no unicode support...
|
||||
if not USE_UNICODE:
|
||||
stream.write(fs % msg)
|
||||
else:
|
||||
try:
|
||||
if (isinstance(msg, unicode) and # noqa: F821
|
||||
getattr(stream, 'encoding', None)):
|
||||
ufs = u'%s\n'
|
||||
try:
|
||||
stream.write(ufs % msg)
|
||||
except UnicodeEncodeError:
|
||||
stream.write((ufs % msg).encode(stream.encoding))
|
||||
else:
|
||||
if (getattr(stream, 'encoding', 'utf-8')):
|
||||
ufs = u'%s\n'
|
||||
stream.write(ufs % unicode(msg)) # noqa: F821
|
||||
else:
|
||||
stream.write(fs % msg)
|
||||
except UnicodeError:
|
||||
stream.write(fs % msg.encode("UTF-8"))
|
||||
stream.write(f"{msg}\n")
|
||||
self.flush()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
|
|
@ -141,8 +118,6 @@ class Logger:
|
|||
process_data = None
|
||||
# Cached process name or ability to set different process name
|
||||
_process_name = None
|
||||
# TODO Remove 'mongo_process_id' in 1.x.x
|
||||
mongo_process_id = uuid.uuid4().hex
|
||||
|
||||
@classmethod
|
||||
def get_logger(cls, name=None):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import os
|
||||
import re
|
||||
import logging
|
||||
import platform
|
||||
|
||||
import clique
|
||||
|
||||
|
|
@ -38,31 +37,7 @@ def create_hard_link(src_path, dst_path):
|
|||
dst_path(str): Full path to a file where a link of source will be
|
||||
added.
|
||||
"""
|
||||
# Use `os.link` if is available
|
||||
# - should be for all platforms with newer python versions
|
||||
if hasattr(os, "link"):
|
||||
os.link(src_path, dst_path)
|
||||
return
|
||||
|
||||
# Windows implementation of hardlinks
|
||||
# - used in Python 2
|
||||
if platform.system().lower() == "windows":
|
||||
import ctypes
|
||||
from ctypes.wintypes import BOOL
|
||||
CreateHardLink = ctypes.windll.kernel32.CreateHardLinkW
|
||||
CreateHardLink.argtypes = [
|
||||
ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_void_p
|
||||
]
|
||||
CreateHardLink.restype = BOOL
|
||||
|
||||
res = CreateHardLink(dst_path, src_path, None)
|
||||
if res == 0:
|
||||
raise ctypes.WinError()
|
||||
return
|
||||
# Raises not implemented error if gets here
|
||||
raise NotImplementedError(
|
||||
"Implementation of hardlink for current environment is missing."
|
||||
)
|
||||
os.link(src_path, dst_path)
|
||||
|
||||
|
||||
def collect_frames(files):
|
||||
|
|
@ -210,7 +185,7 @@ def get_last_version_from_path(path_dir, filter):
|
|||
assert isinstance(filter, list) and (
|
||||
len(filter) != 0), "`filter` argument needs to be list and not empty"
|
||||
|
||||
filtred_files = list()
|
||||
filtered_files = list()
|
||||
|
||||
# form regex for filtering
|
||||
pattern = r".*".join(filter)
|
||||
|
|
@ -218,10 +193,10 @@ def get_last_version_from_path(path_dir, filter):
|
|||
for file in os.listdir(path_dir):
|
||||
if not re.findall(pattern, file):
|
||||
continue
|
||||
filtred_files.append(file)
|
||||
filtered_files.append(file)
|
||||
|
||||
if filtred_files:
|
||||
sorted(filtred_files)
|
||||
return filtred_files[-1]
|
||||
if filtered_files:
|
||||
filtered_files.sort()
|
||||
return filtered_files[-1]
|
||||
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from typing import (
|
|||
Iterable,
|
||||
Tuple,
|
||||
List,
|
||||
Set,
|
||||
Dict,
|
||||
Any,
|
||||
Callable,
|
||||
|
|
@ -252,8 +253,10 @@ class CreateContext:
|
|||
# Shared data across creators during collection phase
|
||||
self._collection_shared_data = None
|
||||
|
||||
# Context validation cache
|
||||
self._folder_id_by_folder_path = {}
|
||||
# Entities cache
|
||||
self._folder_entities_by_path = {}
|
||||
self._task_entities_by_id = {}
|
||||
self._task_ids_by_folder_path = {}
|
||||
self._task_names_by_folder_path = {}
|
||||
|
||||
self.thumbnail_paths_by_instance_id = {}
|
||||
|
|
@ -356,12 +359,12 @@ class CreateContext:
|
|||
return self._host_is_valid
|
||||
|
||||
@property
|
||||
def host_name(self):
|
||||
def host_name(self) -> str:
|
||||
if hasattr(self.host, "name"):
|
||||
return self.host.name
|
||||
return os.environ["AYON_HOST_NAME"]
|
||||
|
||||
def get_current_project_name(self):
|
||||
def get_current_project_name(self) -> Optional[str]:
|
||||
"""Project name which was used as current context on context reset.
|
||||
|
||||
Returns:
|
||||
|
|
@ -370,7 +373,7 @@ class CreateContext:
|
|||
|
||||
return self._current_project_name
|
||||
|
||||
def get_current_folder_path(self):
|
||||
def get_current_folder_path(self) -> Optional[str]:
|
||||
"""Folder path which was used as current context on context reset.
|
||||
|
||||
Returns:
|
||||
|
|
@ -379,7 +382,7 @@ class CreateContext:
|
|||
|
||||
return self._current_folder_path
|
||||
|
||||
def get_current_task_name(self):
|
||||
def get_current_task_name(self) -> Optional[str]:
|
||||
"""Task name which was used as current context on context reset.
|
||||
|
||||
Returns:
|
||||
|
|
@ -388,7 +391,7 @@ class CreateContext:
|
|||
|
||||
return self._current_task_name
|
||||
|
||||
def get_current_task_type(self):
|
||||
def get_current_task_type(self) -> Optional[str]:
|
||||
"""Task type which was used as current context on context reset.
|
||||
|
||||
Returns:
|
||||
|
|
@ -403,7 +406,7 @@ class CreateContext:
|
|||
self._current_task_type = task_type
|
||||
return self._current_task_type
|
||||
|
||||
def get_current_project_entity(self):
|
||||
def get_current_project_entity(self) -> Optional[Dict[str, Any]]:
|
||||
"""Project entity for current context project.
|
||||
|
||||
Returns:
|
||||
|
|
@ -419,26 +422,21 @@ class CreateContext:
|
|||
self._current_project_entity = project_entity
|
||||
return copy.deepcopy(self._current_project_entity)
|
||||
|
||||
def get_current_folder_entity(self):
|
||||
def get_current_folder_entity(self) -> Optional[Dict[str, Any]]:
|
||||
"""Folder entity for current context folder.
|
||||
|
||||
Returns:
|
||||
Union[dict[str, Any], None]: Folder entity.
|
||||
Optional[dict[str, Any]]: Folder entity.
|
||||
|
||||
"""
|
||||
if self._current_folder_entity is not _NOT_SET:
|
||||
return copy.deepcopy(self._current_folder_entity)
|
||||
folder_entity = None
|
||||
|
||||
folder_path = self.get_current_folder_path()
|
||||
if folder_path:
|
||||
project_name = self.get_current_project_name()
|
||||
folder_entity = ayon_api.get_folder_by_path(
|
||||
project_name, folder_path
|
||||
)
|
||||
self._current_folder_entity = folder_entity
|
||||
self._current_folder_entity = self.get_folder_entity(folder_path)
|
||||
return copy.deepcopy(self._current_folder_entity)
|
||||
|
||||
def get_current_task_entity(self):
|
||||
def get_current_task_entity(self) -> Optional[Dict[str, Any]]:
|
||||
"""Task entity for current context task.
|
||||
|
||||
Returns:
|
||||
|
|
@ -447,18 +445,12 @@ class CreateContext:
|
|||
"""
|
||||
if self._current_task_entity is not _NOT_SET:
|
||||
return copy.deepcopy(self._current_task_entity)
|
||||
task_entity = None
|
||||
|
||||
folder_path = self.get_current_folder_path()
|
||||
task_name = self.get_current_task_name()
|
||||
if task_name:
|
||||
folder_entity = self.get_current_folder_entity()
|
||||
if folder_entity:
|
||||
project_name = self.get_current_project_name()
|
||||
task_entity = ayon_api.get_task_by_name(
|
||||
project_name,
|
||||
folder_id=folder_entity["id"],
|
||||
task_name=task_name
|
||||
)
|
||||
self._current_task_entity = task_entity
|
||||
self._current_task_entity = self.get_task_entity(
|
||||
folder_path, task_name
|
||||
)
|
||||
return copy.deepcopy(self._current_task_entity)
|
||||
|
||||
def get_current_workfile_path(self):
|
||||
|
|
@ -566,8 +558,13 @@ class CreateContext:
|
|||
|
||||
# Give ability to store shared data for collection phase
|
||||
self._collection_shared_data = {}
|
||||
self._folder_id_by_folder_path = {}
|
||||
|
||||
self._folder_entities_by_path = {}
|
||||
self._task_entities_by_id = {}
|
||||
|
||||
self._task_ids_by_folder_path = {}
|
||||
self._task_names_by_folder_path = {}
|
||||
|
||||
self._event_hub.clear_callbacks()
|
||||
|
||||
def reset_finalization(self):
|
||||
|
|
@ -1468,6 +1465,260 @@ class CreateContext:
|
|||
if failed_info:
|
||||
raise CreatorsCreateFailed(failed_info)
|
||||
|
||||
def get_folder_entities(self, folder_paths: Iterable[str]):
|
||||
"""Get folder entities by paths.
|
||||
|
||||
Args:
|
||||
folder_paths (Iterable[str]): Folder paths.
|
||||
|
||||
Returns:
|
||||
Dict[str, Optional[Dict[str, Any]]]: Folder entities by path.
|
||||
|
||||
"""
|
||||
output = {
|
||||
folder_path: None
|
||||
for folder_path in folder_paths
|
||||
}
|
||||
remainder_paths = set()
|
||||
for folder_path in output:
|
||||
# Skip invalid folder paths (folder name or empty path)
|
||||
if not folder_path or "/" not in folder_path:
|
||||
continue
|
||||
|
||||
if folder_path not in self._folder_entities_by_path:
|
||||
remainder_paths.add(folder_path)
|
||||
continue
|
||||
|
||||
output[folder_path] = self._folder_entities_by_path[folder_path]
|
||||
|
||||
if not remainder_paths:
|
||||
return output
|
||||
|
||||
found_paths = set()
|
||||
for folder_entity in ayon_api.get_folders(
|
||||
self.project_name,
|
||||
folder_paths=remainder_paths,
|
||||
):
|
||||
folder_path = folder_entity["path"]
|
||||
found_paths.add(folder_path)
|
||||
output[folder_path] = folder_entity
|
||||
self._folder_entities_by_path[folder_path] = folder_entity
|
||||
|
||||
# Cache empty folder entities
|
||||
for path in remainder_paths - found_paths:
|
||||
self._folder_entities_by_path[path] = None
|
||||
|
||||
return output
|
||||
|
||||
def get_task_entities(
|
||||
self,
|
||||
task_names_by_folder_paths: Dict[str, Set[str]]
|
||||
) -> Dict[str, Dict[str, Optional[Dict[str, Any]]]]:
|
||||
"""Get task entities by folder path and task name.
|
||||
|
||||
Entities are cached until reset.
|
||||
|
||||
Args:
|
||||
task_names_by_folder_paths (Dict[str, Set[str]]): Task names by
|
||||
folder path.
|
||||
|
||||
Returns:
|
||||
Dict[str, Dict[str, Dict[str, Any]]]: Task entities by folder path
|
||||
and task name.
|
||||
|
||||
"""
|
||||
output = {}
|
||||
for folder_path, task_names in task_names_by_folder_paths.items():
|
||||
if folder_path is None:
|
||||
continue
|
||||
output[folder_path] = {
|
||||
task_name: None
|
||||
for task_name in task_names
|
||||
if task_name is not None
|
||||
}
|
||||
|
||||
missing_folder_paths = set()
|
||||
for folder_path, output_task_entities_by_name in output.items():
|
||||
if not output_task_entities_by_name:
|
||||
continue
|
||||
|
||||
if folder_path not in self._task_ids_by_folder_path:
|
||||
missing_folder_paths.add(folder_path)
|
||||
continue
|
||||
|
||||
all_tasks_filled = True
|
||||
task_ids = self._task_ids_by_folder_path[folder_path]
|
||||
task_entities_by_name = {}
|
||||
for task_id in task_ids:
|
||||
task_entity = self._task_entities_by_id.get(task_id)
|
||||
if task_entity is None:
|
||||
all_tasks_filled = False
|
||||
continue
|
||||
task_entities_by_name[task_entity["name"]] = task_entity
|
||||
|
||||
any_missing = False
|
||||
for task_name in set(output_task_entities_by_name):
|
||||
task_entity = task_entities_by_name.get(task_name)
|
||||
if task_entity is None:
|
||||
any_missing = True
|
||||
continue
|
||||
|
||||
output_task_entities_by_name[task_name] = task_entity
|
||||
|
||||
if any_missing and not all_tasks_filled:
|
||||
missing_folder_paths.add(folder_path)
|
||||
|
||||
if not missing_folder_paths:
|
||||
return output
|
||||
|
||||
folder_entities_by_path = self.get_folder_entities(
|
||||
missing_folder_paths
|
||||
)
|
||||
folder_path_by_id = {}
|
||||
for folder_path, folder_entity in folder_entities_by_path.items():
|
||||
if folder_entity is not None:
|
||||
folder_path_by_id[folder_entity["id"]] = folder_path
|
||||
|
||||
if not folder_path_by_id:
|
||||
return output
|
||||
|
||||
task_entities_by_parent_id = collections.defaultdict(list)
|
||||
for task_entity in ayon_api.get_tasks(
|
||||
self.project_name,
|
||||
folder_ids=folder_path_by_id.keys()
|
||||
):
|
||||
folder_id = task_entity["folderId"]
|
||||
task_entities_by_parent_id[folder_id].append(task_entity)
|
||||
|
||||
for folder_id, task_entities in task_entities_by_parent_id.items():
|
||||
folder_path = folder_path_by_id[folder_id]
|
||||
task_ids = set()
|
||||
task_names = set()
|
||||
for task_entity in task_entities:
|
||||
task_id = task_entity["id"]
|
||||
task_name = task_entity["name"]
|
||||
task_ids.add(task_id)
|
||||
task_names.add(task_name)
|
||||
self._task_entities_by_id[task_id] = task_entity
|
||||
|
||||
output[folder_path][task_name] = task_entity
|
||||
self._task_ids_by_folder_path[folder_path] = task_ids
|
||||
self._task_names_by_folder_path[folder_path] = task_names
|
||||
|
||||
return output
|
||||
|
||||
def get_folder_entity(
|
||||
self,
|
||||
folder_path: Optional[str],
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Get folder entity by path.
|
||||
|
||||
Entities are cached until reset.
|
||||
|
||||
Args:
|
||||
folder_path (Optional[str]): Folder path.
|
||||
|
||||
Returns:
|
||||
Optional[Dict[str, Any]]: Folder entity.
|
||||
|
||||
"""
|
||||
if not folder_path:
|
||||
return None
|
||||
return self.get_folder_entities([folder_path]).get(folder_path)
|
||||
|
||||
def get_task_entity(
|
||||
self,
|
||||
folder_path: Optional[str],
|
||||
task_name: Optional[str],
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Get task entity by name and folder path.
|
||||
|
||||
Entities are cached until reset.
|
||||
|
||||
Args:
|
||||
folder_path (Optional[str]): Folder path.
|
||||
task_name (Optional[str]): Task name.
|
||||
|
||||
Returns:
|
||||
Optional[Dict[str, Any]]: Task entity.
|
||||
|
||||
"""
|
||||
if not folder_path or not task_name:
|
||||
return None
|
||||
|
||||
output = self.get_task_entities({folder_path: {task_name}})
|
||||
return output.get(folder_path, {}).get(task_name)
|
||||
|
||||
def get_instances_folder_entities(
|
||||
self, instances: Optional[Iterable["CreatedInstance"]] = None
|
||||
) -> Dict[str, Optional[Dict[str, Any]]]:
|
||||
if instances is None:
|
||||
instances = self._instances_by_id.values()
|
||||
instances = list(instances)
|
||||
output = {
|
||||
instance.id: None
|
||||
for instance in instances
|
||||
}
|
||||
if not instances:
|
||||
return output
|
||||
|
||||
folder_paths = {
|
||||
instance.get("folderPath")
|
||||
for instance in instances
|
||||
}
|
||||
folder_paths.discard(None)
|
||||
folder_entities_by_path = self.get_folder_entities(folder_paths)
|
||||
for instance in instances:
|
||||
folder_path = instance.get("folderPath")
|
||||
output[instance.id] = folder_entities_by_path.get(folder_path)
|
||||
return output
|
||||
|
||||
def get_instances_task_entities(
|
||||
self, instances: Optional[Iterable["CreatedInstance"]] = None
|
||||
):
|
||||
"""Get task entities for instances.
|
||||
|
||||
Args:
|
||||
instances (Optional[Iterable[CreatedInstance]]): Instances to
|
||||
get task entities. If not provided all instances are used.
|
||||
|
||||
Returns:
|
||||
Dict[str, Optional[Dict[str, Any]]]: Task entity by instance id.
|
||||
|
||||
"""
|
||||
if instances is None:
|
||||
instances = self._instances_by_id.values()
|
||||
instances = list(instances)
|
||||
|
||||
output = {
|
||||
instance.id: None
|
||||
for instance in instances
|
||||
}
|
||||
if not instances:
|
||||
return output
|
||||
|
||||
filtered_instances = []
|
||||
task_names_by_folder_path = collections.defaultdict(set)
|
||||
for instance in instances:
|
||||
folder_path = instance.get("folderPath")
|
||||
task_name = instance.get("task")
|
||||
if not folder_path or not task_name:
|
||||
continue
|
||||
filtered_instances.append(instance)
|
||||
task_names_by_folder_path[folder_path].add(task_name)
|
||||
|
||||
task_entities_by_folder_path = self.get_task_entities(
|
||||
task_names_by_folder_path
|
||||
)
|
||||
for instance in filtered_instances:
|
||||
folder_path = instance["folderPath"]
|
||||
task_name = instance["task"]
|
||||
output[instance.id] = (
|
||||
task_entities_by_folder_path[folder_path][task_name]
|
||||
)
|
||||
|
||||
return output
|
||||
|
||||
def get_instances_context_info(
|
||||
self, instances: Optional[Iterable["CreatedInstance"]] = None
|
||||
) -> Dict[str, InstanceContextInfo]:
|
||||
|
|
@ -1508,15 +1759,16 @@ class CreateContext:
|
|||
if instance.has_promised_context:
|
||||
context_info.folder_is_valid = True
|
||||
context_info.task_is_valid = True
|
||||
# NOTE missing task type
|
||||
continue
|
||||
# TODO allow context promise
|
||||
folder_path = context_info.folder_path
|
||||
if not folder_path:
|
||||
continue
|
||||
|
||||
if folder_path in self._folder_id_by_folder_path:
|
||||
folder_id = self._folder_id_by_folder_path[folder_path]
|
||||
if folder_id is None:
|
||||
if folder_path in self._folder_entities_by_path:
|
||||
folder_entity = self._folder_entities_by_path[folder_path]
|
||||
if folder_entity is None:
|
||||
continue
|
||||
context_info.folder_is_valid = True
|
||||
|
||||
|
|
@ -1535,72 +1787,78 @@ class CreateContext:
|
|||
|
||||
# Backwards compatibility for cases where folder name is set instead
|
||||
# of folder path
|
||||
folder_names = set()
|
||||
folder_paths = set()
|
||||
for folder_path in task_names_by_folder_path.keys():
|
||||
task_names_by_folder_name = {}
|
||||
task_names_by_folder_path_clean = {}
|
||||
for folder_path, task_names in task_names_by_folder_path.items():
|
||||
if folder_path is None:
|
||||
pass
|
||||
elif "/" in folder_path:
|
||||
folder_paths.add(folder_path)
|
||||
else:
|
||||
folder_names.add(folder_path)
|
||||
continue
|
||||
|
||||
folder_paths_by_id = {}
|
||||
if folder_paths:
|
||||
clean_task_names = {
|
||||
task_name
|
||||
for task_name in task_names
|
||||
if task_name
|
||||
}
|
||||
|
||||
if "/" not in folder_path:
|
||||
task_names_by_folder_name[folder_path] = clean_task_names
|
||||
continue
|
||||
|
||||
folder_paths.add(folder_path)
|
||||
if not clean_task_names:
|
||||
continue
|
||||
|
||||
task_names_by_folder_path_clean[folder_path] = clean_task_names
|
||||
|
||||
folder_paths_by_name = collections.defaultdict(list)
|
||||
if task_names_by_folder_name:
|
||||
for folder_entity in ayon_api.get_folders(
|
||||
project_name,
|
||||
folder_paths=folder_paths,
|
||||
fields={"id", "path"}
|
||||
folder_names=task_names_by_folder_name.keys(),
|
||||
fields={"name", "path"}
|
||||
):
|
||||
folder_id = folder_entity["id"]
|
||||
folder_path = folder_entity["path"]
|
||||
folder_paths_by_id[folder_id] = folder_path
|
||||
self._folder_id_by_folder_path[folder_path] = folder_id
|
||||
|
||||
folder_entities_by_name = collections.defaultdict(list)
|
||||
if folder_names:
|
||||
for folder_entity in ayon_api.get_folders(
|
||||
project_name,
|
||||
folder_names=folder_names,
|
||||
fields={"id", "name", "path"}
|
||||
):
|
||||
folder_id = folder_entity["id"]
|
||||
folder_name = folder_entity["name"]
|
||||
folder_path = folder_entity["path"]
|
||||
folder_paths_by_id[folder_id] = folder_path
|
||||
folder_entities_by_name[folder_name].append(folder_entity)
|
||||
self._folder_id_by_folder_path[folder_path] = folder_id
|
||||
folder_paths_by_name[folder_name].append(folder_path)
|
||||
|
||||
tasks_entities = ayon_api.get_tasks(
|
||||
project_name,
|
||||
folder_ids=folder_paths_by_id.keys(),
|
||||
fields={"name", "folderId"}
|
||||
folder_path_by_name = {}
|
||||
for folder_name, paths in folder_paths_by_name.items():
|
||||
if len(paths) != 1:
|
||||
continue
|
||||
path = paths[0]
|
||||
folder_path_by_name[folder_name] = path
|
||||
folder_paths.add(path)
|
||||
clean_task_names = task_names_by_folder_name[folder_name]
|
||||
if not clean_task_names:
|
||||
continue
|
||||
folder_task_names = task_names_by_folder_path_clean.setdefault(
|
||||
path, set()
|
||||
)
|
||||
folder_task_names |= clean_task_names
|
||||
|
||||
folder_entities_by_path = self.get_folder_entities(folder_paths)
|
||||
task_entities_by_folder_path = self.get_task_entities(
|
||||
task_names_by_folder_path_clean
|
||||
)
|
||||
|
||||
task_names_by_folder_path = collections.defaultdict(set)
|
||||
for task_entity in tasks_entities:
|
||||
folder_id = task_entity["folderId"]
|
||||
folder_path = folder_paths_by_id[folder_id]
|
||||
task_names_by_folder_path[folder_path].add(task_entity["name"])
|
||||
self._task_names_by_folder_path.update(task_names_by_folder_path)
|
||||
|
||||
for instance in to_validate:
|
||||
folder_path = instance["folderPath"]
|
||||
task_name = instance.get("task")
|
||||
if folder_path and "/" not in folder_path:
|
||||
folder_entities = folder_entities_by_name.get(folder_path)
|
||||
if len(folder_entities) == 1:
|
||||
folder_path = folder_entities[0]["path"]
|
||||
instance["folderPath"] = folder_path
|
||||
new_folder_path = folder_path_by_name.get(folder_path)
|
||||
if new_folder_path:
|
||||
folder_path = new_folder_path
|
||||
instance["folderPath"] = new_folder_path
|
||||
|
||||
if folder_path not in task_names_by_folder_path:
|
||||
folder_entity = folder_entities_by_path.get(folder_path)
|
||||
if not folder_entity:
|
||||
continue
|
||||
context_info = info_by_instance_id[instance.id]
|
||||
context_info.folder_is_valid = True
|
||||
|
||||
if (
|
||||
not task_name
|
||||
or task_name in task_names_by_folder_path[folder_path]
|
||||
or task_name in task_entities_by_folder_path[folder_path]
|
||||
):
|
||||
context_info.task_is_valid = True
|
||||
return info_by_instance_id
|
||||
|
|
|
|||
|
|
@ -132,6 +132,10 @@ class AttributeValues:
|
|||
def __contains__(self, key):
|
||||
return key in self._attr_defs_by_key
|
||||
|
||||
def __iter__(self):
|
||||
for key in self._attr_defs_by_key:
|
||||
yield key
|
||||
|
||||
def get(self, key, default=None):
|
||||
if key in self._attr_defs_by_key:
|
||||
return self[key]
|
||||
|
|
@ -425,11 +429,18 @@ class CreatedInstance:
|
|||
__immutable_keys = (
|
||||
"id",
|
||||
"instance_id",
|
||||
"product_type",
|
||||
"productType",
|
||||
"creator_identifier",
|
||||
"creator_attributes",
|
||||
"publish_attributes"
|
||||
)
|
||||
# Keys that can be changed, but should not be removed from instance
|
||||
__required_keys = {
|
||||
"folderPath": None,
|
||||
"task": None,
|
||||
"productName": None,
|
||||
"active": True,
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -511,6 +522,9 @@ class CreatedInstance:
|
|||
if data:
|
||||
self._data.update(data)
|
||||
|
||||
for key, default in self.__required_keys.items():
|
||||
self._data.setdefault(key, default)
|
||||
|
||||
if not self._data.get("instance_id"):
|
||||
self._data["instance_id"] = str(uuid4())
|
||||
|
||||
|
|
@ -563,6 +577,8 @@ class CreatedInstance:
|
|||
has_key = key in self._data
|
||||
output = self._data.pop(key, *args, **kwargs)
|
||||
if has_key:
|
||||
if key in self.__required_keys:
|
||||
self._data[key] = self.__required_keys[key]
|
||||
self._create_context.instance_values_changed(
|
||||
self.id, {key: None}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -383,6 +383,13 @@ def get_representations_delivery_template_data(
|
|||
continue
|
||||
|
||||
template_data = repre_entity["context"]
|
||||
# Bug in 'ayon_api', 'get_representations_hierarchy' did not fully
|
||||
# convert representation entity. Fixed in 'ayon_api' 1.0.10.
|
||||
if isinstance(template_data, str):
|
||||
con = ayon_api.get_server_api_connection()
|
||||
repre_entity = con._representation_conversion(repre_entity)
|
||||
template_data = repre_entity["context"]
|
||||
|
||||
template_data.update(copy.deepcopy(general_template_data))
|
||||
template_data.update(get_folder_template_data(
|
||||
repre_hierarchy.folder, project_name
|
||||
|
|
@ -402,5 +409,9 @@ def get_representations_delivery_template_data(
|
|||
"version": version_entity["version"],
|
||||
})
|
||||
_merge_data(template_data, repre_entity["context"])
|
||||
|
||||
# Remove roots from template data to auto-fill them with anatomy data
|
||||
template_data.pop("root", None)
|
||||
|
||||
output[repre_id] = template_data
|
||||
return output
|
||||
|
|
|
|||
|
|
@ -205,9 +205,9 @@ class AYONPyblishPluginMixin:
|
|||
if not cls.__instanceEnabled__:
|
||||
return False
|
||||
|
||||
for _ in pyblish.logic.plugins_by_families(
|
||||
[cls], [instance.product_type]
|
||||
):
|
||||
families = [instance.product_type]
|
||||
families.extend(instance.get("families", []))
|
||||
for _ in pyblish.logic.plugins_by_families([cls], families):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ class OpenTaskPath(LauncherAction):
|
|||
if platform_name == "windows":
|
||||
args = ["start", path]
|
||||
elif platform_name == "darwin":
|
||||
args = ["open", "-na", path]
|
||||
args = ["open", "-R", path]
|
||||
elif platform_name == "linux":
|
||||
args = ["xdg-open", path]
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -95,9 +95,42 @@ class CollectOtioReview(pyblish.api.InstancePlugin):
|
|||
instance.data["label"] = label + " (review)"
|
||||
instance.data["families"] += ["review", "ftrack"]
|
||||
instance.data["otioReviewClips"] = otio_review_clips
|
||||
|
||||
self.log.info(
|
||||
"Creating review track: {}".format(otio_review_clips))
|
||||
|
||||
# get colorspace from metadata if available
|
||||
# get metadata from first clip with media reference
|
||||
r_otio_cl = next(
|
||||
(
|
||||
clip
|
||||
for clip in otio_review_clips
|
||||
if (
|
||||
isinstance(clip, otio.schema.Clip)
|
||||
and clip.media_reference
|
||||
)
|
||||
),
|
||||
None
|
||||
)
|
||||
if r_otio_cl is not None:
|
||||
media_ref = r_otio_cl.media_reference
|
||||
media_metadata = media_ref.metadata
|
||||
|
||||
# TODO: we might need some alternative method since
|
||||
# native OTIO exports do not support ayon metadata
|
||||
review_colorspace = media_metadata.get(
|
||||
"ayon.source.colorspace"
|
||||
)
|
||||
if review_colorspace is None:
|
||||
# Backwards compatibility for older scenes
|
||||
review_colorspace = media_metadata.get(
|
||||
"openpype.source.colourtransform"
|
||||
)
|
||||
if review_colorspace:
|
||||
instance.data["reviewColorspace"] = review_colorspace
|
||||
self.log.info(
|
||||
"Review colorspace: {}".format(review_colorspace))
|
||||
|
||||
self.log.debug(
|
||||
"_ instance.data: {}".format(pformat(instance.data)))
|
||||
self.log.debug(
|
||||
|
|
|
|||
|
|
@ -10,12 +10,16 @@ import os
|
|||
import clique
|
||||
import pyblish.api
|
||||
|
||||
from ayon_core.pipeline import publish
|
||||
from ayon_core.pipeline.publish import (
|
||||
get_publish_template_name
|
||||
)
|
||||
|
||||
|
||||
class CollectOtioSubsetResources(pyblish.api.InstancePlugin):
|
||||
class CollectOtioSubsetResources(
|
||||
pyblish.api.InstancePlugin,
|
||||
publish.ColormanagedPyblishPluginMixin
|
||||
):
|
||||
"""Get Resources for a product version"""
|
||||
|
||||
label = "Collect OTIO Subset Resources"
|
||||
|
|
@ -190,9 +194,13 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin):
|
|||
instance.data["originalDirname"] = self.staging_dir
|
||||
|
||||
if repre:
|
||||
colorspace = instance.data.get("colorspace")
|
||||
# add colorspace data to representation
|
||||
self.set_representation_colorspace(
|
||||
repre, instance.context, colorspace)
|
||||
|
||||
# add representation to instance data
|
||||
instance.data["representations"].append(repre)
|
||||
self.log.debug(">>>>>>>> {}".format(repre))
|
||||
|
||||
self.log.debug(instance.data)
|
||||
|
||||
|
|
|
|||
|
|
@ -157,12 +157,15 @@ class ExtractOIIOTranscode(publish.Extractor):
|
|||
|
||||
files_to_convert = self._translate_to_sequence(
|
||||
files_to_convert)
|
||||
self.log.debug("Files to convert: {}".format(files_to_convert))
|
||||
for file_name in files_to_convert:
|
||||
self.log.debug("Transcoding file: `{}`".format(file_name))
|
||||
input_path = os.path.join(original_staging_dir,
|
||||
file_name)
|
||||
output_path = self._get_output_file_path(input_path,
|
||||
new_staging_dir,
|
||||
output_extension)
|
||||
|
||||
convert_colorspace(
|
||||
input_path,
|
||||
output_path,
|
||||
|
|
|
|||
|
|
@ -37,6 +37,9 @@ class ExtractColorspaceData(publish.Extractor,
|
|||
# get colorspace settings
|
||||
context = instance.context
|
||||
|
||||
# colorspace name could be kept in instance.data
|
||||
colorspace = instance.data.get("colorspace")
|
||||
|
||||
# loop representations
|
||||
for representation in representations:
|
||||
# skip if colorspaceData is already at representation
|
||||
|
|
@ -44,5 +47,4 @@ class ExtractColorspaceData(publish.Extractor,
|
|||
continue
|
||||
|
||||
self.set_representation_colorspace(
|
||||
representation, context
|
||||
)
|
||||
representation, context, colorspace)
|
||||
|
|
|
|||
|
|
@ -154,7 +154,9 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin):
|
|||
# TODO check if existing entity have 'task' type
|
||||
if task_entity is None:
|
||||
task_entity = entity_hub.add_new_task(
|
||||
task_info["type"],
|
||||
task_type=task_info["type"],
|
||||
# TODO change 'parent_id' to 'folder_id' when ayon api
|
||||
# is updated
|
||||
parent_id=entity.id,
|
||||
name=task_name
|
||||
)
|
||||
|
|
@ -182,7 +184,7 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin):
|
|||
folder_type = "Folder"
|
||||
|
||||
child_entity = entity_hub.add_new_folder(
|
||||
folder_type,
|
||||
folder_type=folder_type,
|
||||
parent_id=entity.id,
|
||||
name=child_name
|
||||
)
|
||||
|
|
|
|||
|
|
@ -26,7 +26,10 @@ from ayon_core.lib import (
|
|||
from ayon_core.pipeline import publish
|
||||
|
||||
|
||||
class ExtractOTIOReview(publish.Extractor):
|
||||
class ExtractOTIOReview(
|
||||
publish.Extractor,
|
||||
publish.ColormanagedPyblishPluginMixin
|
||||
):
|
||||
"""
|
||||
Extract OTIO timeline into one concuted image sequence file.
|
||||
|
||||
|
|
@ -71,14 +74,19 @@ class ExtractOTIOReview(publish.Extractor):
|
|||
# TODO: what if handles are different in `versionData`?
|
||||
handle_start = instance.data["handleStart"]
|
||||
handle_end = instance.data["handleEnd"]
|
||||
otio_review_clips = instance.data["otioReviewClips"]
|
||||
otio_review_clips = instance.data.get("otioReviewClips")
|
||||
|
||||
if otio_review_clips is None:
|
||||
self.log.info(f"Instance `{instance}` has no otioReviewClips")
|
||||
|
||||
# add plugin wide attributes
|
||||
self.representation_files = []
|
||||
self.used_frames = []
|
||||
self.workfile_start = int(instance.data.get(
|
||||
"workfileFrameStart", 1001)) - handle_start
|
||||
self.padding = len(str(self.workfile_start))
|
||||
# NOTE: padding has to be converted from
|
||||
# end frame since start could be lower then 1000
|
||||
self.padding = len(str(instance.data.get("frameEnd", 1001)))
|
||||
self.used_frames.append(self.workfile_start)
|
||||
self.to_width = instance.data.get(
|
||||
"resolutionWidth") or self.to_width
|
||||
|
|
@ -86,8 +94,10 @@ class ExtractOTIOReview(publish.Extractor):
|
|||
"resolutionHeight") or self.to_height
|
||||
|
||||
# skip instance if no reviewable data available
|
||||
if (not isinstance(otio_review_clips[0], otio.schema.Clip)) \
|
||||
and (len(otio_review_clips) == 1):
|
||||
if (
|
||||
not isinstance(otio_review_clips[0], otio.schema.Clip)
|
||||
and len(otio_review_clips) == 1
|
||||
):
|
||||
self.log.warning(
|
||||
"Instance `{}` has nothing to process".format(instance))
|
||||
return
|
||||
|
|
@ -168,7 +178,7 @@ class ExtractOTIOReview(publish.Extractor):
|
|||
start -= clip_handle_start
|
||||
duration += clip_handle_start
|
||||
elif len(otio_review_clips) > 1 \
|
||||
and (index == len(otio_review_clips) - 1):
|
||||
and (index == len(otio_review_clips) - 1):
|
||||
# more clips | last clip reframing with handle
|
||||
duration += clip_handle_end
|
||||
elif len(otio_review_clips) == 1:
|
||||
|
|
@ -263,6 +273,13 @@ class ExtractOTIOReview(publish.Extractor):
|
|||
|
||||
# creating and registering representation
|
||||
representation = self._create_representation(start, duration)
|
||||
|
||||
# add colorspace data to representation
|
||||
if colorspace := instance.data.get("reviewColorspace"):
|
||||
self.set_representation_colorspace(
|
||||
representation, instance.context, colorspace
|
||||
)
|
||||
|
||||
instance.data["representations"].append(representation)
|
||||
self.log.info("Adding representation: {}".format(representation))
|
||||
|
||||
|
|
|
|||
|
|
@ -458,7 +458,18 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
|
|||
return new_instance
|
||||
|
||||
@classmethod
|
||||
def get_attribute_defs(cls):
|
||||
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 []
|
||||
|
||||
# Attributes logic
|
||||
publish_attributes = instance["publish_attributes"].get(
|
||||
cls.__name__, {})
|
||||
|
||||
visible = publish_attributes.get("contribution_enabled", True)
|
||||
variant_visible = visible and publish_attributes.get(
|
||||
"contribution_apply_as_variant", True)
|
||||
|
||||
return [
|
||||
UISeparatorDef("usd_container_settings1"),
|
||||
|
|
@ -484,7 +495,8 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
|
|||
"the contribution itself will be added to the "
|
||||
"department layer."
|
||||
),
|
||||
default="usdAsset"),
|
||||
default="usdAsset",
|
||||
visible=visible),
|
||||
EnumDef("contribution_target_product_init",
|
||||
label="Initialize as",
|
||||
tooltip=(
|
||||
|
|
@ -495,7 +507,8 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
|
|||
"setting will do nothing."
|
||||
),
|
||||
items=["asset", "shot"],
|
||||
default="asset"),
|
||||
default="asset",
|
||||
visible=visible),
|
||||
|
||||
# Asset layer, e.g. model.usd, look.usd, rig.usd
|
||||
EnumDef("contribution_layer",
|
||||
|
|
@ -507,7 +520,8 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
|
|||
"the list) will contribute as a stronger opinion."
|
||||
),
|
||||
items=list(cls.contribution_layers.keys()),
|
||||
default="model"),
|
||||
default="model",
|
||||
visible=visible),
|
||||
BoolDef("contribution_apply_as_variant",
|
||||
label="Add as variant",
|
||||
tooltip=(
|
||||
|
|
@ -518,13 +532,16 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
|
|||
"appended to as a sublayer to the department layer "
|
||||
"instead."
|
||||
),
|
||||
default=True),
|
||||
default=True,
|
||||
visible=visible),
|
||||
TextDef("contribution_variant_set_name",
|
||||
label="Variant Set Name",
|
||||
default="{layer}"),
|
||||
default="{layer}",
|
||||
visible=variant_visible),
|
||||
TextDef("contribution_variant",
|
||||
label="Variant Name",
|
||||
default="{variant}"),
|
||||
default="{variant}",
|
||||
visible=variant_visible),
|
||||
BoolDef("contribution_variant_is_default",
|
||||
label="Set as default variant selection",
|
||||
tooltip=(
|
||||
|
|
@ -535,10 +552,41 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
|
|||
"The behavior is unpredictable if multiple instances "
|
||||
"for the same variant set have this enabled."
|
||||
),
|
||||
default=False),
|
||||
default=False,
|
||||
visible=variant_visible),
|
||||
UISeparatorDef("usd_container_settings3"),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def register_create_context_callbacks(cls, create_context):
|
||||
create_context.add_value_changed_callback(cls.on_values_changed)
|
||||
|
||||
@classmethod
|
||||
def on_values_changed(cls, event):
|
||||
"""Update instance attribute definitions on attribute changes."""
|
||||
|
||||
# Update attributes if any of the following plug-in attributes
|
||||
# change:
|
||||
keys = ["contribution_enabled", "contribution_apply_as_variant"]
|
||||
|
||||
for instance_change in event["changes"]:
|
||||
instance = instance_change["instance"]
|
||||
if not cls.instance_matches_plugin_families(instance):
|
||||
continue
|
||||
value_changes = instance_change["changes"]
|
||||
plugin_attribute_changes = (
|
||||
value_changes.get("publish_attributes", {})
|
||||
.get(cls.__name__, {}))
|
||||
|
||||
if not any(key in plugin_attribute_changes for key in keys):
|
||||
continue
|
||||
|
||||
# Update the attribute definitions
|
||||
new_attrs = cls.get_attr_defs_for_instance(
|
||||
event["create_context"], instance
|
||||
)
|
||||
instance.set_publish_plugin_attr_defs(cls.__name__, new_attrs)
|
||||
|
||||
|
||||
class CollectUSDLayerContributionsHoudiniLook(CollectUSDLayerContributions):
|
||||
"""
|
||||
|
|
@ -551,9 +599,12 @@ class CollectUSDLayerContributionsHoudiniLook(CollectUSDLayerContributions):
|
|||
label = CollectUSDLayerContributions.label + " (Look)"
|
||||
|
||||
@classmethod
|
||||
def get_attribute_defs(cls):
|
||||
defs = super(CollectUSDLayerContributionsHoudiniLook,
|
||||
cls).get_attribute_defs()
|
||||
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")
|
||||
|
|
|
|||
|
|
@ -60,7 +60,11 @@
|
|||
"icon-alert-tools": "#AA5050",
|
||||
"icon-entity-default": "#bfccd6",
|
||||
"icon-entity-disabled": "#808080",
|
||||
|
||||
"font-entity-deprecated": "#666666",
|
||||
|
||||
"font-overridden": "#91CDFC",
|
||||
|
||||
"overlay-messages": {
|
||||
"close-btn": "#D3D8DE",
|
||||
"bg-success": "#458056",
|
||||
|
|
|
|||
|
|
@ -1585,6 +1585,10 @@ CreateNextPageOverlay {
|
|||
}
|
||||
|
||||
/* Attribute Definition widgets */
|
||||
AttributeDefinitionsLabel[overridden="1"] {
|
||||
color: {color:font-overridden};
|
||||
}
|
||||
|
||||
AttributeDefinitionsWidget QAbstractSpinBox, QLineEdit, QPlainTextEdit, QTextEdit {
|
||||
padding: 1px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from .widgets import (
|
||||
create_widget_for_attr_def,
|
||||
AttributeDefinitionsWidget,
|
||||
AttributeDefinitionsLabel,
|
||||
)
|
||||
|
||||
from .dialog import (
|
||||
|
|
@ -11,6 +12,7 @@ from .dialog import (
|
|||
__all__ = (
|
||||
"create_widget_for_attr_def",
|
||||
"AttributeDefinitionsWidget",
|
||||
"AttributeDefinitionsLabel",
|
||||
|
||||
"AttributeDefinitionsDialog",
|
||||
)
|
||||
|
|
|
|||
1
client/ayon_core/tools/attribute_defs/_constants.py
Normal file
1
client/ayon_core/tools/attribute_defs/_constants.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
REVERT_TO_DEFAULT_LABEL = "Revert to default"
|
||||
|
|
@ -17,6 +17,8 @@ from ayon_core.tools.utils import (
|
|||
PixmapLabel
|
||||
)
|
||||
|
||||
from ._constants import REVERT_TO_DEFAULT_LABEL
|
||||
|
||||
ITEM_ID_ROLE = QtCore.Qt.UserRole + 1
|
||||
ITEM_LABEL_ROLE = QtCore.Qt.UserRole + 2
|
||||
ITEM_ICON_ROLE = QtCore.Qt.UserRole + 3
|
||||
|
|
@ -598,7 +600,7 @@ class FilesView(QtWidgets.QListView):
|
|||
"""View showing instances and their groups."""
|
||||
|
||||
remove_requested = QtCore.Signal()
|
||||
context_menu_requested = QtCore.Signal(QtCore.QPoint)
|
||||
context_menu_requested = QtCore.Signal(QtCore.QPoint, bool)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(FilesView, self).__init__(*args, **kwargs)
|
||||
|
|
@ -690,9 +692,8 @@ class FilesView(QtWidgets.QListView):
|
|||
|
||||
def _on_context_menu_request(self, pos):
|
||||
index = self.indexAt(pos)
|
||||
if index.isValid():
|
||||
point = self.viewport().mapToGlobal(pos)
|
||||
self.context_menu_requested.emit(point)
|
||||
point = self.viewport().mapToGlobal(pos)
|
||||
self.context_menu_requested.emit(point, index.isValid())
|
||||
|
||||
def _on_selection_change(self):
|
||||
self._remove_btn.setEnabled(self.has_selected_item_ids())
|
||||
|
|
@ -721,27 +722,34 @@ class FilesView(QtWidgets.QListView):
|
|||
|
||||
class FilesWidget(QtWidgets.QFrame):
|
||||
value_changed = QtCore.Signal()
|
||||
revert_requested = QtCore.Signal()
|
||||
|
||||
def __init__(self, single_item, allow_sequences, extensions_label, parent):
|
||||
super(FilesWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
self.setAcceptDrops(True)
|
||||
|
||||
wrapper_widget = QtWidgets.QWidget(self)
|
||||
|
||||
empty_widget = DropEmpty(
|
||||
single_item, allow_sequences, extensions_label, self
|
||||
single_item, allow_sequences, extensions_label, wrapper_widget
|
||||
)
|
||||
|
||||
files_model = FilesModel(single_item, allow_sequences)
|
||||
files_proxy_model = FilesProxyModel()
|
||||
files_proxy_model.setSourceModel(files_model)
|
||||
files_view = FilesView(self)
|
||||
files_view = FilesView(wrapper_widget)
|
||||
files_view.setModel(files_proxy_model)
|
||||
|
||||
layout = QtWidgets.QStackedLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setStackingMode(QtWidgets.QStackedLayout.StackAll)
|
||||
layout.addWidget(empty_widget)
|
||||
layout.addWidget(files_view)
|
||||
layout.setCurrentWidget(empty_widget)
|
||||
wrapper_layout = QtWidgets.QStackedLayout(wrapper_widget)
|
||||
wrapper_layout.setContentsMargins(0, 0, 0, 0)
|
||||
wrapper_layout.setStackingMode(QtWidgets.QStackedLayout.StackAll)
|
||||
wrapper_layout.addWidget(empty_widget)
|
||||
wrapper_layout.addWidget(files_view)
|
||||
wrapper_layout.setCurrentWidget(empty_widget)
|
||||
|
||||
main_layout = QtWidgets.QHBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.addWidget(wrapper_widget, 1)
|
||||
|
||||
files_proxy_model.rowsInserted.connect(self._on_rows_inserted)
|
||||
files_proxy_model.rowsRemoved.connect(self._on_rows_removed)
|
||||
|
|
@ -761,7 +769,11 @@ class FilesWidget(QtWidgets.QFrame):
|
|||
|
||||
self._widgets_by_id = {}
|
||||
|
||||
self._layout = layout
|
||||
self._wrapper_widget = wrapper_widget
|
||||
self._wrapper_layout = wrapper_layout
|
||||
|
||||
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(self._on_context_menu)
|
||||
|
||||
def _set_multivalue(self, multivalue):
|
||||
if self._multivalue is multivalue:
|
||||
|
|
@ -770,7 +782,7 @@ class FilesWidget(QtWidgets.QFrame):
|
|||
self._files_view.set_multivalue(multivalue)
|
||||
self._files_model.set_multivalue(multivalue)
|
||||
self._files_proxy_model.set_multivalue(multivalue)
|
||||
self.setEnabled(not multivalue)
|
||||
self._wrapper_widget.setEnabled(not multivalue)
|
||||
|
||||
def set_value(self, value, multivalue):
|
||||
self._in_set_value = True
|
||||
|
|
@ -888,22 +900,28 @@ class FilesWidget(QtWidgets.QFrame):
|
|||
if items_to_delete:
|
||||
self._remove_item_by_ids(items_to_delete)
|
||||
|
||||
def _on_context_menu_requested(self, pos):
|
||||
if self._multivalue:
|
||||
return
|
||||
def _on_context_menu(self, pos):
|
||||
self._on_context_menu_requested(pos, False)
|
||||
|
||||
def _on_context_menu_requested(self, pos, valid_index):
|
||||
menu = QtWidgets.QMenu(self._files_view)
|
||||
if valid_index and not self._multivalue:
|
||||
if self._files_view.has_selected_sequence():
|
||||
split_action = QtWidgets.QAction("Split sequence", menu)
|
||||
split_action.triggered.connect(self._on_split_request)
|
||||
menu.addAction(split_action)
|
||||
|
||||
if self._files_view.has_selected_sequence():
|
||||
split_action = QtWidgets.QAction("Split sequence", menu)
|
||||
split_action.triggered.connect(self._on_split_request)
|
||||
menu.addAction(split_action)
|
||||
remove_action = QtWidgets.QAction("Remove", menu)
|
||||
remove_action.triggered.connect(self._on_remove_requested)
|
||||
menu.addAction(remove_action)
|
||||
|
||||
remove_action = QtWidgets.QAction("Remove", menu)
|
||||
remove_action.triggered.connect(self._on_remove_requested)
|
||||
menu.addAction(remove_action)
|
||||
if not valid_index:
|
||||
revert_action = QtWidgets.QAction(REVERT_TO_DEFAULT_LABEL, menu)
|
||||
revert_action.triggered.connect(self.revert_requested)
|
||||
menu.addAction(revert_action)
|
||||
|
||||
menu.popup(pos)
|
||||
if menu.actions():
|
||||
menu.popup(pos)
|
||||
|
||||
def dragEnterEvent(self, event):
|
||||
if self._multivalue:
|
||||
|
|
@ -1011,5 +1029,5 @@ class FilesWidget(QtWidgets.QFrame):
|
|||
current_widget = self._files_view
|
||||
else:
|
||||
current_widget = self._empty_widget
|
||||
self._layout.setCurrentWidget(current_widget)
|
||||
self._wrapper_layout.setCurrentWidget(current_widget)
|
||||
self._files_view.update_remove_btn_visibility()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import copy
|
||||
import typing
|
||||
from typing import Optional
|
||||
|
||||
from qtpy import QtWidgets, QtCore
|
||||
|
||||
|
|
@ -20,14 +22,25 @@ from ayon_core.tools.utils import (
|
|||
FocusSpinBox,
|
||||
FocusDoubleSpinBox,
|
||||
MultiSelectionComboBox,
|
||||
set_style_property,
|
||||
)
|
||||
from ayon_core.tools.utils import NiceCheckbox
|
||||
|
||||
from ._constants import REVERT_TO_DEFAULT_LABEL
|
||||
from .files_widget import FilesWidget
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from typing import Union
|
||||
|
||||
def create_widget_for_attr_def(attr_def, parent=None):
|
||||
widget = _create_widget_for_attr_def(attr_def, parent)
|
||||
|
||||
def create_widget_for_attr_def(
|
||||
attr_def: AbstractAttrDef,
|
||||
parent: Optional[QtWidgets.QWidget] = None,
|
||||
handle_revert_to_default: Optional[bool] = True,
|
||||
):
|
||||
widget = _create_widget_for_attr_def(
|
||||
attr_def, parent, handle_revert_to_default
|
||||
)
|
||||
if not attr_def.visible:
|
||||
widget.setVisible(False)
|
||||
|
||||
|
|
@ -36,42 +49,96 @@ def create_widget_for_attr_def(attr_def, parent=None):
|
|||
return widget
|
||||
|
||||
|
||||
def _create_widget_for_attr_def(attr_def, parent=None):
|
||||
def _create_widget_for_attr_def(
|
||||
attr_def: AbstractAttrDef,
|
||||
parent: "Union[QtWidgets.QWidget, None]",
|
||||
handle_revert_to_default: bool,
|
||||
):
|
||||
if not isinstance(attr_def, AbstractAttrDef):
|
||||
raise TypeError("Unexpected type \"{}\" expected \"{}\"".format(
|
||||
str(type(attr_def)), AbstractAttrDef
|
||||
))
|
||||
|
||||
cls = None
|
||||
if isinstance(attr_def, NumberDef):
|
||||
return NumberAttrWidget(attr_def, parent)
|
||||
cls = NumberAttrWidget
|
||||
|
||||
if isinstance(attr_def, TextDef):
|
||||
return TextAttrWidget(attr_def, parent)
|
||||
elif isinstance(attr_def, TextDef):
|
||||
cls = TextAttrWidget
|
||||
|
||||
if isinstance(attr_def, EnumDef):
|
||||
return EnumAttrWidget(attr_def, parent)
|
||||
elif isinstance(attr_def, EnumDef):
|
||||
cls = EnumAttrWidget
|
||||
|
||||
if isinstance(attr_def, BoolDef):
|
||||
return BoolAttrWidget(attr_def, parent)
|
||||
elif isinstance(attr_def, BoolDef):
|
||||
cls = BoolAttrWidget
|
||||
|
||||
if isinstance(attr_def, UnknownDef):
|
||||
return UnknownAttrWidget(attr_def, parent)
|
||||
elif isinstance(attr_def, UnknownDef):
|
||||
cls = UnknownAttrWidget
|
||||
|
||||
if isinstance(attr_def, HiddenDef):
|
||||
return HiddenAttrWidget(attr_def, parent)
|
||||
elif isinstance(attr_def, HiddenDef):
|
||||
cls = HiddenAttrWidget
|
||||
|
||||
if isinstance(attr_def, FileDef):
|
||||
return FileAttrWidget(attr_def, parent)
|
||||
elif isinstance(attr_def, FileDef):
|
||||
cls = FileAttrWidget
|
||||
|
||||
if isinstance(attr_def, UISeparatorDef):
|
||||
return SeparatorAttrWidget(attr_def, parent)
|
||||
elif isinstance(attr_def, UISeparatorDef):
|
||||
cls = SeparatorAttrWidget
|
||||
|
||||
if isinstance(attr_def, UILabelDef):
|
||||
return LabelAttrWidget(attr_def, parent)
|
||||
elif isinstance(attr_def, UILabelDef):
|
||||
cls = LabelAttrWidget
|
||||
|
||||
raise ValueError("Unknown attribute definition \"{}\"".format(
|
||||
str(type(attr_def))
|
||||
))
|
||||
if cls is None:
|
||||
raise ValueError("Unknown attribute definition \"{}\"".format(
|
||||
str(type(attr_def))
|
||||
))
|
||||
|
||||
return cls(attr_def, parent, handle_revert_to_default)
|
||||
|
||||
|
||||
class AttributeDefinitionsLabel(QtWidgets.QLabel):
|
||||
"""Label related to value attribute definition.
|
||||
|
||||
Label is used to show attribute definition label and to show if value
|
||||
is overridden.
|
||||
|
||||
Label can be right-clicked to revert value to default.
|
||||
"""
|
||||
revert_to_default_requested = QtCore.Signal(str)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
attr_id: str,
|
||||
label: str,
|
||||
parent: QtWidgets.QWidget,
|
||||
):
|
||||
super().__init__(label, parent)
|
||||
|
||||
self._attr_id = attr_id
|
||||
self._overridden = False
|
||||
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
|
||||
self.customContextMenuRequested.connect(self._on_context_menu)
|
||||
|
||||
def set_overridden(self, overridden: bool):
|
||||
if self._overridden == overridden:
|
||||
return
|
||||
self._overridden = overridden
|
||||
set_style_property(
|
||||
self,
|
||||
"overridden",
|
||||
"1" if overridden else ""
|
||||
)
|
||||
|
||||
def _on_context_menu(self, point: QtCore.QPoint):
|
||||
menu = QtWidgets.QMenu(self)
|
||||
action = QtWidgets.QAction(menu)
|
||||
action.setText(REVERT_TO_DEFAULT_LABEL)
|
||||
action.triggered.connect(self._request_revert_to_default)
|
||||
menu.addAction(action)
|
||||
menu.exec_(self.mapToGlobal(point))
|
||||
|
||||
def _request_revert_to_default(self):
|
||||
self.revert_to_default_requested.emit(self._attr_id)
|
||||
|
||||
|
||||
class AttributeDefinitionsWidget(QtWidgets.QWidget):
|
||||
|
|
@ -83,16 +150,18 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget):
|
|||
"""
|
||||
|
||||
def __init__(self, attr_defs=None, parent=None):
|
||||
super(AttributeDefinitionsWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
self._widgets = []
|
||||
self._widgets_by_id = {}
|
||||
self._labels_by_id = {}
|
||||
self._current_keys = set()
|
||||
|
||||
self.set_attr_defs(attr_defs)
|
||||
|
||||
def clear_attr_defs(self):
|
||||
"""Remove all existing widgets and reset layout if needed."""
|
||||
self._widgets = []
|
||||
self._widgets_by_id = {}
|
||||
self._labels_by_id = {}
|
||||
self._current_keys = set()
|
||||
|
||||
layout = self.layout()
|
||||
|
|
@ -133,7 +202,7 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget):
|
|||
|
||||
self._current_keys.add(attr_def.key)
|
||||
widget = create_widget_for_attr_def(attr_def, self)
|
||||
self._widgets.append(widget)
|
||||
self._widgets_by_id[attr_def.id] = widget
|
||||
|
||||
if not attr_def.visible:
|
||||
continue
|
||||
|
|
@ -145,7 +214,13 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget):
|
|||
col_num = 2 - expand_cols
|
||||
|
||||
if attr_def.is_value_def and attr_def.label:
|
||||
label_widget = QtWidgets.QLabel(attr_def.label, self)
|
||||
label_widget = AttributeDefinitionsLabel(
|
||||
attr_def.id, attr_def.label, self
|
||||
)
|
||||
label_widget.revert_to_default_requested.connect(
|
||||
self._on_revert_request
|
||||
)
|
||||
self._labels_by_id[attr_def.id] = label_widget
|
||||
tooltip = attr_def.tooltip
|
||||
if tooltip:
|
||||
label_widget.setToolTip(tooltip)
|
||||
|
|
@ -160,6 +235,9 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget):
|
|||
if not attr_def.is_label_horizontal:
|
||||
row += 1
|
||||
|
||||
if attr_def.is_value_def:
|
||||
widget.value_changed.connect(self._on_value_change)
|
||||
|
||||
layout.addWidget(
|
||||
widget, row, col_num, 1, expand_cols
|
||||
)
|
||||
|
|
@ -168,7 +246,7 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget):
|
|||
def set_value(self, value):
|
||||
new_value = copy.deepcopy(value)
|
||||
unused_keys = set(new_value.keys())
|
||||
for widget in self._widgets:
|
||||
for widget in self._widgets_by_id.values():
|
||||
attr_def = widget.attr_def
|
||||
if attr_def.key not in new_value:
|
||||
continue
|
||||
|
|
@ -181,22 +259,42 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget):
|
|||
|
||||
def current_value(self):
|
||||
output = {}
|
||||
for widget in self._widgets:
|
||||
for widget in self._widgets_by_id.values():
|
||||
attr_def = widget.attr_def
|
||||
if not isinstance(attr_def, UIDef):
|
||||
output[attr_def.key] = widget.current_value()
|
||||
|
||||
return output
|
||||
|
||||
def _on_revert_request(self, attr_id):
|
||||
widget = self._widgets_by_id.get(attr_id)
|
||||
if widget is not None:
|
||||
widget.set_value(widget.attr_def.default)
|
||||
|
||||
def _on_value_change(self, value, attr_id):
|
||||
widget = self._widgets_by_id.get(attr_id)
|
||||
if widget is None:
|
||||
return
|
||||
label = self._labels_by_id.get(attr_id)
|
||||
if label is not None:
|
||||
label.set_overridden(value != widget.attr_def.default)
|
||||
|
||||
|
||||
class _BaseAttrDefWidget(QtWidgets.QWidget):
|
||||
# Type 'object' may not work with older PySide versions
|
||||
value_changed = QtCore.Signal(object, str)
|
||||
revert_to_default_requested = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, attr_def, parent):
|
||||
super(_BaseAttrDefWidget, self).__init__(parent)
|
||||
def __init__(
|
||||
self,
|
||||
attr_def: AbstractAttrDef,
|
||||
parent: "Union[QtWidgets.QWidget, None]",
|
||||
handle_revert_to_default: Optional[bool] = True,
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
self.attr_def = attr_def
|
||||
self.attr_def: AbstractAttrDef = attr_def
|
||||
self._handle_revert_to_default: bool = handle_revert_to_default
|
||||
|
||||
main_layout = QtWidgets.QHBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
|
@ -205,6 +303,15 @@ class _BaseAttrDefWidget(QtWidgets.QWidget):
|
|||
|
||||
self._ui_init()
|
||||
|
||||
def revert_to_default_value(self):
|
||||
if not self.attr_def.is_value_def:
|
||||
return
|
||||
|
||||
if self._handle_revert_to_default:
|
||||
self.set_value(self.attr_def.default)
|
||||
else:
|
||||
self.revert_to_default_requested.emit(self.attr_def.id)
|
||||
|
||||
def _ui_init(self):
|
||||
raise NotImplementedError(
|
||||
"Method '_ui_init' is not implemented. {}".format(
|
||||
|
|
@ -255,7 +362,7 @@ class ClickableLineEdit(QtWidgets.QLineEdit):
|
|||
clicked = QtCore.Signal()
|
||||
|
||||
def __init__(self, text, parent):
|
||||
super(ClickableLineEdit, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
self.setText(text)
|
||||
self.setReadOnly(True)
|
||||
|
||||
|
|
@ -264,7 +371,7 @@ class ClickableLineEdit(QtWidgets.QLineEdit):
|
|||
def mousePressEvent(self, event):
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
self._mouse_pressed = True
|
||||
super(ClickableLineEdit, self).mousePressEvent(event)
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if self._mouse_pressed:
|
||||
|
|
@ -272,7 +379,7 @@ class ClickableLineEdit(QtWidgets.QLineEdit):
|
|||
if self.rect().contains(event.pos()):
|
||||
self.clicked.emit()
|
||||
|
||||
super(ClickableLineEdit, self).mouseReleaseEvent(event)
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
|
||||
class NumberAttrWidget(_BaseAttrDefWidget):
|
||||
|
|
@ -284,6 +391,9 @@ class NumberAttrWidget(_BaseAttrDefWidget):
|
|||
else:
|
||||
input_widget = FocusSpinBox(self)
|
||||
|
||||
# Override context menu event to add revert to default action
|
||||
input_widget.contextMenuEvent = self._input_widget_context_event
|
||||
|
||||
if self.attr_def.tooltip:
|
||||
input_widget.setToolTip(self.attr_def.tooltip)
|
||||
|
||||
|
|
@ -321,6 +431,16 @@ class NumberAttrWidget(_BaseAttrDefWidget):
|
|||
self._set_multiselection_visible(True)
|
||||
return False
|
||||
|
||||
def _input_widget_context_event(self, event):
|
||||
line_edit = self._input_widget.lineEdit()
|
||||
menu = line_edit.createStandardContextMenu()
|
||||
menu.setAttribute(QtCore.Qt.WA_DeleteOnClose)
|
||||
action = QtWidgets.QAction(menu)
|
||||
action.setText(REVERT_TO_DEFAULT_LABEL)
|
||||
action.triggered.connect(self.revert_to_default_value)
|
||||
menu.addAction(action)
|
||||
menu.popup(event.globalPos())
|
||||
|
||||
def current_value(self):
|
||||
return self._input_widget.value()
|
||||
|
||||
|
|
@ -386,6 +506,9 @@ class TextAttrWidget(_BaseAttrDefWidget):
|
|||
else:
|
||||
input_widget = QtWidgets.QLineEdit(self)
|
||||
|
||||
# Override context menu event to add revert to default action
|
||||
input_widget.contextMenuEvent = self._input_widget_context_event
|
||||
|
||||
if (
|
||||
self.attr_def.placeholder
|
||||
and hasattr(input_widget, "setPlaceholderText")
|
||||
|
|
@ -407,6 +530,15 @@ class TextAttrWidget(_BaseAttrDefWidget):
|
|||
|
||||
self.main_layout.addWidget(input_widget, 0)
|
||||
|
||||
def _input_widget_context_event(self, event):
|
||||
menu = self._input_widget.createStandardContextMenu()
|
||||
menu.setAttribute(QtCore.Qt.WA_DeleteOnClose)
|
||||
action = QtWidgets.QAction(menu)
|
||||
action.setText(REVERT_TO_DEFAULT_LABEL)
|
||||
action.triggered.connect(self.revert_to_default_value)
|
||||
menu.addAction(action)
|
||||
menu.popup(event.globalPos())
|
||||
|
||||
def _on_value_change(self):
|
||||
if self.multiline:
|
||||
new_value = self._input_widget.toPlainText()
|
||||
|
|
@ -459,6 +591,20 @@ class BoolAttrWidget(_BaseAttrDefWidget):
|
|||
self.main_layout.addWidget(input_widget, 0)
|
||||
self.main_layout.addStretch(1)
|
||||
|
||||
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(self._on_context_menu)
|
||||
|
||||
def _on_context_menu(self, pos):
|
||||
self._menu = QtWidgets.QMenu(self)
|
||||
|
||||
action = QtWidgets.QAction(self._menu)
|
||||
action.setText(REVERT_TO_DEFAULT_LABEL)
|
||||
action.triggered.connect(self.revert_to_default_value)
|
||||
self._menu.addAction(action)
|
||||
|
||||
global_pos = self.mapToGlobal(pos)
|
||||
self._menu.exec_(global_pos)
|
||||
|
||||
def _on_value_change(self):
|
||||
new_value = self._input_widget.isChecked()
|
||||
self.value_changed.emit(new_value, self.attr_def.id)
|
||||
|
|
@ -487,7 +633,7 @@ class BoolAttrWidget(_BaseAttrDefWidget):
|
|||
class EnumAttrWidget(_BaseAttrDefWidget):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._multivalue = False
|
||||
super(EnumAttrWidget, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def multiselection(self):
|
||||
|
|
@ -522,6 +668,20 @@ class EnumAttrWidget(_BaseAttrDefWidget):
|
|||
|
||||
self.main_layout.addWidget(input_widget, 0)
|
||||
|
||||
input_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
input_widget.customContextMenuRequested.connect(self._on_context_menu)
|
||||
|
||||
def _on_context_menu(self, pos):
|
||||
menu = QtWidgets.QMenu(self)
|
||||
|
||||
action = QtWidgets.QAction(menu)
|
||||
action.setText(REVERT_TO_DEFAULT_LABEL)
|
||||
action.triggered.connect(self.revert_to_default_value)
|
||||
menu.addAction(action)
|
||||
|
||||
global_pos = self.mapToGlobal(pos)
|
||||
menu.exec_(global_pos)
|
||||
|
||||
def _on_value_change(self):
|
||||
new_value = self.current_value()
|
||||
if self._multivalue:
|
||||
|
|
@ -614,7 +774,7 @@ class HiddenAttrWidget(_BaseAttrDefWidget):
|
|||
def setVisible(self, visible):
|
||||
if visible:
|
||||
visible = False
|
||||
super(HiddenAttrWidget, self).setVisible(visible)
|
||||
super().setVisible(visible)
|
||||
|
||||
def current_value(self):
|
||||
if self._multivalue:
|
||||
|
|
@ -650,10 +810,25 @@ class FileAttrWidget(_BaseAttrDefWidget):
|
|||
|
||||
self.main_layout.addWidget(input_widget, 0)
|
||||
|
||||
input_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
input_widget.customContextMenuRequested.connect(self._on_context_menu)
|
||||
input_widget.revert_requested.connect(self.revert_to_default_value)
|
||||
|
||||
def _on_value_change(self):
|
||||
new_value = self.current_value()
|
||||
self.value_changed.emit(new_value, self.attr_def.id)
|
||||
|
||||
def _on_context_menu(self, pos):
|
||||
menu = QtWidgets.QMenu(self)
|
||||
|
||||
action = QtWidgets.QAction(menu)
|
||||
action.setText(REVERT_TO_DEFAULT_LABEL)
|
||||
action.triggered.connect(self.revert_to_default_value)
|
||||
menu.addAction(action)
|
||||
|
||||
global_pos = self.mapToGlobal(pos)
|
||||
menu.exec_(global_pos)
|
||||
|
||||
def current_value(self):
|
||||
return self._input_widget.current_value()
|
||||
|
||||
|
|
|
|||
|
|
@ -222,6 +222,7 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
|
|||
|
||||
editor = VersionComboBox(product_id, parent)
|
||||
editor.setProperty("itemId", item_id)
|
||||
editor.setFocusPolicy(QtCore.Qt.NoFocus)
|
||||
|
||||
editor.value_changed.connect(self._on_editor_change)
|
||||
editor.destroyed.connect(self._on_destroy)
|
||||
|
|
|
|||
|
|
@ -366,7 +366,7 @@ class AbstractPublisherFrontend(AbstractPublisherCommon):
|
|||
@abstractmethod
|
||||
def get_creator_attribute_definitions(
|
||||
self, instance_ids: Iterable[str]
|
||||
) -> List[Tuple[AbstractAttrDef, List[str], List[Any]]]:
|
||||
) -> List[Tuple[AbstractAttrDef, Dict[str, Dict[str, Any]]]]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -375,6 +375,14 @@ class AbstractPublisherFrontend(AbstractPublisherCommon):
|
|||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def revert_instances_create_attr_values(
|
||||
self,
|
||||
instance_ids: List["Union[str, None]"],
|
||||
key: str,
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_publish_attribute_definitions(
|
||||
self,
|
||||
|
|
@ -383,7 +391,7 @@ class AbstractPublisherFrontend(AbstractPublisherCommon):
|
|||
) -> List[Tuple[
|
||||
str,
|
||||
List[AbstractAttrDef],
|
||||
Dict[str, List[Tuple[str, Any]]]
|
||||
Dict[str, List[Tuple[str, Any, Any]]]
|
||||
]]:
|
||||
pass
|
||||
|
||||
|
|
@ -397,6 +405,15 @@ class AbstractPublisherFrontend(AbstractPublisherCommon):
|
|||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def revert_instances_publish_attr_values(
|
||||
self,
|
||||
instance_ids: List["Union[str, None]"],
|
||||
plugin_name: str,
|
||||
key: str,
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_product_name(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -412,6 +412,11 @@ class PublisherController(
|
|||
instance_ids, key, value
|
||||
)
|
||||
|
||||
def revert_instances_create_attr_values(self, instance_ids, key):
|
||||
self._create_model.revert_instances_create_attr_values(
|
||||
instance_ids, key
|
||||
)
|
||||
|
||||
def get_publish_attribute_definitions(self, instance_ids, include_context):
|
||||
"""Collect publish attribute definitions for passed instances.
|
||||
|
||||
|
|
@ -432,6 +437,13 @@ class PublisherController(
|
|||
instance_ids, plugin_name, key, value
|
||||
)
|
||||
|
||||
def revert_instances_publish_attr_values(
|
||||
self, instance_ids, plugin_name, key
|
||||
):
|
||||
return self._create_model.revert_instances_publish_attr_values(
|
||||
instance_ids, plugin_name, key
|
||||
)
|
||||
|
||||
def get_product_name(
|
||||
self,
|
||||
creator_identifier,
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ from ayon_core.tools.publisher.abstract import (
|
|||
)
|
||||
|
||||
CREATE_EVENT_SOURCE = "publisher.create.model"
|
||||
_DEFAULT_VALUE = object()
|
||||
|
||||
|
||||
class CreatorType:
|
||||
|
|
@ -295,7 +296,7 @@ class InstanceItem:
|
|||
return InstanceItem(
|
||||
instance.id,
|
||||
instance.creator_identifier,
|
||||
instance.label,
|
||||
instance.label or "N/A",
|
||||
instance.group_label,
|
||||
instance.product_type,
|
||||
instance.product_name,
|
||||
|
|
@ -752,24 +753,16 @@ class CreateModel:
|
|||
self._remove_instances_from_context(instance_ids)
|
||||
|
||||
def set_instances_create_attr_values(self, instance_ids, key, value):
|
||||
with self._create_context.bulk_value_changes(CREATE_EVENT_SOURCE):
|
||||
for instance_id in instance_ids:
|
||||
instance = self._get_instance_by_id(instance_id)
|
||||
creator_attributes = instance["creator_attributes"]
|
||||
attr_def = creator_attributes.get_attr_def(key)
|
||||
if (
|
||||
attr_def is None
|
||||
or not attr_def.is_value_def
|
||||
or not attr_def.visible
|
||||
or not attr_def.enabled
|
||||
or not attr_def.is_value_valid(value)
|
||||
):
|
||||
continue
|
||||
creator_attributes[key] = value
|
||||
self._set_instances_create_attr_values(instance_ids, key, value)
|
||||
|
||||
def revert_instances_create_attr_values(self, instance_ids, key):
|
||||
self._set_instances_create_attr_values(
|
||||
instance_ids, key, _DEFAULT_VALUE
|
||||
)
|
||||
|
||||
def get_creator_attribute_definitions(
|
||||
self, instance_ids: List[str]
|
||||
) -> List[Tuple[AbstractAttrDef, List[str], List[Any]]]:
|
||||
) -> List[Tuple[AbstractAttrDef, Dict[str, Dict[str, Any]]]]:
|
||||
"""Collect creator attribute definitions for multuple instances.
|
||||
|
||||
Args:
|
||||
|
|
@ -796,37 +789,38 @@ class CreateModel:
|
|||
|
||||
if found_idx is None:
|
||||
idx = len(output)
|
||||
output.append((attr_def, [instance_id], [value]))
|
||||
output.append((
|
||||
attr_def,
|
||||
{
|
||||
instance_id: {
|
||||
"value": value,
|
||||
"default": attr_def.default
|
||||
}
|
||||
}
|
||||
))
|
||||
_attr_defs[idx] = attr_def
|
||||
else:
|
||||
_, ids, values = output[found_idx]
|
||||
ids.append(instance_id)
|
||||
values.append(value)
|
||||
_, info_by_id = output[found_idx]
|
||||
info_by_id[instance_id] = {
|
||||
"value": value,
|
||||
"default": attr_def.default
|
||||
}
|
||||
|
||||
return output
|
||||
|
||||
def set_instances_publish_attr_values(
|
||||
self, instance_ids, plugin_name, key, value
|
||||
self, instance_ids, plugin_name, key, value
|
||||
):
|
||||
with self._create_context.bulk_value_changes(CREATE_EVENT_SOURCE):
|
||||
for instance_id in instance_ids:
|
||||
if instance_id is None:
|
||||
instance = self._create_context
|
||||
else:
|
||||
instance = self._get_instance_by_id(instance_id)
|
||||
plugin_val = instance.publish_attributes[plugin_name]
|
||||
attr_def = plugin_val.get_attr_def(key)
|
||||
# Ignore if attribute is not available or enabled/visible
|
||||
# on the instance, or the value is not valid for definition
|
||||
if (
|
||||
attr_def is None
|
||||
or not attr_def.is_value_def
|
||||
or not attr_def.visible
|
||||
or not attr_def.enabled
|
||||
or not attr_def.is_value_valid(value)
|
||||
):
|
||||
continue
|
||||
self._set_instances_publish_attr_values(
|
||||
instance_ids, plugin_name, key, value
|
||||
)
|
||||
|
||||
plugin_val[key] = value
|
||||
def revert_instances_publish_attr_values(
|
||||
self, instance_ids, plugin_name, key
|
||||
):
|
||||
self._set_instances_publish_attr_values(
|
||||
instance_ids, plugin_name, key, _DEFAULT_VALUE
|
||||
)
|
||||
|
||||
def get_publish_attribute_definitions(
|
||||
self,
|
||||
|
|
@ -835,7 +829,7 @@ class CreateModel:
|
|||
) -> List[Tuple[
|
||||
str,
|
||||
List[AbstractAttrDef],
|
||||
Dict[str, List[Tuple[str, Any]]]
|
||||
Dict[str, List[Tuple[str, Any, Any]]]
|
||||
]]:
|
||||
"""Collect publish attribute definitions for passed instances.
|
||||
|
||||
|
|
@ -865,21 +859,21 @@ class CreateModel:
|
|||
attr_defs = attr_val.attr_defs
|
||||
if not attr_defs:
|
||||
continue
|
||||
|
||||
plugin_attr_defs = all_defs_by_plugin_name.setdefault(
|
||||
plugin_name, []
|
||||
)
|
||||
plugin_attr_defs.append(attr_defs)
|
||||
|
||||
plugin_values = all_plugin_values.setdefault(plugin_name, {})
|
||||
|
||||
plugin_attr_defs.append(attr_defs)
|
||||
|
||||
for attr_def in attr_defs:
|
||||
if isinstance(attr_def, UIDef):
|
||||
continue
|
||||
|
||||
attr_values = plugin_values.setdefault(attr_def.key, [])
|
||||
|
||||
value = attr_val[attr_def.key]
|
||||
attr_values.append((item_id, value))
|
||||
attr_values.append(
|
||||
(item_id, attr_val[attr_def.key], attr_def.default)
|
||||
)
|
||||
|
||||
attr_defs_by_plugin_name = {}
|
||||
for plugin_name, attr_defs in all_defs_by_plugin_name.items():
|
||||
|
|
@ -893,7 +887,7 @@ class CreateModel:
|
|||
output.append((
|
||||
plugin_name,
|
||||
attr_defs_by_plugin_name[plugin_name],
|
||||
all_plugin_values
|
||||
all_plugin_values[plugin_name],
|
||||
))
|
||||
return output
|
||||
|
||||
|
|
@ -1053,6 +1047,53 @@ class CreateModel:
|
|||
CreatorItem.from_creator(creator)
|
||||
)
|
||||
|
||||
def _set_instances_create_attr_values(self, instance_ids, key, value):
|
||||
with self._create_context.bulk_value_changes(CREATE_EVENT_SOURCE):
|
||||
for instance_id in instance_ids:
|
||||
instance = self._get_instance_by_id(instance_id)
|
||||
creator_attributes = instance["creator_attributes"]
|
||||
attr_def = creator_attributes.get_attr_def(key)
|
||||
if (
|
||||
attr_def is None
|
||||
or not attr_def.is_value_def
|
||||
or not attr_def.visible
|
||||
or not attr_def.enabled
|
||||
):
|
||||
continue
|
||||
|
||||
if value is _DEFAULT_VALUE:
|
||||
creator_attributes[key] = attr_def.default
|
||||
|
||||
elif attr_def.is_value_valid(value):
|
||||
creator_attributes[key] = value
|
||||
|
||||
def _set_instances_publish_attr_values(
|
||||
self, instance_ids, plugin_name, key, value
|
||||
):
|
||||
with self._create_context.bulk_value_changes(CREATE_EVENT_SOURCE):
|
||||
for instance_id in instance_ids:
|
||||
if instance_id is None:
|
||||
instance = self._create_context
|
||||
else:
|
||||
instance = self._get_instance_by_id(instance_id)
|
||||
plugin_val = instance.publish_attributes[plugin_name]
|
||||
attr_def = plugin_val.get_attr_def(key)
|
||||
# Ignore if attribute is not available or enabled/visible
|
||||
# on the instance, or the value is not valid for definition
|
||||
if (
|
||||
attr_def is None
|
||||
or not attr_def.is_value_def
|
||||
or not attr_def.visible
|
||||
or not attr_def.enabled
|
||||
):
|
||||
continue
|
||||
|
||||
if value is _DEFAULT_VALUE:
|
||||
plugin_val[key] = attr_def.default
|
||||
|
||||
elif attr_def.is_value_valid(value):
|
||||
plugin_val[key] = value
|
||||
|
||||
def _cc_added_instance(self, event):
|
||||
instance_ids = {
|
||||
instance.id
|
||||
|
|
|
|||
|
|
@ -1,13 +1,58 @@
|
|||
import typing
|
||||
from typing import Dict, List, Any
|
||||
|
||||
from qtpy import QtWidgets, QtCore
|
||||
|
||||
from ayon_core.lib.attribute_definitions import UnknownDef
|
||||
from ayon_core.tools.attribute_defs import create_widget_for_attr_def
|
||||
from ayon_core.lib.attribute_definitions import AbstractAttrDef, UnknownDef
|
||||
from ayon_core.tools.attribute_defs import (
|
||||
create_widget_for_attr_def,
|
||||
AttributeDefinitionsLabel,
|
||||
)
|
||||
from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend
|
||||
from ayon_core.tools.publisher.constants import (
|
||||
INPUTS_LAYOUT_HSPACING,
|
||||
INPUTS_LAYOUT_VSPACING,
|
||||
)
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from typing import Union
|
||||
|
||||
|
||||
class _CreateAttrDefInfo:
|
||||
"""Helper class to store information about create attribute definition."""
|
||||
def __init__(
|
||||
self,
|
||||
attr_def: AbstractAttrDef,
|
||||
instance_ids: List["Union[str, None]"],
|
||||
defaults: List[Any],
|
||||
label_widget: "Union[AttributeDefinitionsLabel, None]",
|
||||
):
|
||||
self.attr_def: AbstractAttrDef = attr_def
|
||||
self.instance_ids: List["Union[str, None]"] = instance_ids
|
||||
self.defaults: List[Any] = defaults
|
||||
self.label_widget: "Union[AttributeDefinitionsLabel, None]" = (
|
||||
label_widget
|
||||
)
|
||||
|
||||
|
||||
class _PublishAttrDefInfo:
|
||||
"""Helper class to store information about publish attribute definition."""
|
||||
def __init__(
|
||||
self,
|
||||
attr_def: AbstractAttrDef,
|
||||
plugin_name: str,
|
||||
instance_ids: List["Union[str, None]"],
|
||||
defaults: List[Any],
|
||||
label_widget: "Union[AttributeDefinitionsLabel, None]",
|
||||
):
|
||||
self.attr_def: AbstractAttrDef = attr_def
|
||||
self.plugin_name: str = plugin_name
|
||||
self.instance_ids: List["Union[str, None]"] = instance_ids
|
||||
self.defaults: List[Any] = defaults
|
||||
self.label_widget: "Union[AttributeDefinitionsLabel, None]" = (
|
||||
label_widget
|
||||
)
|
||||
|
||||
|
||||
class CreatorAttrsWidget(QtWidgets.QWidget):
|
||||
"""Widget showing creator specific attributes for selected instances.
|
||||
|
|
@ -51,8 +96,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
|
|||
self._controller: AbstractPublisherFrontend = controller
|
||||
self._scroll_area = scroll_area
|
||||
|
||||
self._attr_def_id_to_instances = {}
|
||||
self._attr_def_id_to_attr_def = {}
|
||||
self._attr_def_info_by_id: Dict[str, _CreateAttrDefInfo] = {}
|
||||
self._current_instance_ids = set()
|
||||
|
||||
# To store content of scroll area to prevent garbage collection
|
||||
|
|
@ -81,8 +125,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
|
|||
prev_content_widget.deleteLater()
|
||||
|
||||
self._content_widget = None
|
||||
self._attr_def_id_to_instances = {}
|
||||
self._attr_def_id_to_attr_def = {}
|
||||
self._attr_def_info_by_id = {}
|
||||
|
||||
result = self._controller.get_creator_attribute_definitions(
|
||||
self._current_instance_ids
|
||||
|
|
@ -97,9 +140,21 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
|
|||
content_layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING)
|
||||
|
||||
row = 0
|
||||
for attr_def, instance_ids, values in result:
|
||||
widget = create_widget_for_attr_def(attr_def, content_widget)
|
||||
for attr_def, info_by_id in result:
|
||||
widget = create_widget_for_attr_def(
|
||||
attr_def, content_widget, handle_revert_to_default=False
|
||||
)
|
||||
default_values = []
|
||||
if attr_def.is_value_def:
|
||||
values = []
|
||||
for item in info_by_id.values():
|
||||
values.append(item["value"])
|
||||
# 'set' cannot be used for default values because they can
|
||||
# be unhashable types, e.g. 'list'.
|
||||
default = item["default"]
|
||||
if default not in default_values:
|
||||
default_values.append(default)
|
||||
|
||||
if len(values) == 1:
|
||||
value = values[0]
|
||||
if value is not None:
|
||||
|
|
@ -108,8 +163,13 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
|
|||
widget.set_value(values, True)
|
||||
|
||||
widget.value_changed.connect(self._input_value_changed)
|
||||
self._attr_def_id_to_instances[attr_def.id] = instance_ids
|
||||
self._attr_def_id_to_attr_def[attr_def.id] = attr_def
|
||||
widget.revert_to_default_requested.connect(
|
||||
self._on_request_revert_to_default
|
||||
)
|
||||
attr_def_info = _CreateAttrDefInfo(
|
||||
attr_def, list(info_by_id), default_values, None
|
||||
)
|
||||
self._attr_def_info_by_id[attr_def.id] = attr_def_info
|
||||
|
||||
if not attr_def.visible:
|
||||
continue
|
||||
|
|
@ -121,10 +181,18 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
|
|||
col_num = 2 - expand_cols
|
||||
|
||||
label = None
|
||||
is_overriden = False
|
||||
if attr_def.is_value_def:
|
||||
is_overriden = any(
|
||||
item["value"] != item["default"]
|
||||
for item in info_by_id.values()
|
||||
)
|
||||
label = attr_def.label or attr_def.key
|
||||
|
||||
if label:
|
||||
label_widget = QtWidgets.QLabel(label, self)
|
||||
label_widget = AttributeDefinitionsLabel(
|
||||
attr_def.id, label, self
|
||||
)
|
||||
tooltip = attr_def.tooltip
|
||||
if tooltip:
|
||||
label_widget.setToolTip(tooltip)
|
||||
|
|
@ -138,6 +206,11 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
|
|||
)
|
||||
if not attr_def.is_label_horizontal:
|
||||
row += 1
|
||||
attr_def_info.label_widget = label_widget
|
||||
label_widget.set_overridden(is_overriden)
|
||||
label_widget.revert_to_default_requested.connect(
|
||||
self._on_request_revert_to_default
|
||||
)
|
||||
|
||||
content_layout.addWidget(
|
||||
widget, row, col_num, 1, expand_cols
|
||||
|
|
@ -159,20 +232,37 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
|
|||
for instance_id, changes in event["instance_changes"].items():
|
||||
if (
|
||||
instance_id in self._current_instance_ids
|
||||
and "creator_attributes" not in changes
|
||||
and "creator_attributes" in changes
|
||||
):
|
||||
self._refresh_content()
|
||||
break
|
||||
|
||||
def _input_value_changed(self, value, attr_id):
|
||||
instance_ids = self._attr_def_id_to_instances.get(attr_id)
|
||||
attr_def = self._attr_def_id_to_attr_def.get(attr_id)
|
||||
if not instance_ids or not attr_def:
|
||||
attr_def_info = self._attr_def_info_by_id.get(attr_id)
|
||||
if attr_def_info is None:
|
||||
return
|
||||
|
||||
if attr_def_info.label_widget is not None:
|
||||
defaults = attr_def_info.defaults
|
||||
is_overriden = len(defaults) != 1 or value not in defaults
|
||||
attr_def_info.label_widget.set_overridden(is_overriden)
|
||||
|
||||
self._controller.set_instances_create_attr_values(
|
||||
instance_ids, attr_def.key, value
|
||||
attr_def_info.instance_ids,
|
||||
attr_def_info.attr_def.key,
|
||||
value
|
||||
)
|
||||
|
||||
def _on_request_revert_to_default(self, attr_id):
|
||||
attr_def_info = self._attr_def_info_by_id.get(attr_id)
|
||||
if attr_def_info is None:
|
||||
return
|
||||
self._controller.revert_instances_create_attr_values(
|
||||
attr_def_info.instance_ids,
|
||||
attr_def_info.attr_def.key,
|
||||
)
|
||||
self._refresh_content()
|
||||
|
||||
|
||||
class PublishPluginAttrsWidget(QtWidgets.QWidget):
|
||||
"""Widget showing publish plugin attributes for selected instances.
|
||||
|
|
@ -223,9 +313,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget):
|
|||
self._controller: AbstractPublisherFrontend = controller
|
||||
self._scroll_area = scroll_area
|
||||
|
||||
self._attr_def_id_to_instances = {}
|
||||
self._attr_def_id_to_attr_def = {}
|
||||
self._attr_def_id_to_plugin_name = {}
|
||||
self._attr_def_info_by_id: Dict[str, _PublishAttrDefInfo] = {}
|
||||
|
||||
# Store content of scroll area to prevent garbage collection
|
||||
self._content_widget = None
|
||||
|
|
@ -254,9 +342,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget):
|
|||
|
||||
self._content_widget = None
|
||||
|
||||
self._attr_def_id_to_instances = {}
|
||||
self._attr_def_id_to_attr_def = {}
|
||||
self._attr_def_id_to_plugin_name = {}
|
||||
self._attr_def_info_by_id = {}
|
||||
|
||||
result = self._controller.get_publish_attribute_definitions(
|
||||
self._current_instance_ids, self._context_selected
|
||||
|
|
@ -275,12 +361,10 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget):
|
|||
content_layout.addStretch(1)
|
||||
|
||||
row = 0
|
||||
for plugin_name, attr_defs, all_plugin_values in result:
|
||||
plugin_values = all_plugin_values[plugin_name]
|
||||
|
||||
for plugin_name, attr_defs, plugin_values in result:
|
||||
for attr_def in attr_defs:
|
||||
widget = create_widget_for_attr_def(
|
||||
attr_def, content_widget
|
||||
attr_def, content_widget, handle_revert_to_default=False
|
||||
)
|
||||
visible_widget = attr_def.visible
|
||||
# Hide unknown values of publish plugins
|
||||
|
|
@ -290,6 +374,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget):
|
|||
widget.setVisible(False)
|
||||
visible_widget = False
|
||||
|
||||
label_widget = None
|
||||
if visible_widget:
|
||||
expand_cols = 2
|
||||
if attr_def.is_value_def and attr_def.is_label_horizontal:
|
||||
|
|
@ -300,7 +385,12 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget):
|
|||
if attr_def.is_value_def:
|
||||
label = attr_def.label or attr_def.key
|
||||
if label:
|
||||
label_widget = QtWidgets.QLabel(label, content_widget)
|
||||
label_widget = AttributeDefinitionsLabel(
|
||||
attr_def.id, label, content_widget
|
||||
)
|
||||
label_widget.revert_to_default_requested.connect(
|
||||
self._on_request_revert_to_default
|
||||
)
|
||||
tooltip = attr_def.tooltip
|
||||
if tooltip:
|
||||
label_widget.setToolTip(tooltip)
|
||||
|
|
@ -323,38 +413,76 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget):
|
|||
continue
|
||||
|
||||
widget.value_changed.connect(self._input_value_changed)
|
||||
widget.revert_to_default_requested.connect(
|
||||
self._on_request_revert_to_default
|
||||
)
|
||||
|
||||
attr_values = plugin_values[attr_def.key]
|
||||
multivalue = len(attr_values) > 1
|
||||
instance_ids = []
|
||||
values = []
|
||||
instances = []
|
||||
for instance, value in attr_values:
|
||||
default_values = []
|
||||
is_overriden = False
|
||||
for (instance_id, value, default_value) in (
|
||||
plugin_values.get(attr_def.key, [])
|
||||
):
|
||||
instance_ids.append(instance_id)
|
||||
values.append(value)
|
||||
instances.append(instance)
|
||||
if not is_overriden and value != default_value:
|
||||
is_overriden = True
|
||||
# 'set' cannot be used for default values because they can
|
||||
# be unhashable types, e.g. 'list'.
|
||||
if default_value not in default_values:
|
||||
default_values.append(default_value)
|
||||
|
||||
self._attr_def_id_to_attr_def[attr_def.id] = attr_def
|
||||
self._attr_def_id_to_instances[attr_def.id] = instances
|
||||
self._attr_def_id_to_plugin_name[attr_def.id] = plugin_name
|
||||
multivalue = len(values) > 1
|
||||
|
||||
self._attr_def_info_by_id[attr_def.id] = _PublishAttrDefInfo(
|
||||
attr_def,
|
||||
plugin_name,
|
||||
instance_ids,
|
||||
default_values,
|
||||
label_widget,
|
||||
)
|
||||
|
||||
if multivalue:
|
||||
widget.set_value(values, multivalue)
|
||||
else:
|
||||
widget.set_value(values[0])
|
||||
|
||||
if label_widget is not None:
|
||||
label_widget.set_overridden(is_overriden)
|
||||
|
||||
self._scroll_area.setWidget(content_widget)
|
||||
self._content_widget = content_widget
|
||||
|
||||
def _input_value_changed(self, value, attr_id):
|
||||
instance_ids = self._attr_def_id_to_instances.get(attr_id)
|
||||
attr_def = self._attr_def_id_to_attr_def.get(attr_id)
|
||||
plugin_name = self._attr_def_id_to_plugin_name.get(attr_id)
|
||||
if not instance_ids or not attr_def or not plugin_name:
|
||||
attr_def_info = self._attr_def_info_by_id.get(attr_id)
|
||||
if attr_def_info is None:
|
||||
return
|
||||
|
||||
if attr_def_info.label_widget is not None:
|
||||
defaults = attr_def_info.defaults
|
||||
is_overriden = len(defaults) != 1 or value not in defaults
|
||||
attr_def_info.label_widget.set_overridden(is_overriden)
|
||||
|
||||
self._controller.set_instances_publish_attr_values(
|
||||
instance_ids, plugin_name, attr_def.key, value
|
||||
attr_def_info.instance_ids,
|
||||
attr_def_info.plugin_name,
|
||||
attr_def_info.attr_def.key,
|
||||
value
|
||||
)
|
||||
|
||||
def _on_request_revert_to_default(self, attr_id):
|
||||
attr_def_info = self._attr_def_info_by_id.get(attr_id)
|
||||
if attr_def_info is None:
|
||||
return
|
||||
|
||||
self._controller.revert_instances_publish_attr_values(
|
||||
attr_def_info.instance_ids,
|
||||
attr_def_info.plugin_name,
|
||||
attr_def_info.attr_def.key,
|
||||
)
|
||||
self._refresh_content()
|
||||
|
||||
def _on_instance_attr_defs_change(self, event):
|
||||
for instance_id in event.data:
|
||||
if (
|
||||
|
|
@ -370,7 +498,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget):
|
|||
for instance_id, changes in event["instance_changes"].items():
|
||||
if (
|
||||
instance_id in self._current_instance_ids
|
||||
and "publish_attributes" not in changes
|
||||
and "publish_attributes" in changes
|
||||
):
|
||||
self._refresh_content()
|
||||
break
|
||||
|
|
|
|||
|
|
@ -194,14 +194,14 @@ class InventoryModel(QtGui.QStandardItemModel):
|
|||
group_items = []
|
||||
for repre_id, container_items in items_by_repre_id.items():
|
||||
repre_info = repre_info_by_id[repre_id]
|
||||
version_label = "N/A"
|
||||
version_color = None
|
||||
is_latest = False
|
||||
is_hero = False
|
||||
status_name = None
|
||||
if not repre_info.is_valid:
|
||||
version_label = "N/A"
|
||||
group_name = "< Entity N/A >"
|
||||
item_icon = invalid_item_icon
|
||||
is_latest = False
|
||||
is_hero = False
|
||||
status_name = None
|
||||
|
||||
else:
|
||||
group_name = "{}_{}: ({})".format(
|
||||
|
|
@ -217,6 +217,7 @@ class InventoryModel(QtGui.QStandardItemModel):
|
|||
version_item = version_items[repre_info.version_id]
|
||||
version_label = format_version(version_item.version)
|
||||
is_hero = version_item.version < 0
|
||||
is_latest = version_item.is_latest
|
||||
if not version_item.is_latest:
|
||||
version_color = self.OUTDATED_COLOR
|
||||
status_name = version_item.status
|
||||
|
|
@ -425,7 +426,7 @@ class FilterProxyModel(QtCore.QSortFilterProxyModel):
|
|||
state = bool(state)
|
||||
|
||||
if state != self._filter_outdated:
|
||||
self._filter_outdated = bool(state)
|
||||
self._filter_outdated = state
|
||||
self.invalidateFilter()
|
||||
|
||||
def set_hierarchy_view(self, state):
|
||||
|
|
|
|||
|
|
@ -383,7 +383,6 @@ class ContainersModel:
|
|||
container_items_by_id[item.item_id] = item
|
||||
container_items.append(item)
|
||||
|
||||
|
||||
self._containers_by_id = containers_by_id
|
||||
self._container_items_by_id = container_items_by_id
|
||||
self._items_cache = container_items
|
||||
|
|
|
|||
|
|
@ -3,12 +3,9 @@ import sys
|
|||
import json
|
||||
import hashlib
|
||||
import platform
|
||||
import subprocess
|
||||
import csv
|
||||
import time
|
||||
import signal
|
||||
import locale
|
||||
from typing import Optional, Dict, Tuple, Any
|
||||
from typing import Optional, List, Dict, Tuple, Any
|
||||
|
||||
import requests
|
||||
from ayon_api.utils import get_default_settings_variant
|
||||
|
|
@ -53,15 +50,101 @@ def _get_server_and_variant(
|
|||
return server_url, variant
|
||||
|
||||
|
||||
def _windows_get_pid_args(pid: int) -> Optional[List[str]]:
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
|
||||
# Define constants
|
||||
PROCESS_COMMANDLINE_INFO = 60
|
||||
STATUS_NOT_FOUND = 0xC0000225
|
||||
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
|
||||
|
||||
# Define the UNICODE_STRING structure
|
||||
class UNICODE_STRING(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("Length", wintypes.USHORT),
|
||||
("MaximumLength", wintypes.USHORT),
|
||||
("Buffer", wintypes.LPWSTR)
|
||||
]
|
||||
|
||||
shell32 = ctypes.WinDLL("shell32", use_last_error=True)
|
||||
|
||||
CommandLineToArgvW = shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [
|
||||
wintypes.LPCWSTR, ctypes.POINTER(ctypes.c_int)
|
||||
]
|
||||
CommandLineToArgvW.restype = ctypes.POINTER(wintypes.LPWSTR)
|
||||
|
||||
output = None
|
||||
# Open the process
|
||||
handle = ctypes.windll.kernel32.OpenProcess(
|
||||
PROCESS_QUERY_LIMITED_INFORMATION, False, pid
|
||||
)
|
||||
if not handle:
|
||||
return output
|
||||
|
||||
try:
|
||||
buffer_len = wintypes.ULONG()
|
||||
# Get the right buffer size first
|
||||
status = ctypes.windll.ntdll.NtQueryInformationProcess(
|
||||
handle,
|
||||
PROCESS_COMMANDLINE_INFO,
|
||||
ctypes.c_void_p(None),
|
||||
0,
|
||||
ctypes.byref(buffer_len)
|
||||
)
|
||||
|
||||
if status == STATUS_NOT_FOUND:
|
||||
return output
|
||||
|
||||
# Create buffer with collected size
|
||||
buffer = ctypes.create_string_buffer(buffer_len.value)
|
||||
|
||||
# Get the command line
|
||||
status = ctypes.windll.ntdll.NtQueryInformationProcess(
|
||||
handle,
|
||||
PROCESS_COMMANDLINE_INFO,
|
||||
buffer,
|
||||
buffer_len,
|
||||
ctypes.byref(buffer_len)
|
||||
)
|
||||
if status:
|
||||
return output
|
||||
# Build the string
|
||||
tmp = ctypes.cast(buffer, ctypes.POINTER(UNICODE_STRING)).contents
|
||||
size = tmp.Length // 2 + 1
|
||||
cmdline_buffer = ctypes.create_unicode_buffer(size)
|
||||
ctypes.cdll.msvcrt.wcscpy(cmdline_buffer, tmp.Buffer)
|
||||
|
||||
args_len = ctypes.c_int()
|
||||
args = CommandLineToArgvW(
|
||||
cmdline_buffer, ctypes.byref(args_len)
|
||||
)
|
||||
output = [args[idx] for idx in range(args_len.value)]
|
||||
ctypes.windll.kernel32.LocalFree(args)
|
||||
|
||||
finally:
|
||||
ctypes.windll.kernel32.CloseHandle(handle)
|
||||
return output
|
||||
|
||||
|
||||
def _windows_pid_is_running(pid: int) -> bool:
|
||||
args = ["tasklist.exe", "/fo", "csv", "/fi", f"PID eq {pid}"]
|
||||
output = subprocess.check_output(args)
|
||||
encoding = locale.getpreferredencoding()
|
||||
csv_content = csv.DictReader(output.decode(encoding).splitlines())
|
||||
# if "PID" not in csv_content.fieldnames:
|
||||
# return False
|
||||
for _ in csv_content:
|
||||
args = _windows_get_pid_args(pid)
|
||||
if not args:
|
||||
return False
|
||||
executable_path = args[0]
|
||||
|
||||
filename = os.path.basename(executable_path).lower()
|
||||
if "ayon" in filename:
|
||||
return True
|
||||
|
||||
# Try to handle tray running from code
|
||||
# - this might be potential danger that kills other python process running
|
||||
# 'start.py' script (low chance, but still)
|
||||
if "python" in filename and len(args) > 1:
|
||||
script_filename = os.path.basename(args[1].lower())
|
||||
if script_filename == "start.py":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import os
|
||||
|
||||
from qtpy import QtWidgets
|
||||
from qtpy import QtWidgets, QT6
|
||||
|
||||
|
||||
class Action(QtWidgets.QAction):
|
||||
|
|
@ -112,20 +112,21 @@ module.{module_name}()"""
|
|||
Run the command of the instance or copy the command to the active shelf
|
||||
based on the current modifiers.
|
||||
|
||||
If callbacks have been registered with fouind modifier integer the
|
||||
If callbacks have been registered with found modifier integer the
|
||||
function will trigger all callbacks. When a callback function returns a
|
||||
non zero integer it will not execute the action's command
|
||||
|
||||
"""
|
||||
|
||||
# get the current application and its linked keyboard modifiers
|
||||
app = QtWidgets.QApplication.instance()
|
||||
modifiers = app.keyboardModifiers()
|
||||
if not QT6:
|
||||
modifiers = int(modifiers)
|
||||
|
||||
# If the menu has a callback registered for the current modifier
|
||||
# we run the callback instead of the action itself.
|
||||
registered = self._root.registered_callbacks
|
||||
callbacks = registered.get(int(modifiers), [])
|
||||
callbacks = registered.get(modifiers, [])
|
||||
for callback in callbacks:
|
||||
signal = callback(self)
|
||||
if signal != 0:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import maya.cmds as cmds
|
|||
import maya.mel as mel
|
||||
|
||||
import scriptsmenu
|
||||
from qtpy import QtCore, QtWidgets
|
||||
from qtpy import QtCore, QtWidgets, QT6
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -130,7 +130,7 @@ def main(title="Scripts", parent=None, objectName=None):
|
|||
|
||||
# Register control + shift callback to add to shelf (maya behavior)
|
||||
modifiers = QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier
|
||||
if int(cmds.about(version=True)) < 2025:
|
||||
if not QT6:
|
||||
modifiers = int(modifiers)
|
||||
|
||||
menu.register_callback(modifiers, to_shelf)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring AYON addon 'core' version."""
|
||||
__version__ = "1.0.4+dev"
|
||||
__version__ = "1.0.8+dev"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
name = "core"
|
||||
title = "Core"
|
||||
version = "1.0.4+dev"
|
||||
version = "1.0.8+dev"
|
||||
|
||||
client_dir = "ayon_core"
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
[tool.poetry]
|
||||
name = "ayon-core"
|
||||
version = "1.0.4+dev"
|
||||
version = "1.0.8+dev"
|
||||
description = ""
|
||||
authors = ["Ynput Team <team@ynput.io>"]
|
||||
readme = "README.md"
|
||||
|
|
|
|||
|
|
@ -459,8 +459,8 @@ class ExtractReviewFilterModel(BaseSettingsModel):
|
|||
single_frame_filter: str = SettingsField(
|
||||
"everytime", # codespell:ignore everytime
|
||||
description=(
|
||||
"Use output <b>always</b> / only if input <b>is 1 frame</b>"
|
||||
" image / only if has <b>2+ frames</b> or <b>is video</b>"
|
||||
"Use output **always** / only if input **is 1 frame**"
|
||||
" image / only if has **2+ frames** or **is video**"
|
||||
),
|
||||
enum_resolver=extract_review_filter_enum
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue