(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.
5 changed files with 1004 additions and 38 deletions

View File

@ -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,

View File

@ -14,6 +14,7 @@ from bpy.props import (
EnumProperty,
PointerProperty,
IntProperty,
CollectionProperty,
)
from bpy.types import (
bpy_struct,
@ -582,7 +583,6 @@ 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):
"""Overrides SnapBakeOpMixin to also select the IK pole before keying."""
print("PBONES:", pbones, select)
@ -598,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.
@ -647,7 +646,9 @@ class POSE_OT_cloudrig_toggle_ikfk_bake(SnapBakeOpMixin, CloudRigOperator):
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.
@ -714,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
):
@ -744,7 +743,6 @@ class POSE_OT_cloudrig_toggle_ikfk_bake(SnapBakeOpMixin, CloudRigOperator):
bone_column.label(text=f"{' '*10} {self.ik_pole}")
#######################################
######## Convenience Operators ########
#######################################
@ -945,6 +943,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():
@ -1426,6 +1428,407 @@ def unquote_custom_prop_name(prop_name: str) -> str:
return prop_name
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",
),
('OPERATOR', "Operator", "A single Operator. Must belong to a Row"),
],
)
display_name: StringProperty(
name="Display Name",
description="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.rig.cloudrig_ui[self.parent_index]
@parent.setter
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
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
@property
def index(self):
for i, elem in enumerate(self.rig.cloudrig_ui):
if elem == self:
return i
return -1
@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",
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="{}",
)
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(
# 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.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' and self.prop_owner:
self.display_name = self.prop_owner.name
prop_name: StringProperty(
# Supported Types: Property
name="Property Name",
description="Name of the property to be drawn",
update=update_prop_name,
)
@property
def bracketed_prop_name(self):
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'):
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'}
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",
)
@property
def custom_prop_settings(self):
if not self.is_custom_prop:
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.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:
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 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
# We copy the layout stack so we can modify it without affecting the higher scope
layouts = layouts[:]
layout = layouts[-1]
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)
self.draw_ui_edit_buttons(header)
elif self.element_type == 'LABEL':
row = layout.row()
if self.display_name:
row.label(text=self.display_name)
self.draw_ui_edit_buttons(row)
elif self.element_type == 'ROW':
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':
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 = 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)
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
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',
)
return
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)
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}:
texts = [t.strip() for t in self.texts.split(",")]
if (
texts
and not is_array
and len(texts) - 1 >= int(prop_value) >= 0
):
text = texts[int(prop_value)]
if text:
display_name += ": " + text
if value_type == bool:
icon = self.icon if prop_value else self.icon_false
layout.prop(
self.prop_owner,
bracketed_prop_name,
toggle=True,
text=display_name,
icon=icon,
)
elif value_type in {int, float}:
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
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'
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
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
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 = False
layout.use_property_decorate = False
col = layout.column(align=True)
rig = context.active_object # TODO
for elem in rig.cloudrig_ui:
if not elem.parent:
elem.draw_ui_recursive(context, [layout, col])
#######################################
########### Rig Preferences ###########
#######################################
@ -1615,7 +2018,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
@ -2652,7 +3054,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.")
@ -2971,10 +3375,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,
@ -3036,13 +3442,12 @@ 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(
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'}

View File

@ -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

View File

@ -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")

View File

@ -0,0 +1,558 @@
from ..generation.cloudrig import CloudRig_UIElement, find_cloudrig, feed_op_props
from .properties_ui import UIPathProperty
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, json
def draw_ui_editing(context, layout, ui_element, operator):
draw_parent_picking(context, layout, ui_element, operator)
if operator.element_type == 'PROPERTY':
draw_prop_editing(context, layout, ui_element, 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')
# 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')
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'
)
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')
def draw_prop_editing(context, layout, ui_element, operator):
rig = find_cloudrig(context)
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 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')
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):
if operator.use_batch_add:
return
layout.prop(operator.temp_kmi, 'idname', text="Operator")
operator.op_kwargs_dict = {}
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')
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)
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",
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)
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)
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)
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':
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,
)
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'},
default=False,
description="Add all custom properties of the selected ID to the UI",
)
@classmethod
def poll(cls, context):
rig = find_cloudrig(context)
if not rig:
return False
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:
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]
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)
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
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']
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()
element_type: StringProperty()
def invoke(self, context, _event):
update_parent_selector(context)
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]
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\nCtrl: 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]
fallback_parent = element_to_remove.parent
indicies_to_remove = []
if self.recursive:
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 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}
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,
]