(On Hold) Rework Properties UI Editor #159
@ -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,
|
||||
|
@ -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,7 +613,7 @@ 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.
|
||||
"""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.
|
||||
@ -623,41 +621,43 @@ class POSE_OT_cloudrig_toggle_ikfk_bake(SnapBakeOpMixin, CloudRigOperator):
|
||||
# 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
|
||||
"""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
|
||||
"""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.
|
||||
"""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
|
||||
"""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
|
||||
@ -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
|
||||
"""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
|
||||
|
||||
@ -1695,7 +1744,8 @@ class CLOUDRIG_PT_custom_ui(CLOUDRIG_PT_base):
|
||||
|
||||
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(
|
||||
|
@ -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,
|
||||
]
|
Loading…
Reference in New Issue
Block a user