Merge remote-tracking branch 'origin/develop' into feature/911-new-traits-based-integrator

This commit is contained in:
Ondřej Samohel 2024-11-20 18:11:23 +01:00
commit ec29e31614
No known key found for this signature in database
GPG key ID: 02376E18990A97C6
30 changed files with 909 additions and 337 deletions

View file

@ -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
@ -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,12 +514,12 @@ 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()
regex = None
if self.regex is not None:
@ -545,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((
@ -567,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 = []
@ -577,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:
@ -609,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'
@ -625,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():
@ -682,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)
@ -694,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
@ -702,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
@ -731,7 +741,7 @@ class FileDefItem:
)
@property
def label(self):
def label(self) -> Optional[str]:
if self.is_empty:
return None
@ -774,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")
@ -785,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])
@ -794,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
@ -810,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
@ -830,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]
@ -872,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"],
@ -881,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)
@ -910,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,
@ -948,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
@ -966,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((
@ -985,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
@ -1026,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]
@ -1044,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
@ -1062,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

View file

@ -429,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,
@ -515,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())
@ -567,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}
)

View file

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

View file

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

View file

@ -13,8 +13,7 @@ class CollectHierarchy(pyblish.api.ContextPlugin):
label = "Collect Hierarchy"
order = pyblish.api.CollectorOrder - 0.076
families = ["shot"]
hosts = ["resolve", "hiero", "flame"]
hosts = ["resolve", "hiero", "flame", "traypublisher"]
def process(self, context):
project_name = context.data["projectName"]
@ -32,36 +31,49 @@ class CollectHierarchy(pyblish.api.ContextPlugin):
product_type = instance.data["productType"]
families = instance.data["families"]
# exclude other families then self.families with intersection
if not set(self.families).intersection(
set(families + [product_type])
):
# exclude other families then "shot" with intersection
if "shot" not in (families + [product_type]):
self.log.debug("Skipping not a shot: {}".format(families))
continue
# exclude if not masterLayer True
# Skip if is not a hero track
if not instance.data.get("heroTrack"):
self.log.debug("Skipping not a shot from hero track")
continue
shot_data = {
"entity_type": "folder",
# WARNING Default folder type is hardcoded
# suppose that all instances are Shots
"folder_type": "Shot",
# WARNING unless overwritten, default folder type is hardcoded to shot
"folder_type": instance.data.get("folder_type") or "Shot",
"tasks": instance.data.get("tasks") or {},
"comments": instance.data.get("comments", []),
"attributes": {
"handleStart": instance.data["handleStart"],
"handleEnd": instance.data["handleEnd"],
"frameStart": instance.data["frameStart"],
"frameEnd": instance.data["frameEnd"],
"clipIn": instance.data["clipIn"],
"clipOut": instance.data["clipOut"],
"fps": instance.data["fps"],
"resolutionWidth": instance.data["resolutionWidth"],
"resolutionHeight": instance.data["resolutionHeight"],
"pixelAspect": instance.data["pixelAspect"],
},
}
shot_data["attributes"] = {}
SHOT_ATTRS = (
"handleStart",
"handleEnd",
"frameStart",
"frameEnd",
"clipIn",
"clipOut",
"fps",
"resolutionWidth",
"resolutionHeight",
"pixelAspect",
)
for shot_attr in SHOT_ATTRS:
attr_value = instance.data.get(shot_attr)
if attr_value is None:
# Shot attribute might not be defined (e.g. CSV ingest)
self.log.debug(
"%s shot attribute is not defined for instance.",
shot_attr
)
continue
shot_data["attributes"][shot_attr] = attr_value
# Split by '/' for AYON where asset is a path
name = instance.data["folderPath"].split("/")[-1]
actual = {name: shot_data}

View file

@ -29,6 +29,10 @@ class CollectOtioFrameRanges(pyblish.api.InstancePlugin):
otio_range_with_handles
)
if not instance.data.get("otioClip"):
self.log.debug("Skipping collect OTIO frame range.")
return
# get basic variables
otio_clip = instance.data["otioClip"]
workfile_start = instance.data["workfileFrameStart"]

View file

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

View file

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

View file

@ -5,7 +5,6 @@ import pyblish.api
from ayon_core.pipeline import publish
from ayon_core.lib import (
is_oiio_supported,
)
@ -154,12 +153,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,

View file

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

View file

@ -22,7 +22,6 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin):
order = pyblish.api.ExtractorOrder - 0.01
label = "Extract Hierarchy To AYON"
families = ["clip", "shot"]
def process(self, context):
if not context.data.get("hierarchyContext"):

View file

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

View file

