From 3534faebd4b6dffaa7aa08df70afcb61e2ac173c Mon Sep 17 00:00:00 2001 From: Demeter Dzadik Date: Tue, 6 Aug 2024 10:44:44 +0200 Subject: [PATCH 01/10] New Props UI WIP: Data structure & drawing --- generation/cloudrig.py | 252 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 252 insertions(+) diff --git a/generation/cloudrig.py b/generation/cloudrig.py index b9ace57..80a2edf 100644 --- a/generation/cloudrig.py +++ b/generation/cloudrig.py @@ -14,6 +14,7 @@ from bpy.props import ( EnumProperty, PointerProperty, IntProperty, + CollectionProperty, ) from bpy.types import ( bpy_struct, @@ -1426,6 +1427,254 @@ def unquote_custom_prop_name(prop_name: str) -> str: return prop_name +class CloudRig_UIElement(PropertyGroup): + element_type: EnumProperty( + name="Element Type", + description="How this UI element is drawn", + items=[ + ('PANEL', "Panel", "Collapsible panel. May contain Panels, Labels, Rows"), + ('LABEL', "Label", "Label. May contain Panels, Labels, Rows"), + ('ROW', "Row", "Grouping for elements that allow multiple per row. Must be used for such elements, even if there is only one in the row. May contain Rows, Properties, and Operators"), + ('PROPERTY', "Property", "A single Property. Must belong to a Row. May contain conditional Panels, Labels, Rows"), + ('OPERATOR', "Operator", "A single Operator. Must belong to a Row"), + ], + ) + + display_name: StringProperty( + name="Display Name", + description="(Optional) Display name of this UI element", + default="", + ) + # NOTE: This needs to be updated when elements are removed. + parent_index: IntProperty( + # Supported Types: Panel, Label, Row. + # TODO: Deletion will need to treat this carefully! + name="Parent Index", + description="Index of the parent UI element", + default=-1, + ) + @property + def parent(self): + if self.parent_index >= 0: + return self.id_data.cloudrig_ui[self.parent_index] + @parent.setter + def parent(self, value): + self.parent_index = value.index + + @property + def index(self): + for i, elem in enumerate(self.id_data.cloudrig_ui): + if elem == self: + return i + + parent_values: StringProperty( + # Supported Types: Panel, Label, Row, only when Element Type of parent element is Property. + name="Parent Values", + description="Condition for this UI element to be drawn, when its parent is a Property. This UI element will only be drawn if the parent property has one of these comma-separated values" + ) + + texts: StringProperty( + # Supported Types: Property, only Boolean and Integer. + name="Texts", + description="Comma-separated display texts for Integer and Boolean Properties" + ) + + bl_idname: StringProperty( + # Supported Types: Operator + name="Operator ID", + description="Operator bl_idname" + ) + op_kwargs: StringProperty( + # Supported Types: Operator + name="Operator Arguments", + description="Operator Keyword Arguments, as a json dict", + default="{}" + ) + icon: StringProperty( + # Supported Types: Label, Row, Property(bool), Operator + name="Icon", + description="Icon" + ) + icon_false: StringProperty( + # Supported Types: Property(bool) + name="Icon False", + description="Icon to display when this boolean property is False" + ) + + prop_owner_path: StringProperty( + # Supported Types: Property + name="Property Owner", + description="Data Path from the rig object to the direct owner of the property to be drawn" + ) + @property + def prop_owner(self): + try: + return self.id_data.path_resolve(self.prop_owner_path) + except ValueError: + # This can happen eg. if user adds a constraint influence to the UI, then deletes the constraint. + return + + prop_name: StringProperty( + # Supported Types: Property + name="Property Name", + description="Name of the property to be drawn" + ) + @property + def bracketed_prop_name(self): + if self.prop_is_custom: + return f'["{self.prop_name}"]' + return self.prop_name + @property + def prop_value(self): + if not hasattr(self.prop_owner, 'path_resolve'): + print("cloudrig.py: Cannot resolve path from: ", self.prop_owner) + return + try: + return self.prop_owner.path_resolve(self.bracketed_prop_name) + except ValueError: + # Property may have been removed. + return {'MISSING'} + + prop_is_custom: BoolProperty( + # Supported Types: Property + name="Is Custom Property", + description="Whether this is a custom or a built-in property. Set automatically" + ) + @property + def custom_prop_settings(self): + if not self.prop_is_custom: + return + try: + return self.prop_owner.id_properties_ui(self.prop_name).as_dict() + except TypeError: + # This happens for Python properties. There's no point drawing them. + return + + @property + def children(self): + return [elem for elem in self.id_data.cloudrig_ui if elem.parent==self] + + @property + def should_draw(self): + if not self.parent: + return True + if self.parent.element_type != 'PROPERTY': + return True + parent_value_str = str(self.parent.prop_value) + if parent_value_str in [v.strip() for v in self.parent_values.split(",")]: + return True + return False + + def draw(self, context, layout): + if not self.should_draw or not layout: + return + + if self.element_type == 'PANEL': + # TODO: Figure out how to allow elements to be drawn in the header. + header, layout = layout.panel(idname=str(self.index) + self.display_name) + header.label(text=self.display_name) + if not layout: + return + if self.element_type == 'LABEL': + if self.display_name: + layout.label(text=self.display_name) + if self.element_type == 'ROW': + layout = layout.row() + if self.display_name: + layout.label(text=self.display_name) + if self.element_type == 'PROPERTY': + self.draw_property(context, layout) + if any([child.should_draw for child in self.children]): + layout = layout.box() + if self.element_type == 'OPERATOR': + self.draw_operator(context, layout) + + for child in self.children: + child.draw(context, layout) + + def draw_property(self, context, layout): + prop_owner, prop_value = self.prop_owner, self.prop_value + if not prop_owner: + layout.alert = True + layout.label( + text=f"Missing property owner: '{self.prop_owner_path}' for property '{self.prop_name}'.", + icon='ERROR', + ) + return + if prop_value == {'MISSING'}: + layout.alert = True + layout.label( + text=f"Missing property '{self.prop_name}' of owner '{self.prop_owner_path}'.", + icon='ERROR', + ) + + if not self.display_name: + display_name = self.prop_name + + bracketed_prop_name = self.bracketed_prop_name + value_type, is_array = rna_idprop_value_item_type(prop_value) + + if value_type is type(None) or issubclass(value_type, ID): + # Property is a Datablock Pointer. + layout.prop(self.prop_owner, bracketed_prop_name, text=display_name) + elif value_type in {int, float, bool}: + if self.texts and not is_array and len(self.texts) - 1 >= int(prop_value) >= 0: + text = self.texts[int(prop_value)].strip() + if text: + display_name += ": " + text + if value_type == bool: + icon = self.icon if prop_value else self.icon_flase + layout.prop(self.prop_owner, bracketed_prop_name, toggle=True, text=display_name, icon=icon) + elif value_type in {int, float}: + if self.prop_is_custom: + # Property is a float/int/color + # For large ranges, a slider doesn't make sense. + prop_settings = self.custom_prop_settings + is_slider = ( + not is_array + and prop_settings['soft_max'] - prop_settings['soft_min'] < 100 + ) + layout.prop(prop_owner, bracketed_prop_name, slider=is_slider, text=display_name) + else: + layout.prop(prop_owner, bracketed_prop_name, text=display_name) + elif value_type == str: + if ( + issubclass(type(prop_owner), bpy.types.Constraint) + and bracketed_prop_name == 'subtarget' + and prop_owner.target + and prop_owner.target.type == 'ARMATURE' + ): + # Special case for nice constraint sub-target selectors. + layout.prop_search(prop_owner, bracketed_prop_name, prop_owner.target.pose, 'bones') + else: + layout.prop(prop_owner, bracketed_prop_name) + else: + layout.prop(prop_owner, bracketed_prop_name, text=display_name) + + def draw_operator(self, context, layout): + op_icon = self.icon + if not self.icon or self.icon == 'NONE': + op_icon = 'BLANK1' + op_props = layout.operator(self.bl_idname, text=self.display_name, icon=op_icon) + feed_op_props(op_props, self.op_kwargs) + return op_props + + +class CLOUDRIG_PT_custom_ui(CLOUDRIG_PT_base): + bl_idname = "CLOUDRIG_PT_custom_ui" + bl_label = "Rig UI" + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + + rig = context.active_object # TODO + + for elem in rig.cloudrig_ui: + if not elem.parent: + elem.draw(context, layout) + ####################################### ########### Rig Preferences ########### ####################################### @@ -2971,10 +3220,12 @@ def register_hotkey( ####################################### classes = ( + CloudRig_UIElement, CloudRig_RigPreferences, CloudRigBoneCollection, CLOUDRIG_UL_collections, CLOUDRIG_PT_settings, + CLOUDRIG_PT_custom_ui, CLOUDRIG_PT_hotkeys_panel, CLOUDRIG_PT_collections_sidebar, CLOUDRIG_PT_collections_filter, @@ -3043,6 +3294,7 @@ def register(): bpy.types.Object.cloudrig_prefs = PointerProperty( type=CloudRig_RigPreferences, override={'LIBRARY_OVERRIDABLE'} ) + bpy.types.Object.cloudrig_ui = CollectionProperty(type=CloudRig_UIElement) bpy.types.BoneCollection.cloudrig_info = PointerProperty( type=CloudRigBoneCollection, override={'LIBRARY_OVERRIDABLE'} -- 2.30.2 From fd02c2e7eb4a988a38af2c3374fc83aaf06f6e0b Mon Sep 17 00:00:00 2001 From: Demeter Dzadik Date: Thu, 8 Aug 2024 18:50:14 +0100 Subject: [PATCH 02/10] WIP add operator --- generation/cloudrig.py | 4 ++++ rig_component_features/__init__.py | 2 ++ .../properties_ui_editor.py | 22 +++++++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 rig_component_features/properties_ui_editor.py diff --git a/generation/cloudrig.py b/generation/cloudrig.py index 80a2edf..d0dcd4c 100644 --- a/generation/cloudrig.py +++ b/generation/cloudrig.py @@ -946,6 +946,10 @@ class CLOUDRIG_PT_settings(CLOUDRIG_PT_base): if rig.cloudrig.ui_edit_mode: if hasattr(bpy.ops.pose, 'cloudrig_add_property_to_ui'): layout.operator('pose.cloudrig_add_property_to_ui', icon='ADD') + if hasattr(bpy.ops.object, 'cloudrig_ui_element_add'): + layout.operator('object.cloudrig_ui_element_add', icon='ADD') + else: + print("Why didn't the class register") if ui_data: for panel_name, panel_data in ui_data.items(): diff --git a/rig_component_features/__init__.py b/rig_component_features/__init__.py index b51ac14..dcadc4e 100644 --- a/rig_component_features/__init__.py +++ b/rig_component_features/__init__.py @@ -9,6 +9,7 @@ from . import ( object, parenting, properties_ui, + properties_ui_editor, ) @@ -23,6 +24,7 @@ modules = [ parenting, properties_ui, component_params_ui, + properties_ui_editor, ] # Dictionary of modules that have a Params class, and want to register diff --git a/rig_component_features/properties_ui_editor.py b/rig_component_features/properties_ui_editor.py new file mode 100644 index 0000000..38d4c23 --- /dev/null +++ b/rig_component_features/properties_ui_editor.py @@ -0,0 +1,22 @@ +from ..generation.cloudrig import CloudRig_UIElement +from bpy.types import Operator + +class CLOUDRIG_OT_ui_element_add(Operator): + """Add a UI element""" + + bl_idname = "object.cloudrig_ui_element_add" + bl_label = "Add Property to UI" + bl_options = {'REGISTER', 'UNDO'} + + # Copy the definition of a single UIElement, which will be added + # by this operator, when the "OK" button is clicked. + __annotations__ = CloudRig_UIElement.__annotations__ + + def execute(self, context): + return {'FINISHED'} + + + +registry = [ + CLOUDRIG_OT_ui_element_add +] \ No newline at end of file -- 2.30.2 From e3ec87b486759a0180f2775c284db422c3ce5c26 Mon Sep 17 00:00:00 2001 From: Demeter Dzadik Date: Thu, 8 Aug 2024 21:23:35 +0100 Subject: [PATCH 03/10] Ability to add basic elements and recursive-remove elements --- generation/cloudrig.py | 22 ++++- rig_component_features/properties_ui.py | 3 +- .../properties_ui_editor.py | 94 ++++++++++++++++++- 3 files changed, 112 insertions(+), 7 deletions(-) diff --git a/generation/cloudrig.py b/generation/cloudrig.py index d0dcd4c..87b03b1 100644 --- a/generation/cloudrig.py +++ b/generation/cloudrig.py @@ -1471,6 +1471,15 @@ class CloudRig_UIElement(PropertyGroup): if elem == self: return i + @property + def identifier(self): + id = self.display_name + parent = self.parent + while parent: + id = parent.display_name + " -> " + id + parent = parent.parent + return id + parent_values: StringProperty( # Supported Types: Panel, Label, Row, only when Element Type of parent element is Property. name="Parent Values", @@ -1573,12 +1582,13 @@ class CloudRig_UIElement(PropertyGroup): if not self.should_draw or not layout: return + remove_op_ui = layout + if self.element_type == 'PANEL': # TODO: Figure out how to allow elements to be drawn in the header. header, layout = layout.panel(idname=str(self.index) + self.display_name) header.label(text=self.display_name) - if not layout: - return + remove_op_ui = header if self.element_type == 'LABEL': if self.display_name: layout.label(text=self.display_name) @@ -1593,6 +1603,11 @@ class CloudRig_UIElement(PropertyGroup): if self.element_type == 'OPERATOR': self.draw_operator(context, layout) + if self.id_data.cloudrig.ui_edit_mode: + remove_op_ui.operator('object.cloudrig_ui_element_remove', text="", icon='X').element_index = self.index + + if not layout: + return for child in self.children: child.draw(context, layout) @@ -1662,6 +1677,9 @@ class CloudRig_UIElement(PropertyGroup): op_props = layout.operator(self.bl_idname, text=self.display_name, icon=op_icon) feed_op_props(op_props, self.op_kwargs) return op_props + + def __repr__(self): + return self.identifier class CLOUDRIG_PT_custom_ui(CLOUDRIG_PT_base): diff --git a/rig_component_features/properties_ui.py b/rig_component_features/properties_ui.py index 0ec603b..4d2e121 100644 --- a/rig_component_features/properties_ui.py +++ b/rig_component_features/properties_ui.py @@ -13,7 +13,7 @@ from bpy.types import ( Modifier, ) from typing import Any -from bpy.props import StringProperty, BoolProperty, CollectionProperty +from bpy.props import StringProperty, BoolProperty, CollectionProperty, IntProperty from collections import OrderedDict from ..generation.cloudrig import ( unquote_custom_prop_name, @@ -1203,6 +1203,7 @@ def redraw_viewport(): class UIPathProperty(PropertyGroup): name: StringProperty() + index: IntProperty() ui_path: StringProperty() current: StringProperty(description="Current value of this property. Used for pre-filling the Parent Values field") diff --git a/rig_component_features/properties_ui_editor.py b/rig_component_features/properties_ui_editor.py index 38d4c23..0994b4d 100644 --- a/rig_component_features/properties_ui_editor.py +++ b/rig_component_features/properties_ui_editor.py @@ -1,5 +1,8 @@ -from ..generation.cloudrig import CloudRig_UIElement +from ..generation.cloudrig import CloudRig_UIElement, find_cloudrig +from .properties_ui import UIPathProperty from bpy.types import Operator +from bpy.props import CollectionProperty, StringProperty, IntProperty, BoolProperty +import bpy class CLOUDRIG_OT_ui_element_add(Operator): """Add a UI element""" @@ -12,11 +15,94 @@ class CLOUDRIG_OT_ui_element_add(Operator): # by this operator, when the "OK" button is clicked. __annotations__ = CloudRig_UIElement.__annotations__ + parent_element: StringProperty(name="Parent Element") + + @classmethod + def poll(cls, context): + rig = find_cloudrig(context) + if not rig: + return False + return True + + def invoke(self, context, _event): + context.scene.cloudrig_ui_parent_selector.clear() + + rig = find_cloudrig(context) + + for ui_element in rig.cloudrig_ui: + if ui_element.element_type in {'PANEL', 'LABEL', 'ROW'}: + parent_option = context.scene.cloudrig_ui_parent_selector.add() + parent_option.name = ui_element.identifier + parent_option.index = ui_element.index + + return context.window_manager.invoke_props_dialog(self, width=500) + + def draw(self, context): + layout = self.layout + layout.use_property_decorate = False + layout.use_property_split = True + + layout.prop(self, 'element_type') + layout.prop(self, 'display_name') + if self.element_type in {'PANEL', 'LABEL', 'ROW'}: + layout.prop_search(self, 'parent_element', context.scene, 'cloudrig_ui_parent_selector') + def execute(self, context): + rig = find_cloudrig(context) + new_ui_element = rig.cloudrig_ui.add() + new_ui_element.display_name = self.display_name + new_ui_element.element_type = self.element_type + if self.parent_element: + new_ui_element.parent_index = context.scene.cloudrig_ui_parent_selector[self.parent_element].index return {'FINISHED'} - - + +class CLOUDRIG_OT_ui_element_remove(Operator): + """Remove this UI element.\n\n""" \ + """Ctrl: Do not remove children""" + + bl_idname = "object.cloudrig_ui_element_remove" + bl_label = "Remove UI Element" + bl_options = {'INTERNAL', 'REGISTER', 'UNDO'} + + element_index: IntProperty() + recursive: BoolProperty(default=True) + + @classmethod + def poll(cls, context): + return find_cloudrig(context) + + def invoke(self, context, event): + self.recursive = not event.ctrl + return self.execute(context) + + def execute(self, context): + rig = find_cloudrig(context) + self.remove_element(rig, self.element_index) + return {'FINISHED'} + + def remove_element(self, rig, index): + element_to_remove = rig.cloudrig_ui[index] + + if self.recursive: + for child in element_to_remove.children: + self.remove_element(rig, child.index) + else: + for child in element_to_remove.children: + child.parent_index = -1 + + for element in rig.cloudrig_ui: + if element.parent_index > index: + element.parent_index -= 1 + + rig.cloudrig_ui.remove(index) + + +def register(): + bpy.types.Scene.cloudrig_ui_parent_selector = CollectionProperty( + type=UIPathProperty + ) registry = [ - CLOUDRIG_OT_ui_element_add + CLOUDRIG_OT_ui_element_add, + CLOUDRIG_OT_ui_element_remove, ] \ No newline at end of file -- 2.30.2 From 3ce0db6ce4df039b82a492d4fbb2ab8521e96aa5 Mon Sep 17 00:00:00 2001 From: Demeter Dzadik Date: Fri, 9 Aug 2024 01:01:35 +0100 Subject: [PATCH 04/10] Ability to add properties to the UI --- __init__.py | 2 +- generation/cloudrig.py | 189 ++++++---- .../properties_ui_editor.py | 342 ++++++++++++++++-- 3 files changed, 430 insertions(+), 103 deletions(-) diff --git a/__init__.py b/__init__.py index 24644ff..0347dc1 100644 --- a/__init__.py +++ b/__init__.py @@ -45,10 +45,10 @@ modules = [ # be registered before they themselves are. # - For Panels, they must be registered before their bl_parent_id is. # - Hotkeys must come after `cloudrig`, since we're storing them on a panel. - rig_component_features, rig_components, prefs, generation, + rig_component_features, operators, properties, metarigs, diff --git a/generation/cloudrig.py b/generation/cloudrig.py index 87b03b1..49bbaac 100644 --- a/generation/cloudrig.py +++ b/generation/cloudrig.py @@ -583,8 +583,7 @@ class POSE_OT_cloudrig_toggle_ikfk_bake(SnapBakeOpMixin, CloudRigOperator): self.report({'INFO'}, "Snapping complete.") return {'FINISHED'} - - def set_bone_selection(self, rig, select=False, pbones: list[PoseBone]=None): + def set_bone_selection(self, rig, select=False, pbones: list[PoseBone] = None): """Overrides SnapBakeOpMixin to also select the IK pole before keying.""" print("PBONES:", pbones, select) if select and self.target_value == 1 and self.ik_pole: @@ -599,7 +598,6 @@ class POSE_OT_cloudrig_toggle_ikfk_bake(SnapBakeOpMixin, CloudRigOperator): if self.target_value == 1 and self.ik_pole: self.snap_pole_target() - def snap_pole_target(self) -> Matrix: """Snap the pole target based on the first IK bone. This needs to run after the IK wrist control had already been snapped. @@ -615,56 +613,58 @@ class POSE_OT_cloudrig_toggle_ikfk_bake(SnapBakeOpMixin, CloudRigOperator): # https://blenderartists.org/t/visual-transform-helper-functions-for-2-5/500965 def perpendicular_vector(v): - """ Returns a vector that is perpendicular to the one given. - The returned vector is _not_ guaranteed to be normalized. + """Returns a vector that is perpendicular to the one given. + The returned vector is _not_ guaranteed to be normalized. """ # Create a vector that is not aligned with v. # It doesn't matter what vector. Just any vector # that's guaranteed to not be pointing in the same # direction. if abs(v[0]) < abs(v[1]): - tv = Vector((1,0,0)) + tv = Vector((1, 0, 0)) else: - tv = Vector((0,1,0)) + tv = Vector((0, 1, 0)) # Use cross prouct to generate a vector perpendicular to # both tv and (more importantly) v. return v.cross(tv) def rotation_difference(mat1, mat2): - """ Returns the shortest-path rotational difference between two - matrices. + """Returns the shortest-path rotational difference between two + matrices. """ q1 = mat1.to_quaternion() q2 = mat2.to_quaternion() - angle = acos(min(1,max(-1,q1.dot(q2)))) * 2 + angle = acos(min(1, max(-1, q1.dot(q2)))) * 2 if angle > pi: - angle = -angle + (2*pi) + angle = -angle + (2 * pi) return angle def get_pose_matrix_in_other_space(mat, pose_bone): - """ Returns the transform matrix relative to pose_bone's current - transform space. In other words, presuming that mat is in - armature space, slapping the returned matrix onto pose_bone - should give it the armature-space transforms of mat. + """Returns the transform matrix relative to pose_bone's current + transform space. In other words, presuming that mat is in + armature space, slapping the returned matrix onto pose_bone + should give it the armature-space transforms of mat. """ - return pose_bone.id_data.convert_space(matrix=mat, pose_bone=pose_bone, from_space='POSE', to_space='LOCAL') + return pose_bone.id_data.convert_space( + matrix=mat, pose_bone=pose_bone, from_space='POSE', to_space='LOCAL' + ) def set_pose_translation(pose_bone, mat): - """ Sets the pose bone's translation to the same translation as the given matrix. - Matrix should be given in bone's local space. + """Sets the pose bone's translation to the same translation as the given matrix. + Matrix should be given in bone's local space. """ pose_bone.location = mat.to_translation() def match_pole_target(ik_first, ik_last, pole, match_bone): - """ Places an IK chain's pole target to match ik_first's - transforms to match_bone. All bones should be given as pose bones. - You need to be in pose mode on the relevant armature object. - ik_first: first bone in the IK chain - ik_last: last bone in the IK chain - pole: pole target bone for the IK chain - match_bone: bone to match ik_first to (probably first bone in a matching FK chain) - length: distance pole target should be placed from the chain center + """Places an IK chain's pole target to match ik_first's + transforms to match_bone. All bones should be given as pose bones. + You need to be in pose mode on the relevant armature object. + ik_first: first bone in the IK chain + ik_last: last bone in the IK chain + pole: pole target bone for the IK chain + match_bone: bone to match ik_first to (probably first bone in a matching FK chain) + length: distance pole target should be placed from the chain center """ a = ik_first.matrix.to_translation() b = ik_last.matrix.to_translation() + ik_last.vector @@ -679,11 +679,11 @@ class POSE_OT_cloudrig_toggle_ikfk_bake(SnapBakeOpMixin, CloudRigOperator): pv = perpendicular_vector(ikv).normalized() * length def set_pole(pvi): - """ Set pole target's position based on a vector - from the arm center line. + """Set pole target's position based on a vector + from the arm center line. """ # Translate pvi into armature space - ploc = a + (ikv/2) + pvi + ploc = a + (ikv / 2) + pvi # Set pole target to location mat = get_pose_matrix_in_other_space(Matrix.Translation(ploc), pole) @@ -715,8 +715,6 @@ class POSE_OT_cloudrig_toggle_ikfk_bake(SnapBakeOpMixin, CloudRigOperator): match_pole_target(ik_first, self.ik_last, ik_pole, fk_first) return ik_pole.matrix - - def map_single_frame_to_bone_matrices( self, context, frame_number, bones_to_snap, snap_to_bones ): @@ -745,7 +743,6 @@ class POSE_OT_cloudrig_toggle_ikfk_bake(SnapBakeOpMixin, CloudRigOperator): bone_column.label(text=f"{' '*10} {self.ik_pole}") - ####################################### ######## Convenience Operators ######## ####################################### @@ -1432,21 +1429,33 @@ def unquote_custom_prop_name(prop_name: str) -> str: class CloudRig_UIElement(PropertyGroup): + @property + def rig(self): + return self.id_data + element_type: EnumProperty( name="Element Type", description="How this UI element is drawn", items=[ ('PANEL', "Panel", "Collapsible panel. May contain Panels, Labels, Rows"), ('LABEL', "Label", "Label. May contain Panels, Labels, Rows"), - ('ROW', "Row", "Grouping for elements that allow multiple per row. Must be used for such elements, even if there is only one in the row. May contain Rows, Properties, and Operators"), - ('PROPERTY', "Property", "A single Property. Must belong to a Row. May contain conditional Panels, Labels, Rows"), + ( + 'ROW', + "Row", + "Grouping for elements that allow multiple per row. Must be used for such elements, even if there is only one in the row. May contain Rows, Properties, and Operators", + ), + ( + 'PROPERTY', + "Property", + "A single Property. Must belong to a Row. May contain conditional Panels, Labels, Rows", + ), ('OPERATOR', "Operator", "A single Operator. Must belong to a Row"), ], ) display_name: StringProperty( name="Display Name", - description="(Optional) Display name of this UI element", + description="Display name of this UI element", default="", ) # NOTE: This needs to be updated when elements are removed. @@ -1457,17 +1466,19 @@ class CloudRig_UIElement(PropertyGroup): description="Index of the parent UI element", default=-1, ) + @property def parent(self): if self.parent_index >= 0: - return self.id_data.cloudrig_ui[self.parent_index] + return self.rig.cloudrig_ui[self.parent_index] + @parent.setter def parent(self, value): self.parent_index = value.index @property def index(self): - for i, elem in enumerate(self.id_data.cloudrig_ui): + for i, elem in enumerate(self.rig.cloudrig_ui): if elem == self: return i @@ -1483,60 +1494,70 @@ class CloudRig_UIElement(PropertyGroup): parent_values: StringProperty( # Supported Types: Panel, Label, Row, only when Element Type of parent element is Property. name="Parent Values", - description="Condition for this UI element to be drawn, when its parent is a Property. This UI element will only be drawn if the parent property has one of these comma-separated values" + description="Condition for this UI element to be drawn, when its parent is a Property. This UI element will only be drawn if the parent property has one of these comma-separated values", ) texts: StringProperty( # Supported Types: Property, only Boolean and Integer. name="Texts", - description="Comma-separated display texts for Integer and Boolean Properties" + description="Comma-separated display texts for Integer and Boolean Properties", ) bl_idname: StringProperty( # Supported Types: Operator name="Operator ID", - description="Operator bl_idname" + description="Operator bl_idname", ) op_kwargs: StringProperty( # Supported Types: Operator name="Operator Arguments", description="Operator Keyword Arguments, as a json dict", - default="{}" + default="{}", ) icon: StringProperty( # Supported Types: Label, Row, Property(bool), Operator name="Icon", - description="Icon" + description="Icon", ) icon_false: StringProperty( # Supported Types: Property(bool) name="Icon False", - description="Icon to display when this boolean property is False" + description="Icon to display when this boolean property is False", ) prop_owner_path: StringProperty( # Supported Types: Property name="Property Owner", - description="Data Path from the rig object to the direct owner of the property to be drawn" + description="Data Path from the rig object to the direct owner of the property to be drawn", ) + @property def prop_owner(self): try: - return self.id_data.path_resolve(self.prop_owner_path) + return self.rig.path_resolve(self.prop_owner_path) except ValueError: # This can happen eg. if user adds a constraint influence to the UI, then deletes the constraint. return + def update_prop_name(self, context): + if self.is_custom_prop: + self.display_name = self.prop_name.replace("_", " ").title() + elif self.prop_name == 'is_visible': + self.display_name = self.prop_owner.name + prop_name: StringProperty( # Supported Types: Property name="Property Name", - description="Name of the property to be drawn" + description="Name of the property to be drawn", + update=update_prop_name, ) + @property def bracketed_prop_name(self): - if self.prop_is_custom: + if self.is_custom_prop: return f'["{self.prop_name}"]' return self.prop_name + @property def prop_value(self): if not hasattr(self.prop_owner, 'path_resolve'): @@ -1548,14 +1569,15 @@ class CloudRig_UIElement(PropertyGroup): # Property may have been removed. return {'MISSING'} - prop_is_custom: BoolProperty( - # Supported Types: Property + is_custom_prop: BoolProperty( + # Supported Types: Property # TODO: This should be set from the update of prop_name. name="Is Custom Property", - description="Whether this is a custom or a built-in property. Set automatically" + description="Whether this is a custom or a built-in property. Set automatically", ) + @property def custom_prop_settings(self): - if not self.prop_is_custom: + if not self.is_custom_prop: return try: return self.prop_owner.id_properties_ui(self.prop_name).as_dict() @@ -1565,7 +1587,7 @@ class CloudRig_UIElement(PropertyGroup): @property def children(self): - return [elem for elem in self.id_data.cloudrig_ui if elem.parent==self] + return [elem for elem in self.rig.cloudrig_ui if elem.parent == self] @property def should_draw(self): @@ -1578,7 +1600,7 @@ class CloudRig_UIElement(PropertyGroup): return True return False - def draw(self, context, layout): + def draw_ui_element(self, context, layout): if not self.should_draw or not layout: return @@ -1597,19 +1619,23 @@ class CloudRig_UIElement(PropertyGroup): if self.display_name: layout.label(text=self.display_name) if self.element_type == 'PROPERTY': + if not self.parent or self.parent.element_type != 'ROW': + layout = remove_op_ui = layout.row() self.draw_property(context, layout) if any([child.should_draw for child in self.children]): layout = layout.box() if self.element_type == 'OPERATOR': self.draw_operator(context, layout) - if self.id_data.cloudrig.ui_edit_mode: - remove_op_ui.operator('object.cloudrig_ui_element_remove', text="", icon='X').element_index = self.index + if self.rig.cloudrig.ui_edit_mode: + remove_op_ui.operator( + 'object.cloudrig_ui_element_remove', text="", icon='X' + ).element_index = self.index if not layout: return for child in self.children: - child.draw(context, layout) + child.draw_ui_element(context, layout) def draw_property(self, context, layout): prop_owner, prop_value = self.prop_owner, self.prop_value @@ -1627,8 +1653,7 @@ class CloudRig_UIElement(PropertyGroup): icon='ERROR', ) - if not self.display_name: - display_name = self.prop_name + display_name = self.display_name or self.prop_name bracketed_prop_name = self.bracketed_prop_name value_type, is_array = rna_idprop_value_item_type(prop_value) @@ -1637,15 +1662,25 @@ class CloudRig_UIElement(PropertyGroup): # Property is a Datablock Pointer. layout.prop(self.prop_owner, bracketed_prop_name, text=display_name) elif value_type in {int, float, bool}: - if self.texts and not is_array and len(self.texts) - 1 >= int(prop_value) >= 0: + if ( + self.texts + and not is_array + and len(self.texts) - 1 >= int(prop_value) >= 0 + ): text = self.texts[int(prop_value)].strip() if text: display_name += ": " + text if value_type == bool: icon = self.icon if prop_value else self.icon_flase - layout.prop(self.prop_owner, bracketed_prop_name, toggle=True, text=display_name, icon=icon) + layout.prop( + self.prop_owner, + bracketed_prop_name, + toggle=True, + text=display_name, + icon=icon, + ) elif value_type in {int, float}: - if self.prop_is_custom: + if self.is_custom_prop: # Property is a float/int/color # For large ranges, a slider doesn't make sense. prop_settings = self.custom_prop_settings @@ -1653,7 +1688,12 @@ class CloudRig_UIElement(PropertyGroup): not is_array and prop_settings['soft_max'] - prop_settings['soft_min'] < 100 ) - layout.prop(prop_owner, bracketed_prop_name, slider=is_slider, text=display_name) + layout.prop( + prop_owner, + bracketed_prop_name, + slider=is_slider, + text=display_name, + ) else: layout.prop(prop_owner, bracketed_prop_name, text=display_name) elif value_type == str: @@ -1664,7 +1704,9 @@ class CloudRig_UIElement(PropertyGroup): and prop_owner.target.type == 'ARMATURE' ): # Special case for nice constraint sub-target selectors. - layout.prop_search(prop_owner, bracketed_prop_name, prop_owner.target.pose, 'bones') + layout.prop_search( + prop_owner, bracketed_prop_name, prop_owner.target.pose, 'bones' + ) else: layout.prop(prop_owner, bracketed_prop_name) else: @@ -1677,7 +1719,14 @@ class CloudRig_UIElement(PropertyGroup): op_props = layout.operator(self.bl_idname, text=self.display_name, icon=op_icon) feed_op_props(op_props, self.op_kwargs) return op_props - + + def reset(self): + rna = self.bl_rna + for prop_name, prop_data in rna.properties.items(): + if prop_name == 'rna_type': + continue + setattr(self, prop_name, prop_data.default) + def __repr__(self): return self.identifier @@ -1691,11 +1740,12 @@ class CLOUDRIG_PT_custom_ui(CLOUDRIG_PT_base): layout.use_property_split = True layout.use_property_decorate = False - rig = context.active_object # TODO + rig = context.active_object # TODO for elem in rig.cloudrig_ui: if not elem.parent: - elem.draw(context, layout) + elem.draw_ui_element(context, layout) + ####################################### ########### Rig Preferences ########### @@ -1886,7 +1936,6 @@ class CloudRigBoneCollection(PropertyGroup): if not component.active_bone_set: component.bone_sets_active_index = 0 - # Metarig: Update bone sets with this collection assigned to refer to the new name. if is_active_cloud_metarig(context): rig = context.pose_object or context.active_object @@ -2923,7 +2972,9 @@ class POSE_OT_cloudrig_collection_clipboard_copy(CloudRigOperator): counter += 1 json_obj[coll.name]['bone_names'] = [bone.name for bone in coll.bones] json_obj[coll.name]['cloudrig_info'] = coll['cloudrig_info'].to_dict() - json_obj[coll.name]['parent_name'] = coll.parent.name if coll.parent else "" + json_obj[coll.name]['parent_name'] = ( + coll.parent.name if coll.parent else "" + ) if counter == 0: self.report({'ERROR'}, "No visible collections to copy.") @@ -3309,8 +3360,6 @@ def register(): for c in classes: if not is_registered(c): - # This if statement is important to avoid re-registering UI panels, - # which would cause them to lose their sub-panels. (They would become top-level.) register_class(c) bpy.types.Object.cloudrig_prefs = PointerProperty( diff --git a/rig_component_features/properties_ui_editor.py b/rig_component_features/properties_ui_editor.py index 0994b4d..db08f63 100644 --- a/rig_component_features/properties_ui_editor.py +++ b/rig_component_features/properties_ui_editor.py @@ -1,21 +1,212 @@ -from ..generation.cloudrig import CloudRig_UIElement, find_cloudrig +from ..generation.cloudrig import CloudRig_UIElement, find_cloudrig, feed_op_props from .properties_ui import UIPathProperty -from bpy.types import Operator -from bpy.props import CollectionProperty, StringProperty, IntProperty, BoolProperty +from bpy.types import Operator, UILayout, ID, PoseBone, BoneCollection +from bpy.props import ( + CollectionProperty, + StringProperty, + IntProperty, + BoolProperty, + EnumProperty, + PointerProperty, +) +from rna_prop_ui import rna_idprop_value_item_type import bpy -class CLOUDRIG_OT_ui_element_add(Operator): - """Add a UI element""" - - bl_idname = "object.cloudrig_ui_element_add" - bl_label = "Add Property to UI" - bl_options = {'REGISTER', 'UNDO'} - # Copy the definition of a single UIElement, which will be added - # by this operator, when the "OK" button is clicked. - __annotations__ = CloudRig_UIElement.__annotations__ +def draw_ui_editing(context, layout, ui_element, operator): + rig = find_cloudrig(context) - parent_element: StringProperty(name="Parent Element") + layout.prop(ui_element, 'element_type') + name_row = layout.row() + if ( + ui_element.element_type in {'PANEL', 'LABEL', 'ROW'} + and ui_element.display_name.strip() == "" + ): + name_row.alert = True + + if ui_element.element_type == 'PROPERTY': + layout.prop(operator, 'prop_owner_type', expand=True) + if operator.prop_owner_type == 'BONE': + layout.prop_search(operator, 'prop_bone', rig.pose, 'bones') + elif operator.prop_owner_type == 'COLLECTION': + layout.prop_search( + operator, + 'prop_coll', + rig.data, + 'collections_all', + icon='OUTLINER_COLLECTION', + ) + elif operator.prop_owner_type == 'DATA_PATH': + layout.prop(operator, 'prop_data_path', icon='RNA') + + if context.scene.cloudrig_ui_prop_selector: + layout.prop_search( + ui_element, 'prop_name', context.scene, 'cloudrig_ui_prop_selector' + ) + else: + layout.prop(ui_element, 'prop_name') + + if ui_element.element_type == 'OPERATOR': + draw_op_editing(context, layout, ui_element, operator) + + name_row.prop(ui_element, 'display_name') + if ( + ui_element.element_type in {'PANEL', 'LABEL', 'ROW'} + and ui_element.display_name.strip() == "" + ): + return + + # debug + # layout.prop(ui_element, 'prop_owner_path') + # layout.prop(ui_element, 'is_custom_prop') + + layout.prop_search( + operator, 'parent_element', context.scene, 'cloudrig_ui_parent_selector' + ) + + +def draw_op_editing(context, layout, ui_element, operator): + if operator.use_batch_add: + return + op_box = layout.box().column() + op_box.prop(operator.temp_kmi, 'idname', text="Operator") + operator.op_kwargs_dict = {} + if operator.temp_kmi.idname: + box = None + op_rna = eval("bpy.ops." + operator.temp_kmi.idname).get_rna_type() + for key, value in op_rna.properties.items(): + if key == 'rna_type': + continue + if not box: + box = op_box.box().column(align=True) + box.prop(operator.temp_kmi.properties, key) + operator.op_kwargs_dict[key] = str( + getattr(operator.temp_kmi.properties, key) + ) + icons = UILayout.bl_rna.functions["prop"].parameters["icon"] + op_box.prop_search( + ui_element, 'icon', icons, 'enum_items', icon=ui_element.icon + ) + + +def update_parent_selector(context): + context.scene.cloudrig_ui_parent_selector.clear() + + rig = find_cloudrig(context) + + for ui_element in rig.cloudrig_ui: + if ui_element.element_type in {'PANEL', 'LABEL', 'ROW', 'PROPERTY'}: + parent_option = context.scene.cloudrig_ui_parent_selector.add() + parent_option.name = ui_element.identifier + parent_option.index = ui_element.index + + +def wipe_parent_selector(context): + if 'cloudrig_ui_parent_selector' in context.scene: + del context.scene['cloudrig_ui_parent_selector'] + + +def get_new_ui_element(context): + rig = find_cloudrig(context) + return rig.cloudrig_ui_new_element + + +def update_property_selector(self, context): + context.scene.cloudrig_ui_prop_selector.clear() + + ui_element = get_new_ui_element(context) + + prop_owner = ui_element.prop_owner + + if not prop_owner: + return + + # Populate the property drop-down selector with available custom properties. + ui_element.is_custom_prop = True + for key in get_drawable_custom_properties(prop_owner): + name_entry = context.scene.cloudrig_ui_prop_selector.add() + name_entry.name = key + + if len(context.scene.cloudrig_ui_prop_selector) == 0: + # If that failed, populate it with built-in properties instead. + ui_element.is_custom_prop = False + for key in get_drawable_builtin_properties(prop_owner): + name_entry = context.scene.cloudrig_ui_prop_selector.add() + name_entry.name = key + + +class UIElementAddMixin: + def update_parent_element(self, context): + ui_element = get_new_ui_element(context) + ui_element.parent_index = context.scene.cloudrig_ui_parent_selector[ + self.parent_element + ].index + + parent_element: StringProperty(name="Parent Element", update=update_parent_element) + + def update_prop_bone(self, context): + ui_element = get_new_ui_element(context) + ui_element.prop_owner_path = f'pose.bones["{self.prop_bone}"]' + update_property_selector(self, context) + + prop_bone: StringProperty(name="Bone Name", update=update_prop_bone) + + def update_prop_coll(self, context): + ui_element = get_new_ui_element(context) + ui_element.display_name = self.prop_coll + ui_element.prop_owner_path = f'data.collections_all["{self.prop_coll}"]' + update_property_selector(self, context) + + prop_coll: StringProperty(name="Bone Collection", update=update_prop_coll) + + def update_prop_data_path(self, context): + ui_element = get_new_ui_element(context) + ui_element.prop_owner_path = self.prop_data_path + update_property_selector(self, context) + + prop_data_path: StringProperty(name="Data Path", update=update_prop_data_path) + + def update_prop_owner_type(self, context): + ui_element = get_new_ui_element(context) + if self.prop_owner_type == 'COLLECTION': + ui_element.prop_name = 'is_visible' + ui_element.icon = 'HIDE_OFF' + ui_element.icon_false = 'HIDE_ON' + self.prop_coll = self.prop_coll + if self.prop_owner_type == 'BONE': + self.prop_bone = self.prop_bone + if self.prop_owner_type == 'DATA_PATH': + self.prop_data_path = ui_element.prop_owner_path + + prop_owner_type: EnumProperty( + name="Property Owner Type", + description="How you would like to select the owner of the property which will be added to the UI", + items=[ + ('BONE', 'Bone', 'Select a bone from the rig', 'BONE_DATA', 0), + ( + 'COLLECTION', + 'Collection', + 'Select a bone collection from the rig', + 'OUTLINER_COLLECTION', + 1, + ), + ( + 'DATA_PATH', + 'Data Path', + 'Enter a Python Data Path to any property of the rig', + 'RNA', + 2, + ), + ], + update=update_prop_owner_type, + ) + + use_batch_add: BoolProperty( + name="Batch Add", + options={'SKIP_SAVE'}, + default=False, + description="Add all custom properties of the selected ID to the UI", + ) @classmethod def poll(cls, context): @@ -25,40 +216,83 @@ class CLOUDRIG_OT_ui_element_add(Operator): return True def invoke(self, context, _event): - context.scene.cloudrig_ui_parent_selector.clear() + update_parent_selector(context) - rig = find_cloudrig(context) + self.ui_element = get_new_ui_element(context) + self.ui_element.reset() - for ui_element in rig.cloudrig_ui: - if ui_element.element_type in {'PANEL', 'LABEL', 'ROW'}: - parent_option = context.scene.cloudrig_ui_parent_selector.add() - parent_option.name = ui_element.identifier - parent_option.index = ui_element.index + self.temp_kmi = context.window_manager.keyconfigs.default.keymaps[ + 'Info' + ].keymap_items.new('', 'NUMPAD_5', 'PRESS') + if self.bl_idname: + self.temp_kmi.idname = self.bl_idname + if self.ui_element.op_kwargs: + op_props = self.temp_kmi.properties + feed_op_props(op_props, self.ui_element.op_kwargs) return context.window_manager.invoke_props_dialog(self, width=500) - + def draw(self, context): layout = self.layout layout.use_property_decorate = False layout.use_property_split = True - layout.prop(self, 'element_type') - layout.prop(self, 'display_name') - if self.element_type in {'PANEL', 'LABEL', 'ROW'}: - layout.prop_search(self, 'parent_element', context.scene, 'cloudrig_ui_parent_selector') + draw_ui_editing(context, layout, self.ui_element, self) + + +class CLOUDRIG_OT_ui_element_add(UIElementAddMixin, Operator): + """Add UI Element""" + + bl_idname = "object.cloudrig_ui_element_add" + bl_label = "Add UI Element" + bl_options = {'INTERNAL', 'REGISTER', 'UNDO'} def execute(self, context): rig = find_cloudrig(context) + temp_ui_element = get_new_ui_element(context) + + if ( + temp_ui_element.element_type in {'PANEL', 'LABEL', 'ROW'} + and temp_ui_element.display_name.strip() == "" + ): + self.report({'ERROR'}, "This UI element must have a display name!") + return {'CANCELLED'} + new_ui_element = rig.cloudrig_ui.add() - new_ui_element.display_name = self.display_name - new_ui_element.element_type = self.element_type - if self.parent_element: - new_ui_element.parent_index = context.scene.cloudrig_ui_parent_selector[self.parent_element].index + + for prop_name in new_ui_element.bl_rna.properties.keys(): + if prop_name == 'rna_type': + continue + setattr(new_ui_element, prop_name, getattr(temp_ui_element, prop_name)) + + wipe_parent_selector(context) + del rig['cloudrig_ui_new_element'] + return {'FINISHED'} + +class CLOUDRIG_OT_ui_element_edit(UIElementAddMixin, Operator): + """Add a UI element""" + + bl_idname = "object.cloudrig_ui_element_edit" + bl_label = "Edit UI Element" + bl_options = {'INTERNAL', 'REGISTER', 'UNDO'} + + element_index: IntProperty() + + def draw(self, context): + layout = self.layout + layout.use_property_decorate = False + layout.use_property_split = True + + rig = find_cloudrig(context) + elem_to_edit = rig.cloudrig_ui[self.element_index] + + draw_ui_editing(context, layout, elem_to_edit, self) + + class CLOUDRIG_OT_ui_element_remove(Operator): - """Remove this UI element.\n\n""" \ - """Ctrl: Do not remove children""" + """Remove this UI element.\n\n""" """Ctrl: Do not remove children""" bl_idname = "object.cloudrig_ui_element_remove" bl_label = "Remove UI Element" @@ -97,12 +331,56 @@ class CLOUDRIG_OT_ui_element_remove(Operator): rig.cloudrig_ui.remove(index) +def supports_custom_props(prop_owner): + return isinstance(prop_owner, ID) or type(prop_owner) in {PoseBone, BoneCollection} + + +def has_custom_props(prop_owner) -> bool: + if not supports_custom_props(prop_owner): + return False + return bool(list(get_drawable_custom_properties(prop_owner))) + + +def get_drawable_custom_properties(prop_owner): + if not supports_custom_props(prop_owner): + return [] + for prop_name in prop_owner.keys(): + try: + prop_owner.id_properties_ui(prop_name).as_dict() + except TypeError: + # This happens for Python properties. There's not much point in drawing them. + continue + yield prop_name + + +def path_resolve_safe(owner, data_path): + try: + return owner.path_resolve(data_path) + except ValueError: + # This can happen eg. if user adds a constraint influence to the UI, then deletes the constraint. + return + + +def get_drawable_builtin_properties(prop_owner): + for prop_name, prop_data in prop_owner.bl_rna.properties.items(): + if prop_data.is_runtime: + continue + prop_value = getattr(prop_owner, prop_name) + value_type, is_array = rna_idprop_value_item_type(prop_value) + if value_type in {bool, int, float, str}: + yield prop_name + + def register(): bpy.types.Scene.cloudrig_ui_parent_selector = CollectionProperty( type=UIPathProperty ) + bpy.types.Scene.cloudrig_ui_prop_selector = CollectionProperty(type=UIPathProperty) + bpy.types.Object.cloudrig_ui_new_element = PointerProperty(type=CloudRig_UIElement) + registry = [ CLOUDRIG_OT_ui_element_add, + CLOUDRIG_OT_ui_element_edit, CLOUDRIG_OT_ui_element_remove, -] \ No newline at end of file +] -- 2.30.2 From 0181297bcb6506b20fbd2ffff22740c561b9ea38 Mon Sep 17 00:00:00 2001 From: Demeter Dzadik Date: Fri, 9 Aug 2024 11:22:01 +0100 Subject: [PATCH 05/10] Ability to create a panel, label, and row all in one go, like before --- generation/cloudrig.py | 29 ++++--- .../properties_ui_editor.py | 75 ++++++++++++++----- 2 files changed, 73 insertions(+), 31 deletions(-) diff --git a/generation/cloudrig.py b/generation/cloudrig.py index 49bbaac..e80edf7 100644 --- a/generation/cloudrig.py +++ b/generation/cloudrig.py @@ -1473,14 +1473,18 @@ class CloudRig_UIElement(PropertyGroup): return self.rig.cloudrig_ui[self.parent_index] @parent.setter - def parent(self, value): - self.parent_index = value.index + def parent(self, value: 'CloudRig_UIElement'): + if not value: + self.parent_index = -1 + else: + self.parent_index = value.index @property def index(self): for i, elem in enumerate(self.rig.cloudrig_ui): if elem == self: return i + return -1 @property def identifier(self): @@ -1604,7 +1608,7 @@ class CloudRig_UIElement(PropertyGroup): if not self.should_draw or not layout: return - remove_op_ui = layout + parent_layout = remove_op_ui = layout if self.element_type == 'PANEL': # TODO: Figure out how to allow elements to be drawn in the header. @@ -1612,12 +1616,13 @@ class CloudRig_UIElement(PropertyGroup): header.label(text=self.display_name) remove_op_ui = header if self.element_type == 'LABEL': + layout = remove_op_ui = layout.row() if self.display_name: layout.label(text=self.display_name) if self.element_type == 'ROW': - layout = layout.row() - if self.display_name: - layout.label(text=self.display_name) + layout = remove_op_ui = parent_layout = layout.row() + # if self.display_name: + # layout.label(text=self.display_name) if self.element_type == 'PROPERTY': if not self.parent or self.parent.element_type != 'ROW': layout = remove_op_ui = layout.row() @@ -1627,16 +1632,16 @@ class CloudRig_UIElement(PropertyGroup): if self.element_type == 'OPERATOR': self.draw_operator(context, layout) + if not layout: + return + for child in self.children: + child.draw_ui_element(context, parent_layout) + if self.rig.cloudrig.ui_edit_mode: remove_op_ui.operator( 'object.cloudrig_ui_element_remove', text="", icon='X' ).element_index = self.index - if not layout: - return - for child in self.children: - child.draw_ui_element(context, layout) - def draw_property(self, context, layout): prop_owner, prop_value = self.prop_owner, self.prop_value if not prop_owner: @@ -1652,6 +1657,7 @@ class CloudRig_UIElement(PropertyGroup): text=f"Missing property '{self.prop_name}' of owner '{self.prop_owner_path}'.", icon='ERROR', ) + return display_name = self.display_name or self.prop_name @@ -1739,6 +1745,7 @@ class CLOUDRIG_PT_custom_ui(CLOUDRIG_PT_base): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False + layout = layout.column(align=True) rig = context.active_object # TODO diff --git a/rig_component_features/properties_ui_editor.py b/rig_component_features/properties_ui_editor.py index db08f63..7a2b7ba 100644 --- a/rig_component_features/properties_ui_editor.py +++ b/rig_component_features/properties_ui_editor.py @@ -16,15 +16,21 @@ import bpy def draw_ui_editing(context, layout, ui_element, operator): rig = find_cloudrig(context) - layout.prop(ui_element, 'element_type') - name_row = layout.row() - if ( - ui_element.element_type in {'PANEL', 'LABEL', 'ROW'} - and ui_element.display_name.strip() == "" - ): - name_row.alert = True + layout.prop(operator, 'element_type', expand=True) - if ui_element.element_type == 'PROPERTY': + parent_row = layout.row() + if operator.create_new_ui: + parent_row.prop(operator, 'new_panel_name') + layout.prop(operator, 'new_label_name') + layout.prop(operator, 'new_row_name') + else: + parent_row.prop_search( + operator, 'parent_element', context.scene, 'cloudrig_ui_parent_selector' + ) + if context.scene.cloudrig_ui_parent_selector: + parent_row.prop(operator, 'create_new_ui', text="", icon='ADD') + + if operator.element_type == 'PROPERTY': layout.prop(operator, 'prop_owner_type', expand=True) if operator.prop_owner_type == 'BONE': layout.prop_search(operator, 'prop_bone', rig.pose, 'bones') @@ -46,24 +52,15 @@ def draw_ui_editing(context, layout, ui_element, operator): else: layout.prop(ui_element, 'prop_name') - if ui_element.element_type == 'OPERATOR': + if operator.element_type == 'OPERATOR': draw_op_editing(context, layout, ui_element, operator) - name_row.prop(ui_element, 'display_name') - if ( - ui_element.element_type in {'PANEL', 'LABEL', 'ROW'} - and ui_element.display_name.strip() == "" - ): - return + layout.prop(ui_element, 'display_name') # debug # layout.prop(ui_element, 'prop_owner_path') # layout.prop(ui_element, 'is_custom_prop') - layout.prop_search( - operator, 'parent_element', context.scene, 'cloudrig_ui_parent_selector' - ) - def draw_op_editing(context, layout, ui_element, operator): if operator.use_batch_add: @@ -201,6 +198,18 @@ class UIElementAddMixin: update=update_prop_owner_type, ) + element_type: EnumProperty( + name="Element Type", + items=[ + ('PROPERTY', 'Property', "Property"), + ('OPERATOR', 'Operator', "Operator") + ] + ) + create_new_ui: BoolProperty(name="Create Containers", description="Instead of placing this UI element in an existing panel, label, and row, create new ones") + new_panel_name: StringProperty(name="Panel Name", description="Optional. Elements parented to this panel can be hidden by collapsing the panel") + new_label_name: StringProperty(name="Label Name", description="Optional. Elements parented to this label will be displayed below it") + new_row_name: StringProperty(name="Row Name", description="Optional. Elements parented to this row will be displayed side-by-side") + use_batch_add: BoolProperty( name="Batch Add", options={'SKIP_SAVE'}, @@ -217,6 +226,8 @@ class UIElementAddMixin: def invoke(self, context, _event): update_parent_selector(context) + if not context.scene.cloudrig_ui_parent_selector: + self.create_new_ui = True self.ui_element = get_new_ui_element(context) self.ui_element.reset() @@ -251,6 +262,25 @@ class CLOUDRIG_OT_ui_element_add(UIElementAddMixin, Operator): rig = find_cloudrig(context) temp_ui_element = get_new_ui_element(context) + parent = None + if self.create_new_ui: + if self.new_panel_name: + parent = rig.cloudrig_ui.add() + parent.element_type = 'PANEL' + parent.display_name = self.new_panel_name + if self.new_label_name: + label = rig.cloudrig_ui.add() + label.parent = parent + label.element_type = 'LABEL' + label.display_name = self.new_label_name + parent = label + if self.new_row_name: + row = rig.cloudrig_ui.add() + row.parent = parent + row.element_type = 'ROW' + row.display_name = self.new_row_name + parent = row + if ( temp_ui_element.element_type in {'PANEL', 'LABEL', 'ROW'} and temp_ui_element.display_name.strip() == "" @@ -265,6 +295,10 @@ class CLOUDRIG_OT_ui_element_add(UIElementAddMixin, Operator): continue setattr(new_ui_element, prop_name, getattr(temp_ui_element, prop_name)) + if parent: + new_ui_element.parent = parent + new_ui_element.element_type = self.element_type + wipe_parent_selector(context) del rig['cloudrig_ui_new_element'] @@ -316,13 +350,14 @@ class CLOUDRIG_OT_ui_element_remove(Operator): def remove_element(self, rig, index): element_to_remove = rig.cloudrig_ui[index] + fallback_parent = element_to_remove.parent if self.recursive: for child in element_to_remove.children: self.remove_element(rig, child.index) else: for child in element_to_remove.children: - child.parent_index = -1 + child.parent = fallback_parent for element in rig.cloudrig_ui: if element.parent_index > index: -- 2.30.2 From e322ab8588add45bd17e821373df71d21893d36b Mon Sep 17 00:00:00 2001 From: Demeter Dzadik Date: Fri, 9 Aug 2024 13:17:15 +0100 Subject: [PATCH 06/10] Handle texts, icons, different prop owner types --- generation/cloudrig.py | 15 +- .../properties_ui_editor.py | 142 ++++++++++++------ 2 files changed, 108 insertions(+), 49 deletions(-) diff --git a/generation/cloudrig.py b/generation/cloudrig.py index e80edf7..ca5407c 100644 --- a/generation/cloudrig.py +++ b/generation/cloudrig.py @@ -1522,11 +1522,13 @@ class CloudRig_UIElement(PropertyGroup): # Supported Types: Label, Row, Property(bool), Operator name="Icon", description="Icon", + default='CHECKBOX_HLT', ) icon_false: StringProperty( # Supported Types: Property(bool) name="Icon False", description="Icon to display when this boolean property is False", + default='CHECKBOX_DEHLT', ) prop_owner_path: StringProperty( @@ -1546,7 +1548,7 @@ class CloudRig_UIElement(PropertyGroup): def update_prop_name(self, context): if self.is_custom_prop: self.display_name = self.prop_name.replace("_", " ").title() - elif self.prop_name == 'is_visible': + elif self.prop_name == 'is_visible' and self.prop_owner: self.display_name = self.prop_owner.name prop_name: StringProperty( @@ -1668,16 +1670,17 @@ class CloudRig_UIElement(PropertyGroup): # Property is a Datablock Pointer. layout.prop(self.prop_owner, bracketed_prop_name, text=display_name) elif value_type in {int, float, bool}: + texts = [t.strip() for t in self.texts.split(",")] if ( - self.texts + texts and not is_array - and len(self.texts) - 1 >= int(prop_value) >= 0 + and len(texts) - 1 >= int(prop_value) >= 0 ): - text = self.texts[int(prop_value)].strip() + text = texts[int(prop_value)] if text: display_name += ": " + text if value_type == bool: - icon = self.icon if prop_value else self.icon_flase + icon = self.icon if prop_value else self.icon_false layout.prop( self.prop_owner, bracketed_prop_name, @@ -1743,7 +1746,7 @@ class CLOUDRIG_PT_custom_ui(CLOUDRIG_PT_base): def draw(self, context): layout = self.layout - layout.use_property_split = True + layout.use_property_split = False layout.use_property_decorate = False layout = layout.column(align=True) diff --git a/rig_component_features/properties_ui_editor.py b/rig_component_features/properties_ui_editor.py index 7a2b7ba..c98ac98 100644 --- a/rig_component_features/properties_ui_editor.py +++ b/rig_component_features/properties_ui_editor.py @@ -10,14 +10,25 @@ from bpy.props import ( PointerProperty, ) from rna_prop_ui import rna_idprop_value_item_type -import bpy +import bpy, json def draw_ui_editing(context, layout, ui_element, operator): - rig = find_cloudrig(context) - layout.prop(operator, 'element_type', expand=True) + draw_parent_picking(context, layout, ui_element, operator) + + if operator.element_type == 'PROPERTY': + draw_prop_editing(context, layout, ui_element, operator) + + if operator.element_type == 'OPERATOR': + draw_op_editing(context, layout, ui_element, operator) + + # debug + # layout.prop(ui_element, 'prop_owner_path') + # layout.prop(ui_element, 'is_custom_prop') + +def draw_parent_picking(context, layout, ui_element, operator): parent_row = layout.row() if operator.create_new_ui: parent_row.prop(operator, 'new_panel_name') @@ -30,36 +41,59 @@ def draw_ui_editing(context, layout, ui_element, operator): if context.scene.cloudrig_ui_parent_selector: parent_row.prop(operator, 'create_new_ui', text="", icon='ADD') - if operator.element_type == 'PROPERTY': - layout.prop(operator, 'prop_owner_type', expand=True) - if operator.prop_owner_type == 'BONE': - layout.prop_search(operator, 'prop_bone', rig.pose, 'bones') - elif operator.prop_owner_type == 'COLLECTION': - layout.prop_search( - operator, - 'prop_coll', - rig.data, - 'collections_all', - icon='OUTLINER_COLLECTION', - ) - elif operator.prop_owner_type == 'DATA_PATH': - layout.prop(operator, 'prop_data_path', icon='RNA') +def draw_prop_editing(context, layout, ui_element, operator): + rig = find_cloudrig(context) - if context.scene.cloudrig_ui_prop_selector: - layout.prop_search( - ui_element, 'prop_name', context.scene, 'cloudrig_ui_prop_selector' - ) - else: - layout.prop(ui_element, 'prop_name') + owner_row = layout.row() + if operator.prop_owner_type == 'BONE': + owner_row.prop_search(operator, 'prop_bone', rig.pose, 'bones') + elif operator.prop_owner_type == 'COLLECTION': + owner_row.prop_search( + operator, + 'prop_coll', + rig.data, + 'collections_all', + icon='OUTLINER_COLLECTION', + ) + elif operator.prop_owner_type == 'DATA_PATH': + owner_row.prop(operator, 'prop_data_path', icon='RNA') + owner_row.prop(operator, 'prop_owner_type', expand=True, text="") - if operator.element_type == 'OPERATOR': - draw_op_editing(context, layout, ui_element, operator) + if not ui_element.prop_owner: + return + if operator.prop_owner_type == 'COLLECTION' and not operator.prop_coll: + return + if operator.prop_owner_type == 'BONE' and not operator.prop_bone: + return + + if context.scene.cloudrig_ui_prop_selector: + layout.prop_search( + ui_element, 'prop_name', context.scene, 'cloudrig_ui_prop_selector' + ) + else: + layout.prop(ui_element, 'prop_name') + + if not ui_element.prop_name: + return layout.prop(ui_element, 'display_name') - # debug - # layout.prop(ui_element, 'prop_owner_path') - # layout.prop(ui_element, 'is_custom_prop') + value_type, is_array = rna_idprop_value_item_type(ui_element.prop_value) + if not is_array: + if value_type in {bool, int}: + layout.prop(ui_element, 'texts') + if value_type == bool: + icons = UILayout.bl_rna.functions["prop"].parameters["icon"] + layout.prop_search( + ui_element, 'icon', icons, 'enum_items', icon=ui_element.icon + ) + layout.prop_search( + ui_element, + 'icon_false', + icons, + 'enum_items', + icon=ui_element.icon_false, + ) def draw_op_editing(context, layout, ui_element, operator): @@ -85,6 +119,8 @@ def draw_op_editing(context, layout, ui_element, operator): ui_element, 'icon', icons, 'enum_items', icon=ui_element.icon ) + layout.prop(ui_element, 'display_name') + def update_parent_selector(context): context.scene.cloudrig_ui_parent_selector.clear() @@ -139,20 +175,31 @@ class UIElementAddMixin: self.parent_element ].index - parent_element: StringProperty(name="Parent Element", update=update_parent_element) + parent_element: StringProperty( + name="Parent Element", + description="Optional. UI element that this new one should be a part of", + update=update_parent_element, + ) def update_prop_bone(self, context): ui_element = get_new_ui_element(context) - ui_element.prop_owner_path = f'pose.bones["{self.prop_bone}"]' - update_property_selector(self, context) + if self.prop_bone: + ui_element.prop_owner_path = f'pose.bones["{self.prop_bone}"]' + update_property_selector(self, context) + if ui_element.prop_name not in ui_element.prop_owner: + ui_element.prop_name = "" prop_bone: StringProperty(name="Bone Name", update=update_prop_bone) def update_prop_coll(self, context): ui_element = get_new_ui_element(context) - ui_element.display_name = self.prop_coll - ui_element.prop_owner_path = f'data.collections_all["{self.prop_coll}"]' - update_property_selector(self, context) + if self.prop_coll: + ui_element.prop_owner_path = f'data.collections_all["{self.prop_coll}"]' + update_property_selector(self, context) + ui_element.prop_name = 'is_visible' + ui_element.display_name = self.prop_coll + ui_element.icon = 'HIDE_OFF' + ui_element.icon_false = 'HIDE_ON' prop_coll: StringProperty(name="Bone Collection", update=update_prop_coll) @@ -166,9 +213,6 @@ class UIElementAddMixin: def update_prop_owner_type(self, context): ui_element = get_new_ui_element(context) if self.prop_owner_type == 'COLLECTION': - ui_element.prop_name = 'is_visible' - ui_element.icon = 'HIDE_OFF' - ui_element.icon_false = 'HIDE_ON' self.prop_coll = self.prop_coll if self.prop_owner_type == 'BONE': self.prop_bone = self.prop_bone @@ -202,13 +246,25 @@ class UIElementAddMixin: name="Element Type", items=[ ('PROPERTY', 'Property', "Property"), - ('OPERATOR', 'Operator', "Operator") - ] + ('OPERATOR', 'Operator', "Operator"), + ], + ) + create_new_ui: BoolProperty( + name="Create Containers", + description="Instead of placing this UI element in an existing panel, label, and row, create new ones", + ) + new_panel_name: StringProperty( + name="Panel Name", + description="Optional. Elements parented to this panel can be hidden by collapsing the panel", + ) + new_label_name: StringProperty( + name="Label Name", + description="Optional. Elements parented to this label will be displayed below it", + ) + new_row_name: StringProperty( + name="Row Name", + description="Optional. Elements parented to this row will be displayed side-by-side", ) - create_new_ui: BoolProperty(name="Create Containers", description="Instead of placing this UI element in an existing panel, label, and row, create new ones") - new_panel_name: StringProperty(name="Panel Name", description="Optional. Elements parented to this panel can be hidden by collapsing the panel") - new_label_name: StringProperty(name="Label Name", description="Optional. Elements parented to this label will be displayed below it") - new_row_name: StringProperty(name="Row Name", description="Optional. Elements parented to this row will be displayed side-by-side") use_batch_add: BoolProperty( name="Batch Add", -- 2.30.2 From 2cc2c3c54cfc7f2eb6cb08408054f6d5d49b3618 Mon Sep 17 00:00:00 2001 From: Demeter Dzadik Date: Fri, 9 Aug 2024 16:28:35 +0100 Subject: [PATCH 07/10] Get operators working --- generation/cloudrig.py | 68 +++++++++++++------ .../properties_ui_editor.py | 61 ++++++++--------- 2 files changed, 78 insertions(+), 51 deletions(-) diff --git a/generation/cloudrig.py b/generation/cloudrig.py index ca5407c..f07ff4e 100644 --- a/generation/cloudrig.py +++ b/generation/cloudrig.py @@ -1518,17 +1518,26 @@ class CloudRig_UIElement(PropertyGroup): description="Operator Keyword Arguments, as a json dict", default="{}", ) + def update_icons(self, context): + # Prevent illegal icon values when users clicks X button. + if self.icon == '': + self.icon = 'BLANK1' + if self.icon_false == '': + self.icon_false = 'BLANK1' + icon: StringProperty( # Supported Types: Label, Row, Property(bool), Operator name="Icon", description="Icon", default='CHECKBOX_HLT', + update=update_icons, ) icon_false: StringProperty( # Supported Types: Property(bool) name="Icon False", description="Icon to display when this boolean property is False", default='CHECKBOX_DEHLT', + update=update_icons, ) prop_owner_path: StringProperty( @@ -1601,49 +1610,65 @@ class CloudRig_UIElement(PropertyGroup): return True if self.parent.element_type != 'PROPERTY': return True + if not self.parent_values: + return True + parent_value_str = str(self.parent.prop_value) if parent_value_str in [v.strip() for v in self.parent_values.split(",")]: return True + return False - def draw_ui_element(self, context, layout): - if not self.should_draw or not layout: + def draw_ui_recursive(self, context, layouts): + if not self.should_draw or not layouts: return - parent_layout = remove_op_ui = layout + layout = layouts[-1] + + if self.parent and self.parent.element_type == 'PROPERTY': + layouts.pop() + layout = layouts[-1] + + remove_op_ui = layout if self.element_type == 'PANEL': # TODO: Figure out how to allow elements to be drawn in the header. header, layout = layout.panel(idname=str(self.index) + self.display_name) header.label(text=self.display_name) remove_op_ui = header - if self.element_type == 'LABEL': + layouts.append(layout) + elif self.element_type == 'LABEL': layout = remove_op_ui = layout.row() if self.display_name: layout.label(text=self.display_name) - if self.element_type == 'ROW': - layout = remove_op_ui = parent_layout = layout.row() - # if self.display_name: - # layout.label(text=self.display_name) - if self.element_type == 'PROPERTY': - if not self.parent or self.parent.element_type != 'ROW': - layout = remove_op_ui = layout.row() + elif self.element_type == 'ROW': + layout = remove_op_ui = layout.row() + layouts.append(layout) + elif self.element_type == 'PROPERTY': + layout = remove_op_ui = layout.row(align=True) + layouts.append(layout) self.draw_property(context, layout) - if any([child.should_draw for child in self.children]): + if any([child.should_draw and child.element_type!='OPERATOR' for child in self.children]): layout = layout.box() - if self.element_type == 'OPERATOR': + layouts.append(layout) + elif self.element_type == 'OPERATOR': + if not self.parent or self.parent.element_type != 'ROW': + layout = remove_op_ui = layout.row(align=True) + layouts.append(layout) self.draw_operator(context, layout) - if not layout: - return - for child in self.children: - child.draw_ui_element(context, parent_layout) + if layout: + for child in self.children: + child.draw_ui_recursive(context, layouts) + if self.element_type == 'ROW' and child != self.children[-1]: + layouts[-1].separator() if self.rig.cloudrig.ui_edit_mode: remove_op_ui.operator( 'object.cloudrig_ui_element_remove', text="", icon='X' ).element_index = self.index + def draw_property(self, context, layout): prop_owner, prop_value = self.prop_owner, self.prop_value if not prop_owner: @@ -1725,7 +1750,10 @@ class CloudRig_UIElement(PropertyGroup): op_icon = self.icon if not self.icon or self.icon == 'NONE': op_icon = 'BLANK1' - op_props = layout.operator(self.bl_idname, text=self.display_name, icon=op_icon) + display_name = self.display_name + if self.parent and self.parent.element_type == 'PROPERTY': + display_name = "" + op_props = layout.operator(self.bl_idname, text=display_name, icon=op_icon) feed_op_props(op_props, self.op_kwargs) return op_props @@ -1748,13 +1776,13 @@ class CLOUDRIG_PT_custom_ui(CLOUDRIG_PT_base): layout = self.layout layout.use_property_split = False layout.use_property_decorate = False - layout = layout.column(align=True) + col = layout.column(align=True) rig = context.active_object # TODO for elem in rig.cloudrig_ui: if not elem.parent: - elem.draw_ui_element(context, layout) + elem.draw_ui_recursive(context, [layout, col]) ####################################### diff --git a/rig_component_features/properties_ui_editor.py b/rig_component_features/properties_ui_editor.py index c98ac98..6fd8014 100644 --- a/rig_component_features/properties_ui_editor.py +++ b/rig_component_features/properties_ui_editor.py @@ -28,6 +28,7 @@ def draw_ui_editing(context, layout, ui_element, operator): # layout.prop(ui_element, 'prop_owner_path') # layout.prop(ui_element, 'is_custom_prop') + def draw_parent_picking(context, layout, ui_element, operator): parent_row = layout.row() if operator.create_new_ui: @@ -41,6 +42,7 @@ def draw_parent_picking(context, layout, ui_element, operator): if context.scene.cloudrig_ui_parent_selector: parent_row.prop(operator, 'create_new_ui', text="", icon='ADD') + def draw_prop_editing(context, layout, ui_element, operator): rig = find_cloudrig(context) @@ -99,25 +101,26 @@ def draw_prop_editing(context, layout, ui_element, operator): def draw_op_editing(context, layout, ui_element, operator): if operator.use_batch_add: return - op_box = layout.box().column() - op_box.prop(operator.temp_kmi, 'idname', text="Operator") + layout.prop(operator.temp_kmi, 'idname', text="Operator") operator.op_kwargs_dict = {} - if operator.temp_kmi.idname: - box = None - op_rna = eval("bpy.ops." + operator.temp_kmi.idname).get_rna_type() - for key, value in op_rna.properties.items(): - if key == 'rna_type': - continue - if not box: - box = op_box.box().column(align=True) - box.prop(operator.temp_kmi.properties, key) - operator.op_kwargs_dict[key] = str( - getattr(operator.temp_kmi.properties, key) - ) - icons = UILayout.bl_rna.functions["prop"].parameters["icon"] - op_box.prop_search( - ui_element, 'icon', icons, 'enum_items', icon=ui_element.icon + if not operator.temp_kmi.idname: + return + + box = None + op_rna = eval("bpy.ops." + operator.temp_kmi.idname).get_rna_type() + for key, value in op_rna.properties.items(): + if key == 'rna_type': + continue + if not box: + box = layout.box().column(align=True) + box.prop(operator.temp_kmi.properties, key) + operator.op_kwargs_dict[key] = str( + getattr(operator.temp_kmi.properties, key) ) + icons = UILayout.bl_rna.functions["prop"].parameters["icon"] + layout.prop_search( + ui_element, 'icon', icons, 'enum_items', icon=ui_element.icon + ) layout.prop(ui_element, 'display_name') @@ -291,8 +294,8 @@ class UIElementAddMixin: self.temp_kmi = context.window_manager.keyconfigs.default.keymaps[ 'Info' ].keymap_items.new('', 'NUMPAD_5', 'PRESS') - if self.bl_idname: - self.temp_kmi.idname = self.bl_idname + if self.ui_element.bl_idname: + self.temp_kmi.idname = self.ui_element.bl_idname if self.ui_element.op_kwargs: op_props = self.temp_kmi.properties feed_op_props(op_props, self.ui_element.op_kwargs) @@ -330,19 +333,12 @@ class CLOUDRIG_OT_ui_element_add(UIElementAddMixin, Operator): label.element_type = 'LABEL' label.display_name = self.new_label_name parent = label - if self.new_row_name: - row = rig.cloudrig_ui.add() - row.parent = parent - row.element_type = 'ROW' - row.display_name = self.new_row_name - parent = row - if ( - temp_ui_element.element_type in {'PANEL', 'LABEL', 'ROW'} - and temp_ui_element.display_name.strip() == "" - ): - self.report({'ERROR'}, "This UI element must have a display name!") - return {'CANCELLED'} + row = rig.cloudrig_ui.add() + row.parent = parent + row.element_type = 'ROW' + row.display_name = self.new_row_name or temp_ui_element.prop_name + parent = row new_ui_element = rig.cloudrig_ui.add() @@ -355,6 +351,9 @@ class CLOUDRIG_OT_ui_element_add(UIElementAddMixin, Operator): new_ui_element.parent = parent new_ui_element.element_type = self.element_type + if self.element_type == 'OPERATOR': + new_ui_element.bl_idname = self.temp_kmi.idname + wipe_parent_selector(context) del rig['cloudrig_ui_new_element'] -- 2.30.2 From aeb89d2a7b6512c08dd576a9999b8693648b1e92 Mon Sep 17 00:00:00 2001 From: Demeter Dzadik Date: Fri, 9 Aug 2024 17:19:37 +0100 Subject: [PATCH 08/10] Ability to edit UI elements --- generation/cloudrig.py | 12 ++ .../properties_ui_editor.py | 152 ++++++++++++------ 2 files changed, 111 insertions(+), 53 deletions(-) diff --git a/generation/cloudrig.py b/generation/cloudrig.py index f07ff4e..fdf30f2 100644 --- a/generation/cloudrig.py +++ b/generation/cloudrig.py @@ -1476,6 +1476,9 @@ class CloudRig_UIElement(PropertyGroup): def parent(self, value: 'CloudRig_UIElement'): if not value: self.parent_index = -1 + elif self == value: + # Trying to set self as parent, just ignore and move on. + return else: self.parent_index = value.index @@ -1619,6 +1622,12 @@ class CloudRig_UIElement(PropertyGroup): return False + def copy_from(self, other): + for prop_name in self.bl_rna.properties.keys(): + if prop_name == 'rna_type': + continue + setattr(self, prop_name, getattr(other, prop_name)) + def draw_ui_recursive(self, context, layouts): if not self.should_draw or not layouts: return @@ -1664,6 +1673,9 @@ class CloudRig_UIElement(PropertyGroup): layouts[-1].separator() if self.rig.cloudrig.ui_edit_mode: + remove_op_ui.operator( + 'object.cloudrig_ui_element_edit', text="", icon='GREASEPENCIL' + ).element_index = self.index remove_op_ui.operator( 'object.cloudrig_ui_element_remove', text="", icon='X' ).element_index = self.index diff --git a/rig_component_features/properties_ui_editor.py b/rig_component_features/properties_ui_editor.py index 6fd8014..82cb694 100644 --- a/rig_component_features/properties_ui_editor.py +++ b/rig_component_features/properties_ui_editor.py @@ -14,15 +14,14 @@ import bpy, json def draw_ui_editing(context, layout, ui_element, operator): - layout.prop(operator, 'element_type', expand=True) - draw_parent_picking(context, layout, ui_element, operator) if operator.element_type == 'PROPERTY': draw_prop_editing(context, layout, ui_element, operator) - - if operator.element_type == 'OPERATOR': + elif operator.element_type == 'OPERATOR': draw_op_editing(context, layout, ui_element, operator) + else: + layout.prop(ui_element, 'display_name') # debug # layout.prop(ui_element, 'prop_owner_path') @@ -174,9 +173,12 @@ def update_property_selector(self, context): class UIElementAddMixin: def update_parent_element(self, context): ui_element = get_new_ui_element(context) - ui_element.parent_index = context.scene.cloudrig_ui_parent_selector[ - self.parent_element - ].index + if self.parent_element: + ui_element.parent_index = context.scene.cloudrig_ui_parent_selector[ + self.parent_element + ].index + else: + ui_element.parent_index = -1 parent_element: StringProperty( name="Parent Element", @@ -283,44 +285,7 @@ class UIElementAddMixin: return False return True - def invoke(self, context, _event): - update_parent_selector(context) - if not context.scene.cloudrig_ui_parent_selector: - self.create_new_ui = True - - self.ui_element = get_new_ui_element(context) - self.ui_element.reset() - - self.temp_kmi = context.window_manager.keyconfigs.default.keymaps[ - 'Info' - ].keymap_items.new('', 'NUMPAD_5', 'PRESS') - if self.ui_element.bl_idname: - self.temp_kmi.idname = self.ui_element.bl_idname - if self.ui_element.op_kwargs: - op_props = self.temp_kmi.properties - feed_op_props(op_props, self.ui_element.op_kwargs) - - return context.window_manager.invoke_props_dialog(self, width=500) - - def draw(self, context): - layout = self.layout - layout.use_property_decorate = False - layout.use_property_split = True - - draw_ui_editing(context, layout, self.ui_element, self) - - -class CLOUDRIG_OT_ui_element_add(UIElementAddMixin, Operator): - """Add UI Element""" - - bl_idname = "object.cloudrig_ui_element_add" - bl_label = "Add UI Element" - bl_options = {'INTERNAL', 'REGISTER', 'UNDO'} - - def execute(self, context): - rig = find_cloudrig(context) - temp_ui_element = get_new_ui_element(context) - + def ensure_parent_elements(self, rig, ui_element): parent = None if self.create_new_ui: if self.new_panel_name: @@ -337,22 +302,61 @@ class CLOUDRIG_OT_ui_element_add(UIElementAddMixin, Operator): row = rig.cloudrig_ui.add() row.parent = parent row.element_type = 'ROW' - row.display_name = self.new_row_name or temp_ui_element.prop_name + row.display_name = self.new_row_name or ui_element.prop_name parent = row + return parent + def init_temp_kmi(self, context): + self.temp_kmi = context.window_manager.keyconfigs.default.keymaps[ + 'Info' + ].keymap_items.new('', 'NUMPAD_5', 'PRESS') + if self.temp_element.bl_idname: + self.temp_kmi.idname = self.temp_element.bl_idname + if self.temp_element.op_kwargs: + op_props = self.temp_kmi.properties + feed_op_props(op_props, self.temp_element.op_kwargs) + +class CLOUDRIG_OT_ui_element_add(UIElementAddMixin, Operator): + """Add UI Element""" + + bl_idname = "object.cloudrig_ui_element_add" + bl_label = "Add UI Element" + bl_options = {'INTERNAL', 'REGISTER', 'UNDO'} + + def invoke(self, context, _event): + update_parent_selector(context) + if not context.scene.cloudrig_ui_parent_selector: + self.create_new_ui = True + + self.temp_element = get_new_ui_element(context) + self.temp_element.reset() + + self.init_temp_kmi(context) + + return context.window_manager.invoke_props_dialog(self, width=500) + + def draw(self, context): + layout = self.layout + layout.use_property_decorate = False + layout.use_property_split = True + + layout.prop(self, 'element_type', expand=True) + draw_ui_editing(context, layout, self.temp_element, self) + + def execute(self, context): + rig = find_cloudrig(context) + temp_ui_element = get_new_ui_element(context) new_ui_element = rig.cloudrig_ui.add() + new_ui_element.copy_from(temp_ui_element) - for prop_name in new_ui_element.bl_rna.properties.keys(): - if prop_name == 'rna_type': - continue - setattr(new_ui_element, prop_name, getattr(temp_ui_element, prop_name)) - + parent = self.ensure_parent_elements(rig, temp_ui_element) if parent: new_ui_element.parent = parent new_ui_element.element_type = self.element_type if self.element_type == 'OPERATOR': new_ui_element.bl_idname = self.temp_kmi.idname + # NOTE: The op kwargs have been fed to the UIElement in draw_op_editing(). wipe_parent_selector(context) del rig['cloudrig_ui_new_element'] @@ -368,17 +372,59 @@ class CLOUDRIG_OT_ui_element_edit(UIElementAddMixin, Operator): bl_options = {'INTERNAL', 'REGISTER', 'UNDO'} element_index: IntProperty() + element_type: StringProperty() + + def invoke(self, context, _event): + rig = find_cloudrig(context) + + self.temp_element = get_new_ui_element(context) + self.temp_element.reset() + elem_to_edit = rig.cloudrig_ui[self.element_index] + self.temp_element.copy_from(elem_to_edit) + + if self.temp_element.element_type in {'PROPERTY', 'OPERATOR'}: + self.element_type = self.temp_element.element_type + else: + self.element_type = "" + + self.init_temp_kmi(context) + + prop_owner = self.temp_element.prop_owner + + if type(prop_owner) == PoseBone: + self.prop_owner_type = 'BONE' + self.prop_bone = prop_owner.name + elif type(prop_owner) == BoneCollection: + self.prop_owner_type = 'COLLECTION' + self.prop_coll = prop_owner.name + else: + self.prop_owner_type = 'DATA_PATH' + + if self.temp_element.parent_index > -1: + self.parent_element = rig.cloudrig_ui[self.temp_element.parent_index].identifier + else: + self.parent_element = "" + + self.prop_data_path = self.temp_element.prop_owner_path + + return context.window_manager.invoke_props_dialog(self, width=500) def draw(self, context): layout = self.layout layout.use_property_decorate = False layout.use_property_split = True + draw_ui_editing(context, layout, self.temp_element, self) + + def execute(self, context): rig = find_cloudrig(context) elem_to_edit = rig.cloudrig_ui[self.element_index] - draw_ui_editing(context, layout, elem_to_edit, self) - + elem_to_edit.copy_from(self.temp_element) + if self.element_type == 'OPERATOR': + elem_to_edit.bl_idname = self.temp_kmi.idname + # NOTE: The op kwargs have been fed to the UIElement in draw_op_editing(). + return {'FINISHED'} class CLOUDRIG_OT_ui_element_remove(Operator): """Remove this UI element.\n\n""" """Ctrl: Do not remove children""" -- 2.30.2 From 391b0914cc3fe6967e4052acbea8ba9c7a936d9a Mon Sep 17 00:00:00 2001 From: Demeter Dzadik Date: Sun, 11 Aug 2024 01:16:09 +0100 Subject: [PATCH 09/10] Fix nested drawing and deletion --- generation/cloudrig.py | 88 +++++++++++++------ .../properties_ui_editor.py | 30 +++++-- 2 files changed, 81 insertions(+), 37 deletions(-) diff --git a/generation/cloudrig.py b/generation/cloudrig.py index fdf30f2..3b73970 100644 --- a/generation/cloudrig.py +++ b/generation/cloudrig.py @@ -1479,6 +1479,12 @@ class CloudRig_UIElement(PropertyGroup): elif self == value: # Trying to set self as parent, just ignore and move on. return + elif value.element_type == 'OPERATOR': + # Operators are not allowed children. + return + elif self.element_type == 'ROW' == value.element_type: + # Rows cannot be parented to rows. + return else: self.parent_index = value.index @@ -1607,6 +1613,13 @@ class CloudRig_UIElement(PropertyGroup): def children(self): return [elem for elem in self.rig.cloudrig_ui if elem.parent == self] + @property + def children_recursive(self): + ret = self.children + for child in self.children: + ret.extend(child.children_recursive) + return ret + @property def should_draw(self): if not self.parent: @@ -1632,54 +1645,73 @@ class CloudRig_UIElement(PropertyGroup): if not self.should_draw or not layouts: return + # We copy the layout stack so we can modify it without affecting the higher scope + layouts = layouts[:] layout = layouts[-1] - if self.parent and self.parent.element_type == 'PROPERTY': - layouts.pop() - layout = layouts[-1] - - remove_op_ui = layout - if self.element_type == 'PANEL': # TODO: Figure out how to allow elements to be drawn in the header. header, layout = layout.panel(idname=str(self.index) + self.display_name) header.label(text=self.display_name) - remove_op_ui = header - layouts.append(layout) + self.draw_ui_edit_buttons(header) + elif self.element_type == 'LABEL': - layout = remove_op_ui = layout.row() + row = layout.row() if self.display_name: - layout.label(text=self.display_name) + row.label(text=self.display_name) + self.draw_ui_edit_buttons(row) + elif self.element_type == 'ROW': - layout = remove_op_ui = layout.row() - layouts.append(layout) + row = layout.row(align=True) + layouts.append(row) + for child in self.children: + child.draw_ui_recursive(context, layouts) + if child != self.children[-1]: + row.separator() + layouts.pop() + # NOTE: We deliberately skip drawing edit buttons for a Row. + # A Row should be automatically deleted when it no longer has any elements. + return + elif self.element_type == 'PROPERTY': - layout = remove_op_ui = layout.row(align=True) - layouts.append(layout) - self.draw_property(context, layout) - if any([child.should_draw and child.element_type!='OPERATOR' for child in self.children]): - layout = layout.box() - layouts.append(layout) + row = layout.row(align=True) + self.draw_property(context, row) + for child in self.children: + if child.element_type == 'OPERATOR': + child.draw_ui_recursive(context, layouts + [row]) + for child in self.children: + box = None + if child.should_draw and child.element_type == 'PROPERTY': + if not box: + if self.parent.element_type == 'ROW': + layouts.pop() + box = layouts[-1].box() + layouts.append(box) + child.draw_ui_recursive(context, layouts) + self.draw_ui_edit_buttons(row) + return + elif self.element_type == 'OPERATOR': if not self.parent or self.parent.element_type != 'ROW': - layout = remove_op_ui = layout.row(align=True) + layout = layout.row(align=True) layouts.append(layout) self.draw_operator(context, layout) + self.draw_ui_edit_buttons(layout) if layout: for child in self.children: child.draw_ui_recursive(context, layouts) - if self.element_type == 'ROW' and child != self.children[-1]: - layouts[-1].separator() - if self.rig.cloudrig.ui_edit_mode: - remove_op_ui.operator( - 'object.cloudrig_ui_element_edit', text="", icon='GREASEPENCIL' - ).element_index = self.index - remove_op_ui.operator( - 'object.cloudrig_ui_element_remove', text="", icon='X' - ).element_index = self.index + def draw_ui_edit_buttons(self, layout): + if not self.rig.cloudrig.ui_edit_mode: + return + layout.operator( + 'object.cloudrig_ui_element_edit', text="", icon='GREASEPENCIL' + ).element_index = self.index + layout.operator( + 'object.cloudrig_ui_element_remove', text="", icon='X' + ).element_index = self.index def draw_property(self, context, layout): prop_owner, prop_value = self.prop_owner, self.prop_value diff --git a/rig_component_features/properties_ui_editor.py b/rig_component_features/properties_ui_editor.py index 82cb694..b84ed53 100644 --- a/rig_component_features/properties_ui_editor.py +++ b/rig_component_features/properties_ui_editor.py @@ -38,6 +38,15 @@ def draw_parent_picking(context, layout, ui_element, operator): parent_row.prop_search( operator, 'parent_element', context.scene, 'cloudrig_ui_parent_selector' ) + rig = find_cloudrig(context) + if operator.parent_element and operator.parent_element in context.scene.cloudrig_ui_parent_selector: + parent_ui = rig.cloudrig_ui[context.scene.cloudrig_ui_parent_selector[operator.parent_element].index] + if parent_ui and parent_ui.element_type == 'PROPERTY': + value = parent_ui.prop_value + value_type, is_array = rna_idprop_value_item_type(value) + if value_type in {bool, int}: + layout.prop(ui_element, 'parent_values') + if context.scene.cloudrig_ui_parent_selector: parent_row.prop(operator, 'create_new_ui', text="", icon='ADD') @@ -375,6 +384,7 @@ class CLOUDRIG_OT_ui_element_edit(UIElementAddMixin, Operator): element_type: StringProperty() def invoke(self, context, _event): + update_parent_selector(context) rig = find_cloudrig(context) self.temp_element = get_new_ui_element(context) @@ -427,7 +437,7 @@ class CLOUDRIG_OT_ui_element_edit(UIElementAddMixin, Operator): return {'FINISHED'} class CLOUDRIG_OT_ui_element_remove(Operator): - """Remove this UI element.\n\n""" """Ctrl: Do not remove children""" + """Remove this UI element.\n\nCtrl: Do not remove children""" bl_idname = "object.cloudrig_ui_element_remove" bl_label = "Remove UI Element" @@ -453,19 +463,21 @@ class CLOUDRIG_OT_ui_element_remove(Operator): element_to_remove = rig.cloudrig_ui[index] fallback_parent = element_to_remove.parent + indicies_to_remove = [] + if self.recursive: - for child in element_to_remove.children: - self.remove_element(rig, child.index) + for child in element_to_remove.children_recursive: + indicies_to_remove.append(child.index) else: for child in element_to_remove.children: child.parent = fallback_parent + indicies_to_remove.append(element_to_remove.index) - for element in rig.cloudrig_ui: - if element.parent_index > index: - element.parent_index -= 1 - - rig.cloudrig_ui.remove(index) - + for i in reversed(sorted(indicies_to_remove)): + rig.cloudrig_ui.remove(i) + for elem in rig.cloudrig_ui: + if elem.parent_index > i: + elem.parent_index -= 1 def supports_custom_props(prop_owner): return isinstance(prop_owner, ID) or type(prop_owner) in {PoseBone, BoneCollection} -- 2.30.2 From b4e75c883788d42688ae33d97d3a40de41ae5480 Mon Sep 17 00:00:00 2001 From: Demeter Dzadik Date: Sun, 11 Aug 2024 01:34:01 +0100 Subject: [PATCH 10/10] Ability to add rows to existing labels with the + icon --- .../properties_ui_editor.py | 56 +++++++++++++------ 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/rig_component_features/properties_ui_editor.py b/rig_component_features/properties_ui_editor.py index b84ed53..6e7a8b2 100644 --- a/rig_component_features/properties_ui_editor.py +++ b/rig_component_features/properties_ui_editor.py @@ -295,24 +295,47 @@ class UIElementAddMixin: return True def ensure_parent_elements(self, rig, ui_element): + """Of the specified Panel, Label, and Row names, + create any that don't already exist. + If no Row name was provided, create one based on the UI element name. + """ parent = None + + root_panels = {elem.display_name: elem for elem in rig.cloudrig_ui if not elem.parent and elem.element_type == 'PANEL'} + if self.create_new_ui: if self.new_panel_name: - parent = rig.cloudrig_ui.add() - parent.element_type = 'PANEL' - parent.display_name = self.new_panel_name - if self.new_label_name: - label = rig.cloudrig_ui.add() - label.parent = parent - label.element_type = 'LABEL' - label.display_name = self.new_label_name - parent = label + if self.new_panel_name not in root_panels: + parent = rig.cloudrig_ui.add() + parent.element_type = 'PANEL' + parent.display_name = self.new_panel_name + else: + parent = root_panels[self.new_panel_name] + + panel_labels = {elem.display_name : elem for elem in parent.children if elem.element_type == 'LABEL'} + if self.new_label_name: + if self.new_label_name not in panel_labels: + label = rig.cloudrig_ui.add() + label.parent = parent + label.element_type = 'LABEL' + label.display_name = self.new_label_name + parent = label + else: + parent = panel_labels[self.new_label_name] + + parent_rows = {elem.display_name : elem for elem in parent.children if elem.element_type == 'ROW'} + + if not self.new_row_name: + self.new_row_name = "Row: " + ui_element.display_name + if self.new_row_name not in parent_rows: + row = rig.cloudrig_ui.add() + row.parent = parent + row.element_type = 'ROW' + row.display_name = self.new_row_name or ui_element.prop_name + parent = row + else: + parent = parent_rows[self.new_row_name] - row = rig.cloudrig_ui.add() - row.parent = parent - row.element_type = 'ROW' - row.display_name = self.new_row_name or ui_element.prop_name - parent = row return parent def init_temp_kmi(self, context): @@ -355,10 +378,11 @@ class CLOUDRIG_OT_ui_element_add(UIElementAddMixin, Operator): def execute(self, context): rig = find_cloudrig(context) temp_ui_element = get_new_ui_element(context) - new_ui_element = rig.cloudrig_ui.add() - new_ui_element.copy_from(temp_ui_element) parent = self.ensure_parent_elements(rig, temp_ui_element) + + new_ui_element = rig.cloudrig_ui.add() + new_ui_element.copy_from(temp_ui_element) if parent: new_ui_element.parent = parent new_ui_element.element_type = self.element_type -- 2.30.2