Merge pull request #935 from ynput/enhancement/AY-2420_Callbacks-and-groups-with-Publisher-attributes

Create Context: Per instance attributes and Callbacks
This commit is contained in:
Jakub Trllo 2024-10-21 14:11:59 +02:00 committed by GitHub
commit d80ae9fab6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 3620 additions and 2083 deletions

View file

@ -6,6 +6,7 @@ import json
import copy
import warnings
from abc import ABCMeta, abstractmethod
from typing import Any, Optional
import clique
@ -147,15 +148,15 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta):
def __init__(
self,
key,
default,
label=None,
tooltip=None,
is_label_horizontal=None,
visible=None,
enabled=None,
hidden=None,
disabled=None,
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
@ -167,53 +168,85 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta):
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.visible = visible
self.enabled = enabled
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 clone(self):
data = self.serialize()
data.pop("type")
return self.deserialize(data)
@property
def hidden(self):
def hidden(self) -> bool:
return not self.visible
@hidden.setter
def hidden(self, value):
def hidden(self, value: bool):
self.visible = not value
@property
def disabled(self):
def disabled(self) -> bool:
return not self.enabled
@disabled.setter
def disabled(self, value):
def disabled(self, value: bool):
self.enabled = not value
def __eq__(self, other):
if not isinstance(other, self.__class__):
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.default == other.default
and self.visible == other.visible
and self.enabled == other.enabled
(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:
@ -260,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
@ -272,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
@ -286,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
@ -309,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
@ -331,6 +374,9 @@ class HiddenDef(AbstractAttrDef):
kwargs["visible"] = False
super().__init__(key, **kwargs)
def is_value_valid(self, value: Any) -> bool:
return True
def convert_value(self, value):
return value
@ -380,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):
@ -410,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.
@ -439,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
@ -456,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):
@ -471,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.
@ -513,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:
@ -538,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
@ -606,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.
@ -619,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):
@ -917,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 (
@ -930,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]

View file