@ -44,10 +44,6 @@ QLabel {
background: transparent;
}
QLabel[overriden="1"] {
color: {color:font-overridden};
}
/* Inputs */
QAbstractSpinBox, QLineEdit, QPlainTextEdit, QTextEdit {
border: 1px solid {color:border};
@ -1589,6 +1585,10 @@ CreateNextPageOverlay {
}
/* Attribute Definition widgets */
AttributeDefinitionsLabel[overridden="1"] {
color: {color:font-overridden};
}
AttributeDefinitionsWidget QAbstractSpinBox, QLineEdit, QPlainTextEdit, QTextEdit {
padding: 1px;
}

View file

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

View file

@ -0,0 +1 @@
REVERT_TO_DEFAULT_LABEL = "Revert to default"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,20 +753,12 @@ 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]
@ -816,28 +809,18 @@ class CreateModel:
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,
@ -1064,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

View file

@ -4,8 +4,10 @@ from typing import Dict, List, Any
from qtpy import QtWidgets, QtCore
from ayon_core.lib.attribute_definitions import AbstractAttrDef, UnknownDef
from ayon_core.tools.utils import set_style_property
from ayon_core.tools.attribute_defs import create_widget_for_attr_def
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,
@ -16,14 +18,6 @@ if typing.TYPE_CHECKING:
from typing import Union
def _set_label_overriden(label: QtWidgets.QLabel, overriden: bool):
set_style_property(
label,
"overriden",
"1" if overriden else ""
)
class _CreateAttrDefInfo:
"""Helper class to store information about create attribute definition."""
def __init__(
@ -31,12 +25,14 @@ class _CreateAttrDefInfo:
attr_def: AbstractAttrDef,
instance_ids: List["Union[str, None]"],
defaults: List[Any],
label_widget: "Union[None, QtWidgets.QLabel]",
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[None, QtWidgets.QLabel]" = label_widget
self.label_widget: "Union[AttributeDefinitionsLabel, None]" = (
label_widget
)
class _PublishAttrDefInfo:
@ -47,13 +43,15 @@ class _PublishAttrDefInfo:
plugin_name: str,
instance_ids: List["Union[str, None]"],
defaults: List[Any],
label_widget: "Union[None, QtWidgets.QLabel]",
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[None, QtWidgets.QLabel]" = label_widget
self.label_widget: "Union[AttributeDefinitionsLabel, None]" = (
label_widget
)
class CreatorAttrsWidget(QtWidgets.QWidget):
@ -143,7 +141,9 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
row = 0
for attr_def, info_by_id in result:
widget = create_widget_for_attr_def(attr_def, content_widget)
widget = create_widget_for_attr_def(
attr_def, content_widget, handle_revert_to_default=False
)
default_values = []
if attr_def.is_value_def:
values = []
@ -163,6 +163,9 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
widget.set_value(values, True)
widget.value_changed.connect(self._input_value_changed)
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
)
@ -187,7 +190,9 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
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)
@ -202,7 +207,10 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
if not attr_def.is_label_horizontal:
row += 1
attr_def_info.label_widget = label_widget
_set_label_overriden(label_widget, is_overriden)
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
@ -224,7 +232,7 @@ 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
@ -237,7 +245,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
if attr_def_info.label_widget is not None:
defaults = attr_def_info.defaults
is_overriden = len(defaults) != 1 or value not in defaults
_set_label_overriden(attr_def_info.label_widget, is_overriden)
attr_def_info.label_widget.set_overridden(is_overriden)
self._controller.set_instances_create_attr_values(
attr_def_info.instance_ids,
@ -245,6 +253,16 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
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.
@ -346,7 +364,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget):
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
@ -367,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)
@ -390,6 +413,9 @@ 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
)
instance_ids = []
values = []
@ -423,7 +449,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget):
widget.set_value(values[0])
if label_widget is not None:
_set_label_overriden(label_widget, is_overriden)
label_widget.set_overridden(is_overriden)
self._scroll_area.setWidget(content_widget)
self._content_widget = content_widget
@ -436,7 +462,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget):
if attr_def_info.label_widget is not None:
defaults = attr_def_info.defaults
is_overriden = len(defaults) != 1 or value not in defaults
_set_label_overriden(attr_def_info.label_widget, is_overriden)
attr_def_info.label_widget.set_overridden(is_overriden)
self._controller.set_instances_publish_attr_values(
attr_def_info.instance_ids,
@ -445,6 +471,18 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget):
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 (
@ -460,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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring AYON addon 'core' version."""
__version__ = "1.0.6+dev"
__version__ = "1.0.8+dev"

View file

@ -1,6 +1,6 @@
name = "core"
title = "Core"
version = "1.0.6+dev"
version = "1.0.8+dev"
client_dir = "ayon_core"

View file

@ -5,7 +5,7 @@
[tool.poetry]
name = "ayon-core"
version = "1.0.6+dev"
version = "1.0.8+dev"
description = ""
authors = ["Ynput Team <team@ynput.io>"]
readme = "README.md"