mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge pull request #1777 from pypeclub/feature/settings_conditional_dict
Settings conditional dict
This commit is contained in:
commit
e617873543
15 changed files with 1260 additions and 35 deletions
|
|
@ -111,6 +111,7 @@ from .enum_entity import (
|
|||
from .list_entity import ListEntity
|
||||
from .dict_immutable_keys_entity import DictImmutableKeysEntity
|
||||
from .dict_mutable_keys_entity import DictMutableKeysEntity
|
||||
from .dict_conditional import DictConditionalEntity
|
||||
|
||||
from .anatomy_entities import AnatomyEntity
|
||||
|
||||
|
|
@ -166,5 +167,7 @@ __all__ = (
|
|||
|
||||
"DictMutableKeysEntity",
|
||||
|
||||
"DictConditionalEntity",
|
||||
|
||||
"AnatomyEntity"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -136,6 +136,7 @@ class BaseItemEntity(BaseEntity):
|
|||
# Override state defines which values are used, saved and how.
|
||||
# TODO convert to private attribute
|
||||
self._override_state = OverrideState.NOT_DEFINED
|
||||
self._ignore_missing_defaults = None
|
||||
|
||||
# These attributes may change values during existence of an object
|
||||
# Default value, studio override values and project override values
|
||||
|
|
@ -285,7 +286,7 @@ class BaseItemEntity(BaseEntity):
|
|||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_override_state(self, state):
|
||||
def set_override_state(self, state, ignore_missing_defaults):
|
||||
"""Set override state and trigger it on children.
|
||||
|
||||
Method discard all changes in hierarchy and use values, metadata
|
||||
|
|
@ -295,8 +296,15 @@ class BaseItemEntity(BaseEntity):
|
|||
Should start on root entity and when triggered then must be called on
|
||||
all entities in hierarchy.
|
||||
|
||||
Argument `ignore_missing_defaults` should be used when entity has
|
||||
children that are not saved or used all the time but override statu
|
||||
must be changed and children must have any default value.
|
||||
|
||||
Args:
|
||||
state (OverrideState): State to which should be data changed.
|
||||
ignore_missing_defaults (bool): Ignore missing default values.
|
||||
Entity won't raise `DefaultsNotDefined` and
|
||||
`StudioDefaultsNotDefined`.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
|
|
|||
707
openpype/settings/entities/dict_conditional.py
Normal file
707
openpype/settings/entities/dict_conditional.py
Normal file
|
|
@ -0,0 +1,707 @@
|
|||
import copy
|
||||
|
||||
from .lib import (
|
||||
OverrideState,
|
||||
NOT_SET
|
||||
)
|
||||
from openpype.settings.constants import (
|
||||
METADATA_KEYS,
|
||||
M_OVERRIDEN_KEY,
|
||||
KEY_REGEX
|
||||
)
|
||||
from . import (
|
||||
BaseItemEntity,
|
||||
ItemEntity,
|
||||
GUIEntity
|
||||
)
|
||||
from .exceptions import (
|
||||
SchemaDuplicatedKeys,
|
||||
EntitySchemaError,
|
||||
InvalidKeySymbols
|
||||
)
|
||||
|
||||
|
||||
class DictConditionalEntity(ItemEntity):
|
||||
"""Entity represents dictionay with only one persistent key definition.
|
||||
|
||||
The persistent key is enumerator which define rest of children under
|
||||
dictionary. There is not possibility of shared children.
|
||||
|
||||
Entity's keys can't be removed or added. But they may change based on
|
||||
the persistent key. If you're change value manually (key by key) make sure
|
||||
you'll change value of the persistent key as first. It is recommended to
|
||||
use `set` method which handle this for you.
|
||||
|
||||
It is possible to use entity similar way as `dict` object. Returned values
|
||||
are not real settings values but entities representing the value.
|
||||
"""
|
||||
schema_types = ["dict-conditional"]
|
||||
_default_label_wrap = {
|
||||
"use_label_wrap": False,
|
||||
"collapsible": False,
|
||||
"collapsed": True
|
||||
}
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Return entity inder key."""
|
||||
if key == self.enum_key:
|
||||
return self.enum_entity
|
||||
return self.non_gui_children[self.current_enum][key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""Set value of item under key."""
|
||||
if key == self.enum_key:
|
||||
child_obj = self.enum_entity
|
||||
else:
|
||||
child_obj = self.non_gui_children[self.current_enum][key]
|
||||
child_obj.set(value)
|
||||
|
||||
def __iter__(self):
|
||||
"""Iter through keys."""
|
||||
for key in self.keys():
|
||||
yield key
|
||||
|
||||
def __contains__(self, key):
|
||||
"""Check if key is available."""
|
||||
if key == self.enum_key:
|
||||
return True
|
||||
return key in self.non_gui_children[self.current_enum]
|
||||
|
||||
def get(self, key, default=None):
|
||||
"""Safe entity getter by key."""
|
||||
if key == self.enum_key:
|
||||
return self.enum_entity
|
||||
return self.non_gui_children[self.current_enum].get(key, default)
|
||||
|
||||
def keys(self):
|
||||
"""Entity's keys."""
|
||||
keys = list(self.non_gui_children[self.current_enum].keys())
|
||||
keys.insert(0, [self.enum_key])
|
||||
return keys
|
||||
|
||||
def values(self):
|
||||
"""Children entities."""
|
||||
values = [
|
||||
self.enum_entity
|
||||
]
|
||||
for child_entiy in self.non_gui_children[self.current_enum].values():
|
||||
values.append(child_entiy)
|
||||
return values
|
||||
|
||||
def items(self):
|
||||
"""Children entities paired with their key (key, value)."""
|
||||
items = [
|
||||
(self.enum_key, self.enum_entity)
|
||||
]
|
||||
for key, value in self.non_gui_children[self.current_enum].items():
|
||||
items.append((key, value))
|
||||
return items
|
||||
|
||||
def set(self, value):
|
||||
"""Set value."""
|
||||
new_value = self.convert_to_valid_type(value)
|
||||
# First change value of enum key if available
|
||||
if self.enum_key in new_value:
|
||||
self.enum_entity.set(new_value.pop(self.enum_key))
|
||||
|
||||
for _key, _value in new_value.items():
|
||||
self.non_gui_children[self.current_enum][_key].set(_value)
|
||||
|
||||
def _item_initalization(self):
|
||||
self._default_metadata = NOT_SET
|
||||
self._studio_override_metadata = NOT_SET
|
||||
self._project_override_metadata = NOT_SET
|
||||
|
||||
self._ignore_child_changes = False
|
||||
|
||||
# `current_metadata` are still when schema is loaded
|
||||
# - only metadata stored with dict item are gorup overrides in
|
||||
# M_OVERRIDEN_KEY
|
||||
self._current_metadata = {}
|
||||
self._metadata_are_modified = False
|
||||
|
||||
# Entity must be group or in group
|
||||
if (
|
||||
self.group_item is None
|
||||
and not self.is_dynamic_item
|
||||
and not self.is_in_dynamic_item
|
||||
):
|
||||
self.is_group = True
|
||||
|
||||
# Children are stored by key as keys are immutable and are defined by
|
||||
# schema
|
||||
self.valid_value_types = (dict, )
|
||||
self.children = {}
|
||||
self.non_gui_children = {}
|
||||
self.gui_layout = {}
|
||||
|
||||
if self.is_dynamic_item:
|
||||
self.require_key = False
|
||||
|
||||
self.enum_key = self.schema_data.get("enum_key")
|
||||
self.enum_label = self.schema_data.get("enum_label")
|
||||
self.enum_children = self.schema_data.get("enum_children")
|
||||
|
||||
self.enum_entity = None
|
||||
|
||||
self.highlight_content = self.schema_data.get(
|
||||
"highlight_content", False
|
||||
)
|
||||
self.show_borders = self.schema_data.get("show_borders", True)
|
||||
|
||||
self._add_children()
|
||||
|
||||
@property
|
||||
def current_enum(self):
|
||||
"""Current value of enum entity.
|
||||
|
||||
This value define what children are used.
|
||||
"""
|
||||
if self.enum_entity is None:
|
||||
return None
|
||||
return self.enum_entity.value
|
||||
|
||||
def schema_validations(self):
|
||||
"""Validation of schema data."""
|
||||
# Enum key must be defined
|
||||
if self.enum_key is None:
|
||||
raise EntitySchemaError(self, "Key 'enum_key' is not set.")
|
||||
|
||||
# Validate type of enum children
|
||||
if not isinstance(self.enum_children, list):
|
||||
raise EntitySchemaError(
|
||||
self, "Key 'enum_children' must be a list. Got: {}".format(
|
||||
str(type(self.enum_children))
|
||||
)
|
||||
)
|
||||
|
||||
# Without defined enum children entity has nothing to do
|
||||
if not self.enum_children:
|
||||
raise EntitySchemaError(self, (
|
||||
"Key 'enum_children' have empty value. Entity can't work"
|
||||
" without children definitions."
|
||||
))
|
||||
|
||||
children_def_keys = []
|
||||
for children_def in self.enum_children:
|
||||
if not isinstance(children_def, dict):
|
||||
raise EntitySchemaError((
|
||||
"Children definition under key 'enum_children' must"
|
||||
" be a dictionary."
|
||||
))
|
||||
|
||||
if "key" not in children_def:
|
||||
raise EntitySchemaError((
|
||||
"Children definition under key 'enum_children' miss"
|
||||
" 'key' definition."
|
||||
))
|
||||
# We don't validate regex of these keys because they will be stored
|
||||
# as value at the end.
|
||||
key = children_def["key"]
|
||||
if key in children_def_keys:
|
||||
# TODO this hould probably be different exception?
|
||||
raise SchemaDuplicatedKeys(self, key)
|
||||
children_def_keys.append(key)
|
||||
|
||||
# Validate key duplications per each enum item
|
||||
for children in self.children.values():
|
||||
children_keys = set()
|
||||
children_keys.add(self.enum_key)
|
||||
for child_entity in children:
|
||||
if not isinstance(child_entity, BaseItemEntity):
|
||||
continue
|
||||
elif child_entity.key not in children_keys:
|
||||
children_keys.add(child_entity.key)
|
||||
else:
|
||||
raise SchemaDuplicatedKeys(self, child_entity.key)
|
||||
|
||||
# Enum key must match key regex
|
||||
if not KEY_REGEX.match(self.enum_key):
|
||||
raise InvalidKeySymbols(self.path, self.enum_key)
|
||||
|
||||
# Validate all remaining keys with key regex
|
||||
for children_by_key in self.non_gui_children.values():
|
||||
for key in children_by_key.keys():
|
||||
if not KEY_REGEX.match(key):
|
||||
raise InvalidKeySymbols(self.path, key)
|
||||
|
||||
super(DictConditionalEntity, self).schema_validations()
|
||||
# Trigger schema validation on children entities
|
||||
for children in self.children.values():
|
||||
for child_obj in children:
|
||||
child_obj.schema_validations()
|
||||
|
||||
def on_change(self):
|
||||
"""Update metadata on change and pass change to parent."""
|
||||
self._update_current_metadata()
|
||||
|
||||
for callback in self.on_change_callbacks:
|
||||
callback()
|
||||
self.parent.on_child_change(self)
|
||||
|
||||
def on_child_change(self, child_obj):
|
||||
"""Trigger on change callback if child changes are not ignored."""
|
||||
if self._ignore_child_changes:
|
||||
return
|
||||
|
||||
if (
|
||||
child_obj is self.enum_entity
|
||||
or child_obj in self.children[self.current_enum]
|
||||
):
|
||||
self.on_change()
|
||||
|
||||
def _add_children(self):
|
||||
"""Add children from schema data and repare enum items.
|
||||
|
||||
Each enum item must have defined it's children. None are shared across
|
||||
all enum items.
|
||||
|
||||
Nice to have: Have ability to have shared keys across all enum items.
|
||||
|
||||
All children are stored by their enum item.
|
||||
"""
|
||||
# Skip if are not defined
|
||||
# - schema validations should raise and exception
|
||||
if not self.enum_children or not self.enum_key:
|
||||
return
|
||||
|
||||
valid_enum_items = []
|
||||
for item in self.enum_children:
|
||||
if isinstance(item, dict) and "key" in item:
|
||||
valid_enum_items.append(item)
|
||||
|
||||
enum_items = []
|
||||
for item in valid_enum_items:
|
||||
item_key = item["key"]
|
||||
item_label = item.get("label") or item_key
|
||||
enum_items.append({item_key: item_label})
|
||||
|
||||
if not enum_items:
|
||||
return
|
||||
|
||||
# Create Enum child first
|
||||
enum_key = self.enum_key or "invalid"
|
||||
enum_schema = {
|
||||
"type": "enum",
|
||||
"multiselection": False,
|
||||
"enum_items": enum_items,
|
||||
"key": enum_key,
|
||||
"label": self.enum_label or enum_key
|
||||
}
|
||||
|
||||
enum_entity = self.create_schema_object(enum_schema, self)
|
||||
self.enum_entity = enum_entity
|
||||
|
||||
# Create children per each enum item
|
||||
for item in valid_enum_items:
|
||||
item_key = item["key"]
|
||||
# Make sure all keys have set value in these variables
|
||||
# - key 'children' is optional
|
||||
self.non_gui_children[item_key] = {}
|
||||
self.children[item_key] = []
|
||||
self.gui_layout[item_key] = []
|
||||
|
||||
children = item.get("children") or []
|
||||
for children_schema in children:
|
||||
child_obj = self.create_schema_object(children_schema, self)
|
||||
self.children[item_key].append(child_obj)
|
||||
self.gui_layout[item_key].append(child_obj)
|
||||
if isinstance(child_obj, GUIEntity):
|
||||
continue
|
||||
|
||||
self.non_gui_children[item_key][child_obj.key] = child_obj
|
||||
|
||||
def get_child_path(self, child_obj):
|
||||
"""Get hierarchical path of child entity.
|
||||
|
||||
Child must be entity's direct children. This must be possible to get
|
||||
for any children even if not from current enum value.
|
||||
"""
|
||||
if child_obj is self.enum_entity:
|
||||
return "/".join([self.path, self.enum_key])
|
||||
|
||||
result_key = None
|
||||
for children in self.non_gui_children.values():
|
||||
for key, _child_obj in children.items():
|
||||
if _child_obj is child_obj:
|
||||
result_key = key
|
||||
break
|
||||
|
||||
if result_key is None:
|
||||
raise ValueError("Didn't found child {}".format(child_obj))
|
||||
|
||||
return "/".join([self.path, result_key])
|
||||
|
||||
def _update_current_metadata(self):
|
||||
current_metadata = {}
|
||||
for key, child_obj in self.non_gui_children[self.current_enum].items():
|
||||
if self._override_state is OverrideState.DEFAULTS:
|
||||
break
|
||||
|
||||
if not child_obj.is_group:
|
||||
continue
|
||||
|
||||
if (
|
||||
self._override_state is OverrideState.STUDIO
|
||||
and not child_obj.has_studio_override
|
||||
):
|
||||
continue
|
||||
|
||||
if (
|
||||
self._override_state is OverrideState.PROJECT
|
||||
and not child_obj.has_project_override
|
||||
):
|
||||
continue
|
||||
|
||||
if M_OVERRIDEN_KEY not in current_metadata:
|
||||
current_metadata[M_OVERRIDEN_KEY] = []
|
||||
current_metadata[M_OVERRIDEN_KEY].append(key)
|
||||
|
||||
# Define if current metadata are avaialble for current override state
|
||||
metadata = NOT_SET
|
||||
if self._override_state is OverrideState.STUDIO:
|
||||
metadata = self._studio_override_metadata
|
||||
|
||||
elif self._override_state is OverrideState.PROJECT:
|
||||
metadata = self._project_override_metadata
|
||||
|
||||
if metadata is NOT_SET:
|
||||
metadata = {}
|
||||
|
||||
self._metadata_are_modified = current_metadata != metadata
|
||||
self._current_metadata = current_metadata
|
||||
|
||||
def set_override_state(self, state, ignore_missing_defaults):
|
||||
# Trigger override state change of root if is not same
|
||||
if self.root_item.override_state is not state:
|
||||
self.root_item.set_override_state(state)
|
||||
return
|
||||
|
||||
# Change has/had override states
|
||||
self._override_state = state
|
||||
self._ignore_missing_defaults = ignore_missing_defaults
|
||||
|
||||
# Set override state on enum entity first
|
||||
self.enum_entity.set_override_state(state, ignore_missing_defaults)
|
||||
|
||||
# Set override state on other enum children
|
||||
# - these must not raise exception about missing defaults
|
||||
for children_by_key in self.non_gui_children.values():
|
||||
for child_obj in children_by_key.values():
|
||||
child_obj.set_override_state(state, True)
|
||||
|
||||
self._update_current_metadata()
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
output = {
|
||||
self.enum_key: self.enum_entity.value
|
||||
}
|
||||
for key, child_obj in self.non_gui_children[self.current_enum].items():
|
||||
output[key] = child_obj.value
|
||||
return output
|
||||
|
||||
@property
|
||||
def has_unsaved_changes(self):
|
||||
if self._metadata_are_modified:
|
||||
return True
|
||||
|
||||
return self._child_has_unsaved_changes
|
||||
|
||||
@property
|
||||
def _child_has_unsaved_changes(self):
|
||||
if self.enum_entity.has_unsaved_changes:
|
||||
return True
|
||||
|
||||
for child_obj in self.non_gui_children[self.current_enum].values():
|
||||
if child_obj.has_unsaved_changes:
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def has_studio_override(self):
|
||||
return self._child_has_studio_override
|
||||
|
||||
@property
|
||||
def _child_has_studio_override(self):
|
||||
if self._override_state >= OverrideState.STUDIO:
|
||||
if self.enum_entity.has_studio_override:
|
||||
return True
|
||||
|
||||
for child_obj in self.non_gui_children[self.current_enum].values():
|
||||
if child_obj.has_studio_override:
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def has_project_override(self):
|
||||
return self._child_has_project_override
|
||||
|
||||
@property
|
||||
def _child_has_project_override(self):
|
||||
if self._override_state >= OverrideState.PROJECT:
|
||||
if self.enum_entity.has_project_override:
|
||||
return True
|
||||
|
||||
for child_obj in self.non_gui_children[self.current_enum].values():
|
||||
if child_obj.has_project_override:
|
||||
return True
|
||||
return False
|
||||
|
||||
def settings_value(self):
|
||||
if self._override_state is OverrideState.NOT_DEFINED:
|
||||
return NOT_SET
|
||||
|
||||
if self._override_state is OverrideState.DEFAULTS:
|
||||
children_items = [
|
||||
(self.enum_key, self.enum_entity)
|
||||
]
|
||||
for item in self.non_gui_children[self.current_enum].items():
|
||||
children_items.append(item)
|
||||
|
||||
output = {}
|
||||
for key, child_obj in children_items:
|
||||
child_value = child_obj.settings_value()
|
||||
if not child_obj.is_file and not child_obj.file_item:
|
||||
for _key, _value in child_value.items():
|
||||
new_key = "/".join([key, _key])
|
||||
output[new_key] = _value
|
||||
else:
|
||||
output[key] = child_value
|
||||
return output
|
||||
|
||||
if self.is_group:
|
||||
if self._override_state is OverrideState.STUDIO:
|
||||
if not self.has_studio_override:
|
||||
return NOT_SET
|
||||
elif self._override_state is OverrideState.PROJECT:
|
||||
if not self.has_project_override:
|
||||
return NOT_SET
|
||||
|
||||
output = {}
|
||||
children_items = [
|
||||
(self.enum_key, self.enum_entity)
|
||||
]
|
||||
for item in self.non_gui_children[self.current_enum].items():
|
||||
children_items.append(item)
|
||||
|
||||
for key, child_obj in children_items:
|
||||
value = child_obj.settings_value()
|
||||
if value is not NOT_SET:
|
||||
output[key] = value
|
||||
|
||||
if not output:
|
||||
return NOT_SET
|
||||
|
||||
output.update(self._current_metadata)
|
||||
return output
|
||||
|
||||
def _prepare_value(self, value):
|
||||
if value is NOT_SET or self.enum_key not in value:
|
||||
return NOT_SET, NOT_SET
|
||||
|
||||
enum_value = value.get(self.enum_key)
|
||||
if enum_value not in self.non_gui_children:
|
||||
return NOT_SET, NOT_SET
|
||||
|
||||
# Create copy of value before poping values
|
||||
value = copy.deepcopy(value)
|
||||
metadata = {}
|
||||
for key in METADATA_KEYS:
|
||||
if key in value:
|
||||
metadata[key] = value.pop(key)
|
||||
|
||||
enum_value = value.get(self.enum_key)
|
||||
|
||||
old_metadata = metadata.get(M_OVERRIDEN_KEY)
|
||||
if old_metadata:
|
||||
old_metadata_set = set(old_metadata)
|
||||
new_metadata = []
|
||||
non_gui_children = self.non_gui_children[enum_value]
|
||||
for key in non_gui_children.keys():
|
||||
if key in old_metadata:
|
||||
new_metadata.append(key)
|
||||
old_metadata_set.remove(key)
|
||||
|
||||
for key in old_metadata_set:
|
||||
new_metadata.append(key)
|
||||
metadata[M_OVERRIDEN_KEY] = new_metadata
|
||||
|
||||
return value, metadata
|
||||
|
||||
def update_default_value(self, value):
|
||||
"""Update default values.
|
||||
|
||||
Not an api method, should be called by parent.
|
||||
"""
|
||||
value = self._check_update_value(value, "default")
|
||||
self.has_default_value = value is not NOT_SET
|
||||
# TODO add value validation
|
||||
value, metadata = self._prepare_value(value)
|
||||
self._default_metadata = metadata
|
||||
|
||||
if value is NOT_SET:
|
||||
self.enum_entity.update_default_value(value)
|
||||
for children_by_key in self.non_gui_children.values():
|
||||
for child_obj in children_by_key.values():
|
||||
child_obj.update_default_value(value)
|
||||
return
|
||||
|
||||
value_keys = set(value.keys())
|
||||
enum_value = value[self.enum_key]
|
||||
expected_keys = set(self.non_gui_children[enum_value].keys())
|
||||
expected_keys.add(self.enum_key)
|
||||
unknown_keys = value_keys - expected_keys
|
||||
if unknown_keys:
|
||||
self.log.warning(
|
||||
"{} Unknown keys in default values: {}".format(
|
||||
self.path,
|
||||
", ".join("\"{}\"".format(key) for key in unknown_keys)
|
||||
)
|
||||
)
|
||||
|
||||
self.enum_entity.update_default_value(enum_value)
|
||||
for children_by_key in self.non_gui_children.values():
|
||||
for key, child_obj in children_by_key.items():
|
||||
child_value = value.get(key, NOT_SET)
|
||||
child_obj.update_default_value(child_value)
|
||||
|
||||
def update_studio_value(self, value):
|
||||
"""Update studio override values.
|
||||
|
||||
Not an api method, should be called by parent.
|
||||
"""
|
||||
value = self._check_update_value(value, "studio override")
|
||||
value, metadata = self._prepare_value(value)
|
||||
self._studio_override_metadata = metadata
|
||||
self.had_studio_override = metadata is not NOT_SET
|
||||
|
||||
if value is NOT_SET:
|
||||
self.enum_entity.update_studio_value(value)
|
||||
for children_by_key in self.non_gui_children.values():
|
||||
for child_obj in children_by_key.values():
|
||||
child_obj.update_studio_value(value)
|
||||
return
|
||||
|
||||
value_keys = set(value.keys())
|
||||
enum_value = value[self.enum_key]
|
||||
expected_keys = set(self.non_gui_children[enum_value])
|
||||
expected_keys.add(self.enum_key)
|
||||
unknown_keys = value_keys - expected_keys
|
||||
if unknown_keys:
|
||||
self.log.warning(
|
||||
"{} Unknown keys in studio overrides: {}".format(
|
||||
self.path,
|
||||
", ".join("\"{}\"".format(key) for key in unknown_keys)
|
||||
)
|
||||
)
|
||||
|
||||
self.enum_entity.update_studio_value(enum_value)
|
||||
for children_by_key in self.non_gui_children.values():
|
||||
for key, child_obj in children_by_key.items():
|
||||
child_value = value.get(key, NOT_SET)
|
||||
child_obj.update_studio_value(child_value)
|
||||
|
||||
def update_project_value(self, value):
|
||||
"""Update project override values.
|
||||
|
||||
Not an api method, should be called by parent.
|
||||
"""
|
||||
value = self._check_update_value(value, "project override")
|
||||
value, metadata = self._prepare_value(value)
|
||||
self._project_override_metadata = metadata
|
||||
self.had_project_override = metadata is not NOT_SET
|
||||
|
||||
if value is NOT_SET:
|
||||
self.enum_entity.update_project_value(value)
|
||||
for children_by_key in self.non_gui_children.values():
|
||||
for child_obj in children_by_key.values():
|
||||
child_obj.update_project_value(value)
|
||||
return
|
||||
|
||||
value_keys = set(value.keys())
|
||||
enum_value = value[self.enum_key]
|
||||
expected_keys = set(self.non_gui_children[enum_value])
|
||||
expected_keys.add(self.enum_key)
|
||||
unknown_keys = value_keys - expected_keys
|
||||
if unknown_keys:
|
||||
self.log.warning(
|
||||
"{} Unknown keys in project overrides: {}".format(
|
||||
self.path,
|
||||
", ".join("\"{}\"".format(key) for key in unknown_keys)
|
||||
)
|
||||
)
|
||||
|
||||
self.enum_entity.update_project_value(enum_value)
|
||||
for children_by_key in self.non_gui_children.values():
|
||||
for key, child_obj in children_by_key.items():
|
||||
child_value = value.get(key, NOT_SET)
|
||||
child_obj.update_project_value(child_value)
|
||||
|
||||
def _discard_changes(self, on_change_trigger):
|
||||
self._ignore_child_changes = True
|
||||
|
||||
self.enum_entity.discard_changes(on_change_trigger)
|
||||
for children_by_key in self.non_gui_children.values():
|
||||
for child_obj in children_by_key.values():
|
||||
child_obj.discard_changes(on_change_trigger)
|
||||
|
||||
self._ignore_child_changes = False
|
||||
|
||||
def _add_to_studio_default(self, on_change_trigger):
|
||||
self._ignore_child_changes = True
|
||||
|
||||
self.enum_entity.add_to_studio_default(on_change_trigger)
|
||||
for children_by_key in self.non_gui_children.values():
|
||||
for child_obj in children_by_key.values():
|
||||
child_obj.add_to_studio_default(on_change_trigger)
|
||||
|
||||
self._ignore_child_changes = False
|
||||
|
||||
self._update_current_metadata()
|
||||
|
||||
self.parent.on_child_change(self)
|
||||
|
||||
def _remove_from_studio_default(self, on_change_trigger):
|
||||
self._ignore_child_changes = True
|
||||
|
||||
self.enum_entity.remove_from_studio_default(on_change_trigger)
|
||||
for children_by_key in self.non_gui_children.values():
|
||||
for child_obj in children_by_key.values():
|
||||
child_obj.remove_from_studio_default(on_change_trigger)
|
||||
|
||||
self._ignore_child_changes = False
|
||||
|
||||
def _add_to_project_override(self, on_change_trigger):
|
||||
self._ignore_child_changes = True
|
||||
|
||||
self.enum_entity.add_to_project_override(on_change_trigger)
|
||||
for children_by_key in self.non_gui_children.values():
|
||||
for child_obj in children_by_key.values():
|
||||
child_obj.add_to_project_override(on_change_trigger)
|
||||
|
||||
self._ignore_child_changes = False
|
||||
|
||||
self._update_current_metadata()
|
||||
|
||||
self.parent.on_child_change(self)
|
||||
|
||||
def _remove_from_project_override(self, on_change_trigger):
|
||||
if self._override_state is not OverrideState.PROJECT:
|
||||
return
|
||||
|
||||
self._ignore_child_changes = True
|
||||
|
||||
self.enum_entity.remove_from_project_override(on_change_trigger)
|
||||
for children_by_key in self.non_gui_children.values():
|
||||
for child_obj in children_by_key.values():
|
||||
child_obj.remove_from_project_override(on_change_trigger)
|
||||
|
||||
self._ignore_child_changes = False
|
||||
|
||||
def reset_callbacks(self):
|
||||
"""Reset registered callbacks on entity and children."""
|
||||
super(DictConditionalEntity, self).reset_callbacks()
|
||||
for children in self.children.values():
|
||||
for child_entity in children:
|
||||
child_entity.reset_callbacks()
|
||||
|
|
@ -258,7 +258,7 @@ class DictImmutableKeysEntity(ItemEntity):
|
|||
self._metadata_are_modified = current_metadata != metadata
|
||||
self._current_metadata = current_metadata
|
||||
|
||||
def set_override_state(self, state):
|
||||
def set_override_state(self, state, ignore_missing_defaults):
|
||||
# Trigger override state change of root if is not same
|
||||
if self.root_item.override_state is not state:
|
||||
self.root_item.set_override_state(state)
|
||||
|
|
@ -266,9 +266,10 @@ class DictImmutableKeysEntity(ItemEntity):
|
|||
|
||||
# Change has/had override states
|
||||
self._override_state = state
|
||||
self._ignore_missing_defaults = ignore_missing_defaults
|
||||
|
||||
for child_obj in self.non_gui_children.values():
|
||||
child_obj.set_override_state(state)
|
||||
child_obj.set_override_state(state, ignore_missing_defaults)
|
||||
|
||||
self._update_current_metadata()
|
||||
|
||||
|
|
|
|||
|
|
@ -154,7 +154,9 @@ class DictMutableKeysEntity(EndpointEntity):
|
|||
|
||||
def add_key(self, key):
|
||||
new_child = self._add_key(key)
|
||||
new_child.set_override_state(self._override_state)
|
||||
new_child.set_override_state(
|
||||
self._override_state, self._ignore_missing_defaults
|
||||
)
|
||||
self.on_change()
|
||||
return new_child
|
||||
|
||||
|
|
@ -320,7 +322,7 @@ class DictMutableKeysEntity(EndpointEntity):
|
|||
def _metadata_for_current_state(self):
|
||||
return self._get_metadata_for_state(self._override_state)
|
||||
|
||||
def set_override_state(self, state):
|
||||
def set_override_state(self, state, ignore_missing_defaults):
|
||||
# Trigger override state change of root if is not same
|
||||
if self.root_item.override_state is not state:
|
||||
self.root_item.set_override_state(state)
|
||||
|
|
@ -328,14 +330,22 @@ class DictMutableKeysEntity(EndpointEntity):
|
|||
|
||||
# TODO change metadata
|
||||
self._override_state = state
|
||||
self._ignore_missing_defaults = ignore_missing_defaults
|
||||
|
||||
# Ignore if is dynamic item and use default in that case
|
||||
if not self.is_dynamic_item and not self.is_in_dynamic_item:
|
||||
if state > OverrideState.DEFAULTS:
|
||||
if not self.has_default_value:
|
||||
if (
|
||||
not self.has_default_value
|
||||
and not ignore_missing_defaults
|
||||
):
|
||||
raise DefaultsNotDefined(self)
|
||||
|
||||
elif state > OverrideState.STUDIO:
|
||||
if not self.had_studio_override:
|
||||
if (
|
||||
not self.had_studio_override
|
||||
and not ignore_missing_defaults
|
||||
):
|
||||
raise StudioDefaultsNotDefined(self)
|
||||
|
||||
if state is OverrideState.STUDIO:
|
||||
|
|
@ -426,7 +436,7 @@ class DictMutableKeysEntity(EndpointEntity):
|
|||
|
||||
if label:
|
||||
children_label_by_id[child_entity.id] = label
|
||||
child_entity.set_override_state(state)
|
||||
child_entity.set_override_state(state, ignore_missing_defaults)
|
||||
|
||||
self.children_label_by_id = children_label_by_id
|
||||
|
||||
|
|
@ -610,7 +620,9 @@ class DictMutableKeysEntity(EndpointEntity):
|
|||
if not self._can_discard_changes:
|
||||
return
|
||||
|
||||
self.set_override_state(self._override_state)
|
||||
self.set_override_state(
|
||||
self._override_state, self._ignore_missing_defaults
|
||||
)
|
||||
on_change_trigger.append(self.on_change)
|
||||
|
||||
def _add_to_studio_default(self, _on_change_trigger):
|
||||
|
|
@ -645,7 +657,9 @@ class DictMutableKeysEntity(EndpointEntity):
|
|||
if label:
|
||||
children_label_by_id[child_entity.id] = label
|
||||
|
||||
child_entity.set_override_state(self._override_state)
|
||||
child_entity.set_override_state(
|
||||
self._override_state, self._ignore_missing_defaults
|
||||
)
|
||||
|
||||
self.children_label_by_id = children_label_by_id
|
||||
|
||||
|
|
@ -694,7 +708,9 @@ class DictMutableKeysEntity(EndpointEntity):
|
|||
if label:
|
||||
children_label_by_id[child_entity.id] = label
|
||||
|
||||
child_entity.set_override_state(self._override_state)
|
||||
child_entity.set_override_state(
|
||||
self._override_state, self._ignore_missing_defaults
|
||||
)
|
||||
|
||||
self.children_label_by_id = children_label_by_id
|
||||
|
||||
|
|
|
|||
|
|
@ -217,21 +217,28 @@ class InputEntity(EndpointEntity):
|
|||
return True
|
||||
return False
|
||||
|
||||
def set_override_state(self, state):
|
||||
def set_override_state(self, state, ignore_missing_defaults):
|
||||
# Trigger override state change of root if is not same
|
||||
if self.root_item.override_state is not state:
|
||||
self.root_item.set_override_state(state)
|
||||
return
|
||||
|
||||
self._override_state = state
|
||||
self._ignore_missing_defaults = ignore_missing_defaults
|
||||
# Ignore if is dynamic item and use default in that case
|
||||
if not self.is_dynamic_item and not self.is_in_dynamic_item:
|
||||
if state > OverrideState.DEFAULTS:
|
||||
if not self.has_default_value:
|
||||
if (
|
||||
not self.has_default_value
|
||||
and not ignore_missing_defaults
|
||||
):
|
||||
raise DefaultsNotDefined(self)
|
||||
|
||||
elif state > OverrideState.STUDIO:
|
||||
if not self.had_studio_override:
|
||||
if (
|
||||
not self.had_studio_override
|
||||
and not ignore_missing_defaults
|
||||
):
|
||||
raise StudioDefaultsNotDefined(self)
|
||||
|
||||
if state is OverrideState.STUDIO:
|
||||
|
|
|
|||
|
|
@ -150,14 +150,15 @@ class PathEntity(ItemEntity):
|
|||
def value(self):
|
||||
return self.child_obj.value
|
||||
|
||||
def set_override_state(self, state):
|
||||
def set_override_state(self, state, ignore_missing_defaults):
|
||||
# Trigger override state change of root if is not same
|
||||
if self.root_item.override_state is not state:
|
||||
self.root_item.set_override_state(state)
|
||||
return
|
||||
|
||||
self._override_state = state
|
||||
self.child_obj.set_override_state(state)
|
||||
self._ignore_missing_defaults = ignore_missing_defaults
|
||||
self.child_obj.set_override_state(state, ignore_missing_defaults)
|
||||
|
||||
def update_default_value(self, value):
|
||||
self.child_obj.update_default_value(value)
|
||||
|
|
@ -344,25 +345,32 @@ class ListStrictEntity(ItemEntity):
|
|||
return True
|
||||
return False
|
||||
|
||||
def set_override_state(self, state):
|
||||
def set_override_state(self, state, ignore_missing_defaults):
|
||||
# Trigger override state change of root if is not same
|
||||
if self.root_item.override_state is not state:
|
||||
self.root_item.set_override_state(state)
|
||||
return
|
||||
|
||||
self._override_state = state
|
||||
self._ignore_missing_defaults = ignore_missing_defaults
|
||||
# Ignore if is dynamic item and use default in that case
|
||||
if not self.is_dynamic_item and not self.is_in_dynamic_item:
|
||||
if state > OverrideState.DEFAULTS:
|
||||
if not self.has_default_value:
|
||||
if (
|
||||
not self.has_default_value
|
||||
and not ignore_missing_defaults
|
||||
):
|
||||
raise DefaultsNotDefined(self)
|
||||
|
||||
elif state > OverrideState.STUDIO:
|
||||
if not self.had_studio_override:
|
||||
if (
|
||||
not self.had_studio_override
|
||||
and not ignore_missing_defaults
|
||||
):
|
||||
raise StudioDefaultsNotDefined(self)
|
||||
|
||||
for child_entity in self.children:
|
||||
child_entity.set_override_state(state)
|
||||
child_entity.set_override_state(state, ignore_missing_defaults)
|
||||
|
||||
self.initial_value = self.settings_value()
|
||||
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ class SchemasHub:
|
|||
crashed_item = self._crashed_on_load[schema_name]
|
||||
raise KeyError(
|
||||
"Unable to parse schema file \"{}\". {}".format(
|
||||
crashed_item["filpath"], crashed_item["message"]
|
||||
crashed_item["filepath"], crashed_item["message"]
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -176,8 +176,8 @@ class SchemasHub:
|
|||
elif template_name in self._crashed_on_load:
|
||||
crashed_item = self._crashed_on_load[template_name]
|
||||
raise KeyError(
|
||||
"Unable to parse templace file \"{}\". {}".format(
|
||||
crashed_item["filpath"], crashed_item["message"]
|
||||
"Unable to parse template file \"{}\". {}".format(
|
||||
crashed_item["filepath"], crashed_item["message"]
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -345,7 +345,7 @@ class SchemasHub:
|
|||
" One of them crashed on load \"{}\" {}"
|
||||
).format(
|
||||
filename,
|
||||
crashed_item["filpath"],
|
||||
crashed_item["filepath"],
|
||||
crashed_item["message"]
|
||||
))
|
||||
|
||||
|
|
|
|||
|
|
@ -102,7 +102,9 @@ class ListEntity(EndpointEntity):
|
|||
|
||||
def add_new_item(self, idx=None, trigger_change=True):
|
||||
child_obj = self._add_new_item(idx)
|
||||
child_obj.set_override_state(self._override_state)
|
||||
child_obj.set_override_state(
|
||||
self._override_state, self._ignore_missing_defaults
|
||||
)
|
||||
|
||||
if trigger_change:
|
||||
self.on_child_change(child_obj)
|
||||
|
|
@ -205,13 +207,14 @@ class ListEntity(EndpointEntity):
|
|||
self._has_project_override = True
|
||||
self.on_change()
|
||||
|
||||
def set_override_state(self, state):
|
||||
def set_override_state(self, state, ignore_missing_defaults):
|
||||
# Trigger override state change of root if is not same
|
||||
if self.root_item.override_state is not state:
|
||||
self.root_item.set_override_state(state)
|
||||
return
|
||||
|
||||
self._override_state = state
|
||||
self._ignore_missing_defaults = ignore_missing_defaults
|
||||
|
||||
while self.children:
|
||||
self.children.pop(0)
|
||||
|
|
@ -219,11 +222,17 @@ class ListEntity(EndpointEntity):
|
|||
# Ignore if is dynamic item and use default in that case
|
||||
if not self.is_dynamic_item and not self.is_in_dynamic_item:
|
||||
if state > OverrideState.DEFAULTS:
|
||||
if not self.has_default_value:
|
||||
if (
|
||||
not self.has_default_value
|
||||
and not ignore_missing_defaults
|
||||
):
|
||||
raise DefaultsNotDefined(self)
|
||||
|
||||
elif state > OverrideState.STUDIO:
|
||||
if not self.had_studio_override:
|
||||
if (
|
||||
not self.had_studio_override
|
||||
and not ignore_missing_defaults
|
||||
):
|
||||
raise StudioDefaultsNotDefined(self)
|
||||
|
||||
value = NOT_SET
|
||||
|
|
@ -257,7 +266,9 @@ class ListEntity(EndpointEntity):
|
|||
child_obj.update_studio_value(item)
|
||||
|
||||
for child_obj in self.children:
|
||||
child_obj.set_override_state(self._override_state)
|
||||
child_obj.set_override_state(
|
||||
self._override_state, ignore_missing_defaults
|
||||
)
|
||||
|
||||
self.initial_value = self.settings_value()
|
||||
|
||||
|
|
@ -395,7 +406,9 @@ class ListEntity(EndpointEntity):
|
|||
if self.had_studio_override:
|
||||
child_obj.update_studio_value(item)
|
||||
|
||||
child_obj.set_override_state(self._override_state)
|
||||
child_obj.set_override_state(
|
||||
self._override_state, self._ignore_missing_defaults
|
||||
)
|
||||
|
||||
if self._override_state >= OverrideState.PROJECT:
|
||||
self._has_project_override = self.had_project_override
|
||||
|
|
@ -427,7 +440,9 @@ class ListEntity(EndpointEntity):
|
|||
for item in value:
|
||||
child_obj = self._add_new_item()
|
||||
child_obj.update_default_value(item)
|
||||
child_obj.set_override_state(self._override_state)
|
||||
child_obj.set_override_state(
|
||||
self._override_state, self._ignore_missing_defaults
|
||||
)
|
||||
|
||||
self._ignore_child_changes = False
|
||||
|
||||
|
|
@ -460,7 +475,10 @@ class ListEntity(EndpointEntity):
|
|||
child_obj.update_default_value(item)
|
||||
if self._has_studio_override:
|
||||
child_obj.update_studio_value(item)
|
||||
child_obj.set_override_state(self._override_state)
|
||||
child_obj.set_override_state(
|
||||
self._override_state,
|
||||
self._ignore_missing_defaults
|
||||
)
|
||||
|
||||
self._ignore_child_changes = False
|
||||
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ class RootEntity(BaseItemEntity):
|
|||
schema_data, *args, **kwargs
|
||||
)
|
||||
|
||||
def set_override_state(self, state):
|
||||
def set_override_state(self, state, ignore_missing_defaults=None):
|
||||
"""Set override state and trigger it on children.
|
||||
|
||||
Method will discard all changes in hierarchy and use values, metadata
|
||||
|
|
@ -226,9 +226,12 @@ class RootEntity(BaseItemEntity):
|
|||
Args:
|
||||
state (OverrideState): State to which should be data changed.
|
||||
"""
|
||||
if not ignore_missing_defaults:
|
||||
ignore_missing_defaults = False
|
||||
|
||||
self._override_state = state
|
||||
for child_obj in self.non_gui_children.values():
|
||||
child_obj.set_override_state(state)
|
||||
child_obj.set_override_state(state, ignore_missing_defaults)
|
||||
|
||||
def on_change(self):
|
||||
"""Trigger callbacks on change."""
|
||||
|
|
|
|||
|
|
@ -181,6 +181,103 @@
|
|||
}
|
||||
```
|
||||
|
||||
## dict-conditional
|
||||
- is similar to `dict` but has only one child entity that will be always available
|
||||
- the one entity is enumerator of possible values and based on value of the entity are defined and used other children entities
|
||||
- each value of enumerator have defined children that will be used
|
||||
- there is no way how to have shared entities across multiple enum items
|
||||
- value from enumerator is also stored next to other values
|
||||
- to define the key under which will be enum value stored use `enum_key`
|
||||
- `enum_key` must match key regex and any enum item can't have children with same key
|
||||
- `enum_label` is label of the entity for UI purposes
|
||||
- enum items are define with `enum_children`
|
||||
- it's a list where each item represents enum item
|
||||
- all items in `enum_children` must have at least `key` key which represents value stored under `enum_key`
|
||||
- items can define `label` for UI purposes
|
||||
- most important part is that item can define `children` key where are definitions of it's children (`children` value works the same way as in `dict`)
|
||||
- entity must have defined `"label"` if is not used as widget
|
||||
- is set as group if any parent is not group
|
||||
- if `"label"` is entetered there which will be shown in GUI
|
||||
- item with label can be collapsible
|
||||
- that can be set with key `"collapsible"` as `True`/`False` (Default: `True`)
|
||||
- with key `"collapsed"` as `True`/`False` can be set that is collapsed when GUI is opened (Default: `False`)
|
||||
- it is possible to add darker background with `"highlight_content"` (Default: `False`)
|
||||
- darker background has limits of maximum applies after 3-4 nested highlighted items there is not difference in the color
|
||||
- output is dictionary `{the "key": children values}`
|
||||
```
|
||||
# Example
|
||||
{
|
||||
"type": "dict-conditional",
|
||||
"key": "my_key",
|
||||
"label": "My Key",
|
||||
"enum_key": "type",
|
||||
"enum_label": "label",
|
||||
"enum_children": [
|
||||
# Each item must be a dictionary with 'key'
|
||||
{
|
||||
"key": "action",
|
||||
"label": "Action",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"key": "key",
|
||||
"label": "Key"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "label",
|
||||
"label": "Label"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "command",
|
||||
"label": "Comand"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "menu",
|
||||
"label": "Menu",
|
||||
"children": [
|
||||
{
|
||||
"key": "children",
|
||||
"label": "Children",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
# Separator does not have children as "separator" value is enough
|
||||
"key": "separator",
|
||||
"label": "Separator"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
How output of the schema could look like on save:
|
||||
```
|
||||
{
|
||||
"type": "separator"
|
||||
}
|
||||
|
||||
{
|
||||
"type": "action",
|
||||
"key": "action_1",
|
||||
"label": "Action 1",
|
||||
"command": "run command -arg"
|
||||
}
|
||||
|
||||
{
|
||||
"type": "menu",
|
||||
"children": [
|
||||
"child_1",
|
||||
"child_2"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Inputs for setting any kind of value (`Pure` inputs)
|
||||
- all these input must have defined `"key"` under which will be stored and `"label"` which will be shown next to input
|
||||
- unless they are used in different types of inputs (later) "as widgets" in that case `"key"` and `"label"` are not required as there is not place where to set them
|
||||
|
|
|
|||
|
|
@ -9,6 +9,54 @@
|
|||
"label": "Color input",
|
||||
"type": "color"
|
||||
},
|
||||
{
|
||||
"type": "dict-conditional",
|
||||
"use_label_wrap": true,
|
||||
"collapsible": true,
|
||||
"key": "menu_items",
|
||||
"label": "Menu items",
|
||||
"enum_key": "type",
|
||||
"enum_label": "Type",
|
||||
"enum_children": [
|
||||
{
|
||||
"key": "action",
|
||||
"label": "Action",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"key": "key",
|
||||
"label": "Key"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "label",
|
||||
"label": "Label"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "command",
|
||||
"label": "Comand"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "menu",
|
||||
"label": "Menu",
|
||||
"children": [
|
||||
{
|
||||
"key": "children",
|
||||
"label": "Children",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "separator",
|
||||
"label": "Separator"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "schema_template_exaples",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from openpype.settings.entities import (
|
|||
GUIEntity,
|
||||
DictImmutableKeysEntity,
|
||||
DictMutableKeysEntity,
|
||||
DictConditionalEntity,
|
||||
ListEntity,
|
||||
PathEntity,
|
||||
ListStrictEntity,
|
||||
|
|
@ -35,6 +36,7 @@ from .base import GUIWidget
|
|||
from .list_item_widget import ListWidget
|
||||
from .list_strict_widget import ListStrictWidget
|
||||
from .dict_mutable_widget import DictMutableKeysWidget
|
||||
from .dict_conditional import DictConditionalWidget
|
||||
from .item_widgets import (
|
||||
BoolWidget,
|
||||
DictImmutableKeysWidget,
|
||||
|
|
@ -100,6 +102,9 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
if isinstance(entity, GUIEntity):
|
||||
return GUIWidget(*args)
|
||||
|
||||
elif isinstance(entity, DictConditionalEntity):
|
||||
return DictConditionalWidget(*args)
|
||||
|
||||
elif isinstance(entity, DictImmutableKeysEntity):
|
||||
return DictImmutableKeysWidget(*args)
|
||||
|
||||
|
|
|
|||
304
openpype/tools/settings/settings/dict_conditional.py
Normal file
304
openpype/tools/settings/settings/dict_conditional.py
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
from Qt import QtWidgets
|
||||
|
||||
from .widgets import (
|
||||
ExpandingWidget,
|
||||
GridLabelWidget
|
||||
)
|
||||
from .wrapper_widgets import (
|
||||
WrapperWidget,
|
||||
CollapsibleWrapper,
|
||||
FormWrapper
|
||||
)
|
||||
from .base import BaseWidget
|
||||
from openpype.tools.settings import CHILD_OFFSET
|
||||
|
||||
|
||||
class DictConditionalWidget(BaseWidget):
|
||||
def create_ui(self):
|
||||
self.input_fields = []
|
||||
|
||||
self._content_by_enum_value = {}
|
||||
self._last_enum_value = None
|
||||
|
||||
self.label_widget = None
|
||||
self.body_widget = None
|
||||
self.content_widget = None
|
||||
self.content_layout = None
|
||||
|
||||
label = None
|
||||
if self.entity.is_dynamic_item:
|
||||
self._ui_as_dynamic_item()
|
||||
|
||||
elif self.entity.use_label_wrap:
|
||||
self._ui_label_wrap()
|
||||
|
||||
else:
|
||||
self._ui_item_base()
|
||||
label = self.entity.label
|
||||
|
||||
self._parent_widget_by_entity_id = {}
|
||||
self._enum_key_by_wrapper_id = {}
|
||||
self._added_wrapper_ids = set()
|
||||
|
||||
self.content_layout.setColumnStretch(0, 0)
|
||||
self.content_layout.setColumnStretch(1, 1)
|
||||
|
||||
# Add enum entity to layout mapping
|
||||
enum_entity = self.entity.enum_entity
|
||||
self._parent_widget_by_entity_id[enum_entity.id] = self.content_widget
|
||||
|
||||
# Add rest of entities to wrapper mappings
|
||||
for enum_key, children in self.entity.gui_layout.items():
|
||||
parent_widget_by_entity_id = {}
|
||||
|
||||
content_widget = QtWidgets.QWidget(self.content_widget)
|
||||
content_layout = QtWidgets.QGridLayout(content_widget)
|
||||
content_layout.setColumnStretch(0, 0)
|
||||
content_layout.setColumnStretch(1, 1)
|
||||
content_layout.setContentsMargins(0, 0, 0, 0)
|
||||
content_layout.setSpacing(5)
|
||||
|
||||
self._content_by_enum_value[enum_key] = {
|
||||
"widget": content_widget,
|
||||
"layout": content_layout
|
||||
}
|
||||
|
||||
self._prepare_entity_layouts(
|
||||
children,
|
||||
content_widget,
|
||||
parent_widget_by_entity_id
|
||||
)
|
||||
for item_id in parent_widget_by_entity_id.keys():
|
||||
self._enum_key_by_wrapper_id[item_id] = enum_key
|
||||
self._parent_widget_by_entity_id.update(parent_widget_by_entity_id)
|
||||
|
||||
enum_input_field = self.create_ui_for_entity(
|
||||
self.category_widget, self.entity.enum_entity, self
|
||||
)
|
||||
self.enum_input_field = enum_input_field
|
||||
self.input_fields.append(enum_input_field)
|
||||
|
||||
for item_key, children in self.entity.children.items():
|
||||
content_widget = self._content_by_enum_value[item_key]["widget"]
|
||||
row = self.content_layout.rowCount()
|
||||
self.content_layout.addWidget(content_widget, row, 0, 1, 2)
|
||||
|
||||
for child_obj in children:
|
||||
self.input_fields.append(
|
||||
self.create_ui_for_entity(
|
||||
self.category_widget, child_obj, self
|
||||
)
|
||||
)
|
||||
|
||||
if self.entity.use_label_wrap and self.content_layout.count() == 0:
|
||||
self.body_widget.hide_toolbox(True)
|
||||
|
||||
self.entity_widget.add_widget_to_layout(self, label)
|
||||
|
||||
def _prepare_entity_layouts(
|
||||
self, gui_layout, widget, parent_widget_by_entity_id
|
||||
):
|
||||
for child in gui_layout:
|
||||
if not isinstance(child, dict):
|
||||
parent_widget_by_entity_id[child.id] = widget
|
||||
continue
|
||||
|
||||
if child["type"] == "collapsible-wrap":
|
||||
wrapper = CollapsibleWrapper(child, widget)
|
||||
|
||||
elif child["type"] == "form":
|
||||
wrapper = FormWrapper(child, widget)
|
||||
|
||||
else:
|
||||
raise KeyError(
|
||||
"Unknown Wrapper type \"{}\"".format(child["type"])
|
||||
)
|
||||
|
||||
parent_widget_by_entity_id[wrapper.id] = widget
|
||||
|
||||
self._prepare_entity_layouts(
|
||||
child["children"], wrapper, parent_widget_by_entity_id
|
||||
)
|
||||
|
||||
def _ui_item_base(self):
|
||||
self.setObjectName("DictInvisible")
|
||||
|
||||
self.content_widget = self
|
||||
self.content_layout = QtWidgets.QGridLayout(self)
|
||||
self.content_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.content_layout.setSpacing(5)
|
||||
|
||||
def _ui_as_dynamic_item(self):
|
||||
content_widget = QtWidgets.QWidget(self)
|
||||
content_widget.setObjectName("DictAsWidgetBody")
|
||||
|
||||
show_borders = str(int(self.entity.show_borders))
|
||||
content_widget.setProperty("show_borders", show_borders)
|
||||
|
||||
label_widget = QtWidgets.QLabel(self.entity.label)
|
||||
|
||||
content_layout = QtWidgets.QGridLayout(content_widget)
|
||||
content_layout.setContentsMargins(5, 5, 5, 5)
|
||||
|
||||
main_layout = QtWidgets.QHBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.setSpacing(5)
|
||||
main_layout.addWidget(content_widget)
|
||||
|
||||
self.label_widget = label_widget
|
||||
self.content_widget = content_widget
|
||||
self.content_layout = content_layout
|
||||
|
||||
def _ui_label_wrap(self):
|
||||
content_widget = QtWidgets.QWidget(self)
|
||||
content_widget.setObjectName("ContentWidget")
|
||||
|
||||
if self.entity.highlight_content:
|
||||
content_state = "hightlighted"
|
||||
bottom_margin = 5
|
||||
else:
|
||||
content_state = ""
|
||||
bottom_margin = 0
|
||||
content_widget.setProperty("content_state", content_state)
|
||||
content_layout_margins = (CHILD_OFFSET, 5, 0, bottom_margin)
|
||||
|
||||
body_widget = ExpandingWidget(self.entity.label, self)
|
||||
label_widget = body_widget.label_widget
|
||||
body_widget.set_content_widget(content_widget)
|
||||
|
||||
content_layout = QtWidgets.QGridLayout(content_widget)
|
||||
content_layout.setContentsMargins(*content_layout_margins)
|
||||
|
||||
main_layout = QtWidgets.QHBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.setSpacing(0)
|
||||
main_layout.addWidget(body_widget)
|
||||
|
||||
self.label_widget = label_widget
|
||||
self.body_widget = body_widget
|
||||
self.content_widget = content_widget
|
||||
self.content_layout = content_layout
|
||||
|
||||
if self.entity.collapsible:
|
||||
if not self.entity.collapsed:
|
||||
body_widget.toggle_content()
|
||||
else:
|
||||
body_widget.hide_toolbox(hide_content=False)
|
||||
|
||||
def add_widget_to_layout(self, widget, label=None):
|
||||
if not widget.entity:
|
||||
map_id = widget.id
|
||||
else:
|
||||
map_id = widget.entity.id
|
||||
|
||||
content_widget = self.content_widget
|
||||
content_layout = self.content_layout
|
||||
if map_id != self.entity.enum_entity.id:
|
||||
enum_value = self._enum_key_by_wrapper_id[map_id]
|
||||
content_widget = self._content_by_enum_value[enum_value]["widget"]
|
||||
content_layout = self._content_by_enum_value[enum_value]["layout"]
|
||||
|
||||
wrapper = self._parent_widget_by_entity_id[map_id]
|
||||
if wrapper is not content_widget:
|
||||
wrapper.add_widget_to_layout(widget, label)
|
||||
if wrapper.id not in self._added_wrapper_ids:
|
||||
self.add_widget_to_layout(wrapper)
|
||||
self._added_wrapper_ids.add(wrapper.id)
|
||||
return
|
||||
|
||||
row = content_layout.rowCount()
|
||||
if not label or isinstance(widget, WrapperWidget):
|
||||
content_layout.addWidget(widget, row, 0, 1, 2)
|
||||
else:
|
||||
label_widget = GridLabelWidget(label, widget)
|
||||
label_widget.input_field = widget
|
||||
widget.label_widget = label_widget
|
||||
content_layout.addWidget(label_widget, row, 0, 1, 1)
|
||||
content_layout.addWidget(widget, row, 1, 1, 1)
|
||||
|
||||
def set_entity_value(self):
|
||||
for input_field in self.input_fields:
|
||||
input_field.set_entity_value()
|
||||
|
||||
self._on_entity_change()
|
||||
|
||||
def hierarchical_style_update(self):
|
||||
self.update_style()
|
||||
for input_field in self.input_fields:
|
||||
input_field.hierarchical_style_update()
|
||||
|
||||
def update_style(self):
|
||||
if not self.body_widget and not self.label_widget:
|
||||
return
|
||||
|
||||
if self.entity.group_item:
|
||||
group_item = self.entity.group_item
|
||||
has_unsaved_changes = group_item.has_unsaved_changes
|
||||
has_project_override = group_item.has_project_override
|
||||
has_studio_override = group_item.has_studio_override
|
||||
else:
|
||||
has_unsaved_changes = self.entity.has_unsaved_changes
|
||||
has_project_override = self.entity.has_project_override
|
||||
has_studio_override = self.entity.has_studio_override
|
||||
|
||||
style_state = self.get_style_state(
|
||||
self.is_invalid,
|
||||
has_unsaved_changes,
|
||||
has_project_override,
|
||||
has_studio_override
|
||||
)
|
||||
if self._style_state == style_state:
|
||||
return
|
||||
|
||||
self._style_state = style_state
|
||||
|
||||
if self.body_widget:
|
||||
if style_state:
|
||||
child_style_state = "child-{}".format(style_state)
|
||||
else:
|
||||
child_style_state = ""
|
||||
|
||||
self.body_widget.side_line_widget.setProperty(
|
||||
"state", child_style_state
|
||||
)
|
||||
self.body_widget.side_line_widget.style().polish(
|
||||
self.body_widget.side_line_widget
|
||||
)
|
||||
|
||||
# There is nothing to care if there is no label
|
||||
if not self.label_widget:
|
||||
return
|
||||
|
||||
# Don't change label if is not group or under group item
|
||||
if not self.entity.is_group and not self.entity.group_item:
|
||||
return
|
||||
|
||||
self.label_widget.setProperty("state", style_state)
|
||||
self.label_widget.style().polish(self.label_widget)
|
||||
|
||||
def _on_entity_change(self):
|
||||
enum_value = self.enum_input_field.entity.value
|
||||
if enum_value == self._last_enum_value:
|
||||
return
|
||||
|
||||
self._last_enum_value = enum_value
|
||||
for item_key, content in self._content_by_enum_value.items():
|
||||
widget = content["widget"]
|
||||
widget.setVisible(item_key == enum_value)
|
||||
|
||||
@property
|
||||
def is_invalid(self):
|
||||
return self._is_invalid or self._child_invalid
|
||||
|
||||
@property
|
||||
def _child_invalid(self):
|
||||
for input_field in self.input_fields:
|
||||
if input_field.is_invalid:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_invalid(self):
|
||||
invalid = []
|
||||
for input_field in self.input_fields:
|
||||
invalid.extend(input_field.get_invalid())
|
||||
return invalid
|
||||
|
|
@ -145,7 +145,7 @@ class DictImmutableKeysWidget(BaseWidget):
|
|||
self.content_widget = content_widget
|
||||
self.content_layout = content_layout
|
||||
|
||||
if len(self.input_fields) == 1 and self.checkbox_widget:
|
||||
if len(self.input_fields) == 1 and self.checkbox_child:
|
||||
body_widget.hide_toolbox(hide_content=True)
|
||||
|
||||
elif self.entity.collapsible:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue