(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.
# - 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

@ -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:
@ -1678,6 +1720,13 @@ class CloudRig_UIElement(PropertyGroup):
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(

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 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'}
def draw_ui_editing(context, layout, ui_element, operator):
rig = find_cloudrig(context)
# Copy the definition of a single UIElement, which will be added
# by this operator, when the "OK" button is clicked.
__annotations__ = CloudRig_UIElement.__annotations__
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
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
def poll(cls, context):
@ -25,15 +216,19 @@ 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)
@ -42,23 +237,62 @@ class CLOUDRIG_OT_ui_element_add(Operator):
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,
]