mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-01 08:24:53 +01:00
Merge branch 'enhancement/usd_contribution_attributes_per_instance_toggle' of https://github.com/BigRoy/ayon-core into enhancement/usd_contribution_attributes_per_instance_toggle
This commit is contained in:
commit
5f7d6bd313
31 changed files with 1339 additions and 283 deletions
|
|
@ -28,7 +28,8 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook):
|
||||||
"substancepainter",
|
"substancepainter",
|
||||||
"aftereffects",
|
"aftereffects",
|
||||||
"wrap",
|
"wrap",
|
||||||
"openrv"
|
"openrv",
|
||||||
|
"cinema4d"
|
||||||
}
|
}
|
||||||
launch_types = {LaunchTypes.local}
|
launch_types = {LaunchTypes.local}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@ import collections
|
||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
import copy
|
import copy
|
||||||
|
import warnings
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
import clique
|
import clique
|
||||||
|
|
||||||
|
|
@ -90,6 +92,30 @@ class AbstractAttrDefMeta(ABCMeta):
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_reversed_attr(
|
||||||
|
main_value, depr_value, main_label, depr_label, default
|
||||||
|
):
|
||||||
|
if main_value is not None and depr_value is not None:
|
||||||
|
if main_value == depr_value:
|
||||||
|
print(
|
||||||
|
f"Got invalid '{main_label}' and '{depr_label}' arguments."
|
||||||
|
f" Using '{main_label}' value."
|
||||||
|
)
|
||||||
|
elif depr_value is not None:
|
||||||
|
warnings.warn(
|
||||||
|
(
|
||||||
|
"DEPRECATION WARNING: Using deprecated argument"
|
||||||
|
f" '{depr_label}' please use '{main_label}' instead."
|
||||||
|
),
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=4,
|
||||||
|
)
|
||||||
|
main_value = not depr_value
|
||||||
|
elif main_value is None:
|
||||||
|
main_value = default
|
||||||
|
return main_value
|
||||||
|
|
||||||
|
|
||||||
class AbstractAttrDef(metaclass=AbstractAttrDefMeta):
|
class AbstractAttrDef(metaclass=AbstractAttrDefMeta):
|
||||||
"""Abstraction of attribute definition.
|
"""Abstraction of attribute definition.
|
||||||
|
|
||||||
|
|
@ -106,12 +132,14 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta):
|
||||||
Args:
|
Args:
|
||||||
key (str): Under which key will be attribute value stored.
|
key (str): Under which key will be attribute value stored.
|
||||||
default (Any): Default value of an attribute.
|
default (Any): Default value of an attribute.
|
||||||
label (str): Attribute label.
|
label (Optional[str]): Attribute label.
|
||||||
tooltip (str): Attribute tooltip.
|
tooltip (Optional[str]): Attribute tooltip.
|
||||||
is_label_horizontal (bool): UI specific argument. Specify if label is
|
is_label_horizontal (Optional[bool]): UI specific argument. Specify
|
||||||
next to value input or ahead.
|
if label is next to value input or ahead.
|
||||||
hidden (bool): Will be item hidden (for UI purposes).
|
visible (Optional[bool]): Item is shown to user (for UI purposes).
|
||||||
disabled (bool): Item will be visible but disabled (for UI purposes).
|
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 = []
|
type_attributes = []
|
||||||
|
|
@ -120,51 +148,105 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
key,
|
key: str,
|
||||||
default,
|
default: Any,
|
||||||
label=None,
|
label: Optional[str] = None,
|
||||||
tooltip=None,
|
tooltip: Optional[str] = None,
|
||||||
is_label_horizontal=None,
|
is_label_horizontal: Optional[bool] = None,
|
||||||
hidden=False,
|
visible: Optional[bool] = None,
|
||||||
disabled=False
|
enabled: Optional[bool] = None,
|
||||||
|
hidden: Optional[bool] = None,
|
||||||
|
disabled: Optional[bool] = None,
|
||||||
):
|
):
|
||||||
if is_label_horizontal is None:
|
if is_label_horizontal is None:
|
||||||
is_label_horizontal = True
|
is_label_horizontal = True
|
||||||
|
|
||||||
if hidden is None:
|
enabled = _convert_reversed_attr(
|
||||||
hidden = False
|
enabled, disabled, "enabled", "disabled", True
|
||||||
|
)
|
||||||
|
visible = _convert_reversed_attr(
|
||||||
|
visible, hidden, "visible", "hidden", True
|
||||||
|
)
|
||||||
|
|
||||||
self.key = key
|
self.key: str = key
|
||||||
self.label = label
|
self.label: Optional[str] = label
|
||||||
self.tooltip = tooltip
|
self.tooltip: Optional[str] = tooltip
|
||||||
self.default = default
|
self.default: Any = default
|
||||||
self.is_label_horizontal = is_label_horizontal
|
self.is_label_horizontal: bool = is_label_horizontal
|
||||||
self.hidden = hidden
|
self.visible: bool = visible
|
||||||
self.disabled = disabled
|
self.enabled: bool = enabled
|
||||||
self._id = uuid.uuid4().hex
|
self._id: str = uuid.uuid4().hex
|
||||||
|
|
||||||
self.__init__class__ = AbstractAttrDef
|
self.__init__class__ = AbstractAttrDef
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def id(self):
|
def id(self) -> str:
|
||||||
return self._id
|
return self._id
|
||||||
|
|
||||||
def __eq__(self, other):
|
def clone(self):
|
||||||
if not isinstance(other, self.__class__):
|
data = self.serialize()
|
||||||
|
data.pop("type")
|
||||||
|
return self.deserialize(data)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hidden(self) -> bool:
|
||||||
|
return not self.visible
|
||||||
|
|
||||||
|
@hidden.setter
|
||||||
|
def hidden(self, value: bool):
|
||||||
|
self.visible = not value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def disabled(self) -> bool:
|
||||||
|
return not self.enabled
|
||||||
|
|
||||||
|
@disabled.setter
|
||||||
|
def disabled(self, value: bool):
|
||||||
|
self.enabled = not value
|
||||||
|
|
||||||
|
def __eq__(self, other: Any) -> bool:
|
||||||
|
return self.compare_to_def(other)
|
||||||
|
|
||||||
|
def __ne__(self, other: Any) -> bool:
|
||||||
|
return not self.compare_to_def(other)
|
||||||
|
|
||||||
|
def compare_to_def(
|
||||||
|
self,
|
||||||
|
other: Any,
|
||||||
|
ignore_default: Optional[bool] = False,
|
||||||
|
ignore_enabled: Optional[bool] = False,
|
||||||
|
ignore_visible: Optional[bool] = False,
|
||||||
|
ignore_def_type_compare: Optional[bool] = False,
|
||||||
|
) -> bool:
|
||||||
|
if not isinstance(other, self.__class__) or self.key != other.key:
|
||||||
|
return False
|
||||||
|
if not ignore_def_type_compare and not self._def_type_compare(other):
|
||||||
return False
|
return False
|
||||||
return (
|
return (
|
||||||
self.key == other.key
|
(ignore_default or self.default == other.default)
|
||||||
and self.hidden == other.hidden
|
and (ignore_visible or self.visible == other.visible)
|
||||||
and self.default == other.default
|
and (ignore_enabled or self.enabled == other.enabled)
|
||||||
and self.disabled == other.disabled
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __ne__(self, other):
|
@abstractmethod
|
||||||
return not self.__eq__(other)
|
def is_value_valid(self, value: Any) -> bool:
|
||||||
|
"""Check if value is valid.
|
||||||
|
|
||||||
|
This should return False if value is not valid based
|
||||||
|
on definition type.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value (Any): Value to validate based on definition type.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if value is valid.
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def type(self):
|
def type(self) -> str:
|
||||||
"""Attribute definition type also used as identifier of class.
|
"""Attribute definition type also used as identifier of class.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
@ -198,8 +280,8 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta):
|
||||||
"tooltip": self.tooltip,
|
"tooltip": self.tooltip,
|
||||||
"default": self.default,
|
"default": self.default,
|
||||||
"is_label_horizontal": self.is_label_horizontal,
|
"is_label_horizontal": self.is_label_horizontal,
|
||||||
"hidden": self.hidden,
|
"visible": self.visible,
|
||||||
"disabled": self.disabled
|
"enabled": self.enabled
|
||||||
}
|
}
|
||||||
for attr in self.type_attributes:
|
for attr in self.type_attributes:
|
||||||
data[attr] = getattr(self, attr)
|
data[attr] = getattr(self, attr)
|
||||||
|
|
@ -211,9 +293,15 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta):
|
||||||
|
|
||||||
Data can be received using 'serialize' method.
|
Data can be received using 'serialize' method.
|
||||||
"""
|
"""
|
||||||
|
if "type" in data:
|
||||||
|
data = dict(data)
|
||||||
|
data.pop("type")
|
||||||
|
|
||||||
return cls(**data)
|
return cls(**data)
|
||||||
|
|
||||||
|
def _def_type_compare(self, other: "AbstractAttrDef") -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------
|
# -----------------------------------------
|
||||||
# UI attribute definitions won't hold value
|
# UI attribute definitions won't hold value
|
||||||
|
|
@ -223,7 +311,10 @@ class UIDef(AbstractAttrDef):
|
||||||
is_value_def = False
|
is_value_def = False
|
||||||
|
|
||||||
def __init__(self, key=None, default=None, *args, **kwargs):
|
def __init__(self, key=None, default=None, *args, **kwargs):
|
||||||
super(UIDef, self).__init__(key, default, *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):
|
||||||
return value
|
return value
|
||||||
|
|
@ -237,11 +328,9 @@ class UILabelDef(UIDef):
|
||||||
type = "label"
|
type = "label"
|
||||||
|
|
||||||
def __init__(self, label, key=None):
|
def __init__(self, label, key=None):
|
||||||
super(UILabelDef, self).__init__(label=label, key=key)
|
super().__init__(label=label, key=key)
|
||||||
|
|
||||||
def __eq__(self, other):
|
def _def_type_compare(self, other: "UILabelDef") -> bool:
|
||||||
if not super(UILabelDef, self).__eq__(other):
|
|
||||||
return False
|
|
||||||
return self.label == other.label
|
return self.label == other.label
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -260,7 +349,10 @@ class UnknownDef(AbstractAttrDef):
|
||||||
|
|
||||||
def __init__(self, key, default=None, **kwargs):
|
def __init__(self, key, default=None, **kwargs):
|
||||||
kwargs["default"] = default
|
kwargs["default"] = default
|
||||||
super(UnknownDef, self).__init__(key, **kwargs)
|
super().__init__(key, **kwargs)
|
||||||
|
|
||||||
|
def is_value_valid(self, value: Any) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
def convert_value(self, value):
|
def convert_value(self, value):
|
||||||
return value
|
return value
|
||||||
|
|
@ -279,8 +371,11 @@ class HiddenDef(AbstractAttrDef):
|
||||||
|
|
||||||
def __init__(self, key, default=None, **kwargs):
|
def __init__(self, key, default=None, **kwargs):
|
||||||
kwargs["default"] = default
|
kwargs["default"] = default
|
||||||
kwargs["hidden"] = True
|
kwargs["visible"] = False
|
||||||
super(HiddenDef, self).__init__(key, **kwargs)
|
super().__init__(key, **kwargs)
|
||||||
|
|
||||||
|
def is_value_valid(self, value: Any) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
def convert_value(self, value):
|
def convert_value(self, value):
|
||||||
return value
|
return value
|
||||||
|
|
@ -331,21 +426,21 @@ class NumberDef(AbstractAttrDef):
|
||||||
elif default > maximum:
|
elif default > maximum:
|
||||||
default = maximum
|
default = maximum
|
||||||
|
|
||||||
super(NumberDef, self).__init__(key, default=default, **kwargs)
|
super().__init__(key, default=default, **kwargs)
|
||||||
|
|
||||||
self.minimum = minimum
|
self.minimum = minimum
|
||||||
self.maximum = maximum
|
self.maximum = maximum
|
||||||
self.decimals = 0 if decimals is None else decimals
|
self.decimals = 0 if decimals is None else decimals
|
||||||
|
|
||||||
def __eq__(self, other):
|
def is_value_valid(self, value: Any) -> bool:
|
||||||
if not super(NumberDef, self).__eq__(other):
|
if self.decimals == 0:
|
||||||
|
if not isinstance(value, int):
|
||||||
|
return False
|
||||||
|
elif not isinstance(value, float):
|
||||||
return False
|
return False
|
||||||
|
if self.minimum > value > self.maximum:
|
||||||
return (
|
return False
|
||||||
self.decimals == other.decimals
|
return True
|
||||||
and self.maximum == other.maximum
|
|
||||||
and self.maximum == other.maximum
|
|
||||||
)
|
|
||||||
|
|
||||||
def convert_value(self, value):
|
def convert_value(self, value):
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
|
|
@ -361,6 +456,13 @@ class NumberDef(AbstractAttrDef):
|
||||||
return int(value)
|
return int(value)
|
||||||
return round(float(value), self.decimals)
|
return round(float(value), self.decimals)
|
||||||
|
|
||||||
|
def _def_type_compare(self, other: "NumberDef") -> bool:
|
||||||
|
return (
|
||||||
|
self.decimals == other.decimals
|
||||||
|
and self.maximum == other.maximum
|
||||||
|
and self.maximum == other.maximum
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TextDef(AbstractAttrDef):
|
class TextDef(AbstractAttrDef):
|
||||||
"""Text definition.
|
"""Text definition.
|
||||||
|
|
@ -390,7 +492,7 @@ class TextDef(AbstractAttrDef):
|
||||||
if default is None:
|
if default is None:
|
||||||
default = ""
|
default = ""
|
||||||
|
|
||||||
super(TextDef, self).__init__(key, default=default, **kwargs)
|
super().__init__(key, default=default, **kwargs)
|
||||||
|
|
||||||
if multiline is None:
|
if multiline is None:
|
||||||
multiline = False
|
multiline = False
|
||||||
|
|
@ -407,14 +509,12 @@ class TextDef(AbstractAttrDef):
|
||||||
self.placeholder = placeholder
|
self.placeholder = placeholder
|
||||||
self.regex = regex
|
self.regex = regex
|
||||||
|
|
||||||
def __eq__(self, other):
|
def is_value_valid(self, value: Any) -> bool:
|
||||||
if not super(TextDef, self).__eq__(other):
|
if not isinstance(value, str):
|
||||||
return False
|
return False
|
||||||
|
if self.regex and not self.regex.match(value):
|
||||||
return (
|
return False
|
||||||
self.multiline == other.multiline
|
return True
|
||||||
and self.regex == other.regex
|
|
||||||
)
|
|
||||||
|
|
||||||
def convert_value(self, value):
|
def convert_value(self, value):
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
|
|
@ -422,10 +522,18 @@ class TextDef(AbstractAttrDef):
|
||||||
return self.default
|
return self.default
|
||||||
|
|
||||||
def serialize(self):
|
def serialize(self):
|
||||||
data = super(TextDef, self).serialize()
|
data = super().serialize()
|
||||||
data["regex"] = self.regex.pattern
|
data["regex"] = self.regex.pattern
|
||||||
|
data["multiline"] = self.multiline
|
||||||
|
data["placeholder"] = self.placeholder
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
def _def_type_compare(self, other: "TextDef") -> bool:
|
||||||
|
return (
|
||||||
|
self.multiline == other.multiline
|
||||||
|
and self.regex == other.regex
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class EnumDef(AbstractAttrDef):
|
class EnumDef(AbstractAttrDef):
|
||||||
"""Enumeration of items.
|
"""Enumeration of items.
|
||||||
|
|
@ -464,21 +572,12 @@ class EnumDef(AbstractAttrDef):
|
||||||
elif default not in item_values:
|
elif default not in item_values:
|
||||||
default = next(iter(item_values), None)
|
default = next(iter(item_values), None)
|
||||||
|
|
||||||
super(EnumDef, self).__init__(key, default=default, **kwargs)
|
super().__init__(key, default=default, **kwargs)
|
||||||
|
|
||||||
self.items = items
|
self.items = items
|
||||||
self._item_values = item_values_set
|
self._item_values = item_values_set
|
||||||
self.multiselection = multiselection
|
self.multiselection = multiselection
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
if not super(EnumDef, self).__eq__(other):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return (
|
|
||||||
self.items == other.items
|
|
||||||
and self.multiselection == other.multiselection
|
|
||||||
)
|
|
||||||
|
|
||||||
def convert_value(self, value):
|
def convert_value(self, value):
|
||||||
if not self.multiselection:
|
if not self.multiselection:
|
||||||
if value in self._item_values:
|
if value in self._item_values:
|
||||||
|
|
@ -489,8 +588,19 @@ class EnumDef(AbstractAttrDef):
|
||||||
return copy.deepcopy(self.default)
|
return copy.deepcopy(self.default)
|
||||||
return list(self._item_values.intersection(value))
|
return list(self._item_values.intersection(value))
|
||||||
|
|
||||||
|
def is_value_valid(self, value: Any) -> bool:
|
||||||
|
"""Check if item is available in possible values."""
|
||||||
|
if isinstance(value, list):
|
||||||
|
if not self.multiselection:
|
||||||
|
return False
|
||||||
|
return all(value in self._item_values for value in value)
|
||||||
|
|
||||||
|
if self.multiselection:
|
||||||
|
return False
|
||||||
|
return value in self._item_values
|
||||||
|
|
||||||
def serialize(self):
|
def serialize(self):
|
||||||
data = super(EnumDef, self).serialize()
|
data = super().serialize()
|
||||||
data["items"] = copy.deepcopy(self.items)
|
data["items"] = copy.deepcopy(self.items)
|
||||||
data["multiselection"] = self.multiselection
|
data["multiselection"] = self.multiselection
|
||||||
return data
|
return data
|
||||||
|
|
@ -557,6 +667,12 @@ class EnumDef(AbstractAttrDef):
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
def _def_type_compare(self, other: "EnumDef") -> bool:
|
||||||
|
return (
|
||||||
|
self.items == other.items
|
||||||
|
and self.multiselection == other.multiselection
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BoolDef(AbstractAttrDef):
|
class BoolDef(AbstractAttrDef):
|
||||||
"""Boolean representation.
|
"""Boolean representation.
|
||||||
|
|
@ -570,7 +686,10 @@ class BoolDef(AbstractAttrDef):
|
||||||
def __init__(self, key, default=None, **kwargs):
|
def __init__(self, key, default=None, **kwargs):
|
||||||
if default is None:
|
if default is None:
|
||||||
default = False
|
default = False
|
||||||
super(BoolDef, self).__init__(key, default=default, **kwargs)
|
super().__init__(key, default=default, **kwargs)
|
||||||
|
|
||||||
|
def is_value_valid(self, value: Any) -> bool:
|
||||||
|
return isinstance(value, bool)
|
||||||
|
|
||||||
def convert_value(self, value):
|
def convert_value(self, value):
|
||||||
if isinstance(value, bool):
|
if isinstance(value, bool):
|
||||||
|
|
@ -868,10 +987,10 @@ class FileDef(AbstractAttrDef):
|
||||||
self.extensions = set(extensions)
|
self.extensions = set(extensions)
|
||||||
self.allow_sequences = allow_sequences
|
self.allow_sequences = allow_sequences
|
||||||
self.extensions_label = extensions_label
|
self.extensions_label = extensions_label
|
||||||
super(FileDef, self).__init__(key, default=default, **kwargs)
|
super().__init__(key, default=default, **kwargs)
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
if not super(FileDef, self).__eq__(other):
|
if not super().__eq__(other):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -881,6 +1000,29 @@ class FileDef(AbstractAttrDef):
|
||||||
and self.allow_sequences == other.allow_sequences
|
and self.allow_sequences == other.allow_sequences
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def is_value_valid(self, value: Any) -> bool:
|
||||||
|
if self.single_item:
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
FileDefItem.from_dict(value)
|
||||||
|
return True
|
||||||
|
except (ValueError, KeyError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not isinstance(value, list):
|
||||||
|
return False
|
||||||
|
|
||||||
|
for item in value:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
FileDefItem.from_dict(item)
|
||||||
|
except (ValueError, KeyError):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def convert_value(self, value):
|
def convert_value(self, value):
|
||||||
if isinstance(value, (str, dict)):
|
if isinstance(value, (str, dict)):
|
||||||
value = [value]
|
value = [value]
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,9 @@ class AttributeValues:
|
||||||
for key in self._attr_defs_by_key.keys():
|
for key in self._attr_defs_by_key.keys():
|
||||||
yield key, self._data.get(key)
|
yield key, self._data.get(key)
|
||||||
|
|
||||||
|
def get_attr_def(self, key, default=None):
|
||||||
|
return self._attr_defs_by_key.get(key, default)
|
||||||
|
|
||||||
def update(self, value):
|
def update(self, value):
|
||||||
changes = {}
|
changes = {}
|
||||||
for _key, _value in dict(value).items():
|
for _key, _value in dict(value).items():
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,20 @@ import os
|
||||||
import copy
|
import copy
|
||||||
import shutil
|
import shutil
|
||||||
import glob
|
import glob
|
||||||
import clique
|
|
||||||
import collections
|
import collections
|
||||||
|
from typing import Dict, Any, Iterable
|
||||||
|
|
||||||
|
import clique
|
||||||
|
import ayon_api
|
||||||
|
|
||||||
from ayon_core.lib import create_hard_link
|
from ayon_core.lib import create_hard_link
|
||||||
|
|
||||||
|
from .template_data import (
|
||||||
|
get_general_template_data,
|
||||||
|
get_folder_template_data,
|
||||||
|
get_task_template_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _copy_file(src_path, dst_path):
|
def _copy_file(src_path, dst_path):
|
||||||
"""Hardlink file if possible(to save space), copy if not.
|
"""Hardlink file if possible(to save space), copy if not.
|
||||||
|
|
@ -327,3 +336,71 @@ def deliver_sequence(
|
||||||
uploaded += 1
|
uploaded += 1
|
||||||
|
|
||||||
return report_items, uploaded
|
return report_items, uploaded
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_data(data, new_data):
|
||||||
|
queue = collections.deque()
|
||||||
|
queue.append((data, new_data))
|
||||||
|
while queue:
|
||||||
|
q_data, q_new_data = queue.popleft()
|
||||||
|
for key, value in q_new_data.items():
|
||||||
|
if key in q_data and isinstance(value, dict):
|
||||||
|
queue.append((q_data[key], value))
|
||||||
|
continue
|
||||||
|
q_data[key] = value
|
||||||
|
|
||||||
|
|
||||||
|
def get_representations_delivery_template_data(
|
||||||
|
project_name: str,
|
||||||
|
representation_ids: Iterable[str],
|
||||||
|
) -> Dict[str, Dict[str, Any]]:
|
||||||
|
representation_ids = set(representation_ids)
|
||||||
|
|
||||||
|
output = {
|
||||||
|
repre_id: {}
|
||||||
|
for repre_id in representation_ids
|
||||||
|
}
|
||||||
|
if not representation_ids:
|
||||||
|
return output
|
||||||
|
|
||||||
|
project_entity = ayon_api.get_project(project_name)
|
||||||
|
|
||||||
|
general_template_data = get_general_template_data()
|
||||||
|
|
||||||
|
repres_hierarchy = ayon_api.get_representations_hierarchy(
|
||||||
|
project_name,
|
||||||
|
representation_ids,
|
||||||
|
project_fields=set(),
|
||||||
|
folder_fields={"path", "folderType"},
|
||||||
|
task_fields={"name", "taskType"},
|
||||||
|
product_fields={"name", "productType"},
|
||||||
|
version_fields={"version", "productId"},
|
||||||
|
representation_fields=None,
|
||||||
|
)
|
||||||
|
for repre_id, repre_hierarchy in repres_hierarchy.items():
|
||||||
|
repre_entity = repre_hierarchy.representation
|
||||||
|
if repre_entity is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
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
|
||||||
|
))
|
||||||
|
if repre_hierarchy.task:
|
||||||
|
template_data.update(get_task_template_data(
|
||||||
|
project_entity, repre_hierarchy.task
|
||||||
|
))
|
||||||
|
|
||||||
|
product_entity = repre_hierarchy.product
|
||||||
|
version_entity = repre_hierarchy.version
|
||||||
|
template_data.update({
|
||||||
|
"product": {
|
||||||
|
"name": product_entity["name"],
|
||||||
|
"type": product_entity["productType"],
|
||||||
|
},
|
||||||
|
"version": version_entity["version"],
|
||||||
|
})
|
||||||
|
_merge_data(template_data, repre_entity["context"])
|
||||||
|
output[repre_id] = template_data
|
||||||
|
return output
|
||||||
|
|
|
||||||
|
|
@ -292,13 +292,26 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end):
|
||||||
# Note that 24fps is slower than 25fps hence extended duration
|
# Note that 24fps is slower than 25fps hence extended duration
|
||||||
# to preserve media range
|
# to preserve media range
|
||||||
|
|
||||||
# Compute new source range based on available rate
|
# Compute new source range based on available rate.
|
||||||
conformed_src_in = source_range.start_time.rescaled_to(available_range_rate)
|
|
||||||
conformed_src_duration = source_range.duration.rescaled_to(available_range_rate)
|
# Backward-compatibility for Hiero OTIO exporter.
|
||||||
conformed_source_range = otio.opentime.TimeRange(
|
# NTSC compatibility might introduce floating rates, when these are
|
||||||
start_time=conformed_src_in,
|
# not exactly the same (23.976 vs 23.976024627685547)
|
||||||
duration=conformed_src_duration
|
# this will cause precision issue in computation.
|
||||||
)
|
# Currently round to 2 decimals for comparison,
|
||||||
|
# but this should always rescale after that.
|
||||||
|
rounded_av_rate = round(available_range_rate, 2)
|
||||||
|
rounded_src_rate = round(source_range.start_time.rate, 2)
|
||||||
|
if rounded_av_rate != rounded_src_rate:
|
||||||
|
conformed_src_in = source_range.start_time.rescaled_to(available_range_rate)
|
||||||
|
conformed_src_duration = source_range.duration.rescaled_to(available_range_rate)
|
||||||
|
conformed_source_range = otio.opentime.TimeRange(
|
||||||
|
start_time=conformed_src_in,
|
||||||
|
duration=conformed_src_duration
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
conformed_source_range = source_range
|
||||||
|
|
||||||
# modifiers
|
# modifiers
|
||||||
time_scalar = 1.
|
time_scalar = 1.
|
||||||
|
|
|
||||||
|
|
@ -788,15 +788,15 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data,
|
||||||
colorspace = product.colorspace
|
colorspace = product.colorspace
|
||||||
break
|
break
|
||||||
|
|
||||||
if isinstance(files, (list, tuple)):
|
if isinstance(collected_files, (list, tuple)):
|
||||||
files = [os.path.basename(f) for f in files]
|
collected_files = [os.path.basename(f) for f in collected_files]
|
||||||
else:
|
else:
|
||||||
files = os.path.basename(files)
|
collected_files = os.path.basename(collected_files)
|
||||||
|
|
||||||
rep = {
|
rep = {
|
||||||
"name": ext,
|
"name": ext,
|
||||||
"ext": ext,
|
"ext": ext,
|
||||||
"files": files,
|
"files": collected_files,
|
||||||
"frameStart": int(skeleton["frameStartHandle"]),
|
"frameStart": int(skeleton["frameStartHandle"]),
|
||||||
"frameEnd": int(skeleton["frameEndHandle"]),
|
"frameEnd": int(skeleton["frameEndHandle"]),
|
||||||
# If expectedFile are absolute, we need only filenames
|
# If expectedFile are absolute, we need only filenames
|
||||||
|
|
|
||||||
|
|
@ -242,6 +242,26 @@ class LoaderPlugin(list):
|
||||||
if hasattr(self, "_fname"):
|
if hasattr(self, "_fname"):
|
||||||
return self._fname
|
return self._fname
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_representation_name_aliases(cls, representation_name: str):
|
||||||
|
"""Return representation names to which switching is allowed from
|
||||||
|
the input representation name, like an alias replacement of the input
|
||||||
|
`representation_name`.
|
||||||
|
|
||||||
|
For example, to allow an automated switch on update from representation
|
||||||
|
`ma` to `mb` or `abc`, then when `representation_name` is `ma` return:
|
||||||
|
["mb", "abc"]
|
||||||
|
|
||||||
|
The order of the names in the returned representation names is
|
||||||
|
important, because the first one existing under the new version will
|
||||||
|
be chosen.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[str]: Representation names switching to is allowed on update
|
||||||
|
if the input representation name is not found on the new version.
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
class ProductLoaderPlugin(LoaderPlugin):
|
class ProductLoaderPlugin(LoaderPlugin):
|
||||||
"""Load product into host application
|
"""Load product into host application
|
||||||
|
|
|
||||||
|
|
@ -505,21 +505,6 @@ def update_container(container, version=-1):
|
||||||
project_name, product_entity["folderId"]
|
project_name, product_entity["folderId"]
|
||||||
)
|
)
|
||||||
|
|
||||||
repre_name = current_representation["name"]
|
|
||||||
new_representation = ayon_api.get_representation_by_name(
|
|
||||||
project_name, repre_name, new_version["id"]
|
|
||||||
)
|
|
||||||
if new_representation is None:
|
|
||||||
raise ValueError(
|
|
||||||
"Representation '{}' wasn't found on requested version".format(
|
|
||||||
repre_name
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
path = get_representation_path(new_representation)
|
|
||||||
if not path or not os.path.exists(path):
|
|
||||||
raise ValueError("Path {} doesn't exist".format(path))
|
|
||||||
|
|
||||||
# Run update on the Loader for this container
|
# Run update on the Loader for this container
|
||||||
Loader = _get_container_loader(container)
|
Loader = _get_container_loader(container)
|
||||||
if not Loader:
|
if not Loader:
|
||||||
|
|
@ -527,6 +512,39 @@ def update_container(container, version=-1):
|
||||||
"Can't update container because loader '{}' was not found."
|
"Can't update container because loader '{}' was not found."
|
||||||
.format(container.get("loader"))
|
.format(container.get("loader"))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
repre_name = current_representation["name"]
|
||||||
|
new_representation = ayon_api.get_representation_by_name(
|
||||||
|
project_name, repre_name, new_version["id"]
|
||||||
|
)
|
||||||
|
if new_representation is None:
|
||||||
|
# The representation name is not found in the new version.
|
||||||
|
# Allow updating to a 'matching' representation if the loader
|
||||||
|
# has defined compatible update conversions
|
||||||
|
repre_name_aliases = Loader.get_representation_name_aliases(repre_name)
|
||||||
|
if repre_name_aliases:
|
||||||
|
representations = ayon_api.get_representations(
|
||||||
|
project_name,
|
||||||
|
representation_names=repre_name_aliases,
|
||||||
|
version_ids=[new_version["id"]])
|
||||||
|
representations_by_name = {
|
||||||
|
repre["name"]: repre for repre in representations
|
||||||
|
}
|
||||||
|
for name in repre_name_aliases:
|
||||||
|
if name in representations_by_name:
|
||||||
|
new_representation = representations_by_name[name]
|
||||||
|
break
|
||||||
|
|
||||||
|
if new_representation is None:
|
||||||
|
raise ValueError(
|
||||||
|
"Representation '{}' wasn't found on requested version".format(
|
||||||
|
repre_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
path = get_representation_path(new_representation)
|
||||||
|
if not path or not os.path.exists(path):
|
||||||
|
raise ValueError("Path {} doesn't exist".format(path))
|
||||||
project_entity = ayon_api.get_project(project_name)
|
project_entity = ayon_api.get_project(project_name)
|
||||||
context = {
|
context = {
|
||||||
"project": project_entity,
|
"project": project_entity,
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,19 @@
|
||||||
import inspect
|
import inspect
|
||||||
from abc import ABCMeta
|
from abc import ABCMeta
|
||||||
|
import typing
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import pyblish.api
|
import pyblish.api
|
||||||
import pyblish.logic
|
import pyblish.logic
|
||||||
from pyblish.plugin import MetaPlugin, ExplicitMetaPlugin
|
from pyblish.plugin import MetaPlugin, ExplicitMetaPlugin
|
||||||
|
|
||||||
from ayon_core.lib import BoolDef
|
from ayon_core.lib import BoolDef
|
||||||
|
|
||||||
|
from ayon_core.pipeline.colorspace import (
|
||||||
|
get_colorspace_settings_from_publish_context,
|
||||||
|
set_colorspace_data_to_representation
|
||||||
|
)
|
||||||
|
|
||||||
from .lib import (
|
from .lib import (
|
||||||
load_help_content_from_plugin,
|
load_help_content_from_plugin,
|
||||||
get_errored_instances_from_context,
|
get_errored_instances_from_context,
|
||||||
|
|
@ -12,10 +21,8 @@ from .lib import (
|
||||||
get_instance_staging_dir,
|
get_instance_staging_dir,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ayon_core.pipeline.colorspace import (
|
if typing.TYPE_CHECKING:
|
||||||
get_colorspace_settings_from_publish_context,
|
from ayon_core.pipeline.create import CreateContext, CreatedInstance
|
||||||
set_colorspace_data_to_representation
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AbstractMetaInstancePlugin(ABCMeta, MetaPlugin):
|
class AbstractMetaInstancePlugin(ABCMeta, MetaPlugin):
|
||||||
|
|
@ -127,7 +134,9 @@ class AYONPyblishPluginMixin:
|
||||||
# callback(self)
|
# callback(self)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def register_create_context_callbacks(cls, create_context):
|
def register_create_context_callbacks(
|
||||||
|
cls, create_context: "CreateContext"
|
||||||
|
):
|
||||||
"""Register callbacks for create context.
|
"""Register callbacks for create context.
|
||||||
|
|
||||||
It is possible to register callbacks listening to changes happened
|
It is possible to register callbacks listening to changes happened
|
||||||
|
|
@ -160,7 +169,7 @@ class AYONPyblishPluginMixin:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_attr_defs_for_context (cls, create_context):
|
def get_attr_defs_for_context(cls, create_context: "CreateContext"):
|
||||||
"""Publish attribute definitions for context.
|
"""Publish attribute definitions for context.
|
||||||
|
|
||||||
Attributes available for all families in plugin's `families` attribute.
|
Attributes available for all families in plugin's `families` attribute.
|
||||||
|
|
@ -177,7 +186,9 @@ class AYONPyblishPluginMixin:
|
||||||
return cls.get_attribute_defs()
|
return cls.get_attribute_defs()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def instance_matches_plugin_families(cls, instance):
|
def instance_matches_plugin_families(
|
||||||
|
cls, instance: Optional["CreatedInstance"]
|
||||||
|
):
|
||||||
"""Check if instance matches families.
|
"""Check if instance matches families.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -201,7 +212,9 @@ class AYONPyblishPluginMixin:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_attr_defs_for_instance(cls, create_context, instance):
|
def get_attr_defs_for_instance(
|
||||||
|
cls, create_context: "CreateContext", instance: "CreatedInstance"
|
||||||
|
):
|
||||||
"""Publish attribute definitions for an instance.
|
"""Publish attribute definitions for an instance.
|
||||||
|
|
||||||
Attributes available for all families in plugin's `families` attribute.
|
Attributes available for all families in plugin's `families` attribute.
|
||||||
|
|
@ -220,7 +233,9 @@ class AYONPyblishPluginMixin:
|
||||||
return cls.get_attribute_defs()
|
return cls.get_attribute_defs()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def convert_attribute_values(cls, create_context, instance):
|
def convert_attribute_values(
|
||||||
|
cls, create_context: "CreateContext", instance: "CreatedInstance"
|
||||||
|
):
|
||||||
"""Convert attribute values for instance.
|
"""Convert attribute values for instance.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,22 @@
|
||||||
import copy
|
|
||||||
import platform
|
import platform
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
import ayon_api
|
import ayon_api
|
||||||
from qtpy import QtWidgets, QtCore, QtGui
|
from qtpy import QtWidgets, QtCore, QtGui
|
||||||
|
|
||||||
from ayon_core.pipeline import load, Anatomy
|
|
||||||
from ayon_core import resources, style
|
from ayon_core import resources, style
|
||||||
|
|
||||||
from ayon_core.lib import (
|
from ayon_core.lib import (
|
||||||
format_file_size,
|
format_file_size,
|
||||||
collect_frames,
|
collect_frames,
|
||||||
get_datetime_data,
|
get_datetime_data,
|
||||||
)
|
)
|
||||||
|
from ayon_core.pipeline import load, Anatomy
|
||||||
from ayon_core.pipeline.load import get_representation_path_with_anatomy
|
from ayon_core.pipeline.load import get_representation_path_with_anatomy
|
||||||
from ayon_core.pipeline.delivery import (
|
from ayon_core.pipeline.delivery import (
|
||||||
get_format_dict,
|
get_format_dict,
|
||||||
check_destination_path,
|
check_destination_path,
|
||||||
deliver_single_file
|
deliver_single_file,
|
||||||
|
get_representations_delivery_template_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -200,20 +199,31 @@ class DeliveryOptionsDialog(QtWidgets.QDialog):
|
||||||
format_dict = get_format_dict(self.anatomy, self.root_line_edit.text())
|
format_dict = get_format_dict(self.anatomy, self.root_line_edit.text())
|
||||||
renumber_frame = self.renumber_frame.isChecked()
|
renumber_frame = self.renumber_frame.isChecked()
|
||||||
frame_offset = self.first_frame_start.value()
|
frame_offset = self.first_frame_start.value()
|
||||||
|
filtered_repres = []
|
||||||
|
repre_ids = set()
|
||||||
for repre in self._representations:
|
for repre in self._representations:
|
||||||
if repre["name"] not in selected_repres:
|
if repre["name"] in selected_repres:
|
||||||
continue
|
filtered_repres.append(repre)
|
||||||
|
repre_ids.add(repre["id"])
|
||||||
|
|
||||||
|
template_data_by_repre_id = (
|
||||||
|
get_representations_delivery_template_data(
|
||||||
|
self.anatomy.project_name, repre_ids
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for repre in filtered_repres:
|
||||||
repre_path = get_representation_path_with_anatomy(
|
repre_path = get_representation_path_with_anatomy(
|
||||||
repre, self.anatomy
|
repre, self.anatomy
|
||||||
)
|
)
|
||||||
|
|
||||||
anatomy_data = copy.deepcopy(repre["context"])
|
template_data = template_data_by_repre_id[repre["id"]]
|
||||||
new_report_items = check_destination_path(repre["id"],
|
new_report_items = check_destination_path(
|
||||||
self.anatomy,
|
repre["id"],
|
||||||
anatomy_data,
|
self.anatomy,
|
||||||
datetime_data,
|
template_data,
|
||||||
template_name)
|
datetime_data,
|
||||||
|
template_name
|
||||||
|
)
|
||||||
|
|
||||||
report_items.update(new_report_items)
|
report_items.update(new_report_items)
|
||||||
if new_report_items:
|
if new_report_items:
|
||||||
|
|
@ -224,7 +234,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog):
|
||||||
repre,
|
repre,
|
||||||
self.anatomy,
|
self.anatomy,
|
||||||
template_name,
|
template_name,
|
||||||
anatomy_data,
|
template_data,
|
||||||
format_dict,
|
format_dict,
|
||||||
report_items,
|
report_items,
|
||||||
self.log
|
self.log
|
||||||
|
|
@ -267,9 +277,9 @@ class DeliveryOptionsDialog(QtWidgets.QDialog):
|
||||||
|
|
||||||
if frame is not None:
|
if frame is not None:
|
||||||
if repre["context"].get("frame"):
|
if repre["context"].get("frame"):
|
||||||
anatomy_data["frame"] = frame
|
template_data["frame"] = frame
|
||||||
elif repre["context"].get("udim"):
|
elif repre["context"].get("udim"):
|
||||||
anatomy_data["udim"] = frame
|
template_data["udim"] = frame
|
||||||
else:
|
else:
|
||||||
# Fallback
|
# Fallback
|
||||||
self.log.warning(
|
self.log.warning(
|
||||||
|
|
@ -277,7 +287,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog):
|
||||||
" data. Supplying sequence frame to '{frame}'"
|
" data. Supplying sequence frame to '{frame}'"
|
||||||
" formatting data."
|
" formatting data."
|
||||||
)
|
)
|
||||||
anatomy_data["frame"] = frame
|
template_data["frame"] = frame
|
||||||
new_report_items, uploaded = deliver_single_file(*args)
|
new_report_items, uploaded = deliver_single_file(*args)
|
||||||
report_items.update(new_report_items)
|
report_items.update(new_report_items)
|
||||||
self._update_progress(uploaded)
|
self._update_progress(uploaded)
|
||||||
|
|
@ -342,8 +352,8 @@ class DeliveryOptionsDialog(QtWidgets.QDialog):
|
||||||
def _get_selected_repres(self):
|
def _get_selected_repres(self):
|
||||||
"""Returns list of representation names filtered from checkboxes."""
|
"""Returns list of representation names filtered from checkboxes."""
|
||||||
selected_repres = []
|
selected_repres = []
|
||||||
for repre_name, chckbox in self._representation_checkboxes.items():
|
for repre_name, checkbox in self._representation_checkboxes.items():
|
||||||
if chckbox.isChecked():
|
if checkbox.isChecked():
|
||||||
selected_repres.append(repre_name)
|
selected_repres.append(repre_name)
|
||||||
|
|
||||||
return selected_repres
|
return selected_repres
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,8 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
||||||
"traypublisher",
|
"traypublisher",
|
||||||
"substancepainter",
|
"substancepainter",
|
||||||
"nuke",
|
"nuke",
|
||||||
"aftereffects"
|
"aftereffects",
|
||||||
|
"unreal"
|
||||||
]
|
]
|
||||||
enabled = False
|
enabled = False
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,10 +28,10 @@ from .files_widget import FilesWidget
|
||||||
|
|
||||||
def create_widget_for_attr_def(attr_def, parent=None):
|
def create_widget_for_attr_def(attr_def, parent=None):
|
||||||
widget = _create_widget_for_attr_def(attr_def, parent)
|
widget = _create_widget_for_attr_def(attr_def, parent)
|
||||||
if attr_def.hidden:
|
if not attr_def.visible:
|
||||||
widget.setVisible(False)
|
widget.setVisible(False)
|
||||||
|
|
||||||
if attr_def.disabled:
|
if not attr_def.enabled:
|
||||||
widget.setEnabled(False)
|
widget.setEnabled(False)
|
||||||
return widget
|
return widget
|
||||||
|
|
||||||
|
|
@ -135,7 +135,7 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget):
|
||||||
widget = create_widget_for_attr_def(attr_def, self)
|
widget = create_widget_for_attr_def(attr_def, self)
|
||||||
self._widgets.append(widget)
|
self._widgets.append(widget)
|
||||||
|
|
||||||
if attr_def.hidden:
|
if not attr_def.visible:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
expand_cols = 2
|
expand_cols = 2
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,273 @@
|
||||||
|
"""
|
||||||
|
Brought from https://gist.github.com/BigRoy/1972822065e38f8fae7521078e44eca2
|
||||||
|
Code Credits: [BigRoy](https://github.com/BigRoy)
|
||||||
|
|
||||||
|
Requirement:
|
||||||
|
It requires pyblish version >= 1.8.12
|
||||||
|
|
||||||
|
How it works:
|
||||||
|
This tool makes use of pyblish event `pluginProcessed` to:
|
||||||
|
1. Pause the publishing.
|
||||||
|
2. Collect some info about the plugin.
|
||||||
|
3. Show that info to the tool's window.
|
||||||
|
4. Continue publishing on clicking `step` button.
|
||||||
|
|
||||||
|
How to use it:
|
||||||
|
1. Launch the tool from AYON experimental tools window.
|
||||||
|
2. Launch the publisher tool and click validate.
|
||||||
|
3. Click Step to run plugins one by one.
|
||||||
|
|
||||||
|
Note :
|
||||||
|
Pyblish debugger also works when triggering the validation or
|
||||||
|
publishing from code.
|
||||||
|
Here's an example about validating from code:
|
||||||
|
https://github.com/MustafaJafar/ayon-recipes/blob/main/validate_from_code.py
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import json
|
||||||
|
from qtpy import QtWidgets, QtCore, QtGui
|
||||||
|
|
||||||
|
import pyblish.api
|
||||||
|
from ayon_core import style
|
||||||
|
|
||||||
|
TAB = 4* " "
|
||||||
|
HEADER_SIZE = "15px"
|
||||||
|
|
||||||
|
KEY_COLOR = QtGui.QColor("#ffffff")
|
||||||
|
NEW_KEY_COLOR = QtGui.QColor("#00ff00")
|
||||||
|
VALUE_TYPE_COLOR = QtGui.QColor("#ffbbbb")
|
||||||
|
NEW_VALUE_TYPE_COLOR = QtGui.QColor("#ff4444")
|
||||||
|
VALUE_COLOR = QtGui.QColor("#777799")
|
||||||
|
NEW_VALUE_COLOR = QtGui.QColor("#DDDDCC")
|
||||||
|
CHANGED_VALUE_COLOR = QtGui.QColor("#CCFFCC")
|
||||||
|
|
||||||
|
MAX_VALUE_STR_LEN = 100
|
||||||
|
|
||||||
|
|
||||||
|
def failsafe_deepcopy(data):
|
||||||
|
"""Allow skipping the deepcopy for unsupported types"""
|
||||||
|
try:
|
||||||
|
return copy.deepcopy(data)
|
||||||
|
except TypeError:
|
||||||
|
if isinstance(data, dict):
|
||||||
|
return {
|
||||||
|
key: failsafe_deepcopy(value)
|
||||||
|
for key, value in data.items()
|
||||||
|
}
|
||||||
|
elif isinstance(data, list):
|
||||||
|
return data.copy()
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class DictChangesModel(QtGui.QStandardItemModel):
|
||||||
|
# TODO: Replace this with a QAbstractItemModel
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(DictChangesModel, self).__init__(*args, **kwargs)
|
||||||
|
self._data = {}
|
||||||
|
|
||||||
|
columns = ["Key", "Type", "Value"]
|
||||||
|
self.setColumnCount(len(columns))
|
||||||
|
for i, label in enumerate(columns):
|
||||||
|
self.setHeaderData(i, QtCore.Qt.Horizontal, label)
|
||||||
|
|
||||||
|
def _update_recursive(self, data, parent, previous_data):
|
||||||
|
for key, value in data.items():
|
||||||
|
|
||||||
|
# Find existing item or add new row
|
||||||
|
parent_index = parent.index()
|
||||||
|
for row in range(self.rowCount(parent_index)):
|
||||||
|
# Update existing item if it exists
|
||||||
|
index = self.index(row, 0, parent_index)
|
||||||
|
if index.data() == key:
|
||||||
|
item = self.itemFromIndex(index)
|
||||||
|
type_item = self.itemFromIndex(self.index(row, 1, parent_index)) # noqa
|
||||||
|
value_item = self.itemFromIndex(self.index(row, 2, parent_index)) # noqa
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
item = QtGui.QStandardItem(key)
|
||||||
|
type_item = QtGui.QStandardItem()
|
||||||
|
value_item = QtGui.QStandardItem()
|
||||||
|
parent.appendRow([item, type_item, value_item])
|
||||||
|
|
||||||
|
# Key
|
||||||
|
key_color = NEW_KEY_COLOR if key not in previous_data else KEY_COLOR # noqa
|
||||||
|
item.setData(key_color, QtCore.Qt.ForegroundRole)
|
||||||
|
|
||||||
|
# Type
|
||||||
|
type_str = type(value).__name__
|
||||||
|
type_color = VALUE_TYPE_COLOR
|
||||||
|
if (
|
||||||
|
key in previous_data
|
||||||
|
and type(previous_data[key]).__name__ != type_str
|
||||||
|
):
|
||||||
|
type_color = NEW_VALUE_TYPE_COLOR
|
||||||
|
|
||||||
|
type_item.setText(type_str)
|
||||||
|
type_item.setData(type_color, QtCore.Qt.ForegroundRole)
|
||||||
|
|
||||||
|
# Value
|
||||||
|
value_changed = False
|
||||||
|
if key not in previous_data or previous_data[key] != value:
|
||||||
|
value_changed = True
|
||||||
|
value_color = NEW_VALUE_COLOR if value_changed else VALUE_COLOR
|
||||||
|
|
||||||
|
value_item.setData(value_color, QtCore.Qt.ForegroundRole)
|
||||||
|
if value_changed:
|
||||||
|
value_str = str(value)
|
||||||
|
if len(value_str) > MAX_VALUE_STR_LEN:
|
||||||
|
value_str = value_str[:MAX_VALUE_STR_LEN] + "..."
|
||||||
|
value_item.setText(value_str)
|
||||||
|
|
||||||
|
# Preferably this is deferred to only when the data gets
|
||||||
|
# requested since this formatting can be slow for very large
|
||||||
|
# data sets like project settings and system settings
|
||||||
|
# This will also be MUCH faster if we don't clear the
|
||||||
|
# items on each update but only updated/add/remove changed
|
||||||
|
# items so that this also runs much less often
|
||||||
|
value_item.setData(
|
||||||
|
json.dumps(value, default=str, indent=4),
|
||||||
|
QtCore.Qt.ToolTipRole
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(value, dict):
|
||||||
|
previous_value = previous_data.get(key, {})
|
||||||
|
if previous_data.get(key) != value:
|
||||||
|
# Update children if the value is not the same as before
|
||||||
|
self._update_recursive(value,
|
||||||
|
parent=item,
|
||||||
|
previous_data=previous_value)
|
||||||
|
else:
|
||||||
|
# TODO: Ensure all children are updated to be not marked
|
||||||
|
# as 'changed' in the most optimal way possible
|
||||||
|
self._update_recursive(value,
|
||||||
|
parent=item,
|
||||||
|
previous_data=previous_value)
|
||||||
|
|
||||||
|
self._data = data
|
||||||
|
|
||||||
|
def update(self, data):
|
||||||
|
parent = self.invisibleRootItem()
|
||||||
|
|
||||||
|
data = failsafe_deepcopy(data)
|
||||||
|
previous_data = self._data
|
||||||
|
self._update_recursive(data, parent, previous_data)
|
||||||
|
self._data = data # store previous data for next update
|
||||||
|
|
||||||
|
|
||||||
|
class DebugUI(QtWidgets.QDialog):
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super(DebugUI, self).__init__(parent=parent)
|
||||||
|
self.setStyleSheet(style.load_stylesheet())
|
||||||
|
|
||||||
|
self._set_window_title()
|
||||||
|
self.setWindowFlags(
|
||||||
|
QtCore.Qt.Window
|
||||||
|
| QtCore.Qt.CustomizeWindowHint
|
||||||
|
| QtCore.Qt.WindowTitleHint
|
||||||
|
| QtCore.Qt.WindowMinimizeButtonHint
|
||||||
|
| QtCore.Qt.WindowCloseButtonHint
|
||||||
|
| QtCore.Qt.WindowStaysOnTopHint
|
||||||
|
)
|
||||||
|
|
||||||
|
layout = QtWidgets.QVBoxLayout(self)
|
||||||
|
text_edit = QtWidgets.QTextEdit()
|
||||||
|
text_edit.setFixedHeight(65)
|
||||||
|
font = QtGui.QFont("NONEXISTENTFONT")
|
||||||
|
font.setStyleHint(QtGui.QFont.TypeWriter)
|
||||||
|
text_edit.setFont(font)
|
||||||
|
text_edit.setLineWrapMode(QtWidgets.QTextEdit.NoWrap)
|
||||||
|
|
||||||
|
step = QtWidgets.QPushButton("Step")
|
||||||
|
step.setEnabled(False)
|
||||||
|
|
||||||
|
model = DictChangesModel()
|
||||||
|
proxy = QtCore.QSortFilterProxyModel()
|
||||||
|
proxy.setRecursiveFilteringEnabled(True)
|
||||||
|
proxy.setSourceModel(model)
|
||||||
|
view = QtWidgets.QTreeView()
|
||||||
|
view.setModel(proxy)
|
||||||
|
view.setSortingEnabled(True)
|
||||||
|
|
||||||
|
filter_field = QtWidgets.QLineEdit()
|
||||||
|
filter_field.setPlaceholderText("Filter keys...")
|
||||||
|
filter_field.textChanged.connect(proxy.setFilterFixedString)
|
||||||
|
|
||||||
|
layout.addWidget(text_edit)
|
||||||
|
layout.addWidget(filter_field)
|
||||||
|
layout.addWidget(view)
|
||||||
|
layout.addWidget(step)
|
||||||
|
|
||||||
|
step.clicked.connect(self.on_step)
|
||||||
|
|
||||||
|
self._pause = False
|
||||||
|
self.model = model
|
||||||
|
self.filter = filter_field
|
||||||
|
self.proxy = proxy
|
||||||
|
self.view = view
|
||||||
|
self.text = text_edit
|
||||||
|
self.step = step
|
||||||
|
self.resize(700, 500)
|
||||||
|
|
||||||
|
self._previous_data = {}
|
||||||
|
|
||||||
|
def _set_window_title(self, plugin=None):
|
||||||
|
title = "Pyblish Debug Stepper"
|
||||||
|
if plugin is not None:
|
||||||
|
plugin_label = plugin.label or plugin.__name__
|
||||||
|
title += f" | {plugin_label}"
|
||||||
|
self.setWindowTitle(title)
|
||||||
|
|
||||||
|
def pause(self, state):
|
||||||
|
self._pause = state
|
||||||
|
self.step.setEnabled(state)
|
||||||
|
|
||||||
|
def on_step(self):
|
||||||
|
self.pause(False)
|
||||||
|
|
||||||
|
def showEvent(self, event):
|
||||||
|
print("Registering callback..")
|
||||||
|
pyblish.api.register_callback("pluginProcessed",
|
||||||
|
self.on_plugin_processed)
|
||||||
|
|
||||||
|
def hideEvent(self, event):
|
||||||
|
self.pause(False)
|
||||||
|
print("Deregistering callback..")
|
||||||
|
pyblish.api.deregister_callback("pluginProcessed",
|
||||||
|
self.on_plugin_processed)
|
||||||
|
|
||||||
|
def on_plugin_processed(self, result):
|
||||||
|
self.pause(True)
|
||||||
|
|
||||||
|
self._set_window_title(plugin=result["plugin"])
|
||||||
|
|
||||||
|
print(10*"<", result["plugin"].__name__, 10*">")
|
||||||
|
|
||||||
|
plugin_order = result["plugin"].order
|
||||||
|
plugin_name = result["plugin"].__name__
|
||||||
|
duration = result['duration']
|
||||||
|
plugin_instance = result["instance"]
|
||||||
|
context = result["context"]
|
||||||
|
|
||||||
|
msg = ""
|
||||||
|
msg += f"Order: {plugin_order}<br>"
|
||||||
|
msg += f"Plugin: {plugin_name}"
|
||||||
|
if plugin_instance is not None:
|
||||||
|
msg += f" -> instance: {plugin_instance}"
|
||||||
|
msg += "<br>"
|
||||||
|
msg += f"Duration: {duration} ms<br>"
|
||||||
|
self.text.setHtml(msg)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"context": context.data
|
||||||
|
}
|
||||||
|
for instance in context:
|
||||||
|
data[instance.name] = instance.data
|
||||||
|
self.model.update(data)
|
||||||
|
|
||||||
|
app = QtWidgets.QApplication.instance()
|
||||||
|
while self._pause:
|
||||||
|
# Allow user interaction with the UI
|
||||||
|
app.processEvents()
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import os
|
import os
|
||||||
|
from .pyblish_debug_stepper import DebugUI
|
||||||
|
|
||||||
# Constant key under which local settings are stored
|
# Constant key under which local settings are stored
|
||||||
LOCAL_EXPERIMENTAL_KEY = "experimental_tools"
|
LOCAL_EXPERIMENTAL_KEY = "experimental_tools"
|
||||||
|
|
@ -95,6 +96,12 @@ class ExperimentalTools:
|
||||||
"hiero",
|
"hiero",
|
||||||
"resolve",
|
"resolve",
|
||||||
]
|
]
|
||||||
|
),
|
||||||
|
ExperimentalHostTool(
|
||||||
|
"pyblish_debug_stepper",
|
||||||
|
"Pyblish Debug Stepper",
|
||||||
|
"Debug Pyblish plugins step by step.",
|
||||||
|
self._show_pyblish_debugger,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -162,9 +169,16 @@ class ExperimentalTools:
|
||||||
local_settings.get(LOCAL_EXPERIMENTAL_KEY)
|
local_settings.get(LOCAL_EXPERIMENTAL_KEY)
|
||||||
) or {}
|
) or {}
|
||||||
|
|
||||||
for identifier, eperimental_tool in self.tools_by_identifier.items():
|
# Enable the following tools by default.
|
||||||
|
# Because they will always be disabled due
|
||||||
|
# to the fact their settings don't exist.
|
||||||
|
experimental_settings.update({
|
||||||
|
"pyblish_debug_stepper": True,
|
||||||
|
})
|
||||||
|
|
||||||
|
for identifier, experimental_tool in self.tools_by_identifier.items():
|
||||||
enabled = experimental_settings.get(identifier, False)
|
enabled = experimental_settings.get(identifier, False)
|
||||||
eperimental_tool.set_enabled(enabled)
|
experimental_tool.set_enabled(enabled)
|
||||||
|
|
||||||
def _show_publisher(self):
|
def _show_publisher(self):
|
||||||
if self._publisher_tool is None:
|
if self._publisher_tool is None:
|
||||||
|
|
@ -175,3 +189,7 @@ class ExperimentalTools:
|
||||||
)
|
)
|
||||||
|
|
||||||
self._publisher_tool.show()
|
self._publisher_tool.show()
|
||||||
|
|
||||||
|
def _show_pyblish_debugger(self):
|
||||||
|
window = DebugUI(parent=self._parent_widget)
|
||||||
|
window.show()
|
||||||
|
|
|
||||||
|
|
@ -353,6 +353,12 @@ class AbstractPublisherFrontend(AbstractPublisherCommon):
|
||||||
):
|
):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def set_instances_active_state(
|
||||||
|
self, active_state_by_id: Dict[str, bool]
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_existing_product_names(self, folder_path: str) -> List[str]:
|
def get_existing_product_names(self, folder_path: str) -> List[str]:
|
||||||
pass
|
pass
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,27 @@ class PublisherController(
|
||||||
Known topics:
|
Known topics:
|
||||||
"show.detailed.help" - Detailed help requested (UI related).
|
"show.detailed.help" - Detailed help requested (UI related).
|
||||||
"show.card.message" - Show card message request (UI related).
|
"show.card.message" - Show card message request (UI related).
|
||||||
"instances.refresh.finished" - Instances are refreshed.
|
# --- Create model ---
|
||||||
|
"create.model.reset" - Reset of create model.
|
||||||
|
"instances.create.failed" - Creation failed.
|
||||||
|
"convertors.convert.failed" - Convertor failed.
|
||||||
|
"instances.save.failed" - Save failed.
|
||||||
|
"instance.thumbnail.changed" - Thumbnail changed.
|
||||||
|
"instances.collection.failed" - Collection of instances failed.
|
||||||
|
"convertors.find.failed" - Convertor find failed.
|
||||||
|
"instances.create.failed" - Create instances failed.
|
||||||
|
"instances.remove.failed" - Remove instances failed.
|
||||||
|
"create.context.added.instance" - Create instance added to context.
|
||||||
|
"create.context.value.changed" - Create instance or context value
|
||||||
|
changed.
|
||||||
|
"create.context.pre.create.attrs.changed" - Pre create attributes
|
||||||
|
changed.
|
||||||
|
"create.context.create.attrs.changed" - Create attributes changed.
|
||||||
|
"create.context.publish.attrs.changed" - Publish attributes changed.
|
||||||
|
"create.context.removed.instance" - Instance removed from context.
|
||||||
|
"create.model.instances.context.changed" - Instances changed context.
|
||||||
|
like folder, task or variant.
|
||||||
|
# --- Publish model ---
|
||||||
"plugins.refresh.finished" - Plugins refreshed.
|
"plugins.refresh.finished" - Plugins refreshed.
|
||||||
"publish.reset.finished" - Reset finished.
|
"publish.reset.finished" - Reset finished.
|
||||||
"controller.reset.started" - Controller reset started.
|
"controller.reset.started" - Controller reset started.
|
||||||
|
|
@ -200,6 +220,9 @@ class PublisherController(
|
||||||
changes_by_instance_id
|
changes_by_instance_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def set_instances_active_state(self, active_state_by_id):
|
||||||
|
self._create_model.set_instances_active_state(active_state_by_id)
|
||||||
|
|
||||||
def get_convertor_items(self):
|
def get_convertor_items(self):
|
||||||
return self._create_model.get_convertor_items()
|
return self._create_model.get_convertor_items()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,21 @@
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from typing import Union, List, Dict, Tuple, Any, Optional, Iterable, Pattern
|
from typing import (
|
||||||
|
Union,
|
||||||
|
List,
|
||||||
|
Dict,
|
||||||
|
Tuple,
|
||||||
|
Any,
|
||||||
|
Optional,
|
||||||
|
Iterable,
|
||||||
|
Pattern,
|
||||||
|
)
|
||||||
|
|
||||||
from ayon_core.lib.attribute_definitions import (
|
from ayon_core.lib.attribute_definitions import (
|
||||||
serialize_attr_defs,
|
serialize_attr_defs,
|
||||||
deserialize_attr_defs,
|
deserialize_attr_defs,
|
||||||
AbstractAttrDef,
|
AbstractAttrDef,
|
||||||
|
EnumDef,
|
||||||
)
|
)
|
||||||
from ayon_core.lib.profiles_filtering import filter_profiles
|
from ayon_core.lib.profiles_filtering import filter_profiles
|
||||||
from ayon_core.lib.attribute_definitions import UIDef
|
from ayon_core.lib.attribute_definitions import UIDef
|
||||||
|
|
@ -17,6 +27,7 @@ from ayon_core.pipeline.create import (
|
||||||
Creator,
|
Creator,
|
||||||
CreateContext,
|
CreateContext,
|
||||||
CreatedInstance,
|
CreatedInstance,
|
||||||
|
AttributeValues,
|
||||||
)
|
)
|
||||||
from ayon_core.pipeline.create import (
|
from ayon_core.pipeline.create import (
|
||||||
CreatorsOperationFailed,
|
CreatorsOperationFailed,
|
||||||
|
|
@ -296,7 +307,88 @@ class InstanceItem:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_attr_defs(
|
||||||
|
attr_def_src: AbstractAttrDef, attr_def_new: AbstractAttrDef
|
||||||
|
) -> Optional[AbstractAttrDef]:
|
||||||
|
if not attr_def_src.enabled and attr_def_new.enabled:
|
||||||
|
attr_def_src.enabled = True
|
||||||
|
if not attr_def_src.visible and attr_def_new.visible:
|
||||||
|
attr_def_src.visible = True
|
||||||
|
|
||||||
|
if not isinstance(attr_def_src, EnumDef):
|
||||||
|
return None
|
||||||
|
if attr_def_src.items == attr_def_new.items:
|
||||||
|
return None
|
||||||
|
|
||||||
|
src_item_values = {
|
||||||
|
item["value"]
|
||||||
|
for item in attr_def_src.items
|
||||||
|
}
|
||||||
|
for item in attr_def_new.items:
|
||||||
|
if item["value"] not in src_item_values:
|
||||||
|
attr_def_src.items.append(item)
|
||||||
|
|
||||||
|
|
||||||
|
def merge_attr_defs(attr_defs: List[List[AbstractAttrDef]]):
|
||||||
|
if not attr_defs:
|
||||||
|
return []
|
||||||
|
if len(attr_defs) == 1:
|
||||||
|
return attr_defs[0]
|
||||||
|
|
||||||
|
# Pop first and create clone of attribute definitions
|
||||||
|
defs_union: List[AbstractAttrDef] = [
|
||||||
|
attr_def.clone()
|
||||||
|
for attr_def in attr_defs.pop(0)
|
||||||
|
]
|
||||||
|
for instance_attr_defs in attr_defs:
|
||||||
|
idx = 0
|
||||||
|
for attr_idx, attr_def in enumerate(instance_attr_defs):
|
||||||
|
# QUESTION should we merge NumberDef too? Use lowest min and
|
||||||
|
# biggest max...
|
||||||
|
is_enum = isinstance(attr_def, EnumDef)
|
||||||
|
match_idx = None
|
||||||
|
match_attr = None
|
||||||
|
for union_idx, union_def in enumerate(defs_union):
|
||||||
|
if is_enum and (
|
||||||
|
not isinstance(union_def, EnumDef)
|
||||||
|
or union_def.multiselection != attr_def.multiselection
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if (
|
||||||
|
attr_def.compare_to_def(
|
||||||
|
union_def,
|
||||||
|
ignore_default=True,
|
||||||
|
ignore_enabled=True,
|
||||||
|
ignore_visible=True,
|
||||||
|
ignore_def_type_compare=is_enum
|
||||||
|
)
|
||||||
|
):
|
||||||
|
match_idx = union_idx
|
||||||
|
match_attr = union_def
|
||||||
|
break
|
||||||
|
|
||||||
|
if match_attr is not None:
|
||||||
|
new_attr_def = _merge_attr_defs(match_attr, attr_def)
|
||||||
|
if new_attr_def is not None:
|
||||||
|
defs_union[match_idx] = new_attr_def
|
||||||
|
idx = match_idx + 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
defs_union.insert(idx, attr_def.clone())
|
||||||
|
idx += 1
|
||||||
|
return defs_union
|
||||||
|
|
||||||
|
|
||||||
class CreateModel:
|
class CreateModel:
|
||||||
|
_CONTEXT_KEYS = {
|
||||||
|
"active",
|
||||||
|
"folderPath",
|
||||||
|
"task",
|
||||||
|
"variant",
|
||||||
|
"productName",
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, controller: AbstractPublisherBackend):
|
def __init__(self, controller: AbstractPublisherBackend):
|
||||||
self._log = None
|
self._log = None
|
||||||
self._controller: AbstractPublisherBackend = controller
|
self._controller: AbstractPublisherBackend = controller
|
||||||
|
|
@ -453,6 +545,27 @@ class CreateModel:
|
||||||
instance = self._get_instance_by_id(instance_id)
|
instance = self._get_instance_by_id(instance_id)
|
||||||
for key, value in changes.items():
|
for key, value in changes.items():
|
||||||
instance[key] = value
|
instance[key] = value
|
||||||
|
self._emit_event(
|
||||||
|
"create.model.instances.context.changed",
|
||||||
|
{
|
||||||
|
"instance_ids": list(changes_by_instance_id.keys())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_instances_active_state(
|
||||||
|
self, active_state_by_id: Dict[str, bool]
|
||||||
|
):
|
||||||
|
with self._create_context.bulk_value_changes(CREATE_EVENT_SOURCE):
|
||||||
|
for instance_id, active in active_state_by_id.items():
|
||||||
|
instance = self._create_context.get_instance_by_id(instance_id)
|
||||||
|
instance["active"] = active
|
||||||
|
|
||||||
|
self._emit_event(
|
||||||
|
"create.model.instances.context.changed",
|
||||||
|
{
|
||||||
|
"instance_ids": set(active_state_by_id.keys())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def get_convertor_items(self) -> Dict[str, ConvertorItem]:
|
def get_convertor_items(self) -> Dict[str, ConvertorItem]:
|
||||||
return self._create_context.convertor_items_by_id
|
return self._create_context.convertor_items_by_id
|
||||||
|
|
@ -643,8 +756,16 @@ class CreateModel:
|
||||||
for instance_id in instance_ids:
|
for instance_id in instance_ids:
|
||||||
instance = self._get_instance_by_id(instance_id)
|
instance = self._get_instance_by_id(instance_id)
|
||||||
creator_attributes = instance["creator_attributes"]
|
creator_attributes = instance["creator_attributes"]
|
||||||
if key in creator_attributes:
|
attr_def = creator_attributes.get_attr_def(key)
|
||||||
creator_attributes[key] = value
|
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
|
||||||
|
|
||||||
def get_creator_attribute_definitions(
|
def get_creator_attribute_definitions(
|
||||||
self, instance_ids: List[str]
|
self, instance_ids: List[str]
|
||||||
|
|
@ -693,6 +814,18 @@ class CreateModel:
|
||||||
else:
|
else:
|
||||||
instance = self._get_instance_by_id(instance_id)
|
instance = self._get_instance_by_id(instance_id)
|
||||||
plugin_val = instance.publish_attributes[plugin_name]
|
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
|
||||||
|
|
||||||
plugin_val[key] = value
|
plugin_val[key] = value
|
||||||
|
|
||||||
def get_publish_attribute_definitions(
|
def get_publish_attribute_definitions(
|
||||||
|
|
@ -725,13 +858,17 @@ class CreateModel:
|
||||||
item_id = None
|
item_id = None
|
||||||
if isinstance(item, CreatedInstance):
|
if isinstance(item, CreatedInstance):
|
||||||
item_id = item.id
|
item_id = item.id
|
||||||
|
|
||||||
for plugin_name, attr_val in item.publish_attributes.items():
|
for plugin_name, attr_val in item.publish_attributes.items():
|
||||||
|
if not isinstance(attr_val, AttributeValues):
|
||||||
|
continue
|
||||||
attr_defs = attr_val.attr_defs
|
attr_defs = attr_val.attr_defs
|
||||||
if not attr_defs:
|
if not attr_defs:
|
||||||
continue
|
continue
|
||||||
|
plugin_attr_defs = all_defs_by_plugin_name.setdefault(
|
||||||
if plugin_name not in all_defs_by_plugin_name:
|
plugin_name, []
|
||||||
all_defs_by_plugin_name[plugin_name] = attr_val.attr_defs
|
)
|
||||||
|
plugin_attr_defs.append(attr_defs)
|
||||||
|
|
||||||
plugin_values = all_plugin_values.setdefault(plugin_name, {})
|
plugin_values = all_plugin_values.setdefault(plugin_name, {})
|
||||||
|
|
||||||
|
|
@ -744,6 +881,10 @@ class CreateModel:
|
||||||
value = attr_val[attr_def.key]
|
value = attr_val[attr_def.key]
|
||||||
attr_values.append((item_id, value))
|
attr_values.append((item_id, value))
|
||||||
|
|
||||||
|
attr_defs_by_plugin_name = {}
|
||||||
|
for plugin_name, attr_defs in all_defs_by_plugin_name.items():
|
||||||
|
attr_defs_by_plugin_name[plugin_name] = merge_attr_defs(attr_defs)
|
||||||
|
|
||||||
output = []
|
output = []
|
||||||
for plugin in self._create_context.plugins_with_defs:
|
for plugin in self._create_context.plugins_with_defs:
|
||||||
plugin_name = plugin.__name__
|
plugin_name = plugin.__name__
|
||||||
|
|
@ -751,7 +892,7 @@ class CreateModel:
|
||||||
continue
|
continue
|
||||||
output.append((
|
output.append((
|
||||||
plugin_name,
|
plugin_name,
|
||||||
all_defs_by_plugin_name[plugin_name],
|
attr_defs_by_plugin_name[plugin_name],
|
||||||
all_plugin_values
|
all_plugin_values
|
||||||
))
|
))
|
||||||
return output
|
return output
|
||||||
|
|
@ -783,8 +924,12 @@ class CreateModel:
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def _emit_event(self, topic: str, data: Optional[Dict[str, Any]] = None):
|
def _emit_event(
|
||||||
self._controller.emit_event(topic, data)
|
self,
|
||||||
|
topic: str,
|
||||||
|
data: Optional[Dict[str, Any]] = None
|
||||||
|
):
|
||||||
|
self._controller.emit_event(topic, data, CREATE_EVENT_SOURCE)
|
||||||
|
|
||||||
def _get_current_project_settings(self) -> Dict[str, Any]:
|
def _get_current_project_settings(self) -> Dict[str, Any]:
|
||||||
"""Current project settings.
|
"""Current project settings.
|
||||||
|
|
@ -933,16 +1078,28 @@ class CreateModel:
|
||||||
return
|
return
|
||||||
|
|
||||||
instance_changes = {}
|
instance_changes = {}
|
||||||
|
context_changed_ids = set()
|
||||||
for item in event.data["changes"]:
|
for item in event.data["changes"]:
|
||||||
instance_id = None
|
instance_id = None
|
||||||
if item["instance"]:
|
if item["instance"]:
|
||||||
instance_id = item["instance"].id
|
instance_id = item["instance"].id
|
||||||
instance_changes[instance_id] = item["changes"]
|
changes = item["changes"]
|
||||||
|
instance_changes[instance_id] = changes
|
||||||
|
if instance_id is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if self._CONTEXT_KEYS.intersection(set(changes)):
|
||||||
|
context_changed_ids.add(instance_id)
|
||||||
|
|
||||||
self._emit_event(
|
self._emit_event(
|
||||||
"create.context.value.changed",
|
"create.context.value.changed",
|
||||||
{"instance_changes": instance_changes},
|
{"instance_changes": instance_changes},
|
||||||
)
|
)
|
||||||
|
if context_changed_ids:
|
||||||
|
self._emit_event(
|
||||||
|
"create.model.instances.context.changed",
|
||||||
|
{"instance_ids": list(context_changed_ids)},
|
||||||
|
)
|
||||||
|
|
||||||
def _cc_pre_create_attr_changed(self, event):
|
def _cc_pre_create_attr_changed(self, event):
|
||||||
identifiers = event["identifiers"]
|
identifiers = event["identifiers"]
|
||||||
|
|
|
||||||
|
|
@ -32,17 +32,20 @@ PLUGIN_ORDER_OFFSET = 0.5
|
||||||
class MessageHandler(logging.Handler):
|
class MessageHandler(logging.Handler):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.records = []
|
self._records = []
|
||||||
|
|
||||||
def clear_records(self):
|
def clear_records(self):
|
||||||
self.records = []
|
self._records = []
|
||||||
|
|
||||||
def emit(self, record):
|
def emit(self, record):
|
||||||
try:
|
try:
|
||||||
record.msg = record.getMessage()
|
record.msg = record.getMessage()
|
||||||
except Exception:
|
except Exception:
|
||||||
record.msg = str(record.msg)
|
record.msg = str(record.msg)
|
||||||
self.records.append(record)
|
self._records.append(record)
|
||||||
|
|
||||||
|
def get_records(self):
|
||||||
|
return self._records
|
||||||
|
|
||||||
|
|
||||||
class PublishErrorInfo:
|
class PublishErrorInfo:
|
||||||
|
|
@ -1328,7 +1331,18 @@ class PublishModel:
|
||||||
plugin, self._publish_context, instance
|
plugin, self._publish_context, instance
|
||||||
)
|
)
|
||||||
if log_handler is not None:
|
if log_handler is not None:
|
||||||
result["records"] = log_handler.records
|
records = log_handler.get_records()
|
||||||
|
exception = result.get("error")
|
||||||
|
if exception is not None and records:
|
||||||
|
last_record = records[-1]
|
||||||
|
if (
|
||||||
|
last_record.name == "pyblish.plugin"
|
||||||
|
and last_record.levelno == logging.ERROR
|
||||||
|
):
|
||||||
|
# Remove last record made by pyblish
|
||||||
|
# - `log.exception(formatted_traceback)`
|
||||||
|
records.pop(-1)
|
||||||
|
result["records"] = records
|
||||||
|
|
||||||
exception = result.get("error")
|
exception = result.get("error")
|
||||||
if exception:
|
if exception:
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ Only one item can be selected at a time.
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import collections
|
import collections
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
from qtpy import QtWidgets, QtCore
|
from qtpy import QtWidgets, QtCore
|
||||||
|
|
||||||
|
|
@ -217,11 +218,18 @@ class InstanceGroupWidget(BaseGroupWidget):
|
||||||
def update_icons(self, group_icons):
|
def update_icons(self, group_icons):
|
||||||
self._group_icons = group_icons
|
self._group_icons = group_icons
|
||||||
|
|
||||||
def update_instance_values(self, context_info_by_id):
|
def update_instance_values(
|
||||||
|
self, context_info_by_id, instance_items_by_id, instance_ids
|
||||||
|
):
|
||||||
"""Trigger update on instance widgets."""
|
"""Trigger update on instance widgets."""
|
||||||
|
|
||||||
for instance_id, widget in self._widgets_by_id.items():
|
for instance_id, widget in self._widgets_by_id.items():
|
||||||
widget.update_instance_values(context_info_by_id[instance_id])
|
if instance_ids is not None and instance_id not in instance_ids:
|
||||||
|
continue
|
||||||
|
widget.update_instance(
|
||||||
|
instance_items_by_id[instance_id],
|
||||||
|
context_info_by_id[instance_id]
|
||||||
|
)
|
||||||
|
|
||||||
def update_instances(self, instances, context_info_by_id):
|
def update_instances(self, instances, context_info_by_id):
|
||||||
"""Update instances for the group.
|
"""Update instances for the group.
|
||||||
|
|
@ -307,8 +315,9 @@ class CardWidget(BaseClickableFrame):
|
||||||
|
|
||||||
def set_selected(self, selected):
|
def set_selected(self, selected):
|
||||||
"""Set card as selected."""
|
"""Set card as selected."""
|
||||||
if selected == self._selected:
|
if selected is self._selected:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._selected = selected
|
self._selected = selected
|
||||||
state = "selected" if selected else ""
|
state = "selected" if selected else ""
|
||||||
self.setProperty("state", state)
|
self.setProperty("state", state)
|
||||||
|
|
@ -391,9 +400,6 @@ class ConvertorItemCardWidget(CardWidget):
|
||||||
self._icon_widget = icon_widget
|
self._icon_widget = icon_widget
|
||||||
self._label_widget = label_widget
|
self._label_widget = label_widget
|
||||||
|
|
||||||
def update_instance_values(self, context_info):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class InstanceCardWidget(CardWidget):
|
class InstanceCardWidget(CardWidget):
|
||||||
"""Card widget representing instance."""
|
"""Card widget representing instance."""
|
||||||
|
|
@ -461,7 +467,7 @@ class InstanceCardWidget(CardWidget):
|
||||||
self._active_checkbox = active_checkbox
|
self._active_checkbox = active_checkbox
|
||||||
self._expand_btn = expand_btn
|
self._expand_btn = expand_btn
|
||||||
|
|
||||||
self.update_instance_values(context_info)
|
self._update_instance_values(context_info)
|
||||||
|
|
||||||
def set_active_toggle_enabled(self, enabled):
|
def set_active_toggle_enabled(self, enabled):
|
||||||
self._active_checkbox.setEnabled(enabled)
|
self._active_checkbox.setEnabled(enabled)
|
||||||
|
|
@ -470,23 +476,16 @@ class InstanceCardWidget(CardWidget):
|
||||||
def is_active(self):
|
def is_active(self):
|
||||||
return self._active_checkbox.isChecked()
|
return self._active_checkbox.isChecked()
|
||||||
|
|
||||||
def set_active(self, new_value):
|
def _set_active(self, new_value):
|
||||||
"""Set instance as active."""
|
"""Set instance as active."""
|
||||||
checkbox_value = self._active_checkbox.isChecked()
|
checkbox_value = self._active_checkbox.isChecked()
|
||||||
instance_value = self.instance.is_active
|
|
||||||
|
|
||||||
# First change instance value and them change checkbox
|
|
||||||
# - prevent to trigger `active_changed` signal
|
|
||||||
if instance_value != new_value:
|
|
||||||
self.instance.is_active = new_value
|
|
||||||
|
|
||||||
if checkbox_value != new_value:
|
if checkbox_value != new_value:
|
||||||
self._active_checkbox.setChecked(new_value)
|
self._active_checkbox.setChecked(new_value)
|
||||||
|
|
||||||
def update_instance(self, instance, context_info):
|
def update_instance(self, instance, context_info):
|
||||||
"""Update instance object and update UI."""
|
"""Update instance object and update UI."""
|
||||||
self.instance = instance
|
self.instance = instance
|
||||||
self.update_instance_values(context_info)
|
self._update_instance_values(context_info)
|
||||||
|
|
||||||
def _validate_context(self, context_info):
|
def _validate_context(self, context_info):
|
||||||
valid = context_info.is_valid
|
valid = context_info.is_valid
|
||||||
|
|
@ -522,10 +521,10 @@ class InstanceCardWidget(CardWidget):
|
||||||
QtCore.Qt.NoTextInteraction
|
QtCore.Qt.NoTextInteraction
|
||||||
)
|
)
|
||||||
|
|
||||||
def update_instance_values(self, context_info):
|
def _update_instance_values(self, context_info):
|
||||||
"""Update instance data"""
|
"""Update instance data"""
|
||||||
self._update_product_name()
|
self._update_product_name()
|
||||||
self.set_active(self.instance.is_active)
|
self._set_active(self.instance.is_active)
|
||||||
self._validate_context(context_info)
|
self._validate_context(context_info)
|
||||||
|
|
||||||
def _set_expanded(self, expanded=None):
|
def _set_expanded(self, expanded=None):
|
||||||
|
|
@ -539,7 +538,6 @@ class InstanceCardWidget(CardWidget):
|
||||||
if new_value == old_value:
|
if new_value == old_value:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.instance.is_active = new_value
|
|
||||||
self.active_changed.emit(self._id, new_value)
|
self.active_changed.emit(self._id, new_value)
|
||||||
|
|
||||||
def _on_expend_clicked(self):
|
def _on_expend_clicked(self):
|
||||||
|
|
@ -596,7 +594,7 @@ class InstanceCardView(AbstractInstanceView):
|
||||||
self._context_widget = None
|
self._context_widget = None
|
||||||
self._convertor_items_group = None
|
self._convertor_items_group = None
|
||||||
self._active_toggle_enabled = True
|
self._active_toggle_enabled = True
|
||||||
self._widgets_by_group = {}
|
self._widgets_by_group: Dict[str, InstanceGroupWidget] = {}
|
||||||
self._ordered_groups = []
|
self._ordered_groups = []
|
||||||
|
|
||||||
self._explicitly_selected_instance_ids = []
|
self._explicitly_selected_instance_ids = []
|
||||||
|
|
@ -625,24 +623,25 @@ class InstanceCardView(AbstractInstanceView):
|
||||||
return
|
return
|
||||||
|
|
||||||
widgets = self._get_selected_widgets()
|
widgets = self._get_selected_widgets()
|
||||||
changed = False
|
active_state_by_id = {}
|
||||||
for widget in widgets:
|
for widget in widgets:
|
||||||
if not isinstance(widget, InstanceCardWidget):
|
if not isinstance(widget, InstanceCardWidget):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
instance_id = widget.id
|
||||||
is_active = widget.is_active
|
is_active = widget.is_active
|
||||||
if value == -1:
|
if value == -1:
|
||||||
widget.set_active(not is_active)
|
active_state_by_id[instance_id] = not is_active
|
||||||
changed = True
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
_value = bool(value)
|
_value = bool(value)
|
||||||
if is_active is not _value:
|
if is_active is not _value:
|
||||||
widget.set_active(_value)
|
active_state_by_id[instance_id] = _value
|
||||||
changed = True
|
|
||||||
|
|
||||||
if changed:
|
if not active_state_by_id:
|
||||||
self.active_changed.emit()
|
return
|
||||||
|
|
||||||
|
self._controller.set_instances_active_state(active_state_by_id)
|
||||||
|
|
||||||
def keyPressEvent(self, event):
|
def keyPressEvent(self, event):
|
||||||
if event.key() == QtCore.Qt.Key_Space:
|
if event.key() == QtCore.Qt.Key_Space:
|
||||||
|
|
@ -702,7 +701,7 @@ class InstanceCardView(AbstractInstanceView):
|
||||||
# Prepare instances by group and identifiers by group
|
# Prepare instances by group and identifiers by group
|
||||||
instances_by_group = collections.defaultdict(list)
|
instances_by_group = collections.defaultdict(list)
|
||||||
identifiers_by_group = collections.defaultdict(set)
|
identifiers_by_group = collections.defaultdict(set)
|
||||||
for instance in self._controller.get_instances():
|
for instance in self._controller.get_instance_items():
|
||||||
group_name = instance.group_label
|
group_name = instance.group_label
|
||||||
instances_by_group[group_name].append(instance)
|
instances_by_group[group_name].append(instance)
|
||||||
identifiers_by_group[group_name].add(
|
identifiers_by_group[group_name].add(
|
||||||
|
|
@ -817,23 +816,31 @@ class InstanceCardView(AbstractInstanceView):
|
||||||
|
|
||||||
self._convertor_items_group.update_items(convertor_items)
|
self._convertor_items_group.update_items(convertor_items)
|
||||||
|
|
||||||
def refresh_instance_states(self):
|
def refresh_instance_states(self, instance_ids=None):
|
||||||
"""Trigger update of instances on group widgets."""
|
"""Trigger update of instances on group widgets."""
|
||||||
|
if instance_ids is not None:
|
||||||
|
instance_ids = set(instance_ids)
|
||||||
context_info_by_id = self._controller.get_instances_context_info()
|
context_info_by_id = self._controller.get_instances_context_info()
|
||||||
|
instance_items_by_id = self._controller.get_instance_items_by_id(
|
||||||
|
instance_ids
|
||||||
|
)
|
||||||
for widget in self._widgets_by_group.values():
|
for widget in self._widgets_by_group.values():
|
||||||
widget.update_instance_values(context_info_by_id)
|
widget.update_instance_values(
|
||||||
|
context_info_by_id, instance_items_by_id, instance_ids
|
||||||
|
)
|
||||||
|
|
||||||
def _on_active_changed(self, group_name, instance_id, value):
|
def _on_active_changed(self, group_name, instance_id, value):
|
||||||
group_widget = self._widgets_by_group[group_name]
|
group_widget = self._widgets_by_group[group_name]
|
||||||
instance_widget = group_widget.get_widget_by_item_id(instance_id)
|
instance_widget = group_widget.get_widget_by_item_id(instance_id)
|
||||||
if instance_widget.is_selected:
|
active_state_by_id = {}
|
||||||
|
if not instance_widget.is_selected:
|
||||||
|
active_state_by_id[instance_id] = value
|
||||||
|
else:
|
||||||
for widget in self._get_selected_widgets():
|
for widget in self._get_selected_widgets():
|
||||||
if isinstance(widget, InstanceCardWidget):
|
if isinstance(widget, InstanceCardWidget):
|
||||||
widget.set_active(value)
|
active_state_by_id[widget.id] = value
|
||||||
else:
|
|
||||||
self._select_item_clear(instance_id, group_name, instance_widget)
|
self._controller.set_instances_active_state(active_state_by_id)
|
||||||
self.selection_changed.emit()
|
|
||||||
self.active_changed.emit()
|
|
||||||
|
|
||||||
def _on_widget_selection(self, instance_id, group_name, selection_type):
|
def _on_widget_selection(self, instance_id, group_name, selection_type):
|
||||||
"""Select specific item by instance id.
|
"""Select specific item by instance id.
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ class ListItemDelegate(QtWidgets.QStyledItemDelegate):
|
||||||
class InstanceListItemWidget(QtWidgets.QWidget):
|
class InstanceListItemWidget(QtWidgets.QWidget):
|
||||||
"""Widget with instance info drawn over delegate paint.
|
"""Widget with instance info drawn over delegate paint.
|
||||||
|
|
||||||
This is required to be able use custom checkbox on custom place.
|
This is required to be able to use custom checkbox on custom place.
|
||||||
"""
|
"""
|
||||||
active_changed = QtCore.Signal(str, bool)
|
active_changed = QtCore.Signal(str, bool)
|
||||||
double_clicked = QtCore.Signal()
|
double_clicked = QtCore.Signal()
|
||||||
|
|
@ -118,7 +118,7 @@ class InstanceListItemWidget(QtWidgets.QWidget):
|
||||||
def __init__(self, instance, context_info, parent):
|
def __init__(self, instance, context_info, parent):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
self.instance = instance
|
self._instance_id = instance.id
|
||||||
|
|
||||||
instance_label = instance.label
|
instance_label = instance.label
|
||||||
if instance_label is None:
|
if instance_label is None:
|
||||||
|
|
@ -171,47 +171,34 @@ class InstanceListItemWidget(QtWidgets.QWidget):
|
||||||
|
|
||||||
def is_active(self):
|
def is_active(self):
|
||||||
"""Instance is activated."""
|
"""Instance is activated."""
|
||||||
return self.instance.is_active
|
return self._active_checkbox.isChecked()
|
||||||
|
|
||||||
def set_active(self, new_value):
|
def set_active(self, new_value):
|
||||||
"""Change active state of instance and checkbox."""
|
"""Change active state of instance and checkbox."""
|
||||||
checkbox_value = self._active_checkbox.isChecked()
|
old_value = self.is_active()
|
||||||
instance_value = self.instance.is_active
|
|
||||||
if new_value is None:
|
if new_value is None:
|
||||||
new_value = not instance_value
|
new_value = not old_value
|
||||||
|
|
||||||
# First change instance value and them change checkbox
|
if new_value != old_value:
|
||||||
# - prevent to trigger `active_changed` signal
|
self._active_checkbox.blockSignals(True)
|
||||||
if instance_value != new_value:
|
|
||||||
self.instance.is_active = new_value
|
|
||||||
|
|
||||||
if checkbox_value != new_value:
|
|
||||||
self._active_checkbox.setChecked(new_value)
|
self._active_checkbox.setChecked(new_value)
|
||||||
|
self._active_checkbox.blockSignals(False)
|
||||||
|
|
||||||
def update_instance(self, instance, context_info):
|
def update_instance(self, instance, context_info):
|
||||||
"""Update instance object."""
|
"""Update instance object."""
|
||||||
self.instance = instance
|
|
||||||
self.update_instance_values(context_info)
|
|
||||||
|
|
||||||
def update_instance_values(self, context_info):
|
|
||||||
"""Update instance data propagated to widgets."""
|
|
||||||
# Check product name
|
# Check product name
|
||||||
label = self.instance.label
|
label = instance.label
|
||||||
if label != self._instance_label_widget.text():
|
if label != self._instance_label_widget.text():
|
||||||
self._instance_label_widget.setText(html_escape(label))
|
self._instance_label_widget.setText(html_escape(label))
|
||||||
# Check active state
|
# Check active state
|
||||||
self.set_active(self.instance.is_active)
|
self.set_active(instance.is_active)
|
||||||
# Check valid states
|
# Check valid states
|
||||||
self._set_valid_property(context_info.is_valid)
|
self._set_valid_property(context_info.is_valid)
|
||||||
|
|
||||||
def _on_active_change(self):
|
def _on_active_change(self):
|
||||||
new_value = self._active_checkbox.isChecked()
|
self.active_changed.emit(
|
||||||
old_value = self.instance.is_active
|
self._instance_id, self._active_checkbox.isChecked()
|
||||||
if new_value == old_value:
|
)
|
||||||
return
|
|
||||||
|
|
||||||
self.instance.is_active = new_value
|
|
||||||
self.active_changed.emit(self.instance.id, new_value)
|
|
||||||
|
|
||||||
def set_active_toggle_enabled(self, enabled):
|
def set_active_toggle_enabled(self, enabled):
|
||||||
self._active_checkbox.setEnabled(enabled)
|
self._active_checkbox.setEnabled(enabled)
|
||||||
|
|
@ -245,8 +232,8 @@ class ListContextWidget(QtWidgets.QFrame):
|
||||||
class InstanceListGroupWidget(QtWidgets.QFrame):
|
class InstanceListGroupWidget(QtWidgets.QFrame):
|
||||||
"""Widget representing group of instances.
|
"""Widget representing group of instances.
|
||||||
|
|
||||||
Has collapse/expand indicator, label of group and checkbox modifying all of
|
Has collapse/expand indicator, label of group and checkbox modifying all
|
||||||
it's children.
|
of its children.
|
||||||
"""
|
"""
|
||||||
expand_changed = QtCore.Signal(str, bool)
|
expand_changed = QtCore.Signal(str, bool)
|
||||||
toggle_requested = QtCore.Signal(str, int)
|
toggle_requested = QtCore.Signal(str, int)
|
||||||
|
|
@ -392,7 +379,7 @@ class InstanceTreeView(QtWidgets.QTreeView):
|
||||||
def _mouse_press(self, event):
|
def _mouse_press(self, event):
|
||||||
"""Store index of pressed group.
|
"""Store index of pressed group.
|
||||||
|
|
||||||
This is to be able change state of group and process mouse
|
This is to be able to change state of group and process mouse
|
||||||
"double click" as 2x "single click".
|
"double click" as 2x "single click".
|
||||||
"""
|
"""
|
||||||
if event.button() != QtCore.Qt.LeftButton:
|
if event.button() != QtCore.Qt.LeftButton:
|
||||||
|
|
@ -588,7 +575,7 @@ class InstanceListView(AbstractInstanceView):
|
||||||
# Prepare instances by their groups
|
# Prepare instances by their groups
|
||||||
instances_by_group_name = collections.defaultdict(list)
|
instances_by_group_name = collections.defaultdict(list)
|
||||||
group_names = set()
|
group_names = set()
|
||||||
for instance in self._controller.get_instances():
|
for instance in self._controller.get_instance_items():
|
||||||
group_label = instance.group_label
|
group_label = instance.group_label
|
||||||
group_names.add(group_label)
|
group_names.add(group_label)
|
||||||
instances_by_group_name[group_label].append(instance)
|
instances_by_group_name[group_label].append(instance)
|
||||||
|
|
@ -612,7 +599,7 @@ class InstanceListView(AbstractInstanceView):
|
||||||
# Mapping of existing instances under group item
|
# Mapping of existing instances under group item
|
||||||
existing_mapping = {}
|
existing_mapping = {}
|
||||||
|
|
||||||
# Get group index to be able get children indexes
|
# Get group index to be able to get children indexes
|
||||||
group_index = self._instance_model.index(
|
group_index = self._instance_model.index(
|
||||||
group_item.row(), group_item.column()
|
group_item.row(), group_item.column()
|
||||||
)
|
)
|
||||||
|
|
@ -873,30 +860,40 @@ class InstanceListView(AbstractInstanceView):
|
||||||
widget = self._group_widgets.pop(group_name)
|
widget = self._group_widgets.pop(group_name)
|
||||||
widget.deleteLater()
|
widget.deleteLater()
|
||||||
|
|
||||||
def refresh_instance_states(self):
|
def refresh_instance_states(self, instance_ids=None):
|
||||||
"""Trigger update of all instances."""
|
"""Trigger update of all instances."""
|
||||||
|
if instance_ids is not None:
|
||||||
|
instance_ids = set(instance_ids)
|
||||||
context_info_by_id = self._controller.get_instances_context_info()
|
context_info_by_id = self._controller.get_instances_context_info()
|
||||||
|
instance_items_by_id = self._controller.get_instance_items_by_id(
|
||||||
|
instance_ids
|
||||||
|
)
|
||||||
for instance_id, widget in self._widgets_by_id.items():
|
for instance_id, widget in self._widgets_by_id.items():
|
||||||
context_info = context_info_by_id[instance_id]
|
if instance_ids is not None and instance_id not in instance_ids:
|
||||||
widget.update_instance_values(context_info)
|
continue
|
||||||
|
widget.update_instance(
|
||||||
|
instance_items_by_id[instance_id],
|
||||||
|
context_info_by_id[instance_id],
|
||||||
|
)
|
||||||
|
|
||||||
def _on_active_changed(self, changed_instance_id, new_value):
|
def _on_active_changed(self, changed_instance_id, new_value):
|
||||||
selected_instance_ids, _, _ = self.get_selected_items()
|
selected_instance_ids, _, _ = self.get_selected_items()
|
||||||
|
|
||||||
selected_ids = set()
|
active_by_id = {}
|
||||||
found = False
|
found = False
|
||||||
for instance_id in selected_instance_ids:
|
for instance_id in selected_instance_ids:
|
||||||
selected_ids.add(instance_id)
|
active_by_id[instance_id] = new_value
|
||||||
if not found and instance_id == changed_instance_id:
|
if not found and instance_id == changed_instance_id:
|
||||||
found = True
|
found = True
|
||||||
|
|
||||||
if not found:
|
if not found:
|
||||||
selected_ids = set()
|
active_by_id = {changed_instance_id: new_value}
|
||||||
selected_ids.add(changed_instance_id)
|
|
||||||
|
|
||||||
self._change_active_instances(selected_ids, new_value)
|
self._controller.set_instances_active_state(active_by_id)
|
||||||
|
|
||||||
|
self._change_active_instances(active_by_id, new_value)
|
||||||
group_names = set()
|
group_names = set()
|
||||||
for instance_id in selected_ids:
|
for instance_id in active_by_id:
|
||||||
group_name = self._group_by_instance_id.get(instance_id)
|
group_name = self._group_by_instance_id.get(instance_id)
|
||||||
if group_name is not None:
|
if group_name is not None:
|
||||||
group_names.add(group_name)
|
group_names.add(group_name)
|
||||||
|
|
@ -908,16 +905,11 @@ class InstanceListView(AbstractInstanceView):
|
||||||
if not instance_ids:
|
if not instance_ids:
|
||||||
return
|
return
|
||||||
|
|
||||||
changed_ids = set()
|
|
||||||
for instance_id in instance_ids:
|
for instance_id in instance_ids:
|
||||||
widget = self._widgets_by_id.get(instance_id)
|
widget = self._widgets_by_id.get(instance_id)
|
||||||
if widget:
|
if widget:
|
||||||
changed_ids.add(instance_id)
|
|
||||||
widget.set_active(new_value)
|
widget.set_active(new_value)
|
||||||
|
|
||||||
if changed_ids:
|
|
||||||
self.active_changed.emit()
|
|
||||||
|
|
||||||
def _on_selection_change(self, *_args):
|
def _on_selection_change(self, *_args):
|
||||||
self.selection_changed.emit()
|
self.selection_changed.emit()
|
||||||
|
|
||||||
|
|
@ -956,14 +948,16 @@ class InstanceListView(AbstractInstanceView):
|
||||||
if not group_item:
|
if not group_item:
|
||||||
return
|
return
|
||||||
|
|
||||||
instance_ids = set()
|
active_by_id = {}
|
||||||
for row in range(group_item.rowCount()):
|
for row in range(group_item.rowCount()):
|
||||||
item = group_item.child(row)
|
item = group_item.child(row)
|
||||||
instance_id = item.data(INSTANCE_ID_ROLE)
|
instance_id = item.data(INSTANCE_ID_ROLE)
|
||||||
if instance_id is not None:
|
if instance_id is not None:
|
||||||
instance_ids.add(instance_id)
|
active_by_id[instance_id] = active
|
||||||
|
|
||||||
self._change_active_instances(instance_ids, active)
|
self._controller.set_instances_active_state(active_by_id)
|
||||||
|
|
||||||
|
self._change_active_instances(active_by_id, active)
|
||||||
|
|
||||||
proxy_index = self._proxy_model.mapFromSource(group_item.index())
|
proxy_index = self._proxy_model.mapFromSource(group_item.index())
|
||||||
if not self._instance_view.isExpanded(proxy_index):
|
if not self._instance_view.isExpanded(proxy_index):
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,6 @@ from .product_info import ProductInfoWidget
|
||||||
|
|
||||||
|
|
||||||
class OverviewWidget(QtWidgets.QFrame):
|
class OverviewWidget(QtWidgets.QFrame):
|
||||||
active_changed = QtCore.Signal()
|
|
||||||
instance_context_changed = QtCore.Signal()
|
|
||||||
create_requested = QtCore.Signal()
|
create_requested = QtCore.Signal()
|
||||||
convert_requested = QtCore.Signal()
|
convert_requested = QtCore.Signal()
|
||||||
publish_tab_requested = QtCore.Signal()
|
publish_tab_requested = QtCore.Signal()
|
||||||
|
|
@ -126,17 +124,7 @@ class OverviewWidget(QtWidgets.QFrame):
|
||||||
product_view_cards.double_clicked.connect(
|
product_view_cards.double_clicked.connect(
|
||||||
self.publish_tab_requested
|
self.publish_tab_requested
|
||||||
)
|
)
|
||||||
# Active instances changed
|
|
||||||
product_list_view.active_changed.connect(
|
|
||||||
self._on_active_changed
|
|
||||||
)
|
|
||||||
product_view_cards.active_changed.connect(
|
|
||||||
self._on_active_changed
|
|
||||||
)
|
|
||||||
# Instance context has changed
|
# Instance context has changed
|
||||||
product_attributes_widget.instance_context_changed.connect(
|
|
||||||
self._on_instance_context_change
|
|
||||||
)
|
|
||||||
product_attributes_widget.convert_requested.connect(
|
product_attributes_widget.convert_requested.connect(
|
||||||
self._on_convert_requested
|
self._on_convert_requested
|
||||||
)
|
)
|
||||||
|
|
@ -163,6 +151,10 @@ class OverviewWidget(QtWidgets.QFrame):
|
||||||
"create.context.removed.instance",
|
"create.context.removed.instance",
|
||||||
self._on_instances_removed
|
self._on_instances_removed
|
||||||
)
|
)
|
||||||
|
controller.register_event_callback(
|
||||||
|
"create.model.instances.context.changed",
|
||||||
|
self._on_instance_context_change
|
||||||
|
)
|
||||||
|
|
||||||
self._product_content_widget = product_content_widget
|
self._product_content_widget = product_content_widget
|
||||||
self._product_content_layout = product_content_layout
|
self._product_content_layout = product_content_layout
|
||||||
|
|
@ -312,11 +304,6 @@ class OverviewWidget(QtWidgets.QFrame):
|
||||||
instances, context_selected, convertor_identifiers
|
instances, context_selected, convertor_identifiers
|
||||||
)
|
)
|
||||||
|
|
||||||
def _on_active_changed(self):
|
|
||||||
if self._refreshing_instances:
|
|
||||||
return
|
|
||||||
self.active_changed.emit()
|
|
||||||
|
|
||||||
def _on_change_anim(self, value):
|
def _on_change_anim(self, value):
|
||||||
self._create_widget.setVisible(True)
|
self._create_widget.setVisible(True)
|
||||||
self._product_attributes_wrap.setVisible(True)
|
self._product_attributes_wrap.setVisible(True)
|
||||||
|
|
@ -362,7 +349,7 @@ class OverviewWidget(QtWidgets.QFrame):
|
||||||
self._current_state == "publish"
|
self._current_state == "publish"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _on_instance_context_change(self):
|
def _on_instance_context_change(self, event):
|
||||||
current_idx = self._product_views_layout.currentIndex()
|
current_idx = self._product_views_layout.currentIndex()
|
||||||
for idx in range(self._product_views_layout.count()):
|
for idx in range(self._product_views_layout.count()):
|
||||||
if idx == current_idx:
|
if idx == current_idx:
|
||||||
|
|
@ -372,9 +359,7 @@ class OverviewWidget(QtWidgets.QFrame):
|
||||||
widget.set_refreshed(False)
|
widget.set_refreshed(False)
|
||||||
|
|
||||||
current_widget = self._product_views_layout.widget(current_idx)
|
current_widget = self._product_views_layout.widget(current_idx)
|
||||||
current_widget.refresh_instance_states()
|
current_widget.refresh_instance_states(event["instance_ids"])
|
||||||
|
|
||||||
self.instance_context_changed.emit()
|
|
||||||
|
|
||||||
def _on_convert_requested(self):
|
def _on_convert_requested(self):
|
||||||
self.convert_requested.emit()
|
self.convert_requested.emit()
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
|
||||||
self._attr_def_id_to_instances[attr_def.id] = instance_ids
|
self._attr_def_id_to_instances[attr_def.id] = instance_ids
|
||||||
self._attr_def_id_to_attr_def[attr_def.id] = attr_def
|
self._attr_def_id_to_attr_def[attr_def.id] = attr_def
|
||||||
|
|
||||||
if attr_def.hidden:
|
if not attr_def.visible:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
expand_cols = 2
|
expand_cols = 2
|
||||||
|
|
@ -282,15 +282,15 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget):
|
||||||
widget = create_widget_for_attr_def(
|
widget = create_widget_for_attr_def(
|
||||||
attr_def, content_widget
|
attr_def, content_widget
|
||||||
)
|
)
|
||||||
hidden_widget = attr_def.hidden
|
visible_widget = attr_def.visible
|
||||||
# Hide unknown values of publish plugins
|
# Hide unknown values of publish plugins
|
||||||
# - The keys in most of the cases does not represent what
|
# - The keys in most of the cases does not represent what
|
||||||
# would label represent
|
# would label represent
|
||||||
if isinstance(attr_def, UnknownDef):
|
if isinstance(attr_def, UnknownDef):
|
||||||
widget.setVisible(False)
|
widget.setVisible(False)
|
||||||
hidden_widget = True
|
visible_widget = False
|
||||||
|
|
||||||
if not hidden_widget:
|
if visible_widget:
|
||||||
expand_cols = 2
|
expand_cols = 2
|
||||||
if attr_def.is_value_def and attr_def.is_label_horizontal:
|
if attr_def.is_value_def and attr_def.is_label_horizontal:
|
||||||
expand_cols = 1
|
expand_cols = 1
|
||||||
|
|
|
||||||
|
|
@ -621,7 +621,6 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
|
||||||
product name: [ immutable ]
|
product name: [ immutable ]
|
||||||
[Submit] [Cancel]
|
[Submit] [Cancel]
|
||||||
"""
|
"""
|
||||||
instance_context_changed = QtCore.Signal()
|
|
||||||
|
|
||||||
multiselection_text = "< Multiselection >"
|
multiselection_text = "< Multiselection >"
|
||||||
unknown_value = "N/A"
|
unknown_value = "N/A"
|
||||||
|
|
@ -775,7 +774,6 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
|
||||||
|
|
||||||
self._controller.set_instances_context_info(changes_by_id)
|
self._controller.set_instances_context_info(changes_by_id)
|
||||||
self._refresh_items()
|
self._refresh_items()
|
||||||
self.instance_context_changed.emit()
|
|
||||||
|
|
||||||
def _on_cancel(self):
|
def _on_cancel(self):
|
||||||
"""Cancel changes and set back to their irigin value."""
|
"""Cancel changes and set back to their irigin value."""
|
||||||
|
|
@ -917,7 +915,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
|
||||||
if instance_id not in self._current_instances_by_id:
|
if instance_id not in self._current_instances_by_id:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for key, attr_name in (
|
for key in (
|
||||||
"folderPath",
|
"folderPath",
|
||||||
"task",
|
"task",
|
||||||
"variant",
|
"variant",
|
||||||
|
|
@ -933,4 +931,3 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
|
||||||
if changed:
|
if changed:
|
||||||
self._refresh_items()
|
self._refresh_items()
|
||||||
self._refresh_content()
|
self._refresh_content()
|
||||||
self.instance_context_changed.emit()
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ class ProductInfoWidget(QtWidgets.QWidget):
|
||||||
│ │ attributes │
|
│ │ attributes │
|
||||||
└───────────────────────────────┘
|
└───────────────────────────────┘
|
||||||
"""
|
"""
|
||||||
instance_context_changed = QtCore.Signal()
|
|
||||||
convert_requested = QtCore.Signal()
|
convert_requested = QtCore.Signal()
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
@ -123,13 +122,14 @@ class ProductInfoWidget(QtWidgets.QWidget):
|
||||||
self._context_selected = False
|
self._context_selected = False
|
||||||
self._all_instances_valid = True
|
self._all_instances_valid = True
|
||||||
|
|
||||||
global_attrs_widget.instance_context_changed.connect(
|
|
||||||
self._on_instance_context_changed
|
|
||||||
)
|
|
||||||
convert_btn.clicked.connect(self._on_convert_click)
|
convert_btn.clicked.connect(self._on_convert_click)
|
||||||
thumbnail_widget.thumbnail_created.connect(self._on_thumbnail_create)
|
thumbnail_widget.thumbnail_created.connect(self._on_thumbnail_create)
|
||||||
thumbnail_widget.thumbnail_cleared.connect(self._on_thumbnail_clear)
|
thumbnail_widget.thumbnail_cleared.connect(self._on_thumbnail_clear)
|
||||||
|
|
||||||
|
controller.register_event_callback(
|
||||||
|
"create.model.instances.context.changed",
|
||||||
|
self._on_instance_context_change
|
||||||
|
)
|
||||||
controller.register_event_callback(
|
controller.register_event_callback(
|
||||||
"instance.thumbnail.changed",
|
"instance.thumbnail.changed",
|
||||||
self._on_thumbnail_changed
|
self._on_thumbnail_changed
|
||||||
|
|
@ -196,7 +196,7 @@ class ProductInfoWidget(QtWidgets.QWidget):
|
||||||
|
|
||||||
self._update_thumbnails()
|
self._update_thumbnails()
|
||||||
|
|
||||||
def _on_instance_context_changed(self):
|
def _on_instance_context_change(self):
|
||||||
instance_ids = {
|
instance_ids = {
|
||||||
instance.id
|
instance.id
|
||||||
for instance in self._current_instances
|
for instance in self._current_instances
|
||||||
|
|
@ -214,8 +214,6 @@ class ProductInfoWidget(QtWidgets.QWidget):
|
||||||
self.creator_attrs_widget.set_instances_valid(all_valid)
|
self.creator_attrs_widget.set_instances_valid(all_valid)
|
||||||
self.publish_attrs_widget.set_instances_valid(all_valid)
|
self.publish_attrs_widget.set_instances_valid(all_valid)
|
||||||
|
|
||||||
self.instance_context_changed.emit()
|
|
||||||
|
|
||||||
def _on_convert_click(self):
|
def _on_convert_click(self):
|
||||||
self.convert_requested.emit()
|
self.convert_requested.emit()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -298,7 +298,6 @@ class ChangeViewBtn(PublishIconBtn):
|
||||||
class AbstractInstanceView(QtWidgets.QWidget):
|
class AbstractInstanceView(QtWidgets.QWidget):
|
||||||
"""Abstract class for instance view in creation part."""
|
"""Abstract class for instance view in creation part."""
|
||||||
selection_changed = QtCore.Signal()
|
selection_changed = QtCore.Signal()
|
||||||
active_changed = QtCore.Signal()
|
|
||||||
# Refreshed attribute is not changed by view itself
|
# Refreshed attribute is not changed by view itself
|
||||||
# - widget which triggers `refresh` is changing the state
|
# - widget which triggers `refresh` is changing the state
|
||||||
# TODO store that information in widget which cares about refreshing
|
# TODO store that information in widget which cares about refreshing
|
||||||
|
|
|
||||||
|
|
@ -253,12 +253,6 @@ class PublisherWindow(QtWidgets.QDialog):
|
||||||
|
|
||||||
help_btn.clicked.connect(self._on_help_click)
|
help_btn.clicked.connect(self._on_help_click)
|
||||||
tabs_widget.tab_changed.connect(self._on_tab_change)
|
tabs_widget.tab_changed.connect(self._on_tab_change)
|
||||||
overview_widget.active_changed.connect(
|
|
||||||
self._on_context_or_active_change
|
|
||||||
)
|
|
||||||
overview_widget.instance_context_changed.connect(
|
|
||||||
self._on_context_or_active_change
|
|
||||||
)
|
|
||||||
overview_widget.create_requested.connect(
|
overview_widget.create_requested.connect(
|
||||||
self._on_create_request
|
self._on_create_request
|
||||||
)
|
)
|
||||||
|
|
@ -281,7 +275,19 @@ class PublisherWindow(QtWidgets.QDialog):
|
||||||
)
|
)
|
||||||
|
|
||||||
controller.register_event_callback(
|
controller.register_event_callback(
|
||||||
"instances.refresh.finished", self._on_instances_refresh
|
"create.model.reset", self._on_create_model_reset
|
||||||
|
)
|
||||||
|
controller.register_event_callback(
|
||||||
|
"create.context.added.instance",
|
||||||
|
self._event_callback_validate_instances
|
||||||
|
)
|
||||||
|
controller.register_event_callback(
|
||||||
|
"create.context.removed.instance",
|
||||||
|
self._event_callback_validate_instances
|
||||||
|
)
|
||||||
|
controller.register_event_callback(
|
||||||
|
"create.model.instances.context.changed",
|
||||||
|
self._event_callback_validate_instances
|
||||||
)
|
)
|
||||||
controller.register_event_callback(
|
controller.register_event_callback(
|
||||||
"publish.reset.finished", self._on_publish_reset
|
"publish.reset.finished", self._on_publish_reset
|
||||||
|
|
@ -936,13 +942,16 @@ class PublisherWindow(QtWidgets.QDialog):
|
||||||
|
|
||||||
self._set_footer_enabled(bool(all_valid))
|
self._set_footer_enabled(bool(all_valid))
|
||||||
|
|
||||||
def _on_instances_refresh(self):
|
def _on_create_model_reset(self):
|
||||||
self._validate_create_instances()
|
self._validate_create_instances()
|
||||||
|
|
||||||
context_title = self._controller.get_context_title()
|
context_title = self._controller.get_context_title()
|
||||||
self.set_context_label(context_title)
|
self.set_context_label(context_title)
|
||||||
self._update_publish_details_widget()
|
self._update_publish_details_widget()
|
||||||
|
|
||||||
|
def _event_callback_validate_instances(self, _event):
|
||||||
|
self._validate_create_instances()
|
||||||
|
|
||||||
def _set_comment_input_visiblity(self, visible):
|
def _set_comment_input_visiblity(self, visible):
|
||||||
self._comment_input.setVisible(visible)
|
self._comment_input.setVisible(visible)
|
||||||
self._footer_spacer.setVisible(not visible)
|
self._footer_spacer.setVisible(not visible)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Package declaring AYON addon 'core' version."""
|
"""Package declaring AYON addon 'core' version."""
|
||||||
__version__ = "1.0.0+dev"
|
__version__ = "1.0.4+dev"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
name = "core"
|
name = "core"
|
||||||
title = "Core"
|
title = "Core"
|
||||||
version = "1.0.0+dev"
|
version = "1.0.4+dev"
|
||||||
|
|
||||||
client_dir = "ayon_core"
|
client_dir = "ayon_core"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "ayon-core"
|
name = "ayon-core"
|
||||||
version = "1.0.0+dev"
|
version = "1.0.4+dev"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Ynput Team <team@ynput.io>"]
|
authors = ["Ynput Team <team@ynput.io>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,255 @@
|
||||||
|
{
|
||||||
|
"OTIO_SCHEMA": "Clip.2",
|
||||||
|
"metadata": {
|
||||||
|
"active": true,
|
||||||
|
"applieswhole": 1,
|
||||||
|
"asset": "sh020",
|
||||||
|
"audio": true,
|
||||||
|
"families": [
|
||||||
|
"clip"
|
||||||
|
],
|
||||||
|
"family": "plate",
|
||||||
|
"handleEnd": 8,
|
||||||
|
"handleStart": 0,
|
||||||
|
"heroTrack": true,
|
||||||
|
"hierarchy": "shots/sq001",
|
||||||
|
"hierarchyData": {
|
||||||
|
"episode": "ep01",
|
||||||
|
"folder": "shots",
|
||||||
|
"sequence": "sq001",
|
||||||
|
"shot": "sh020",
|
||||||
|
"track": "reference"
|
||||||
|
},
|
||||||
|
"hiero_source_type": "TrackItem",
|
||||||
|
"id": "pyblish.avalon.instance",
|
||||||
|
"label": "openpypeData",
|
||||||
|
"note": "OpenPype data container",
|
||||||
|
"parents": [
|
||||||
|
{
|
||||||
|
"entity_name": "shots",
|
||||||
|
"entity_type": "folder"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"entity_name": "sq001",
|
||||||
|
"entity_type": "sequence"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"publish": true,
|
||||||
|
"reviewTrack": null,
|
||||||
|
"sourceResolution": false,
|
||||||
|
"subset": "plateP01",
|
||||||
|
"variant": "Main",
|
||||||
|
"workfileFrameStart": 1001
|
||||||
|
},
|
||||||
|
"name": "sh020",
|
||||||
|
"source_range": {
|
||||||
|
"OTIO_SCHEMA": "TimeRange.1",
|
||||||
|
"duration": {
|
||||||
|
"OTIO_SCHEMA": "RationalTime.1",
|
||||||
|
"rate": 23.976024627685547,
|
||||||
|
"value": 51.0
|
||||||
|
},
|
||||||
|
"start_time": {
|
||||||
|
"OTIO_SCHEMA": "RationalTime.1",
|
||||||
|
"rate": 23.976024627685547,
|
||||||
|
"value": 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"effects": [],
|
||||||
|
"markers": [
|
||||||
|
{
|
||||||
|
"OTIO_SCHEMA": "Marker.2",
|
||||||
|
"metadata": {
|
||||||
|
"active": true,
|
||||||
|
"applieswhole": 1,
|
||||||
|
"asset": "sh020",
|
||||||
|
"audio": true,
|
||||||
|
"families": [
|
||||||
|
"clip"
|
||||||
|
],
|
||||||
|
"family": "plate",
|
||||||
|
"handleEnd": 8,
|
||||||
|
"handleStart": 0,
|
||||||
|
"heroTrack": true,
|
||||||
|
"hierarchy": "shots/sq001",
|
||||||
|
"hierarchyData": {
|
||||||
|
"episode": "ep01",
|
||||||
|
"folder": "shots",
|
||||||
|
"sequence": "sq001",
|
||||||
|
"shot": "sh020",
|
||||||
|
"track": "reference"
|
||||||
|
},
|
||||||
|
"hiero_source_type": "TrackItem",
|
||||||
|
"id": "pyblish.avalon.instance",
|
||||||
|
"label": "openpypeData",
|
||||||
|
"note": "OpenPype data container",
|
||||||
|
"parents": [
|
||||||
|
{
|
||||||
|
"entity_name": "shots",
|
||||||
|
"entity_type": "folder"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"entity_name": "sq001",
|
||||||
|
"entity_type": "sequence"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"publish": true,
|
||||||
|
"reviewTrack": null,
|
||||||
|
"sourceResolution": false,
|
||||||
|
"subset": "plateP01",
|
||||||
|
"variant": "Main",
|
||||||
|
"workfileFrameStart": 1001
|
||||||
|
},
|
||||||
|
"name": "openpypeData",
|
||||||
|
"color": "RED",
|
||||||
|
"marked_range": {
|
||||||
|
"OTIO_SCHEMA": "TimeRange.1",
|
||||||
|
"duration": {
|
||||||
|
"OTIO_SCHEMA": "RationalTime.1",
|
||||||
|
"rate": 23.976024627685547,
|
||||||
|
"value": 0.0
|
||||||
|
},
|
||||||
|
"start_time": {
|
||||||
|
"OTIO_SCHEMA": "RationalTime.1",
|
||||||
|
"rate": 23.976024627685547,
|
||||||
|
"value": 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"OTIO_SCHEMA": "Marker.2",
|
||||||
|
"metadata": {
|
||||||
|
"applieswhole": 1,
|
||||||
|
"family": "task",
|
||||||
|
"hiero_source_type": "TrackItem",
|
||||||
|
"label": "comp",
|
||||||
|
"note": "Compositing",
|
||||||
|
"type": "Compositing"
|
||||||
|
},
|
||||||
|
"name": "comp",
|
||||||
|
"color": "RED",
|
||||||
|
"marked_range": {
|
||||||
|
"OTIO_SCHEMA": "TimeRange.1",
|
||||||
|
"duration": {
|
||||||
|
"OTIO_SCHEMA": "RationalTime.1",
|
||||||
|
"rate": 23.976024627685547,
|
||||||
|
"value": 0.0
|
||||||
|
},
|
||||||
|
"start_time": {
|
||||||
|
"OTIO_SCHEMA": "RationalTime.1",
|
||||||
|
"rate": 23.976024627685547,
|
||||||
|
"value": 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"media_references": {
|
||||||
|
"DEFAULT_MEDIA": {
|
||||||
|
"OTIO_SCHEMA": "ImageSequenceReference.1",
|
||||||
|
"metadata": {
|
||||||
|
"clip.properties.blendfunc": "0",
|
||||||
|
"clip.properties.colourspacename": "default",
|
||||||
|
"clip.properties.domainroot": "",
|
||||||
|
"clip.properties.enabled": "1",
|
||||||
|
"clip.properties.expanded": "1",
|
||||||
|
"clip.properties.opacity": "1",
|
||||||
|
"clip.properties.valuesource": "",
|
||||||
|
"foundry.source.audio": "",
|
||||||
|
"foundry.source.bitmapsize": "0",
|
||||||
|
"foundry.source.bitsperchannel": "0",
|
||||||
|
"foundry.source.channelformat": "integer",
|
||||||
|
"foundry.source.colourtransform": "ACES - ACES2065-1",
|
||||||
|
"foundry.source.duration": "59",
|
||||||
|
"foundry.source.filename": "MER_sq001_sh020_P01.%04d.exr 997-1055",
|
||||||
|
"foundry.source.filesize": "",
|
||||||
|
"foundry.source.fragments": "59",
|
||||||
|
"foundry.source.framerate": "23.98",
|
||||||
|
"foundry.source.fullpath": "",
|
||||||
|
"foundry.source.height": "1080",
|
||||||
|
"foundry.source.layers": "colour",
|
||||||
|
"foundry.source.path": "C:/projects/AY01_VFX_demo/resources/plates/MER_sq001_sh020_P01/MER_sq001_sh020_P01.%04d.exr 997-1055",
|
||||||
|
"foundry.source.pixelAspect": "1",
|
||||||
|
"foundry.source.pixelAspectRatio": "",
|
||||||
|
"foundry.source.pixelformat": "RGBA (Float16) Open Color IO space: 11",
|
||||||
|
"foundry.source.reelID": "",
|
||||||
|
"foundry.source.resolution": "",
|
||||||
|
"foundry.source.samplerate": "Invalid",
|
||||||
|
"foundry.source.shortfilename": "MER_sq001_sh020_P01.%04d.exr 997-1055",
|
||||||
|
"foundry.source.shot": "",
|
||||||
|
"foundry.source.shotDate": "",
|
||||||
|
"foundry.source.startTC": "",
|
||||||
|
"foundry.source.starttime": "997",
|
||||||
|
"foundry.source.timecode": "172800",
|
||||||
|
"foundry.source.umid": "1bf7437a-b446-440c-07c5-7cae7acf4f5e",
|
||||||
|
"foundry.source.umidOriginator": "foundry.source.umid",
|
||||||
|
"foundry.source.width": "1920",
|
||||||
|
"foundry.timeline.autodiskcachemode": "Manual",
|
||||||
|
"foundry.timeline.colorSpace": "ACES - ACES2065-1",
|
||||||
|
"foundry.timeline.duration": "59",
|
||||||
|
"foundry.timeline.framerate": "23.98",
|
||||||
|
"foundry.timeline.outputformat": "",
|
||||||
|
"foundry.timeline.poster": "0",
|
||||||
|
"foundry.timeline.posterLayer": "colour",
|
||||||
|
"foundry.timeline.readParams": "AAAAAQAAAAAAAAAFAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAMAAAAC2VkZ2VfcGl4ZWxzAAAABWludDMyAAAAAAAAABFpZ25vcmVfcGFydF9uYW1lcwAAAARib29sAAAAAAhub3ByZWZpeAAAAARib29sAAAAAB5vZmZzZXRfbmVnYXRpdmVfZGlzcGxheV93aW5kb3cAAAAEYm9vbAE=",
|
||||||
|
"foundry.timeline.samplerate": "Invalid",
|
||||||
|
"isSequence": true,
|
||||||
|
"media.exr.channels": "B:{1 0 1 1},G:{1 0 1 1},R:{1 0 1 1}",
|
||||||
|
"media.exr.compression": "8",
|
||||||
|
"media.exr.compressionName": "DWAA",
|
||||||
|
"media.exr.dataWindow": "0,0,1919,1079",
|
||||||
|
"media.exr.displayWindow": "0,0,1919,1079",
|
||||||
|
"media.exr.dwaCompressionLevel": "90",
|
||||||
|
"media.exr.lineOrder": "0",
|
||||||
|
"media.exr.pixelAspectRatio": "1",
|
||||||
|
"media.exr.screenWindowCenter": "0,0",
|
||||||
|
"media.exr.screenWindowWidth": "1",
|
||||||
|
"media.exr.type": "scanlineimage",
|
||||||
|
"media.exr.version": "1",
|
||||||
|
"media.input.bitsperchannel": "16-bit half float",
|
||||||
|
"media.input.ctime": "2022-04-21 11:56:03",
|
||||||
|
"media.input.filename": "C:/projects/AY01_VFX_demo/resources/plates/MER_sq001_sh020_P01/MER_sq001_sh020_P01.0997.exr",
|
||||||
|
"media.input.filereader": "exr",
|
||||||
|
"media.input.filesize": "1235182",
|
||||||
|
"media.input.frame": "1",
|
||||||
|
"media.input.frame_rate": "23.976",
|
||||||
|
"media.input.height": "1080",
|
||||||
|
"media.input.mtime": "2022-03-06 10:14:41",
|
||||||
|
"media.input.timecode": "02:00:00:00",
|
||||||
|
"media.input.width": "1920",
|
||||||
|
"media.nuke.full_layer_names": "0",
|
||||||
|
"media.nuke.node_hash": "ffffffffffffffff",
|
||||||
|
"media.nuke.version": "12.2v3",
|
||||||
|
"openpype.source.colourtransform": "ACES - ACES2065-1",
|
||||||
|
"openpype.source.height": 1080,
|
||||||
|
"openpype.source.pixelAspect": 1.0,
|
||||||
|
"openpype.source.width": 1920,
|
||||||
|
"padding": 4
|
||||||
|
},
|
||||||
|
"name": "",
|
||||||
|
"available_range": {
|
||||||
|
"OTIO_SCHEMA": "TimeRange.1",
|
||||||
|
"duration": {
|
||||||
|
"OTIO_SCHEMA": "RationalTime.1",
|
||||||
|
"rate": 23.976,
|
||||||
|
"value": 59.0
|
||||||
|
},
|
||||||
|
"start_time": {
|
||||||
|
"OTIO_SCHEMA": "RationalTime.1",
|
||||||
|
"rate": 23.976,
|
||||||
|
"value": 997.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"available_image_bounds": null,
|
||||||
|
"target_url_base": "C:/projects/AY01_VFX_demo/resources/plates/MER_sq001_sh020_P01\\",
|
||||||
|
"name_prefix": "MER_sq001_sh020_P01.",
|
||||||
|
"name_suffix": ".exr",
|
||||||
|
"start_frame": 997,
|
||||||
|
"frame_step": 1,
|
||||||
|
"rate": 23.976,
|
||||||
|
"frame_zero_padding": 4,
|
||||||
|
"missing_frame_policy": "error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active_media_reference_key": "DEFAULT_MEDIA"
|
||||||
|
}
|
||||||
|
|
@ -166,3 +166,24 @@ def test_img_sequence_relative_source_range():
|
||||||
"legacy_img_sequence.json",
|
"legacy_img_sequence.json",
|
||||||
expected_data
|
expected_data
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_img_sequence_conform_to_23_976fps():
|
||||||
|
"""
|
||||||
|
Img sequence clip
|
||||||
|
available files = 997-1047 23.976fps
|
||||||
|
source_range = 997-1055 23.976024627685547fps
|
||||||
|
"""
|
||||||
|
expected_data = {
|
||||||
|
'mediaIn': 997,
|
||||||
|
'mediaOut': 1047,
|
||||||
|
'handleStart': 0,
|
||||||
|
'handleEnd': 8,
|
||||||
|
'speed': 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
_check_expected_retimed_values(
|
||||||
|
"img_seq_23.976_metadata.json",
|
||||||
|
expected_data,
|
||||||
|
handle_start=0,
|
||||||
|
handle_end=8,
|
||||||
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue