mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +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",
|
||||
"aftereffects",
|
||||
"wrap",
|
||||
"openrv"
|
||||
"openrv",
|
||||
"cinema4d"
|
||||
}
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import collections
|
|||
import uuid
|
||||
import json
|
||||
import copy
|
||||
import warnings
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from typing import Any, Optional
|
||||
|
||||
import clique
|
||||
|
||||
|
|
@ -90,6 +92,30 @@ class AbstractAttrDefMeta(ABCMeta):
|
|||
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):
|
||||
"""Abstraction of attribute definition.
|
||||
|
||||
|
|
@ -106,12 +132,14 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta):
|
|||
Args:
|
||||
key (str): Under which key will be attribute value stored.
|
||||
default (Any): Default value of an attribute.
|
||||
label (str): Attribute label.
|
||||
tooltip (str): Attribute tooltip.
|
||||
is_label_horizontal (bool): UI specific argument. Specify if label is
|
||||
next to value input or ahead.
|
||||
hidden (bool): Will be item hidden (for UI purposes).
|
||||
disabled (bool): Item will be visible but disabled (for UI purposes).
|
||||
label (Optional[str]): Attribute label.
|
||||
tooltip (Optional[str]): Attribute tooltip.
|
||||
is_label_horizontal (Optional[bool]): UI specific argument. Specify
|
||||
if label is next to value input or ahead.
|
||||
visible (Optional[bool]): Item is shown to user (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 = []
|
||||
|
|
@ -120,51 +148,105 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
key,
|
||||
default,
|
||||
label=None,
|
||||
tooltip=None,
|
||||
is_label_horizontal=None,
|
||||
hidden=False,
|
||||
disabled=False
|
||||
key: str,
|
||||
default: Any,
|
||||
label: Optional[str] = None,
|
||||
tooltip: Optional[str] = None,
|
||||
is_label_horizontal: Optional[bool] = None,
|
||||
visible: Optional[bool] = None,
|
||||
enabled: Optional[bool] = None,
|
||||
hidden: Optional[bool] = None,
|
||||
disabled: Optional[bool] = None,
|
||||
):
|
||||
if is_label_horizontal is None:
|
||||
is_label_horizontal = True
|
||||
|
||||
if hidden is None:
|
||||
hidden = False
|
||||
enabled = _convert_reversed_attr(
|
||||
enabled, disabled, "enabled", "disabled", True
|
||||
)
|
||||
visible = _convert_reversed_attr(
|
||||
visible, hidden, "visible", "hidden", True
|
||||
)
|
||||
|
||||
self.key = key
|
||||
self.label = label
|
||||
self.tooltip = tooltip
|
||||
self.default = default
|
||||
self.is_label_horizontal = is_label_horizontal
|
||||
self.hidden = hidden
|
||||
self.disabled = disabled
|
||||
self._id = uuid.uuid4().hex
|
||||
self.key: str = key
|
||||
self.label: Optional[str] = label
|
||||
self.tooltip: Optional[str] = tooltip
|
||||
self.default: Any = default
|
||||
self.is_label_horizontal: bool = is_label_horizontal
|
||||
self.visible: bool = visible
|
||||
self.enabled: bool = enabled
|
||||
self._id: str = uuid.uuid4().hex
|
||||
|
||||
self.__init__class__ = AbstractAttrDef
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
def id(self) -> str:
|
||||
return self._id
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, self.__class__):
|
||||
def clone(self):
|
||||
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 (
|
||||
self.key == other.key
|
||||
and self.hidden == other.hidden
|
||||
and self.default == other.default
|
||||
and self.disabled == other.disabled
|
||||
(ignore_default or self.default == other.default)
|
||||
and (ignore_visible or self.visible == other.visible)
|
||||
and (ignore_enabled or self.enabled == other.enabled)
|
||||
)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
@abstractmethod
|
||||
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
|
||||
@abstractmethod
|
||||
def type(self):
|
||||
def type(self) -> str:
|
||||
"""Attribute definition type also used as identifier of class.
|
||||
|
||||
Returns:
|
||||
|
|
@ -198,8 +280,8 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta):
|
|||
"tooltip": self.tooltip,
|
||||
"default": self.default,
|
||||
"is_label_horizontal": self.is_label_horizontal,
|
||||
"hidden": self.hidden,
|
||||
"disabled": self.disabled
|
||||
"visible": self.visible,
|
||||
"enabled": self.enabled
|
||||
}
|
||||
for attr in self.type_attributes:
|
||||
data[attr] = getattr(self, attr)
|
||||
|
|
@ -211,9 +293,15 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta):
|
|||
|
||||
Data can be received using 'serialize' method.
|
||||
"""
|
||||
if "type" in data:
|
||||
data = dict(data)
|
||||
data.pop("type")
|
||||
|
||||
return cls(**data)
|
||||
|
||||
def _def_type_compare(self, other: "AbstractAttrDef") -> bool:
|
||||
return True
|
||||
|
||||
|
||||
# -----------------------------------------
|
||||
# UI attribute definitions won't hold value
|
||||
|
|
@ -223,7 +311,10 @@ class UIDef(AbstractAttrDef):
|
|||
is_value_def = False
|
||||
|
||||
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):
|
||||
return value
|
||||
|
|
@ -237,11 +328,9 @@ class UILabelDef(UIDef):
|
|||
type = "label"
|
||||
|
||||
def __init__(self, label, key=None):
|
||||
super(UILabelDef, self).__init__(label=label, key=key)
|
||||
super().__init__(label=label, key=key)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not super(UILabelDef, self).__eq__(other):
|
||||
return False
|
||||
def _def_type_compare(self, other: "UILabelDef") -> bool:
|
||||
return self.label == other.label
|
||||
|
||||
|
||||
|
|
@ -260,7 +349,10 @@ class UnknownDef(AbstractAttrDef):
|
|||
|
||||
def __init__(self, key, default=None, **kwargs):
|
||||
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):
|
||||
return value
|
||||
|
|
@ -279,8 +371,11 @@ class HiddenDef(AbstractAttrDef):
|
|||
|
||||
def __init__(self, key, default=None, **kwargs):
|
||||
kwargs["default"] = default
|
||||
kwargs["hidden"] = True
|
||||
super(HiddenDef, self).__init__(key, **kwargs)
|
||||
kwargs["visible"] = False
|
||||
super().__init__(key, **kwargs)
|
||||
|
||||
def is_value_valid(self, value: Any) -> bool:
|
||||
return True
|
||||
|
||||
def convert_value(self, value):
|
||||
return value
|
||||
|
|
@ -331,21 +426,21 @@ class NumberDef(AbstractAttrDef):
|
|||
elif default > maximum:
|
||||
default = maximum
|
||||
|
||||
super(NumberDef, self).__init__(key, default=default, **kwargs)
|
||||
super().__init__(key, default=default, **kwargs)
|
||||
|
||||
self.minimum = minimum
|
||||
self.maximum = maximum
|
||||
self.decimals = 0 if decimals is None else decimals
|
||||
|
||||
def __eq__(self, other):
|
||||
if not super(NumberDef, self).__eq__(other):
|
||||
def is_value_valid(self, value: Any) -> bool:
|
||||
if self.decimals == 0:
|
||||
if not isinstance(value, int):
|
||||
return False
|
||||
elif not isinstance(value, float):
|
||||
return False
|
||||
|
||||
return (
|
||||
self.decimals == other.decimals
|
||||
and self.maximum == other.maximum
|
||||
and self.maximum == other.maximum
|
||||
)
|
||||
if self.minimum > value > self.maximum:
|
||||
return False
|
||||
return True
|
||||
|
||||
def convert_value(self, value):
|
||||
if isinstance(value, str):
|
||||
|
|
@ -361,6 +456,13 @@ class NumberDef(AbstractAttrDef):
|
|||
return int(value)
|
||||
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):
|
||||
"""Text definition.
|
||||
|
|
@ -390,7 +492,7 @@ class TextDef(AbstractAttrDef):
|
|||
if default is None:
|
||||
default = ""
|
||||
|
||||
super(TextDef, self).__init__(key, default=default, **kwargs)
|
||||
super().__init__(key, default=default, **kwargs)
|
||||
|
||||
if multiline is None:
|
||||
multiline = False
|
||||
|
|
@ -407,14 +509,12 @@ class TextDef(AbstractAttrDef):
|
|||
self.placeholder = placeholder
|
||||
self.regex = regex
|
||||
|
||||
def __eq__(self, other):
|
||||
if not super(TextDef, self).__eq__(other):
|
||||
def is_value_valid(self, value: Any) -> bool:
|
||||
if not isinstance(value, str):
|
||||
return False
|
||||
|
||||
return (
|
||||
self.multiline == other.multiline
|
||||
and self.regex == other.regex
|
||||
)
|
||||
if self.regex and not self.regex.match(value):
|
||||
return False
|
||||
return True
|
||||
|
||||
def convert_value(self, value):
|
||||
if isinstance(value, str):
|
||||
|
|
@ -422,10 +522,18 @@ class TextDef(AbstractAttrDef):
|
|||
return self.default
|
||||
|
||||
def serialize(self):
|
||||
data = super(TextDef, self).serialize()
|
||||
data = super().serialize()
|
||||
data["regex"] = self.regex.pattern
|
||||
data["multiline"] = self.multiline
|
||||
data["placeholder"] = self.placeholder
|
||||
return data
|
||||
|
||||
def _def_type_compare(self, other: "TextDef") -> bool:
|
||||
return (
|
||||
self.multiline == other.multiline
|
||||
and self.regex == other.regex
|
||||
)
|
||||
|
||||
|
||||
class EnumDef(AbstractAttrDef):
|
||||
"""Enumeration of items.
|
||||
|
|
@ -464,21 +572,12 @@ class EnumDef(AbstractAttrDef):
|
|||
elif default not in item_values:
|
||||
default = next(iter(item_values), None)
|
||||
|
||||
super(EnumDef, self).__init__(key, default=default, **kwargs)
|
||||
super().__init__(key, default=default, **kwargs)
|
||||
|
||||
self.items = items
|
||||
self._item_values = item_values_set
|
||||
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):
|
||||
if not self.multiselection:
|
||||
if value in self._item_values:
|
||||
|
|
@ -489,8 +588,19 @@ class EnumDef(AbstractAttrDef):
|
|||
return copy.deepcopy(self.default)
|
||||
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):
|
||||
data = super(EnumDef, self).serialize()
|
||||
data = super().serialize()
|
||||
data["items"] = copy.deepcopy(self.items)
|
||||
data["multiselection"] = self.multiselection
|
||||
return data
|
||||
|
|
@ -557,6 +667,12 @@ class EnumDef(AbstractAttrDef):
|
|||
|
||||
return output
|
||||
|
||||
def _def_type_compare(self, other: "EnumDef") -> bool:
|
||||
return (
|
||||
self.items == other.items
|
||||
and self.multiselection == other.multiselection
|
||||
)
|
||||
|
||||
|
||||
class BoolDef(AbstractAttrDef):
|
||||
"""Boolean representation.
|
||||
|
|
@ -570,7 +686,10 @@ class BoolDef(AbstractAttrDef):
|
|||
def __init__(self, key, default=None, **kwargs):
|
||||
if default is None:
|
||||
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):
|
||||
if isinstance(value, bool):
|
||||
|
|
@ -868,10 +987,10 @@ class FileDef(AbstractAttrDef):
|
|||
self.extensions = set(extensions)
|
||||
self.allow_sequences = allow_sequences
|
||||
self.extensions_label = extensions_label
|
||||
super(FileDef, self).__init__(key, default=default, **kwargs)
|
||||
super().__init__(key, default=default, **kwargs)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not super(FileDef, self).__eq__(other):
|
||||
if not super().__eq__(other):
|
||||
return False
|
||||
|
||||
return (
|
||||
|
|
@ -881,6 +1000,29 @@ class FileDef(AbstractAttrDef):
|
|||
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):
|
||||
if isinstance(value, (str, dict)):
|
||||
value = [value]
|
||||
|
|
|
|||
|
|
@ -148,6 +148,9 @@ class AttributeValues:
|
|||
for key in self._attr_defs_by_key.keys():
|
||||
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):
|
||||
changes = {}
|
||||
for _key, _value in dict(value).items():
|
||||
|
|
|
|||
|
|
@ -3,11 +3,20 @@ import os
|
|||
import copy
|
||||
import shutil
|
||||
import glob
|
||||
import clique
|
||||
import collections
|
||||
from typing import Dict, Any, Iterable
|
||||
|
||||
import clique
|
||||
import ayon_api
|
||||
|
||||
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):
|
||||
"""Hardlink file if possible(to save space), copy if not.
|
||||
|
|
@ -327,3 +336,71 @@ def deliver_sequence(
|
|||
uploaded += 1
|
||||
|
||||
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
|
||||
# to preserve media range
|
||||
|
||||
# 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)
|
||||
conformed_source_range = otio.opentime.TimeRange(
|
||||
start_time=conformed_src_in,
|
||||
duration=conformed_src_duration
|
||||
)
|
||||
# Compute new source range based on available rate.
|
||||
|
||||
# Backward-compatibility for Hiero OTIO exporter.
|
||||
# NTSC compatibility might introduce floating rates, when these are
|
||||
# not exactly the same (23.976 vs 23.976024627685547)
|
||||
# this will cause precision issue in computation.
|
||||
# Currently round to 2 decimals for comparison,
|
||||
# but this should always rescale after that.
|
||||
rounded_av_rate = round(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
|
||||
time_scalar = 1.
|
||||
|
|
|
|||
|
|
@ -788,15 +788,15 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data,
|
|||
colorspace = product.colorspace
|
||||
break
|
||||
|
||||
if isinstance(files, (list, tuple)):
|
||||
files = [os.path.basename(f) for f in files]
|
||||
if isinstance(collected_files, (list, tuple)):
|
||||
collected_files = [os.path.basename(f) for f in collected_files]
|
||||
else:
|
||||
files = os.path.basename(files)
|
||||
collected_files = os.path.basename(collected_files)
|
||||
|
||||
rep = {
|
||||
"name": ext,
|
||||
"ext": ext,
|
||||
"files": files,
|
||||
"files": collected_files,
|
||||
"frameStart": int(skeleton["frameStartHandle"]),
|
||||
"frameEnd": int(skeleton["frameEndHandle"]),
|
||||
# If expectedFile are absolute, we need only filenames
|
||||
|
|
|
|||
|
|
@ -242,6 +242,26 @@ class LoaderPlugin(list):
|
|||
if hasattr(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):
|
||||
"""Load product into host application
|
||||
|
|
|
|||
|
|
@ -505,21 +505,6 @@ def update_container(container, version=-1):
|
|||
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
|
||||
Loader = _get_container_loader(container)
|
||||
if not Loader:
|
||||
|
|
@ -527,6 +512,39 @@ def update_container(container, version=-1):
|
|||
"Can't update container because loader '{}' was not found."
|
||||
.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)
|
||||
context = {
|
||||
"project": project_entity,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,19 @@
|
|||
import inspect
|
||||
from abc import ABCMeta
|
||||
import typing
|
||||
from typing import Optional
|
||||
|
||||
import pyblish.api
|
||||
import pyblish.logic
|
||||
from pyblish.plugin import MetaPlugin, ExplicitMetaPlugin
|
||||
|
||||
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 (
|
||||
load_help_content_from_plugin,
|
||||
get_errored_instances_from_context,
|
||||
|
|
@ -12,10 +21,8 @@ from .lib import (
|
|||
get_instance_staging_dir,
|
||||
)
|
||||
|
||||
from ayon_core.pipeline.colorspace import (
|
||||
get_colorspace_settings_from_publish_context,
|
||||
set_colorspace_data_to_representation
|
||||
)
|
||||
if typing.TYPE_CHECKING:
|
||||
from ayon_core.pipeline.create import CreateContext, CreatedInstance
|
||||
|
||||
|
||||
class AbstractMetaInstancePlugin(ABCMeta, MetaPlugin):
|
||||
|
|
@ -127,7 +134,9 @@ class AYONPyblishPluginMixin:
|
|||
# callback(self)
|
||||
|
||||
@classmethod
|
||||
def register_create_context_callbacks(cls, create_context):
|
||||
def register_create_context_callbacks(
|
||||
cls, create_context: "CreateContext"
|
||||
):
|
||||
"""Register callbacks for create context.
|
||||
|
||||
It is possible to register callbacks listening to changes happened
|
||||
|
|
@ -160,7 +169,7 @@ class AYONPyblishPluginMixin:
|
|||
return []
|
||||
|
||||
@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.
|
||||
|
||||
Attributes available for all families in plugin's `families` attribute.
|
||||
|
|
@ -177,7 +186,9 @@ class AYONPyblishPluginMixin:
|
|||
return cls.get_attribute_defs()
|
||||
|
||||
@classmethod
|
||||
def instance_matches_plugin_families(cls, instance):
|
||||
def instance_matches_plugin_families(
|
||||
cls, instance: Optional["CreatedInstance"]
|
||||
):
|
||||
"""Check if instance matches families.
|
||||
|
||||
Args:
|
||||
|
|
@ -201,7 +212,9 @@ class AYONPyblishPluginMixin:
|
|||
return False
|
||||
|
||||
@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.
|
||||
|
||||
Attributes available for all families in plugin's `families` attribute.
|
||||
|
|
@ -220,7 +233,9 @@ class AYONPyblishPluginMixin:
|
|||
return cls.get_attribute_defs()
|
||||
|
||||
@classmethod
|
||||
def convert_attribute_values(cls, create_context, instance):
|
||||
def convert_attribute_values(
|
||||
cls, create_context: "CreateContext", instance: "CreatedInstance"
|
||||
):
|
||||
"""Convert attribute values for instance.
|
||||
|
||||
Args:
|
||||
|
|
|
|||
|
|
@ -1,23 +1,22 @@
|
|||
import copy
|
||||
import platform
|
||||
from collections import defaultdict
|
||||
|
||||
import ayon_api
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
||||
from ayon_core.pipeline import load, Anatomy
|
||||
from ayon_core import resources, style
|
||||
|
||||
from ayon_core.lib import (
|
||||
format_file_size,
|
||||
collect_frames,
|
||||
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.delivery import (
|
||||
get_format_dict,
|
||||
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())
|
||||
renumber_frame = self.renumber_frame.isChecked()
|
||||
frame_offset = self.first_frame_start.value()
|
||||
filtered_repres = []
|
||||
repre_ids = set()
|
||||
for repre in self._representations:
|
||||
if repre["name"] not in selected_repres:
|
||||
continue
|
||||
if repre["name"] in selected_repres:
|
||||
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, self.anatomy
|
||||
)
|
||||
|
||||
anatomy_data = copy.deepcopy(repre["context"])
|
||||
new_report_items = check_destination_path(repre["id"],
|
||||
self.anatomy,
|
||||
anatomy_data,
|
||||
datetime_data,
|
||||
template_name)
|
||||
template_data = template_data_by_repre_id[repre["id"]]
|
||||
new_report_items = check_destination_path(
|
||||
repre["id"],
|
||||
self.anatomy,
|
||||
template_data,
|
||||
datetime_data,
|
||||
template_name
|
||||
)
|
||||
|
||||
report_items.update(new_report_items)
|
||||
if new_report_items:
|
||||
|
|
@ -224,7 +234,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog):
|
|||
repre,
|
||||
self.anatomy,
|
||||
template_name,
|
||||
anatomy_data,
|
||||
template_data,
|
||||
format_dict,
|
||||
report_items,
|
||||
self.log
|
||||
|
|
@ -267,9 +277,9 @@ class DeliveryOptionsDialog(QtWidgets.QDialog):
|
|||
|
||||
if frame is not None:
|
||||
if repre["context"].get("frame"):
|
||||
anatomy_data["frame"] = frame
|
||||
template_data["frame"] = frame
|
||||
elif repre["context"].get("udim"):
|
||||
anatomy_data["udim"] = frame
|
||||
template_data["udim"] = frame
|
||||
else:
|
||||
# Fallback
|
||||
self.log.warning(
|
||||
|
|
@ -277,7 +287,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog):
|
|||
" data. Supplying sequence frame to '{frame}'"
|
||||
" formatting data."
|
||||
)
|
||||
anatomy_data["frame"] = frame
|
||||
template_data["frame"] = frame
|
||||
new_report_items, uploaded = deliver_single_file(*args)
|
||||
report_items.update(new_report_items)
|
||||
self._update_progress(uploaded)
|
||||
|
|
@ -342,8 +352,8 @@ class DeliveryOptionsDialog(QtWidgets.QDialog):
|
|||
def _get_selected_repres(self):
|
||||
"""Returns list of representation names filtered from checkboxes."""
|
||||
selected_repres = []
|
||||
for repre_name, chckbox in self._representation_checkboxes.items():
|
||||
if chckbox.isChecked():
|
||||
for repre_name, checkbox in self._representation_checkboxes.items():
|
||||
if checkbox.isChecked():
|
||||
selected_repres.append(repre_name)
|
||||
|
||||
return selected_repres
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
"traypublisher",
|
||||
"substancepainter",
|
||||
"nuke",
|
||||
"aftereffects"
|
||||
"aftereffects",
|
||||
"unreal"
|
||||
]
|
||||
enabled = False
|
||||
|
||||
|
|
|
|||
|
|
@ -28,10 +28,10 @@ from .files_widget import FilesWidget
|
|||
|
||||
def create_widget_for_attr_def(attr_def, parent=None):
|
||||
widget = _create_widget_for_attr_def(attr_def, parent)
|
||||
if attr_def.hidden:
|
||||
if not attr_def.visible:
|
||||
widget.setVisible(False)
|
||||
|
||||
if attr_def.disabled:
|
||||
if not attr_def.enabled:
|
||||
widget.setEnabled(False)
|
||||
return widget
|
||||
|
||||
|
|
@ -135,7 +135,7 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget):
|
|||
widget = create_widget_for_attr_def(attr_def, self)
|
||||
self._widgets.append(widget)
|
||||
|
||||
if attr_def.hidden:
|
||||
if not attr_def.visible:
|
||||
continue
|
||||
|
||||
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
|
||||
from .pyblish_debug_stepper import DebugUI
|
||||
|
||||
# Constant key under which local settings are stored
|
||||
LOCAL_EXPERIMENTAL_KEY = "experimental_tools"
|
||||
|
|
@ -95,6 +96,12 @@ class ExperimentalTools:
|
|||
"hiero",
|
||||
"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)
|
||||
) 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)
|
||||
eperimental_tool.set_enabled(enabled)
|
||||
experimental_tool.set_enabled(enabled)
|
||||
|
||||
def _show_publisher(self):
|
||||
if self._publisher_tool is None:
|
||||
|
|
@ -175,3 +189,7 @@ class ExperimentalTools:
|
|||
)
|
||||
|
||||
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
|
||||
|
||||
@abstractmethod
|
||||
def set_instances_active_state(
|
||||
self, active_state_by_id: Dict[str, bool]
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_existing_product_names(self, folder_path: str) -> List[str]:
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -35,7 +35,27 @@ class PublisherController(
|
|||
Known topics:
|
||||
"show.detailed.help" - Detailed help requested (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.
|
||||
"publish.reset.finished" - Reset finished.
|
||||
"controller.reset.started" - Controller reset started.
|
||||
|
|
@ -200,6 +220,9 @@ class PublisherController(
|
|||
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):
|
||||
return self._create_model.get_convertor_items()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,21 @@
|
|||
import logging
|
||||
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 (
|
||||
serialize_attr_defs,
|
||||
deserialize_attr_defs,
|
||||
AbstractAttrDef,
|
||||
EnumDef,
|
||||
)
|
||||
from ayon_core.lib.profiles_filtering import filter_profiles
|
||||
from ayon_core.lib.attribute_definitions import UIDef
|
||||
|
|
@ -17,6 +27,7 @@ from ayon_core.pipeline.create import (
|
|||
Creator,
|
||||
CreateContext,
|
||||
CreatedInstance,
|
||||
AttributeValues,
|
||||
)
|
||||
from ayon_core.pipeline.create import (
|
||||
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:
|
||||
_CONTEXT_KEYS = {
|
||||
"active",
|
||||
"folderPath",
|
||||
"task",
|
||||
"variant",
|
||||
"productName",
|
||||
}
|
||||
|
||||
def __init__(self, controller: AbstractPublisherBackend):
|
||||
self._log = None
|
||||
self._controller: AbstractPublisherBackend = controller
|
||||
|
|
@ -453,6 +545,27 @@ class CreateModel:
|
|||
instance = self._get_instance_by_id(instance_id)
|
||||
for key, value in changes.items():
|
||||
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]:
|
||||
return self._create_context.convertor_items_by_id
|
||||
|
|
@ -643,8 +756,16 @@ class CreateModel:
|
|||
for instance_id in instance_ids:
|
||||
instance = self._get_instance_by_id(instance_id)
|
||||
creator_attributes = instance["creator_attributes"]
|
||||
if key in creator_attributes:
|
||||
creator_attributes[key] = value
|
||||
attr_def = creator_attributes.get_attr_def(key)
|
||||
if (
|
||||
attr_def is None
|
||||
or not attr_def.is_value_def
|
||||
or not attr_def.visible
|
||||
or not attr_def.enabled
|
||||
or not attr_def.is_value_valid(value)
|
||||
):
|
||||
continue
|
||||
creator_attributes[key] = value
|
||||
|
||||
def get_creator_attribute_definitions(
|
||||
self, instance_ids: List[str]
|
||||
|
|
@ -693,6 +814,18 @@ class CreateModel:
|
|||
else:
|
||||
instance = self._get_instance_by_id(instance_id)
|
||||
plugin_val = instance.publish_attributes[plugin_name]
|
||||
attr_def = plugin_val.get_attr_def(key)
|
||||
# Ignore if attribute is not available or enabled/visible
|
||||
# on the instance, or the value is not valid for definition
|
||||
if (
|
||||
attr_def is None
|
||||
or not attr_def.is_value_def
|
||||
or not attr_def.visible
|
||||
or not attr_def.enabled
|
||||
or not attr_def.is_value_valid(value)
|
||||
):
|
||||
continue
|
||||
|
||||
plugin_val[key] = value
|
||||
|
||||
def get_publish_attribute_definitions(
|
||||
|
|
@ -725,13 +858,17 @@ class CreateModel:
|
|||
item_id = None
|
||||
if isinstance(item, CreatedInstance):
|
||||
item_id = item.id
|
||||
|
||||
for plugin_name, attr_val in item.publish_attributes.items():
|
||||
if not isinstance(attr_val, AttributeValues):
|
||||
continue
|
||||
attr_defs = attr_val.attr_defs
|
||||
if not attr_defs:
|
||||
continue
|
||||
|
||||
if plugin_name not in all_defs_by_plugin_name:
|
||||
all_defs_by_plugin_name[plugin_name] = attr_val.attr_defs
|
||||
plugin_attr_defs = all_defs_by_plugin_name.setdefault(
|
||||
plugin_name, []
|
||||
)
|
||||
plugin_attr_defs.append(attr_defs)
|
||||
|
||||
plugin_values = all_plugin_values.setdefault(plugin_name, {})
|
||||
|
||||
|
|
@ -744,6 +881,10 @@ class CreateModel:
|
|||
value = attr_val[attr_def.key]
|
||||
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 = []
|
||||
for plugin in self._create_context.plugins_with_defs:
|
||||
plugin_name = plugin.__name__
|
||||
|
|
@ -751,7 +892,7 @@ class CreateModel:
|
|||
continue
|
||||
output.append((
|
||||
plugin_name,
|
||||
all_defs_by_plugin_name[plugin_name],
|
||||
attr_defs_by_plugin_name[plugin_name],
|
||||
all_plugin_values
|
||||
))
|
||||
return output
|
||||
|
|
@ -783,8 +924,12 @@ class CreateModel:
|
|||
}
|
||||
)
|
||||
|
||||
def _emit_event(self, topic: str, data: Optional[Dict[str, Any]] = None):
|
||||
self._controller.emit_event(topic, data)
|
||||
def _emit_event(
|
||||
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]:
|
||||
"""Current project settings.
|
||||
|
|
@ -933,16 +1078,28 @@ class CreateModel:
|
|||
return
|
||||
|
||||
instance_changes = {}
|
||||
context_changed_ids = set()
|
||||
for item in event.data["changes"]:
|
||||
instance_id = None
|
||||
if item["instance"]:
|
||||
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(
|
||||
"create.context.value.changed",
|
||||
{"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):
|
||||
identifiers = event["identifiers"]
|
||||
|
|
|
|||
|
|
@ -32,17 +32,20 @@ PLUGIN_ORDER_OFFSET = 0.5
|
|||
class MessageHandler(logging.Handler):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.records = []
|
||||
self._records = []
|
||||
|
||||
def clear_records(self):
|
||||
self.records = []
|
||||
self._records = []
|
||||
|
||||
def emit(self, record):
|
||||
try:
|
||||
record.msg = record.getMessage()
|
||||
except Exception:
|
||||
record.msg = str(record.msg)
|
||||
self.records.append(record)
|
||||
self._records.append(record)
|
||||
|
||||
def get_records(self):
|
||||
return self._records
|
||||
|
||||
|
||||
class PublishErrorInfo:
|
||||
|
|
@ -1328,7 +1331,18 @@ class PublishModel:
|
|||
plugin, self._publish_context, instance
|
||||
)
|
||||
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")
|
||||
if exception:
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ Only one item can be selected at a time.
|
|||
|
||||
import re
|
||||
import collections
|
||||
from typing import Dict
|
||||
|
||||
from qtpy import QtWidgets, QtCore
|
||||
|
||||
|
|
@ -217,11 +218,18 @@ class InstanceGroupWidget(BaseGroupWidget):
|
|||
def update_icons(self, 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."""
|
||||
|
||||
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):
|
||||
"""Update instances for the group.
|
||||
|
|
@ -307,8 +315,9 @@ class CardWidget(BaseClickableFrame):
|
|||
|
||||
def set_selected(self, selected):
|
||||
"""Set card as selected."""
|
||||
if selected == self._selected:
|
||||
if selected is self._selected:
|
||||
return
|
||||
|
||||
self._selected = selected
|
||||
state = "selected" if selected else ""
|
||||
self.setProperty("state", state)
|
||||
|
|
@ -391,9 +400,6 @@ class ConvertorItemCardWidget(CardWidget):
|
|||
self._icon_widget = icon_widget
|
||||
self._label_widget = label_widget
|
||||
|
||||
def update_instance_values(self, context_info):
|
||||
pass
|
||||
|
||||
|
||||
class InstanceCardWidget(CardWidget):
|
||||
"""Card widget representing instance."""
|
||||
|
|
@ -461,7 +467,7 @@ class InstanceCardWidget(CardWidget):
|
|||
self._active_checkbox = active_checkbox
|
||||
self._expand_btn = expand_btn
|
||||
|
||||
self.update_instance_values(context_info)
|
||||
self._update_instance_values(context_info)
|
||||
|
||||
def set_active_toggle_enabled(self, enabled):
|
||||
self._active_checkbox.setEnabled(enabled)
|
||||
|
|
@ -470,23 +476,16 @@ class InstanceCardWidget(CardWidget):
|
|||
def is_active(self):
|
||||
return self._active_checkbox.isChecked()
|
||||
|
||||
def set_active(self, new_value):
|
||||
def _set_active(self, new_value):
|
||||
"""Set instance as active."""
|
||||
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:
|
||||
self._active_checkbox.setChecked(new_value)
|
||||
|
||||
def update_instance(self, instance, context_info):
|
||||
"""Update instance object and update UI."""
|
||||
self.instance = instance
|
||||
self.update_instance_values(context_info)
|
||||
self._update_instance_values(context_info)
|
||||
|
||||
def _validate_context(self, context_info):
|
||||
valid = context_info.is_valid
|
||||
|
|
@ -522,10 +521,10 @@ class InstanceCardWidget(CardWidget):
|
|||
QtCore.Qt.NoTextInteraction
|
||||
)
|
||||
|
||||
def update_instance_values(self, context_info):
|
||||
def _update_instance_values(self, context_info):
|
||||
"""Update instance data"""
|
||||
self._update_product_name()
|
||||
self.set_active(self.instance.is_active)
|
||||
self._set_active(self.instance.is_active)
|
||||
self._validate_context(context_info)
|
||||
|
||||
def _set_expanded(self, expanded=None):
|
||||
|
|
@ -539,7 +538,6 @@ class InstanceCardWidget(CardWidget):
|
|||
if new_value == old_value:
|
||||
return
|
||||
|
||||
self.instance.is_active = new_value
|
||||
self.active_changed.emit(self._id, new_value)
|
||||
|
||||
def _on_expend_clicked(self):
|
||||
|
|
@ -596,7 +594,7 @@ class InstanceCardView(AbstractInstanceView):
|
|||
self._context_widget = None
|
||||
self._convertor_items_group = None
|
||||
self._active_toggle_enabled = True
|
||||
self._widgets_by_group = {}
|
||||
self._widgets_by_group: Dict[str, InstanceGroupWidget] = {}
|
||||
self._ordered_groups = []
|
||||
|
||||
self._explicitly_selected_instance_ids = []
|
||||
|
|
@ -625,24 +623,25 @@ class InstanceCardView(AbstractInstanceView):
|
|||
return
|
||||
|
||||
widgets = self._get_selected_widgets()
|
||||
changed = False
|
||||
active_state_by_id = {}
|
||||
for widget in widgets:
|
||||
if not isinstance(widget, InstanceCardWidget):
|
||||
continue
|
||||
|
||||
instance_id = widget.id
|
||||
is_active = widget.is_active
|
||||
if value == -1:
|
||||
widget.set_active(not is_active)
|
||||
changed = True
|
||||
active_state_by_id[instance_id] = not is_active
|
||||
continue
|
||||
|
||||
_value = bool(value)
|
||||
if is_active is not _value:
|
||||
widget.set_active(_value)
|
||||
changed = True
|
||||
active_state_by_id[instance_id] = _value
|
||||
|
||||
if changed:
|
||||
self.active_changed.emit()
|
||||
if not active_state_by_id:
|
||||
return
|
||||
|
||||
self._controller.set_instances_active_state(active_state_by_id)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
if event.key() == QtCore.Qt.Key_Space:
|
||||
|
|
@ -702,7 +701,7 @@ class InstanceCardView(AbstractInstanceView):
|
|||
# Prepare instances by group and identifiers by group
|
||||
instances_by_group = collections.defaultdict(list)
|
||||
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
|
||||
instances_by_group[group_name].append(instance)
|
||||
identifiers_by_group[group_name].add(
|
||||
|
|
@ -817,23 +816,31 @@ class InstanceCardView(AbstractInstanceView):
|
|||
|
||||
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."""
|
||||
if instance_ids is not None:
|
||||
instance_ids = set(instance_ids)
|
||||
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():
|
||||
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):
|
||||
group_widget = self._widgets_by_group[group_name]
|
||||
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():
|
||||
if isinstance(widget, InstanceCardWidget):
|
||||
widget.set_active(value)
|
||||
else:
|
||||
self._select_item_clear(instance_id, group_name, instance_widget)
|
||||
self.selection_changed.emit()
|
||||
self.active_changed.emit()
|
||||
active_state_by_id[widget.id] = value
|
||||
|
||||
self._controller.set_instances_active_state(active_state_by_id)
|
||||
|
||||
def _on_widget_selection(self, instance_id, group_name, selection_type):
|
||||
"""Select specific item by instance id.
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ class ListItemDelegate(QtWidgets.QStyledItemDelegate):
|
|||
class InstanceListItemWidget(QtWidgets.QWidget):
|
||||
"""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)
|
||||
double_clicked = QtCore.Signal()
|
||||
|
|
@ -118,7 +118,7 @@ class InstanceListItemWidget(QtWidgets.QWidget):
|
|||
def __init__(self, instance, context_info, parent):
|
||||
super().__init__(parent)
|
||||
|
||||
self.instance = instance
|
||||
self._instance_id = instance.id
|
||||
|
||||
instance_label = instance.label
|
||||
if instance_label is None:
|
||||
|
|
@ -171,47 +171,34 @@ class InstanceListItemWidget(QtWidgets.QWidget):
|
|||
|
||||
def is_active(self):
|
||||
"""Instance is activated."""
|
||||
return self.instance.is_active
|
||||
return self._active_checkbox.isChecked()
|
||||
|
||||
def set_active(self, new_value):
|
||||
"""Change active state of instance and checkbox."""
|
||||
checkbox_value = self._active_checkbox.isChecked()
|
||||
instance_value = self.instance.is_active
|
||||
old_value = self.is_active()
|
||||
if new_value is None:
|
||||
new_value = not instance_value
|
||||
new_value = not old_value
|
||||
|
||||
# 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 new_value != old_value:
|
||||
self._active_checkbox.blockSignals(True)
|
||||
self._active_checkbox.setChecked(new_value)
|
||||
self._active_checkbox.blockSignals(False)
|
||||
|
||||
def update_instance(self, instance, context_info):
|
||||
"""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
|
||||
label = self.instance.label
|
||||
label = instance.label
|
||||
if label != self._instance_label_widget.text():
|
||||
self._instance_label_widget.setText(html_escape(label))
|
||||
# Check active state
|
||||
self.set_active(self.instance.is_active)
|
||||
self.set_active(instance.is_active)
|
||||
# Check valid states
|
||||
self._set_valid_property(context_info.is_valid)
|
||||
|
||||
def _on_active_change(self):
|
||||
new_value = self._active_checkbox.isChecked()
|
||||
old_value = self.instance.is_active
|
||||
if new_value == old_value:
|
||||
return
|
||||
|
||||
self.instance.is_active = new_value
|
||||
self.active_changed.emit(self.instance.id, new_value)
|
||||
self.active_changed.emit(
|
||||
self._instance_id, self._active_checkbox.isChecked()
|
||||
)
|
||||
|
||||
def set_active_toggle_enabled(self, enabled):
|
||||
self._active_checkbox.setEnabled(enabled)
|
||||
|
|
@ -245,8 +232,8 @@ class ListContextWidget(QtWidgets.QFrame):
|
|||
class InstanceListGroupWidget(QtWidgets.QFrame):
|
||||
"""Widget representing group of instances.
|
||||
|
||||
Has collapse/expand indicator, label of group and checkbox modifying all of
|
||||
it's children.
|
||||
Has collapse/expand indicator, label of group and checkbox modifying all
|
||||
of its children.
|
||||
"""
|
||||
expand_changed = QtCore.Signal(str, bool)
|
||||
toggle_requested = QtCore.Signal(str, int)
|
||||
|
|
@ -392,7 +379,7 @@ class InstanceTreeView(QtWidgets.QTreeView):
|
|||
def _mouse_press(self, event):
|
||||
"""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".
|
||||
"""
|
||||
if event.button() != QtCore.Qt.LeftButton:
|
||||
|
|
@ -588,7 +575,7 @@ class InstanceListView(AbstractInstanceView):
|
|||
# Prepare instances by their groups
|
||||
instances_by_group_name = collections.defaultdict(list)
|
||||
group_names = set()
|
||||
for instance in self._controller.get_instances():
|
||||
for instance in self._controller.get_instance_items():
|
||||
group_label = instance.group_label
|
||||
group_names.add(group_label)
|
||||
instances_by_group_name[group_label].append(instance)
|
||||
|
|
@ -612,7 +599,7 @@ class InstanceListView(AbstractInstanceView):
|
|||
# Mapping of existing instances under group item
|
||||
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_item.row(), group_item.column()
|
||||
)
|
||||
|
|
@ -873,30 +860,40 @@ class InstanceListView(AbstractInstanceView):
|
|||
widget = self._group_widgets.pop(group_name)
|
||||
widget.deleteLater()
|
||||
|
||||
def refresh_instance_states(self):
|
||||
def refresh_instance_states(self, instance_ids=None):
|
||||
"""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()
|
||||
instance_items_by_id = self._controller.get_instance_items_by_id(
|
||||
instance_ids
|
||||
)
|
||||
for instance_id, widget in self._widgets_by_id.items():
|
||||
context_info = context_info_by_id[instance_id]
|
||||
widget.update_instance_values(context_info)
|
||||
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 _on_active_changed(self, changed_instance_id, new_value):
|
||||
selected_instance_ids, _, _ = self.get_selected_items()
|
||||
|
||||
selected_ids = set()
|
||||
active_by_id = {}
|
||||
found = False
|
||||
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:
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
selected_ids = set()
|
||||
selected_ids.add(changed_instance_id)
|
||||
active_by_id = {changed_instance_id: new_value}
|
||||
|
||||
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()
|
||||
for instance_id in selected_ids:
|
||||
for instance_id in active_by_id:
|
||||
group_name = self._group_by_instance_id.get(instance_id)
|
||||
if group_name is not None:
|
||||
group_names.add(group_name)
|
||||
|
|
@ -908,16 +905,11 @@ class InstanceListView(AbstractInstanceView):
|
|||
if not instance_ids:
|
||||
return
|
||||
|
||||
changed_ids = set()
|
||||
for instance_id in instance_ids:
|
||||
widget = self._widgets_by_id.get(instance_id)
|
||||
if widget:
|
||||
changed_ids.add(instance_id)
|
||||
widget.set_active(new_value)
|
||||
|
||||
if changed_ids:
|
||||
self.active_changed.emit()
|
||||
|
||||
def _on_selection_change(self, *_args):
|
||||
self.selection_changed.emit()
|
||||
|
||||
|
|
@ -956,14 +948,16 @@ class InstanceListView(AbstractInstanceView):
|
|||
if not group_item:
|
||||
return
|
||||
|
||||
instance_ids = set()
|
||||
active_by_id = {}
|
||||
for row in range(group_item.rowCount()):
|
||||
item = group_item.child(row)
|
||||
instance_id = item.data(INSTANCE_ID_ROLE)
|
||||
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())
|
||||
if not self._instance_view.isExpanded(proxy_index):
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@ from .product_info import ProductInfoWidget
|
|||
|
||||
|
||||
class OverviewWidget(QtWidgets.QFrame):
|
||||
active_changed = QtCore.Signal()
|
||||
instance_context_changed = QtCore.Signal()
|
||||
create_requested = QtCore.Signal()
|
||||
convert_requested = QtCore.Signal()
|
||||
publish_tab_requested = QtCore.Signal()
|
||||
|
|
@ -126,17 +124,7 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
product_view_cards.double_clicked.connect(
|
||||
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
|
||||
product_attributes_widget.instance_context_changed.connect(
|
||||
self._on_instance_context_change
|
||||
)
|
||||
product_attributes_widget.convert_requested.connect(
|
||||
self._on_convert_requested
|
||||
)
|
||||
|
|
@ -163,6 +151,10 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
"create.context.removed.instance",
|
||||
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_layout = product_content_layout
|
||||
|
|
@ -312,11 +304,6 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
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):
|
||||
self._create_widget.setVisible(True)
|
||||
self._product_attributes_wrap.setVisible(True)
|
||||
|
|
@ -362,7 +349,7 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
self._current_state == "publish"
|
||||
)
|
||||
|
||||
def _on_instance_context_change(self):
|
||||
def _on_instance_context_change(self, event):
|
||||
current_idx = self._product_views_layout.currentIndex()
|
||||
for idx in range(self._product_views_layout.count()):
|
||||
if idx == current_idx:
|
||||
|
|
@ -372,9 +359,7 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
widget.set_refreshed(False)
|
||||
|
||||
current_widget = self._product_views_layout.widget(current_idx)
|
||||
current_widget.refresh_instance_states()
|
||||
|
||||
self.instance_context_changed.emit()
|
||||
current_widget.refresh_instance_states(event["instance_ids"])
|
||||
|
||||
def _on_convert_requested(self):
|
||||
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_attr_def[attr_def.id] = attr_def
|
||||
|
||||
if attr_def.hidden:
|
||||
if not attr_def.visible:
|
||||
continue
|
||||
|
||||
expand_cols = 2
|
||||
|
|
@ -282,15 +282,15 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget):
|
|||
widget = create_widget_for_attr_def(
|
||||
attr_def, content_widget
|
||||
)
|
||||
hidden_widget = attr_def.hidden
|
||||
visible_widget = attr_def.visible
|
||||
# Hide unknown values of publish plugins
|
||||
# - The keys in most of the cases does not represent what
|
||||
# would label represent
|
||||
if isinstance(attr_def, UnknownDef):
|
||||
widget.setVisible(False)
|
||||
hidden_widget = True
|
||||
visible_widget = False
|
||||
|
||||
if not hidden_widget:
|
||||
if visible_widget:
|
||||
expand_cols = 2
|
||||
if attr_def.is_value_def and attr_def.is_label_horizontal:
|
||||
expand_cols = 1
|
||||
|
|
|
|||
|
|
@ -621,7 +621,6 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
|
|||
product name: [ immutable ]
|
||||
[Submit] [Cancel]
|
||||
"""
|
||||
instance_context_changed = QtCore.Signal()
|
||||
|
||||
multiselection_text = "< Multiselection >"
|
||||
unknown_value = "N/A"
|
||||
|
|
@ -775,7 +774,6 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
|
|||
|
||||
self._controller.set_instances_context_info(changes_by_id)
|
||||
self._refresh_items()
|
||||
self.instance_context_changed.emit()
|
||||
|
||||
def _on_cancel(self):
|
||||
"""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:
|
||||
continue
|
||||
|
||||
for key, attr_name in (
|
||||
for key in (
|
||||
"folderPath",
|
||||
"task",
|
||||
"variant",
|
||||
|
|
@ -933,4 +931,3 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
|
|||
if changed:
|
||||
self._refresh_items()
|
||||
self._refresh_content()
|
||||
self.instance_context_changed.emit()
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ class ProductInfoWidget(QtWidgets.QWidget):
|
|||
│ │ attributes │
|
||||
└───────────────────────────────┘
|
||||
"""
|
||||
instance_context_changed = QtCore.Signal()
|
||||
convert_requested = QtCore.Signal()
|
||||
|
||||
def __init__(
|
||||
|
|
@ -123,13 +122,14 @@ class ProductInfoWidget(QtWidgets.QWidget):
|
|||
self._context_selected = False
|
||||
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)
|
||||
thumbnail_widget.thumbnail_created.connect(self._on_thumbnail_create)
|
||||
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(
|
||||
"instance.thumbnail.changed",
|
||||
self._on_thumbnail_changed
|
||||
|
|
@ -196,7 +196,7 @@ class ProductInfoWidget(QtWidgets.QWidget):
|
|||
|
||||
self._update_thumbnails()
|
||||
|
||||
def _on_instance_context_changed(self):
|
||||
def _on_instance_context_change(self):
|
||||
instance_ids = {
|
||||
instance.id
|
||||
for instance in self._current_instances
|
||||
|
|
@ -214,8 +214,6 @@ class ProductInfoWidget(QtWidgets.QWidget):
|
|||
self.creator_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):
|
||||
self.convert_requested.emit()
|
||||
|
||||
|
|
|
|||
|
|
@ -298,7 +298,6 @@ class ChangeViewBtn(PublishIconBtn):
|
|||
class AbstractInstanceView(QtWidgets.QWidget):
|
||||
"""Abstract class for instance view in creation part."""
|
||||
selection_changed = QtCore.Signal()
|
||||
active_changed = QtCore.Signal()
|
||||
# Refreshed attribute is not changed by view itself
|
||||
# - widget which triggers `refresh` is changing the state
|
||||
# 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)
|
||||
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(
|
||||
self._on_create_request
|
||||
)
|
||||
|
|
@ -281,7 +275,19 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
)
|
||||
|
||||
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(
|
||||
"publish.reset.finished", self._on_publish_reset
|
||||
|
|
@ -936,13 +942,16 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
|
||||
self._set_footer_enabled(bool(all_valid))
|
||||
|
||||
def _on_instances_refresh(self):
|
||||
def _on_create_model_reset(self):
|
||||
self._validate_create_instances()
|
||||
|
||||
context_title = self._controller.get_context_title()
|
||||
self.set_context_label(context_title)
|
||||
self._update_publish_details_widget()
|
||||
|
||||
def _event_callback_validate_instances(self, _event):
|
||||
self._validate_create_instances()
|
||||
|
||||
def _set_comment_input_visiblity(self, visible):
|
||||
self._comment_input.setVisible(visible)
|
||||
self._footer_spacer.setVisible(not visible)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring AYON addon 'core' version."""
|
||||
__version__ = "1.0.0+dev"
|
||||
__version__ = "1.0.4+dev"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
name = "core"
|
||||
title = "Core"
|
||||
version = "1.0.0+dev"
|
||||
version = "1.0.4+dev"
|
||||
|
||||
client_dir = "ayon_core"
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
[tool.poetry]
|
||||
name = "ayon-core"
|
||||
version = "1.0.0+dev"
|
||||
version = "1.0.4+dev"
|
||||
description = ""
|
||||
authors = ["Ynput Team <team@ynput.io>"]
|
||||
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",
|
||||
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