diff --git a/pype/api.py b/pype/api.py index ee49240395..71c31efa93 100644 --- a/pype/api.py +++ b/pype/api.py @@ -5,7 +5,8 @@ from .settings import ( get_anatomy_settings, get_environments, - SystemSettings + SystemSettings, + ProjectSettings ) from .lib import ( PypeLogger, diff --git a/pype/settings/__init__.py b/pype/settings/__init__.py index 56f342558e..b4187829fc 100644 --- a/pype/settings/__init__.py +++ b/pype/settings/__init__.py @@ -5,7 +5,10 @@ from .lib import ( get_anatomy_settings, get_environments ) -from .entities import SystemSettings +from .entities import ( + SystemSettings, + ProjectSettings +) __all__ = ( @@ -15,5 +18,6 @@ __all__ = ( "get_anatomy_settings", "get_environments", - "SystemSettings" + "SystemSettings", + "ProjectSettings" ) diff --git a/pype/settings/defaults/project_settings/ftrack/project_schemas/default.json b/pype/settings/defaults/project_settings/ftrack/project_schemas/default.json deleted file mode 100644 index a90a0b3a8b..0000000000 --- a/pype/settings/defaults/project_settings/ftrack/project_schemas/default.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "object_types": ["Milestone", "Task", "Folder", "Asset Build", "Shot", "Library", "Sequence"], - "version_workflow": ["Pending Review", "Client Review", "On Farm", "Reviewed", "Render Complete", "Approved", "CBB", "Delivered", "Render Failed", "data"], - "task_workflow": ["Not Ready", "Ready", "Change Requested", "In progress", "Pending Review", "On Farm", "Waiting", "Render Complete", "Complete", "CBB", "On Hold", "Render Failed", "Omitted"], - "overrides": [{ - "task_types": ["Animation"], - "statuses": ["Not Ready", "Ready", "Change Requested", "Blocking", "Animating", "blocking review", "anim review", "Complete", "CBB", "On Hold", "Omitted"] - }, { - "task_types": ["Lighting"], - "statuses": ["Not Ready", "Ready", "Change Requested", "In progress", "To render", "On Farm", "Render Complete", "Complete", "CBB", "On Hold", "Render Failed", "Omitted"] - }], - "task_type_schema": ["Layout", "Animation", "Modeling", "Previz", "Lookdev", "FX", "Lighting", "Compositing", "Rigging", "Texture", "Matte-paint", "Roto-paint", "Art", "Match-moving", "Production", "Build", "Setdress", "Edit", "R&D", "Boards"], - "schemas": [{ - "object_type": "Shot", - "statuses": ["Omitted", "Normal", "Complete"], - "task_types": [] - }, { - "object_type": "Asset Build", - "statuses": ["Omitted", "Normal", "Complete"], - "task_types": ["Setups", "Sets", "Characters", "Props", "Locations", "Assembly", "R&D", "Elements"] - }, { - "object_type": "Milestone", - "statuses": ["Normal", "Complete"], - "task_types": ["Generic"] - }], - "task_templates": [{ - "name": "Character", - "task_types": ["Art", "Modeling", "Lookdev", "Rigging"] - }, { - "name": "Element", - "task_types": ["Modeling", "Lookdev"] - }, { - "name": "Prop", - "task_types": ["Modeling", "Lookdev", "Rigging"] - }, { - "name": "Location", - "task_types": ["Layout", "Setdress"] - }] -} \ No newline at end of file diff --git a/pype/settings/defaults/project_settings/standalonepublisher.json b/pype/settings/defaults/project_settings/standalonepublisher.json index 67b4826692..ad1b5e82b2 100644 --- a/pype/settings/defaults/project_settings/standalonepublisher.json +++ b/pype/settings/defaults/project_settings/standalonepublisher.json @@ -10,18 +10,6 @@ } }, "create": { - "__dynamic_keys_labels__": { - "create_workfile": "Workfile", - "create_model": "Model", - "create_rig": "Rig", - "create_pointcache": "Pointcache", - "create_plate": "Plate", - "create_camera": "Camera", - "create_editorial": "Editorial", - "create_image": "Image", - "create_matchmove": "Matchmove", - "": "" - }, "create_workfile": { "name": "workfile", "label": "Workfile", @@ -121,6 +109,17 @@ "Mocap" ], "help": "Script exported from matchmoving application" + }, + "__dynamic_keys_labels__": { + "create_workfile": "Workfile", + "create_model": "Model", + "create_rig": "Rig", + "create_pointcache": "Pointcache", + "create_plate": "Plate", + "create_camera": "Camera", + "create_editorial": "Editorial", + "create_image": "Image", + "create_matchmove": "Matchmove" } } } \ No newline at end of file diff --git a/pype/settings/entities/__init__.py b/pype/settings/entities/__init__.py index 2c53416cbd..f67286832c 100644 --- a/pype/settings/entities/__init__.py +++ b/pype/settings/entities/__init__.py @@ -55,6 +55,7 @@ print(system_settings["general"]["studio_name"].value) from .exceptions import ( DefaultsNotDefined, + StudioDefaultsNotDefined, InvalidValueType, SchemaMissingFileInfo, SchemeGroupHierarchyBug, @@ -73,7 +74,10 @@ from .base_entity import ( ItemEntity ) -from .root_entities import SystemSettings +from .root_entities import ( + SystemSettings, + ProjectSettings +) from .item_entities import ( PathEntity, @@ -96,9 +100,16 @@ from .list_entity import ListEntity from .dict_immutable_keys_entity import DictImmutableKeysEntity from .dict_mutable_keys_entity import DictMutableKeysEntity +from .anatomy_entities import ( + AnatomyEntity, + AnatomyRootsEntity, + AnatomyTemplatesEntity +) + __all__ = ( "DefaultsNotDefined", + "StudioDefaultsNotDefined", "InvalidValueType", "SchemaMissingFileInfo", "SchemeGroupHierarchyBug", @@ -115,6 +126,7 @@ __all__ = ( "ItemEntity", "SystemSettings", + "ProjectSettings", "PathEntity", "ListStrictEntity", @@ -133,5 +145,9 @@ __all__ = ( "DictImmutableKeysEntity", - "DictMutableKeysEntity" + "DictMutableKeysEntity", + + "AnatomyEntity", + "AnatomyRootsEntity", + "AnatomyTemplatesEntity" ) diff --git a/pype/settings/entities/anatomy_entities.py b/pype/settings/entities/anatomy_entities.py new file mode 100644 index 0000000000..1b98bda4dd --- /dev/null +++ b/pype/settings/entities/anatomy_entities.py @@ -0,0 +1,102 @@ +from .dict_immutable_keys_entity import DictImmutableKeysEntity +from .dict_mutable_keys_entity import DictMutableKeysEntity + + +class AnatomyEntity(DictImmutableKeysEntity): + schema_types = ["anatomy"] + + def _item_initalization(self): + self._roots_entity = None + self._templates_entity = None + + super(AnatomyEntity, self)._item_initalization() + + @property + def roots_entity(self): + if self._roots_entity is None: + _roots_entity = None + for child_entity in self.non_gui_children.values(): + if isinstance(child_entity, AnatomyRootsEntity): + _roots_entity = child_entity + break + + if _roots_entity is None: + raise KeyError( + "AnatomyEntity does not contain AnatomyRootsEntity" + ) + + self._roots_entity = _roots_entity + return self._roots_entity + + @property + def templates_entity(self): + if self._templates_entity is None: + _templates_entity = None + for child_entity in self.non_gui_children.values(): + if isinstance(child_entity, AnatomyTemplatesEntity): + _templates_entity = child_entity + break + + if _templates_entity is None: + raise KeyError( + "AnatomyEntity does not contain AnatomyRootsEntity" + ) + + self._templates_entity = _templates_entity + return self._templates_entity + + +class AnatomyRootsEntity(DictMutableKeysEntity): + schema_types = ["anatomy_roots"] + + def schema_validations(self): + if not isinstance(self.parent, AnatomyEntity): + raise TypeError("Parent of {} is not AnatomyEntity object".format( + self.__class__.__name__ + )) + super(AnatomyRootsEntity, self).schema_validations() + + @property + def has_studio_override(self): + output = super(AnatomyRootsEntity, self).has_studio_override + if not output: + output = self.parent.templates_entity._child_has_studio_override + return output + + @property + def has_project_override(self): + output = super(AnatomyRootsEntity, self).has_project_override + if not output: + output = self.parent.templates_entity._child_has_project_override + return output + + +class AnatomyTemplatesEntity(DictImmutableKeysEntity): + schema_types = ["anatomy_templates"] + + def schema_validations(self): + if not isinstance(self.parent, AnatomyEntity): + raise TypeError("Parent of {} is not AnatomyEntity object".format( + self.__class__.__name__ + )) + super(AnatomyTemplatesEntity, self).schema_validations() + + @property + def has_studio_override(self): + output = super(AnatomyTemplatesEntity, self).has_studio_override + if not output: + output = ( + self.parent.roots_entity._has_studio_override + or self.parent.roots_entity._child_has_studio_override + ) + return output + + @property + def has_project_override(self): + output = super(AnatomyTemplatesEntity, self).has_project_override + if not output: + output = ( + self.parent.roots_entity._has_project_override + or self.parent.roots_entity._child_has_project_override + ) + return output diff --git a/pype/settings/entities/base_entity.py b/pype/settings/entities/base_entity.py index 763d67494c..b2be6819d5 100644 --- a/pype/settings/entities/base_entity.py +++ b/pype/settings/entities/base_entity.py @@ -57,7 +57,7 @@ class GUIEntity(BaseEntity): """Entity without any specific logic that should be handled only in GUI.""" gui_type = True - schema_types = ["divider", "splitter", "label"] + schema_types = ["separator", "splitter", "label"] def __getitem__(self, key): return self.schema_data[key] @@ -247,6 +247,14 @@ class BaseItemEntity(BaseEntity): "{}: Item has set label but is used as dynamic item." ).format(self.path)) + # Dynamic items or items in dynamic item must not have set `is_group` + if self.is_group and (self.is_dynamic_item or self.is_in_dynamic_item): + raise ValueError( + "{} Dynamic entity has set `is_group` to true.".format( + self.path + ) + ) + @abstractmethod def set_override_state(self, state): """Set override state and trigger it on children. @@ -409,6 +417,94 @@ class BaseItemEntity(BaseEntity): """Value of entity without metadata.""" pass + @property + def can_discard_changes(self): + """Result defines if `discard_changes` will be processed. + + Also can be used as validation before the method is called. + """ + return self.has_unsaved_changes + + @property + def can_add_to_studio_default(self): + """Result defines if `add_to_studio_default` will be processed. + + Also can be used as validation before the method is called. + """ + if self._override_state is not OverrideState.STUDIO: + return False + + if self.is_dynamic_item or self.is_in_dynamic_item: + return False + + # Skip if entity is under group + if self.group_item: + return False + + # Skip if is group and any children is already marked with studio + # overrides + if self.is_group and self.has_studio_override: + return False + return True + + @property + def can_remove_from_studio_default(self): + """Result defines if `remove_from_studio_default` can be triggered. + + This can be also used as validation before the method is called. + """ + if self._override_state is not OverrideState.STUDIO: + return False + + if self.is_dynamic_item or self.is_in_dynamic_item: + return False + + if not self.has_studio_override: + return False + return True + + @property + def can_add_to_project_override(self): + """Result defines if `add_to_project_override` can be triggered. + + Also can be used as validation before the method is called. + """ + if self.is_dynamic_item or self.is_in_dynamic_item: + return False + + # Show only when project overrides are set + if self._override_state is not OverrideState.PROJECT: + return False + + # Do not show on items under group item + if self.group_item: + return False + + # Skip if already is marked to save project overrides + if self.is_group and self.has_project_override: + return False + return True + + @property + def can_remove_from_project_override(self): + """Result defines if `remove_from_project_override` can be triggered. + + This can be also used as validation before the method is called. + """ + if self.is_dynamic_item or self.is_in_dynamic_item: + return False + + if self._override_state is not OverrideState.PROJECT: + return False + + # Dynamic items can't have these actions + if self.is_dynamic_item or self.is_in_dynamic_item: + return False + + if not self.has_project_override: + return False + return True + def discard_changes(self, on_change_trigger=None): """Discard changes on entity and it's children. @@ -433,6 +529,9 @@ class BaseItemEntity(BaseEntity): """ initialized = False if on_change_trigger is None: + if not self.can_discard_changes: + return + initialized = True on_change_trigger = [] @@ -447,8 +546,23 @@ class BaseItemEntity(BaseEntity): """Entity's implementation to discard all changes made by user.""" pass + def add_to_studio_default(self, on_change_trigger=None): + initialized = False + if on_change_trigger is None: + if not self.can_add_to_studio_default: + return + + initialized = True + on_change_trigger = [] + + self._add_to_studio_default(on_change_trigger) + + if initialized: + for callback in on_change_trigger: + callback() + @abstractmethod - def add_to_studio_default(self): + def _add_to_studio_default(self, on_change_trigger): """Item's implementation to set current values as studio's overrides. Mark item and it's children as they have studio overrides. @@ -470,11 +584,11 @@ class BaseItemEntity(BaseEntity): on_change_trigger (list): Callbacks of `on_change` should be stored to trigger them afterwards. """ - if self._override_state is not OverrideState.STUDIO: - return - initialized = False if on_change_trigger is None: + if not self.can_remove_from_studio_default: + return + initialized = True on_change_trigger = [] @@ -493,6 +607,29 @@ class BaseItemEntity(BaseEntity): """ pass + def add_to_project_override(self, on_change_trigger=None): + initialized = False + if on_change_trigger is None: + if not self.can_add_to_project_override: + return + + initialized = True + on_change_trigger = [] + + self._add_to_project_override(on_change_trigger) + + if initialized: + for callback in on_change_trigger: + callback() + + @abstractmethod + def _add_to_project_override(self, on_change_trigger): + """Item's implementation to set values as overriden for project. + + Mark item and all it's children to be stored as project overrides. + """ + pass + def remove_from_project_override(self, on_change_trigger=None): """Remove project overrides from entity and it's children. @@ -513,6 +650,8 @@ class BaseItemEntity(BaseEntity): initialized = False if on_change_trigger is None: + if not self.can_remove_from_project_override: + return initialized = True on_change_trigger = [] @@ -522,14 +661,6 @@ class BaseItemEntity(BaseEntity): for callback in on_change_trigger: callback() - @abstractmethod - def add_to_project_override(self): - """Item's implementation to set values as overriden for project. - - Mark item and all it's children to be stored as project overrides. - """ - pass - @abstractmethod def _remove_from_project_override(self, on_change_trigger): """Item's implementation to remove project overrides. @@ -630,7 +761,7 @@ class ItemEntity(BaseItemEntity): pass @abstractmethod - def update_studio_values(self, parent_values): + def update_studio_value(self, parent_values): """Fill studio override values on startup or on refresh. Set studio value if is not set to NOT_SET, in that case studio @@ -643,7 +774,7 @@ class ItemEntity(BaseItemEntity): pass @abstractmethod - def update_project_values(self, parent_values): + def update_project_value(self, parent_values): """Fill project override values on startup, refresh or project change. Set project value if is not set to NOT_SET, in that case project diff --git a/pype/settings/entities/dict_immutable_keys_entity.py b/pype/settings/entities/dict_immutable_keys_entity.py index c6ed41c402..854c904f3c 100644 --- a/pype/settings/entities/dict_immutable_keys_entity.py +++ b/pype/settings/entities/dict_immutable_keys_entity.py @@ -169,7 +169,7 @@ class DictImmutableKeysEntity(ItemEntity): "highlight_content", False ) self.show_borders = self.schema_data.get("show_borders", True) - self.collapsible = self.schema_data.get("collapsable", True) + self.collapsible = self.schema_data.get("collapsible", True) self.collapsed = self.schema_data.get("collapsed", True) # Not yet implemented @@ -192,22 +192,6 @@ class DictImmutableKeysEntity(ItemEntity): return "/".join([self.path, result_key]) def _update_current_metadata(self): - # Define if current metadata are avaialble for current override state - metadata = NOT_SET - if self._override_state is OverrideState.DEFAULTS: - metadata = {} - - if self._override_state is OverrideState.PROJECT: - # metadata are NOT_SET if project overrides do not override this - # item - metadata = self._project_override_metadata - - if ( - self._override_state >= OverrideState.STUDIO - and metadata is NOT_SET - ): - metadata = self._studio_override_metadata - current_metadata = {} for key, child_obj in self.non_gui_children.items(): if self._override_state is OverrideState.DEFAULTS: @@ -232,10 +216,18 @@ class DictImmutableKeysEntity(ItemEntity): current_metadata[M_OVERRIDEN_KEY] = [] current_metadata[M_OVERRIDEN_KEY].append(key) - if metadata is NOT_SET and not current_metadata: - self._metadata_are_modified = False - else: - self._metadata_are_modified = current_metadata != metadata + # 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): @@ -246,24 +238,6 @@ class DictImmutableKeysEntity(ItemEntity): # Change has/had override states self._override_state = state - if state is OverrideState.NOT_DEFINED: - pass - - elif state is OverrideState.DEFAULTS: - self.has_default_value = self._default_value is not NOT_SET - - elif state is OverrideState.STUDIO: - self.had_studio_override = ( - self._studio_override_metadata is not NOT_SET - ) - self._has_studio_override = self.had_studio_override - - elif state is OverrideState.PROJECT: - self._has_studio_override = self.had_studio_override - self.had_project_override = ( - self._project_override_metadata is not NOT_SET - ) - self._has_project_override = self.had_project_override for child_obj in self.non_gui_children.values(): child_obj.set_override_state(state) @@ -282,18 +256,6 @@ class DictImmutableKeysEntity(ItemEntity): if self._metadata_are_modified: return True - if ( - self._override_state is OverrideState.PROJECT - and self._has_project_override != self.had_project_override - ): - return True - - elif ( - self._override_state is OverrideState.STUDIO - and self._has_studio_override != self.had_studio_override - ): - return True - return self._child_has_unsaved_changes @property @@ -305,7 +267,7 @@ class DictImmutableKeysEntity(ItemEntity): @property def has_studio_override(self): - return self._has_studio_override or self._child_has_studio_override + return self._child_has_studio_override @property def _child_has_studio_override(self): @@ -317,7 +279,7 @@ class DictImmutableKeysEntity(ItemEntity): @property def has_project_override(self): - return self._has_project_override or self._child_has_project_override + return self._child_has_project_override @property def _child_has_project_override(self): @@ -345,10 +307,10 @@ class DictImmutableKeysEntity(ItemEntity): if self.is_group: if self._override_state is OverrideState.STUDIO: - if not self._has_studio_override: + if not self.has_studio_override: return NOT_SET elif self._override_state is OverrideState.PROJECT: - if not self._has_project_override: + if not self.has_project_override: return NOT_SET output = {} @@ -396,7 +358,8 @@ class DictImmutableKeysEntity(ItemEntity): unknown_keys = value_keys - expected_keys if unknown_keys: self.log.warning( - "Unknown keys in default values: {}".format( + "{} Unknown keys in default values: {}".format( + self.path, ", ".join("\"{}\"".format(key) for key in unknown_keys) ) ) @@ -405,7 +368,7 @@ class DictImmutableKeysEntity(ItemEntity): child_value = value.get(key, NOT_SET) child_obj.update_default_value(child_value) - def update_studio_values(self, value): + def update_studio_value(self, value): """Update studio override values. Not an api method, should be called by parent. @@ -413,10 +376,11 @@ class DictImmutableKeysEntity(ItemEntity): 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: for child_obj in self.non_gui_children.values(): - child_obj.update_studio_values(value) + child_obj.update_studio_value(value) return value_keys = set(value.keys()) @@ -424,15 +388,16 @@ class DictImmutableKeysEntity(ItemEntity): unknown_keys = value_keys - expected_keys if unknown_keys: self.log.warning( - "Unknown keys in studio overrides: {}".format( + "{} Unknown keys in studio overrides: {}".format( + self.path, ", ".join("\"{}\"".format(key) for key in unknown_keys) ) ) for key, child_obj in self.non_gui_children.items(): child_value = value.get(key, NOT_SET) - child_obj.update_studio_values(child_value) + child_obj.update_studio_value(child_value) - def update_project_values(self, value): + def update_project_value(self, value): """Update project override values. Not an api method, should be called by parent. @@ -440,10 +405,11 @@ class DictImmutableKeysEntity(ItemEntity): 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: for child_obj in self.non_gui_children.values(): - child_obj.update_project_values(value) + child_obj.update_project_value(value) return value_keys = set(value.keys()) @@ -451,14 +417,15 @@ class DictImmutableKeysEntity(ItemEntity): unknown_keys = value_keys - expected_keys if unknown_keys: self.log.warning( - "Unknown keys in project overrides: {}".format( + "{} Unknown keys in project overrides: {}".format( + self.path, ", ".join("\"{}\"".format(key) for key in unknown_keys) ) ) for key, child_obj in self.non_gui_children.items(): child_value = value.get(key, NOT_SET) - child_obj.update_project_values(child_value) + child_obj.update_project_value(child_value) def _discard_changes(self, on_change_trigger): self._ignore_child_changes = True @@ -468,13 +435,10 @@ class DictImmutableKeysEntity(ItemEntity): self._ignore_child_changes = False - def add_to_studio_default(self): - if self._override_state is not OverrideState.STUDIO: - return - + def _add_to_studio_default(self, on_change_trigger): self._ignore_child_changes = True for child_obj in self.non_gui_children.values(): - child_obj.add_to_studio_default() + child_obj.add_to_studio_default(on_change_trigger) self._ignore_child_changes = False self.parent.on_child_change(self) @@ -483,25 +447,21 @@ class DictImmutableKeysEntity(ItemEntity): for child_obj in self.non_gui_children.values(): child_obj.remove_from_studio_default(on_change_trigger) self._ignore_child_changes = False - self._has_studio_override = False - - def add_to_project_override(self): - if self._override_state is not OverrideState.PROJECT: - return + def _add_to_project_override(self, _on_change_trigger): self._ignore_child_changes = True for child_obj in self.non_gui_children.values(): - child_obj.add_to_project_override() + child_obj.add_to_project_override(_on_change_trigger) self._ignore_child_changes = False self.parent.on_child_change(self) - def _remove_from_project_override(self): + def _remove_from_project_override(self, on_change_trigger): if self._override_state is not OverrideState.PROJECT: return self._ignore_child_changes = True for child_obj in self.non_gui_children.values(): - child_obj.remove_from_project_override() + child_obj.remove_from_project_override(on_change_trigger) self._ignore_child_changes = False def reset_callbacks(self): diff --git a/pype/settings/entities/dict_mutable_keys_entity.py b/pype/settings/entities/dict_mutable_keys_entity.py index 833b49f100..8fe71db2a3 100644 --- a/pype/settings/entities/dict_mutable_keys_entity.py +++ b/pype/settings/entities/dict_mutable_keys_entity.py @@ -5,7 +5,10 @@ from .lib import ( OverrideState ) from . import EndpointEntity -from .exceptions import DefaultsNotDefined +from .exceptions import ( + DefaultsNotDefined, + StudioDefaultsNotDefined +) from pype.settings.constants import ( METADATA_KEYS, M_DYNAMIC_KEY_LABEL, @@ -14,7 +17,17 @@ from pype.settings.constants import ( class DictMutableKeysEntity(EndpointEntity): - """""" + """Dictionary entity that has mutable keys. + + Keys of entity's children can be modified, removed or added. Children have + defined entity type so it is not possible to have 2 different entity types + as children. + + TODOs: + - cleanup children on pop + - remove child's reference to parent + - clear callbacks + """ schema_types = ["dict-modifiable"] _miss_arg = object() @@ -22,7 +35,7 @@ class DictMutableKeysEntity(EndpointEntity): return self.children_by_key[key] def __setitem__(self, key, value): - self.set_value_for_key(key, value) + self.set_key_value(key, value) def __iter__(self): for key in self.keys(): @@ -58,14 +71,14 @@ class DictMutableKeysEntity(EndpointEntity): prev_keys = set(self.keys()) for _key, _value in value.items(): - self.set_value_for_key(_key, _value) + self.set_key_value(_key, _value) if _key in prev_keys: prev_keys.remove(_key) for key in prev_keys: self.pop(key) - def set_value_for_key(self, key, value): + def set_key_value(self, key, value): # TODO Check for value type if is Settings entity? child_obj = self.children_by_key.get(key) if not child_obj: @@ -77,6 +90,13 @@ class DictMutableKeysEntity(EndpointEntity): if new_key == old_key: return self.children_by_key[new_key] = self.children_by_key.pop(old_key) + self._on_key_label_change() + + def _on_key_label_change(self): + if self._override_state is OverrideState.STUDIO: + self._has_studio_override = True + elif self._override_state is OverrideState.PROJECT: + self._has_project_override = True self.on_change() def _add_key(self, key): @@ -114,6 +134,22 @@ class DictMutableKeysEntity(EndpointEntity): return key return None + # Label methods + def get_child_label(self, child_entity): + return self.children_label_by_id.get(child_entity.id) + + def set_child_label(self, child_entity, label): + self.children_label_by_id[child_entity.id] = label + self._on_key_label_change() + + def get_key_label(self, key): + child_entity = self.children_by_key[key] + return self.get_child_label(child_entity) + + def set_key_label(self, key, label): + child_entity = self.children_by_key[key] + self.set_child_label(child_entity, label) + def _item_initalization(self): self._default_metadata = {} self._studio_override_metadata = {} @@ -133,12 +169,12 @@ class DictMutableKeysEntity(EndpointEntity): self.schema_data.get("value_is_env_group") or False ) self.required_keys = self.schema_data.get("required_keys") or [] - self.collapsible_key = self.schema_data.get("collapsable_key") or False + self.collapsible_key = self.schema_data.get("collapsible_key") or False # GUI attributes self.hightlight_content = ( self.schema_data.get("highlight_content") or False ) - self.collapsible = self.schema_data.get("collapsable", True) + self.collapsible = self.schema_data.get("collapsible", True) self.collapsed = self.schema_data.get("collapsed", True) object_type = self.schema_data["object_type"] @@ -192,9 +228,10 @@ class DictMutableKeysEntity(EndpointEntity): return if self._override_state is OverrideState.STUDIO: - self._has_studio_override = self._child_has_studio_override + self._has_studio_override = True elif self._override_state is OverrideState.PROJECT: - self._has_project_override = self._child_has_project_override + self._has_project_override = True + self.on_change() def _metadata_for_current_state(self): @@ -220,10 +257,15 @@ class DictMutableKeysEntity(EndpointEntity): # TODO change metadata self._override_state = state - if not self.has_default_value and state > OverrideState.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: - raise DefaultsNotDefined(self) + # 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: + raise DefaultsNotDefined(self) + + elif state > OverrideState.STUDIO: + if not self.had_studio_override: + raise StudioDefaultsNotDefined(self) if state is OverrideState.STUDIO: self._has_studio_override = self.had_studio_override @@ -232,20 +274,25 @@ class DictMutableKeysEntity(EndpointEntity): self._has_project_override = self.had_project_override self._has_studio_override = self.had_studio_override - using_overrides = True + using_project_overrides = False + using_studio_overrides = False if ( state is OverrideState.PROJECT and self.had_project_override ): + using_project_overrides = True value = self._project_override_value metadata = self._project_override_metadata - elif self.had_studio_override: + elif ( + state >= OverrideState.STUDIO + and self.had_studio_override + ): + using_studio_overrides = True value = self._studio_override_value metadata = self._studio_override_metadata else: - using_overrides = False value = self._default_value metadata = self._default_metadata @@ -256,24 +303,23 @@ class DictMutableKeysEntity(EndpointEntity): # Simulate `clear` method without triggering value change for key in tuple(self.children_by_key.keys()): - child_obj = self.children_by_key.pop(key) + self.children_by_key.pop(key) # Create new children children_label_by_id = {} metadata_labels = metadata.get(M_DYNAMIC_KEY_LABEL) or {} for _key, _value in new_value.items(): - child_obj = self._add_key(_key) - child_obj.update_default_value(_value) - if using_overrides: - if state is OverrideState.STUDIO: - child_obj.update_studio_values(_value) - else: - child_obj.update_project_values(_value) + child_entity = self._add_key(_key) + child_entity.update_default_value(_value) + if using_project_overrides: + child_entity.update_project_value(_value) + elif using_studio_overrides: + child_entity.update_studio_value(_value) label = metadata_labels.get(_key) if label: - children_label_by_id[child_obj.id] = label - child_obj.set_override_state(state) + children_label_by_id[child_entity.id] = label + child_entity.set_override_state(state) self.children_label_by_id = children_label_by_id @@ -301,8 +347,9 @@ class DictMutableKeysEntity(EndpointEntity): children_key_by_id = self.children_key_by_id() label_metadata = {} for child_id, label in self.children_label_by_id.items(): - key = children_key_by_id[child_id] - label_metadata[key] = label + key = children_key_by_id.get(child_id) + if key: + label_metadata[key] = label output[M_DYNAMIC_KEY_LABEL] = label_metadata @@ -368,6 +415,8 @@ class DictMutableKeysEntity(EndpointEntity): output = {} for key, child_entity in self.children_by_key.items(): child_value = child_entity.settings_value() + # TODO child should have setter of env group key se child can + # know what env group represents. if self.value_is_env_group: if key not in child_value[M_ENVIRONMENT_KEY]: _metadata = child_value[M_ENVIRONMENT_KEY] @@ -393,14 +442,14 @@ class DictMutableKeysEntity(EndpointEntity): self._default_value = value self._default_metadata = metadata - def update_studio_values(self, value): + def update_studio_value(self, value): value = self._check_update_value(value, "studio override") value, metadata = self._prepare_value(value) self._studio_override_value = value self._studio_override_metadata = metadata self.had_studio_override = value is not NOT_SET - def update_project_values(self, value): + def update_project_value(self, value): value = self._check_update_value(value, "project override") value, metadata = self._prepare_value(value) self._project_override_value = value @@ -411,9 +460,7 @@ class DictMutableKeysEntity(EndpointEntity): self.set_override_state(self._override_state) on_change_trigger.append(self.on_change) - def add_to_studio_default(self): - if self._override_state is not OverrideState.STUDIO: - return + def _add_to_studio_default(self, _on_change_trigger): self._has_studio_override = True self.on_change() @@ -431,7 +478,7 @@ class DictMutableKeysEntity(EndpointEntity): # Create new children for _key, _value in new_value.items(): - child_obj = self.add_key(_key) + child_obj = self._add_key(_key) child_obj.update_default_value(_value) child_obj.set_override_state(self._override_state) @@ -441,9 +488,7 @@ class DictMutableKeysEntity(EndpointEntity): on_change_trigger.append(self.on_change) - def add_to_project_override(self): - if self._override_state is not OverrideState.PROJECT: - return + def _add_to_project_override(self, _on_change_trigger): self._has_project_override = True self.on_change() @@ -454,10 +499,8 @@ class DictMutableKeysEntity(EndpointEntity): if not self.has_project_override: return - using_overrides = False if self._has_studio_override: value = self._studio_override_value - using_overrides = True elif self.has_default_value: value = self._default_value else: @@ -473,9 +516,9 @@ class DictMutableKeysEntity(EndpointEntity): # Create new children for _key, _value in new_value.items(): - child_obj = self.add_key(_key) + child_obj = self._add_key(_key) child_obj.update_default_value(_value) - if using_overrides: + if self._has_studio_override: child_obj.update_studio_value(_value) child_obj.set_override_state(self._override_state) diff --git a/pype/settings/entities/exceptions.py b/pype/settings/entities/exceptions.py index 1d5624c967..951cd07243 100644 --- a/pype/settings/entities/exceptions.py +++ b/pype/settings/entities/exceptions.py @@ -4,6 +4,14 @@ class DefaultsNotDefined(Exception): super(DefaultsNotDefined, self).__init__(msg) +class StudioDefaultsNotDefined(Exception): + def __init__(self, obj): + msg = "Studio default values for object are not set. {}".format( + obj.path + ) + super(StudioDefaultsNotDefined, self).__init__(msg) + + class InvalidValueType(Exception): msg_template = "{}" diff --git a/pype/settings/entities/input_entities.py b/pype/settings/entities/input_entities.py index 700c883783..0eaafb6c25 100644 --- a/pype/settings/entities/input_entities.py +++ b/pype/settings/entities/input_entities.py @@ -1,12 +1,16 @@ import copy from abc import abstractmethod -from .item_entities import ItemEntity +from .base_entity import ItemEntity from .lib import ( NOT_SET, OverrideState ) -from .exceptions import DefaultsNotDefined +from .exceptions import ( + DefaultsNotDefined, + StudioDefaultsNotDefined +) + from pype.settings.constants import ( METADATA_KEYS, M_ENVIRONMENT_KEY @@ -24,7 +28,10 @@ class EndpointEntity(ItemEntity): def __init__(self, *args, **kwargs): super(EndpointEntity, self).__init__(*args, **kwargs) - if not self.group_item and not self.is_group: + if ( + not (self.group_item or self.is_group) + and not (self.is_dynamic_item or self.is_in_dynamic_item) + ): self.is_group = True def schema_validations(self): @@ -37,12 +44,6 @@ class EndpointEntity(ItemEntity): ) ) - # Input entity must have file parent. - if not self.file_item: - raise ValueError( - "{}: Missing parent file entity.".format(self.path) - ) - super(EndpointEntity, self).schema_validations() @abstractmethod @@ -55,10 +56,10 @@ class EndpointEntity(ItemEntity): if self.is_group: if self._override_state is OverrideState.STUDIO: - if not self._has_studio_override: + if not self.has_studio_override: return NOT_SET elif self._override_state is OverrideState.PROJECT: - if not self._has_project_override: + if not self.has_project_override: return NOT_SET return self._settings_value() @@ -72,12 +73,12 @@ class EndpointEntity(ItemEntity): self._default_value = value self.has_default_value = value is not NOT_SET - def update_studio_values(self, value): + def update_studio_value(self, value): value = self._check_update_value(value, "studio override") self._studio_override_value = value self.had_studio_override = bool(value is not NOT_SET) - def update_project_values(self, value): + def update_project_value(self, value): value = self._check_update_value(value, "project override") self._project_override_value = value self.had_project_override = bool(value is not NOT_SET) @@ -100,6 +101,15 @@ class InputEntity(EndpointEntity): self.__class__.__name__ )) + def schema_validations(self): + # Input entity must have file parent. + if not self.file_item: + raise ValueError( + "{}: Missing parent file entity.".format(self.path) + ) + + super(InputEntity, self).schema_validations() + @property def value(self): """Entity's value without metadata.""" @@ -201,10 +211,15 @@ class InputEntity(EndpointEntity): return self._override_state = state - if not self.has_default_value and state > OverrideState.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: - raise DefaultsNotDefined(self) + # 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: + raise DefaultsNotDefined(self) + + elif state > OverrideState.STUDIO: + if not self.had_studio_override: + raise StudioDefaultsNotDefined(self) if state is OverrideState.STUDIO: self._has_studio_override = ( @@ -237,9 +252,6 @@ class InputEntity(EndpointEntity): self._current_value = copy.deepcopy(value) def _discard_changes(self, on_change_trigger=None): - if self._override_state is OverrideState.NOT_DEFINED: - return - self._value_is_modified = False if self._override_state >= OverrideState.PROJECT: self._has_project_override = self.had_project_override @@ -270,9 +282,7 @@ class InputEntity(EndpointEntity): raise NotImplementedError("BUG: Unexcpected part of code.") - def add_to_studio_default(self): - if self._override_state is not OverrideState.STUDIO: - return + def _add_to_studio_default(self, _on_change_trigger): self._has_studio_override = True self.on_change() @@ -287,9 +297,7 @@ class InputEntity(EndpointEntity): on_change_trigger.append(self.on_change) - def add_to_project_override(self): - if self._override_state is not OverrideState.PROJECT: - return + def _add_to_project_override(self, _on_change_trigger): self._has_project_override = True self.on_change() @@ -510,14 +518,14 @@ class RawJsonEntity(InputEntity): self._default_value = value self.default_metadata = metadata - def update_studio_values(self, value): + def update_studio_value(self, value): value = self._check_update_value(value, "studio override") self.had_studio_override = value is not NOT_SET value, metadata = self._prepare_value(value) self._studio_override_value = value self.studio_override_metadata = metadata - def update_project_values(self, value): + def update_project_value(self, value): value = self._check_update_value(value, "project override") self.had_project_override = value is not NOT_SET value, metadata = self._prepare_value(value) diff --git a/pype/settings/entities/item_entities.py b/pype/settings/entities/item_entities.py index acadec1ed3..5986d685d6 100644 --- a/pype/settings/entities/item_entities.py +++ b/pype/settings/entities/item_entities.py @@ -2,7 +2,10 @@ from .lib import ( NOT_SET, OverrideState ) -from .exceptions import DefaultsNotDefined +from .exceptions import ( + DefaultsNotDefined, + StudioDefaultsNotDefined +) from .base_entity import ItemEntity @@ -156,26 +159,26 @@ class PathEntity(ItemEntity): def update_default_value(self, value): self.child_obj.update_default_value(value) - def update_project_values(self, value): - self.child_obj.update_project_values(value) + def update_project_value(self, value): + self.child_obj.update_project_value(value) - def update_studio_values(self, value): - self.child_obj.update_studio_values(value) + def update_studio_value(self, value): + self.child_obj.update_studio_value(value) - def _discard_changes(self, *args): - self.child_obj.discard_changes(*args) + def _discard_changes(self, *args, **kwargs): + self.child_obj.discard_changes(*args, **kwargs) - def add_to_studio_default(self): - self.child_obj.add_to_studio_default() + def _add_to_studio_default(self, *args, **kwargs): + self.child_obj.add_to_studio_default(*args, **kwargs) - def _remove_from_studio_default(self, *args): - self.child_obj.remove_from_studio_default(*args) + def _remove_from_studio_default(self, *args, **kwargs): + self.child_obj.remove_from_studio_default(*args, **kwargs) - def add_to_project_override(self): - self.child_obj.add_to_project_override() + def _add_to_project_override(self, *args, **kwargs): + self.child_obj.add_to_project_override(*args, **kwargs) - def _remove_from_project_override(self, *args): - self.child_obj.remove_from_project_override(*args) + def _remove_from_project_override(self, *args, **kwargs): + self.child_obj.remove_from_project_override(*args, **kwargs) def reset_callbacks(self): super(PathEntity, self).reset_callbacks() @@ -206,6 +209,15 @@ class ListStrictEntity(ItemEntity): if not self.group_item and not self.is_group: self.is_group = True + def schema_validations(self): + # List entity must have file parent. + if not self.file_item and not self.is_file: + raise ValueError( + "{}: Missing file entity in hierarchy.".format(self.path) + ) + + super(ListStrictEntity, self).schema_validations() + def get_child_path(self, child_obj): result_idx = None for idx, _child_obj in enumerate(self.children): @@ -231,6 +243,20 @@ class ListStrictEntity(ItemEntity): self.children[idx].set(item) def settings_value(self): + if self._override_state is OverrideState.NOT_DEFINED: + return NOT_SET + + if ( + self.is_group + and self._override_state is not OverrideState.DEFAULTS + ): + 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 = [] for child_obj in self.children: output.append(child_obj.settings_value()) @@ -246,12 +272,13 @@ class ListStrictEntity(ItemEntity): return if self._override_state is OverrideState.STUDIO: - self._has_studio_override = self.child_has_studio_override + self._has_studio_override = self._child_has_studio_override elif self._override_state is OverrideState.PROJECT: - self._has_project_override = self.child_has_project_override + self._has_project_override = self._child_has_project_override self.on_change() + @property def has_unsaved_changes(self): if self._override_state is OverrideState.NOT_DEFINED: return False @@ -285,6 +312,14 @@ class ListStrictEntity(ItemEntity): return True return False + @property + def has_studio_override(self): + return self._has_studio_override or self._child_has_studio_override + + @property + def has_project_override(self): + return self._has_project_override or self._child_has_project_override + @property def _child_has_unsaved_changes(self): for child_obj in self.children: @@ -313,13 +348,18 @@ class ListStrictEntity(ItemEntity): return self._override_state = state - if not self.has_default_value and state > OverrideState.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: - raise DefaultsNotDefined(self) + # 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: + raise DefaultsNotDefined(self) - for child_obj in self.children: - child_obj.set_override_state(state) + elif state > OverrideState.STUDIO: + if not self.had_studio_override: + raise StudioDefaultsNotDefined(self) + + for child_entity in self.children: + child_entity.set_override_state(state) self.initial_value = self.settings_value() @@ -327,9 +367,7 @@ class ListStrictEntity(ItemEntity): for child_obj in self.children: child_obj.discard_changes(on_change_trigger) - def add_to_studio_default(self): - if self._override_state is not OverrideState.STUDIO: - return + def _add_to_studio_default(self, _on_change_trigger): self._has_studio_override = True self.on_change() @@ -343,14 +381,11 @@ class ListStrictEntity(ItemEntity): self._has_studio_override = False - def add_to_project_override(self): + def _add_to_project_override(self, _on_change_trigger): self._has_project_override = True self.on_change() def _remove_from_project_override(self, on_change_trigger): - if self._override_state is not OverrideState.PROJECT: - return - self._ignore_child_changes = True for child_obj in self.children: @@ -403,25 +438,25 @@ class ListStrictEntity(ItemEntity): for idx, item_value in enumerate(value): self.children[idx].update_default_value(item_value) - def update_studio_values(self, value): + def update_studio_value(self, value): value = self._check_update_value(value, "studio override") if value is NOT_SET: for child_obj in self.children: - child_obj.update_studio_values(value) + child_obj.update_studio_value(value) else: for idx, item_value in enumerate(value): - self.children[idx].update_studio_values(item_value) + self.children[idx].update_studio_value(item_value) - def update_project_values(self, value): + def update_project_value(self, value): value = self._check_update_value(value, "project override") if value is NOT_SET: for child_obj in self.children: - child_obj.update_project_values(value) + child_obj.update_project_value(value) else: for idx, item_value in enumerate(value): - self.children[idx].update_project_values(item_value) + self.children[idx].update_project_value(item_value) def reset_callbacks(self): super(ListStrictEntity, self).reset_callbacks() diff --git a/pype/settings/entities/list_entity.py b/pype/settings/entities/list_entity.py index 8fbbe7456d..07221929b7 100644 --- a/pype/settings/entities/list_entity.py +++ b/pype/settings/entities/list_entity.py @@ -7,7 +7,10 @@ from .lib import ( NOT_SET, OverrideState ) -from .exceptions import DefaultsNotDefined +from .exceptions import ( + DefaultsNotDefined, + StudioDefaultsNotDefined +) class ListEntity(EndpointEntity): @@ -36,6 +39,19 @@ class ListEntity(EndpointEntity): return True return False + def index(self, item): + if isinstance(item, BaseEntity): + for idx, child_entity in enumerate(self.children): + if child_entity.id == item.id: + return idx + else: + for idx, _item in enumerate(self.value): + if item == _item: + return idx + raise ValueError( + "{} is not in {}".format(item, self.__class__.__name__) + ) + def append(self, item): child_obj = self._add_new_item() child_obj.set_override_state(self._override_state) @@ -89,6 +105,22 @@ class ListEntity(EndpointEntity): self.on_change() return child_obj + def swap_items(self, item_1, item_2): + index_1 = self.index(item_1) + index_2 = self.index(item_2) + self.swap_indexes(index_1, index_2) + + def swap_indexes(self, index_1, index_2): + children_len = len(self.children) + if index_1 > children_len or index_2 > children_len: + raise IndexError( + "{} index out of range".format(self.__class__.__name__) + ) + self.children[index_1], self.children[index_2] = ( + self.children[index_2], self.children[index_1] + ) + self.on_change() + def _item_initalization(self): self.valid_value_types = (list, ) self.children = [] @@ -153,11 +185,10 @@ class ListEntity(EndpointEntity): if self._ignore_child_changes: return - # TODO is this enough? if self._override_state is OverrideState.STUDIO: - self._has_studio_override = self._child_has_studio_override + self._has_studio_override = True elif self._override_state is OverrideState.PROJECT: - self._has_project_override = self._child_has_project_override + self._has_project_override = True self.on_change() def set_override_state(self, state): @@ -171,10 +202,15 @@ class ListEntity(EndpointEntity): while self.children: self.children.pop(0) - if not self.has_default_value and state > OverrideState.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: - raise DefaultsNotDefined(self) + # 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: + raise DefaultsNotDefined(self) + + elif state > OverrideState.STUDIO: + if not self.had_studio_override: + raise StudioDefaultsNotDefined(self) value = NOT_SET if self._override_state is OverrideState.PROJECT: @@ -198,13 +234,13 @@ class ListEntity(EndpointEntity): child_obj.update_default_value(item) if self._override_state is OverrideState.PROJECT: if self.had_project_override: - child_obj.update_project_values(item) + child_obj.update_project_value(item) elif self.had_studio_override: - child_obj.update_studio_values(item) + child_obj.update_studio_value(item) elif self._override_state is OverrideState.STUDIO: if self.had_studio_override: - child_obj.update_studio_values(item) + child_obj.update_studio_value(item) for child_obj in self.children: child_obj.set_override_state(self._override_state) @@ -337,13 +373,13 @@ class ListEntity(EndpointEntity): child_obj.update_default_value(item) if self._override_state is OverrideState.PROJECT: if self.had_project_override: - child_obj.update_project_values(item) + child_obj.update_project_value(item) elif self.had_studio_override: - child_obj.update_studio_values(item) + child_obj.update_studio_value(item) elif self._override_state is OverrideState.STUDIO: if self.had_studio_override: - child_obj.update_studio_values(item) + child_obj.update_studio_value(item) child_obj.set_override_state(self._override_state) @@ -357,9 +393,7 @@ class ListEntity(EndpointEntity): on_change_trigger.append(self.on_change) - def add_to_studio_default(self): - if self._override_state is not OverrideState.STUDIO: - return + def _add_to_studio_default(self, _on_change_trigger): self._has_studio_override = True self.on_change() @@ -387,7 +421,7 @@ class ListEntity(EndpointEntity): on_change_trigger.append(self.on_change) - def add_to_project_override(self): + def _add_to_project_override(self, _on_change_trigger): self._has_project_override = True self.on_change() @@ -407,11 +441,14 @@ class ListEntity(EndpointEntity): self._ignore_child_changes = True + while self.children: + self.children.pop(0) + for item in value: child_obj = self._add_new_item() child_obj.update_default_value(item) if self._has_studio_override: - child_obj.update_studio_values(item) + child_obj.update_studio_value(item) child_obj.set_override_state(self._override_state) self._ignore_child_changes = False diff --git a/pype/settings/entities/root_entities.py b/pype/settings/entities/root_entities.py index 6e8ef1a9fb..53cfbb5f3d 100644 --- a/pype/settings/entities/root_entities.py +++ b/pype/settings/entities/root_entities.py @@ -12,7 +12,11 @@ from .lib import ( OverrideState, gui_schema ) -from pype.settings.constants import SYSTEM_SETTINGS_KEY +from pype.settings.constants import ( + SYSTEM_SETTINGS_KEY, + PROJECT_SETTINGS_KEY, + PROJECT_ANATOMY_KEY +) from pype.settings.lib import ( DEFAULTS_DIR, @@ -20,15 +24,18 @@ from pype.settings.lib import ( get_default_settings, get_studio_system_settings_overrides, - save_studio_settings, + get_studio_project_settings_overrides, + get_studio_project_anatomy_overrides, + get_project_settings_overrides, + get_project_anatomy_overrides, + save_project_settings, + save_project_anatomy, + find_environments, apply_overrides ) -# TODO implement -# ProjectSettings -# AnatomySettings class RootEntity(BaseItemEntity): @@ -132,8 +139,16 @@ class RootEntity(BaseItemEntity): self._add_children(self.schema_data) - for children in self.children: - children.schema_validations() + self.schema_validations() + + def schema_validations(self): + for child_entity in self.children: + if child_entity.is_group: + raise ValueError(( + "Root entity \"{}\" has child with `is_group`" + " attribute set to True but root can't save overrides." + ).format(self.__class__.__name__)) + child_entity.schema_validations() def create_schema_object(self, schema_data, *args, **kwargs): """Create entity by entered schema data. @@ -166,10 +181,11 @@ class RootEntity(BaseItemEntity): if not issubclass(item, entities.BaseEntity): continue + # Skip class that is abstract by design + if item in known_abstract_classes: + continue + if inspect.isabstract(item): - # Skip class that is abstract by design - if item in known_abstract_classes: - continue # Create an object to get crash and get traceback item() @@ -302,25 +318,25 @@ class RootEntity(BaseItemEntity): for child_obj in self.non_gui_children.values(): child_obj.discard_changes(on_change_trigger) - def add_to_studio_default(self): + def _add_to_studio_default(self, *args, **kwargs): """Implementation of abstract method only trigger children callback.""" for child_obj in self.non_gui_children.values(): - child_obj.add_to_studio_default() + child_obj.add_to_studio_default(*args, **kwargs) def _remove_from_studio_default(self, on_change_trigger): """Implementation of abstract method only trigger children callback.""" for child_obj in self.non_gui_children.values(): child_obj.remove_from_studio_default(on_change_trigger) - def add_to_project_override(self): + def _add_to_project_override(self, *args, **kwargs): """Implementation of abstract method only trigger children callback.""" for child_obj in self.non_gui_children.values(): - child_obj.add_to_project_override() + child_obj.add_to_project_override(*args, **kwargs) - def _remove_from_project_override(self): + def _remove_from_project_override(self, on_change_trigger): """Implementation of abstract method only trigger children callback.""" for child_obj in self.non_gui_children.values(): - child_obj.remove_from_project_override() + child_obj.remove_from_project_override(on_change_trigger) def save(self): """Save values for current override state. @@ -450,7 +466,7 @@ class SystemSettings(RootEntity): studio_overrides = get_studio_system_settings_overrides() for key, child_obj in self.non_gui_children.items(): value = studio_overrides.get(key, NOT_SET) - child_obj.update_studio_values(value) + child_obj.update_studio_value(value) def reset(self, new_state=None): """Discard changes and reset entit's values. @@ -478,7 +494,7 @@ class SystemSettings(RootEntity): Implementation of abstract method. """ - return os.path.join(DEFAULTS_DIR, SYSTEM_SETTINGS_KEY) + return DEFAULTS_DIR def _save_studio_values(self): settings_value = self.settings_value() @@ -521,3 +537,147 @@ class SystemSettings(RootEntity): overrides. """ raise ValueError("System settings can't save project overrides.") + + +class ProjectSettings(RootEntity): + """Root entity for project settings. + + Allows to modify project settings via entity system and loaded schemas. + + Args: + project_name (str): Project name which overrides will be loaded. + Use `None` to modify studio defaults. + change_state (bool): Set values on initialization. By + default is set to True. + reset (bool): Reset values on initialization. By default is set to + True. + schema_data (dict): Pass schema data to entity. This is for development + and debugging purposes. + """ + def __init__( + self, + project_name=None, + change_state=True, + reset=True, + schema_data=None + ): + self._project_name = project_name + + if schema_data is None: + # Load system schemas + schema_data = gui_schema("projects_schema", "schema_main") + + super(ProjectSettings, self).__init__(schema_data, reset) + + if change_state: + if self.project_name is None: + self.set_studio_state() + else: + self.set_project_state() + + @property + def project_name(self): + return self._project_name + + @project_name.setter + def project_name(self, project_name): + self.change_project(project_name) + + def change_project(self, project_name): + if project_name == self._project_name: + return + + self._project_name = project_name + if project_name is None: + self.set_studio_state() + return + + project_override_value = { + PROJECT_SETTINGS_KEY: get_project_settings_overrides(project_name), + PROJECT_ANATOMY_KEY: get_project_anatomy_overrides(project_name) + } + for key, child_obj in self.non_gui_children.items(): + value = project_override_value.get(key, NOT_SET) + child_obj.update_project_value(value) + + self.set_project_state() + + def _reset_values(self): + default_values = { + PROJECT_SETTINGS_KEY: get_default_settings()[PROJECT_SETTINGS_KEY], + PROJECT_ANATOMY_KEY: get_default_settings()[PROJECT_ANATOMY_KEY] + } + for key, child_obj in self.non_gui_children.items(): + value = default_values.get(key, NOT_SET) + child_obj.update_default_value(value) + + studio_overrides = { + PROJECT_SETTINGS_KEY: get_studio_project_settings_overrides(), + PROJECT_ANATOMY_KEY: get_studio_project_anatomy_overrides() + } + + for key, child_obj in self.non_gui_children.items(): + value = studio_overrides.get(key, NOT_SET) + child_obj.update_studio_value(value) + + if not self.project_name: + return + + project_name = self.project_name + project_override_value = { + PROJECT_SETTINGS_KEY: get_project_settings_overrides(project_name), + PROJECT_ANATOMY_KEY: get_project_anatomy_overrides(project_name) + } + for key, child_obj in self.non_gui_children.items(): + value = project_override_value.get(key, NOT_SET) + child_obj.update_project_value(value) + + def reset(self, new_state=None): + """Discard changes and reset entit's values. + + Reload default values and studio override values and update entities. + + Args: + new_state (OverrideState): It is possible to change override state + during reset. Current state is used if not defined. + """ + if new_state is None: + new_state = self._override_state + + if new_state is OverrideState.NOT_DEFINED: + new_state = OverrideState.DEFAULTS + + self._reset_values() + self.set_override_state(new_state) + + def defaults_dir(self): + """Path to defaults directory. + + Implementation of abstract method. + """ + return DEFAULTS_DIR + + def _save_studio_values(self): + settings_value = self.settings_value() + + self._validate_values_to_save(settings_value) + + self.log.debug("Saving project settings: {}".format( + json.dumps(settings_value, indent=4) + )) + project_settings = settings_value.get(PROJECT_SETTINGS_KEY) or {} + project_anatomy = settings_value.get(PROJECT_ANATOMY_KEY) or {} + + save_project_settings(self.project_name, project_settings) + save_project_anatomy(self.project_name, project_anatomy) + + def _validate_defaults_to_save(self, value): + """Valiations of default values before save.""" + pass + + def _validate_values_to_save(self, value): + pass + + def _save_project_values(self): + """Project overrides are saved same ways as studio overrides.""" + self._save_studio_values() diff --git a/pype/settings/entities/schemas/README.md b/pype/settings/entities/schemas/README.md new file mode 100644 index 0000000000..65d73a0728 --- /dev/null +++ b/pype/settings/entities/schemas/README.md @@ -0,0 +1,490 @@ +# Creating GUI schemas + +## Basic rules +- configurations does not define GUI, but GUI defines configurations! +- output is always json (yaml is not needed for anatomy templates anymore) +- GUI schema has multiple input types, all inputs are represented by a dictionary +- each input may have "input modifiers" (keys in dictionary) that are required or optional + - only required modifier for all input items is key `"type"` which says what type of item it is +- there are special keys across all inputs + - `"is_file"` - this key is for storing pype defaults in `pype` repo + - reasons of existence: developing new schemas does not require to create defaults manually + - key is validated, must be once in hierarchy else it won't be possible to store pype defaults + - `"is_group"` - define that all values under key in hierarchy will be overriden if any value is modified, this information is also stored to overrides + - this keys is not allowed for all inputs as they may have not reason for that + - key is validated, can be only once in hierarchy but is not required +- currently there are `system configurations` and `project configurations` + +## Inner schema +- GUI schemas are huge json files, to be able to split whole configuration into multiple schema there's type `schema` +- system configuration schemas are stored in `~/tools/settings/settings/gui_schemas/system_schema/` and project configurations in `~/tools/settings/settings/gui_schemas/projects_schema/` +- each schema name is filename of json file except extension (without ".json") +- if content is dictionary content will be used as `schema` else will be used as `schema_template` + +### schema +- can have only key `"children"` which is list of strings, each string should represent another schema (order matters) string represebts name of the schema +- will just paste schemas from other schema file in order of "children" list + +``` +{ + "type": "schema", + "name": "my_schema_name" +} +``` + +### schema_template +- allows to define schema "templates" to not duplicate same content multiple times +```javascript +// EXAMPLE json file content (filename: example_template.json) +[ + { + "__default_values__": { + "multipath_executables": true + } + }, { + "type": "raw-json", + "label": "{host_label} Environments", + "key": "{host_name}_environments", + "env_group_key": "{host_name}" + }, { + "type": "path-widget", + "key": "{host_name}_executables", + "label": "{host_label} - Full paths to executables", + "multiplatform": "{multipath_executables}", + "multipath": true + } +] +``` +```javascript +// EXAMPLE usage of the template in schema +{ + "type": "dict", + "key": "schema_template_examples", + "label": "Schema template examples", + "children": [ + { + "type": "schema_template", + // filename of template (example_template.json) + "name": "example_template", + "template_data": { + "host_label": "Maya 2019", + "host_name": "maya_2019", + "multipath_executables": false + } + }, { + "type": "schema_template", + "name": "example_template", + "template_data": { + "host_label": "Maya 2020", + "host_name": "maya_2020" + } + } + ] +} +``` +- item in schema mush contain `"type"` and `"name"` keys but it is also expected that `"template_data"` will be entered too +- all items in the list, except `__default_values__`, will replace `schema_template` item in schema +- template may contain another template or schema +- it is expected that schema template will have unfilled fields as in example + - unfilled fields are allowed only in values of schema dictionary +```javascript +{ + ... + // Allowed + "key": "{to_fill}" + ... + // Not allowed + "{to_fill}": "value" + ... +} +``` +- Unfilled fields can be also used for non string values, in that case value must contain only one key and value for fill must contain right type. +```javascript +{ + ... + // Allowed + "multiplatform": "{executable_multiplatform}" + ... + // Not allowed + "multiplatform": "{executable_multiplatform}_enhanced_string" + ... +} +``` +- It is possible to define default values for unfilled fields to do so one of items in list must be dictionary with key `"__default_values__"` and value as dictionary with default key: values (as in example above). + + +## Basic Dictionary inputs +- these inputs wraps another inputs into {key: value} relation + +## dict +- this is another dictionary input wrapping more inputs but visually makes them different +- item may be used as widget (in `list` or `dict-modifiable`) + - in that case the only key modifier is `children` which is list of it's keys + - USAGE: e.g. List of dictionaries where each dictionary have same structure. +- item may be with or without `"label"` if is not used as widget + - required keys are `"key"` under which will be stored + - without label it is just wrap item holding `"key"` + - can't have `"is_group"` key set to True as it breaks visual override showing + - 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 +{ + "key": "applications", + "type": "dict", + "label": "Applications", + "collapsible": true, + "highlight_content": true, + "is_group": true, + "is_file": true, + "children": [ + ...ITEMS... + ] +} + +# Without label +{ + "type": "dict", + "key": "global", + "children": [ + ...ITEMS... + ] +} + +# When used as widget +{ + "type": "list", + "key": "profiles", + "label": "Profiles", + "object_type": { + "type": "dict", + "children": [ + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, { + "key": "hosts", + "label": "Hosts", + "type": "list", + "object_type": "text" + } + ... + ] + } +} +``` + +## 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 + +### boolean +- simple checkbox, nothing more to set +``` +{ + "type": "boolean", + "key": "my_boolean_key", + "label": "Do you want to use Pype?" +} +``` + +### number +- number input, can be used for both integer and float + - key `"decimal"` defines how many decimal places will be used, 0 is for integer input (Default: `0`) + - key `"minimum"` as minimum allowed number to enter (Default: `-99999`) + - key `"maxium"` as maximum allowed number to enter (Default: `99999`) +``` +{ + "type": "number", + "key": "fps", + "label": "Frame rate (FPS)" + "decimal": 2, + "minimum": 1, + "maximum": 300000 +} +``` + +### text +- simple text input + - key `"multiline"` allows to enter multiple lines of text (Default: `False`) + - key `"placeholder"` allows to show text inside input when is empty (Default: `None`) + +``` +{ + "type": "text", + "key": "deadline_pool", + "label": "Deadline pool" +} +``` + +### path-input +- enhanced text input + - does not allow to enter backslash, is auto-converted to forward slash + - may be added another validations, like do not allow end path with slash +- this input is implemented to add additional features to text input +- this is meant to be used in proxy input `path-widget` + - DO NOT USE this input in schema please + +### raw-json +- a little bit enhanced text input for raw json +- has validations of json format + - empty value is invalid value, always must be at least `{}` of `[]` + +``` +{ + "type": "raw-json", + "key": "profiles", + "label": "Extract Review profiles" +} +``` + +### enum +- returns value of single on multiple items from predefined values +- multiselection can be allowed with setting key `"multiselection"` to `True` (Default: `False`) +- values are defined under value of key `"enum_items"` as list + - each item in list is simple dictionary where value is label and key is value which will be stored + - should be possible to enter single dictionary if order of items doesn't matter + +``` +{ + "key": "tags", + "label": "Tags", + "type": "enum", + "multiselection": true, + "enum_items": [ + {"burnin": "Add burnins"}, + {"ftrackreview": "Add to Ftrack"}, + {"delete": "Delete output"}, + {"slate-frame": "Add slate frame"}, + {"no-hnadles": "Skip handle frames"} + ] +} +``` + +## Inputs for setting value using Pure inputs +- these inputs also have required `"key"` +- attribute `"label"` is required in few conditions + - when item is marked `as_group` or when `use_label_wrap` +- they use Pure inputs "as widgets" + +### list +- output is list +- items can be added and removed +- items in list must be the same type +- to wrap item in collapsible widget with label on top set `use_label_wrap` to `True` + - when this is used `collapsible` and `collapsed` can be set (same as `dict` item does) +- type of items is defined with key `"object_type"` +- there are 2 possible ways how to set the type: + 1.) dictionary with item modifiers (`number` input has `minimum`, `maximum` and `decimals`) in that case item type must be set as value of `"type"` (example below) + 2.) item type name as string without modifiers (e.g. `text`) + +1.) with item modifiers +``` +{ + "type": "list", + "key": "exclude_ports", + "label": "Exclude ports", + "object_type": { + "type": "number", # number item type + "minimum": 1, # minimum modifier + "maximum": 65535 # maximum modifier + } +} +``` + +2.) without modifiers +``` +{ + "type": "list", + "key": "exclude_ports", + "label": "Exclude ports", + "object_type": "text" +} +``` + +### dict-modifiable +- one of dictionary inputs, this is only used as value input +- items in this input can be removed and added same way as in `list` input +- value items in dictionary must be the same type +- type of items is defined with key `"object_type"` +- required keys may be defined under `"required_keys"` + - required keys must be defined as a list (e.g. `["key_1"]`) and are moved to the top + - these keys can't be removed or edited (it is possible to edit label if item is collapsible) +- there are 2 possible ways how to set the type: + 1.) dictionary with item modifiers (`number` input has `minimum`, `maximum` and `decimals`) in that case item type must be set as value of `"type"` (example below) + 2.) item type name as string without modifiers (e.g. `text`) +- this input 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`) + +1.) with item modifiers +``` +{ + "type": "dict-modifiable", + "object_type": { + "type": "number", + "minimum": 0, + "maximum": 300 + }, + "is_group": true, + "key": "templates_mapping", + "label": "Muster - Templates mapping", + "is_file": true +} +``` + +2.) without modifiers +``` +{ + "type": "dict-modifiable", + "object_type": "text", + "is_group": true, + "key": "templates_mapping", + "label": "Muster - Templates mapping", + "is_file": true +} +``` + +### path-widget +- input for paths, use `path-input` internally +- has 2 input modifiers `"multiplatform"` and `"multipath"` + - `"multiplatform"` - adds `"windows"`, `"linux"` and `"darwin"` path inputs result is dictionary + - `"multipath"` - it is possible to enter multiple paths + - if both are enabled result is dictionary with lists + +``` +{ + "type": "path-widget", + "key": "ffmpeg_path", + "label": "FFmpeg path", + "multiplatform": true, + "multipath": true +} +``` + +### list-strict +- input for strict number of items in list +- each child item can be different type with different possible modifiers +- it is possible to display them in horizontal or vertical layout + - key `"horizontal"` as `True`/`False` (Default: `True`) +- each child may have defined `"label"` which is shown next to input + - label does not reflect modifications or overrides (TODO) +- children item are defined under key `"object_types"` which is list of dictionaries + - key `"children"` is not used because is used for hierarchy validations in schema +- USAGE: For colors, transformations, etc. Custom number and different modifiers + give ability to define if color is HUE or RGB, 0-255, 0-1, 0-100 etc. + +``` +{ + "type": "list-strict", + "key": "color", + "label": "Color", + "object_types": [ + { + "label": "Red", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Green", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Blue", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Alpha", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 6 + } + ] +} +``` + + +## Noninteractive widgets +- have nothing to do with data + +### label +- add label with note or explanations +- it is possible to use html tags inside the label + +``` +{ + "type": "label", + "label": "RED LABEL: Normal label" +} +``` + +### splitter +- visual splitter of items (more divider than splitter) + +``` +{ + "type": "splitter" +} +``` + +## Proxy wrappers +- should wraps multiple inputs only visually +- these does not have `"key"` key and do not allow to have `"is_file"` or `"is_group"` modifiers enabled +- can't be used as widget (first item in e.g. `list`, `dict-modifiable`, etc.) + +### form +- wraps inputs into form look layout +- should be used only for Pure inputs + +``` +{ + "type": "dict-form", + "children": [ + { + "type": "text", + "key": "deadline_department", + "label": "Deadline apartment" + }, { + "type": "number", + "key": "deadline_priority", + "label": "Deadline priority" + }, { + ... + } + ] +} +``` + + +### collapsible-wrap +- wraps inputs into collapsible widget + - looks like `dict` but does not hold `"key"` +- should be used only for Pure inputs + +``` +{ + "type": "collapsible-wrap", + "label": "Collapsible example" + "children": [ + { + "type": "text", + "key": "_example_input_collapsible", + "label": "Example input in collapsible wrapper" + }, { + ... + } + ] +} diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_main.json b/pype/settings/entities/schemas/projects_schema/schema_main.json similarity index 87% rename from pype/tools/settings/settings/gui_schemas/projects_schema/schema_main.json rename to pype/settings/entities/schemas/projects_schema/schema_main.json index 73266a9e79..8ad059f1c7 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_main.json +++ b/pype/settings/entities/schemas/projects_schema/schema_main.json @@ -5,12 +5,19 @@ { "type": "anatomy", "key": "project_anatomy", + "label": "Anatomy", "children": [ { - "type": "anatomy_roots", "key": "roots", "label": "Roots", - "is_file": true + "type": "anatomy_roots", + "is_file": true, + "is_group": true, + "expandable": false, + "object_type": { + "type": "path-widget", + "multiplatform": true + } }, { "type": "schema", diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_plugins.json b/pype/settings/entities/schemas/projects_schema/schema_plugins.json similarity index 89% rename from pype/tools/settings/settings/gui_schemas/projects_schema/schema_plugins.json rename to pype/settings/entities/schemas/projects_schema/schema_plugins.json index ce90ceb881..a7ed64be17 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_plugins.json +++ b/pype/settings/entities/schemas/projects_schema/schema_plugins.json @@ -1,32 +1,32 @@ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "plugins", "label": "Plugins", "children": [ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "standalonepublisher", "label": "Standalone Publisher", "children": [ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "publish", "label": "Publish plugins", "is_file": true, "children": [ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "ExtractThumbnailSP", "label": "ExtractThumbnailSP", "is_group": true, "children": [ { "type": "dict", - "collapsable": false, + "collapsible": false, "key": "ffmpeg_args", "label": "ffmpeg_args", "children": [ diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_celaction.json b/pype/settings/entities/schemas/projects_schema/schema_project_celaction.json similarity index 95% rename from pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_celaction.json rename to pype/settings/entities/schemas/projects_schema/schema_project_celaction.json index 86d6141c4a..500e5b2298 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_celaction.json +++ b/pype/settings/entities/schemas/projects_schema/schema_project_celaction.json @@ -1,19 +1,19 @@ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "celaction", "label": "CelAction", "is_file": true, "children": [ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "publish", "label": "Publish plugins", "children": [ { "type": "dict", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "key": "ExtractCelactionDeadline", "label": "ExtractCelactionDeadline", diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_ftrack.json b/pype/settings/entities/schemas/projects_schema/schema_project_ftrack.json similarity index 98% rename from pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_ftrack.json rename to pype/settings/entities/schemas/projects_schema/schema_project_ftrack.json index 508b8e870d..39cd79e851 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_ftrack.json +++ b/pype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -2,8 +2,7 @@ "type": "dict", "key": "ftrack", "label": "Ftrack", - "collapsable": true, - "checkbox_key": "enabled", + "collapsible": true, "is_file": true, "children": [ { @@ -162,6 +161,7 @@ "type": "dict", "key": "status_update", "label": "Update status on task action", + "is_group": true, "checkbox_key": "enabled", "children": [ { @@ -240,6 +240,7 @@ "type": "dict", "key": "status_task_to_version", "label": "Sync status from Task to Version", + "is_group": true, "checkbox_key": "enabled", "children": [ { @@ -272,6 +273,7 @@ "type": "dict", "key": "status_version_to_task", "label": "Sync status from Version to Task", + "is_group": true, "checkbox_key": "enabled", "children": [ { @@ -327,6 +329,7 @@ { "type": "dict", "key": "next_task_update", + "is_group": true, "label": "Update status on next task", "checkbox_key": "enabled", "children": [ @@ -386,6 +389,7 @@ { "type": "dict", "key": "application_launch_statuses", + "is_group": true, "label": "Application - Status change on launch", "checkbox_key": "enabled", "children": [ @@ -605,14 +609,14 @@ }, { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "publish", "label": "Publish plugins", "is_file": true, "children": [ { "type": "dict", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "key": "IntegrateFtrackNote", "label": "IntegrateFtrackNote", @@ -639,7 +643,7 @@ { "type": "dict", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "key": "ValidateFtrackAttributes", "label": "ValidateFtrackAttributes", diff --git a/pype/settings/entities/schemas/projects_schema/schema_project_global.json b/pype/settings/entities/schemas/projects_schema/schema_project_global.json new file mode 100644 index 0000000000..fa5db9af88 --- /dev/null +++ b/pype/settings/entities/schemas/projects_schema/schema_project_global.json @@ -0,0 +1,26 @@ +{ + "type": "dict", + "collapsible": true, + "key": "global", + "label": "Global", + "is_file": true, + "children": [ + { + "type": "schema", + "name": "schema_global_publish" + }, + { + "type": "schema", + "name": "schema_global_tools" + }, + { + "type": "raw-json", + "label": "Project Folder Structure", + "key": "project_folder_structure" + }, + { + "type": "schema", + "name": "schema_project_syncserver" + } + ] +} diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_harmony.json b/pype/settings/entities/schemas/projects_schema/schema_project_harmony.json similarity index 90% rename from pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_harmony.json rename to pype/settings/entities/schemas/projects_schema/schema_project_harmony.json index 92ad39ac2c..5d1cbff1b8 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_harmony.json +++ b/pype/settings/entities/schemas/projects_schema/schema_project_harmony.json @@ -1,20 +1,20 @@ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "harmony", "label": "Harmony", "is_file": true, "children": [ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "publish", "label": "Publish plugins", "children": [] }, { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "general", "label": "General", "children": [ diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_hiero.json b/pype/settings/entities/schemas/projects_schema/schema_project_hiero.json similarity index 93% rename from pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_hiero.json rename to pype/settings/entities/schemas/projects_schema/schema_project_hiero.json index b6276ccf9e..d2191a45a0 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_hiero.json +++ b/pype/settings/entities/schemas/projects_schema/schema_project_hiero.json @@ -1,19 +1,19 @@ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "hiero", "label": "Hiero", "is_file": true, "children": [ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "create", "label": "Create plugins", "children": [ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "CreateShotClip", "label": "Create Shot Clip", "is_group": true, @@ -21,7 +21,7 @@ { "type": "collapsible-wrap", "label": "Shot Hierarchy And Rename Settings", - "collapsable": false, + "collapsible": false, "children": [ { "type": "text", @@ -53,7 +53,7 @@ { "type": "collapsible-wrap", "label": "Shot Template Keywords", - "collapsable": false, + "collapsible": false, "children": [ { "type": "text", @@ -85,7 +85,7 @@ { "type": "collapsible-wrap", "label": "Vertical Synchronization Of Attributes", - "collapsable": false, + "collapsible": false, "children": [ { "type": "boolean", @@ -97,7 +97,7 @@ { "type": "collapsible-wrap", "label": "Shot Attributes", - "collapsable": false, + "collapsible": false, "children": [ { "type": "number", @@ -122,13 +122,13 @@ }, { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "publish", "label": "Publish plugins", "children": [ { "type": "dict", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "key": "CollectInstanceVersion", "label": "Collect Instance Version", @@ -143,7 +143,7 @@ }, { "type": "dict", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "key": "ExtractReviewCutUpVideo", "label": "Extract Review Cut Up Video", diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_maya.json b/pype/settings/entities/schemas/projects_schema/schema_project_maya.json similarity index 95% rename from pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_maya.json rename to pype/settings/entities/schemas/projects_schema/schema_project_maya.json index 069ae3c977..5ba5de1557 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_maya.json +++ b/pype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -1,6 +1,6 @@ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "maya", "label": "Maya", "is_file": true, diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_nuke.json b/pype/settings/entities/schemas/projects_schema/schema_project_nuke.json similarity index 93% rename from pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_nuke.json rename to pype/settings/entities/schemas/projects_schema/schema_project_nuke.json index 7a6960bcbd..b60b548cb0 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_nuke.json +++ b/pype/settings/entities/schemas/projects_schema/schema_project_nuke.json @@ -1,19 +1,19 @@ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "nuke", "label": "Nuke", "is_file": true, "children": [ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "create", "label": "Create plugins", "children": [ { "type": "dict", - "collapsable": false, + "collapsible": false, "key": "CreateWriteRender", "label": "CreateWriteRender", "is_group": true, @@ -27,7 +27,7 @@ }, { "type": "dict", - "collapsable": false, + "collapsible": false, "key": "CreateWritePrerender", "label": "CreateWritePrerender", "is_group": true, @@ -43,13 +43,13 @@ }, { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "publish", "label": "Publish plugins", "children": [ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "PreCollectNukeInstances", "label": "PreCollectNukeInstances", "is_group": true, @@ -63,7 +63,7 @@ }, { "type": "dict", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "key": "ExtractThumbnail", "label": "ExtractThumbnail", @@ -83,7 +83,7 @@ }, { "type": "dict", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "key": "ValidateKnobs", "label": "ValidateKnobs", @@ -103,7 +103,7 @@ }, { "type": "dict", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "key": "ExtractReviewDataLut", "label": "ExtractReviewDataLut", @@ -118,7 +118,7 @@ }, { "type": "dict", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "key": "ExtractReviewDataMov", "label": "ExtractReviewDataMov", @@ -138,7 +138,7 @@ }, { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "ExtractSlateFrame", "label": "ExtractSlateFrame", "is_group": true, @@ -152,7 +152,7 @@ }, { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "NukeSubmitDeadline", "label": "NukeSubmitDeadline", "is_group": true, diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_resolve.json b/pype/settings/entities/schemas/projects_schema/schema_project_resolve.json similarity index 94% rename from pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_resolve.json rename to pype/settings/entities/schemas/projects_schema/schema_project_resolve.json index fb9b9b7a0a..68e405b7d7 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_resolve.json +++ b/pype/settings/entities/schemas/projects_schema/schema_project_resolve.json @@ -1,19 +1,19 @@ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "resolve", "label": "DaVinci Resolve", "is_file": true, "children": [ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "create", "label": "Creator plugins", "children": [ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "CreateShotClip", "label": "Create Shot Clip", "is_group": true, @@ -21,7 +21,7 @@ { "type": "collapsible-wrap", "label": "Shot Hierarchy And Rename Settings", - "collapsable": false, + "collapsible": false, "children": [ { "type": "text", @@ -53,7 +53,7 @@ { "type": "collapsible-wrap", "label": "Shot Template Keywords", - "collapsable": false, + "collapsible": false, "children": [ { "type": "text", @@ -85,7 +85,7 @@ { "type": "collapsible-wrap", "label": "Vertical Synchronization Of Attributes", - "collapsable": false, + "collapsible": false, "children": [ { "type": "boolean", @@ -97,7 +97,7 @@ { "type": "collapsible-wrap", "label": "Shot Attributes", - "collapsable": false, + "collapsible": false, "children": [ { "type": "number", diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_standalonepublisher.json b/pype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json similarity index 92% rename from pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_standalonepublisher.json rename to pype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json index 40b27f766f..735b9611d2 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_standalonepublisher.json +++ b/pype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json @@ -1,27 +1,27 @@ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "standalonepublisher", "label": "Standalone Publisher", "is_file": true, "children": [ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "publish", "label": "Publish plugins", "is_file": true, "children": [ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "ExtractThumbnailSP", "label": "ExtractThumbnailSP", "is_group": true, "children": [ { "type": "dict", - "collapsable": false, + "collapsible": false, "key": "ffmpeg_args", "label": "ffmpeg_args", "children": [ @@ -45,10 +45,10 @@ }, { "type": "dict-modifiable", - "collapsable": true, + "collapsible": true, "key": "create", "label": "Creator plugins", - "collapsable_key": true, + "collapsible_key": true, "is_file": true, "object_type": { "type": "dict", diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_syncserver.json b/pype/settings/entities/schemas/projects_schema/schema_project_syncserver.json similarity index 93% rename from pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_syncserver.json rename to pype/settings/entities/schemas/projects_schema/schema_project_syncserver.json index 396e4ca2dc..005e188b4d 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_syncserver.json +++ b/pype/settings/entities/schemas/projects_schema/schema_project_syncserver.json @@ -2,7 +2,7 @@ "type": "dict", "key": "sync_server", "label": "Sync Server (currently unused)", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "is_file": true, "children": [ @@ -15,7 +15,7 @@ "type": "dict", "key": "config", "label": "Config", - "collapsable": true, + "collapsible": true, "children": [ { @@ -46,10 +46,10 @@ ] }, { "type": "dict-modifiable", - "collapsable": true, + "collapsible": true, "key": "sites", "label": "Sites", - "collapsable_key": false, + "collapsible_key": false, "is_file": true, "object_type": { diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_unreal.json b/pype/settings/entities/schemas/projects_schema/schema_project_unreal.json similarity index 91% rename from pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_unreal.json rename to pype/settings/entities/schemas/projects_schema/schema_project_unreal.json index 392289296a..b6e94d9d03 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_unreal.json +++ b/pype/settings/entities/schemas/projects_schema/schema_project_unreal.json @@ -1,13 +1,13 @@ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "unreal", "label": "Unreal Engine", "is_file": true, "children": [ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "project_setup", "label": "Project Setup", "children": [ diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_anatomy_attributes.json b/pype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json similarity index 97% rename from pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_anatomy_attributes.json rename to pype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json index b045ef978b..8925233bb8 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_anatomy_attributes.json +++ b/pype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json @@ -1,9 +1,10 @@ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "attributes", "label": "Attributes", "is_file": true, + "is_group": true, "children": [ { "type": "number", diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_anatomy_imageio.json b/pype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json similarity index 97% rename from pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_anatomy_imageio.json rename to pype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json index 0032e3de06..7070e4b54f 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_anatomy_imageio.json +++ b/pype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json @@ -3,6 +3,7 @@ "key": "imageio", "label": "Color Management and Output Formats", "is_file": true, + "is_group": true, "children": [ { "key": "hiero", @@ -13,7 +14,7 @@ "key": "workfile", "type": "dict", "label": "Workfile", - "collapsable": false, + "collapsible": false, "children": [ { "type": "form", @@ -87,7 +88,7 @@ "key": "regexInputs", "type": "dict", "label": "Colorspace on Inputs by regex detection", - "collapsable": true, + "collapsible": true, "children": [ { "type": "list", @@ -121,8 +122,7 @@ "key": "workfile", "type": "dict", "label": "Workfile", - "collapsable": false, - "is_group": true, + "collapsible": false, "children": [ { "type": "form", @@ -155,13 +155,13 @@ "spi-anim": "spi-anim" }, { - "aces_1.0.3": "aces_0.1.1" + "aces_0.1.1": "aces_0.1.1" }, { - "aces_1.0.3": "aces_0.7.1" + "aces_0.7.1": "aces_0.7.1" }, { - "aces_1.0.3": "aces_1.0.1" + "aces_1.0.1": "aces_1.0.1" }, { "aces_1.0.3": "aces_1.0.3" @@ -219,8 +219,7 @@ "key": "nodes", "type": "dict", "label": "Nodes", - "collapsable": true, - "is_group": true, + "collapsible": true, "children": [ { "key": "requiredNodes", @@ -324,7 +323,7 @@ "key": "regexInputs", "type": "dict", "label": "Colorspace on Inputs by regex detection", - "collapsable": true, + "collapsible": true, "children": [ { "type": "list", diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_anatomy_templates.json b/pype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_templates.json similarity index 96% rename from pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_anatomy_templates.json rename to pype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_templates.json index 1f545f14be..05718e0bc9 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_anatomy_templates.json +++ b/pype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_templates.json @@ -1,10 +1,11 @@ { - "type": "dict", - "collapsable": true, + "type": "anatomy_templates", "key": "templates", "label": "Templates", - "collapsable_key": true, + "collapsible": true, + "collapsible_key": true, "is_file": true, + "is_group": true, "children": [ { "type": "number", diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_global_publish.json b/pype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json similarity index 98% rename from pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_global_publish.json rename to pype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 5d17830b92..4045870a9a 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_global_publish.json +++ b/pype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -1,12 +1,12 @@ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "publish", "label": "Publish plugins", "children": [ { "type": "dict", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "key": "IntegrateMasterVersion", "label": "IntegrateMasterVersion", @@ -21,7 +21,7 @@ }, { "type": "dict", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "key": "ExtractJpegEXR", "label": "ExtractJpegEXR", @@ -54,7 +54,7 @@ }, { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "ExtractReview", "label": "ExtractReview", "checkbox_key": "enabled", @@ -188,7 +188,7 @@ }, { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "ExtractBurnin", "label": "ExtractBurnin", "checkbox_key": "enabled", @@ -201,7 +201,7 @@ }, { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "options", "label": "Burnin formating options", "children": [ @@ -268,7 +268,7 @@ "label": "Burnins", "type": "dict-modifiable", "highlight_content": true, - "collapsable": false, + "collapsible": false, "object_type": { "type": "dict", "children": [ @@ -312,7 +312,7 @@ }, { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "IntegrateAssetNew", "label": "IntegrateAssetNew", "is_group": true, @@ -326,7 +326,7 @@ }, { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "ProcessSubmittedJobOnFarm", "label": "ProcessSubmittedJobOnFarm", "checkbox_key": "enabled", diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_global_tools.json b/pype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json similarity index 93% rename from pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_global_tools.json rename to pype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json index d89477edd1..fa49e12fb7 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_global_tools.json +++ b/pype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json @@ -1,18 +1,18 @@ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "tools", "label": "Tools", "children": [ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "creator", "label": "Creator", "children": [ { "type": "dict-modifiable", - "collapsable": false, + "collapsible": false, "key": "families_smart_select", "label": "Families smart select", "object_type": { @@ -24,7 +24,7 @@ }, { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "Workfiles", "label": "Workfiles", "children": [ @@ -62,7 +62,7 @@ }, { "type": "dict-modifiable", - "collapsable": true, + "collapsible": true, "key": "sw_folders", "label": "Extra task folders", "is_group": true, diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_maya_capture.json b/pype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json similarity index 99% rename from pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_maya_capture.json rename to pype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json index ba7cf4b525..648fca28e6 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_maya_capture.json +++ b/pype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json @@ -4,7 +4,7 @@ "children": [ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "capture", "label": "Maya Playblast settings", "is_file": true, @@ -311,7 +311,7 @@ }, { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "Viewport Options", "label": "Viewport Options", "children": [ @@ -527,7 +527,7 @@ }, { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "Camera Options", "label": "Camera Options", "children": [ diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_maya_load.json b/pype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json similarity index 98% rename from pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_maya_load.json rename to pype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json index 5aec3715bd..dd9d0508b4 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_maya_load.json +++ b/pype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json @@ -1,12 +1,12 @@ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "load", "label": "Loader plugins", "children": [ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "colors", "label": "Loaded Subsets Outliner Colors", "children": [ diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_maya_publish.json b/pype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json similarity index 95% rename from pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_maya_publish.json rename to pype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index d1de128934..0d705d3d02 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_maya_publish.json +++ b/pype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -1,6 +1,6 @@ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "publish", "label": "Publish plugins", "children": [ @@ -10,7 +10,7 @@ }, { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "CollectMayaRender", "label": "Collect Render Layers", "children": [ @@ -30,7 +30,7 @@ }, { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "ValidateCameraAttributes", "label": "Validate Camera Attributes", "checkbox_key": "enabled", @@ -49,7 +49,7 @@ }, { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "ValidateModelName", "label": "Validate Model Name", "checkbox_key": "enabled", @@ -79,7 +79,7 @@ }, { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "ValidateAssemblyName", "label": "Validate Assembly Name", "checkbox_key": "enabled", @@ -93,7 +93,7 @@ }, { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "ValidateShaderName", "label": "ValidateShaderName", "checkbox_key": "enabled", @@ -116,7 +116,7 @@ }, { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "ValidateMeshHasOverlappingUVs", "label": "ValidateMeshHasOverlappingUVs", "checkbox_key": "enabled", @@ -130,7 +130,7 @@ }, { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "ValidateAttributes", "label": "ValidateAttributes", "checkbox_key": "enabled", @@ -156,7 +156,7 @@ }, { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "ExtractCameraAlembic", "label": "Extract camera to Alembic", "checkbox_key": "enabled", @@ -184,7 +184,7 @@ }, { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "MayaSubmitDeadline", "label": "Submit maya job to deadline", "checkbox_key": "enabled", diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_publish_gui_filter.json b/pype/settings/entities/schemas/projects_schema/schemas/schema_publish_gui_filter.json similarity index 87% rename from pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_publish_gui_filter.json rename to pype/settings/entities/schemas/projects_schema/schemas/schema_publish_gui_filter.json index efc6c1d629..1539bd0738 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_publish_gui_filter.json +++ b/pype/settings/entities/schemas/projects_schema/schemas/schema_publish_gui_filter.json @@ -1,6 +1,6 @@ { "type": "dict-modifiable", - "collapsable": true, + "collapsible": true, "key": "filters", "label": "Publish GUI Filters", "object_type": { diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_workfile_build.json b/pype/settings/entities/schemas/projects_schema/schemas/schema_workfile_build.json similarity index 99% rename from pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_workfile_build.json rename to pype/settings/entities/schemas/projects_schema/schemas/schema_workfile_build.json index bf0aff2d41..0cb36c2f92 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_workfile_build.json +++ b/pype/settings/entities/schemas/projects_schema/schemas/schema_workfile_build.json @@ -1,6 +1,6 @@ { "type": "dict", - "collapsable": true, + "collapsible": true, "key": "workfile_build", "label": "Workfile Build Settings", "children": [ diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schemas/template_color.json b/pype/settings/entities/schemas/projects_schema/schemas/template_color.json similarity index 100% rename from pype/tools/settings/settings/gui_schemas/projects_schema/schemas/template_color.json rename to pype/settings/entities/schemas/projects_schema/schemas/template_color.json diff --git a/pype/settings/entities/schemas/system_schema/example_schema.json b/pype/settings/entities/schemas/system_schema/example_schema.json index 4175cebd42..c8920db88a 100644 --- a/pype/settings/entities/schemas/system_schema/example_schema.json +++ b/pype/settings/entities/schemas/system_schema/example_schema.json @@ -326,10 +326,10 @@ "multipath": true }, { - "key": "collapsable", + "key": "collapsible", "type": "dict", - "label": "collapsable dictionary", - "collapsable": true, + "label": "collapsible dictionary", + "collapsible": true, "is_group": true, "children": [ { @@ -340,10 +340,10 @@ ] }, { - "key": "collapsable_expanded", + "key": "collapsible_expanded", "type": "dict", - "label": "collapsable dictionary, expanded on creation", - "collapsable": true, + "label": "collapsible dictionary, expanded on creation", + "collapsible": true, "collapsed": false, "is_group": true, "children": [ @@ -355,10 +355,10 @@ ] }, { - "key": "not_collapsable", + "key": "not_collapsible", "type": "dict", - "label": "Not collapsable", - "collapsable": false, + "label": "Not collapsible", + "collapsible": false, "is_group": true, "children": [ { diff --git a/pype/settings/entities/schemas/system_schema/host_settings/schema_aftereffects.json b/pype/settings/entities/schemas/system_schema/host_settings/schema_aftereffects.json index 4578e836b5..4304c65445 100644 --- a/pype/settings/entities/schemas/system_schema/host_settings/schema_aftereffects.json +++ b/pype/settings/entities/schemas/system_schema/host_settings/schema_aftereffects.json @@ -2,7 +2,7 @@ "type": "dict", "key": "aftereffects", "label": "Adobe AfterEffects", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "children": [ { diff --git a/pype/settings/entities/schemas/system_schema/host_settings/schema_blender.json b/pype/settings/entities/schemas/system_schema/host_settings/schema_blender.json index fbb9a82b87..5d8cb45da8 100644 --- a/pype/settings/entities/schemas/system_schema/host_settings/schema_blender.json +++ b/pype/settings/entities/schemas/system_schema/host_settings/schema_blender.json @@ -2,7 +2,7 @@ "type": "dict", "key": "blender", "label": "Blender", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "children": [ { diff --git a/pype/settings/entities/schemas/system_schema/host_settings/schema_celaction.json b/pype/settings/entities/schemas/system_schema/host_settings/schema_celaction.json index 0578dcae9e..ab3f0f3f15 100644 --- a/pype/settings/entities/schemas/system_schema/host_settings/schema_celaction.json +++ b/pype/settings/entities/schemas/system_schema/host_settings/schema_celaction.json @@ -2,7 +2,7 @@ "type": "dict", "key": "celaction", "label": "CelAction2D", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "children": [ { diff --git a/pype/settings/entities/schemas/system_schema/host_settings/schema_djv.json b/pype/settings/entities/schemas/system_schema/host_settings/schema_djv.json index 6df690d43c..02c90a92ad 100644 --- a/pype/settings/entities/schemas/system_schema/host_settings/schema_djv.json +++ b/pype/settings/entities/schemas/system_schema/host_settings/schema_djv.json @@ -2,7 +2,7 @@ "type": "dict", "key": "djvview", "label": "DJV View", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "children": [ { diff --git a/pype/settings/entities/schemas/system_schema/host_settings/schema_fusion.json b/pype/settings/entities/schemas/system_schema/host_settings/schema_fusion.json index 189588ace1..1c1b7653d9 100644 --- a/pype/settings/entities/schemas/system_schema/host_settings/schema_fusion.json +++ b/pype/settings/entities/schemas/system_schema/host_settings/schema_fusion.json @@ -2,7 +2,7 @@ "type": "dict", "key": "fusion", "label": "Blackmagic Fusion", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "children": [ { diff --git a/pype/settings/entities/schemas/system_schema/host_settings/schema_harmony.json b/pype/settings/entities/schemas/system_schema/host_settings/schema_harmony.json index 2a042be1e3..b0abf35bfa 100644 --- a/pype/settings/entities/schemas/system_schema/host_settings/schema_harmony.json +++ b/pype/settings/entities/schemas/system_schema/host_settings/schema_harmony.json @@ -2,7 +2,7 @@ "type": "dict", "key": "harmony", "label": "Toon Boom Harmony", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "children": [ { diff --git a/pype/settings/entities/schemas/system_schema/host_settings/schema_houdini.json b/pype/settings/entities/schemas/system_schema/host_settings/schema_houdini.json index 404b2bfbc7..cc0cd54cf2 100644 --- a/pype/settings/entities/schemas/system_schema/host_settings/schema_houdini.json +++ b/pype/settings/entities/schemas/system_schema/host_settings/schema_houdini.json @@ -2,7 +2,7 @@ "type": "dict", "key": "houdini", "label": "SideFX Houdini", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "children": [ { diff --git a/pype/settings/entities/schemas/system_schema/host_settings/schema_maya.json b/pype/settings/entities/schemas/system_schema/host_settings/schema_maya.json index ba639ca134..84782cb3d8 100644 --- a/pype/settings/entities/schemas/system_schema/host_settings/schema_maya.json +++ b/pype/settings/entities/schemas/system_schema/host_settings/schema_maya.json @@ -2,7 +2,7 @@ "type": "dict", "key": "maya", "label": "Autodesk Maya", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "children": [ { diff --git a/pype/settings/entities/schemas/system_schema/host_settings/schema_mayabatch.json b/pype/settings/entities/schemas/system_schema/host_settings/schema_mayabatch.json index cee0cd8e9f..dbd850dcd6 100644 --- a/pype/settings/entities/schemas/system_schema/host_settings/schema_mayabatch.json +++ b/pype/settings/entities/schemas/system_schema/host_settings/schema_mayabatch.json @@ -2,7 +2,7 @@ "type": "dict", "key": "mayabatch", "label": "Autodesk Maya Batch", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "children": [ { diff --git a/pype/settings/entities/schemas/system_schema/host_settings/schema_photoshop.json b/pype/settings/entities/schemas/system_schema/host_settings/schema_photoshop.json index 61b9473f2b..136eb16888 100644 --- a/pype/settings/entities/schemas/system_schema/host_settings/schema_photoshop.json +++ b/pype/settings/entities/schemas/system_schema/host_settings/schema_photoshop.json @@ -2,7 +2,7 @@ "type": "dict", "key": "photoshop", "label": "Adobe Photoshop", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "children": [ { diff --git a/pype/settings/entities/schemas/system_schema/host_settings/schema_resolve.json b/pype/settings/entities/schemas/system_schema/host_settings/schema_resolve.json index c7aa4e15d3..2d11e1def4 100644 --- a/pype/settings/entities/schemas/system_schema/host_settings/schema_resolve.json +++ b/pype/settings/entities/schemas/system_schema/host_settings/schema_resolve.json @@ -2,7 +2,7 @@ "type": "dict", "key": "resolve", "label": "Blackmagic DaVinci Resolve", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "children": [ { diff --git a/pype/settings/entities/schemas/system_schema/host_settings/schema_shell.json b/pype/settings/entities/schemas/system_schema/host_settings/schema_shell.json index f1241ea3bd..4fdbd65c24 100644 --- a/pype/settings/entities/schemas/system_schema/host_settings/schema_shell.json +++ b/pype/settings/entities/schemas/system_schema/host_settings/schema_shell.json @@ -2,7 +2,7 @@ "type": "dict", "key": "shell", "label": "Shell", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "children": [ { diff --git a/pype/settings/entities/schemas/system_schema/host_settings/schema_tvpaint.json b/pype/settings/entities/schemas/system_schema/host_settings/schema_tvpaint.json index b328e5cf79..1c88d12cab 100644 --- a/pype/settings/entities/schemas/system_schema/host_settings/schema_tvpaint.json +++ b/pype/settings/entities/schemas/system_schema/host_settings/schema_tvpaint.json @@ -2,7 +2,7 @@ "type": "dict", "key": "tvpaint", "label": "TVPaint", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "children": [ { diff --git a/pype/settings/entities/schemas/system_schema/host_settings/schema_unreal.json b/pype/settings/entities/schemas/system_schema/host_settings/schema_unreal.json index 8560d8782e..d7065ad3ff 100644 --- a/pype/settings/entities/schemas/system_schema/host_settings/schema_unreal.json +++ b/pype/settings/entities/schemas/system_schema/host_settings/schema_unreal.json @@ -2,7 +2,7 @@ "type": "dict", "key": "unreal", "label": "Unreal Editor", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "children": [ { diff --git a/pype/settings/entities/schemas/system_schema/host_settings/template_host_variant.json b/pype/settings/entities/schemas/system_schema/host_settings/template_host_variant.json index cea7da3a81..06f7859117 100644 --- a/pype/settings/entities/schemas/system_schema/host_settings/template_host_variant.json +++ b/pype/settings/entities/schemas/system_schema/host_settings/template_host_variant.json @@ -9,7 +9,7 @@ "type": "dict", "key": "{host_name}_{host_version}", "label": "{host_version}", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "children": [ { diff --git a/pype/settings/entities/schemas/system_schema/host_settings/template_nuke.json b/pype/settings/entities/schemas/system_schema/host_settings/template_nuke.json index a1f975c2a7..01c3be726a 100644 --- a/pype/settings/entities/schemas/system_schema/host_settings/template_nuke.json +++ b/pype/settings/entities/schemas/system_schema/host_settings/template_nuke.json @@ -3,7 +3,7 @@ "type": "dict", "key": "{nuke_type}", "label": "Foundry {nuke_label}", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "children": [ { diff --git a/pype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json b/pype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json index 0683bd10b5..5647e14ebf 100644 --- a/pype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json +++ b/pype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json @@ -2,7 +2,7 @@ "type": "dict", "key": "ftrack", "label": "Ftrack", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "children": [ { @@ -58,7 +58,7 @@ "key": "intent", "type": "dict", "label": "Intent", - "collapsable_key": true, + "collapsible_key": true, "is_group": true, "children": [ { diff --git a/pype/settings/entities/schemas/system_schema/schema_applications.json b/pype/settings/entities/schemas/system_schema/schema_applications.json index e2b882ba7d..61d47df8b6 100644 --- a/pype/settings/entities/schemas/system_schema/schema_applications.json +++ b/pype/settings/entities/schemas/system_schema/schema_applications.json @@ -2,7 +2,7 @@ "key": "applications", "type": "dict", "label": "Applications", - "collapsable": true, + "collapsible": true, "is_file": true, "children": [ { diff --git a/pype/settings/entities/schemas/system_schema/schema_general.json b/pype/settings/entities/schemas/system_schema/schema_general.json index a919526de8..f75b584c1b 100644 --- a/pype/settings/entities/schemas/system_schema/schema_general.json +++ b/pype/settings/entities/schemas/system_schema/schema_general.json @@ -2,7 +2,7 @@ "key": "general", "type": "dict", "label": "General", - "collapsable": true, + "collapsible": true, "is_file": true, "children": [ { diff --git a/pype/settings/entities/schemas/system_schema/schema_modules.json b/pype/settings/entities/schemas/system_schema/schema_modules.json index a3b34a37c7..7c45cebabb 100644 --- a/pype/settings/entities/schemas/system_schema/schema_modules.json +++ b/pype/settings/entities/schemas/system_schema/schema_modules.json @@ -2,14 +2,14 @@ "key": "modules", "type": "dict", "label": "Modules", - "collapsable": true, + "collapsible": true, "is_file": true, "children": [ { "type": "dict", "key": "avalon", "label": "Avalon", - "collapsable": true, + "collapsible": true, "children": [ { "type": "text", @@ -45,7 +45,7 @@ "type": "dict", "key": "rest_api", "label": "Rest Api", - "collapsable": true, + "collapsible": true, "children": [ { "type": "number", @@ -70,7 +70,7 @@ "type": "dict", "key": "timers_manager", "label": "Timers Manager", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "children": [ { @@ -96,7 +96,7 @@ "type": "dict", "key": "clockify", "label": "Clockify", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "children": [ { @@ -114,7 +114,7 @@ "type": "dict", "key": "sync_server", "label": "Sync Server", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "children": [{ "type": "boolean", @@ -125,7 +125,7 @@ "type": "dict", "key": "deadline", "label": "Deadline", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "children": [ { @@ -144,7 +144,7 @@ "type": "dict", "key": "muster", "label": "Muster", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "children": [ { @@ -175,7 +175,7 @@ "type": "dict", "key": "log_viewer", "label": "Logging", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "children": [ { @@ -189,7 +189,7 @@ "type": "dict", "key": "user", "label": "User setting", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "children": [ { @@ -203,7 +203,7 @@ "type": "dict", "key": "standalonepublish_tool", "label": "Standalone Publish", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "children": [ { @@ -217,7 +217,7 @@ "type": "dict", "key": "idle_manager", "label": "Idle Manager", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "children": [ { diff --git a/pype/settings/entities/schemas/system_schema/schema_tools.json b/pype/settings/entities/schemas/system_schema/schema_tools.json index c8f4829a09..2c04abc47c 100644 --- a/pype/settings/entities/schemas/system_schema/schema_tools.json +++ b/pype/settings/entities/schemas/system_schema/schema_tools.json @@ -2,7 +2,7 @@ "key": "tools", "type": "dict", "label": "Tools", - "collapsable": true, + "collapsible": true, "is_file": true, "children": [ { diff --git a/pype/settings/entities/schemas/system_schema/tool_settings/schema_arnold.json b/pype/settings/entities/schemas/system_schema/tool_settings/schema_arnold.json index aa50610ddc..db2be09c83 100644 --- a/pype/settings/entities/schemas/system_schema/tool_settings/schema_arnold.json +++ b/pype/settings/entities/schemas/system_schema/tool_settings/schema_arnold.json @@ -2,7 +2,7 @@ "type": "dict", "key": "mtoa", "label": "Autodesk Arnold", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "children": [ { diff --git a/pype/settings/entities/schemas/system_schema/tool_settings/schema_vray.json b/pype/settings/entities/schemas/system_schema/tool_settings/schema_vray.json index 0ce24a310c..fdb25610e0 100644 --- a/pype/settings/entities/schemas/system_schema/tool_settings/schema_vray.json +++ b/pype/settings/entities/schemas/system_schema/tool_settings/schema_vray.json @@ -2,7 +2,7 @@ "type": "dict", "key": "vray", "label": "Chaos Group Vray", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "children": [ { diff --git a/pype/settings/entities/schemas/system_schema/tool_settings/schema_yeti.json b/pype/settings/entities/schemas/system_schema/tool_settings/schema_yeti.json index b94dbbfcaa..96c6b624fc 100644 --- a/pype/settings/entities/schemas/system_schema/tool_settings/schema_yeti.json +++ b/pype/settings/entities/schemas/system_schema/tool_settings/schema_yeti.json @@ -2,7 +2,7 @@ "type": "dict", "key": "yeti", "label": "Pergrine Labs Yeti", - "collapsable": true, + "collapsible": true, "checkbox_key": "enabled", "children": [ { diff --git a/pype/tools/settings/settings/README.md b/pype/tools/settings/settings/README.md index 643043b6c8..65d73a0728 100644 --- a/pype/tools/settings/settings/README.md +++ b/pype/tools/settings/settings/README.md @@ -126,8 +126,8 @@ - without label it is just wrap item holding `"key"` - can't have `"is_group"` key set to True as it breaks visual override showing - if `"label"` is entetered there which will be shown in GUI - - item with label can be collapsable - - that can be set with key `"collapsable"` as `True`/`False` (Default: `True`) + - 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 @@ -138,7 +138,7 @@ "key": "applications", "type": "dict", "label": "Applications", - "collapsable": true, + "collapsible": true, "highlight_content": true, "is_group": true, "is_file": true, @@ -320,8 +320,8 @@ - there are 2 possible ways how to set the type: 1.) dictionary with item modifiers (`number` input has `minimum`, `maximum` and `decimals`) in that case item type must be set as value of `"type"` (example below) 2.) item type name as string without modifiers (e.g. `text`) -- this input can be collapsable - - that can be set with key `"collapsable"` as `True`/`False` (Default: `True`) +- this input 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`) 1.) with item modifiers diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_global.json b/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_global.json deleted file mode 100644 index ab9b56115d..0000000000 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_global.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "type": "dict", - "collapsable": true, - "key": "global", - "label": "Global", - "is_file": true, - "children": [ - { - "type": "schema", - "name": "schema_global_publish" - }, - { - "type": "schema", - "name": "schema_global_tools" - }, - - { - "type": "collapsible-wrap", - "label": "Project Folder Structure", - "children": [ - { - "type": "raw-json", - "key": "project_folder_structure" - } - ] - }, - - { - "type": "schema", - "name": "schema_project_syncserver" - } - ] -} diff --git a/pype/tools/settings/settings/widgets/__init__.py b/pype/tools/settings/settings/widgets/__init__.py index 03cc3224da..c9fec16f6e 100644 --- a/pype/tools/settings/settings/widgets/__init__.py +++ b/pype/tools/settings/settings/widgets/__init__.py @@ -1,11 +1,8 @@ from .window import MainWidget -from .categories import ProjectListWidget -from . import item_types -from . import anatomy_types +from .widgets import ProjectListWidget + __all__ = [ "MainWidget", - "ProjectListWidget", - "item_types", - "anatomy_types" + "ProjectListWidget" ] diff --git a/pype/tools/settings/settings/widgets/anatomy_types.py b/pype/tools/settings/settings/widgets/anatomy_types.py deleted file mode 100644 index 556718a161..0000000000 --- a/pype/tools/settings/settings/widgets/anatomy_types.py +++ /dev/null @@ -1,574 +0,0 @@ -from Qt import QtWidgets, QtCore -from .widgets import ExpandingWidget -from .item_types import ( - SettingObject, - ModifiableDict, - PathWidget, - RawJsonWidget, - DictWidget -) -from .lib import NOT_SET, TypeToKlass, CHILD_OFFSET, METADATA_KEY - - -class AnatomyWidget(QtWidgets.QWidget, SettingObject): - value_changed = QtCore.Signal(object) - template_keys = ( - "project[name]", - "project[code]", - "asset", - "task", - "subset", - "family", - "version", - "ext", - "representation" - ) - default_exmaple_data = { - "project": { - "name": "ProjectPype", - "code": "pp", - }, - "asset": "sq01sh0010", - "task": "compositing", - "subset": "renderMain", - "family": "render", - "version": 1, - "ext": ".png", - "representation": "png" - } - - def __init__(self, schema_data, parent, as_widget=False): - if as_widget: - raise TypeError( - "`AnatomyWidget` does not allow to be used as widget." - ) - super(AnatomyWidget, self).__init__(parent) - self.setObjectName("AnatomyWidget") - - self.initial_attributes(schema_data, parent, as_widget) - - self.input_fields = [] - - self.key = schema_data["key"] - - def create_ui(self, label_widget=None): - children_data = self.schema_data["children"] - for schema_data in children_data: - item = TypeToKlass.types[schema_data["type"]](schema_data, self) - item.create_ui() - self.input_fields.append(item) - - self.setAttribute(QtCore.Qt.WA_StyledBackground) - - body_widget = ExpandingWidget("Anatomy", self) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - layout.addWidget(body_widget) - - content_widget = QtWidgets.QWidget(body_widget) - content_layout = QtWidgets.QVBoxLayout(content_widget) - content_layout.setContentsMargins(CHILD_OFFSET, 5, 0, 0) - content_layout.setSpacing(5) - - for input_field in self.input_fields: - content_layout.addWidget(input_field) - input_field.value_changed.connect(self._on_value_change) - - body_widget.set_content_widget(content_widget) - - self.body_widget = body_widget - self.label_widget = body_widget.label_widget - - def update_default_values(self, parent_values): - self._state = None - self._child_state = None - - if isinstance(parent_values, dict): - value = parent_values.get(self.key, NOT_SET) - else: - value = NOT_SET - - for input_field in self.input_fields: - input_field.update_default_values(value) - - def update_studio_values(self, parent_values): - self._state = None - self._child_state = None - - if isinstance(parent_values, dict): - value = parent_values.get(self.key, NOT_SET) - else: - value = NOT_SET - - for input_field in self.input_fields: - input_field.update_studio_values(value) - - def apply_overrides(self, parent_values): - # Make sure this is set to False - self._state = None - self._child_state = None - - value = NOT_SET - if parent_values is not NOT_SET: - value = parent_values.get(self.key, value) - - for input_field in self.input_fields: - input_field.apply_overrides(value) - - def set_value(self, value): - raise TypeError("AnatomyWidget does not allow to use `set_value`") - - def _on_value_change(self, item=None): - if self.ignore_value_changes: - return - - self.hierarchical_style_update() - - self.value_changed.emit(self) - - def update_style(self, is_overriden=None): - child_has_studio_override = self.child_has_studio_override - child_modified = self.child_modified - child_invalid = self.child_invalid - child_state = self.style_state( - child_has_studio_override, - child_invalid, - self.child_overriden, - child_modified - ) - if child_state: - child_state = "child-{}".format(child_state) - - if child_state != self._child_state: - self.body_widget.side_line_widget.setProperty("state", child_state) - self.body_widget.side_line_widget.style().polish( - self.body_widget.side_line_widget - ) - self._child_state = child_state - - def hierarchical_style_update(self): - for input_field in self.input_fields: - input_field.hierarchical_style_update() - - self.update_style() - - @property - def child_has_studio_override(self): - for input_field in self.input_fields: - if input_field.child_has_studio_override: - return True - return False - - @property - def child_modified(self): - for input_field in self.input_fields: - if input_field.child_modified: - return True - return False - - @property - def child_overriden(self): - for input_field in self.input_fields: - if input_field.child_overriden: - return True - return False - - @property - def child_invalid(self): - for input_field in self.input_fields: - if input_field.child_invalid: - return True - return False - - def set_as_overriden(self): - for input_field in self.input_fields: - input_field.child_invalid.set_as_overriden() - - def remove_overrides(self): - for input_field in self.input_fields: - input_field.remove_overrides() - - def reset_to_pype_default(self): - for input_field in self.input_fields: - input_field.reset_to_pype_default() - - def set_studio_default(self): - for input_field in self.input_fields: - input_field.set_studio_default() - - def discard_changes(self): - for input_field in self.input_fields: - input_field.discard_changes() - - def overrides(self): - if self.child_overriden: - return self.config_value(), True - return NOT_SET, False - - def item_value(self): - output = {} - for input_field in self.input_fields: - output.update(input_field.config_value()) - return output - - def studio_overrides(self): - has_overrides = False - for input_field in self.input_fields: - if input_field.child_has_studio_override: - has_overrides = True - break - - if not has_overrides: - return NOT_SET, False - - groups = [] - for input_field in self.input_fields: - groups.append(input_field.key) - - value = self.config_value() - if METADATA_KEY not in value[self.key]: - value[self.key][METADATA_KEY] = {} - value[self.key][METADATA_KEY]["groups"] = groups - - return value, True - - def config_value(self): - return {self.key: self.item_value()} - - -class RootsWidget(QtWidgets.QWidget, SettingObject): - value_changed = QtCore.Signal(object) - - def __init__(self, input_data, parent): - super(RootsWidget, self).__init__(parent) - self.setObjectName("RootsWidget") - - input_data["is_group"] = True - self.initial_attributes(input_data, parent, False) - - self.key = input_data["key"] - - self.was_multiroot = NOT_SET - - def create_ui(self, _label_widget=None): - body_widget = ExpandingWidget("Roots", self) - content_widget = QtWidgets.QWidget(body_widget) - - multiroot_data = { - "key": self.key, - "expandable": False, - "object_type": { - "type": "path-widget", - "multiplatform": True - } - } - roots_widget = ModifiableDict( - multiroot_data, self, - as_widget=True, parent_widget=content_widget - ) - roots_widget.create_ui() - - content_layout = QtWidgets.QVBoxLayout(content_widget) - content_layout.setContentsMargins(0, 0, 0, 0) - content_layout.addWidget(roots_widget) - - body_widget.set_content_widget(content_widget) - self.label_widget = body_widget.label_widget - - main_layout = QtWidgets.QVBoxLayout(self) - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.addWidget(body_widget) - - self.body_widget = body_widget - self.roots_widget = roots_widget - roots_widget.value_changed.connect(self._on_value_change) - - def update_default_values(self, parent_values): - self._state = None - self._is_modified = False - - if isinstance(parent_values, dict): - value = parent_values.get(self.key, NOT_SET) - else: - value = NOT_SET - - self._has_studio_override = False - self._had_studio_override = False - - is_multiroot = True - # Backward compatibility: Allow to switch to multiroot - if isinstance(value, dict): - is_multiroot = False - for _value in value.values(): - if isinstance(_value, dict): - is_multiroot = True - break - - if not is_multiroot: - value = {"": value} - - self.roots_widget.update_default_values(value) - - def update_studio_values(self, parent_values): - self._state = None - self._is_modified = False - - if isinstance(parent_values, dict): - value = parent_values.get(self.key, NOT_SET) - else: - value = NOT_SET - - self._has_studio_override = value is not NOT_SET - self._had_studio_override = value is not NOT_SET - - self.roots_widget.update_studio_values(value) - - def apply_overrides(self, parent_values): - # Make sure this is set to False - self._state = None - self._is_modified = False - - value = NOT_SET - if parent_values is not NOT_SET: - value = parent_values.get(self.key, value) - - self._is_overriden = value is not NOT_SET - self._was_overriden = bool(self._is_overriden) - self.roots_widget.apply_overrides(value) - - def hierarchical_style_update(self): - self.roots_widget.hierarchical_style_update() - self.update_style() - - def update_style(self): - state = self.style_state( - self.has_studio_override, - self.child_invalid, - self.is_overriden, - self.is_modified - ) - if self._state == state: - return - - if state: - child_state = "child-{}".format(state) - else: - child_state = "" - - self.body_widget.side_line_widget.setProperty("state", child_state) - self.body_widget.side_line_widget.style().polish( - self.body_widget.side_line_widget - ) - - self.label_widget.setProperty("state", state) - self.label_widget.style().polish(self.label_widget) - - self._state = state - - def _on_value_change(self, item=None): - if self.ignore_value_changes: - return - - if self.is_group and self.is_overidable: - self._is_overriden = True - - self._is_modified = bool(self.child_modified) - - self.update_style() - - self.value_changed.emit(self) - - @property - def child_has_studio_override(self): - return self.roots_widget.has_studio_override - - @property - def child_modified(self): - return self.roots_widget.child_modified - - @property - def child_overriden(self): - return ( - self.roots_widget.is_overriden - or self.roots_widget.child_overriden - ) - - @property - def child_invalid(self): - return self.roots_widget.child_invalid - - def remove_overrides(self): - self._is_overriden = False - self._is_modified = False - - self.roots_widget.remove_overrides() - - def reset_to_pype_default(self): - self.roots_widget.reset_to_pype_default() - self._has_studio_override = False - - def set_studio_default(self): - self.roots_widget.reset_to_pype_default() - self._has_studio_override = True - - def discard_changes(self): - self._is_overriden = self._was_overriden - self._is_modified = False - - self.roots_widget.discard_changes() - - self._is_modified = self.child_modified - self._has_studio_override = self._had_studio_override - - def set_as_overriden(self): - self._is_overriden = True - self.roots_widget.set_as_overriden() - - def item_value(self): - return self.roots_widget.item_value() - - def config_value(self): - return {self.key: self.item_value()} - - -class TemplatesWidget(QtWidgets.QWidget, SettingObject): - value_changed = QtCore.Signal(object) - - def __init__(self, input_data, parent): - super(TemplatesWidget, self).__init__(parent) - - input_data["is_group"] = True - self.initial_attributes(input_data, parent, False) - - self.key = input_data["key"] - - def create_ui(self, label_widget=None): - body_widget = ExpandingWidget("Templates", self) - content_widget = QtWidgets.QWidget(body_widget) - body_widget.set_content_widget(content_widget) - content_layout = QtWidgets.QVBoxLayout(content_widget) - - template_input_data = { - "key": self.key - } - self.body_widget = body_widget - self.label_widget = body_widget.label_widget - self.value_input = RawJsonWidget(template_input_data, self) - self.value_input.create_ui(label_widget=self.label_widget) - - content_layout.addWidget(self.value_input) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - - layout.addWidget(body_widget) - - self.value_input.value_changed.connect(self._on_value_change) - - def _on_value_change(self, item): - self.update_style() - - self.value_changed.emit(self) - - def update_default_values(self, values): - self._state = None - self.value_input.update_default_values(values) - - def update_studio_values(self, values): - self._state = None - self.value_input.update_studio_values(values) - - def apply_overrides(self, parent_values): - self._state = None - self.value_input.apply_overrides(parent_values) - - def hierarchical_style_update(self): - self.value_input.hierarchical_style_update() - self.update_style() - - def update_style(self): - state = self.style_state( - self.has_studio_override, - self.child_invalid, - self.child_overriden, - self.child_modified - ) - if self._state == state: - return - - if state: - child_state = "child-{}".format(state) - else: - child_state = "" - - self.body_widget.side_line_widget.setProperty("state", child_state) - self.body_widget.side_line_widget.style().polish( - self.body_widget.side_line_widget - ) - - self.label_widget.setProperty("state", state) - self.label_widget.style().polish(self.label_widget) - - self._state = state - - @property - def is_modified(self): - return self.value_input.is_modified - - @property - def is_overriden(self): - return self._is_overriden - - @property - def has_studio_override(self): - return self.value_input._has_studio_override - - @property - def child_has_studio_override(self): - return self.value_input.child_has_studio_override - - @property - def child_modified(self): - return self.value_input.child_modified - - @property - def child_overriden(self): - return self.value_input.child_overriden - - @property - def child_invalid(self): - return self.value_input.child_invalid - - def remove_overrides(self): - self.value_input.remove_overrides() - - def reset_to_pype_default(self): - self.value_input.reset_to_pype_default() - - def set_studio_default(self): - self.value_input.set_studio_default() - - def discard_changes(self): - self.value_input.discard_changes() - - def set_as_overriden(self): - self.value_input.set_as_overriden() - - def overrides(self): - if not self.child_overriden: - return NOT_SET, False - return self.config_value(), True - - def item_value(self): - return self.value_input.item_value() - - def config_value(self): - return self.value_input.config_value() - - -TypeToKlass.types["anatomy"] = AnatomyWidget -TypeToKlass.types["anatomy_roots"] = RootsWidget -TypeToKlass.types["anatomy_templates"] = TemplatesWidget diff --git a/pype/tools/settings/settings/widgets/base.py b/pype/tools/settings/settings/widgets/base.py index 75897196b0..692e9a9859 100644 --- a/pype/tools/settings/settings/widgets/base.py +++ b/pype/tools/settings/settings/widgets/base.py @@ -4,15 +4,12 @@ from Qt import QtWidgets, QtGui, QtCore class BaseWidget(QtWidgets.QWidget): allow_actions = True - def __init__(self, entity, entity_widget): + def __init__(self, category_widget, entity, entity_widget): + self.category_widget = category_widget self.entity = entity self.entity_widget = entity_widget - self.trigger_hierarchical_style_update = ( - self.entity_widget.trigger_hierarchical_style_update - ) self.ignore_input_changes = entity_widget.ignore_input_changes - self.create_ui_for_entity = entity_widget.create_ui_for_entity self._is_invalid = False self._style_state = None @@ -24,6 +21,12 @@ class BaseWidget(QtWidgets.QWidget): self.label_widget = None self.create_ui() + def trigger_hierarchical_style_update(self): + self.category_widget.hierarchical_style_update() + + def create_ui_for_entity(self, *args, **kwargs): + return self.category_widget.create_ui_for_entity(*args, **kwargs) + @property def is_invalid(self): return self._is_invalid @@ -59,14 +62,14 @@ class BaseWidget(QtWidgets.QWidget): def _on_entity_change(self): """Not yet used.""" - print("{}: Wraning missing `_on_entity_change` implementation".format( + print("{}: Warning missing `_on_entity_change` implementation".format( self.__class__.__name__ )) def _discard_changes_action(self, menu, actions_mapping): # TODO use better condition as unsaved changes may be caused due to # changes in schema. - if not self.entity.has_unsaved_changes: + if not self.entity.can_discard_changes: return def discard_changes(): @@ -78,74 +81,46 @@ class BaseWidget(QtWidgets.QWidget): actions_mapping[action] = discard_changes menu.addAction(action) - def _set_project_override_action(self, menu, actions_mapping): - # Show only when project overrides are set - if not self.entity.root_item.is_in_project_state(): - return - - # Do not show on items under group item - if self.entity.group_item: - return - - # Skip if already is marked to save project overrides - if self.entity.is_group and self.entity.has_studio_override: - return - - action = QtWidgets.QAction("Add to project project override") - actions_mapping[action] = self.entity.add_to_project_override - menu.addAction(action) - - def _remove_from_studio_default_action(self, menu, actions_mapping): - if not self.entity.root_item.is_in_studio_state(): - return - - if self.entity.has_studio_override: - def remove_from_studio_default(): - self.ignore_input_changes.set_ignore(True) - self.entity.remove_from_studio_default() - self.ignore_input_changes.set_ignore(False) - action = QtWidgets.QAction("Remove from studio default") - actions_mapping[action] = remove_from_studio_default - menu.addAction(action) - def _add_to_studio_default(self, menu, actions_mapping): """Set values as studio overrides.""" # Skip if not in studio overrides - if not self.entity.root_item.is_in_studio_state(): - return - - # Skip if entity is under group - if self.entity.group_item: - return - - # Skip if is group and any children is already marked with studio - # overrides - if self.entity.is_group and self.entity.has_studio_override: + if not self.entity.can_add_to_studio_default: return action = QtWidgets.QAction("Add to studio default") actions_mapping[action] = self.entity.add_to_studio_default menu.addAction(action) - def _remove_project_override_action(self, menu, actions_mapping): - # Dynamic items can't have these actions - if self.entity.is_dynamic_item or self.entity.is_in_dynamic_item: + def _remove_from_studio_default_action(self, menu, actions_mapping): + if not self.entity.can_remove_from_studio_default: return - if self.entity.is_group: - if not self.entity.has_project_override: - return + def remove_from_studio_default(): + self.ignore_input_changes.set_ignore(True) + self.entity.remove_from_studio_default() + self.ignore_input_changes.set_ignore(False) + action = QtWidgets.QAction("Remove from studio default") + actions_mapping[action] = remove_from_studio_default + menu.addAction(action) - elif self.entity.group_item: - if not self.entity.group_item.has_project_override: - return - - elif not self.entity.has_project_override: + def _add_to_project_override_action(self, menu, actions_mapping): + if not self.entity.can_add_to_project_override: return - # TODO better label + action = QtWidgets.QAction("Add to project project override") + actions_mapping[action] = self.entity.add_to_project_override + menu.addAction(action) + + def _remove_from_project_override_action(self, menu, actions_mapping): + if not self.entity.can_remove_from_project_override: + return + + def remove_from_project_override(): + self.ignore_input_changes.set_ignore(True) + self.entity.remove_from_project_override() + self.ignore_input_changes.set_ignore(False) action = QtWidgets.QAction("Remove from project override") - actions_mapping[action] = self.entity.remove_from_project_override + actions_mapping[action] = remove_from_project_override menu.addAction(action) def show_actions_menu(self, event=None): @@ -164,8 +139,8 @@ class BaseWidget(QtWidgets.QWidget): self._discard_changes_action(menu, actions_mapping) self._add_to_studio_default(menu, actions_mapping) self._remove_from_studio_default_action(menu, actions_mapping) - self._set_project_override_action(menu, actions_mapping) - self._remove_project_override_action(menu, actions_mapping) + self._add_to_project_override_action(menu, actions_mapping) + self._remove_from_project_override_action(menu, actions_mapping) if not actions_mapping: action = QtWidgets.QAction("< No action >") @@ -265,3 +240,28 @@ class GUIWidget(BaseWidget): def get_invalid(self): return [] + + +class MockUpWidget(BaseWidget): + allow_actions = False + child_invalid = False + + def create_ui(self): + self.setObjectName("LabelWidget") + + label = "Mockup widget for entity {}".format(self.entity.path) + label_widget = QtWidgets.QLabel(label, self) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 5, 0, 5) + layout.addWidget(label_widget) + self.entity_widget.add_widget_to_layout(self) + + def set_entity_value(self): + return + + def hierarchical_style_update(self): + pass + + def get_invalid(self): + return [] diff --git a/pype/tools/settings/settings/widgets/categories.py b/pype/tools/settings/settings/widgets/categories.py index b26438ee4f..40bd687faf 100644 --- a/pype/tools/settings/settings/widgets/categories.py +++ b/pype/tools/settings/settings/widgets/categories.py @@ -1,13 +1,10 @@ import os -import json from enum import Enum from Qt import QtWidgets, QtCore, QtGui -from pype.settings.constants import ( - PROJECT_SETTINGS_KEY, - PROJECT_ANATOMY_KEY -) + from pype.settings.entities import ( SystemSettings, + ProjectSettings, GUIEntity, DictImmutableKeysEntity, @@ -23,31 +20,18 @@ from pype.settings.entities import ( PathInput, RawJsonEntity, - DefaultsNotDefined + DefaultsNotDefined, + StudioDefaultsNotDefined ) -from pype.settings.lib import ( - DEFAULTS_DIR, +from pype.settings.lib import get_system_settings +from .widgets import ProjectListWidget - reset_default_settings, - get_default_settings, - - get_studio_project_settings_overrides, - get_studio_project_anatomy_overrides, - - get_project_settings_overrides, - get_project_anatomy_overrides, - - save_project_settings, - save_project_anatomy, - - get_system_settings -) -from .widgets import UnsavedChangesDialog from . import lib from .base import GUIWidget from .list_item_widget import ListWidget +from .list_strict_widget import ListStrictWidget from .dict_mutable_widget import DictMutableKeysWidget from .item_widgets import ( BoolWidget, @@ -59,10 +43,7 @@ from .item_widgets import ( PathWidget, PathInputWidget ) -from avalon.mongodb import ( - AvalonMongoConnection, - AvalonMongoDB -) + from avalon.vendor import qtawesome @@ -71,10 +52,23 @@ class CategoryState(Enum): Working = object() -class SettingsCategoryWidget(QtWidgets.QWidget): - schema_category = None - initial_schema_name = None +class IgnoreInputChangesObj: + def __init__(self, top_widget): + self._ignore_changes = False + self.top_widget = top_widget + def __bool__(self): + return self._ignore_changes + + def set_ignore(self, ignore_changes=True): + if self._ignore_changes == ignore_changes: + return + self._ignore_changes = ignore_changes + if not ignore_changes: + self.top_widget.hierarchical_style_update() + + +class SettingsCategoryWidget(QtWidgets.QWidget): state_changed = QtCore.Signal() saved = QtCore.Signal(QtWidgets.QWidget) @@ -83,48 +77,58 @@ class SettingsCategoryWidget(QtWidgets.QWidget): self.user_role = user_role + self.entity = None + self._state = CategoryState.Idle + self._hide_studio_overrides = False + self.ignore_input_changes = IgnoreInputChangesObj(self) + + self.keys = [] + self.input_fields = [] + self.initialize_attributes() + self.create_ui() @staticmethod - def create_ui_for_entity(entity, entity_widget): + def create_ui_for_entity(category_widget, entity, entity_widget): + args = (category_widget, entity, entity_widget) if isinstance(entity, GUIEntity): - return GUIWidget(entity, entity_widget) + return GUIWidget(*args) elif isinstance(entity, DictImmutableKeysEntity): - return DictImmutableKeysWidget(entity, entity_widget) + return DictImmutableKeysWidget(*args) elif isinstance(entity, BoolEntity): - return BoolWidget(entity, entity_widget) + return BoolWidget(*args) elif isinstance(entity, TextEntity): - return TextWidget(entity, entity_widget) + return TextWidget(*args) elif isinstance(entity, NumberEntity): - return NumberWidget(entity, entity_widget) + return NumberWidget(*args) elif isinstance(entity, RawJsonEntity): - return RawJsonWidget(entity, entity_widget) + return RawJsonWidget(*args) elif isinstance(entity, EnumEntity): - return EnumeratorWidget(entity, entity_widget) + return EnumeratorWidget(*args) elif isinstance(entity, PathEntity): - return PathWidget(entity, entity_widget) + return PathWidget(*args) elif isinstance(entity, PathInput): - return PathInputWidget(entity, entity_widget) + return PathInputWidget(*args) elif isinstance(entity, ListEntity): - return ListWidget(entity, entity_widget) + return ListWidget(*args) elif isinstance(entity, DictMutableKeysEntity): - return DictMutableKeysWidget(entity, entity_widget) + return DictMutableKeysWidget(*args) elif isinstance(entity, ListStrictEntity): - pass + return ListStrictWidget(*args) label = "<{}>: {} ({})".format( entity.__class__.__name__, entity.path, entity.value @@ -152,357 +156,8 @@ class SettingsCategoryWidget(QtWidgets.QWidget): app.processEvents() def initialize_attributes(self): - self._hide_studio_overrides = False - self._ignore_value_changes = False - - self.keys = [] - self.input_fields = [] - self.schema = None - self.main_schema_key = None - - # Required attributes for items - self.is_overidable = False - self._has_studio_override = False - self._is_overriden = False - self._as_widget = False - self._is_group = False - self._any_parent_as_widget = False - self._any_parent_is_group = False - self.has_studio_override = self._has_studio_override - self.is_overriden = self._is_overriden - self.as_widget = self._as_widget - self.is_group = self._as_widget - self.any_parent_as_widget = self._any_parent_as_widget - self.any_parent_is_group = self._any_parent_is_group - - def create_ui(self): - scroll_widget = QtWidgets.QScrollArea(self) - scroll_widget.setObjectName("GroupWidget") - content_widget = QtWidgets.QWidget(scroll_widget) - content_layout = QtWidgets.QVBoxLayout(content_widget) - content_layout.setContentsMargins(3, 3, 3, 3) - content_layout.setSpacing(0) - content_layout.setAlignment(QtCore.Qt.AlignTop) - - scroll_widget.setWidgetResizable(True) - scroll_widget.setWidget(content_widget) - - configurations_widget = QtWidgets.QWidget(self) - - footer_widget = QtWidgets.QWidget(configurations_widget) - footer_layout = QtWidgets.QHBoxLayout(footer_widget) - - if self.user_role == "developer": - self._add_developer_ui(footer_layout) - - save_btn = QtWidgets.QPushButton("Save") - spacer_widget = QtWidgets.QWidget() - footer_layout.addWidget(spacer_widget, 1) - footer_layout.addWidget(save_btn, 0) - - configurations_layout = QtWidgets.QVBoxLayout(configurations_widget) - configurations_layout.setContentsMargins(0, 0, 0, 0) - configurations_layout.setSpacing(0) - - configurations_layout.addWidget(scroll_widget, 1) - configurations_layout.addWidget(footer_widget, 0) - - main_layout = QtWidgets.QHBoxLayout(self) - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.setSpacing(0) - main_layout.addWidget(configurations_widget, 1) - - save_btn.clicked.connect(self._save) - - self.scroll_widget = scroll_widget - self.content_layout = content_layout - self.content_widget = content_widget - self.configurations_widget = configurations_widget - self.main_layout = main_layout - - self.ui_tweaks() - - def ui_tweaks(self): return - def _add_developer_ui(self, footer_layout): - save_as_default_btn = QtWidgets.QPushButton("Save as Default") - - refresh_icon = qtawesome.icon("fa.refresh", color="white") - refresh_button = QtWidgets.QPushButton() - refresh_button.setIcon(refresh_icon) - - hide_studio_overrides = QtWidgets.QCheckBox() - hide_studio_overrides.setChecked(self._hide_studio_overrides) - - hide_studio_overrides_widget = QtWidgets.QWidget() - hide_studio_overrides_layout = QtWidgets.QHBoxLayout( - hide_studio_overrides_widget - ) - _label_widget = QtWidgets.QLabel( - "Hide studio overrides", hide_studio_overrides_widget - ) - hide_studio_overrides_layout.addWidget(_label_widget) - hide_studio_overrides_layout.addWidget(hide_studio_overrides) - - footer_layout.addWidget(save_as_default_btn, 0) - footer_layout.addWidget(refresh_button, 0) - footer_layout.addWidget(hide_studio_overrides_widget, 0) - - save_as_default_btn.clicked.connect(self._save_as_defaults) - refresh_button.clicked.connect(self._on_refresh) - hide_studio_overrides.stateChanged.connect( - self._on_hide_studio_overrides - ) - - def save(self): - """Save procedure.""" - raise NotImplementedError("Method `save` is not implemented.") - - def defaults_dir(self): - """Path to defaults folder.""" - raise NotImplementedError("Method `defaults_dir` is not implemented.") - - def update_values(self): - """Procedure of update values of items on context change or reset.""" - raise NotImplementedError("Method `update_values` is not implemented.") - - def validate_defaults_to_save(self, value): - raise NotImplementedError( - "Method `validate_defaults_to_save` not implemented." - ) - - def any_parent_overriden(self): - return False - - @property - def ignore_value_changes(self): - return self._ignore_value_changes - - @ignore_value_changes.setter - def ignore_value_changes(self, value): - self._ignore_value_changes = value - if value is False: - self.hierarchical_style_update() - - def hierarchical_style_update(self): - for input_field in self.input_fields: - input_field.hierarchical_style_update() - - def reset(self): - self.set_state(CategoryState.Working) - - reset_default_settings() - - self.keys.clear() - self.input_fields.clear() - while self.content_layout.count() != 0: - widget = self.content_layout.itemAt(0).widget() - widget.setVisible(False) - self.content_layout.removeWidget(widget) - widget.deleteLater() - - self.schema = lib.gui_schema( - self.schema_category, self.initial_schema_name - ) - - self.main_schema_key = self.schema["key"] - - self.add_children_gui(self.schema) - self._update_values() - - self.hierarchical_style_update() - - self.set_state(CategoryState.Idle) - - def items_are_valid(self): - has_invalid = False - for item in self.input_fields: - if item.child_invalid: - has_invalid = True - - if not has_invalid: - return True - - invalid_items = [] - for item in self.input_fields: - invalid_items.extend(item.get_invalid()) - msg_box = QtWidgets.QMessageBox( - QtWidgets.QMessageBox.Warning, - "Invalid input", - "There is invalid value in one of inputs." - " Please lead red color and fix them." - ) - msg_box.setStandardButtons(QtWidgets.QMessageBox.Ok) - msg_box.exec_() - - first_invalid_item = invalid_items[0] - self.scroll_widget.ensureWidgetVisible(first_invalid_item) - if first_invalid_item.isVisible(): - first_invalid_item.setFocus(True) - return False - - def on_saved(self, saved_tab_widget): - """Callback on any tab widget save.""" - return - - def _save(self): - self.set_state(CategoryState.Working) - - if self.items_are_valid(): - self.save() - - self._update_values() - - self.set_state(CategoryState.Idle) - - self.saved.emit(self) - - def _on_refresh(self): - self.reset() - - def _on_hide_studio_overrides(self, state): - self._hide_studio_overrides = (state == QtCore.Qt.Checked) - self._update_values() - - def _save_as_defaults(self): - if not self.items_are_valid(): - return - - all_values = {} - for item in self.input_fields: - all_values.update(item.config_value()) - - for key in reversed(self.keys): - all_values = {key: all_values} - - # Skip first key and convert data to store - all_values = lib.convert_gui_data_with_metadata( - all_values[self.main_schema_key] - ) - - if not self.validate_defaults_to_save(all_values): - return - - defaults_dir = self.defaults_dir() - keys_to_file = lib.file_keys_from_schema(self.schema) - for key_sequence in keys_to_file: - # Skip first key - key_sequence = key_sequence[1:] - subpath = "/".join(key_sequence) + ".json" - - new_values = all_values - for key in key_sequence: - new_values = new_values[key] - - output_path = os.path.join(defaults_dir, subpath) - dirpath = os.path.dirname(output_path) - if not os.path.exists(dirpath): - os.makedirs(dirpath) - - print("Saving data to: ", subpath) - with open(output_path, "w") as file_stream: - json.dump(new_values, file_stream, indent=4) - - reset_default_settings() - - self._update_values() - - def _update_values(self): - self.ignore_value_changes = True - self.update_values() - self.ignore_value_changes = False - - def add_children_gui(self, child_configuration): - klass = lib.TypeToKlass.types.get(child_configuration["type"]) - item = klass(child_configuration, self) - item.create_ui() - self.input_fields.append(item) - self.content_layout.addWidget(item, 0) - - # Add spacer to stretch children guis - self.content_layout.addWidget( - QtWidgets.QWidget(self.content_widget), 1 - ) - - -class IgnoreInputChangesObj: - def __init__(self, top_widget): - self._ignore_changes = False - self.top_widget = top_widget - - def __bool__(self): - return self._ignore_changes - - def set_ignore(self, ignore_changes=True): - if self._ignore_changes == ignore_changes: - return - self._ignore_changes = ignore_changes - if not ignore_changes: - self.top_widget.hierarchical_style_update() - - -class SystemWidget(SettingsCategoryWidget): - schema_category = "system_schema" - initial_schema_name = "schema_main" - - def initialize_attributes(self, *args, **kwargs): - self._hide_studio_overrides = False - self.ignore_input_changes = IgnoreInputChangesObj(self) - - self.keys = [] - self.input_fields = [] - - def defaults_dir(self): - print("*** defaults_dir") - - def validate_defaults_to_save(self, values): - print("*** validate_defaults_to_save") - - def trigger_hierarchical_style_update(self): - self.hierarchical_style_update() - - def items_are_valid(self): - invalid_items = self.get_invalid() - if not invalid_items: - return True - - msg_box = QtWidgets.QMessageBox( - QtWidgets.QMessageBox.Warning, - "Invalid input", - "There is invalid value in one of inputs." - " Please lead red color and fix them.", - parent=self - ) - msg_box.setStandardButtons(QtWidgets.QMessageBox.Ok) - msg_box.exec_() - - first_invalid_item = invalid_items[0] - self.scroll_widget.ensureWidgetVisible(first_invalid_item) - if first_invalid_item.isVisible(): - first_invalid_item.setFocus(True) - return False - - def save(self): - if self.items_are_valid(): - self.entity.save() - # NOTE There are relations to previous entities and C++ callbacks - # so it is easier to just use new entity and recreate UI but - # would be nice to change this and add cleanup part so this is - # not required. - self.reset() - - def get_invalid(self): - invalid = [] - for input_field in self.input_fields: - invalid.extend(input_field.get_invalid()) - return invalid - - def update_values(self): - # TODO remove as it breaks entities. Was used in previous - # implementation of category widget. - pass - def create_ui(self): self.modify_defaults_checkbox = None @@ -552,6 +207,9 @@ class SystemWidget(SettingsCategoryWidget): self.ui_tweaks() + def ui_tweaks(self): + return + def _add_developer_ui(self, footer_layout): refresh_icon = qtawesome.icon("fa.refresh", color="white") refresh_button = QtWidgets.QPushButton() @@ -577,13 +235,39 @@ class SystemWidget(SettingsCategoryWidget): ) self.modify_defaults_checkbox = modify_defaults_checkbox - def _on_modify_defaults(self): - if self.modify_defaults_checkbox.isChecked(): - if not self.entity.is_in_defaults_state(): - self.reset() - else: - if not self.entity.is_in_studio_state(): - self.reset() + def get_invalid(self): + invalid = [] + for input_field in self.input_fields: + invalid.extend(input_field.get_invalid()) + return invalid + + def hierarchical_style_update(self): + for input_field in self.input_fields: + input_field.hierarchical_style_update() + + def _on_entity_change(self): + self.hierarchical_style_update() + + def add_widget_to_layout(self, widget, label_widget=None): + if label_widget: + raise NotImplementedError( + "`add_widget_to_layout` on Category item can't accept labels" + ) + self.content_layout.addWidget(widget, 0) + + def save(self): + if self.items_are_valid(): + self.entity.save() + # NOTE There are relations to previous entities and C++ callbacks + # so it is easier to just use new entity and recreate UI but + # would be nice to change this and add cleanup part so this is + # not required. + self.reset() + + def _create_root_entity(self): + raise NotImplementedError( + "`create_root_entity` method not implemented" + ) def reset(self): self.set_state(CategoryState.Working) @@ -596,6 +280,73 @@ class SystemWidget(SettingsCategoryWidget): self.content_layout.removeWidget(widget) widget.deleteLater() + self._create_root_entity() + + self.add_children_gui() + + self.ignore_input_changes.set_ignore(True) + + for input_field in self.input_fields: + input_field.set_entity_value() + + self.ignore_input_changes.set_ignore(False) + + self.set_state(CategoryState.Idle) + + def add_children_gui(self): + for child_obj in self.entity.children: + item = self.create_ui_for_entity(self, child_obj, self) + self.input_fields.append(item) + + # Add spacer to stretch children guis + self.content_layout.addWidget( + QtWidgets.QWidget(self.content_widget), 1 + ) + + def items_are_valid(self): + invalid_items = self.get_invalid() + if not invalid_items: + return True + + msg_box = QtWidgets.QMessageBox( + QtWidgets.QMessageBox.Warning, + "Invalid input", + "There is invalid value in one of inputs." + " Please lead red color and fix them.", + parent=self + ) + msg_box.setStandardButtons(QtWidgets.QMessageBox.Ok) + msg_box.exec_() + + first_invalid_item = invalid_items[0] + self.scroll_widget.ensureWidgetVisible(first_invalid_item) + if first_invalid_item.isVisible(): + first_invalid_item.setFocus(True) + return False + + def on_saved(self, saved_tab_widget): + """Callback on any tab widget save.""" + return + + def _save(self): + self.set_state(CategoryState.Working) + + if self.items_are_valid(): + self.save() + + self.set_state(CategoryState.Idle) + + self.saved.emit(self) + + def _on_refresh(self): + self.reset() + + def _on_hide_studio_overrides(self, state): + self._hide_studio_overrides = (state == QtCore.Qt.Checked) + + +class SystemWidget(SettingsCategoryWidget): + def _create_root_entity(self): self.entity = SystemSettings(set_studio_state=False) self.entity.on_change_callbacks.append(self._on_entity_change) try: @@ -622,198 +373,19 @@ class SystemWidget(SettingsCategoryWidget): self.modify_defaults_checkbox.setChecked(True) self.modify_defaults_checkbox.setEnabled(False) - self.add_children_gui() - - self.ignore_input_changes.set_ignore(True) - - for input_field in self.input_fields: - input_field.set_entity_value() - - self.ignore_input_changes.set_ignore(False) - - self.set_state(CategoryState.Idle) - - def _on_entity_change(self): - self.hierarchical_style_update() - - def hierarchical_style_update(self): - for input_field in self.input_fields: - input_field.hierarchical_style_update() - - def add_widget_to_layout(self, widget, label_widget=None): - if label_widget: - raise NotImplementedError( - "`add_widget_to_layout` is not implemented on Category item" - ) - self.content_layout.addWidget(widget, 0) - - def add_children_gui(self): - for child_obj in self.entity.children: - item = self.create_ui_for_entity(child_obj, self) - self.input_fields.append(item) - - # Add spacer to stretch children guis - self.content_layout.addWidget( - QtWidgets.QWidget(self.content_widget), 1 - ) - - -class ProjectListView(QtWidgets.QListView): - left_mouse_released_at = QtCore.Signal(QtCore.QModelIndex) - - def mouseReleaseEvent(self, event): - if event.button() == QtCore.Qt.LeftButton: - index = self.indexAt(event.pos()) - self.left_mouse_released_at.emit(index) - super(ProjectListView, self).mouseReleaseEvent(event) - - -class ProjectListWidget(QtWidgets.QWidget): - default = "< Default >" - project_changed = QtCore.Signal() - - def __init__(self, parent): - self._parent = parent - - self.current_project = None - - super(ProjectListWidget, self).__init__(parent) - self.setObjectName("ProjectListWidget") - - label_widget = QtWidgets.QLabel("Projects") - project_list = ProjectListView(self) - project_list.setModel(QtGui.QStandardItemModel()) - - # Do not allow editing - project_list.setEditTriggers( - QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers - ) - # Do not automatically handle selection - project_list.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) - - layout = QtWidgets.QVBoxLayout(self) - layout.setSpacing(3) - layout.addWidget(label_widget, 0) - layout.addWidget(project_list, 1) - - project_list.left_mouse_released_at.connect(self.on_item_clicked) - - self.project_list = project_list - - self.dbcon = None - - def on_item_clicked(self, new_index): - new_project_name = new_index.data(QtCore.Qt.DisplayRole) - if new_project_name is None: - return - - if self.current_project == new_project_name: - return - - save_changes = False - change_project = False - if self.validate_context_change(): - change_project = True - + def _on_modify_defaults(self): + if self.modify_defaults_checkbox.isChecked(): + if not self.entity.is_in_defaults_state(): + self.reset() else: - dialog = UnsavedChangesDialog(self) - result = dialog.exec_() - if result == 1: - save_changes = True - change_project = True - - elif result == 2: - change_project = True - - if save_changes: - self._parent._save() - - if change_project: - self.select_project(new_project_name) - self.current_project = new_project_name - self.project_changed.emit() - else: - self.select_project(self.current_project) - - def validate_context_change(self): - # TODO add check if project can be changed (is modified) - for item in self._parent.input_fields: - is_modified = item.child_modified - if is_modified: - return False - return True - - def project_name(self): - if self.current_project == self.default: - return None - return self.current_project - - def select_project(self, project_name): - model = self.project_list.model() - found_items = model.findItems(project_name) - if not found_items: - found_items = model.findItems(self.default) - - index = model.indexFromItem(found_items[0]) - self.project_list.selectionModel().clear() - self.project_list.selectionModel().setCurrentIndex( - index, QtCore.QItemSelectionModel.SelectionFlag.SelectCurrent - ) - - def refresh(self): - selected_project = None - for index in self.project_list.selectedIndexes(): - selected_project = index.data(QtCore.Qt.DisplayRole) - break - - model = self.project_list.model() - model.clear() - - items = [self.default] - - system_settings = get_system_settings() - mongo_url = system_settings["modules"]["avalon"]["AVALON_MONGO"] - if not mongo_url: - mongo_url = os.environ["PYPE_MONGO"] - - # Force uninstall of whole avalon connection if url does not match - # to current environment and set it as environment - if mongo_url != os.environ["AVALON_MONGO"]: - AvalonMongoConnection.uninstall(self.dbcon, force=True) - os.environ["AVALON_MONGO"] = mongo_url - self.dbcon = None - - if not self.dbcon: - try: - self.dbcon = AvalonMongoDB() - self.dbcon.install() - except Exception: - self.dbcon = None - self.current_project = None - - if self.dbcon: - for project_doc in tuple(self.dbcon.projects()): - items.append(project_doc["name"]) - - for item in items: - model.appendRow(QtGui.QStandardItem(item)) - - self.select_project(selected_project) - - self.current_project = self.project_list.currentIndex().data( - QtCore.Qt.DisplayRole - ) + if not self.entity.is_in_studio_state(): + self.reset() class ProjectWidget(SettingsCategoryWidget): - schema_category = "projects_schema" - initial_schema_name = "schema_main" - def initialize_attributes(self): self.project_name = None - super(ProjectWidget, self).initialize_attributes() - def ui_tweaks(self): project_list_widget = ProjectListWidget(self) project_list_widget.refresh() @@ -824,13 +396,6 @@ class ProjectWidget(SettingsCategoryWidget): self.project_list_widget = project_list_widget - def defaults_dir(self): - return DEFAULTS_DIR - - def validate_defaults_to_save(self, _): - # Projects does not have any specific validations - return True - def on_saved(self, saved_tab_widget): """Callback on any tab widget save. @@ -848,82 +413,62 @@ class ProjectWidget(SettingsCategoryWidget): if mongo_url != os.environ["AVALON_MONGO"]: self.project_list_widget.refresh() + def _create_root_entity(self): + self.entity = ProjectSettings(change_state=False) + self.entity.on_change_callbacks.append(self._on_entity_change) + try: + if ( + self.modify_defaults_checkbox + and self.modify_defaults_checkbox.isChecked() + ): + self.entity.set_defaults_state() + + elif self.project_name is None: + self.entity.set_studio_state() + + elif self.project_name == self.entity.project_name: + self.entity.set_project_state() + else: + self.entity.change_project(self.project_name) + + if self.modify_defaults_checkbox: + self.modify_defaults_checkbox.setEnabled(True) + self.project_list_widget.setEnabled(True) + + except DefaultsNotDefined: + if not self.modify_defaults_checkbox: + msg_box = QtWidgets.QMessageBox( + "BUG: Default values are not set and you" + " don't have permissions to modify them." + ) + msg_box.exec_() + return + + self.entity.set_defaults_state() + self.modify_defaults_checkbox.setChecked(True) + self.modify_defaults_checkbox.setEnabled(False) + self.project_list_widget.setEnabled(False) + + except StudioDefaultsNotDefined: + self.select_default_project() + def _on_project_change(self): + project_name = self.project_list_widget.project_name() + if project_name == self.project_name: + return + + self.project_name = project_name + self.set_state(CategoryState.Working) - project_name = self.project_list_widget.project_name() - if project_name is None: - _project_overrides = lib.NOT_SET - _project_anatomy = lib.NOT_SET - self.is_overidable = False - else: - _project_overrides = get_project_settings_overrides(project_name) - _project_anatomy = get_project_anatomy_overrides(project_name) - self.is_overidable = True - - overrides = {self.main_schema_key: { - PROJECT_SETTINGS_KEY: lib.convert_overrides_to_gui_data( - _project_overrides - ), - PROJECT_ANATOMY_KEY: lib.convert_overrides_to_gui_data( - _project_anatomy - ) - }} - self.project_name = project_name - self.ignore_value_changes = True - for item in self.input_fields: - item.apply_overrides(overrides) - self.ignore_value_changes = False + self.reset() self.set_state(CategoryState.Idle) - def save(self): - data = {} - studio_overrides = bool(self.project_name is None) - for item in self.input_fields: - if studio_overrides: - value, _is_group = item.studio_overrides() - else: - value, _is_group = item.overrides() - if value is not lib.NOT_SET: - data.update(value) - - output_data = lib.convert_gui_data_to_overrides( - data.get(self.main_schema_key) or {} - ) - - # Saving overrides data - project_overrides_data = output_data.get(PROJECT_SETTINGS_KEY, {}) - save_project_settings(self.project_name, project_overrides_data) - - # Saving anatomy data - project_anatomy_data = output_data.get(PROJECT_ANATOMY_KEY, {}) - save_project_anatomy(self.project_name, project_anatomy_data) - - def update_values(self): - if self.project_name is not None: - self._on_project_change() - return - - default_values = lib.convert_data_to_gui_data( - {self.main_schema_key: get_default_settings()} - ) - for input_field in self.input_fields: - input_field.update_default_values(default_values) - - if self._hide_studio_overrides: - studio_values = lib.NOT_SET + def _on_modify_defaults(self): + if self.modify_defaults_checkbox.isChecked(): + if not self.entity.is_in_defaults_state(): + self.reset() else: - studio_values = lib.convert_overrides_to_gui_data({ - self.main_schema_key: { - PROJECT_SETTINGS_KEY: ( - get_studio_project_settings_overrides() - ), - PROJECT_ANATOMY_KEY: ( - get_studio_project_anatomy_overrides() - ) - } - }) - - for input_field in self.input_fields: - input_field.update_studio_values(studio_values) + if not self.entity.is_in_studio_state(): + self.reset() diff --git a/pype/tools/settings/settings/widgets/dict_mutable_widget.py b/pype/tools/settings/settings/widgets/dict_mutable_widget.py index a9e11f051c..b27e0e492b 100644 --- a/pype/tools/settings/settings/widgets/dict_mutable_widget.py +++ b/pype/tools/settings/settings/widgets/dict_mutable_widget.py @@ -13,8 +13,6 @@ from .lib import ( CHILD_OFFSET ) -from pype.settings.entities import NOT_SET - def create_add_btn(parent): add_btn = QtWidgets.QPushButton("+", parent) @@ -156,14 +154,13 @@ class ModifiableDictItem(QtWidgets.QWidget): self.entity = entity self.entity_widget = entity_widget - self.create_ui_for_entity = entity_widget.create_ui_for_entity self.ignore_input_changes = entity_widget.ignore_input_changes self.is_key_duplicated = False self.is_required = False - self.origin_key = NOT_SET - self.origin_key_label = NOT_SET + self.origin_key = None + self.origin_key_label = None self.temp_key = "" self.uuid_key = None @@ -172,19 +169,25 @@ class ModifiableDictItem(QtWidgets.QWidget): self.wrapper_widget = None + self.key_label_input = None + if collapsible_key: self.create_collapsible_ui() else: self.create_addible_ui() self.update_style() + @property + def category_widget(self): + return self.entity_widget.category_widget + + def create_ui_for_entity(self, *args, **kwargs): + return self.entity_widget.create_ui_for_entity(*args, **kwargs) + def create_addible_ui(self): key_input = QtWidgets.QLineEdit(self) key_input.setObjectName("DictKey") - spacer_widget = SpacerWidget(self) - spacer_widget.setVisible(False) - add_btn = create_add_btn(self) remove_btn = create_remove_btn(self) @@ -194,7 +197,6 @@ class ModifiableDictItem(QtWidgets.QWidget): layout.addWidget(add_btn, 0) layout.addWidget(remove_btn, 0) layout.addWidget(key_input, 0) - layout.addWidget(spacer_widget, 1) key_input.textChanged.connect(self._on_key_change) key_input.returnPressed.connect(self._on_enter_press) @@ -203,17 +205,18 @@ class ModifiableDictItem(QtWidgets.QWidget): remove_btn.clicked.connect(self.on_remove_clicked) self.key_input = key_input - self.spacer_widget = spacer_widget self.add_btn = add_btn self.remove_btn = remove_btn self.content_widget = self self.content_layout = layout - self.input_field = self.create_ui_for_entity(self.entity, self) + self.input_field = self.create_ui_for_entity( + self.category_widget, self.entity, self + ) def add_widget_to_layout(self, widget, label=None): - self.content_layout.addWidget(widget) + self.content_layout.addWidget(widget, 1) self.setFocusProxy(widget) def create_collapsible_ui(self): @@ -269,12 +272,19 @@ class ModifiableDictItem(QtWidgets.QWidget): key_input.textChanged.connect(self._on_key_change) key_input.returnPressed.connect(self._on_enter_press) - key_label_input.textChanged.connect(self._on_key_change) + key_label_input.textChanged.connect(self._on_key_label_change) key_label_input.returnPressed.connect(self._on_enter_press) edit_btn.clicked.connect(self.on_edit_pressed) remove_btn.clicked.connect(self.on_remove_clicked) + # Hide edit inputs + key_input.setVisible(False) + key_input_label_widget.setVisible(False) + key_label_input.setVisible(False) + key_label_input_label_widget.setVisible(False) + remove_btn.setVisible(False) + self.key_input = key_input self.key_input_label_widget = key_input_label_widget self.key_label_input = key_label_input @@ -286,7 +296,9 @@ class ModifiableDictItem(QtWidgets.QWidget): self.content_widget = content_widget self.content_layout = content_layout - self.input_field = self.create_ui_for_entity(self.entity, self) + self.input_field = self.create_ui_for_entity( + self.category_widget, self.entity, self + ) def get_style_state(self): if self.is_invalid: @@ -327,10 +339,13 @@ class ModifiableDictItem(QtWidgets.QWidget): def set_key_label(self, key, label): self.set_key(key) - if label: - self.key_label_input.setText(label) + self.set_label(label) self.set_edit_mode(False) + def set_label(self, label): + if self.key_label_input and label is not None: + self.key_label_input.setText(label) + def set_as_required(self, key): self.key_input.setText(key) self.key_input.setEnabled(False) @@ -360,6 +375,8 @@ class ModifiableDictItem(QtWidgets.QWidget): self.set_edit_mode(False) def _on_key_label_change(self): + label = self.key_label_value() + self.entity_widget.change_label(label, self) self.update_key_label() def _on_key_change(self): @@ -427,17 +444,6 @@ class ModifiableDictItem(QtWidgets.QWidget): def is_key_label_modified(self): return self.key_label_value() != self.origin_key_label - def is_value_modified(self): - return self.input_field.is_modified - - @property - def is_modified(self): - return ( - self.is_key_modified() - or self.is_key_label_modified() - or self.is_value_modified() - ) - def trigger_hierarchical_style_update(self): self.entity_widget.trigger_hierarchical_style_update() @@ -504,9 +510,9 @@ class ModifiableDictItem(QtWidgets.QWidget): return self.entity_widget.input_fields.index(self) def key_label_value(self): - if self.collapsible_key: + if self.key_label_input: return self.key_label_input.text() - return NOT_SET + return None def mouseReleaseEvent(self, event): return QtWidgets.QWidget.mouseReleaseEvent(self, event) @@ -685,6 +691,13 @@ class DictMutableKeysWidget(BaseWidget): self.entity.children_by_key[sk_old_key] ) + def change_label(self, label, input_field): + entity = input_field.entity + _label = self.entity.get_child_label(entity) + if _label == label: + return + self.entity.set_child_label(entity, label) + def add_widget_for_child( self, child_entity, after_widget=None, first=False ): @@ -756,6 +769,7 @@ class DictMutableKeysWidget(BaseWidget): self.update_style() def _on_entity_change(self): + changed = False to_remove = [] for input_field in self.input_fields: found = False @@ -768,6 +782,7 @@ class DictMutableKeysWidget(BaseWidget): to_remove.append(input_field) for input_field in to_remove: + changed = True self.remove_row(input_field) for key, child_entity in self.entity.items(): @@ -781,6 +796,7 @@ class DictMutableKeysWidget(BaseWidget): break if not found: + changed = True args = [previous_input] if previous_input is None: args.append(True) @@ -788,12 +804,25 @@ class DictMutableKeysWidget(BaseWidget): _input_field = self.add_widget_for_child(child_entity, *args) _input_field.origin_key = key _input_field.set_key(key) + if self.entity.collapsible_key: + label = self.entity.get_child_label(child_entity) + _input_field.origin_key_label = label + _input_field.set_label(label) _input_field.set_entity_value() else: if input_field.key_value() != key: + changed = True input_field.set_key(key) + if self.entity.collapsible_key: + label = self.entity.get_child_label(child_entity) + if input_field.key_label_value() != label: + input_field.set_label(label) + + if changed: + self.on_shuffle() + def set_entity_value(self): while self.input_fields: self.remove_row(self.input_fields[0]) @@ -802,6 +831,10 @@ class DictMutableKeysWidget(BaseWidget): input_field = self.add_widget_for_child(child_entity) input_field.origin_key = key input_field.set_key(key) + if self.entity.collapsible_key: + label = self.entity.get_child_label(child_entity) + input_field.origin_key_label = label + input_field.set_label(label) for input_field in self.input_fields: input_field.set_entity_value() diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py deleted file mode 100644 index 6ab7a03fb9..0000000000 --- a/pype/tools/settings/settings/widgets/item_types.py +++ /dev/null @@ -1,4129 +0,0 @@ -import json -import collections -from Qt import QtWidgets, QtCore, QtGui -from .widgets import ( - IconButton, - ExpandingWidget, - NumberSpinBox, - GridLabelWidget, - ComboBox, - NiceCheckbox -) -from .multiselection_combobox import MultiSelectionComboBox -from .lib import NOT_SET, METADATA_KEY, TypeToKlass, CHILD_OFFSET -from pype.api import Logger -from avalon.vendor import qtawesome - - -class InvalidValueType(Exception): - msg_template = "{}" - - def __init__(self, valid_types, invalid_type, key): - msg = "" - if key: - msg += "Key \"{}\". ".format(key) - - joined_types = ", ".join( - [str(valid_type) for valid_type in valid_types] - ) - msg += "Got invalid type \"{}\". Expected: {}".format( - invalid_type, joined_types - ) - self.msg = msg - super(InvalidValueType, self).__init__(msg) - - -class SettingObject: - """Partially abstract class for Setting's item type workflow.""" - # `is_input_type` attribute says if has implemented item type methods - is_input_type = True - # `is_wrapper_item` attribute says if item will never hold `key` - # information and is just visually different - is_wrapper_item = False - # Each input must have implemented default value for development - # when defaults are not filled yet. - default_input_value = NOT_SET - # Will allow to show actions for the item type (disabled for proxies) else - # item is skipped and try to trigger actions on it's parent. - allow_actions = True - # If item can store environment values - allow_to_environment = False - # All item types must have implemented Qt signal which is emitted when - # it's or it's children value has changed, - value_changed = None - # Item will expand to full width in grid layout - expand_in_grid = False - # types that are valid for `set_value` method - valid_value_types = tuple() - - def validate_value(self, value): - if not self.valid_value_types: - return - - for valid_type in self.valid_value_types: - if type(value) is valid_type: - return - - key = getattr(self, "key", None) - raise InvalidValueType(self.valid_value_types, type(value), key) - - def merge_metadata(self, current_metadata, new_metadata): - for key, value in new_metadata.items(): - if key not in current_metadata: - current_metadata[key] = value - - elif key == "groups": - current_metadata[key].extend(value) - - elif key == "environments": - for group_key, subvalue in value.items(): - if group_key not in current_metadata[key]: - current_metadata[key][group_key] = [] - current_metadata[key][group_key].extend(subvalue) - - else: - raise KeyError("Unknown metadata key: \"{}\"".format(key)) - return current_metadata - - def _set_default_attributes(self): - """Create and reset attributes required for all item types. - - They may not be used in the item but are required to be set. - """ - # Default input attributes - self._has_studio_override = False - self._had_studio_override = False - - self._is_overriden = False - self._was_overriden = False - - self._is_modified = False - self._is_invalid = False - - self._is_nullable = False - self._as_widget = False - self._is_group = False - self._roles = None - self.hidden_by_role = False - - # If value should be stored to environments - self._env_group_key = None - - self._any_parent_as_widget = None - self._any_parent_is_group = None - - # Parent input - self._parent = None - - # States of inputs - self._state = None - self._child_state = None - - # Attributes where values are stored - self.default_value = NOT_SET - self.studio_value = NOT_SET - self.override_value = NOT_SET - - # Log object - self._log = None - - # Only for develop mode - self.defaults_not_set = False - - def available_for_role(self, role_name=None): - if not self._roles: - return True - if role_name is None: - role_name = self.user_role - return role_name in self._roles - - def initial_attributes(self, schema_data, parent, as_widget): - """Prepare attributes based on entered arguments. - - This method should be same for each item type. Few item types - may require to extend with specific attributes for their case. - """ - self._set_default_attributes() - - self.schema_data = schema_data - - self._parent = parent - self._as_widget = as_widget - - self._roles = schema_data.get("roles") - if self._roles is not None and not isinstance(self._roles, list): - self._roles = [self._roles] - - self._is_group = schema_data.get("is_group", False) - self._env_group_key = schema_data.get("env_group_key") - # TODO not implemented yet - self._is_nullable = schema_data.get("is_nullable", False) - - if self.is_environ: - if not self.allow_to_environment: - raise TypeError(( - "Item {} does not allow to store environment values" - ).format(schema_data["type"])) - - any_parent_as_widget = parent.as_widget - if not any_parent_as_widget: - any_parent_as_widget = parent.any_parent_as_widget - - self._any_parent_as_widget = any_parent_as_widget - - any_parent_is_group = parent.is_group - if not any_parent_is_group: - any_parent_is_group = parent.any_parent_is_group - - self._any_parent_is_group = any_parent_is_group - - if not self.available_for_role(): - self.hide() - self.hidden_by_role = True - - if ( - self.is_input_type - and not self.as_widget - and not self.is_wrapper_item - ): - if "key" not in self.schema_data: - error_msg = "Missing \"key\" in schema data. {}".format( - str(schema_data).replace("'", '"') - ) - raise KeyError(error_msg) - - self.key = self.schema_data["key"] - - self.label = self.schema_data.get("label") - if not self.label and self._is_group: - raise ValueError( - "Item is set as `is_group` but has empty `label`." - ) - - @property - def user_role(self): - """Tool is running with any user role. - - Returns: - str: user role as string. - - """ - return self._parent.user_role - - @property - def log(self): - """Auto created logger for debugging.""" - if self._log is None: - self._log = Logger().get_logger(self.__class__.__name__) - return self._log - - @property - def had_studio_override(self): - """Item had studio overrides on refresh. - - Use attribute `_had_studio_override` which should be changed only - during methods `update_studio_values` and `update_default_values`. - - Returns: - bool - - """ - return self._had_studio_override - - @property - def has_studio_override(self): - """Item has studio override at the moment. - - With combination of `had_studio_override` is possible to know if item - is modified (not value change). - - Returns: - bool - - """ - return self._has_studio_override or self._parent.has_studio_override - - @property - def is_environ(self): - return self.env_group_key is not None - - @property - def env_group_key(self): - return self._env_group_key - - @env_group_key.setter - def env_group_key(self, value): - if value is not None and not isinstance(value, str): - raise TypeError( - "Expected 'None' of 'str'. Got {}".format(str(type(value))) - ) - self._env_group_key = value - - @property - def as_widget(self): - """Item is used as widget in parent item. - - Returns: - bool - - """ - return self._as_widget - - @property - def any_parent_as_widget(self): - """Any parent of item is used as widget. - - Attribute holding this information is set during creation and - stored to `_any_parent_as_widget`. - - Why is this information useful: If any parent is used as widget then - modifications and override are not important for whole part. - - Returns: - bool - - """ - if self._any_parent_as_widget is None: - return super(SettingObject, self).any_parent_as_widget - return self._any_parent_as_widget - - @property - def is_group(self): - """Item represents key that can be overriden. - - Attribute `is_group` can be set to True only once in item hierarchy. - - Returns: - bool - - """ - return self._is_group - - @property - def any_parent_is_group(self): - """Any parent of item is group. - - Attribute holding this information is set during creation and - stored to `_any_parent_is_group`. - - Why is this information useful: If any parent is group and - the parent is set as overriden, this item is overriden too. - - Returns: - bool - - """ - if self._any_parent_is_group is None: - return super(SettingObject, self).any_parent_is_group - return self._any_parent_is_group - - @property - def is_modified(self): - """Has object any changes that require saving.""" - if self.any_parent_as_widget: - return self._is_modified or self.defaults_not_set - - if self._is_modified or self.defaults_not_set: - return True - - if self.is_overidable: - if self.as_widget: - return self._was_overriden != self.is_overriden - return self.was_overriden != self.is_overriden - - return self.has_studio_override != self.had_studio_override - - @property - def is_overriden(self): - """Is object overriden so should be saved to overrides.""" - return self._is_overriden or self._parent.is_overriden - - @property - def was_overriden(self): - """Item had set value of project overrides on project change.""" - if self._as_widget: - return self._parent.was_overriden - return self._was_overriden - - @property - def is_invalid(self): - """Value set in is not valid.""" - return self._is_invalid - - @property - def is_nullable(self): - """Value of item can be set to None. - - NOT IMPLEMENTED! - """ - return self._is_nullable - - @property - def is_overidable(self): - """ care about overrides.""" - - return self._parent.is_overidable - - def any_parent_overriden(self): - """Any of parent objects up to top hiearchy item is overriden. - - Returns: - bool - - """ - - if self._parent._is_overriden: - return True - return self._parent.any_parent_overriden() - - @property - def ignore_value_changes(self): - """Most of attribute changes are ignored on value change when True.""" - return self._parent.ignore_value_changes - - @ignore_value_changes.setter - def ignore_value_changes(self, value): - """Setter for global parent item to apply changes for all inputs.""" - self._parent.ignore_value_changes = value - - def config_value(self): - """Output for saving changes or overrides.""" - return {self.key: self.item_value()} - - def environment_value(self): - raise NotImplementedError( - "{} Method `environment_value` not implemented!".format( - repr(self) - ) - ) - - @classmethod - def style_state( - cls, has_studio_override, is_invalid, is_overriden, is_modified - ): - """Return stylesheet state by intered booleans.""" - items = [] - if is_invalid: - items.append("invalid") - else: - if is_overriden: - items.append("overriden") - if is_modified: - items.append("modified") - - if not items and has_studio_override: - items.append("studio") - - return "-".join(items) or "" - - def show_actions_menu(self, event=None): - if event and event.button() != QtCore.Qt.RightButton: - return - - if not self.allow_actions: - if event: - return self.mouseReleaseEvent(event) - return - - menu = QtWidgets.QMenu(self) - - actions_mapping = {} - if self.child_modified: - action = QtWidgets.QAction("Discard changes") - actions_mapping[action] = self._discard_changes - menu.addAction(action) - - if ( - self.is_overidable - and not self.is_overriden - and not self.any_parent_is_group - ): - action = QtWidgets.QAction("Set project override") - actions_mapping[action] = self._set_as_overriden - menu.addAction(action) - - if ( - not self.is_overidable - and ( - self.has_studio_override or self.child_has_studio_override - ) - ): - action = QtWidgets.QAction("Reset to pype default") - actions_mapping[action] = self._reset_to_pype_default - menu.addAction(action) - - if ( - not self.is_overidable - and not self.is_overriden - and not self.any_parent_is_group - and not self._had_studio_override - ): - action = QtWidgets.QAction("Set studio default") - actions_mapping[action] = self._set_studio_default - menu.addAction(action) - - if ( - not self.any_parent_overriden() - and (self.is_overriden or self.child_overriden) - ): - # TODO better label - action = QtWidgets.QAction("Remove project override") - actions_mapping[action] = self._remove_overrides - menu.addAction(action) - - if not actions_mapping: - action = QtWidgets.QAction("< No action >") - actions_mapping[action] = None - menu.addAction(action) - - result = menu.exec_(QtGui.QCursor.pos()) - if result: - to_run = actions_mapping[result] - if to_run: - to_run() - - def mouseReleaseEvent(self, event): - if self.allow_actions and event.button() == QtCore.Qt.RightButton: - return self.show_actions_menu() - - mro = type(self).mro() - index = mro.index(self.__class__) - item = None - for idx in range(index + 1, len(mro)): - _item = mro[idx] - if hasattr(_item, "mouseReleaseEvent"): - item = _item - break - - if item: - return item.mouseReleaseEvent(self, event) - - def _discard_changes(self): - self.ignore_value_changes = True - self.discard_changes() - self.ignore_value_changes = False - - def discard_changes(self): - """Item's implementation to discard all changes made by user. - - Reset all values to same values as had when opened GUI - or when changed project. - - Must not affect `had_studio_override` value or `was_overriden` - value. It must be marked that there are keys/values which are not in - defaults or overrides. - """ - raise NotImplementedError( - "{} Method `discard_changes` not implemented!".format( - repr(self) - ) - ) - - def _set_studio_default(self): - self.ignore_value_changes = True - self.set_studio_default() - self.ignore_value_changes = False - - def set_studio_default(self): - """Item's implementation to set current values as studio's overrides. - - Mark item and it's children as they have studio overrides. - """ - raise NotImplementedError( - "{} Method `set_studio_default` not implemented!".format( - repr(self) - ) - ) - - def _reset_to_pype_default(self): - self.ignore_value_changes = True - self.reset_to_pype_default() - self.ignore_value_changes = False - - def reset_to_pype_default(self): - """Item's implementation to remove studio overrides. - - Mark item as it does not have studio overrides unset studio - override values. - """ - raise NotImplementedError( - "{} Method `reset_to_pype_default` not implemented!".format( - repr(self) - ) - ) - - def _remove_overrides(self): - self.ignore_value_changes = True - self.remove_overrides() - self.ignore_value_changes = False - - def remove_overrides(self): - """Item's implementation to remove project overrides. - - Mark item as does not have project overrides. Must not change - `was_overriden` attribute value. - """ - raise NotImplementedError( - "{} Method `remove_overrides` not implemented!".format( - repr(self) - ) - ) - - def _set_as_overriden(self): - self.ignore_value_changes = True - self.set_as_overriden() - self.ignore_value_changes = False - - def set_as_overriden(self): - """Item's implementation to set values as overriden for project. - - Mark item and all it's children as they're overriden. Must skip - items with children items that has attributes `is_group` - and `any_parent_is_group` set to False. In that case those items - are not meant to be overridable and should trigger the method on it's - children. - - """ - raise NotImplementedError( - "{} Method `set_as_overriden` not implemented!".format(repr(self)) - ) - - def hierarchical_style_update(self): - """Trigger update style method down the hierarchy.""" - raise NotImplementedError( - "{} Method `hierarchical_style_update` not implemented!".format( - repr(self) - ) - ) - - def update_default_values(self, parent_values): - """Fill default values on startup or on refresh. - - Default values stored in `pype` repository should update all items in - schema. Each item should take values for his key and set it's value or - pass values down to children items. - - Args: - parent_values (dict): Values of parent's item. But in case item is - used as widget, `parent_values` contain value for item. - """ - raise NotImplementedError( - "{} does not have implemented `update_default_values`".format(self) - ) - - def update_studio_values(self, parent_values): - """Fill studio override values on startup or on refresh. - - Set studio value if is not set to NOT_SET, in that case studio - overrides are not set yet. - - Args: - parent_values (dict): Values of parent's item. But in case item is - used as widget, `parent_values` contain value for item. - """ - raise NotImplementedError( - "{} does not have implemented `update_studio_values`".format(self) - ) - - def apply_overrides(self, parent_values): - """Fill project override values on startup, refresh or project change. - - Set project value if is not set to NOT_SET, in that case project - overrides are not set yet. - - Args: - parent_values (dict): Values of parent's item. But in case item is - used as widget, `parent_values` contain value for item. - """ - raise NotImplementedError( - "{} does not have implemented `apply_overrides`".format(self) - ) - - @property - def child_has_studio_override(self): - """Any children item has studio overrides.""" - raise NotImplementedError( - "{} does not have implemented `child_has_studio_override`".format( - self - ) - ) - - @property - def child_modified(self): - """Any children item is modified.""" - raise NotImplementedError( - "{} does not have implemented `child_modified`".format(self) - ) - - @property - def child_overriden(self): - """Any children item has project overrides.""" - raise NotImplementedError( - "{} does not have implemented `child_overriden`".format(self) - ) - - @property - def child_invalid(self): - """Any children item does not have valid value.""" - raise NotImplementedError( - "{} does not have implemented `child_invalid`".format(self) - ) - - def get_invalid(self): - """Return invalid item types all down the hierarchy.""" - raise NotImplementedError( - "{} does not have implemented `get_invalid`".format(self) - ) - - def item_value(self): - """Value of an item without key.""" - raise NotImplementedError( - "Method `item_value` not implemented!" - ) - - -class InputObject(SettingObject): - """Class for inputs with pre-implemented methods. - - Class is for item types not creating or using other item types, most - of methods has same code in that case. - """ - - def update_default_values(self, parent_values): - self._state = None - self._is_modified = False - - value = NOT_SET - if self.as_widget: - value = parent_values - elif parent_values is not NOT_SET: - value = parent_values.get(self.key, NOT_SET) - - if value is NOT_SET: - if self.available_for_role("developer"): - self.defaults_not_set = True - value = self.default_input_value - if value is NOT_SET: - raise NotImplementedError(( - "{} Does not have implemented" - " attribute `default_input_value`" - ).format(self)) - - else: - raise ValueError( - "Default value is not set. This is implementation BUG." - ) - else: - self.defaults_not_set = False - - self.default_value = value - self._has_studio_override = False - self._had_studio_override = False - try: - self.set_value(value) - except InvalidValueType as exc: - self.default_value = NOT_SET - self.defaults_not_set = True - self.log.warning(exc.msg) - - def update_studio_values(self, parent_values): - self._state = None - self._is_modified = False - - value = NOT_SET - if self._as_widget: - value = parent_values - elif parent_values is not NOT_SET: - value = parent_values.get(self.key, NOT_SET) - - self.studio_value = value - if value is not NOT_SET: - self._has_studio_override = True - self._had_studio_override = True - - else: - self._has_studio_override = False - self._had_studio_override = False - value = self.default_value - - try: - self.set_value(value) - except InvalidValueType as exc: - self.studio_value = NOT_SET - self.log.warning(exc.msg) - - def apply_overrides(self, parent_values): - self._is_modified = False - self._state = None - self._had_studio_override = bool(self._has_studio_override) - if self._as_widget: - override_value = parent_values - elif parent_values is NOT_SET or self.key not in parent_values: - override_value = NOT_SET - else: - override_value = parent_values[self.key] - - self.override_value = override_value - - if override_value is NOT_SET: - self._is_overriden = False - self._was_overriden = False - if self.has_studio_override: - value = self.studio_value - else: - value = self.default_value - else: - self._is_overriden = True - self._was_overriden = True - value = override_value - - try: - self.set_value(value) - except InvalidValueType as exc: - self.override_value = NOT_SET - self.log.warning(exc.msg) - - def _on_value_change(self, item=None): - if self.ignore_value_changes: - return - - if not self.any_parent_as_widget: - if self.is_overidable: - self._is_overriden = True - else: - self._has_studio_override = True - - if self._is_invalid: - self._is_modified = True - elif self._is_overriden: - self._is_modified = self.item_value() != self.override_value - elif self._has_studio_override: - self._is_modified = self.item_value() != self.studio_value - else: - self._is_modified = self.item_value() != self.default_value - - self.update_style() - - self.value_changed.emit(self) - - def studio_overrides(self): - if ( - not (self.as_widget or self.any_parent_as_widget) - and not self.has_studio_override - ): - return NOT_SET, False - return self.config_value(), self.is_group - - def overrides(self): - if ( - not (self.as_widget or self.any_parent_as_widget) - and not self.is_overriden - ): - return NOT_SET, False - return self.config_value(), self.is_group - - def hierarchical_style_update(self): - self.update_style() - - def _style_state(self): - if self.as_widget: - state = self.style_state( - False, - self._is_invalid, - False, - self.is_modified - ) - else: - state = self.style_state( - self.has_studio_override, - self.is_invalid, - self.is_overriden, - self.is_modified - ) - return state - - def update_style(self): - state = self._style_state() - if self._state == state: - return - - self._state = state - - self.input_field.setProperty("input-state", state) - self.input_field.style().polish(self.input_field) - if self.label_widget: - self.label_widget.setProperty("state", state) - self.label_widget.style().polish(self.label_widget) - - def remove_overrides(self): - self._is_overriden = False - self._is_modified = False - if self.has_studio_override: - self.set_value(self.studio_value) - else: - self.set_value(self.default_value) - self._is_overriden = False - self._is_modified = False - - def reset_to_pype_default(self): - self.set_value(self.default_value) - self._has_studio_override = False - - def set_studio_default(self): - self._has_studio_override = True - - def discard_changes(self): - self._is_overriden = self._was_overriden - self._has_studio_override = self._had_studio_override - if self.is_overidable: - if self._was_overriden and self.override_value is not NOT_SET: - self.set_value(self.override_value) - else: - if self._had_studio_override: - self.set_value(self.studio_value) - else: - self.set_value(self.default_value) - - if not self.is_overidable: - if self.has_studio_override: - self._is_modified = self.studio_value != self.item_value() - else: - self._is_modified = self.default_value != self.item_value() - self._is_overriden = False - return - - self._state = None - self._is_modified = False - self._is_overriden = self._was_overriden - - def set_as_overriden(self): - self._is_overriden = True - - @property - def child_has_studio_override(self): - return self._has_studio_override - - @property - def child_modified(self): - return self.is_modified - - @property - def child_overriden(self): - return self._is_overriden - - @property - def child_invalid(self): - return self.is_invalid - - def get_invalid(self): - output = [] - if self.is_invalid: - output.append(self) - return output - - def reset_children_attributes(self): - return - - -class BooleanWidget(QtWidgets.QWidget, InputObject): - default_input_value = True - value_changed = QtCore.Signal(object) - valid_value_types = (bool, ) - - def __init__( - self, input_data, parent, - as_widget=False, parent_widget=None - ): - if parent_widget is None: - parent_widget = parent - super(BooleanWidget, self).__init__(parent_widget) - - self.initial_attributes(input_data, parent, as_widget) - - def create_ui(self, label_widget=None): - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(5) - - if not self.as_widget and not label_widget: - label = self.schema_data["label"] - label_widget = QtWidgets.QLabel(label) - label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) - layout.addWidget(label_widget, 0) - self.label_widget = label_widget - - checkbox_height = self.style().pixelMetric( - QtWidgets.QStyle.PM_IndicatorHeight - ) - self.input_field = NiceCheckbox(height=checkbox_height, parent=self) - - spacer = QtWidgets.QWidget(self) - spacer.setAttribute(QtCore.Qt.WA_TranslucentBackground) - - layout.addWidget(self.input_field, 0) - layout.addWidget(spacer, 1) - - self.setFocusProxy(self.input_field) - - self.input_field.stateChanged.connect(self._on_value_change) - - def set_value(self, value): - # Ignore value change because if `self.isChecked()` has same - # value as `value` the `_on_value_change` is not triggered - self.validate_value(value) - self.input_field.setChecked(value) - - def item_value(self): - return self.input_field.isChecked() - - -class NumberWidget(QtWidgets.QWidget, InputObject): - default_input_value = 0 - value_changed = QtCore.Signal(object) - input_modifiers = ("minimum", "maximum", "decimal") - valid_value_types = (int, float) - - def __init__( - self, schema_data, parent, as_widget=False, parent_widget=None - ): - if parent_widget is None: - parent_widget = parent - super(NumberWidget, self).__init__(parent_widget) - - self.initial_attributes(schema_data, parent, as_widget) - - def create_ui(self, label_widget=None): - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(5) - - kwargs = { - modifier: self.schema_data.get(modifier) - for modifier in self.input_modifiers - if self.schema_data.get(modifier) - } - self.input_field = NumberSpinBox(self, **kwargs) - - self.setFocusProxy(self.input_field) - - if not self._as_widget and not label_widget: - label = self.schema_data["label"] - label_widget = QtWidgets.QLabel(label) - layout.addWidget(label_widget, 0) - self.label_widget = label_widget - - layout.addWidget(self.input_field, 1) - - self.input_field.valueChanged.connect(self._on_value_change) - - def set_value(self, value): - self.validate_value(value) - self.input_field.setValue(value) - - def item_value(self): - return self.input_field.value() - - -class TextWidget(QtWidgets.QWidget, InputObject): - default_input_value = "" - value_changed = QtCore.Signal(object) - valid_value_types = (str, ) - - def __init__( - self, schema_data, parent, as_widget=False, parent_widget=None - ): - if parent_widget is None: - parent_widget = parent - super(TextWidget, self).__init__(parent_widget) - - self.initial_attributes(schema_data, parent, as_widget) - self.multiline = schema_data.get("multiline", False) - self.placeholder_text = schema_data.get("placeholder") - - def create_ui(self, label_widget=None): - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(5) - - if self.multiline: - self.input_field = QtWidgets.QPlainTextEdit(self) - else: - self.input_field = QtWidgets.QLineEdit(self) - - if self.placeholder_text: - self.input_field.setPlaceholderText(self.placeholder_text) - - self.setFocusProxy(self.input_field) - - layout_kwargs = {} - if self.multiline: - layout_kwargs["alignment"] = QtCore.Qt.AlignTop - - if not self._as_widget and not label_widget: - label = self.schema_data["label"] - label_widget = QtWidgets.QLabel(label) - layout.addWidget(label_widget, 0, **layout_kwargs) - self.label_widget = label_widget - - layout.addWidget(self.input_field, 1, **layout_kwargs) - - self.input_field.textChanged.connect(self._on_value_change) - - def set_value(self, value): - self.validate_value(value) - if self.multiline: - self.input_field.setPlainText(value) - else: - self.input_field.setText(value) - - def item_value(self): - if self.multiline: - return self.input_field.toPlainText() - else: - return self.input_field.text() - - -class PathInputWidget(QtWidgets.QWidget, InputObject): - default_input_value = "" - value_changed = QtCore.Signal(object) - valid_value_types = (str, list) - - def __init__( - self, schema_data, parent, as_widget=False, parent_widget=None - ): - if parent_widget is None: - parent_widget = parent - super(PathInputWidget, self).__init__(parent_widget) - - self.initial_attributes(schema_data, parent, as_widget) - - self.with_arguments = schema_data.get("with_arguments", False) - - def create_ui(self, label_widget=None): - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(5) - - if not self._as_widget and not label_widget: - label = self.schema_data["label"] - label_widget = QtWidgets.QLabel(label) - layout.addWidget(label_widget, 0) - self.label_widget = label_widget - - self.input_field = QtWidgets.QLineEdit(self) - self.args_input_field = None - if self.with_arguments: - self.input_field.setPlaceholderText("Executable path") - self.args_input_field = QtWidgets.QLineEdit(self) - self.args_input_field.setPlaceholderText("Arguments") - - self.setFocusProxy(self.input_field) - layout.addWidget(self.input_field, 8) - self.input_field.textChanged.connect(self._on_value_change) - - if self.args_input_field: - layout.addWidget(self.args_input_field, 2) - self.args_input_field.textChanged.connect(self._on_value_change) - - def set_value(self, value): - self.validate_value(value) - - if not isinstance(value, list): - self.input_field.setText(value) - elif self.with_arguments: - self.input_field.setText(value[0]) - self.args_input_field.setText(value[1]) - else: - self.input_field.setText(value[0]) - - def item_value(self): - path_value = self.input_field.text() - if self.with_arguments: - return [path_value, self.args_input_field.text()] - return path_value - - -class EnumeratorWidget(QtWidgets.QWidget, InputObject): - default_input_value = True - value_changed = QtCore.Signal(object) - - def __init__( - self, schema_data, parent, as_widget=False, parent_widget=None - ): - if parent_widget is None: - parent_widget = parent - super(EnumeratorWidget, self).__init__(parent_widget) - - self.initial_attributes(schema_data, parent, as_widget) - self.multiselection = schema_data.get("multiselection") - self.enum_items = schema_data["enum_items"] - if not self.enum_items: - raise ValueError("Attribute `enum_items` is not defined.") - - def create_ui(self, label_widget=None): - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(5) - - if not self._as_widget and not label_widget: - label = self.schema_data["label"] - label_widget = QtWidgets.QLabel(label) - label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) - layout.addWidget(label_widget, 0) - self.label_widget = label_widget - - if self.multiselection: - placeholder = self.schema_data.get("placeholder") - self.input_field = MultiSelectionComboBox( - placeholder=placeholder, parent=self - ) - else: - self.input_field = ComboBox(self) - - first_value = NOT_SET - for enum_item in self.enum_items: - for value, label in enum_item.items(): - if first_value is NOT_SET: - first_value = value - self.input_field.addItem(label, value) - self._first_value = first_value - - if self.multiselection: - model = self.input_field.model() - for idx in range(self.input_field.count()): - model.item(idx).setCheckable(True) - - layout.addWidget(self.input_field, 0) - - self.setFocusProxy(self.input_field) - - self.input_field.value_changed.connect(self._on_value_change) - - @property - def default_input_value(self): - if self.multiselection: - return [] - return self._first_value - - def set_value(self, value): - # Ignore value change because if `self.isChecked()` has same - # value as `value` the `_on_value_change` is not triggered - self.input_field.set_value(value) - - def update_style(self): - if self.as_widget: - state = self.style_state( - False, - self._is_invalid, - False, - self._is_modified - ) - else: - state = self.style_state( - self.has_studio_override, - self.is_invalid, - self.is_overriden, - self.is_modified - ) - - if self._state == state: - return - - self._state = state - self.input_field.setProperty("input-state", state) - self.input_field.style().polish(self.input_field) - if self.label_widget: - self.label_widget.setProperty("state", state) - self.label_widget.style().polish(self.label_widget) - - def item_value(self): - return self.input_field.value() - - -class RawJsonInput(QtWidgets.QPlainTextEdit): - tab_length = 4 - - def __init__(self, *args, **kwargs): - super(RawJsonInput, self).__init__(*args, **kwargs) - self.setObjectName("RawJsonInput") - self.setTabStopDistance( - QtGui.QFontMetricsF( - self.font() - ).horizontalAdvance(" ") * self.tab_length - ) - - def sizeHint(self): - document = self.document() - layout = document.documentLayout() - - height = document.documentMargin() + 2 * self.frameWidth() + 1 - block = document.begin() - while block != document.end(): - height += layout.blockBoundingRect(block).height() - block = block.next() - - hint = super(RawJsonInput, self).sizeHint() - hint.setHeight(height) - - return hint - - def set_value(self, value): - if value is NOT_SET: - value = "" - - elif not isinstance(value, str): - try: - value = json.dumps(value, indent=4) - except Exception: - value = "" - self.setPlainText(value) - - def json_value(self): - return json.loads(self.toPlainText()) - - def has_invalid_value(self): - try: - self.json_value() - return False - except Exception: - return True - - def resizeEvent(self, event): - self.updateGeometry() - super(RawJsonInput, self).resizeEvent(event) - - -class RawJsonWidget(QtWidgets.QWidget, InputObject): - default_input_value = "{}" - value_changed = QtCore.Signal(object) - valid_value_types = (str, dict, list, type(NOT_SET)) - allow_to_environment = True - - def __init__( - self, schema_data, parent, as_widget=False, parent_widget=None - ): - if parent_widget is None: - parent_widget = parent - super(RawJsonWidget, self).__init__(parent_widget) - - self.initial_attributes(schema_data, parent, as_widget) - # By default must be invalid - self._is_invalid = True - - def create_ui(self, label_widget=None): - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(5) - - self.input_field = RawJsonInput(self) - self.input_field.setSizePolicy( - QtWidgets.QSizePolicy.Minimum, - QtWidgets.QSizePolicy.MinimumExpanding - ) - - self.setFocusProxy(self.input_field) - - if not self.as_widget and not label_widget: - if self.label: - label_widget = QtWidgets.QLabel(self.label) - layout.addWidget(label_widget, 0, alignment=QtCore.Qt.AlignTop) - self.label_widget = label_widget - - layout.addWidget(self.input_field, 1, alignment=QtCore.Qt.AlignTop) - - self.input_field.textChanged.connect(self._on_value_change) - - def update_studio_values(self, parent_values): - self._is_invalid = self.input_field.has_invalid_value() - return super(RawJsonWidget, self).update_studio_values(parent_values) - - def set_value(self, value): - self.validate_value(value) - self.input_field.set_value(value) - - def _on_value_change(self, *args, **kwargs): - self._is_invalid = self.input_field.has_invalid_value() - return super(RawJsonWidget, self)._on_value_change(*args, **kwargs) - - def item_value(self): - if self.is_invalid: - return NOT_SET - - value = self.input_field.json_value() - if not self.is_environ: - return value - - output = {} - for key, value in value.items(): - output[key.upper()] = value - return output - - def config_value(self): - value = self.item_value() - if self.is_environ: - if METADATA_KEY not in value: - value[METADATA_KEY] = {} - - env_keys = [] - for key in value.keys(): - if key is not METADATA_KEY: - env_keys.append(key) - - value[METADATA_KEY]["environments"] = { - self.env_group_key: env_keys - } - return {self.key: value} - - -class ListItem(QtWidgets.QWidget, SettingObject): - _btn_size = 20 - value_changed = QtCore.Signal(object) - - def __init__( - self, item_schema, config_parent, parent, is_strict=False - ): - super(ListItem, self).__init__(parent) - - self._set_default_attributes() - - self._is_strict = is_strict - - self._parent = config_parent - self._any_parent_is_group = True - self._is_empty = False - - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(3) - - char_up = qtawesome.charmap("fa.angle-up") - char_down = qtawesome.charmap("fa.angle-down") - - if not self._is_strict: - self.add_btn = QtWidgets.QPushButton("+") - self.remove_btn = QtWidgets.QPushButton("-") - self.up_btn = QtWidgets.QPushButton(char_up) - self.down_btn = QtWidgets.QPushButton(char_down) - - font_up_down = qtawesome.font("fa", 13) - self.up_btn.setFont(font_up_down) - self.down_btn.setFont(font_up_down) - - self.add_btn.setFocusPolicy(QtCore.Qt.ClickFocus) - self.remove_btn.setFocusPolicy(QtCore.Qt.ClickFocus) - self.up_btn.setFocusPolicy(QtCore.Qt.ClickFocus) - self.down_btn.setFocusPolicy(QtCore.Qt.ClickFocus) - - self.add_btn.setFixedSize(self._btn_size, self._btn_size) - self.remove_btn.setFixedSize(self._btn_size, self._btn_size) - self.up_btn.setFixedSize(self._btn_size, self._btn_size) - self.down_btn.setFixedSize(self._btn_size, self._btn_size) - - self.add_btn.setProperty("btn-type", "tool-item") - self.remove_btn.setProperty("btn-type", "tool-item") - self.up_btn.setProperty("btn-type", "tool-item") - self.down_btn.setProperty("btn-type", "tool-item") - - self.add_btn.clicked.connect(self._on_add_clicked) - self.remove_btn.clicked.connect(self._on_remove_clicked) - self.up_btn.clicked.connect(self._on_up_clicked) - self.down_btn.clicked.connect(self._on_down_clicked) - - layout.addWidget(self.add_btn, 0) - layout.addWidget(self.remove_btn, 0) - - ItemKlass = TypeToKlass.types[item_schema["type"]] - self.value_input = ItemKlass( - item_schema, - self, - as_widget=True - ) - self.value_input.create_ui() - - layout.addWidget(self.value_input, 1) - - if not self._is_strict: - self.spacer_widget = QtWidgets.QWidget(self) - self.spacer_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) - self.spacer_widget.setVisible(False) - - layout.addWidget(self.spacer_widget, 1) - - layout.addWidget(self.up_btn, 0) - layout.addWidget(self.down_btn, 0) - - self.value_input.value_changed.connect(self._on_value_change) - - @property - def as_widget(self): - return self._parent.as_widget - - @property - def any_parent_as_widget(self): - return self.as_widget or self._parent.any_parent_as_widget - - def set_as_empty(self, is_empty=True): - self._is_empty = is_empty - - self.spacer_widget.setVisible(is_empty) - self.value_input.setVisible(not is_empty) - self.remove_btn.setEnabled(not is_empty) - self.up_btn.setVisible(not is_empty) - self.down_btn.setVisible(not is_empty) - self.order_changed() - self._on_value_change() - - def order_changed(self): - row = self.row() - parent_row_count = self.parent_rows_count() - if parent_row_count == 1: - self.up_btn.setVisible(False) - self.down_btn.setVisible(False) - return - - if not self.up_btn.isVisible(): - self.up_btn.setVisible(True) - self.down_btn.setVisible(True) - - if row == 0: - self.up_btn.setEnabled(False) - self.down_btn.setEnabled(True) - - elif row == parent_row_count - 1: - self.up_btn.setEnabled(True) - self.down_btn.setEnabled(False) - - else: - self.up_btn.setEnabled(True) - self.down_btn.setEnabled(True) - - def _on_value_change(self, item=None): - self.value_changed.emit(self) - - def row(self): - return self._parent.input_fields.index(self) - - def parent_rows_count(self): - return len(self._parent.input_fields) - - def _on_add_clicked(self): - if self._is_empty: - self.set_as_empty(False) - else: - self._parent.add_row(row=self.row() + 1) - - def _on_remove_clicked(self): - self._parent.remove_row(self) - - def _on_up_clicked(self): - row = self.row() - self._parent.swap_rows(row - 1, row) - - def _on_down_clicked(self): - row = self.row() - self._parent.swap_rows(row, row + 1) - - def config_value(self): - if not self._is_empty: - return self.value_input.item_value() - return NOT_SET - - @property - def is_modified(self): - if self._is_empty: - return False - return self.value_input.is_modified - - @property - def child_has_studio_override(self): - return self.value_input.child_has_studio_override - - @property - def child_modified(self): - return self.value_input.child_modified - - @property - def child_overriden(self): - return self.value_input.child_overriden - - def hierarchical_style_update(self): - self.value_input.hierarchical_style_update() - - def mouseReleaseEvent(self, event): - return QtWidgets.QWidget.mouseReleaseEvent(self, event) - - def update_default_values(self, value): - self.value_input.update_default_values(value) - - def update_studio_values(self, value): - self.value_input.update_studio_values(value) - - def apply_overrides(self, value): - self.value_input.apply_overrides(value) - - -class ListWidget(QtWidgets.QWidget, InputObject): - default_input_value = [] - value_changed = QtCore.Signal(object) - valid_value_types = (list, ) - - def __init__( - self, schema_data, parent, as_widget=False, parent_widget=None - ): - if parent_widget is None: - parent_widget = parent - super(ListWidget, self).__init__(parent_widget) - self.setObjectName("ListWidget") - - self.initial_attributes(schema_data, parent, as_widget) - - self.use_label_wrap = schema_data.get("use_label_wrap") or False - # Used only if `use_label_wrap` is set to True - self.collapsible = schema_data.get("collapsible") or True - self.collapsed = schema_data.get("collapsed") or False - - self.expand_in_grid = bool(self.use_label_wrap) - - if self.as_widget and self.use_label_wrap: - raise ValueError( - "`ListWidget` can't have set `use_label_wrap` to True and" - " be used as widget at the same time." - ) - - if self.use_label_wrap and not self.label: - raise ValueError( - "`ListWidget` can't have set `use_label_wrap` to True and" - " not have set \"label\" key at the same time." - ) - - self.input_fields = [] - - object_type = schema_data["object_type"] - if isinstance(object_type, dict): - self.item_schema = object_type - else: - self.item_schema = { - "type": object_type - } - - def create_ui(self, label_widget=None): - main_layout = QtWidgets.QHBoxLayout(self) - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.setSpacing(0) - - body_widget = None - if self.as_widget: - pass - - elif self.use_label_wrap: - body_widget = ExpandingWidget(self.label, self) - main_layout.addWidget(body_widget) - - label_widget = body_widget.label_widget - - elif not label_widget: - if self.label: - label_widget = QtWidgets.QLabel(self.label, self) - main_layout.addWidget( - label_widget, alignment=QtCore.Qt.AlignTop - ) - - self.label_widget = label_widget - - self.body_widget = body_widget - - if body_widget is None: - content_parent_widget = self - else: - content_parent_widget = body_widget - - content_state = "" - - inputs_widget = QtWidgets.QWidget(content_parent_widget) - inputs_widget.setObjectName("ContentWidget") - inputs_widget.setProperty("content_state", content_state) - inputs_layout = QtWidgets.QVBoxLayout(inputs_widget) - inputs_layout.setContentsMargins(CHILD_OFFSET, 5, 0, 5) - - if body_widget is None: - main_layout.addWidget(inputs_widget) - else: - body_widget.set_content_widget(inputs_widget) - - self.body_widget = body_widget - self.inputs_widget = inputs_widget - self.inputs_layout = inputs_layout - - if body_widget: - if not self.collapsible: - body_widget.hide_toolbox(hide_content=False) - - elif self.collapsed: - body_widget.toggle_content() - - self.setAttribute(QtCore.Qt.WA_TranslucentBackground) - - self.add_row(is_empty=True) - - def count(self): - return len(self.input_fields) - - def update_studio_values(self, parent_values): - super(ListWidget, self).update_studio_values(parent_values) - - self.hierarchical_style_update() - - def set_value(self, value): - self.validate_value(value) - - previous_inputs = tuple(self.input_fields) - for item_value in value: - self.add_row(value=item_value) - - for input_field in previous_inputs: - self.remove_row(input_field) - - if self.count() == 0: - self.add_row(is_empty=True) - - def swap_rows(self, row_1, row_2): - if row_1 == row_2: - return - - if row_1 > row_2: - row_1, row_2 = row_2, row_1 - - field_1 = self.input_fields[row_1] - field_2 = self.input_fields[row_2] - - self.input_fields[row_1] = field_2 - self.input_fields[row_2] = field_1 - - layout_index = self.inputs_layout.indexOf(field_1) - self.inputs_layout.insertWidget(layout_index + 1, field_1) - - field_1.order_changed() - field_2.order_changed() - - def add_row(self, row=None, value=None, is_empty=False): - # Create new item - item_widget = ListItem(self.item_schema, self, self.inputs_widget) - - previous_field = None - next_field = None - - if row is None: - if self.input_fields: - previous_field = self.input_fields[-1] - self.inputs_layout.addWidget(item_widget) - self.input_fields.append(item_widget) - else: - if row > 0: - previous_field = self.input_fields[row - 1] - - max_index = self.count() - if row < max_index: - next_field = self.input_fields[row] - - self.inputs_layout.insertWidget(row, item_widget) - self.input_fields.insert(row, item_widget) - - if previous_field: - previous_field.order_changed() - - if next_field: - next_field.order_changed() - - if is_empty: - item_widget.set_as_empty() - item_widget.value_changed.connect(self._on_value_change) - - item_widget.order_changed() - - previous_input = None - for input_field in self.input_fields: - if previous_input is not None: - self.setTabOrder( - previous_input, input_field.value_input.focusProxy() - ) - previous_input = input_field.value_input.focusProxy() - - # Set text if entered text is not None - # else (when add button clicked) trigger `_on_value_change` - if value is not None: - if self._is_overriden: - item_widget.apply_overrides(value) - elif not self._has_studio_override: - item_widget.update_default_values(value) - else: - item_widget.update_studio_values(value) - self.hierarchical_style_update() - else: - self._on_value_change() - self.updateGeometry() - - def remove_row(self, item_widget): - item_widget.value_changed.disconnect() - - row = self.input_fields.index(item_widget) - previous_field = None - next_field = None - if row > 0: - previous_field = self.input_fields[row - 1] - - if row != len(self.input_fields) - 1: - next_field = self.input_fields[row + 1] - - self.inputs_layout.removeWidget(item_widget) - self.input_fields.pop(row) - item_widget.setParent(None) - item_widget.deleteLater() - - if previous_field: - previous_field.order_changed() - - if next_field: - next_field.order_changed() - - if self.count() == 0: - self.add_row(is_empty=True) - - self._on_value_change() - self.updateGeometry() - - def apply_overrides(self, parent_values): - self._is_modified = False - if self.as_widget: - override_value = parent_values - elif parent_values is NOT_SET or self.key not in parent_values: - override_value = NOT_SET - else: - override_value = parent_values[self.key] - - self.override_value = override_value - - if override_value is NOT_SET: - self._is_overriden = False - self._was_overriden = False - if self.has_studio_override: - value = self.studio_value - else: - value = self.default_value - else: - self._is_overriden = True - self._was_overriden = True - value = override_value - - self._is_modified = False - self._state = None - - self.set_value(value) - - def hierarchical_style_update(self): - for input_field in self.input_fields: - input_field.hierarchical_style_update() - self.update_style() - - @property - def is_modified(self): - is_modified = super(ListWidget, self).is_modified - if is_modified: - return is_modified - - for input_field in self.input_fields: - if input_field.is_modified: - return True - return False - - def update_style(self, is_overriden=None): - if not self.label_widget: - return - - child_invalid = self.child_invalid - if self.body_widget: - child_state = self.style_state( - self.child_has_studio_override, - child_invalid, - self.child_overriden, - self.child_modified - ) - if child_state: - child_state = "child-{}".format(child_state) - - if child_state != self._child_state: - self.body_widget.side_line_widget.setProperty( - "state", child_state - ) - self.body_widget.side_line_widget.style().polish( - self.body_widget.side_line_widget - ) - self._child_state = child_state - - state = self.style_state( - self.had_studio_override, - child_invalid, - self.is_overriden, - self.is_modified - ) - if self._state == state: - return - - self.label_widget.setProperty("state", state) - self.label_widget.style().polish(self.label_widget) - - self._state = state - - def item_value(self): - output = [] - for item in self.input_fields: - value = item.config_value() - if value is not NOT_SET: - output.append(value) - return output - - -class ListStrictWidget(QtWidgets.QWidget, InputObject): - value_changed = QtCore.Signal(object) - _default_input_value = None - valid_value_types = (list, ) - - def __init__( - self, schema_data, parent, as_widget=False, parent_widget=None - ): - if parent_widget is None: - parent_widget = parent - super(ListStrictWidget, self).__init__(parent_widget) - self.setObjectName("ListStrictWidget") - - self.initial_attributes(schema_data, parent, as_widget) - - self.is_horizontal = schema_data.get("horizontal", True) - self.object_types = self.schema_data["object_types"] - - self.input_fields = [] - - def create_ui(self, label_widget=None): - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 5) - layout.setSpacing(5) - - if not self.as_widget and not label_widget: - label = self.schema_data.get("label") - if label: - label_widget = QtWidgets.QLabel(label, self) - layout.addWidget(label_widget, alignment=QtCore.Qt.AlignTop) - elif self._is_group: - raise KeyError(( - "Schema item must contain \"label\" if `is_group` is True" - " to be able visualize changes and show actions." - )) - - self.label_widget = label_widget - - self._add_children(layout) - - def _add_children(self, layout): - inputs_widget = QtWidgets.QWidget(self) - inputs_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) - layout.addWidget(inputs_widget) - - if self.is_horizontal: - inputs_layout = QtWidgets.QHBoxLayout(inputs_widget) - else: - inputs_layout = QtWidgets.QGridLayout(inputs_widget) - - inputs_layout.setContentsMargins(0, 0, 0, 0) - inputs_layout.setSpacing(3) - - self.inputs_widget = inputs_widget - self.inputs_layout = inputs_layout - - children_item_mapping = [] - for child_configuration in self.object_types: - item_widget = ListItem( - child_configuration, self, self.inputs_widget, is_strict=True - ) - - self.input_fields.append(item_widget) - item_widget.value_changed.connect(self._on_value_change) - - label = child_configuration.get("label") - label_widget = None - if label: - label_widget = QtWidgets.QLabel(label, self) - - children_item_mapping.append((label_widget, item_widget)) - - if self.is_horizontal: - self._add_children_horizontally(children_item_mapping) - else: - self._add_children_vertically(children_item_mapping) - - self.updateGeometry() - - def _add_children_vertically(self, children_item_mapping): - any_has_label = False - for item_mapping in children_item_mapping: - if item_mapping[0]: - any_has_label = True - break - - row = self.inputs_layout.count() - if not any_has_label: - self.inputs_layout.setColumnStretch(1, 1) - for item_mapping in children_item_mapping: - item_widget = item_mapping[1] - self.inputs_layout.addWidget(item_widget, row, 0, 1, 1) - - spacer_widget = QtWidgets.QWidget(self.inputs_widget) - self.inputs_layout.addWidget(spacer_widget, row, 1, 1, 1) - row += 1 - - else: - self.inputs_layout.setColumnStretch(2, 1) - for label_widget, item_widget in children_item_mapping: - self.inputs_layout.addWidget( - label_widget, row, 0, 1, 1, - alignment=QtCore.Qt.AlignRight | QtCore.Qt.AlignTop - ) - self.inputs_layout.addWidget(item_widget, row, 1, 1, 1) - - spacer_widget = QtWidgets.QWidget(self.inputs_widget) - self.inputs_layout.addWidget(spacer_widget, row, 2, 1, 1) - row += 1 - - def _add_children_horizontally(self, children_item_mapping): - for label_widget, item_widget in children_item_mapping: - if label_widget: - self.inputs_layout.addWidget(label_widget, 0) - self.inputs_layout.addWidget(item_widget, 0) - - spacer_widget = QtWidgets.QWidget(self.inputs_widget) - self.inputs_layout.addWidget(spacer_widget, 1) - - @property - def default_input_value(self): - if self._default_input_value is None: - self.set_value(NOT_SET) - self._default_input_value = self.item_value() - return self._default_input_value - - def set_value(self, value): - if self._is_overriden: - method_name = "apply_overrides" - elif not self._has_studio_override: - method_name = "update_default_values" - else: - method_name = "update_studio_values" - - for idx, input_field in enumerate(self.input_fields): - if value is NOT_SET: - _value = value - else: - if idx > len(value) - 1: - _value = NOT_SET - else: - _value = value[idx] - _method = getattr(input_field, method_name) - _method(_value) - - def hierarchical_style_update(self): - for input_field in self.input_fields: - input_field.hierarchical_style_update() - self.update_style() - - def update_style(self): - if not self.label_widget: - return - - state = self._style_state() - - if self._state == state: - return - - self._state = state - self.label_widget.setProperty("state", state) - self.label_widget.style().polish(self.label_widget) - - def item_value(self): - output = [] - for item in self.input_fields: - output.append(item.config_value()) - return output - - -class ModifiableDictItem(QtWidgets.QWidget, SettingObject): - _btn_size = 20 - value_changed = QtCore.Signal(object) - - def __init__(self, item_schema, config_parent, parent): - super(ModifiableDictItem, self).__init__(parent) - - self._set_default_attributes() - self._parent = config_parent - - any_parent_as_widget = config_parent.as_widget - if not any_parent_as_widget: - any_parent_as_widget = config_parent.any_parent_as_widget - - self._any_parent_as_widget = any_parent_as_widget - self._any_parent_is_group = True - - self._is_empty = False - self._is_key_duplicated = False - - self._is_required = False - - self.origin_key = NOT_SET - self.origin_key_label = NOT_SET - - if self.collapsable_key: - layout = QtWidgets.QVBoxLayout(self) - else: - layout = QtWidgets.QHBoxLayout(self) - - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(3) - - ItemKlass = TypeToKlass.types[item_schema["type"]] - value_input = ItemKlass( - item_schema, - self, - as_widget=True - ) - value_input.create_ui() - - key_input = QtWidgets.QLineEdit(self) - key_input.setObjectName("DictKey") - - key_label_input = None - wrapper_widget = None - if self.collapsable_key: - key_label_input = QtWidgets.QLineEdit(self) - - wrapper_widget = ExpandingWidget("", self) - layout.addWidget(wrapper_widget) - - content_widget = QtWidgets.QWidget(wrapper_widget) - content_widget.setObjectName("ContentWidget") - content_layout = QtWidgets.QHBoxLayout(content_widget) - content_layout.setContentsMargins(CHILD_OFFSET, 5, 0, 0) - content_layout.setSpacing(5) - - wrapper_widget.set_content_widget(content_widget) - - content_layout.addWidget(value_input) - - def key_input_focused_out(event): - QtWidgets.QLineEdit.focusOutEvent(key_input, event) - self._on_focus_lose() - - def key_label_input_focused_out(event): - QtWidgets.QLineEdit.focusOutEvent(key_label_input, event) - self._on_focus_lose() - - key_input.focusOutEvent = key_input_focused_out - key_label_input.focusOutEvent = key_label_input_focused_out - - spacer_widget = None - add_btn = None - if not self.collapsable_key: - spacer_widget = QtWidgets.QWidget(self) - spacer_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) - spacer_widget.setVisible(False) - - add_btn = QtWidgets.QPushButton("+") - add_btn.setFocusPolicy(QtCore.Qt.ClickFocus) - add_btn.setProperty("btn-type", "tool-item") - add_btn.setFixedSize(self._btn_size, self._btn_size) - - edit_btn = None - if self.collapsable_key: - edit_btn = IconButton( - "fa.edit", QtCore.Qt.lightGray, QtCore.Qt.white - ) - edit_btn.setFocusPolicy(QtCore.Qt.ClickFocus) - edit_btn.setProperty("btn-type", "tool-item-icon") - edit_btn.setFixedHeight(self._btn_size) - - remove_btn = QtWidgets.QPushButton("-") - remove_btn.setFocusPolicy(QtCore.Qt.ClickFocus) - remove_btn.setProperty("btn-type", "tool-item") - remove_btn.setFixedSize(self._btn_size, self._btn_size) - - key_input_label_widget = None - key_label_input_label_widget = None - if self.collapsable_key: - key_input_label_widget = QtWidgets.QLabel("Key:") - key_label_input_label_widget = QtWidgets.QLabel("Label:") - wrapper_widget.add_widget_before_label(edit_btn) - wrapper_widget.add_widget_after_label(key_input_label_widget) - wrapper_widget.add_widget_after_label(key_input) - wrapper_widget.add_widget_after_label(key_label_input_label_widget) - wrapper_widget.add_widget_after_label(key_label_input) - wrapper_widget.add_widget_after_label(remove_btn) - - else: - layout.addWidget(add_btn, 0) - layout.addWidget(remove_btn, 0) - layout.addWidget(key_input, 0) - layout.addWidget(spacer_widget, 1) - layout.addWidget(value_input, 1) - - self.setFocusProxy(value_input) - - key_input.textChanged.connect(self._on_key_change) - key_input.returnPressed.connect(self._on_enter_press) - if key_label_input: - key_label_input.textChanged.connect(self._on_key_change) - key_label_input.returnPressed.connect(self._on_enter_press) - - value_input.value_changed.connect(self._on_value_change) - if add_btn: - add_btn.clicked.connect(self.on_add_clicked) - if edit_btn: - edit_btn.clicked.connect(self.on_edit_pressed) - remove_btn.clicked.connect(self.on_remove_clicked) - - self.key_input = key_input - self.key_input_label_widget = key_input_label_widget - self.key_label_input = key_label_input - self.key_label_input_label_widget = key_label_input_label_widget - self.value_input = value_input - self.wrapper_widget = wrapper_widget - - self.spacer_widget = spacer_widget - - self.add_btn = add_btn - self.edit_btn = edit_btn - self.remove_btn = remove_btn - - self.set_as_empty(self._is_empty) - - def _style_state(self): - if self.as_widget: - state = self.style_state( - False, - self._is_invalid, - False, - self.is_modified - ) - else: - state = self.style_state( - self.has_studio_override, - self.is_invalid, - self.is_overriden, - self.is_modified - ) - return state - - @property - def collapsable_key(self): - return self._parent.collapsable_key - - def key_value(self): - return self.key_input.text() - - def is_key_invalid(self): - if self._is_empty: - return False - - if self.key_value() == "": - return True - - if self._is_key_duplicated: - return True - return False - - def set_key_is_duplicated(self, duplicated): - if duplicated == self._is_key_duplicated: - return - - self._is_key_duplicated = duplicated - if self.collapsable_key: - if duplicated: - self.set_edit_mode(True) - else: - self._on_focus_lose() - self.update_style() - - def set_as_required(self, key): - self.key_input.setText(key) - self.key_input.setEnabled(False) - self._is_required = True - - if self._is_empty: - self.set_as_empty(False) - - if self.collapsable_key: - self.remove_btn.setVisible(False) - else: - self.remove_btn.setEnabled(False) - self.add_btn.setEnabled(False) - - def set_as_last_required(self): - if self.add_btn: - self.add_btn.setEnabled(True) - - def _on_focus_lose(self): - if ( - self.edit_btn.hasFocus() - or self.key_input.hasFocus() - or self.key_label_input.hasFocus() - or self.remove_btn.hasFocus() - ): - return - self._on_enter_press() - - def _on_enter_press(self): - if not self.collapsable_key: - return - - if self._is_empty: - self.on_add_clicked() - else: - self.set_edit_mode(False) - - def _on_key_label_change(self): - self.update_key_label() - - def _on_key_change(self): - if self.value_is_env_group: - self.value_input.env_group_key = self.key_input.text() - - self.update_key_label() - - self._on_value_change() - - def _on_value_change(self, item=None): - self.update_style() - self.value_changed.emit(self) - - def update_default_values(self, key, label, value): - self.origin_key = key - self.key_input.setText(key) - if self.key_label_input: - label = label or "" - self.origin_key_label = label - self.key_label_input.setText(label) - self.value_input.update_default_values(value) - - def update_studio_values(self, key, label, value): - self.origin_key = key - self.key_input.setText(key) - if self.key_label_input: - label = label or "" - self.origin_key_label = label - self.key_label_input.setText(label) - self.value_input.update_studio_values(value) - - def apply_overrides(self, key, label, value): - self.origin_key = key - self.key_input.setText(key) - if self.key_label_input: - label = label or "" - self.origin_key_label = label - self.key_label_input.setText(label) - self.value_input.apply_overrides(value) - - @property - def value_is_env_group(self): - return self._parent.value_is_env_group - - @property - def is_group(self): - return self._parent.is_group - - def update_key_label(self): - if not self.wrapper_widget: - return - key_value = self.key_input.text() - key_label_value = self.key_label_input.text() - if key_label_value: - label = "{} ({})".format(key_label_value, key_value) - else: - label = key_value - self.wrapper_widget.label_widget.setText(label) - - def on_add_clicked(self): - if not self.collapsable_key: - if self._is_empty: - self.set_as_empty(False) - else: - self._parent.add_row(row=self.row() + 1) - return - - if not self._is_empty: - return - - if not self.key_value(): - return - - self.set_as_empty(False) - self._parent.add_row(row=self.row() + 1, is_empty=True) - - def on_edit_pressed(self): - if not self.key_input.isVisible(): - self.set_edit_mode() - else: - self.key_input.setFocus() - - def set_edit_mode(self, enabled=True): - if self.is_invalid and not enabled: - return - self.wrapper_widget.label_widget.setVisible(not enabled) - self.key_label_input_label_widget.setVisible(enabled) - self.key_input.setVisible(enabled) - self.key_input_label_widget.setVisible(enabled) - self.key_label_input.setVisible(enabled) - if not self._is_required: - self.remove_btn.setVisible(enabled) - if enabled: - if self.key_input.isEnabled(): - self.key_input.setFocus() - else: - self.key_label_input.setFocus() - - def on_remove_clicked(self): - self._parent.remove_row(self) - - def set_as_empty(self, is_empty=True): - self._is_empty = is_empty - - self.value_input.setVisible(not is_empty) - if not self.collapsable_key: - self.key_input.setVisible(not is_empty) - self.remove_btn.setEnabled(not is_empty) - self.spacer_widget.setVisible(is_empty) - - else: - self.remove_btn.setVisible(False) - self.key_input_label_widget.setVisible(is_empty) - self.key_input.setVisible(is_empty) - self.key_label_input_label_widget.setVisible(is_empty) - self.key_label_input.setVisible(is_empty) - self.edit_btn.setVisible(not is_empty) - - self.wrapper_widget.label_widget.setVisible(not is_empty) - if is_empty: - self.wrapper_widget.hide_toolbox() - else: - self.wrapper_widget.show_toolbox() - self._on_value_change() - - @property - def any_parent_is_group(self): - return self._parent.any_parent_is_group - - def is_key_modified(self): - return self.key_value() != self.origin_key - - def is_key_label_modified(self): - return self.key_label_value() != self.origin_key_label - - def is_value_modified(self): - return self.value_input.is_modified - - @property - def is_modified(self): - if self._is_empty: - return False - return ( - self.is_key_modified() - or self.is_key_label_modified() - or self.is_value_modified() - ) - - def hierarchical_style_update(self): - self.value_input.hierarchical_style_update() - self.update_style() - - @property - def is_invalid(self): - if self._is_empty: - return False - return self.is_key_invalid() or self.value_input.is_invalid - - def update_style(self): - key_input_state = "" - if not self._is_empty: - if self.is_key_invalid(): - key_input_state = "invalid" - elif self.is_key_modified(): - key_input_state = "modified" - - self.key_input.setProperty("state", key_input_state) - self.key_input.style().polish(self.key_input) - - if not self.wrapper_widget: - return - - state = self._style_state() - - if self._state == state: - return - - self._state = state - - if self.wrapper_widget.label_widget: - self.wrapper_widget.label_widget.setProperty("state", state) - self.wrapper_widget.label_widget.style().polish( - self.wrapper_widget.label_widget - ) - - if state: - child_state = "child-{}".format(state) - else: - child_state = "" - - self.wrapper_widget.side_line_widget.setProperty("state", child_state) - self.wrapper_widget.side_line_widget.style().polish( - self.wrapper_widget.side_line_widget - ) - - def row(self): - return self._parent.input_fields.index(self) - - def key_label_value(self): - if self.collapsable_key: - return self.key_label_input.text() - return NOT_SET - - def item_value(self): - key = self.key_input.text() - value = self.value_input.item_value() - return {key: value} - - def config_value(self): - if self._is_empty: - return {} - return self.item_value() - - def mouseReleaseEvent(self, event): - return QtWidgets.QWidget.mouseReleaseEvent(self, event) - - -class ModifiableDict(QtWidgets.QWidget, InputObject): - default_input_value = {} - # Should be used only for dictionary with one datatype as value - # TODO this is actually input field (do not care if is group or not) - value_changed = QtCore.Signal(object) - expand_in_grid = True - valid_value_types = (dict, ) - - def __init__( - self, schema_data, parent, as_widget=False, parent_widget=None - ): - if parent_widget is None: - parent_widget = parent - super(ModifiableDict, self).__init__(parent_widget) - self.setObjectName("ModifiableDict") - - self.initial_attributes(schema_data, parent, as_widget) - - self.input_fields = [] - self.required_inputs_by_key = {} - - # Validation of "key" key - self.key = schema_data["key"] - self.value_is_env_group = ( - schema_data.get("value_is_env_group") or False - ) - self.hightlight_content = schema_data.get("highlight_content") or False - self.collapsable_key = schema_data.get("collapsable_key") or False - self.required_keys = schema_data.get("required_keys") or [] - - object_type = schema_data["object_type"] - if isinstance(object_type, dict): - self.item_schema = object_type - else: - # Backwards compatibility - self.item_schema = { - "type": object_type - } - input_modifiers = schema_data.get("input_modifiers") or {} - if input_modifiers: - self.log.warning(( - "Used deprecated key `input_modifiers` to define item." - " Rather use `object_type` as dictionary with modifiers." - )) - self.item_schema.update(input_modifiers) - - if self.value_is_env_group: - self.item_schema["env_group_key"] = "" - - def create_ui(self, label_widget=None): - if self.hightlight_content: - content_state = "hightlighted" - bottom_margin = 5 - else: - content_state = "" - bottom_margin = 0 - - main_layout = QtWidgets.QHBoxLayout(self) - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.setSpacing(0) - - label = self.schema_data.get("label") - - if self.as_widget: - body_widget = None - self.label_widget = label_widget - - elif label is None: - body_widget = None - self.label_widget = None - else: - body_widget = ExpandingWidget(self.schema_data["label"], self) - main_layout.addWidget(body_widget) - - self.label_widget = body_widget.label_widget - - self.body_widget = body_widget - - if body_widget is None: - content_parent_widget = self - else: - content_parent_widget = body_widget - - content_widget = QtWidgets.QWidget(content_parent_widget) - content_widget.setObjectName("ContentWidget") - content_widget.setProperty("content_state", content_state) - content_layout = QtWidgets.QVBoxLayout(content_widget) - content_layout.setContentsMargins(CHILD_OFFSET, 5, 0, bottom_margin) - - if body_widget is None: - main_layout.addWidget(content_widget) - else: - body_widget.set_content_widget(content_widget) - - self.body_widget = body_widget - self.content_widget = content_widget - self.content_layout = content_layout - - if body_widget: - collapsable = self.schema_data.get("collapsable", True) - if collapsable: - collapsed = self.schema_data.get("collapsed", True) - if not collapsed: - body_widget.toggle_content() - - else: - body_widget.hide_toolbox(hide_content=False) - - self.setAttribute(QtCore.Qt.WA_TranslucentBackground) - - last_required_item = None - for key in self.required_keys: - last_required_item = self.add_row(key=key, is_required=True) - - if last_required_item: - last_required_item.set_as_last_required() - else: - self.add_row(is_empty=True) - - def count(self): - return len(self.input_fields) - - def set_value(self, value): - self.validate_value(value) - - metadata = value.get(METADATA_KEY, {}) - dynamic_key_labels = metadata.get("dynamic_key_label") or {} - - required_items = list(self.required_inputs_by_key.values()) - previous_inputs = list() - for input_field in self.input_fields: - if input_field not in required_items: - previous_inputs.append(input_field) - - for item_key, item_value in value.items(): - if item_key is METADATA_KEY: - continue - - label = dynamic_key_labels.get(item_key) - self.add_row(key=item_key, label=label, value=item_value) - - if self.collapsable_key: - self.add_row(is_empty=True) - - for input_field in previous_inputs: - self.remove_row(input_field) - - if self.count() == 0: - self.add_row(is_empty=True) - - def _on_value_change(self, item=None): - if self.ignore_value_changes: - return - - fields_by_keys = collections.defaultdict(list) - for input_field in self.input_fields: - key = input_field.key_value() - fields_by_keys[key].append(input_field) - - for fields in fields_by_keys.values(): - if len(fields) == 1: - field = fields[0] - field.set_key_is_duplicated(False) - else: - for field in fields: - field.set_key_is_duplicated(True) - - if self.is_overidable: - self._is_overriden = True - else: - self._has_studio_override = True - - if self._is_invalid: - self._is_modified = True - elif self._is_overriden: - self._is_modified = self.item_value() != self.override_value - elif self._has_studio_override: - self._is_modified = self.item_value() != self.studio_value - else: - self._is_modified = self.item_value() != self.default_value - - self.update_style() - - self.value_changed.emit(self) - - @property - def is_modified(self): - is_modified = super(ModifiableDict, self).is_modified - if is_modified: - return is_modified - - for input_field in self.input_fields: - if input_field.is_modified: - return True - return False - - def hierarchical_style_update(self): - for input_field in self.input_fields: - input_field.hierarchical_style_update() - self.update_style() - - def update_style(self): - state = self._style_state() - - if self._state == state: - return - - self._state = state - - if self.label_widget: - self.label_widget.setProperty("state", state) - self.label_widget.style().polish(self.label_widget) - - if not self.body_widget: - return - - if state: - child_state = "child-{}".format(state) - else: - child_state = "" - - self.body_widget.side_line_widget.setProperty("state", child_state) - self.body_widget.side_line_widget.style().polish( - self.body_widget.side_line_widget - ) - - def all_item_values(self): - output = {} - for item in self.input_fields: - output.update(item.item_value()) - return output - - def item_value_with_metadata(self): - if not self.collapsable_key: - output = self.item_value() - - else: - output = {} - labels_by_key = {} - for item in self.input_fields: - labels_by_key[item.key_value()] = item.key_label_value() - output.update(item.config_value()) - if METADATA_KEY not in output: - output[METADATA_KEY] = {} - output[METADATA_KEY]["dynamic_key_label"] = labels_by_key - - if self.value_is_env_group: - for env_group_key, value in tuple(output.items()): - env_keys = [] - for key in value.keys(): - if key is not METADATA_KEY: - env_keys.append(key) - - if METADATA_KEY not in value: - value[METADATA_KEY] = {} - - value[METADATA_KEY]["environments"] = {env_group_key: env_keys} - output[env_group_key] = value - return output - - def item_value(self): - output = {} - for item in self.input_fields: - output.update(item.config_value()) - return output - - def config_value(self): - return {self.key: self.item_value_with_metadata()} - - def _create_item(self, row, key, is_empty, is_required): - # Create new item - item_widget = ModifiableDictItem( - self.item_schema, self, self.content_widget - ) - if is_empty: - item_widget.set_as_empty() - - if is_required: - item_widget.set_as_required(key) - self.required_inputs_by_key[key] = item_widget - - item_widget.value_changed.connect(self._on_value_change) - - if row is None: - self.content_layout.addWidget(item_widget) - self.input_fields.append(item_widget) - else: - self.content_layout.insertWidget(row, item_widget) - self.input_fields.insert(row, item_widget) - - previous_input = None - if self.collapsable_key: - for input_field in self.input_fields: - if previous_input is not None: - self.setTabOrder( - previous_input, input_field.value_input - ) - previous_input = input_field.value_input.focusProxy() - - else: - for input_field in self.input_fields: - if previous_input is not None: - self.setTabOrder( - previous_input, input_field.key_input - ) - previous_input = input_field.value_input.focusProxy() - self.setTabOrder( - input_field.key_input, previous_input - ) - return item_widget - - def add_row( - self, - row=None, - key=None, - label=None, - value=None, - is_empty=False, - is_required=False - ): - item_widget = self.required_inputs_by_key.get(key) - if not item_widget: - item_widget = self._create_item(row, key, is_empty, is_required) - - # Set value if entered value is not None - # else (when add button clicked) trigger `_on_value_change` - if value is not None and key is not None: - if not self._has_studio_override: - item_widget.update_default_values(key, label, value) - elif self._is_overriden: - item_widget.apply_overrides(key, label, value) - else: - item_widget.update_studio_values(key, label, value) - self.hierarchical_style_update() - else: - self._on_value_change() - self.parent().updateGeometry() - - return item_widget - - def remove_row(self, item_widget): - item_widget.value_changed.disconnect() - - self.content_layout.removeWidget(item_widget) - self.input_fields.remove(item_widget) - item_widget.setParent(None) - item_widget.deleteLater() - - if self.count() == 0: - self.add_row(is_empty=True) - - self._on_value_change() - self.parent().updateGeometry() - - @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 - - -# Dictionaries -class DictWidget(QtWidgets.QWidget, SettingObject): - value_changed = QtCore.Signal(object) - expand_in_grid = True - valid_value_types = (dict, type(NOT_SET)) - - def __init__( - self, schema_data, parent, as_widget=False, parent_widget=None - ): - if parent_widget is None: - parent_widget = parent - super(DictWidget, self).__init__(parent_widget) - - self.initial_attributes(schema_data, parent, as_widget) - - self.input_fields = [] - - self.checkbox_widget = None - self.checkbox_key = schema_data.get("checkbox_key") - - self.highlight_content = schema_data.get("highlight_content", False) - self.show_borders = schema_data.get("show_borders", True) - self.collapsable = schema_data.get("collapsable", True) - self.collapsed = schema_data.get("collapsed", True) - - def create_ui(self, label_widget=None): - if not self.as_widget and self.schema_data.get("label") is None: - self._ui_item_without_label() - else: - self._ui_item_or_as_widget(label_widget) - - for child_data in self.schema_data.get("children", []): - self.add_children_gui(child_data) - - any_visible = False - for input_field in self.input_fields: - if not input_field.hidden_by_role: - any_visible = True - break - - if not any_visible: - self.hide() - - def _ui_item_without_label(self): - if self._is_group: - raise TypeError( - "Dictionary without label can't be marked as group input." - ) - - self.setObjectName("DictInvisible") - - self.label_widget = None - self.body_widget = None - self.content_layout = QtWidgets.QGridLayout(self) - self.content_layout.setContentsMargins(0, 0, 0, 0) - self.content_layout.setSpacing(5) - - def _ui_item_or_as_widget(self, label_widget): - content_widget = QtWidgets.QWidget(self) - - if self.as_widget: - content_widget.setObjectName("DictAsWidgetBody") - show_borders = str(int(self.show_borders)) - content_widget.setProperty("show_borders", show_borders) - content_layout_margins = (5, 5, 5, 5) - main_layout_spacing = 5 - body_widget = None - - else: - content_widget.setObjectName("ContentWidget") - if self.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) - main_layout_spacing = 0 - - body_widget = ExpandingWidget(self.schema_data["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(main_layout_spacing) - if not body_widget: - main_layout.addWidget(content_widget) - else: - main_layout.addWidget(body_widget) - - self.label_widget = label_widget - self.body_widget = body_widget - self.content_layout = content_layout - - if body_widget: - if len(self.input_fields) == 1 and self.checkbox_widget: - body_widget.hide_toolbox(hide_content=True) - - elif self.collapsable: - if not self.collapsed: - body_widget.toggle_content() - else: - body_widget.hide_toolbox(hide_content=False) - - def add_children_gui(self, child_configuration): - item_type = child_configuration["type"] - klass = TypeToKlass.types.get(item_type) - - row = self.content_layout.rowCount() - if not getattr(klass, "is_input_type", False): - item = klass(child_configuration, self) - self.content_layout.addWidget(item, row, 0, 1, 2) - return item - - if self.checkbox_key and not self.checkbox_widget: - key = child_configuration.get("key") - if key == self.checkbox_key: - if child_configuration["type"] != "boolean": - self.log.warning(( - "SCHEMA BUG: Dictionary item has set as checkbox" - " item invalid type \"{}\". Expected \"boolean\"." - ).format(child_configuration["type"])) - elif self.body_widget is None: - self.log.warning(( - "SCHEMA BUG: Dictionary item has set checkbox" - " item but item does not have label." - ).format(child_configuration["type"])) - else: - return self._add_checkbox_child(child_configuration) - - label_widget = None - item = klass(child_configuration, self) - if not item.expand_in_grid: - label = child_configuration.get("label") - if label is not None: - label_widget = GridLabelWidget(label, self) - self.content_layout.addWidget(label_widget, row, 0, 1, 1) - - item.create_ui(label_widget=label_widget) - item.value_changed.connect(self._on_value_change) - - if label_widget: - if item.hidden_by_role: - label_widget.hide() - label_widget.input_field = item - self.content_layout.addWidget(item, row, 1, 1, 1) - else: - self.content_layout.addWidget(item, row, 0, 1, 2) - - self.input_fields.append(item) - return item - - def _add_checkbox_child(self, child_configuration): - item = BooleanWidget( - child_configuration, self - ) - item.create_ui(label_widget=self.label_widget) - item.value_changed.connect(self._on_value_change) - - self.body_widget.add_widget_before_label(item) - self.checkbox_widget = item - self.input_fields.append(item) - return item - - def remove_overrides(self): - self._is_overriden = False - self._is_modified = False - for input_field in self.input_fields: - input_field.remove_overrides() - - def reset_to_pype_default(self): - for input_field in self.input_fields: - input_field.reset_to_pype_default() - self._has_studio_override = False - - def set_studio_default(self): - for input_field in self.input_fields: - input_field.set_studio_default() - - if self.is_group: - self._has_studio_override = True - - def discard_changes(self): - self._is_modified = False - self._is_overriden = self._was_overriden - self._has_studio_override = self._had_studio_override - - for input_field in self.input_fields: - input_field.discard_changes() - - self._is_modified = self.child_modified - if not self.is_overidable and self.as_widget: - if self.has_studio_override: - self._is_modified = self.studio_value != self.item_value() - else: - self._is_modified = self.default_value != self.item_value() - - self._state = None - self._is_overriden = self._was_overriden - - def set_as_overriden(self): - if self.is_overriden: - return - - if self.is_group: - self._is_overriden = True - return - - for item in self.input_fields: - item.set_as_overriden() - - def update_default_values(self, parent_values): - # Make sure this is set to False - self._state = None - self._child_state = None - - value = NOT_SET - if self.as_widget: - value = parent_values - elif parent_values is not NOT_SET: - value = parent_values.get(self.key, NOT_SET) - - try: - self.validate_value(value) - except InvalidValueType as exc: - value = NOT_SET - self.log.warning(exc.msg) - - for item in self.input_fields: - item.update_default_values(value) - - def update_studio_values(self, parent_values): - # Make sure this is set to False - self._state = None - self._child_state = None - value = NOT_SET - if self.as_widget: - value = parent_values - else: - if parent_values is not NOT_SET: - value = parent_values.get(self.key, NOT_SET) - - self._has_studio_override = False - if self.is_group and value is not NOT_SET: - self._has_studio_override = True - - self._had_studio_override = bool(self._has_studio_override) - - try: - self.validate_value(value) - except InvalidValueType as exc: - value = NOT_SET - self._has_studio_override = False - self._had_studio_override = False - self.log.warning(exc.msg) - - for item in self.input_fields: - item.update_studio_values(value) - - def apply_overrides(self, parent_values): - # Make sure this is set to False - self._state = None - self._child_state = None - - if self.as_widget: - override_values = parent_values - else: - metadata = {} - groups = tuple() - override_values = NOT_SET - if parent_values is not NOT_SET: - metadata = parent_values.get(METADATA_KEY) or metadata - groups = metadata.get("groups") or groups - override_values = parent_values.get(self.key, override_values) - - self._is_overriden = self.key in groups - - try: - self.validate_value(override_values) - except InvalidValueType as exc: - override_values = NOT_SET - self.log.warning(exc.msg) - - for item in self.input_fields: - item.apply_overrides(override_values) - - if not self.as_widget: - if not self._is_overriden: - self._is_overriden = ( - self.is_group - and self.is_overidable - and self.child_overriden - ) - self._was_overriden = bool(self._is_overriden) - - def _on_value_change(self, item=None): - if self.ignore_value_changes: - return - - if self.is_group and not (self.as_widget or self.any_parent_as_widget): - if self.is_overidable: - self._is_overriden = True - else: - self._has_studio_override = True - - # TODO check if this is required - self.hierarchical_style_update() - - self.value_changed.emit(self) - - self.update_style() - - def hierarchical_style_update(self): - for input_field in self.input_fields: - input_field.hierarchical_style_update() - self.update_style() - - def update_style(self, is_overriden=None): - # TODO add style update when used as widget - if not self.body_widget: - return - - child_has_studio_override = self.child_has_studio_override - child_modified = self.child_modified - child_invalid = self.child_invalid - child_state = self.style_state( - child_has_studio_override, - child_invalid, - self.child_overriden, - child_modified - ) - if child_state: - child_state = "child-{}".format(child_state) - - if child_state != self._child_state: - self.body_widget.side_line_widget.setProperty("state", child_state) - self.body_widget.side_line_widget.style().polish( - self.body_widget.side_line_widget - ) - self._child_state = child_state - - state = self.style_state( - self.had_studio_override, - child_invalid, - self.is_overriden, - self.is_modified - ) - if self._state == state: - return - - self.label_widget.setProperty("state", state) - self.label_widget.style().polish(self.label_widget) - - self._state = state - - @property - def is_modified(self): - if self.is_group: - return self._is_modified or self.child_modified - return False - - @property - def child_has_studio_override(self): - for input_field in self.input_fields: - if ( - input_field.has_studio_override - or input_field.child_has_studio_override - ): - return True - return False - - @property - def child_modified(self): - for input_field in self.input_fields: - if input_field.child_modified: - return True - return False - - @property - def child_overriden(self): - for input_field in self.input_fields: - if input_field.is_overriden or input_field.child_overriden: - return True - return False - - @property - def child_invalid(self): - for input_field in self.input_fields: - if input_field.child_invalid: - return True - return False - - def get_invalid(self): - output = [] - for input_field in self.input_fields: - output.extend(input_field.get_invalid()) - return output - - def item_value(self): - output = {} - for input_field in self.input_fields: - # TODO maybe merge instead of update should be used - # NOTE merge is custom function which merges 2 dicts - output.update(input_field.config_value()) - return output - - def _override_values(self, project_overrides): - values = {} - groups = [] - for input_field in self.input_fields: - if project_overrides: - value, is_group = input_field.overrides() - else: - value, is_group = input_field.studio_overrides() - if value is NOT_SET: - continue - - if METADATA_KEY in value and METADATA_KEY in values: - new_metadata = value.pop(METADATA_KEY) - values[METADATA_KEY] = self.merge_metadata( - values[METADATA_KEY], new_metadata - ) - - values.update(value) - if is_group: - groups.extend(value.keys()) - - if groups: - if METADATA_KEY not in values: - values[METADATA_KEY] = {} - values[METADATA_KEY]["groups"] = groups - return {self.key: values}, self.is_group - - def studio_overrides(self): - if ( - not (self.as_widget or self.any_parent_as_widget) - and not self.has_studio_override - and not self.child_has_studio_override - ): - return NOT_SET, False - return self._override_values(False) - - def overrides(self): - if not self.is_overriden and not self.child_overriden: - return NOT_SET, False - return self._override_values(True) - - -class PathWidget(QtWidgets.QWidget, SettingObject): - value_changed = QtCore.Signal(object) - platforms = ("windows", "darwin", "linux") - platform_labels_mapping = { - "windows": "Windows", - "darwin": "MacOS", - "linux": "Linux" - } - - def __init__( - self, schema_data, parent, as_widget=False, parent_widget=None - ): - if parent_widget is None: - parent_widget = parent - super(PathWidget, self).__init__(parent_widget) - - self.initial_attributes(schema_data, parent, as_widget) - - # This is partial input and dictionary input - if not self.any_parent_is_group and not self._as_widget: - self._is_group = True - else: - self._is_group = False - - self.multiplatform = schema_data.get("multiplatform", False) - self.multipath = schema_data.get("multipath", False) - self.with_arguments = schema_data.get("with_arguments", False) - - self.input_field = None - - def create_ui(self, label_widget=None): - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(5) - - if not self.as_widget and not label_widget: - label_widget = QtWidgets.QLabel(self.schema_data["label"]) - label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) - layout.addWidget(label_widget, 0, alignment=QtCore.Qt.AlignTop) - self.label_widget = label_widget - - self.content_widget = QtWidgets.QWidget(self) - self.content_layout = QtWidgets.QVBoxLayout(self.content_widget) - self.content_layout.setSpacing(0) - self.content_layout.setContentsMargins(0, 0, 0, 0) - - layout.addWidget(self.content_widget) - - self.create_ui_inputs() - - @property - def default_input_value(self): - if self.multipath: - value_type = list - else: - value_type = str - - if self.multiplatform: - return { - platform: value_type() - for platform in self.platforms - } - return value_type() - - def create_ui_inputs(self): - if not self.multiplatform and not self.multipath: - item_schema = { - "key": self.key, - "with_arguments": self.with_arguments - } - path_input = PathInputWidget(item_schema, self, as_widget=True) - path_input.create_ui(label_widget=self.label_widget) - - self.setFocusProxy(path_input) - self.content_layout.addWidget(path_input) - self.input_field = path_input - path_input.value_changed.connect(self._on_value_change) - return - - if not self.multiplatform: - item_schema = { - "key": self.key, - "object_type": { - "type": "path-input", - "with_arguments": self.with_arguments - } - } - input_widget = ListWidget(item_schema, self, as_widget=True) - input_widget.create_ui(label_widget=self.label_widget) - self.setFocusProxy(input_widget) - self.content_layout.addWidget(input_widget) - self.input_field = input_widget - input_widget.value_changed.connect(self._on_value_change) - return - - item_schema = { - "type": "dict", - "show_borders": False, - "children": [] - } - for platform_key in self.platforms: - platform_label = self.platform_labels_mapping[platform_key] - child_item = { - "key": platform_key, - "label": platform_label - } - if self.multipath: - child_item["type"] = "list" - child_item["object_type"] = { - "type": "path-input", - "with_arguments": self.with_arguments - } - else: - child_item["type"] = "path-input" - child_item["with_arguments"] = self.with_arguments - - item_schema["children"].append(child_item) - - input_widget = DictWidget(item_schema, self, as_widget=True) - input_widget.create_ui(label_widget=self.label_widget) - - self.content_layout.addWidget(input_widget) - self.input_field = input_widget - input_widget.value_changed.connect(self._on_value_change) - - def update_default_values(self, parent_values): - self._state = None - self._child_state = None - self._is_modified = False - - value = NOT_SET - if self.as_widget: - value = parent_values - elif parent_values is not NOT_SET: - value = parent_values.get(self.key, NOT_SET) - - if value is NOT_SET: - if self.available_for_role("developer"): - self.defaults_not_set = True - value = self.default_input_value - if value is NOT_SET: - raise NotImplementedError(( - "{} Does not have implemented" - " attribute `default_input_value`" - ).format(self)) - - else: - raise ValueError( - "Default value is not set. This is implementation BUG." - ) - else: - self.defaults_not_set = False - - self.default_value = value - self._has_studio_override = False - self._had_studio_override = False - - # TODO handle invalid value type - self.input_field.update_default_values(value) - - def update_studio_values(self, parent_values): - self._state = None - self._child_state = None - self._is_modified = False - - value = NOT_SET - if self.as_widget: - value = parent_values - elif parent_values is not NOT_SET: - value = parent_values.get(self.key, NOT_SET) - - self.studio_value = value - if value is not NOT_SET: - self._has_studio_override = True - self._had_studio_override = True - else: - self._has_studio_override = False - self._had_studio_override = False - - # TODO handle invalid value type - self.input_field.update_studio_values(value) - - def apply_overrides(self, parent_values): - self._is_modified = False - self._state = None - self._child_state = None - - override_values = NOT_SET - if self._as_widget: - override_values = parent_values - elif parent_values is not NOT_SET: - override_values = parent_values.get(self.key, NOT_SET) - - self._is_overriden = override_values is not NOT_SET - self._was_overriden = bool(self._is_overriden) - - # TODO handle invalid value type - self.input_field.update_studio_values(override_values) - - if not self._is_overriden: - self._is_overriden = ( - self.is_group - and self.is_overidable - and self.child_overriden - ) - self._is_modified = False - self._was_overriden = bool(self._is_overriden) - - def set_value(self, value): - if not self.multiplatform: - return self.input_field.set_value(value) - - for _input_field in self.input_field.input_fields: - _value = value.get(_input_field.key, NOT_SET) - if _value is NOT_SET: - continue - _input_field.set_value(_value) - - def _on_value_change(self, item=None): - if self.ignore_value_changes: - return - - if not self.any_parent_as_widget: - if self.is_overidable: - self._is_overriden = True - else: - self._has_studio_override = True - - if self._is_invalid: - self._is_modified = True - elif self._is_overriden: - self._is_modified = self.item_value() != self.override_value - elif self._has_studio_override: - self._is_modified = self.item_value() != self.studio_value - else: - self._is_modified = self.item_value() != self.default_value - - self.hierarchical_style_update() - - self.value_changed.emit(self) - - def update_style(self, is_overriden=None): - child_has_studio_override = self.child_has_studio_override - child_modified = self.child_modified - child_invalid = self.child_invalid - child_state = self.style_state( - child_has_studio_override, - child_invalid, - self.child_overriden, - child_modified - ) - if child_state: - child_state = "child-{}".format(child_state) - - if child_state != self._child_state: - self.setProperty("state", child_state) - self.style().polish(self) - self._child_state = child_state - - if self.label_widget: - state = self.style_state( - child_has_studio_override, - child_invalid, - self.is_overriden, - self.is_modified - ) - if self._state == state: - return - - self.label_widget.setProperty("state", state) - self.label_widget.style().polish(self.label_widget) - - self._state = state - - def remove_overrides(self): - self._is_overriden = False - self._is_modified = False - self.input_field.remove_overrides() - - def reset_to_pype_default(self): - self.input_field.reset_to_pype_default() - self._has_studio_override = False - - def set_studio_default(self): - self.input_field.set_studio_default() - - if self.is_group: - self._has_studio_override = True - - def discard_changes(self): - self._is_modified = False - self._is_overriden = self._was_overriden - self._has_studio_override = self._had_studio_override - - self.input_field.discard_changes() - - self._is_modified = self.child_modified - if not self.is_overidable and self.as_widget: - if self.has_studio_override: - self._is_modified = self.studio_value != self.item_value() - else: - self._is_modified = self.default_value != self.item_value() - - self._state = None - self._is_overriden = self._was_overriden - - def set_as_overriden(self): - self._is_overriden = True - - @property - def child_has_studio_override(self): - return self.has_studio_override - - @property - def child_modified(self): - return self.is_modified - - @property - def child_overriden(self): - return self.is_overriden - - @property - def child_invalid(self): - return self.input_field.child_invalid - - def hierarchical_style_update(self): - self.input_field.hierarchical_style_update() - self.update_style() - - def item_value(self): - return self.input_field.item_value() - - def studio_overrides(self): - if ( - not (self.as_widget or self.any_parent_as_widget) - and not self.has_studio_override - and not self.child_has_studio_override - ): - return NOT_SET, False - - value = {self.key: self.item_value()} - return value, self.is_group - - def overrides(self): - if not self.is_overriden and not self.child_overriden: - return NOT_SET, False - - value = {self.key: self.item_value()} - return value, self.is_group - - -class WrapperItemWidget(QtWidgets.QWidget, SettingObject): - value_changed = QtCore.Signal(object) - allow_actions = False - expand_in_grid = True - is_wrapper_item = True - - def __init__( - self, schema_data, parent, as_widget=False, parent_widget=None - ): - if parent_widget is None: - parent_widget = parent - super(WrapperItemWidget, self).__init__(parent_widget) - - self.input_fields = [] - - self.initial_attributes(schema_data, parent, as_widget) - - if self.as_widget: - raise TypeError( - "Wrapper items ({}) can't be used as widgets.".format( - self.__class__.__name__ - ) - ) - - if self.is_group: - raise TypeError( - "Wrapper items ({}) can't be used as groups.".format( - self.__class__.__name__ - ) - ) - - self.setAttribute(QtCore.Qt.WA_TranslucentBackground) - - self.wrapper_initial_attributes(schema_data) - - def wrapper_initial_attributes(self, schema_data): - """Initialization of attributes for specific wrapper.""" - return - - def create_ui(self, label_widget=None): - """UI implementation.""" - raise NotImplementedError( - "Method `create_ui` not implemented." - ) - - def update_style(self): - """Update items styles.""" - return - - def apply_overrides(self, parent_values): - for item in self.input_fields: - item.apply_overrides(parent_values) - - def discard_changes(self): - self._is_modified = False - self._is_overriden = self._was_overriden - self._has_studio_override = self._had_studio_override - - for input_field in self.input_fields: - input_field.discard_changes() - - self._is_modified = self.child_modified - if not self.is_overidable and self.as_widget: - if self.has_studio_override: - self._is_modified = self.studio_value != self.item_value() - else: - self._is_modified = self.default_value != self.item_value() - - self._state = None - self._is_overriden = self._was_overriden - - def remove_overrides(self): - self._is_overriden = False - self._is_modified = False - for input_field in self.input_fields: - input_field.remove_overrides() - - def reset_to_pype_default(self): - for input_field in self.input_fields: - input_field.reset_to_pype_default() - self._has_studio_override = False - - def set_studio_default(self): - for input_field in self.input_fields: - input_field.set_studio_default() - - if self.is_group: - self._has_studio_override = True - - def set_as_overriden(self): - if self.is_overriden: - return - - if self.is_group: - self._is_overriden = True - return - - for item in self.input_fields: - item.set_as_overriden() - - def update_default_values(self, value): - for item in self.input_fields: - item.update_default_values(value) - - def update_studio_values(self, value): - for item in self.input_fields: - item.update_studio_values(value) - - def _on_value_change(self, item=None): - if self.ignore_value_changes: - return - - self.value_changed.emit(self) - if self.any_parent_is_group: - self.hierarchical_style_update() - self.update_style() - - @property - def child_has_studio_override(self): - for input_field in self.input_fields: - if ( - input_field.has_studio_override - or input_field.child_has_studio_override - ): - return True - return False - - @property - def child_modified(self): - for input_field in self.input_fields: - if input_field.child_modified: - return True - return False - - @property - def child_overriden(self): - for input_field in self.input_fields: - if input_field.is_overriden or input_field.child_overriden: - return True - return False - - @property - def child_invalid(self): - for input_field in self.input_fields: - if input_field.child_invalid: - return True - return False - - def get_invalid(self): - output = [] - for input_field in self.input_fields: - output.extend(input_field.get_invalid()) - return output - - def hierarchical_style_update(self): - for input_field in self.input_fields: - input_field.hierarchical_style_update() - self.update_style() - - def item_value(self): - output = {} - for input_field in self.input_fields: - # TODO maybe merge instead of update should be used - # NOTE merge is custom function which merges 2 dicts - output.update(input_field.config_value()) - return output - - def config_value(self): - return self.item_value() - - def studio_overrides(self): - if ( - not (self.as_widget or self.any_parent_as_widget) - and not self.has_studio_override - and not self.child_has_studio_override - ): - return NOT_SET, False - - values = {} - groups = [] - for input_field in self.input_fields: - value, is_group = input_field.studio_overrides() - if value is not NOT_SET: - values.update(value) - if is_group: - groups.extend(value.keys()) - if groups: - if METADATA_KEY not in values: - values[METADATA_KEY] = {} - values[METADATA_KEY]["groups"] = groups - return values, self.is_group - - def overrides(self): - if not self.is_overriden and not self.child_overriden: - return NOT_SET, False - - values = {} - groups = [] - for input_field in self.input_fields: - value, is_group = input_field.overrides() - if value is not NOT_SET: - values.update(value) - if is_group: - groups.extend(value.keys()) - if groups: - if METADATA_KEY not in values: - values[METADATA_KEY] = {} - values[METADATA_KEY]["groups"] = groups - return values, self.is_group - - -# Proxy for form layout -class FormLabel(QtWidgets.QLabel): - def __init__(self, input_field, *args, **kwargs): - super(FormLabel, self).__init__(*args, **kwargs) - self.input_field = input_field - - def mouseReleaseEvent(self, event): - if self.input_field: - return self.input_field.show_actions_menu(event) - return super(FormLabel, self).mouseReleaseEvent(event) - - -class FormItemWidget(WrapperItemWidget): - def create_ui(self, label_widget=None): - self.content_layout = QtWidgets.QFormLayout(self) - self.content_layout.setContentsMargins(0, 0, 0, 0) - - for child_data in self.schema_data["children"]: - self.add_children_gui(child_data) - - any_visible = False - for input_field in self.input_fields: - if not input_field.hidden_by_role: - any_visible = True - break - - if not any_visible: - self.hidden_by_role = True - self.hide() - - def add_children_gui(self, child_configuration): - item_type = child_configuration["type"] - # Pop label to not be set in child - label = child_configuration["label"] - - klass = TypeToKlass.types.get(item_type) - - item = klass(child_configuration, self) - - label_widget = FormLabel(item, label, self) - - item.create_ui(label_widget=label_widget) - - if item.hidden_by_role: - label_widget.hide() - - item.value_changed.connect(self._on_value_change) - self.content_layout.addRow(label_widget, item) - self.input_fields.append(item) - return item - - -class CollapsibleWrapperItem(WrapperItemWidget): - def wrapper_initial_attributes(self, schema_data): - self.collapsable = schema_data.get("collapsable", True) - self.collapsed = schema_data.get("collapsed", True) - - def create_ui(self, label_widget=None): - content_widget = QtWidgets.QWidget(self) - content_widget.setObjectName("ContentWidget") - content_widget.setProperty("content_state", "") - - content_layout = QtWidgets.QGridLayout(content_widget) - content_layout.setContentsMargins(CHILD_OFFSET, 5, 0, 0) - - body_widget = ExpandingWidget(self.schema_data["label"], self) - body_widget.set_content_widget(content_widget) - - label_widget = body_widget.label_widget - - main_layout = QtWidgets.QHBoxLayout(self) - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.setSpacing(0) - if not body_widget: - main_layout.addWidget(content_widget) - else: - main_layout.addWidget(body_widget) - - self.label_widget = label_widget - self.body_widget = body_widget - self.content_layout = content_layout - - if self.collapsable: - if not self.collapsed: - body_widget.toggle_content() - else: - body_widget.hide_toolbox(hide_content=False) - - for child_data in self.schema_data.get("children", []): - self.add_children_gui(child_data) - - any_visible = False - for input_field in self.input_fields: - if not input_field.hidden_by_role: - any_visible = True - break - - if not any_visible: - self.hide() - - def add_children_gui(self, child_configuration): - item_type = child_configuration["type"] - klass = TypeToKlass.types.get(item_type) - - row = self.content_layout.rowCount() - if not getattr(klass, "is_input_type", False): - item = klass(child_configuration, self) - self.content_layout.addWidget(item, row, 0, 1, 2) - return item - - label_widget = None - item = klass(child_configuration, self) - if not item.expand_in_grid: - label = child_configuration.get("label") - if label is not None: - label_widget = GridLabelWidget(label, self) - self.content_layout.addWidget(label_widget, row, 0, 1, 1) - - item.create_ui(label_widget=label_widget) - item.value_changed.connect(self._on_value_change) - - if label_widget: - if item.hidden_by_role: - label_widget.hide() - label_widget.input_field = item - self.content_layout.addWidget(item, row, 1, 1, 1) - else: - self.content_layout.addWidget(item, row, 0, 1, 2) - - self.input_fields.append(item) - return item - - def update_style(self, is_overriden=None): - child_has_studio_override = self.child_has_studio_override - child_modified = self.child_modified - child_invalid = self.child_invalid - child_state = self.style_state( - child_has_studio_override, - child_invalid, - self.child_overriden, - child_modified - ) - if child_state: - child_state = "child-{}".format(child_state) - - if child_state != self._child_state: - self.body_widget.side_line_widget.setProperty("state", child_state) - self.body_widget.side_line_widget.style().polish( - self.body_widget.side_line_widget - ) - self._child_state = child_state - - state = self.style_state( - self.had_studio_override, - child_invalid, - self.is_overriden, - self.is_modified - ) - if self._state == state: - return - - self.label_widget.setProperty("state", state) - self.label_widget.style().polish(self.label_widget) - - self._state = state - - -class LabelWidget(QtWidgets.QWidget): - is_input_type = False - - def __init__(self, configuration, parent): - super(LabelWidget, self).__init__(parent) - self.setObjectName("LabelWidget") - - label = configuration["label"] - - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 5, 0, 5) - label_widget = QtWidgets.QLabel(label, self) - layout.addWidget(label_widget) - - # Role handling - roles = configuration.get("roles") - if roles is not None and not isinstance(roles, list): - roles = [roles] - - if roles and parent.user_role not in roles: - self.hide() - self.hidden_by_role = True - else: - self.hidden_by_role = False - - -class SplitterWidget(QtWidgets.QWidget): - is_input_type = False - _height = 2 - - def __init__(self, configuration, parent): - super(SplitterWidget, self).__init__(parent) - - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(5, 5, 5, 5) - splitter_item = QtWidgets.QWidget(self) - splitter_item.setObjectName("SplitterItem") - splitter_item.setMinimumHeight(self._height) - splitter_item.setMaximumHeight(self._height) - layout.addWidget(splitter_item) - - # Role handling - roles = configuration.get("roles") - if roles is not None and not isinstance(roles, list): - roles = [roles] - - if roles and parent.user_role not in roles: - self.hide() - self.hidden_by_role = True - else: - self.hidden_by_role = False - - -TypeToKlass.types["boolean"] = BooleanWidget -TypeToKlass.types["number"] = NumberWidget -TypeToKlass.types["text"] = TextWidget -TypeToKlass.types["path-input"] = PathInputWidget -TypeToKlass.types["raw-json"] = RawJsonWidget -TypeToKlass.types["list"] = ListWidget -TypeToKlass.types["list-strict"] = ListStrictWidget -TypeToKlass.types["enum"] = EnumeratorWidget -TypeToKlass.types["dict-modifiable"] = ModifiableDict -# DEPRECATED - remove when removed from schemas -TypeToKlass.types["splitter"] = SplitterWidget -TypeToKlass.types["dict-item"] = DictWidget -# --------------------------------------------- -TypeToKlass.types["dict"] = DictWidget -TypeToKlass.types["path-widget"] = PathWidget - -# Wrappers -TypeToKlass.types["form"] = FormItemWidget -TypeToKlass.types["collapsible-wrap"] = CollapsibleWrapperItem - -# UI items -TypeToKlass.types["label"] = LabelWidget -TypeToKlass.types["separator"] = SplitterWidget diff --git a/pype/tools/settings/settings/widgets/item_widgets.py b/pype/tools/settings/settings/widgets/item_widgets.py index 6f8441581d..fd33f337d7 100644 --- a/pype/tools/settings/settings/widgets/item_widgets.py +++ b/pype/tools/settings/settings/widgets/item_widgets.py @@ -45,7 +45,9 @@ class DictImmutableKeysWidget(BaseWidget): for child_obj in self.entity.children: self.input_fields.append( - self.create_ui_for_entity(child_obj, self) + self.create_ui_for_entity( + self.category_widget, child_obj, self + ) ) self.entity_widget.add_widget_to_layout(self) @@ -87,7 +89,7 @@ class DictImmutableKeysWidget(BaseWidget): if self.entity.is_dynamic_item: content_widget.setObjectName("DictAsWidgetBody") - show_borders = str(int(self.show_borders)) + show_borders = str(int(self.entity.show_borders)) content_widget.setProperty("show_borders", show_borders) content_layout_margins = (5, 5, 5, 5) main_layout_spacing = 5 @@ -231,10 +233,10 @@ class DictImmutableKeysWidget(BaseWidget): @property def is_invalid(self): - return self._is_invalid or self.child_invalid + return self._is_invalid or self._child_invalid @property - def child_invalid(self): + def _child_invalid(self): for input_field in self.input_fields: if input_field.is_invalid: return True @@ -508,7 +510,7 @@ class PathWidget(BaseWidget): self.content_layout.setSpacing(5) self.input_field = self.create_ui_for_entity( - self.entity.child_obj, self + self.category_widget, self.entity.child_obj, self ) self.entity_widget.add_widget_to_layout(self, self.entity.label) diff --git a/pype/tools/settings/settings/widgets/list_item_widget.py b/pype/tools/settings/settings/widgets/list_item_widget.py index 07de7da112..699669abab 100644 --- a/pype/tools/settings/settings/widgets/list_item_widget.py +++ b/pype/tools/settings/settings/widgets/list_item_widget.py @@ -54,7 +54,6 @@ class ListItem(QtWidgets.QWidget): self.entity = entity self.ignore_input_changes = entity_widget.ignore_input_changes - self.create_ui_for_entity = entity_widget.create_ui_for_entity char_up = qtawesome.charmap("fa.angle-up") char_down = qtawesome.charmap("fa.angle-down") @@ -98,7 +97,9 @@ class ListItem(QtWidgets.QWidget): self.content_widget = self self.content_layout = layout - self.input_field = self.create_ui_for_entity(self.entity, self) + self.input_field = self.create_ui_for_entity( + self.category_widget, self.entity, self + ) self.input_field.set_entity_value() spacer_widget = QtWidgets.QWidget(self) @@ -117,6 +118,15 @@ class ListItem(QtWidgets.QWidget): self.spacer_widget = spacer_widget + @property + def category_widget(self): + return self.entity_widget.category_widget + + def create_ui_for_entity(self, *args, **kwargs): + return self.entity_widget.create_ui_for_entity( + *args, **kwargs + ) + @property def is_invalid(self): return self.input_field.is_invalid @@ -253,36 +263,35 @@ class ListWidget(InputWidget): def _on_entity_change(self): # TODO do less inefficient - current_entities = [] - for input_field in self.input_fields: - current_entities.append(input_field.entity) - - for idx, child_entity in enumerate(self.entity): - found = False - for input_field in self.input_fields: - if input_field.entity is child_entity: - found = True - break - - if not found: - self.add_row(child_entity, idx) - + input_field_last_idx = len(self.input_fields) - 1 child_len = len(self.entity) - for idx, child_entity in enumerate(tuple(self.entity)): + for idx, child_entity in enumerate(self.entity): + if idx > input_field_last_idx: + self.add_row(child_entity, idx) + input_field_last_idx += 1 + continue + if self.input_fields[idx].entity is child_entity: continue - for _idx in range(idx, child_len): - input_field = self.input_fields[_idx] - if input_field.entity is not child_entity: - continue + input_field_idx = None + for _input_field_idx, input_field in enumerate(self.input_fields): + if input_field.entity is child_entity: + input_field_idx = _input_field_idx + break - self.content_layout.insertWidget(idx, input_field) - break + if input_field_idx is None: + self.add_row(child_entity, idx) + input_field_last_idx += 1 + continue - input_field_len = len(self.input_fields) - if child_len != input_field_len: - for _idx in range(child_len, input_field_len): + input_field = self.input_fields.pop(input_field_idx) + self.input_fields.insert(idx, input_field) + self.content_layout.insertWidget(idx, input_field) + + new_input_field_len = len(self.input_fields) + if child_len != new_input_field_len: + for _idx in range(child_len, new_input_field_len): # Remove row at the same index self.remove_row(self.input_fields[child_len]) @@ -295,20 +304,7 @@ class ListWidget(InputWidget): if row_1 == row_2: return - if row_1 > row_2: - row_1, row_2 = row_2, row_1 - - field_1 = self.input_fields[row_1] - field_2 = self.input_fields[row_2] - - self.input_fields[row_1] = field_2 - self.input_fields[row_2] = field_1 - - layout_index = self.content_layout.indexOf(field_1) - self.content_layout.insertWidget(layout_index + 1, field_1) - - field_1.order_changed() - field_2.order_changed() + self.entity.swap_indexes(row_1, row_2) def add_new_item(self, row=None): new_entity = self.entity.add_new_item(row) @@ -338,7 +334,7 @@ class ListWidget(InputWidget): if row < max_index: next_field = self.input_fields[row] - self.content_layout.insertWidget(row, item_widget) + self.content_layout.insertWidget(row + 1, item_widget) self.input_fields.insert(row, item_widget) if previous_field: diff --git a/pype/tools/settings/settings/widgets/list_strict_widget.py b/pype/tools/settings/settings/widgets/list_strict_widget.py new file mode 100644 index 0000000000..340db2e8c6 --- /dev/null +++ b/pype/tools/settings/settings/widgets/list_strict_widget.py @@ -0,0 +1,142 @@ +from Qt import QtWidgets, QtCore + +from .widgets import ( + GridLabelWidget, + SpacerWidget +) +from .base import BaseWidget + + +class ListStrictWidget(BaseWidget): + def create_ui(self): + self.setObjectName("ListStrictWidget") + + self._child_style_state = "" + self.input_fields = [] + + content_layout = QtWidgets.QGridLayout(self) + content_layout.setContentsMargins(0, 0, 0, 0) + content_layout.setSpacing(3) + + self.content_layout = content_layout + self.content_widget = self + + any_children_has_label = False + for child_obj in self.entity.children: + if child_obj.label: + any_children_has_label = True + break + + self._any_children_has_label = any_children_has_label + # Change column stretch factor for verticall alignment + if not self.entity.is_horizontal: + col_index = 2 if any_children_has_label else 1 + content_layout.setColumnStretch(col_index, 1) + + for child_obj in self.entity.children: + self.input_fields.append( + self.create_ui_for_entity( + self.category_widget, child_obj, self + ) + ) + + if self.entity.is_horizontal: + col = self.content_layout.columnCount() + spacer = SpacerWidget(self) + self.content_layout.addWidget(spacer, 0, col, 2, 1) + self.content_layout.setColumnStretch(col, 1) + + self.entity_widget.add_widget_to_layout(self, self.entity.label) + + @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 + + def add_widget_to_layout(self, widget, label=None): + # Horizontally added children + if self.entity.is_horizontal: + self._add_child_horizontally(widget, label) + else: + self._add_child_vertically(widget, label) + + self.updateGeometry() + + def _add_child_horizontally(self, widget, label): + col = self.content_layout.columnCount() + # Expand to whole grid if all children are without label + if not self._any_children_has_label: + self.content_layout.addWidget(widget, 0, col, 1, 2) + else: + if label: + label_widget = GridLabelWidget(label, widget) + label_widget.input_field = widget + widget.label_widget = label_widget + self.content_layout.addWidget(label_widget, 0, col, 1, 1) + col += 1 + self.content_layout.addWidget(widget, 0, col, 1, 1) + + def _add_child_vertically(self, widget, label): + row = self.content_layout.rowCount() + if not self._any_children_has_label: + self.content_layout.addWidget(widget, row, 0, 1, 1) + + spacer_widget = SpacerWidget(self) + self.content_layout.addWidget(spacer_widget, row, 1, 1, 1) + + else: + if label: + label_widget = GridLabelWidget(label, widget) + label_widget.input_field = widget + widget.label_widget = label_widget + self.content_layout.addWidget( + label_widget, row, 0, 1, 1, + alignment=QtCore.Qt.AlignRight | QtCore.Qt.AlignTop + ) + self.content_layout.addWidget(widget, row, 1, 1, 1) + + spacer_widget = SpacerWidget(self) + self.content_layout.addWidget(spacer_widget, row, 2, 1, 1) + + def hierarchical_style_update(self): + self.update_style() + for input_field in self.input_fields: + input_field.hierarchical_style_update() + + def set_entity_value(self): + for input_field in self.input_fields: + input_field.set_entity_value() + + def _on_entity_change(self): + pass + + def update_style(self): + if not self.label_widget: + return + + style_state = self.get_style_state( + self.is_invalid, + self.entity.has_unsaved_changes, + self.entity.has_project_override, + self.entity.has_studio_override + ) + + if self._style_state == style_state: + return + + self.label_widget.setProperty("state", style_state) + self.label_widget.style().polish(self.label_widget) + + self._style_state = style_state diff --git a/pype/tools/settings/settings/widgets/widgets.py b/pype/tools/settings/settings/widgets/widgets.py index 3a7cbfddf9..fd8d9d753c 100644 --- a/pype/tools/settings/settings/widgets/widgets.py +++ b/pype/tools/settings/settings/widgets/widgets.py @@ -1,5 +1,12 @@ +import os from Qt import QtWidgets, QtCore, QtGui from avalon.vendor import qtawesome +from avalon.mongodb import ( + AvalonMongoConnection, + AvalonMongoDB +) + +from pype.settings.lib import get_system_settings class ShadowWidget(QtWidgets.QWidget): @@ -523,3 +530,148 @@ class NiceCheckbox(QtWidgets.QFrame): event.accept() return return super(NiceCheckbox, self).mouseReleaseEvent(event) + + +class ProjectListView(QtWidgets.QListView): + left_mouse_released_at = QtCore.Signal(QtCore.QModelIndex) + + def mouseReleaseEvent(self, event): + if event.button() == QtCore.Qt.LeftButton: + index = self.indexAt(event.pos()) + self.left_mouse_released_at.emit(index) + super(ProjectListView, self).mouseReleaseEvent(event) + + +class ProjectListWidget(QtWidgets.QWidget): + default = "< Default >" + project_changed = QtCore.Signal() + + def __init__(self, parent): + self._parent = parent + + self.current_project = None + + super(ProjectListWidget, self).__init__(parent) + self.setObjectName("ProjectListWidget") + + label_widget = QtWidgets.QLabel("Projects") + project_list = ProjectListView(self) + project_list.setModel(QtGui.QStandardItemModel()) + + # Do not allow editing + project_list.setEditTriggers( + QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers + ) + # Do not automatically handle selection + project_list.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) + + layout = QtWidgets.QVBoxLayout(self) + layout.setSpacing(3) + layout.addWidget(label_widget, 0) + layout.addWidget(project_list, 1) + + project_list.left_mouse_released_at.connect(self.on_item_clicked) + + self.project_list = project_list + + self.dbcon = None + + def on_item_clicked(self, new_index): + new_project_name = new_index.data(QtCore.Qt.DisplayRole) + if new_project_name is None: + return + + if self.current_project == new_project_name: + return + + save_changes = False + change_project = False + if self.validate_context_change(): + change_project = True + + else: + dialog = UnsavedChangesDialog(self) + result = dialog.exec_() + if result == 1: + save_changes = True + change_project = True + + elif result == 2: + change_project = True + + if save_changes: + self._parent._save() + + if change_project: + self.select_project(new_project_name) + self.current_project = new_project_name + self.project_changed.emit() + else: + self.select_project(self.current_project) + + def validate_context_change(self): + return not self._parent.entity.has_unsaved_changes + + def project_name(self): + if self.current_project == self.default: + return None + return self.current_project + + def select_default_project(self): + self.select_project(self.default) + + def select_project(self, project_name): + model = self.project_list.model() + found_items = model.findItems(project_name) + if not found_items: + found_items = model.findItems(self.default) + + index = model.indexFromItem(found_items[0]) + self.project_list.selectionModel().clear() + self.project_list.selectionModel().setCurrentIndex( + index, QtCore.QItemSelectionModel.SelectionFlag.SelectCurrent + ) + + def refresh(self): + selected_project = None + for index in self.project_list.selectedIndexes(): + selected_project = index.data(QtCore.Qt.DisplayRole) + break + + model = self.project_list.model() + model.clear() + + items = [self.default] + + system_settings = get_system_settings() + mongo_url = system_settings["modules"]["avalon"]["AVALON_MONGO"] + if not mongo_url: + mongo_url = os.environ["PYPE_MONGO"] + + # Force uninstall of whole avalon connection if url does not match + # to current environment and set it as environment + if mongo_url != os.environ["AVALON_MONGO"]: + AvalonMongoConnection.uninstall(self.dbcon, force=True) + os.environ["AVALON_MONGO"] = mongo_url + self.dbcon = None + + if not self.dbcon: + try: + self.dbcon = AvalonMongoDB() + self.dbcon.install() + except Exception: + self.dbcon = None + self.current_project = None + + if self.dbcon: + for project_doc in tuple(self.dbcon.projects()): + items.append(project_doc["name"]) + + for item in items: + model.appendRow(QtGui.QStandardItem(item)) + + self.select_project(selected_project) + + self.current_project = self.project_list.currentIndex().data( + QtCore.Qt.DisplayRole + ) diff --git a/pype/tools/settings/settings/widgets/wrapper_widgets.py b/pype/tools/settings/settings/widgets/wrapper_widgets.py index 790fb81d87..9d5fdeb213 100644 --- a/pype/tools/settings/settings/widgets/wrapper_widgets.py +++ b/pype/tools/settings/settings/widgets/wrapper_widgets.py @@ -56,7 +56,7 @@ class FormWrapper(WrapperWidget): class CollapsibleWrapper(WrapperWidget): def create_ui(self): - self.collapsible = self.schema_data.get("collapsable", True) + self.collapsible = self.schema_data.get("collapsible", True) self.collapsed = self.schema_data.get("collapsed", True) content_widget = QtWidgets.QWidget(self)