(On Hold) Rework Properties UI Editor #159
@ -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,
|
||||||
|
@ -14,6 +14,7 @@ from bpy.props import (
|
|||||||
EnumProperty,
|
EnumProperty,
|
||||||
PointerProperty,
|
PointerProperty,
|
||||||
IntProperty,
|
IntProperty,
|
||||||
|
CollectionProperty,
|
||||||
)
|
)
|
||||||
from bpy.types import (
|
from bpy.types import (
|
||||||
bpy_struct,
|
bpy_struct,
|
||||||
@ -582,7 +583,6 @@ 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)
|
||||||
@ -598,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.
|
||||||
@ -647,7 +646,9 @@ class POSE_OT_cloudrig_toggle_ikfk_bake(SnapBakeOpMixin, CloudRigOperator):
|
|||||||
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.
|
||||||
@ -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)
|
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
|
||||||
):
|
):
|
||||||
@ -744,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 ########
|
||||||
#######################################
|
#######################################
|
||||||
@ -945,6 +943,10 @@ class CLOUDRIG_PT_settings(CLOUDRIG_PT_base):
|
|||||||
if rig.cloudrig.ui_edit_mode:
|
if rig.cloudrig.ui_edit_mode:
|
||||||
if hasattr(bpy.ops.pose, 'cloudrig_add_property_to_ui'):
|
if hasattr(bpy.ops.pose, 'cloudrig_add_property_to_ui'):
|
||||||
layout.operator('pose.cloudrig_add_property_to_ui', icon='ADD')
|
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:
|
if ui_data:
|
||||||
for panel_name, panel_data in ui_data.items():
|
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
|
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 ###########
|
########### Rig Preferences ###########
|
||||||
#######################################
|
#######################################
|
||||||
@ -1615,7 +2018,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
|
||||||
@ -2652,7 +3054,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.")
|
||||||
@ -2971,10 +3375,12 @@ def register_hotkey(
|
|||||||
#######################################
|
#######################################
|
||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
|
CloudRig_UIElement,
|
||||||
CloudRig_RigPreferences,
|
CloudRig_RigPreferences,
|
||||||
CloudRigBoneCollection,
|
CloudRigBoneCollection,
|
||||||
CLOUDRIG_UL_collections,
|
CLOUDRIG_UL_collections,
|
||||||
CLOUDRIG_PT_settings,
|
CLOUDRIG_PT_settings,
|
||||||
|
CLOUDRIG_PT_custom_ui,
|
||||||
CLOUDRIG_PT_hotkeys_panel,
|
CLOUDRIG_PT_hotkeys_panel,
|
||||||
CLOUDRIG_PT_collections_sidebar,
|
CLOUDRIG_PT_collections_sidebar,
|
||||||
CLOUDRIG_PT_collections_filter,
|
CLOUDRIG_PT_collections_filter,
|
||||||
@ -3036,13 +3442,12 @@ 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(
|
||||||
type=CloudRig_RigPreferences, override={'LIBRARY_OVERRIDABLE'}
|
type=CloudRig_RigPreferences, override={'LIBRARY_OVERRIDABLE'}
|
||||||
)
|
)
|
||||||
|
bpy.types.Object.cloudrig_ui = CollectionProperty(type=CloudRig_UIElement)
|
||||||
|
|
||||||
bpy.types.BoneCollection.cloudrig_info = PointerProperty(
|
bpy.types.BoneCollection.cloudrig_info = PointerProperty(
|
||||||
type=CloudRigBoneCollection, override={'LIBRARY_OVERRIDABLE'}
|
type=CloudRigBoneCollection, override={'LIBRARY_OVERRIDABLE'}
|
||||||
|
@ -9,6 +9,7 @@ from . import (
|
|||||||
object,
|
object,
|
||||||
parenting,
|
parenting,
|
||||||
properties_ui,
|
properties_ui,
|
||||||
|
properties_ui_editor,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -23,6 +24,7 @@ modules = [
|
|||||||
parenting,
|
parenting,
|
||||||
properties_ui,
|
properties_ui,
|
||||||
component_params_ui,
|
component_params_ui,
|
||||||
|
properties_ui_editor,
|
||||||
]
|
]
|
||||||
|
|
||||||
# Dictionary of modules that have a Params class, and want to register
|
# Dictionary of modules that have a Params class, and want to register
|
||||||
|
@ -13,7 +13,7 @@ from bpy.types import (
|
|||||||
Modifier,
|
Modifier,
|
||||||
)
|
)
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from bpy.props import StringProperty, BoolProperty, CollectionProperty
|
from bpy.props import StringProperty, BoolProperty, CollectionProperty, IntProperty
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from ..generation.cloudrig import (
|
from ..generation.cloudrig import (
|
||||||
unquote_custom_prop_name,
|
unquote_custom_prop_name,
|
||||||
@ -1203,6 +1203,7 @@ def redraw_viewport():
|
|||||||
|
|
||||||
class UIPathProperty(PropertyGroup):
|
class UIPathProperty(PropertyGroup):
|
||||||
name: StringProperty()
|
name: StringProperty()
|
||||||
|
index: IntProperty()
|
||||||
ui_path: StringProperty()
|
ui_path: StringProperty()
|
||||||
current: StringProperty(description="Current value of this property. Used for pre-filling the Parent Values field")
|
current: StringProperty(description="Current value of this property. Used for pre-filling the Parent Values field")
|
||||||
|
|
||||||
|
558
rig_component_features/properties_ui_editor.py
Normal file
558
rig_component_features/properties_ui_editor.py
Normal 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,
|
||||||
|
]
|
Loading…
Reference in New Issue
Block a user