@ -566,6 +566,10 @@ class EventSystem:
self._process_event(event)
def clear_callbacks(self):
"""Clear all registered callbacks."""
self._registered_callbacks = []
def _process_event(self, event):
"""Process event topic and trigger callbacks.

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
import copy
import collections
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING, Optional, Dict, Any
from abc import ABC, abstractmethod
@ -19,11 +19,12 @@ from .constants import DEFAULT_VARIANT_VALUE
from .product_name import get_product_name
from .utils import get_next_versions_for_instances
from .legacy_create import LegacyCreator
from .structures import CreatedInstance
if TYPE_CHECKING:
from ayon_core.lib import AbstractAttrDef
# Avoid cyclic imports
from .context import CreateContext, CreatedInstance, UpdateData # noqa: F401
from .context import CreateContext, UpdateData # noqa: F401
class ProductConvertorPlugin(ABC):
@ -204,6 +205,7 @@ class BaseCreator(ABC):
self.headless = headless
self.apply_settings(project_settings)
self.register_callbacks()
@staticmethod
def _get_settings_values(project_settings, category_name, plugin_name):
@ -289,6 +291,14 @@ class BaseCreator(ABC):
))
setattr(self, key, value)
def register_callbacks(self):
"""Register callbacks for creator.
Default implementation does nothing. It can be overridden to register
callbacks for creator.
"""
pass
@property
def identifier(self):
"""Identifier of creator (must be unique).
@ -362,6 +372,35 @@ class BaseCreator(ABC):
self._log = Logger.get_logger(self.__class__.__name__)
return self._log
def _create_instance(
self,
product_name: str,
data: Dict[str, Any],
product_type: Optional[str] = None
) -> CreatedInstance:
"""Create instance and add instance to context.
Args:
product_name (str): Product name.
data (Dict[str, Any]): Instance data.
product_type (Optional[str]): Product type, object attribute
'product_type' is used if not passed.
Returns:
CreatedInstance: Created instance.
"""
if product_type is None:
product_type = self.product_type
instance = CreatedInstance(
product_type,
product_name,
data,
creator=self,
)
self._add_instance_to_context(instance)
return instance
def _add_instance_to_context(self, instance):
"""Helper method to add instance to create context.
@ -551,6 +590,16 @@ class BaseCreator(ABC):
return self.instance_attr_defs
def get_attr_defs_for_instance(self, instance):
"""Get attribute definitions for an instance.
Args:
instance (CreatedInstance): Instance for which to get
attribute definitions.
"""
return self.get_instance_attr_defs()
@property
def collection_shared_data(self):
"""Access to shared data that can be used during creator's collection.
@ -782,6 +831,16 @@ class Creator(BaseCreator):
"""
return self.pre_create_attr_defs
def _pre_create_attr_defs_changed(self):
"""Called when pre-create attribute definitions change.
Create plugin can call this method when knows that
'get_pre_create_attr_defs' should be called again.
"""
self.create_context.create_plugin_pre_create_attr_defs_changed(
self.identifier
)
class HiddenCreator(BaseCreator):
@abstractmethod

View file

@ -1,9 +1,10 @@
import copy
import collections
from uuid import uuid4
from typing import Optional
from typing import Optional, Dict, List, Any
from ayon_core.lib.attribute_definitions import (
AbstractAttrDef,
UnknownDef,
serialize_attr_defs,
deserialize_attr_defs,
@ -79,12 +80,17 @@ class AttributeValues:
Has dictionary like methods. Not all of them are allowed all the time.
Args:
attr_defs(AbstractAttrDef): Definitions of value type and properties.
values(dict): Values after possible conversion.
origin_data(dict): Values loaded from host before conversion.
"""
parent (Union[CreatedInstance, PublishAttributes]): Parent object.
key (str): Key of attribute values.
attr_defs (List[AbstractAttrDef]): Definitions of value type
and properties.
values (dict): Values after possible conversion.
origin_data (dict): Values loaded from host before conversion.
def __init__(self, attr_defs, values, origin_data=None):
"""
def __init__(self, parent, key, attr_defs, values, origin_data=None):
self._parent = parent
self._key = key
if origin_data is None:
origin_data = copy.deepcopy(values)
self._origin_data = origin_data
@ -106,7 +112,10 @@ class AttributeValues:
self._data = {}
for attr_def in attr_defs:
value = values.get(attr_def.key)
if value is not None:
if value is None:
continue
converted_value = attr_def.convert_value(value)
if converted_value == value:
self._data[attr_def.key] = value
def __setitem__(self, key, value):
@ -139,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():
@ -147,7 +159,11 @@ class AttributeValues:
self._data[_key] = _value
changes[_key] = _value
if changes:
self._parent.attribute_value_changed(self._key, changes)
def pop(self, key, default=None):
has_key = key in self._data
value = self._data.pop(key, default)
# Remove attribute definition if is 'UnknownDef'
# - gives option to get rid of unknown values
@ -155,6 +171,8 @@ class AttributeValues:
if isinstance(attr_def, UnknownDef):
self._attr_defs_by_key.pop(key)
self._attr_defs.remove(attr_def)
elif has_key:
self._parent.attribute_value_changed(self._key, {key: None})
return value
def reset_values(self):
@ -204,15 +222,11 @@ class AttributeValues:
class CreatorAttributeValues(AttributeValues):
"""Creator specific attribute values of an instance.
"""Creator specific attribute values of an instance."""
Args:
instance (CreatedInstance): Instance for which are values hold.
"""
def __init__(self, instance, *args, **kwargs):
self.instance = instance
super().__init__(*args, **kwargs)
@property
def instance(self):
return self._parent
class PublishAttributeValues(AttributeValues):
@ -220,19 +234,11 @@ class PublishAttributeValues(AttributeValues):
Values are for single plugin which can be on `CreatedInstance`
or context values stored on `CreateContext`.
Args:
publish_attributes(PublishAttributes): Wrapper for multiple publish
attributes is used as parent object.
"""
def __init__(self, publish_attributes, *args, **kwargs):
self.publish_attributes = publish_attributes
super().__init__(*args, **kwargs)
@property
def parent(self):
return self.publish_attributes.parent
def publish_attributes(self):
return self._parent
class PublishAttributes:
@ -245,22 +251,13 @@ class PublishAttributes:
parent(CreatedInstance, CreateContext): Parent for which will be
data stored and from which are data loaded.
origin_data(dict): Loaded data by plugin class name.
attr_plugins(Union[List[pyblish.api.Plugin], None]): List of publish
plugins that may have defined attribute definitions.
"""
def __init__(self, parent, origin_data, attr_plugins=None):
self.parent = parent
"""
def __init__(self, parent, origin_data):
self._parent = parent
self._origin_data = copy.deepcopy(origin_data)
attr_plugins = attr_plugins or []
self.attr_plugins = attr_plugins
self._data = copy.deepcopy(origin_data)
self._plugin_names_order = []
self._missing_plugins = []
self.set_publish_plugins(attr_plugins)
def __getitem__(self, key):
return self._data[key]
@ -277,6 +274,9 @@ class PublishAttributes:
def items(self):
return self._data.items()
def get(self, key, default=None):
return self._data.get(key, default)
def pop(self, key, default=None):
"""Remove or reset value for plugin.
@ -291,74 +291,65 @@ class PublishAttributes:
if key not in self._data:
return default
if key in self._missing_plugins:
self._missing_plugins.remove(key)
removed_item = self._data.pop(key)
return removed_item.data_to_store()
value = self._data[key]
if not isinstance(value, AttributeValues):
self.attribute_value_changed(key, None)
return self._data.pop(key)
value_item = self._data[key]
# Prepare value to return
output = value_item.data_to_store()
# Reset values
value_item.reset_values()
self.attribute_value_changed(
key, value_item.data_to_store()
)
return output
def plugin_names_order(self):
"""Plugin names order by their 'order' attribute."""
for name in self._plugin_names_order:
yield name
def mark_as_stored(self):
self._origin_data = copy.deepcopy(self.data_to_store())
def data_to_store(self):
"""Convert attribute values to "data to store"."""
output = {}
for key, attr_value in self._data.items():
output[key] = attr_value.data_to_store()
if isinstance(attr_value, AttributeValues):
output[key] = attr_value.data_to_store()
else:
output[key] = attr_value
return output
@property
def origin_data(self):
return copy.deepcopy(self._origin_data)
def set_publish_plugins(self, attr_plugins):
"""Set publish plugins attribute definitions."""
def attribute_value_changed(self, key, changes):
self._parent.publish_attribute_value_changed(key, changes)
self._plugin_names_order = []
self._missing_plugins = []
self.attr_plugins = attr_plugins or []
def set_publish_plugin_attr_defs(
self,
plugin_name: str,
attr_defs: List[AbstractAttrDef],
value: Optional[Dict[str, Any]] = None
):
"""Set attribute definitions for plugin.
origin_data = self._origin_data
data = self._data
self._data = {}
added_keys = set()
for plugin in attr_plugins:
output = plugin.convert_attribute_values(data)
if output is not None:
data = output
attr_defs = plugin.get_attribute_defs()
if not attr_defs:
continue
Args:
plugin_name (str): Name of plugin.
attr_defs (List[AbstractAttrDef]): Attribute definitions.
value (Optional[Dict[str, Any]]): Attribute values.
key = plugin.__name__
added_keys.add(key)
self._plugin_names_order.append(key)
"""
# TODO what if 'attr_defs' is 'None'?
if value is None:
value = self._data.get(plugin_name)
value = data.get(key) or {}
orig_value = copy.deepcopy(origin_data.get(key) or {})
self._data[key] = PublishAttributeValues(
self, attr_defs, value, orig_value
)
if value is None:
value = {}
for key, value in data.items():
if key not in added_keys:
self._missing_plugins.append(key)
self._data[key] = PublishAttributeValues(
self, [], value, value
)
self._data[plugin_name] = PublishAttributeValues(
self, plugin_name, attr_defs, value, value
)
def serialize_attributes(self):
return {
@ -366,14 +357,9 @@ class PublishAttributes:
plugin_name: attrs_value.get_serialized_attr_defs()
for plugin_name, attrs_value in self._data.items()
},
"plugin_names_order": self._plugin_names_order,
"missing_plugins": self._missing_plugins
}
def deserialize_attributes(self, data):
self._plugin_names_order = data["plugin_names_order"]
self._missing_plugins = data["missing_plugins"]
attr_defs = deserialize_attr_defs(data["attr_defs"])
origin_data = self._origin_data
@ -386,15 +372,12 @@ class PublishAttributes:
value = data.get(plugin_name) or {}
orig_value = copy.deepcopy(origin_data.get(plugin_name) or {})
self._data[plugin_name] = PublishAttributeValues(
self, attr_defs, value, orig_value
self, plugin_name, attr_defs, value, orig_value
)
for key, value in data.items():
if key not in added_keys:
self._missing_plugins.append(key)
self._data[key] = PublishAttributeValues(
self, [], value, value
)
self._data[key] = value
class InstanceContextInfo:
@ -432,12 +415,7 @@ class CreatedInstance:
product_name (str): Name of product that will be created.
data (Dict[str, Any]): Data used for filling product name or override
data from already existing instance.
creator (Union[BaseCreator, None]): Creator responsible for instance.
creator_identifier (str): Identifier of creator plugin.
creator_label (str): Creator plugin label.
group_label (str): Default group label from creator plugin.
creator_attr_defs (List[AbstractAttrDef]): Attribute definitions from
creator.
creator (BaseCreator): Creator responsible for instance.
"""
# Keys that can't be changed or removed from data after loading using
@ -458,17 +436,12 @@ class CreatedInstance:
product_type,
product_name,
data,
creator=None,
creator_identifier=None,
creator_label=None,
group_label=None,
creator_attr_defs=None,
creator,
):
if creator is not None:
creator_identifier = creator.identifier
group_label = creator.get_group_label()
creator_label = creator.label
creator_attr_defs = creator.get_instance_attr_defs()
self._creator = creator
creator_identifier = creator.identifier
group_label = creator.get_group_label()
creator_label = creator.label
self._creator_label = creator_label
self._group_label = group_label or creator_identifier
@ -528,18 +501,12 @@ class CreatedInstance:
# {key: value}
creator_values = copy.deepcopy(orig_creator_attributes)
self._data["creator_attributes"] = CreatorAttributeValues(
self,
list(creator_attr_defs),
creator_values,
orig_creator_attributes
)
self._data["creator_attributes"] = creator_values
# Stored publish specific attribute values
# {<plugin name>: {key: value}}
# - must be set using 'set_publish_plugins'
self._data["publish_attributes"] = PublishAttributes(
self, orig_publish_attributes, None
self, orig_publish_attributes
)
if data:
self._data.update(data)
@ -547,6 +514,11 @@ class CreatedInstance:
if not self._data.get("instance_id"):
self._data["instance_id"] = str(uuid4())
creator_attr_defs = creator.get_attr_defs_for_instance(self)
self.set_create_attr_defs(
creator_attr_defs, creator_values
)
def __str__(self):
return (
"<CreatedInstance {product[name]}"
@ -566,13 +538,20 @@ class CreatedInstance:
def __setitem__(self, key, value):
# Validate immutable keys
if key not in self.__immutable_keys:
self._data[key] = value
elif value != self._data.get(key):
if key in self.__immutable_keys:
if value == self._data.get(key):
return
# Raise exception if key is immutable and value has changed
raise ImmutableKeyError(key)
if key in self._data and self._data[key] == value:
return
self._data[key] = value
self._create_context.instance_values_changed(
self.id, {key: value}
)
def get(self, key, default=None):
return self._data.get(key, default)
@ -581,7 +560,13 @@ class CreatedInstance:
if key in self.__immutable_keys:
raise ImmutableKeyError(key)
self._data.pop(key, *args, **kwargs)
has_key = key in self._data
output = self._data.pop(key, *args, **kwargs)
if has_key:
self._create_context.instance_values_changed(
self.id, {key: None}
)
return output
def keys(self):
return self._data.keys()
@ -628,7 +613,7 @@ class CreatedInstance:
@property
def creator_label(self):
return self._creator_label or self.creator_identifier
return self._creator.label or self.creator_identifier
@property
def id(self):
@ -745,11 +730,46 @@ class CreatedInstance:
continue
output[key] = value
output["creator_attributes"] = self.creator_attributes.data_to_store()
if isinstance(self.creator_attributes, AttributeValues):
creator_attributes = self.creator_attributes.data_to_store()
else:
creator_attributes = copy.deepcopy(self.creator_attributes)
output["creator_attributes"] = creator_attributes
output["publish_attributes"] = self.publish_attributes.data_to_store()
return output
def set_create_attr_defs(self, attr_defs, value=None):
"""Create plugin updates create attribute definitions.
Method called by create plugin when attribute definitions should
be changed.
Args:
attr_defs (List[AbstractAttrDef]): Attribute definitions.
value (Optional[Dict[str, Any]]): Values of attribute definitions.
Current values are used if not passed in.
"""
if value is None:
value = self._data["creator_attributes"]
if isinstance(value, AttributeValues):
value = value.data_to_store()
if isinstance(self._data["creator_attributes"], AttributeValues):
origin_data = self._data["creator_attributes"].origin_data
else:
origin_data = copy.deepcopy(self._data["creator_attributes"])
self._data["creator_attributes"] = CreatorAttributeValues(
self,
"creator_attributes",
attr_defs,
value,
origin_data
)
self._create_context.instance_create_attr_defs_changed(self.id)
@classmethod
def from_existing(cls, instance_data, creator):
"""Convert instance data from workfile to CreatedInstance.
@ -776,18 +796,47 @@ class CreatedInstance:
product_type, product_name, instance_data, creator
)
def set_publish_plugins(self, attr_plugins):
"""Set publish plugins with attribute definitions.
This method should be called only from 'CreateContext'.
def attribute_value_changed(self, key, changes):
"""A value changed.
Args:
attr_plugins (List[pyblish.api.Plugin]): Pyblish plugins which
inherit from 'AYONPyblishPluginMixin' and may contain
attribute definitions.
"""
key (str): Key of attribute values.
changes (Dict[str, Any]): Changes in values.
self.publish_attributes.set_publish_plugins(attr_plugins)
"""
self._create_context.instance_values_changed(self.id, {key: changes})
def set_publish_plugin_attr_defs(self, plugin_name, attr_defs):
"""Set attribute definitions for publish plugin.
Args:
plugin_name(str): Name of publish plugin.
attr_defs(List[AbstractAttrDef]): Attribute definitions.
"""
self.publish_attributes.set_publish_plugin_attr_defs(
plugin_name, attr_defs
)
self._create_context.instance_publish_attr_defs_changed(
self.id, plugin_name
)
def publish_attribute_value_changed(self, plugin_name, value):
"""Method called from PublishAttributes.
Args:
plugin_name (str): Plugin name.
value (Dict[str, Any]): Changes in values for the plugin.
"""
self._create_context.instance_values_changed(
self.id,
{
"publish_attributes": {
plugin_name: value,
},
},
)
def add_members(self, members):
"""Currently unused method."""
@ -796,60 +845,12 @@ class CreatedInstance:
if member not in self._members:
self._members.append(member)
def serialize_for_remote(self):
"""Serialize object into data to be possible recreated object.
@property
def _create_context(self):
"""Get create context.
Returns:
Dict[str, Any]: Serialized data.
CreateContext: Context object which wraps object.
"""
creator_attr_defs = self.creator_attributes.get_serialized_attr_defs()
publish_attributes = self.publish_attributes.serialize_attributes()
return {
"data": self.data_to_store(),
"orig_data": self.origin_data,
"creator_attr_defs": creator_attr_defs,
"publish_attributes": publish_attributes,
"creator_label": self._creator_label,
"group_label": self._group_label,
}
@classmethod
def deserialize_on_remote(cls, serialized_data):
"""Convert instance data to CreatedInstance.
This is fake instance in remote process e.g. in UI process. The creator
is not a full creator and should not be used for calling methods when
instance is created from this method (matters on implementation).
Args:
serialized_data (Dict[str, Any]): Serialized data for remote
recreating. Should contain 'data' and 'orig_data'.
"""
instance_data = copy.deepcopy(serialized_data["data"])
creator_identifier = instance_data["creator_identifier"]
product_type = instance_data["productType"]
product_name = instance_data.get("productName", None)
creator_label = serialized_data["creator_label"]
group_label = serialized_data["group_label"]
creator_attr_defs = deserialize_attr_defs(
serialized_data["creator_attr_defs"]
)
publish_attributes = serialized_data["publish_attributes"]
obj = cls(
product_type,
product_name,
instance_data,
creator_identifier=creator_identifier,
creator_label=creator_label,
group_label=group_label,
creator_attr_defs=creator_attr_defs
)
obj._orig_data = serialized_data["orig_data"]
obj.publish_attributes.deserialize_attributes(publish_attributes)
return obj
return self._creator.create_context

View file

@ -1,9 +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,
@ -11,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):
@ -125,32 +133,118 @@ class AYONPyblishPluginMixin:
# for callback in self._state_change_callbacks:
# callback(self)
@classmethod
def register_create_context_callbacks(
cls, create_context: "CreateContext"
):
"""Register callbacks for create context.
It is possible to register callbacks listening to changes happened
in create context.
Methods available on create context:
- add_instances_added_callback
- add_instances_removed_callback
- add_value_changed_callback
- add_pre_create_attr_defs_change_callback
- add_create_attr_defs_change_callback
- add_publish_attr_defs_change_callback
Args:
create_context (CreateContext): Create context.
"""
pass
@classmethod
def get_attribute_defs(cls):
"""Publish attribute definitions.
Attributes available for all families in plugin's `families` attribute.
Returns:
list<AbstractAttrDef>: Attribute definitions for plugin.
"""
Returns:
list[AbstractAttrDef]: Attribute definitions for plugin.
"""
return []
@classmethod
def convert_attribute_values(cls, attribute_values):
if cls.__name__ not in attribute_values:
return attribute_values
def get_attr_defs_for_context(cls, create_context: "CreateContext"):
"""Publish attribute definitions for context.
plugin_values = attribute_values[cls.__name__]
Attributes available for all families in plugin's `families` attribute.
attr_defs = cls.get_attribute_defs()
for attr_def in attr_defs:
key = attr_def.key
if key in plugin_values:
plugin_values[key] = attr_def.convert_value(
plugin_values[key]
)
return attribute_values
Args:
create_context (CreateContext): Create context.
Returns:
list[AbstractAttrDef]: Attribute definitions for plugin.
"""
if cls.__instanceEnabled__:
return []
return cls.get_attribute_defs()
@classmethod
def instance_matches_plugin_families(
cls, instance: Optional["CreatedInstance"]
):
"""Check if instance matches families.
Args:
instance (Optional[CreatedInstance]): Instance to check. Or None
for context.
Returns:
bool: True if instance matches plugin families.
"""
if instance is None:
return not cls.__instanceEnabled__
if not cls.__instanceEnabled__:
return False
for _ in pyblish.logic.plugins_by_families(
[cls], [instance.product_type]
):
return True
return False
@classmethod
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.
Args:
create_context (CreateContext): Create context.
instance (CreatedInstance): Instance for which attributes are
collected.
Returns:
list[AbstractAttrDef]: Attribute definitions for plugin.
"""
if not cls.instance_matches_plugin_families(instance):
return []
return cls.get_attribute_defs()
@classmethod
def convert_attribute_values(
cls, create_context: "CreateContext", instance: "CreatedInstance"
):
"""Convert attribute values for instance.
Args:
create_context (CreateContext): Create context.
instance (CreatedInstance): Instance for which attributes are
converted.
"""
return
@staticmethod
def get_attr_values_from_data_for_plugin(plugin, data):

