(On Hold) Rework Properties UI Editor #159

Open
Demeter Dzadik wants to merge 10 commits from new-props-ux into master

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
3 changed files with 430 additions and 103 deletions
Showing only changes of commit 3ce0db6ce4 - Show all commits

View File

@ -45,10 +45,10 @@ modules = [
# be registered before they themselves are. # be registered before they themselves are.
# - For Panels, they must be registered before their bl_parent_id is. # - 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. # - Hotkeys must come after `cloudrig`, since we're storing them on a panel.
rig_component_features,
rig_components, rig_components,
prefs, prefs,
generation, generation,
rig_component_features,
operators, operators,
properties, properties,
metarigs, metarigs,

View File

@ -583,8 +583,7 @@ class POSE_OT_cloudrig_toggle_ikfk_bake(SnapBakeOpMixin, CloudRigOperator):
self.report({'INFO'}, "Snapping complete.") self.report({'INFO'}, "Snapping complete.")
return {'FINISHED'} 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.""" """Overrides SnapBakeOpMixin to also select the IK pole before keying."""
print("PBONES:", pbones, select) print("PBONES:", pbones, select)
if select and self.target_value == 1 and self.ik_pole: 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: if self.target_value == 1 and self.ik_pole:
self.snap_pole_target() self.snap_pole_target()
def snap_pole_target(self) -> Matrix: def snap_pole_target(self) -> Matrix:
"""Snap the pole target based on the first IK bone. """Snap the pole target based on the first IK bone.
This needs to run after the IK wrist control had already been snapped. This needs to run after the IK wrist control had already been snapped.
@ -615,7 +613,7 @@ class POSE_OT_cloudrig_toggle_ikfk_bake(SnapBakeOpMixin, CloudRigOperator):
# https://blenderartists.org/t/visual-transform-helper-functions-for-2-5/500965 # https://blenderartists.org/t/visual-transform-helper-functions-for-2-5/500965
def perpendicular_vector(v): def perpendicular_vector(v):
""" Returns a vector that is perpendicular to the one given. """Returns a vector that is perpendicular to the one given.
The returned vector is _not_ guaranteed to be normalized. The returned vector is _not_ guaranteed to be normalized.
""" """
# Create a vector that is not aligned with v. # Create a vector that is not aligned with v.
@ -623,41 +621,43 @@ class POSE_OT_cloudrig_toggle_ikfk_bake(SnapBakeOpMixin, CloudRigOperator):
# that's guaranteed to not be pointing in the same # that's guaranteed to not be pointing in the same
# direction. # direction.
if abs(v[0]) < abs(v[1]): if abs(v[0]) < abs(v[1]):
tv = Vector((1,0,0)) tv = Vector((1, 0, 0))
else: else:
tv = Vector((0,1,0)) tv = Vector((0, 1, 0))
# Use cross prouct to generate a vector perpendicular to # Use cross prouct to generate a vector perpendicular to
# both tv and (more importantly) v. # both tv and (more importantly) v.
return v.cross(tv) return v.cross(tv)
def rotation_difference(mat1, mat2): def rotation_difference(mat1, mat2):
""" Returns the shortest-path rotational difference between two """Returns the shortest-path rotational difference between two
matrices. matrices.
""" """
q1 = mat1.to_quaternion() q1 = mat1.to_quaternion()
q2 = mat2.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: if angle > pi:
angle = -angle + (2*pi) angle = -angle + (2 * pi)
return angle return angle
def get_pose_matrix_in_other_space(mat, pose_bone): def get_pose_matrix_in_other_space(mat, pose_bone):
""" Returns the transform matrix relative to pose_bone's current """Returns the transform matrix relative to pose_bone's current
transform space. In other words, presuming that mat is in transform space. In other words, presuming that mat is in
armature space, slapping the returned matrix onto pose_bone armature space, slapping the returned matrix onto pose_bone
should give it the armature-space transforms of mat. 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): def set_pose_translation(pose_bone, mat):
""" Sets the pose bone's translation to the same translation as the given matrix. """Sets the pose bone's translation to the same translation as the given matrix.
Matrix should be given in bone's local space. Matrix should be given in bone's local space.
""" """
pose_bone.location = mat.to_translation() pose_bone.location = mat.to_translation()
def match_pole_target(ik_first, ik_last, pole, match_bone): def match_pole_target(ik_first, ik_last, pole, match_bone):
""" Places an IK chain's pole target to match ik_first's """Places an IK chain's pole target to match ik_first's
transforms to match_bone. All bones should be given as pose bones. transforms to match_bone. All bones should be given as pose bones.
You need to be in pose mode on the relevant armature object. You need to be in pose mode on the relevant armature object.
ik_first: first bone in the IK chain ik_first: first bone in the IK chain
@ -679,11 +679,11 @@ class POSE_OT_cloudrig_toggle_ikfk_bake(SnapBakeOpMixin, CloudRigOperator):
pv = perpendicular_vector(ikv).normalized() * length pv = perpendicular_vector(ikv).normalized() * length
def set_pole(pvi): def set_pole(pvi):
""" Set pole target's position based on a vector """Set pole target's position based on a vector
from the arm center line. from the arm center line.
""" """
# Translate pvi into armature space # Translate pvi into armature space
ploc = a + (ikv/2) + pvi ploc = a + (ikv / 2) + pvi
# Set pole target to location # Set pole target to location
mat = get_pose_matrix_in_other_space(Matrix.Translation(ploc), pole) 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) match_pole_target(ik_first, self.ik_last, ik_pole, fk_first)
return ik_pole.matrix return ik_pole.matrix
def map_single_frame_to_bone_matrices( def map_single_frame_to_bone_matrices(
self, context, frame_number, bones_to_snap, snap_to_bones 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}") bone_column.label(text=f"{' '*10} {self.ik_pole}")
####################################### #######################################
######## Convenience Operators ######## ######## Convenience Operators ########
####################################### #######################################
@ -1432,21 +1429,33 @@ def unquote_custom_prop_name(prop_name: str) -> str:
class CloudRig_UIElement(PropertyGroup): class CloudRig_UIElement(PropertyGroup):
@property
def rig(self):
return self.id_data
element_type: EnumProperty( element_type: EnumProperty(
name="Element Type", name="Element Type",
description="How this UI element is drawn", description="How this UI element is drawn",
items=[ items=[
('PANEL', "Panel", "Collapsible panel. May contain Panels, Labels, Rows"), ('PANEL', "Panel", "Collapsible panel. May contain Panels, Labels, Rows"),
('LABEL', "Label", "Label. 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"), ('OPERATOR', "Operator", "A single Operator. Must belong to a Row"),
], ],
) )
display_name: StringProperty( display_name: StringProperty(
name="Display Name", name="Display Name",
description="(Optional) Display name of this UI element", description="Display name of this UI element",
default="", default="",
) )
# NOTE: This needs to be updated when elements are removed. # 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", description="Index of the parent UI element",
default=-1, default=-1,
) )
@property @property
def parent(self): def parent(self):
if self.parent_index >= 0: if self.parent_index >= 0:
return self.id_data.cloudrig_ui[self.parent_index] return self.rig.cloudrig_ui[self.parent_index]
@parent.setter @parent.setter
def parent(self, value): def parent(self, value):
self.parent_index = value.index self.parent_index = value.index
@property @property
def index(self): 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: if elem == self:
return i return i
@ -1483,60 +1494,70 @@ class CloudRig_UIElement(PropertyGroup):
parent_values: StringProperty( parent_values: StringProperty(
# Supported Types: Panel, Label, Row, only when Element Type of parent element is Property. # Supported Types: Panel, Label, Row, only when Element Type of parent element is Property.
name="Parent Values", 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( texts: StringProperty(
# Supported Types: Property, only Boolean and Integer. # Supported Types: Property, only Boolean and Integer.
name="Texts", 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( bl_idname: StringProperty(
# Supported Types: Operator # Supported Types: Operator
name="Operator ID", name="Operator ID",
description="Operator bl_idname" description="Operator bl_idname",
) )
op_kwargs: StringProperty( op_kwargs: StringProperty(
# Supported Types: Operator # Supported Types: Operator
name="Operator Arguments", name="Operator Arguments",
description="Operator Keyword Arguments, as a json dict", description="Operator Keyword Arguments, as a json dict",
default="{}" default="{}",
) )
icon: StringProperty( icon: StringProperty(
# Supported Types: Label, Row, Property(bool), Operator # Supported Types: Label, Row, Property(bool), Operator
name="Icon", name="Icon",
description="Icon" description="Icon",
) )
icon_false: StringProperty( icon_false: StringProperty(
# Supported Types: Property(bool) # Supported Types: Property(bool)
name="Icon False", 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( prop_owner_path: StringProperty(
# Supported Types: Property # Supported Types: Property
name="Property Owner", 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 @property
def prop_owner(self): def prop_owner(self):
try: try:
return self.id_data.path_resolve(self.prop_owner_path) return self.rig.path_resolve(self.prop_owner_path)
except ValueError: except ValueError:
# This can happen eg. if user adds a constraint influence to the UI, then deletes the constraint. # This can happen eg. if user adds a constraint influence to the UI, then deletes the constraint.
return 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( prop_name: StringProperty(
# Supported Types: Property # Supported Types: Property
name="Property Name", name="Property Name",
description="Name of the property to be drawn" description="Name of the property to be drawn",
update=update_prop_name,
) )
@property @property
def bracketed_prop_name(self): def bracketed_prop_name(self):
if self.prop_is_custom: if self.is_custom_prop:
return f'["{self.prop_name}"]' return f'["{self.prop_name}"]'
return self.prop_name return self.prop_name
@property @property
def prop_value(self): def prop_value(self):
if not hasattr(self.prop_owner, 'path_resolve'): if not hasattr(self.prop_owner, 'path_resolve'):
@ -1548,14 +1569,15 @@ class CloudRig_UIElement(PropertyGroup):
# Property may have been removed. # Property may have been removed.
return {'MISSING'} return {'MISSING'}
prop_is_custom: BoolProperty( is_custom_prop: BoolProperty(
# Supported Types: Property # Supported Types: Property # TODO: This should be set from the update of prop_name.
name="Is Custom Property", 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 @property
def custom_prop_settings(self): def custom_prop_settings(self):
if not self.prop_is_custom: if not self.is_custom_prop:
return return
try: try:
return self.prop_owner.id_properties_ui(self.prop_name).as_dict() return self.prop_owner.id_properties_ui(self.prop_name).as_dict()
@ -1565,7 +1587,7 @@ class CloudRig_UIElement(PropertyGroup):
@property @property
def children(self): 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 @property
def should_draw(self): def should_draw(self):
@ -1578,7 +1600,7 @@ class CloudRig_UIElement(PropertyGroup):
return True return True
return False return False
def draw(self, context, layout): def draw_ui_element(self, context, layout):
if not self.should_draw or not layout: if not self.should_draw or not layout:
return return
@ -1597,19 +1619,23 @@ class CloudRig_UIElement(PropertyGroup):
if self.display_name: if self.display_name:
layout.label(text=self.display_name) layout.label(text=self.display_name)
if self.element_type == 'PROPERTY': 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) self.draw_property(context, layout)
if any([child.should_draw for child in self.children]): if any([child.should_draw for child in self.children]):
layout = layout.box() layout = layout.box()
if self.element_type == 'OPERATOR': if self.element_type == 'OPERATOR':
self.draw_operator(context, layout) self.draw_operator(context, layout)
if self.id_data.cloudrig.ui_edit_mode: if self.rig.cloudrig.ui_edit_mode:
remove_op_ui.operator('object.cloudrig_ui_element_remove', text="", icon='X').element_index = self.index remove_op_ui.operator(
'object.cloudrig_ui_element_remove', text="", icon='X'
).element_index = self.index
if not layout: if not layout:
return return
for child in self.children: for child in self.children:
child.draw(context, layout) child.draw_ui_element(context, layout)
def draw_property(self, context, layout): def draw_property(self, context, layout):
prop_owner, prop_value = self.prop_owner, self.prop_value prop_owner, prop_value = self.prop_owner, self.prop_value
@ -1627,8 +1653,7 @@ class CloudRig_UIElement(PropertyGroup):
icon='ERROR', icon='ERROR',
) )
if not self.display_name: display_name = self.display_name or self.prop_name
display_name = self.prop_name
bracketed_prop_name = self.bracketed_prop_name bracketed_prop_name = self.bracketed_prop_name
value_type, is_array = rna_idprop_value_item_type(prop_value) value_type, is_array = rna_idprop_value_item_type(prop_value)
@ -1637,15 +1662,25 @@ class CloudRig_UIElement(PropertyGroup):
# Property is a Datablock Pointer. # Property is a Datablock Pointer.
layout.prop(self.prop_owner, bracketed_prop_name, text=display_name) layout.prop(self.prop_owner, bracketed_prop_name, text=display_name)
elif value_type in {int, float, bool}: 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() text = self.texts[int(prop_value)].strip()
if text: if text:
display_name += ": " + text display_name += ": " + text
if value_type == bool: if value_type == bool:
icon = self.icon if prop_value else self.icon_flase 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}: elif value_type in {int, float}:
if self.prop_is_custom: if self.is_custom_prop:
# Property is a float/int/color # Property is a float/int/color
# For large ranges, a slider doesn't make sense. # For large ranges, a slider doesn't make sense.
prop_settings = self.custom_prop_settings prop_settings = self.custom_prop_settings
@ -1653,7 +1688,12 @@ class CloudRig_UIElement(PropertyGroup):
not is_array not is_array
and prop_settings['soft_max'] - prop_settings['soft_min'] < 100 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: else:
layout.prop(prop_owner, bracketed_prop_name, text=display_name) layout.prop(prop_owner, bracketed_prop_name, text=display_name)
elif value_type == str: elif value_type == str:
@ -1664,7 +1704,9 @@ class CloudRig_UIElement(PropertyGroup):
and prop_owner.target.type == 'ARMATURE' and prop_owner.target.type == 'ARMATURE'
): ):
# Special case for nice constraint sub-target selectors. # 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: else:
layout.prop(prop_owner, bracketed_prop_name) layout.prop(prop_owner, bracketed_prop_name)
else: else:
@ -1678,6 +1720,13 @@ class CloudRig_UIElement(PropertyGroup):
feed_op_props(op_props, self.op_kwargs) feed_op_props(op_props, self.op_kwargs)
return op_props 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): def __repr__(self):
return self.identifier return self.identifier
@ -1695,7 +1744,8 @@ class CLOUDRIG_PT_custom_ui(CLOUDRIG_PT_base):
for elem in rig.cloudrig_ui: for elem in rig.cloudrig_ui:
if not elem.parent: if not elem.parent:
elem.draw(context, layout) elem.draw_ui_element(context, layout)
####################################### #######################################
########### Rig Preferences ########### ########### Rig Preferences ###########
@ -1886,7 +1936,6 @@ class CloudRigBoneCollection(PropertyGroup):
if not component.active_bone_set: if not component.active_bone_set:
component.bone_sets_active_index = 0 component.bone_sets_active_index = 0
# Metarig: Update bone sets with this collection assigned to refer to the new name. # Metarig: Update bone sets with this collection assigned to refer to the new name.
if is_active_cloud_metarig(context): if is_active_cloud_metarig(context):
rig = context.pose_object or context.active_object rig = context.pose_object or context.active_object
@ -2923,7 +2972,9 @@ class POSE_OT_cloudrig_collection_clipboard_copy(CloudRigOperator):
counter += 1 counter += 1
json_obj[coll.name]['bone_names'] = [bone.name for bone in coll.bones] 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]['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: if counter == 0:
self.report({'ERROR'}, "No visible collections to copy.") self.report({'ERROR'}, "No visible collections to copy.")
@ -3309,8 +3360,6 @@ def register():
for c in classes: for c in classes:
if not is_registered(c): 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) register_class(c)
bpy.types.Object.cloudrig_prefs = PointerProperty( bpy.types.Object.cloudrig_prefs = PointerProperty(

View File

@ -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 .properties_ui import UIPathProperty
from bpy.types import Operator from bpy.types import Operator, UILayout, ID, PoseBone, BoneCollection
from bpy.props import CollectionProperty, StringProperty, IntProperty, BoolProperty from bpy.props import (
CollectionProperty,
StringProperty,
IntProperty,
BoolProperty,
EnumProperty,
PointerProperty,
)
from rna_prop_ui import rna_idprop_value_item_type
import bpy import bpy
class CLOUDRIG_OT_ui_element_add(Operator):
"""Add a UI element"""
bl_idname = "object.cloudrig_ui_element_add" def draw_ui_editing(context, layout, ui_element, operator):
bl_label = "Add Property to UI" rig = find_cloudrig(context)
bl_options = {'REGISTER', 'UNDO'}
# Copy the definition of a single UIElement, which will be added layout.prop(ui_element, 'element_type')
# by this operator, when the "OK" button is clicked. name_row = layout.row()
__annotations__ = CloudRig_UIElement.__annotations__ if (
ui_element.element_type in {'PANEL', 'LABEL', 'ROW'}
and ui_element.display_name.strip() == ""
):
name_row.alert = True
parent_element: StringProperty(name="Parent Element") 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 @classmethod
def poll(cls, context): def poll(cls, context):
@ -25,15 +216,19 @@ class CLOUDRIG_OT_ui_element_add(Operator):
return True return True
def invoke(self, context, _event): 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: self.temp_kmi = context.window_manager.keyconfigs.default.keymaps[
if ui_element.element_type in {'PANEL', 'LABEL', 'ROW'}: 'Info'
parent_option = context.scene.cloudrig_ui_parent_selector.add() ].keymap_items.new('', 'NUMPAD_5', 'PRESS')
parent_option.name = ui_element.identifier if self.bl_idname:
parent_option.index = ui_element.index 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) return context.window_manager.invoke_props_dialog(self, width=500)
@ -42,23 +237,62 @@ class CLOUDRIG_OT_ui_element_add(Operator):
layout.use_property_decorate = False layout.use_property_decorate = False
layout.use_property_split = True layout.use_property_split = True
layout.prop(self, 'element_type') draw_ui_editing(context, layout, self.ui_element, self)
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') 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): def execute(self, context):
rig = find_cloudrig(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 = rig.cloudrig_ui.add()
new_ui_element.display_name = self.display_name
new_ui_element.element_type = self.element_type for prop_name in new_ui_element.bl_rna.properties.keys():
if self.parent_element: if prop_name == 'rna_type':
new_ui_element.parent_index = context.scene.cloudrig_ui_parent_selector[self.parent_element].index 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'} 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): class CLOUDRIG_OT_ui_element_remove(Operator):
"""Remove this UI element.\n\n""" \ """Remove this UI element.\n\n""" """Ctrl: Do not remove children"""
"""Ctrl: Do not remove children"""
bl_idname = "object.cloudrig_ui_element_remove" bl_idname = "object.cloudrig_ui_element_remove"
bl_label = "Remove UI Element" bl_label = "Remove UI Element"
@ -97,12 +331,56 @@ class CLOUDRIG_OT_ui_element_remove(Operator):
rig.cloudrig_ui.remove(index) 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(): def register():
bpy.types.Scene.cloudrig_ui_parent_selector = CollectionProperty( bpy.types.Scene.cloudrig_ui_parent_selector = CollectionProperty(
type=UIPathProperty type=UIPathProperty
) )
bpy.types.Scene.cloudrig_ui_prop_selector = CollectionProperty(type=UIPathProperty)
bpy.types.Object.cloudrig_ui_new_element = PointerProperty(type=CloudRig_UIElement)
registry = [ registry = [
CLOUDRIG_OT_ui_element_add, CLOUDRIG_OT_ui_element_add,
CLOUDRIG_OT_ui_element_edit,
CLOUDRIG_OT_ui_element_remove, CLOUDRIG_OT_ui_element_remove,
] ]