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:
Roy Nieterau 2024-10-23 23:46:32 +02:00
commit 5f7d6bd313
31 changed files with 1339 additions and 283 deletions

View file

@ -28,7 +28,8 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook):
"substancepainter", "substancepainter",
"aftereffects", "aftereffects",
"wrap", "wrap",
"openrv" "openrv",
"cinema4d"
} }
launch_types = {LaunchTypes.local} launch_types = {LaunchTypes.local}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -36,7 +36,8 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
"traypublisher", "traypublisher",
"substancepainter", "substancepainter",
"nuke", "nuke",
"aftereffects" "aftereffects",
"unreal"
] ]
enabled = False enabled = False

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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