View file

@ -15,7 +15,6 @@ from ayon_core.lib import AbstractAttrDef
from ayon_core.host import HostBase
from ayon_core.pipeline.create import (
CreateContext,
CreatedInstance,
ConvertorItem,
)
from ayon_core.tools.common_models import (
@ -26,7 +25,7 @@ from ayon_core.tools.common_models import (
)
if TYPE_CHECKING:
from .models import CreatorItem, PublishErrorInfo
from .models import CreatorItem, PublishErrorInfo, InstanceItem
class CardMessageTypes:
@ -78,7 +77,7 @@ class AbstractPublisherCommon(ABC):
in future e.g. different message timeout or type (color).
Args:
message (str): Message that will be showed.
message (str): Message that will be shown.
message_type (Optional[str]): Message type.
"""
@ -203,7 +202,7 @@ class AbstractPublisherFrontend(AbstractPublisherCommon):
def is_host_valid(self) -> bool:
"""Host is valid for creation part.
Host must have implemented certain functionality to be able create
Host must have implemented certain functionality to be able to create
in Publisher tool.
Returns:
@ -266,6 +265,11 @@ class AbstractPublisherFrontend(AbstractPublisherCommon):
"""
pass
@abstractmethod
def get_folder_id_from_path(self, folder_path: str) -> Optional[str]:
"""Get folder id from folder path."""
pass
# --- Create ---
@abstractmethod
def get_creator_items(self) -> Dict[str, "CreatorItem"]:
@ -277,6 +281,21 @@ class AbstractPublisherFrontend(AbstractPublisherCommon):
"""
pass
@abstractmethod
def get_creator_item_by_id(
self, identifier: str
) -> Optional["CreatorItem"]:
"""Get creator item by identifier.
Args:
identifier (str): Create plugin identifier.
Returns:
Optional[CreatorItem]: Creator item or None.
"""
pass
@abstractmethod
def get_creator_icon(
self, identifier: str
@ -307,19 +326,19 @@ class AbstractPublisherFrontend(AbstractPublisherCommon):
pass
@abstractmethod
def get_instances(self) -> List[CreatedInstance]:
def get_instance_items(self) -> List["InstanceItem"]:
"""Collected/created instances.
Returns:
List[CreatedInstance]: List of created instances.
List[InstanceItem]: List of created instances.
"""
pass
@abstractmethod
def get_instances_by_id(
def get_instance_items_by_id(
self, instance_ids: Optional[Iterable[str]] = None
) -> Dict[str, Union[CreatedInstance, None]]:
) -> Dict[str, Union["InstanceItem", None]]:
pass
@abstractmethod
@ -328,28 +347,56 @@ class AbstractPublisherFrontend(AbstractPublisherCommon):
):
pass
@abstractmethod
def set_instances_context_info(
self, changes_by_instance_id: Dict[str, Dict[str, Any]]
):
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
@abstractmethod
def get_creator_attribute_definitions(
self, instances: List[CreatedInstance]
) -> List[Tuple[AbstractAttrDef, List[CreatedInstance], List[Any]]]:
self, instance_ids: Iterable[str]
) -> List[Tuple[AbstractAttrDef, List[str], List[Any]]]:
pass
@abstractmethod
def set_instances_create_attr_values(
self, instance_ids: Iterable[str], key: str, value: Any
):
pass
@abstractmethod
def get_publish_attribute_definitions(
self,
instances: List[CreatedInstance],
instance_ids: Iterable[str],
include_context: bool
) -> List[Tuple[
str,
List[AbstractAttrDef],
Dict[str, List[Tuple[CreatedInstance, Any]]]
Dict[str, List[Tuple[str, Any]]]
]]:
pass
@abstractmethod
def set_instances_publish_attr_values(
self,
instance_ids: Iterable[str],
plugin_name: str,
key: str,
value: Any
):
pass
@abstractmethod
def get_product_name(
self,
@ -383,7 +430,7 @@ class AbstractPublisherFrontend(AbstractPublisherCommon):
):
"""Trigger creation by creator identifier.
Should also trigger refresh of instanes.
Should also trigger refresh of instances.
Args:
creator_identifier (str): Identifier of Creator plugin.
@ -446,8 +493,8 @@ class AbstractPublisherFrontend(AbstractPublisherCommon):
"""Trigger pyblish action on a plugin.
Args:
plugin_id (str): Id of publish plugin.
action_id (str): Id of publish action.
plugin_id (str): Publish plugin id.
action_id (str): Publish action id.
"""
pass
@ -586,7 +633,7 @@ class AbstractPublisherFrontend(AbstractPublisherCommon):
@abstractmethod
def get_thumbnail_temp_dir_path(self) -> str:
"""Return path to directory where thumbnails can be temporary stored.
"""Path to directory where thumbnails can be temporarily stored.
Returns:
str: Path to a directory.

View file

@ -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.
@ -172,27 +192,37 @@ class PublisherController(
"""
return self._create_model.get_creator_icon(identifier)
def get_instance_items(self):
"""Current instances in create context."""
return self._create_model.get_instance_items()
# --- Legacy for TrayPublisher ---
@property
def instances(self):
"""Current instances in create context.
Deprecated:
Use 'get_instances' instead. Kept for backwards compatibility with
traypublisher.
"""
return self.get_instances()
return self.get_instance_items()
def get_instances(self):
"""Current instances in create context."""
return self._create_model.get_instances()
return self.get_instance_items()
def get_instances_by_id(self, instance_ids=None):
return self._create_model.get_instances_by_id(instance_ids)
def get_instances_by_id(self, *args, **kwargs):
return self.get_instance_items_by_id(*args, **kwargs)
# ---
def get_instance_items_by_id(self, instance_ids=None):
return self._create_model.get_instance_items_by_id(instance_ids)
def get_instances_context_info(self, instance_ids=None):
return self._create_model.get_instances_context_info(instance_ids)
def set_instances_context_info(self, changes_by_instance_id):
return self._create_model.set_instances_context_info(
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()
@ -365,29 +395,41 @@ class PublisherController(
if os.path.exists(dirpath):
shutil.rmtree(dirpath)
def get_creator_attribute_definitions(self, instances):
def get_creator_attribute_definitions(self, instance_ids):
"""Collect creator attribute definitions for multuple instances.
Args:
instances(List[CreatedInstance]): List of created instances for
instance_ids (List[str]): List of created instances for
which should be attribute definitions returned.
"""
return self._create_model.get_creator_attribute_definitions(
instances
instance_ids
)
def get_publish_attribute_definitions(self, instances, include_context):
def set_instances_create_attr_values(self, instance_ids, key, value):
return self._create_model.set_instances_create_attr_values(
instance_ids, key, value
)
def get_publish_attribute_definitions(self, instance_ids, include_context):
"""Collect publish attribute definitions for passed instances.
Args:
instances(list<CreatedInstance>): List of created instances for
instance_ids (List[str]): List of created instances for
which should be attribute definitions returned.
include_context(bool): Add context specific attribute definitions.
include_context (bool): Add context specific attribute definitions.
"""
return self._create_model.get_publish_attribute_definitions(
instances, include_context
instance_ids, include_context
)
def set_instances_publish_attr_values(
self, instance_ids, plugin_name, key, value
):
return self._create_model.set_instances_publish_attr_values(
instance_ids, plugin_name, key, value
)
def get_product_name(

View file

@ -1,10 +1,11 @@
from .create import CreateModel, CreatorItem
from .create import CreateModel, CreatorItem, InstanceItem
from .publish import PublishModel, PublishErrorInfo
__all__ = (
"CreateModel",
"CreatorItem",
"InstanceItem",
"PublishModel",
"PublishErrorInfo",

View file

@ -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,
@ -192,7 +203,192 @@ class CreatorItem:
return cls(**data)
class InstanceItem:
def __init__(
self,
instance_id: str,
creator_identifier: str,
label: str,
group_label: str,
product_type: str,
product_name: str,
variant: str,
folder_path: Optional[str],
task_name: Optional[str],
is_active: bool,
has_promised_context: bool,
):
self._instance_id: str = instance_id
self._creator_identifier: str = creator_identifier
self._label: str = label
self._group_label: str = group_label
self._product_type: str = product_type
self._product_name: str = product_name
self._variant: str = variant
self._folder_path: Optional[str] = folder_path
self._task_name: Optional[str] = task_name
self._is_active: bool = is_active
self._has_promised_context: bool = has_promised_context
@property
def id(self):
return self._instance_id
@property
def creator_identifier(self):
return self._creator_identifier
@property
def label(self):
return self._label
@property
def group_label(self):
return self._group_label
@property
def product_type(self):
return self._product_type
@property
def has_promised_context(self):
return self._has_promised_context
def get_variant(self):
return self._variant
def set_variant(self, variant):
self._variant = variant
def get_product_name(self):
return self._product_name
def set_product_name(self, product_name):
self._product_name = product_name
def get_folder_path(self):
return self._folder_path
def set_folder_path(self, folder_path):
self._folder_path = folder_path
def get_task_name(self):
return self._task_name
def set_task_name(self, task_name):
self._task_name = task_name
def get_is_active(self):
return self._is_active
def set_is_active(self, is_active):
self._is_active = is_active
product_name = property(get_product_name, set_product_name)
variant = property(get_variant, set_variant)
folder_path = property(get_folder_path, set_folder_path)
task_name = property(get_task_name, set_task_name)
is_active = property(get_is_active, set_is_active)
@classmethod
def from_instance(cls, instance: CreatedInstance):
return InstanceItem(
instance.id,
instance.creator_identifier,
instance.label,
instance.group_label,
instance.product_type,
instance.product_name,
instance["variant"],
instance["folderPath"],
instance["task"],
instance["active"],
instance.has_promised_context,
)
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
@ -258,12 +454,34 @@ class CreateModel:
self._creator_items = None
self._reset_instances()
self._emit_event("create.model.reset")
self._create_context.add_instances_added_callback(
self._cc_added_instance
)
self._create_context.add_instances_removed_callback (
self._cc_removed_instance
)
self._create_context.add_value_changed_callback(
self._cc_value_changed
)
self._create_context.add_pre_create_attr_defs_change_callback (
self._cc_pre_create_attr_changed
)
self._create_context.add_create_attr_defs_change_callback (
self._cc_create_attr_changed
)
self._create_context.add_publish_attr_defs_change_callback (
self._cc_publish_attr_changed
)
self._create_context.reset_finalization()
def get_creator_items(self) -> Dict[str, CreatorItem]:
"""Creators that can be shown in create dialog."""
if self._creator_items is None:
self._creator_items = self._collect_creator_items()
self._refresh_creator_items()
return self._creator_items
def get_creator_item_by_id(
@ -287,33 +505,68 @@ class CreateModel:
return creator_item.icon
return None
def get_instances(self) -> List[CreatedInstance]:
def get_instance_items(self) -> List[InstanceItem]:
"""Current instances in create context."""
return list(self._create_context.instances_by_id.values())
return [
InstanceItem.from_instance(instance)
for instance in self._create_context.instances_by_id.values()
]
def get_instance_by_id(
def get_instance_item_by_id(
self, instance_id: str
) -> Union[CreatedInstance, None]:
return self._create_context.instances_by_id.get(instance_id)
) -> Union[InstanceItem, None]:
instance = self._create_context.instances_by_id.get(instance_id)
if instance is None:
return None
def get_instances_by_id(
return InstanceItem.from_instance(instance)
def get_instance_items_by_id(
self, instance_ids: Optional[Iterable[str]] = None
) -> Dict[str, Union[CreatedInstance, None]]:
) -> Dict[str, Union[InstanceItem, None]]:
if instance_ids is None:
instance_ids = self._create_context.instances_by_id.keys()
return {
instance_id: self.get_instance_by_id(instance_id)
instance_id: self.get_instance_item_by_id(instance_id)
for instance_id in instance_ids
}
def get_instances_context_info(
self, instance_ids: Optional[Iterable[str]] = None
):
instances = self.get_instances_by_id(instance_ids).values()
instances = self._get_instances_by_id(instance_ids).values()
return self._create_context.get_instances_context_info(
instances
)
def set_instances_context_info(self, changes_by_instance_id):
with self._create_context.bulk_value_changes(CREATE_EVENT_SOURCE):
for instance_id, changes in changes_by_instance_id.items():
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
@ -341,7 +594,7 @@ class CreateModel:
instance = None
if instance_id:
instance = self.get_instance_by_id(instance_id)
instance = self._get_instance_by_id(instance_id)
project_name = self._controller.get_current_project_name()
folder_item = self._controller.get_folder_item_by_path(
@ -396,9 +649,10 @@ class CreateModel:
success = True
try:
self._create_context.create_with_unified_error(
creator_identifier, product_name, instance_data, options
)
with self._create_context.bulk_add_instances():
self._create_context.create_with_unified_error(
creator_identifier, product_name, instance_data, options
)
except CreatorsOperationFailed as exc:
success = False
@ -410,7 +664,6 @@ class CreateModel:
}
)
self._on_create_instance_change()
return success
def trigger_convertor_items(self, convertor_identifiers: List[str]):
@ -498,23 +751,38 @@ class CreateModel:
# is not required.
self._remove_instances_from_context(instance_ids)
self._on_create_instance_change()
def set_instances_create_attr_values(self, instance_ids, key, value):
with self._create_context.bulk_value_changes(CREATE_EVENT_SOURCE):
for instance_id in instance_ids:
instance = self._get_instance_by_id(instance_id)
creator_attributes = instance["creator_attributes"]
attr_def = creator_attributes.get_attr_def(key)
if (
attr_def is None
or not attr_def.is_value_def
or not attr_def.visible
or not attr_def.enabled
or not attr_def.is_value_valid(value)
):
continue
creator_attributes[key] = value
def get_creator_attribute_definitions(
self, instances: List[CreatedInstance]
) -> List[Tuple[AbstractAttrDef, List[CreatedInstance], List[Any]]]:
self, instance_ids: List[str]
) -> List[Tuple[AbstractAttrDef, List[str], List[Any]]]:
"""Collect creator attribute definitions for multuple instances.
Args:
instances (List[CreatedInstance]): List of created instances for
instance_ids (List[str]): List of created instances for
which should be attribute definitions returned.
"""
"""
# NOTE it would be great if attrdefs would have hash method implemented
# so they could be used as keys in dictionary
output = []
_attr_defs = {}
for instance in instances:
for instance_id in instance_ids:
instance = self._get_instance_by_id(instance_id)
for attr_def in instance.creator_attribute_defs:
found_idx = None
for idx, _attr_def in _attr_defs.items():
@ -525,29 +793,54 @@ class CreateModel:
value = None
if attr_def.is_value_def:
value = instance.creator_attributes[attr_def.key]
if found_idx is None:
idx = len(output)
output.append((attr_def, [instance], [value]))
output.append((attr_def, [instance_id], [value]))
_attr_defs[idx] = attr_def
else:
item = output[found_idx]
item[1].append(instance)
item[2].append(value)
_, ids, values = output[found_idx]
ids.append(instance_id)
values.append(value)
return output
def set_instances_publish_attr_values(
self, instance_ids, plugin_name, key, value
):
with self._create_context.bulk_value_changes(CREATE_EVENT_SOURCE):
for instance_id in instance_ids:
if instance_id is None:
instance = self._create_context
else:
instance = self._get_instance_by_id(instance_id)
plugin_val = instance.publish_attributes[plugin_name]
attr_def = plugin_val.get_attr_def(key)
# Ignore if attribute is not available or enabled/visible
# on the instance, or the value is not valid for definition
if (
attr_def is None
or not attr_def.is_value_def
or not attr_def.visible
or not attr_def.enabled
or not attr_def.is_value_valid(value)
):
continue
plugin_val[key] = value
def get_publish_attribute_definitions(
self,
instances: List[CreatedInstance],
instance_ids: List[str],
include_context: bool
) -> List[Tuple[
str,
List[AbstractAttrDef],
Dict[str, List[Tuple[CreatedInstance, Any]]]
Dict[str, List[Tuple[str, Any]]]
]]:
"""Collect publish attribute definitions for passed instances.
Args:
instances (list[CreatedInstance]): List of created instances for
instance_ids (List[str]): List of created instances for
which should be attribute definitions returned.
include_context (bool): Add context specific attribute definitions.
@ -556,19 +849,26 @@ class CreateModel:
if include_context:
_tmp_items.append(self._create_context)
for instance in instances:
_tmp_items.append(instance)
for instance_id in instance_ids:
_tmp_items.append(self._get_instance_by_id(instance_id))
all_defs_by_plugin_name = {}
all_plugin_values = {}
for item in _tmp_items:
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, {})
@ -579,7 +879,11 @@ class CreateModel:
attr_values = plugin_values.setdefault(attr_def.key, [])
value = attr_val[attr_def.key]
attr_values.append((item, 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 = []
for plugin in self._create_context.plugins_with_defs:
@ -588,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
@ -620,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.
@ -638,11 +946,26 @@ class CreateModel:
return self._create_context.creators
def _get_instance_by_id(
self, instance_id: str
) -> Union[CreatedInstance, None]:
return self._create_context.instances_by_id.get(instance_id)
def _get_instances_by_id(
self, instance_ids: Optional[Iterable[str]]
) -> Dict[str, Union[CreatedInstance, None]]:
if instance_ids is None:
instance_ids = self._create_context.instances_by_id.keys()
return {
instance_id: self._get_instance_by_id(instance_id)
for instance_id in instance_ids
}
def _reset_instances(self):
"""Reset create instances."""
self._create_context.reset_context_data()
with self._create_context.bulk_instances_collection():
with self._create_context.bulk_add_instances():
try:
self._create_context.reset_instances()
except CreatorsOperationFailed as exc:
@ -677,8 +1000,6 @@ class CreateModel:
}
)
self._on_create_instance_change()
def _remove_instances_from_context(self, instance_ids: List[str]):
instances_by_id = self._create_context.instances_by_id
instances = [
@ -696,9 +1017,6 @@ class CreateModel:
}
)
def _on_create_instance_change(self):
self._emit_event("instances.refresh.finished")
def _collect_creator_items(self) -> Dict[str, CreatorItem]:
# TODO add crashed initialization of create plugins to report
output = {}
@ -720,6 +1038,98 @@ class CreateModel:
return output
def _refresh_creator_items(self, identifiers=None):
if identifiers is None:
self._creator_items = self._collect_creator_items()
return
for identifier in identifiers:
if identifier not in self._creator_items:
continue
creator = self._create_context.creators.get(identifier)
if creator is None:
continue
self._creator_items[identifier] = (
CreatorItem.from_creator(creator)
)
def _cc_added_instance(self, event):
instance_ids = {
instance.id
for instance in event.data["instances"]
}
self._emit_event(
"create.context.added.instance",
{"instance_ids": instance_ids},
)
def _cc_removed_instance(self, event):
instance_ids = {
instance.id
for instance in event.data["instances"]
}
self._emit_event(
"create.context.removed.instance",
{"instance_ids": instance_ids},
)
def _cc_value_changed(self, event):
if event.source == CREATE_EVENT_SOURCE:
return
instance_changes = {}
context_changed_ids = set()
for item in event.data["changes"]:
instance_id = None
if item["instance"]:
instance_id = item["instance"].id
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"]
self._refresh_creator_items(identifiers)
self._emit_event(
"create.context.pre.create.attrs.changed",
{"identifiers": identifiers},
)
def _cc_create_attr_changed(self, event):
instance_ids = {
instance.id
for instance in event.data["instances"]
}
self._emit_event(
"create.context.create.attrs.changed",
{"instance_ids": instance_ids},
)
def _cc_publish_attr_changed(self, event):
instance_changes = event.data["instance_changes"]
event_data = {
instance_id: instance_data["plugin_names"]
for instance_id, instance_data in instance_changes.items()
}
self._emit_event(
"create.context.publish.attrs.changed",
event_data,
)
def _get_allowed_creators_pattern(self) -> Union[Pattern, None]:
"""Provide regex pattern for configured creator labels in this context

View file

@ -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,17 +218,24 @@ 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.
Args:
instances (list[CreatedInstance]): List of instances in
instances (list[InstanceItem]): List of instances in
CreateContext.
context_info_by_id (Dict[str, InstanceContextInfo]): Instance
context info by instance id.
@ -238,7 +246,7 @@ class InstanceGroupWidget(BaseGroupWidget):
instances_by_product_name = collections.defaultdict(list)
for instance in instances:
instances_by_id[instance.id] = instance
product_name = instance["productName"]
product_name = instance.product_name
instances_by_product_name[product_name].append(instance)
# Remove instance widgets that are not in passed instances
@ -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["active"]
# First change instance value and them change checkbox
# - prevent to trigger `active_changed` signal
if instance_value != new_value:
self.instance["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
@ -494,8 +493,8 @@ class InstanceCardWidget(CardWidget):
self._context_warning.setVisible(not valid)
def _update_product_name(self):
variant = self.instance["variant"]
product_name = self.instance["productName"]
variant = self.instance.variant
product_name = self.instance.product_name
label = self.instance.label
if (
variant == self._last_variant
@ -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["active"])
self._set_active(self.instance.is_active)
self._validate_context(context_info)
def _set_expanded(self, expanded=None):
@ -535,11 +534,10 @@ class InstanceCardWidget(CardWidget):
def _on_active_change(self):
new_value = self._active_checkbox.isChecked()
old_value = self.instance["active"]
old_value = self.instance.is_active
if new_value == old_value:
return
self.instance["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.

View file

@ -111,7 +111,7 @@ class CreateWidget(QtWidgets.QWidget):
self._folder_path = None
self._product_names = None
self._selected_creator = None
self._selected_creator_identifier = None
self._prereq_available = False
@ -262,6 +262,10 @@ class CreateWidget(QtWidgets.QWidget):
controller.register_event_callback(
"controller.reset.finished", self._on_controler_reset
)
controller.register_event_callback(
"create.context.pre.create.attrs.changed",
self._pre_create_attr_changed
)
self._main_splitter_widget = main_splitter_widget
@ -512,6 +516,15 @@ class CreateWidget(QtWidgets.QWidget):
# Trigger refresh only if is visible
self.refresh()
def _pre_create_attr_changed(self, event):
if (
self._selected_creator_identifier is None
or self._selected_creator_identifier not in event["identifiers"]
):
return
self._set_creator_by_identifier(self._selected_creator_identifier)
def _on_folder_change(self):
self._refresh_product_name()
if self._context_change_is_enabled():
@ -563,12 +576,13 @@ class CreateWidget(QtWidgets.QWidget):
self._set_creator_detailed_text(creator_item)
self._pre_create_widget.set_creator_item(creator_item)
self._selected_creator = creator_item
if not creator_item:
self._selected_creator_identifier = None
self._set_context_enabled(False)
return
self._selected_creator_identifier = creator_item.identifier
if (
creator_item.create_allow_context_change
!= self._context_change_is_enabled()
@ -603,7 +617,7 @@ class CreateWidget(QtWidgets.QWidget):
return
# This should probably never happen?
if not self._selected_creator:
if not self._selected_creator_identifier:
if self.product_name_input.text():
self.product_name_input.setText("")
return
@ -625,11 +639,13 @@ class CreateWidget(QtWidgets.QWidget):
folder_path = self._get_folder_path()
task_name = self._get_task_name()
creator_idenfier = self._selected_creator.identifier
# Calculate product name with Creator plugin
try:
product_name = self._controller.get_product_name(
creator_idenfier, variant_value, task_name, folder_path
self._selected_creator_identifier,
variant_value,
task_name,
folder_path
)
except TaskNotSetError:
self._create_btn.setEnabled(False)
@ -755,7 +771,7 @@ class CreateWidget(QtWidgets.QWidget):
)
if success:
self._set_creator(self._selected_creator)
self._set_creator_by_identifier(self._selected_creator_identifier)
self._variant_widget.setText(variant)
self._controller.emit_card_message("Creation finished...")
self._last_thumbnail_path = None

View file

@ -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:
@ -131,7 +131,7 @@ class InstanceListItemWidget(QtWidgets.QWidget):
product_name_label.setObjectName("ListViewProductName")
active_checkbox = NiceCheckbox(parent=self)
active_checkbox.setChecked(instance["active"])
active_checkbox.setChecked(instance.is_active)
layout = QtWidgets.QHBoxLayout(self)
content_margins = layout.contentsMargins()
@ -171,47 +171,34 @@ class InstanceListItemWidget(QtWidgets.QWidget):
def is_active(self):
"""Instance is activated."""
return self.instance["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["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["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["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["active"]
if new_value == old_value:
return
self.instance["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()
)
@ -639,10 +626,10 @@ class InstanceListView(AbstractInstanceView):
instance_id = instance.id
# Handle group activity
if activity is None:
activity = int(instance["active"])
activity = int(instance.is_active)
elif activity == -1:
pass
elif activity != instance["active"]:
elif activity != instance.is_active:
activity = -1
context_info = context_info_by_id[instance_id]
@ -658,8 +645,8 @@ class InstanceListView(AbstractInstanceView):
# Create new item and store it as new
item = QtGui.QStandardItem()
item.setData(instance["productName"], SORT_VALUE_ROLE)
item.setData(instance["productName"], GROUP_ROLE)
item.setData(instance.product_name, SORT_VALUE_ROLE)
item.setData(instance.product_name, GROUP_ROLE)
item.setData(instance_id, INSTANCE_ID_ROLE)
new_items.append(item)
new_items_with_instance.append((item, instance))
@ -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):

View file

@ -6,17 +6,15 @@ from .border_label_widget import BorderedLabelWidget
from .card_view_widgets import InstanceCardView
from .list_view_widgets import InstanceListView
from .widgets import (
ProductAttributesWidget,
CreateInstanceBtn,
RemoveInstanceBtn,
ChangeViewBtn,
)
from .create_widget import CreateWidget
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()
@ -61,7 +59,7 @@ class OverviewWidget(QtWidgets.QFrame):
product_attributes_wrap = BorderedLabelWidget(
"Publish options", product_content_widget
)
product_attributes_widget = ProductAttributesWidget(
product_attributes_widget = ProductInfoWidget(
controller, product_attributes_wrap
)
product_attributes_wrap.set_center_widget(product_attributes_widget)
@ -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
)
@ -152,7 +140,20 @@ class OverviewWidget(QtWidgets.QFrame):
"publish.reset.finished", self._on_publish_reset
)
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._on_instances_added
)
controller.register_event_callback(
"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
@ -303,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)
@ -353,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:
@ -363,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()
@ -436,6 +430,12 @@ class OverviewWidget(QtWidgets.QFrame):
# Force to change instance and refresh details
self._on_product_change()
# Give a change to process Resize Request
QtWidgets.QApplication.processEvents()
# Trigger update geometry of
widget = self._product_views_layout.currentWidget()
widget.updateGeometry()
def _on_publish_start(self):
"""Publish started."""
@ -461,13 +461,11 @@ class OverviewWidget(QtWidgets.QFrame):
self._controller.is_host_valid()
)
def _on_instances_refresh(self):
"""Controller refreshed instances."""
def _on_create_model_reset(self):
self._refresh_instances()
# Give a change to process Resize Request
QtWidgets.QApplication.processEvents()
# Trigger update geometry of
widget = self._product_views_layout.currentWidget()
widget.updateGeometry()
def _on_instances_added(self):
self._refresh_instances()
def _on_instances_removed(self):
self._refresh_instances()

View file

@ -0,0 +1,376 @@
from qtpy import QtWidgets, QtCore
from ayon_core.lib.attribute_definitions import UnknownDef
from ayon_core.tools.attribute_defs import create_widget_for_attr_def
from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend
from ayon_core.tools.publisher.constants import (
INPUTS_LAYOUT_HSPACING,
INPUTS_LAYOUT_VSPACING,
)
class CreatorAttrsWidget(QtWidgets.QWidget):
"""Widget showing creator specific attributes for selected instances.
Attributes are defined on creator so are dynamic. Their look and type is
based on attribute definitions that are defined in
`~/ayon_core/lib/attribute_definitions.py` and their widget
representation in `~/ayon_core/tools/attribute_defs/*`.
Widgets are disabled if context of instance is not valid.
Definitions are shown for all instance no matter if they are created with
different creators. If creator have same (similar) definitions their
widgets are merged into one (different label does not count).
"""
def __init__(
self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget
):
super().__init__(parent)
scroll_area = QtWidgets.QScrollArea(self)
scroll_area.setWidgetResizable(True)
main_layout = QtWidgets.QHBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
main_layout.addWidget(scroll_area, 1)
controller.register_event_callback(
"create.context.create.attrs.changed",
self._on_instance_attr_defs_change
)
controller.register_event_callback(
"create.context.value.changed",
self._on_instance_value_change
)
self._main_layout = main_layout
self._controller: AbstractPublisherFrontend = controller
self._scroll_area = scroll_area
self._attr_def_id_to_instances = {}
self._attr_def_id_to_attr_def = {}
self._current_instance_ids = set()
# To store content of scroll area to prevent garbage collection
self._content_widget = None
def set_instances_valid(self, valid):
"""Change valid state of current instances."""
if (
self._content_widget is not None
and self._content_widget.isEnabled() != valid
):
self._content_widget.setEnabled(valid)
def set_current_instances(self, instance_ids):
"""Set current instances for which are attribute definitions shown."""
self._current_instance_ids = set(instance_ids)
self._refresh_content()
def _refresh_content(self):
prev_content_widget = self._scroll_area.widget()
if prev_content_widget:
self._scroll_area.takeWidget()
prev_content_widget.hide()
prev_content_widget.deleteLater()
self._content_widget = None
self._attr_def_id_to_instances = {}
self._attr_def_id_to_attr_def = {}
result = self._controller.get_creator_attribute_definitions(
self._current_instance_ids
)
content_widget = QtWidgets.QWidget(self._scroll_area)
content_layout = QtWidgets.QGridLayout(content_widget)
content_layout.setColumnStretch(0, 0)
content_layout.setColumnStretch(1, 1)
content_layout.setAlignment(QtCore.Qt.AlignTop)
content_layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING)
content_layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING)
row = 0
for attr_def, instance_ids, values in result:
widget = create_widget_for_attr_def(attr_def, content_widget)
if attr_def.is_value_def:
if len(values) == 1:
value = values[0]
if value is not None:
widget.set_value(values[0])
else:
widget.set_value(values, True)
widget.value_changed.connect(self._input_value_changed)
self._attr_def_id_to_instances[attr_def.id] = instance_ids
self._attr_def_id_to_attr_def[attr_def.id] = attr_def
if not attr_def.visible:
continue
expand_cols = 2
if attr_def.is_value_def and attr_def.is_label_horizontal:
expand_cols = 1
col_num = 2 - expand_cols
label = None
if attr_def.is_value_def:
label = attr_def.label or attr_def.key
if label:
label_widget = QtWidgets.QLabel(label, self)
tooltip = attr_def.tooltip
if tooltip:
label_widget.setToolTip(tooltip)
if attr_def.is_label_horizontal:
label_widget.setAlignment(
QtCore.Qt.AlignRight
| QtCore.Qt.AlignVCenter
)
content_layout.addWidget(
label_widget, row, 0, 1, expand_cols
)
if not attr_def.is_label_horizontal:
row += 1
content_layout.addWidget(
widget, row, col_num, 1, expand_cols
)
row += 1
self._scroll_area.setWidget(content_widget)
self._content_widget = content_widget
def _on_instance_attr_defs_change(self, event):
for instance_id in event.data["instance_ids"]:
if instance_id in self._current_instance_ids:
self._refresh_content()
break
def _on_instance_value_change(self, event):
# TODO try to find more optimized way to update values instead of
# force refresh of all of them.
for instance_id, changes in event["instance_changes"].items():
if (
instance_id in self._current_instance_ids
and "creator_attributes" not in changes
):
self._refresh_content()
break
def _input_value_changed(self, value, attr_id):
instance_ids = self._attr_def_id_to_instances.get(attr_id)
attr_def = self._attr_def_id_to_attr_def.get(attr_id)
if not instance_ids or not attr_def:
return
self._controller.set_instances_create_attr_values(
instance_ids, attr_def.key, value
)
class PublishPluginAttrsWidget(QtWidgets.QWidget):
"""Widget showing publish plugin attributes for selected instances.
Attributes are defined on publish plugins. Publish plugin may define
attribute definitions but must inherit `AYONPyblishPluginMixin`
(~/ayon_core/pipeline/publish). At the moment requires to implement
`get_attribute_defs` and `convert_attribute_values` class methods.
Look and type of attributes is based on attribute definitions that are
defined in `~/ayon_core/lib/attribute_definitions.py` and their
widget representation in `~/ayon_core/tools/attribute_defs/*`.
Widgets are disabled if context of instance is not valid.
Definitions are shown for all instance no matter if they have different
product types. Similar definitions are merged into one (different label
does not count).
"""
def __init__(
self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget
):
super().__init__(parent)
scroll_area = QtWidgets.QScrollArea(self)
scroll_area.setWidgetResizable(True)
main_layout = QtWidgets.QHBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
main_layout.addWidget(scroll_area, 1)
controller.register_event_callback(
"create.context.publish.attrs.changed",
self._on_instance_attr_defs_change
)
controller.register_event_callback(
"create.context.value.changed",
self._on_instance_value_change
)
self._current_instance_ids = set()
self._context_selected = False
self._main_layout = main_layout
self._controller: AbstractPublisherFrontend = controller
self._scroll_area = scroll_area
self._attr_def_id_to_instances = {}
self._attr_def_id_to_attr_def = {}
self._attr_def_id_to_plugin_name = {}
# Store content of scroll area to prevent garbage collection
self._content_widget = None
def set_instances_valid(self, valid):
"""Change valid state of current instances."""
if (
self._content_widget is not None
and self._content_widget.isEnabled() != valid
):
self._content_widget.setEnabled(valid)
def set_current_instances(self, instance_ids, context_selected):
"""Set current instances for which are attribute definitions shown."""
self._current_instance_ids = set(instance_ids)
self._context_selected = context_selected
self._refresh_content()
def _refresh_content(self):
prev_content_widget = self._scroll_area.widget()
if prev_content_widget:
self._scroll_area.takeWidget()
prev_content_widget.hide()
prev_content_widget.deleteLater()
self._content_widget = None
self._attr_def_id_to_instances = {}
self._attr_def_id_to_attr_def = {}
self._attr_def_id_to_plugin_name = {}
result = self._controller.get_publish_attribute_definitions(
self._current_instance_ids, self._context_selected
)
content_widget = QtWidgets.QWidget(self._scroll_area)
attr_def_widget = QtWidgets.QWidget(content_widget)
attr_def_layout = QtWidgets.QGridLayout(attr_def_widget)
attr_def_layout.setColumnStretch(0, 0)
attr_def_layout.setColumnStretch(1, 1)
attr_def_layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING)
attr_def_layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING)
content_layout = QtWidgets.QVBoxLayout(content_widget)
content_layout.addWidget(attr_def_widget, 0)
content_layout.addStretch(1)
row = 0
for plugin_name, attr_defs, all_plugin_values in result:
plugin_values = all_plugin_values[plugin_name]
for attr_def in attr_defs:
widget = create_widget_for_attr_def(
attr_def, content_widget
)
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)
visible_widget = False
if visible_widget:
expand_cols = 2
if attr_def.is_value_def and attr_def.is_label_horizontal:
expand_cols = 1
col_num = 2 - expand_cols
label = None
if attr_def.is_value_def:
label = attr_def.label or attr_def.key
if label:
label_widget = QtWidgets.QLabel(label, content_widget)
tooltip = attr_def.tooltip
if tooltip:
label_widget.setToolTip(tooltip)
if attr_def.is_label_horizontal:
label_widget.setAlignment(
QtCore.Qt.AlignRight
| QtCore.Qt.AlignVCenter
)
attr_def_layout.addWidget(
label_widget, row, 0, 1, expand_cols
)
if not attr_def.is_label_horizontal:
row += 1
attr_def_layout.addWidget(
widget, row, col_num, 1, expand_cols
)
row += 1
if not attr_def.is_value_def:
continue
widget.value_changed.connect(self._input_value_changed)
attr_values = plugin_values[attr_def.key]
multivalue = len(attr_values) > 1
values = []
instances = []
for instance, value in attr_values:
values.append(value)
instances.append(instance)
self._attr_def_id_to_attr_def[attr_def.id] = attr_def
self._attr_def_id_to_instances[attr_def.id] = instances
self._attr_def_id_to_plugin_name[attr_def.id] = plugin_name
if multivalue:
widget.set_value(values, multivalue)
else:
widget.set_value(values[0])
self._scroll_area.setWidget(content_widget)
self._content_widget = content_widget
def _input_value_changed(self, value, attr_id):
instance_ids = self._attr_def_id_to_instances.get(attr_id)
attr_def = self._attr_def_id_to_attr_def.get(attr_id)
plugin_name = self._attr_def_id_to_plugin_name.get(attr_id)
if not instance_ids or not attr_def or not plugin_name:
return
self._controller.set_instances_publish_attr_values(
instance_ids, plugin_name, attr_def.key, value
)
def _on_instance_attr_defs_change(self, event):
for instance_id in event.data:
if (
instance_id is None and self._context_selected
or instance_id in self._current_instance_ids
):
self._refresh_content()
break
def _on_instance_value_change(self, event):
# TODO try to find more optimized way to update values instead of
# force refresh of all of them.
for instance_id, changes in event["instance_changes"].items():
if (
instance_id in self._current_instance_ids
and "publish_attributes" not in changes
):
self._refresh_content()
break

View file

@ -0,0 +1,933 @@
import re
import copy
import collections
from qtpy import QtWidgets, QtCore, QtGui
import qtawesome
from ayon_core.pipeline.create import (
PRODUCT_NAME_ALLOWED_SYMBOLS,
TaskNotSetError,
)
from ayon_core.tools.utils import (
PlaceholderLineEdit,
BaseClickableFrame,
set_style_property,
)
from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend
from ayon_core.tools.publisher.constants import (
VARIANT_TOOLTIP,
INPUTS_LAYOUT_HSPACING,
INPUTS_LAYOUT_VSPACING,
)
from .folders_dialog import FoldersDialog
from .tasks_model import TasksModel
from .widgets import ClickableLineEdit, MultipleItemWidget
class FoldersFields(BaseClickableFrame):
"""Field where folder path of selected instance/s is showed.
Click on the field will trigger `FoldersDialog`.
"""
value_changed = QtCore.Signal()
def __init__(
self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget
):
super().__init__(parent)
self.setObjectName("FolderPathInputWidget")
# Don't use 'self' for parent!
# - this widget has specific styles
dialog = FoldersDialog(controller, parent)
name_input = ClickableLineEdit(self)
name_input.setObjectName("FolderPathInput")
icon_name = "fa.window-maximize"
icon = qtawesome.icon(icon_name, color="white")
icon_btn = QtWidgets.QPushButton(self)
icon_btn.setIcon(icon)
icon_btn.setObjectName("FolderPathInputButton")
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addWidget(name_input, 1)
layout.addWidget(icon_btn, 0)
# Make sure all widgets are vertically extended to highest widget
for widget in (
name_input,
icon_btn
):
size_policy = widget.sizePolicy()
size_policy.setVerticalPolicy(
QtWidgets.QSizePolicy.MinimumExpanding)
widget.setSizePolicy(size_policy)
name_input.clicked.connect(self._mouse_release_callback)
icon_btn.clicked.connect(self._mouse_release_callback)
dialog.finished.connect(self._on_dialog_finish)
self._controller: AbstractPublisherFrontend = controller
self._dialog = dialog
self._name_input = name_input
self._icon_btn = icon_btn
self._origin_value = []
self._origin_selection = []
self._selected_items = []
self._has_value_changed = False
self._is_valid = True
self._multiselection_text = None
def _on_dialog_finish(self, result):
if not result:
return
folder_path = self._dialog.get_selected_folder_path()
if folder_path is None:
return
self._selected_items = [folder_path]
self._has_value_changed = (
self._origin_value != self._selected_items
)
self.set_text(folder_path)
self._set_is_valid(True)
self.value_changed.emit()
def _mouse_release_callback(self):
self._dialog.set_selected_folders(self._selected_items)
self._dialog.open()
def set_multiselection_text(self, text):
"""Change text for multiselection of different folders.
When there are selected multiple instances at once and they don't have
same folder in context.
"""
self._multiselection_text = text
def _set_is_valid(self, valid):
if valid == self._is_valid:
return
self._is_valid = valid
state = ""
if not valid:
state = "invalid"
self._set_state_property(state)
def _set_state_property(self, state):
set_style_property(self, "state", state)
set_style_property(self._name_input, "state", state)
set_style_property(self._icon_btn, "state", state)
def is_valid(self):
"""Is folder valid."""
return self._is_valid
def has_value_changed(self):
"""Value of folder has changed."""
return self._has_value_changed
def get_selected_items(self):
"""Selected folder paths."""
return list(self._selected_items)
def set_text(self, text):
"""Set text in text field.
Does not change selected items (folders).
"""
self._name_input.setText(text)
self._name_input.end(False)
def set_selected_items(self, folder_paths=None):
"""Set folder paths for selection of instances.
Passed folder paths are validated and if there are 2 or more different
folder paths then multiselection text is shown.
Args:
folder_paths (list, tuple, set, NoneType): List of folder paths.
"""
if folder_paths is None:
folder_paths = []
self._has_value_changed = False
self._origin_value = list(folder_paths)
self._selected_items = list(folder_paths)
is_valid = self._controller.are_folder_paths_valid(folder_paths)
if not folder_paths:
self.set_text("")
elif len(folder_paths) == 1:
folder_path = tuple(folder_paths)[0]
self.set_text(folder_path)
else:
multiselection_text = self._multiselection_text
if multiselection_text is None:
multiselection_text = "|".join(folder_paths)
self.set_text(multiselection_text)
self._set_is_valid(is_valid)
def reset_to_origin(self):
"""Change to folder paths set with last `set_selected_items` call."""
self.set_selected_items(self._origin_value)
def confirm_value(self):
self._origin_value = copy.deepcopy(self._selected_items)
self._has_value_changed = False
class TasksComboboxProxy(QtCore.QSortFilterProxyModel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._filter_empty = False
def set_filter_empty(self, filter_empty):
if self._filter_empty is filter_empty:
return
self._filter_empty = filter_empty
self.invalidate()
def filterAcceptsRow(self, source_row, parent_index):
if self._filter_empty:
model = self.sourceModel()
source_index = model.index(
source_row, self.filterKeyColumn(), parent_index
)
if not source_index.data(QtCore.Qt.DisplayRole):
return False
return True
class TasksCombobox(QtWidgets.QComboBox):
"""Combobox to show tasks for selected instances.
Combobox gives ability to select only from intersection of task names for
folder paths in selected instances.
If folder paths in selected instances does not have same tasks then combobox
will be empty.
"""
value_changed = QtCore.Signal()
def __init__(
self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget
):
super().__init__(parent)
self.setObjectName("TasksCombobox")
# Set empty delegate to propagate stylesheet to a combobox
delegate = QtWidgets.QStyledItemDelegate()
self.setItemDelegate(delegate)
model = TasksModel(controller, True)
proxy_model = TasksComboboxProxy()
proxy_model.setSourceModel(model)
self.setModel(proxy_model)
self.currentIndexChanged.connect(self._on_index_change)
self._delegate = delegate
self._model = model
self._proxy_model = proxy_model
self._origin_value = []
self._origin_selection = []
self._selected_items = []
self._has_value_changed = False
self._ignore_index_change = False
self._multiselection_text = None
self._is_valid = True
self._text = None
# Make sure combobox is extended horizontally
size_policy = self.sizePolicy()
size_policy.setHorizontalPolicy(
QtWidgets.QSizePolicy.MinimumExpanding)
self.setSizePolicy(size_policy)
def set_invalid_empty_task(self, invalid=True):
self._proxy_model.set_filter_empty(invalid)
if invalid:
self._set_is_valid(False)
self.set_text(
"< One or more products require Task selected >"
)
else:
self.set_text(None)
def set_multiselection_text(self, text):
"""Change text shown when multiple different tasks are in context."""
self._multiselection_text = text
def _on_index_change(self):
if self._ignore_index_change:
return
self.set_text(None)
text = self.currentText()
idx = self.findText(text)
if idx < 0:
return
self._set_is_valid(True)
self._selected_items = [text]
self._has_value_changed = (
self._origin_selection != self._selected_items
)
self.value_changed.emit()
def set_text(self, text):
"""Set context shown in combobox without changing selected items."""
if text == self._text:
return
self._text = text
self.repaint()
def paintEvent(self, event):
"""Paint custom text without using QLineEdit.
The easiest way how to draw custom text in combobox and keep combobox
properties and event handling.
"""
painter = QtGui.QPainter(self)
painter.setPen(self.palette().color(QtGui.QPalette.Text))
opt = QtWidgets.QStyleOptionComboBox()
self.initStyleOption(opt)
if self._text is not None:
opt.currentText = self._text
style = self.style()
style.drawComplexControl(
QtWidgets.QStyle.CC_ComboBox, opt, painter, self
)
style.drawControl(
QtWidgets.QStyle.CE_ComboBoxLabel, opt, painter, self
)
painter.end()
def is_valid(self):
"""Are all selected items valid."""
return self._is_valid
def has_value_changed(self):
"""Did selection of task changed."""
return self._has_value_changed
def _set_is_valid(self, valid):
if valid == self._is_valid:
return
self._is_valid = valid
state = ""
if not valid:
state = "invalid"
self._set_state_property(state)
def _set_state_property(self, state):
current_value = self.property("state")
if current_value != state:
self.setProperty("state", state)
self.style().polish(self)
def get_selected_items(self):
"""Get selected tasks.
If value has changed then will return list with single item.
Returns:
list: Selected tasks.
"""
return list(self._selected_items)
def set_folder_paths(self, folder_paths):
"""Set folder paths for which should show tasks."""
self._ignore_index_change = True
self._model.set_folder_paths(folder_paths)
self._proxy_model.set_filter_empty(False)
self._proxy_model.sort(0)
self._ignore_index_change = False
# It is a bug if not exactly one folder got here
if len(folder_paths) != 1:
self.set_selected_item("")
self._set_is_valid(False)
return
folder_path = tuple(folder_paths)[0]
is_valid = False
if self._selected_items:
is_valid = True
valid_task_names = []
for task_name in self._selected_items:
_is_valid = self._model.is_task_name_valid(folder_path, task_name)
if _is_valid:
valid_task_names.append(task_name)
else:
is_valid = _is_valid
self._selected_items = valid_task_names
if len(self._selected_items) == 0:
self.set_selected_item("")
elif len(self._selected_items) == 1:
self.set_selected_item(self._selected_items[0])
else:
multiselection_text = self._multiselection_text
if multiselection_text is None:
multiselection_text = "|".join(self._selected_items)
self.set_selected_item(multiselection_text)
self._set_is_valid(is_valid)
def confirm_value(self, folder_paths):
new_task_name = self._selected_items[0]
self._origin_value = [
(folder_path, new_task_name)
for folder_path in folder_paths
]
self._origin_selection = copy.deepcopy(self._selected_items)
self._has_value_changed = False
def set_selected_items(self, folder_task_combinations=None):
"""Set items for selected instances.
Args:
folder_task_combinations (list): List of tuples. Each item in
the list contain folder path and task name.
"""
self._proxy_model.set_filter_empty(False)
self._proxy_model.sort(0)
if folder_task_combinations is None:
folder_task_combinations = []
task_names = set()
task_names_by_folder_path = collections.defaultdict(set)
for folder_path, task_name in folder_task_combinations:
task_names.add(task_name)
task_names_by_folder_path[folder_path].add(task_name)
folder_paths = set(task_names_by_folder_path.keys())
self._ignore_index_change = True
self._model.set_folder_paths(folder_paths)
self._has_value_changed = False
self._origin_value = copy.deepcopy(folder_task_combinations)
self._origin_selection = list(task_names)
self._selected_items = list(task_names)
# Reset current index
self.setCurrentIndex(-1)
is_valid = True
if not task_names:
self.set_selected_item("")
elif len(task_names) == 1:
task_name = tuple(task_names)[0]
idx = self.findText(task_name)
is_valid = not idx < 0
if not is_valid and len(folder_paths) > 1:
is_valid = self._validate_task_names_by_folder_paths(
task_names_by_folder_path
)
self.set_selected_item(task_name)
else:
for task_name in task_names:
idx = self.findText(task_name)
is_valid = not idx < 0
if not is_valid:
break
if not is_valid and len(folder_paths) > 1:
is_valid = self._validate_task_names_by_folder_paths(
task_names_by_folder_path
)
multiselection_text = self._multiselection_text
if multiselection_text is None:
multiselection_text = "|".join(task_names)
self.set_selected_item(multiselection_text)
self._set_is_valid(is_valid)
self._ignore_index_change = False
self.value_changed.emit()
def _validate_task_names_by_folder_paths(self, task_names_by_folder_path):
for folder_path, task_names in task_names_by_folder_path.items():
for task_name in task_names:
if not self._model.is_task_name_valid(folder_path, task_name):
return False
return True
def set_selected_item(self, item_name):
"""Set task which is set on selected instance.
Args:
item_name(str): Task name which should be selected.
"""
idx = self.findText(item_name)
# Set current index (must be set to -1 if is invalid)
self.setCurrentIndex(idx)
self.set_text(item_name)
def reset_to_origin(self):
"""Change to task names set with last `set_selected_items` call."""
self.set_selected_items(self._origin_value)
class VariantInputWidget(PlaceholderLineEdit):
"""Input widget for variant."""
value_changed = QtCore.Signal()
def __init__(self, parent):
super().__init__(parent)
self.setObjectName("VariantInput")
self.setToolTip(VARIANT_TOOLTIP)
name_pattern = "^[{}]*$".format(PRODUCT_NAME_ALLOWED_SYMBOLS)
self._name_pattern = name_pattern
self._compiled_name_pattern = re.compile(name_pattern)
self._origin_value = []
self._current_value = []
self._ignore_value_change = False
self._has_value_changed = False
self._multiselection_text = None
self._is_valid = True
self.textChanged.connect(self._on_text_change)
def is_valid(self):
"""Is variant text valid."""
return self._is_valid
def has_value_changed(self):
"""Value of variant has changed."""
return self._has_value_changed
def _set_state_property(self, state):
current_value = self.property("state")
if current_value != state:
self.setProperty("state", state)
self.style().polish(self)
def set_multiselection_text(self, text):
"""Change text of multiselection."""
self._multiselection_text = text
def confirm_value(self):
self._origin_value = copy.deepcopy(self._current_value)
self._has_value_changed = False
def _set_is_valid(self, valid):
if valid == self._is_valid:
return
self._is_valid = valid
state = ""
if not valid:
state = "invalid"
self._set_state_property(state)
def _on_text_change(self):
if self._ignore_value_change:
return
is_valid = bool(self._compiled_name_pattern.match(self.text()))
self._set_is_valid(is_valid)
self._current_value = [self.text()]
self._has_value_changed = self._current_value != self._origin_value
self.value_changed.emit()
def reset_to_origin(self):
"""Set origin value of selected instances."""
self.set_value(self._origin_value)
def get_value(self):
"""Get current value.
Origin value returned if didn't change.
"""
return copy.deepcopy(self._current_value)
def set_value(self, variants=None):
"""Set value of currently selected instances."""
if variants is None:
variants = []
self._ignore_value_change = True
self._has_value_changed = False
self._origin_value = list(variants)
self._current_value = list(variants)
self.setPlaceholderText("")
if not variants:
self.setText("")
elif len(variants) == 1:
self.setText(self._current_value[0])
else:
multiselection_text = self._multiselection_text
if multiselection_text is None:
multiselection_text = "|".join(variants)
self.setText("")
self.setPlaceholderText(multiselection_text)
self._ignore_value_change = False
class GlobalAttrsWidget(QtWidgets.QWidget):
"""Global attributes mainly to define context and product name of instances.
product name is or may be affected on context. Gives abiity to modify
context and product name of instance. This change is not autopromoted but
must be submitted.
Warning: Until artist hit `Submit` changes must not be propagated to
instance data.
Global attributes contain these widgets:
Variant: [ text input ]
Folder: [ folder dialog ]
Task: [ combobox ]
Product type: [ immutable ]
product name: [ immutable ]
[Submit] [Cancel]
"""
multiselection_text = "< Multiselection >"
unknown_value = "N/A"
def __init__(
self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget
):
super().__init__(parent)
self._controller: AbstractPublisherFrontend = controller
self._current_instances_by_id = {}
self._invalid_task_item_ids = set()
variant_input = VariantInputWidget(self)
folder_value_widget = FoldersFields(controller, self)
task_value_widget = TasksCombobox(controller, self)
product_type_value_widget = MultipleItemWidget(self)
product_value_widget = MultipleItemWidget(self)
variant_input.set_multiselection_text(self.multiselection_text)
folder_value_widget.set_multiselection_text(self.multiselection_text)
task_value_widget.set_multiselection_text(self.multiselection_text)
variant_input.set_value()
folder_value_widget.set_selected_items()
task_value_widget.set_selected_items()
product_type_value_widget.set_value()
product_value_widget.set_value()
submit_btn = QtWidgets.QPushButton("Confirm", self)
cancel_btn = QtWidgets.QPushButton("Cancel", self)
submit_btn.setEnabled(False)
cancel_btn.setEnabled(False)
btns_layout = QtWidgets.QHBoxLayout()
btns_layout.setContentsMargins(0, 0, 0, 0)
btns_layout.addStretch(1)
btns_layout.setSpacing(5)
btns_layout.addWidget(submit_btn)
btns_layout.addWidget(cancel_btn)
main_layout = QtWidgets.QFormLayout(self)
main_layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING)
main_layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING)
main_layout.addRow("Variant", variant_input)
main_layout.addRow("Folder", folder_value_widget)
main_layout.addRow("Task", task_value_widget)
main_layout.addRow("Product type", product_type_value_widget)
main_layout.addRow("Product name", product_value_widget)
main_layout.addRow(btns_layout)
variant_input.value_changed.connect(self._on_variant_change)
folder_value_widget.value_changed.connect(self._on_folder_change)
task_value_widget.value_changed.connect(self._on_task_change)
submit_btn.clicked.connect(self._on_submit)
cancel_btn.clicked.connect(self._on_cancel)
controller.register_event_callback(
"create.context.value.changed",
self._on_instance_value_change
)
self.variant_input = variant_input
self.folder_value_widget = folder_value_widget
self.task_value_widget = task_value_widget
self.product_type_value_widget = product_type_value_widget
self.product_value_widget = product_value_widget
self.submit_btn = submit_btn
self.cancel_btn = cancel_btn
def _on_submit(self):
"""Commit changes for selected instances."""
variant_value = None
folder_path = None
task_name = None
if self.variant_input.has_value_changed():
variant_value = self.variant_input.get_value()[0]
if self.folder_value_widget.has_value_changed():
folder_path = self.folder_value_widget.get_selected_items()[0]
if self.task_value_widget.has_value_changed():
task_name = self.task_value_widget.get_selected_items()[0]
product_names = set()
invalid_tasks = False
folder_paths = []
changes_by_id = {}
for item in self._current_instances_by_id.values():
# Ignore instances that have promised context
if item.has_promised_context:
continue
instance_changes = {}
new_variant_value = item.variant
new_folder_path = item.folder_path
new_task_name = item.task_name
if variant_value is not None:
instance_changes["variant"] = variant_value
new_variant_value = variant_value
if folder_path is not None:
instance_changes["folderPath"] = folder_path
new_folder_path = folder_path
if task_name is not None:
instance_changes["task"] = task_name or None
new_task_name = task_name or None
folder_paths.append(new_folder_path)
try:
new_product_name = self._controller.get_product_name(
item.creator_identifier,
new_variant_value,
new_task_name,
new_folder_path,
item.id,
)
self._invalid_task_item_ids.discard(item.id)
except TaskNotSetError:
self._invalid_task_item_ids.add(item.id)
invalid_tasks = True
product_names.add(item.product_name)
continue
product_names.add(new_product_name)
if item.product_name != new_product_name:
instance_changes["productName"] = new_product_name
if instance_changes:
changes_by_id[item.id] = instance_changes
if invalid_tasks:
self.task_value_widget.set_invalid_empty_task()
self.product_value_widget.set_value(product_names)
self._set_btns_enabled(False)
self._set_btns_visible(invalid_tasks)
if variant_value is not None:
self.variant_input.confirm_value()
if folder_path is not None:
self.folder_value_widget.confirm_value()
if task_name is not None:
self.task_value_widget.confirm_value(folder_paths)
self._controller.set_instances_context_info(changes_by_id)
self._refresh_items()
def _on_cancel(self):
"""Cancel changes and set back to their irigin value."""
self.variant_input.reset_to_origin()
self.folder_value_widget.reset_to_origin()
self.task_value_widget.reset_to_origin()
self._set_btns_enabled(False)
def _on_value_change(self):
any_invalid = (
not self.variant_input.is_valid()
or not self.folder_value_widget.is_valid()
or not self.task_value_widget.is_valid()
)
any_changed = (
self.variant_input.has_value_changed()
or self.folder_value_widget.has_value_changed()
or self.task_value_widget.has_value_changed()
)
self._set_btns_visible(any_changed or any_invalid)
self.cancel_btn.setEnabled(any_changed)
self.submit_btn.setEnabled(not any_invalid)
def _on_variant_change(self):
self._on_value_change()
def _on_folder_change(self):
folder_paths = self.folder_value_widget.get_selected_items()
self.task_value_widget.set_folder_paths(folder_paths)
self._on_value_change()
def _on_task_change(self):
self._on_value_change()
def _set_btns_visible(self, visible):
self.cancel_btn.setVisible(visible)
self.submit_btn.setVisible(visible)
def _set_btns_enabled(self, enabled):
self.cancel_btn.setEnabled(enabled)
self.submit_btn.setEnabled(enabled)
def set_current_instances(self, instances):
"""Set currently selected instances.
Args:
instances (List[InstanceItem]): List of selected instances.
Empty instances tells that nothing or context is selected.
"""
self._set_btns_visible(False)
self._current_instances_by_id = {
instance.id: instance
for instance in instances
}
self._invalid_task_item_ids = set()
self._refresh_content()
def _refresh_items(self):
instance_ids = set(self._current_instances_by_id.keys())
self._current_instances_by_id = (
self._controller.get_instance_items_by_id(instance_ids)
)
def _refresh_content(self):
folder_paths = set()
variants = set()
product_types = set()
product_names = set()
editable = True
if len(self._current_instances_by_id) == 0:
editable = False
folder_task_combinations = []
context_editable = None
invalid_tasks = False
for item in self._current_instances_by_id.values():
if not item.has_promised_context:
context_editable = True
elif context_editable is None:
context_editable = False
if item.id in self._invalid_task_item_ids:
invalid_tasks = True
# NOTE I'm not sure how this can even happen?
if item.creator_identifier is None:
editable = False
variants.add(item.variant or self.unknown_value)
product_types.add(item.product_type or self.unknown_value)
folder_path = item.folder_path or self.unknown_value
task_name = item.task_name or ""
folder_paths.add(folder_path)
folder_task_combinations.append((folder_path, task_name))
product_names.add(item.product_name or self.unknown_value)
if not editable:
context_editable = False
elif context_editable is None:
context_editable = True
self.variant_input.set_value(variants)
# Set context of folder widget
self.folder_value_widget.set_selected_items(folder_paths)
# Set context of task widget
self.task_value_widget.set_selected_items(folder_task_combinations)
self.product_type_value_widget.set_value(product_types)
self.product_value_widget.set_value(product_names)
self.variant_input.setEnabled(editable)
self.folder_value_widget.setEnabled(context_editable)
self.task_value_widget.setEnabled(context_editable)
if invalid_tasks:
self.task_value_widget.set_invalid_empty_task()
if not editable:
folder_tooltip = "Select instances to change folder path."
task_tooltip = "Select instances to change task name."
elif not context_editable:
folder_tooltip = "Folder path is defined by Create plugin."
task_tooltip = "Task is defined by Create plugin."
else:
folder_tooltip = "Change folder path of selected instances."
task_tooltip = "Change task of selected instances."
self.folder_value_widget.setToolTip(folder_tooltip)
self.task_value_widget.setToolTip(task_tooltip)
def _on_instance_value_change(self, event):
if not self._current_instances_by_id:
return
changed = False
for instance_id, changes in event["instance_changes"].items():
if instance_id not in self._current_instances_by_id:
continue
for key in (
"folderPath",
"task",
"variant",
"productType",
"productName",
):
if key in changes:
changed = True
break
if changed:
break
if changed:
self._refresh_items()
self._refresh_content()

View file

@ -0,0 +1,288 @@
import os
import uuid
import shutil
from qtpy import QtWidgets, QtCore
from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend
from .thumbnail_widget import ThumbnailWidget
from .product_context import GlobalAttrsWidget
from .product_attributes import (
CreatorAttrsWidget,
PublishPluginAttrsWidget,
)
class ProductInfoWidget(QtWidgets.QWidget):
"""Wrapper widget where attributes of instance/s are modified.
Global
attributes Thumbnail TOP
Creator Publish
attributes plugin BOTTOM
attributes
"""
convert_requested = QtCore.Signal()
def __init__(
self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget
):
super().__init__(parent)
# TOP PART
top_widget = QtWidgets.QWidget(self)
# Global attributes
global_attrs_widget = GlobalAttrsWidget(controller, top_widget)
thumbnail_widget = ThumbnailWidget(controller, top_widget)
top_layout = QtWidgets.QHBoxLayout(top_widget)
top_layout.setContentsMargins(0, 0, 0, 0)
top_layout.addWidget(global_attrs_widget, 7)
top_layout.addWidget(thumbnail_widget, 3)
# BOTTOM PART
bottom_widget = QtWidgets.QWidget(self)
# Wrap Creator attributes to widget to be able add convert button
creator_widget = QtWidgets.QWidget(bottom_widget)
# Convert button widget (with layout to handle stretch)
convert_widget = QtWidgets.QWidget(creator_widget)
convert_label = QtWidgets.QLabel(creator_widget)
# Set the label text with 'setText' to apply html
convert_label.setText(
(
"Found old publishable products"
" incompatible with new publisher."
"<br/><br/>Press the <b>update products</b> button"
" to automatically update them"
" to be able to publish again."
)
)
convert_label.setWordWrap(True)
convert_label.setAlignment(QtCore.Qt.AlignCenter)
convert_btn = QtWidgets.QPushButton(
"Update products", convert_widget
)
convert_separator = QtWidgets.QFrame(convert_widget)
convert_separator.setObjectName("Separator")
convert_separator.setMinimumHeight(1)
convert_separator.setMaximumHeight(1)
convert_layout = QtWidgets.QGridLayout(convert_widget)
convert_layout.setContentsMargins(5, 0, 5, 0)
convert_layout.setVerticalSpacing(10)
convert_layout.addWidget(convert_label, 0, 0, 1, 3)
convert_layout.addWidget(convert_btn, 1, 1)
convert_layout.addWidget(convert_separator, 2, 0, 1, 3)
convert_layout.setColumnStretch(0, 1)
convert_layout.setColumnStretch(1, 0)
convert_layout.setColumnStretch(2, 1)
# Creator attributes widget
creator_attrs_widget = CreatorAttrsWidget(
controller, creator_widget
)
creator_layout = QtWidgets.QVBoxLayout(creator_widget)
creator_layout.setContentsMargins(0, 0, 0, 0)
creator_layout.addWidget(convert_widget, 0)
creator_layout.addWidget(creator_attrs_widget, 1)
publish_attrs_widget = PublishPluginAttrsWidget(
controller, bottom_widget
)
bottom_separator = QtWidgets.QWidget(bottom_widget)
bottom_separator.setObjectName("Separator")
bottom_separator.setMinimumWidth(1)
bottom_layout = QtWidgets.QHBoxLayout(bottom_widget)
bottom_layout.setContentsMargins(0, 0, 0, 0)
bottom_layout.addWidget(creator_widget, 1)
bottom_layout.addWidget(bottom_separator, 0)
bottom_layout.addWidget(publish_attrs_widget, 1)
top_bottom = QtWidgets.QWidget(self)
top_bottom.setObjectName("Separator")
top_bottom.setMinimumHeight(1)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(top_widget, 0)
layout.addWidget(top_bottom, 0)
layout.addWidget(bottom_widget, 1)
self._convertor_identifiers = None
self._current_instances = []
self._context_selected = False
self._all_instances_valid = True
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
)
self._controller: AbstractPublisherFrontend = controller
self._convert_widget = convert_widget
self.global_attrs_widget = global_attrs_widget
self.creator_attrs_widget = creator_attrs_widget
self.publish_attrs_widget = publish_attrs_widget
self._thumbnail_widget = thumbnail_widget
self.top_bottom = top_bottom
self.bottom_separator = bottom_separator
def set_current_instances(
self, instances, context_selected, convertor_identifiers
):
"""Change currently selected items.
Args:
instances (List[InstanceItem]): List of currently selected
instances.
context_selected (bool): Is context selected.
convertor_identifiers (List[str]): Identifiers of convert items.
"""
s_convertor_identifiers = set(convertor_identifiers)
self._current_instances = instances
self._context_selected = context_selected
self._convertor_identifiers = s_convertor_identifiers
self._refresh_instances()
def _refresh_instances(self):
instance_ids = {
instance.id
for instance in self._current_instances
}
context_info_by_id = self._controller.get_instances_context_info(
instance_ids
)
all_valid = True
for context_info in context_info_by_id.values():
if not context_info.is_valid:
all_valid = False
break
self._all_instances_valid = all_valid
self._convert_widget.setVisible(len(self._convertor_identifiers) > 0)
self.global_attrs_widget.set_current_instances(
self._current_instances
)
self.creator_attrs_widget.set_current_instances(instance_ids)
self.publish_attrs_widget.set_current_instances(
instance_ids, self._context_selected
)
self.creator_attrs_widget.set_instances_valid(all_valid)
self.publish_attrs_widget.set_instances_valid(all_valid)
self._update_thumbnails()
def _on_instance_context_change(self):
instance_ids = {
instance.id
for instance in self._current_instances
}
context_info_by_id = self._controller.get_instances_context_info(
instance_ids
)
all_valid = True
for instance_id, context_info in context_info_by_id.items():
if not context_info.is_valid:
all_valid = False
break
self._all_instances_valid = all_valid
self.creator_attrs_widget.set_instances_valid(all_valid)
self.publish_attrs_widget.set_instances_valid(all_valid)
def _on_convert_click(self):
self.convert_requested.emit()
def _on_thumbnail_create(self, path):
instance_ids = [
instance.id
for instance in self._current_instances
]
if self._context_selected:
instance_ids.append(None)
if not instance_ids:
return
mapping = {}
if len(instance_ids) == 1:
mapping[instance_ids[0]] = path
else:
for instance_id in instance_ids:
root = os.path.dirname(path)
ext = os.path.splitext(path)[-1]
dst_path = os.path.join(root, str(uuid.uuid4()) + ext)
shutil.copy(path, dst_path)
mapping[instance_id] = dst_path
self._controller.set_thumbnail_paths_for_instances(mapping)
def _on_thumbnail_clear(self):
instance_ids = [
instance.id
for instance in self._current_instances
]
if self._context_selected:
instance_ids.append(None)
if not instance_ids:
return
mapping = {
instance_id: None
for instance_id in instance_ids
}
self._controller.set_thumbnail_paths_for_instances(mapping)
def _on_thumbnail_changed(self, event):
self._update_thumbnails()
def _update_thumbnails(self):
instance_ids = [
instance.id
for instance in self._current_instances
]
if self._context_selected:
instance_ids.append(None)
if not instance_ids:
self._thumbnail_widget.setVisible(False)
self._thumbnail_widget.set_current_thumbnails(None)
return
mapping = self._controller.get_thumbnail_paths_for_instances(
instance_ids
)
thumbnail_paths = []
for instance_id in instance_ids:
path = mapping[instance_id]
if path:
thumbnail_paths.append(path)
self._thumbnail_widget.setVisible(True)
self._thumbnail_widget.set_current_thumbnails(thumbnail_paths)

File diff suppressed because it is too large Load diff

View file

@ -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
@ -918,8 +924,8 @@ class PublisherWindow(QtWidgets.QDialog):
active_instances_by_id = {
instance.id: instance
for instance in self._controller.get_instances()
if instance["active"]
for instance in self._controller.get_instance_items()
if instance.is_active
}
context_info_by_id = self._controller.get_instances_context_info(
active_instances_by_id.keys()
@ -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)