mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 12:54:40 +01:00
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:
commit
d80ae9fab6
19 changed files with 3620 additions and 2083 deletions
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
376
client/ayon_core/tools/publisher/widgets/product_attributes.py
Normal file
376
client/ayon_core/tools/publisher/widgets/product_attributes.py
Normal 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
|
||||
933
client/ayon_core/tools/publisher/widgets/product_context.py
Normal file
933
client/ayon_core/tools/publisher/widgets/product_context.py
Normal 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()
|
||||
288
client/ayon_core/tools/publisher/widgets/product_info.py
Normal file
288
client/ayon_core/tools/publisher/widgets/product_info.py
Normal 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
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue