(On Hold) Rework Properties UI Editor #159
@ -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,
|
||||||
@ -1426,6 +1427,254 @@ def unquote_custom_prop_name(prop_name: str) -> str:
|
|||||||
return prop_name
|
return prop_name
|
||||||
|
|
||||||
|
|
||||||
|
class CloudRig_UIElement(PropertyGroup):
|
||||||
|
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="(Optional) 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.id_data.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):
|
||||||
|
if elem == self:
|
||||||
|
return i
|
||||||
|
|
||||||
|
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="{}"
|
||||||
|
)
|
||||||
|
icon: StringProperty(
|
||||||
|
# Supported Types: Label, Row, Property(bool), Operator
|
||||||
|
name="Icon",
|
||||||
|
description="Icon"
|
||||||
|
)
|
||||||
|
icon_false: StringProperty(
|
||||||
|
# Supported Types: Property(bool)
|
||||||
|
name="Icon 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"
|
||||||
|
)
|
||||||
|
@property
|
||||||
|
def prop_owner(self):
|
||||||
|
try:
|
||||||
|
return self.id_data.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
|
||||||
|
|
||||||
|
prop_name: StringProperty(
|
||||||
|
# Supported Types: Property
|
||||||
|
name="Property Name",
|
||||||
|
description="Name of the property to be drawn"
|
||||||
|
)
|
||||||
|
@property
|
||||||
|
def bracketed_prop_name(self):
|
||||||
|
if self.prop_is_custom:
|
||||||
|
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'}
|
||||||
|
|
||||||
|
prop_is_custom: BoolProperty(
|
||||||
|
# Supported Types: Property
|
||||||
|
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.prop_is_custom:
|
||||||
|
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.id_data.cloudrig_ui if elem.parent==self]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_draw(self):
|
||||||
|
if not self.parent:
|
||||||
|
return True
|
||||||
|
if self.parent.element_type != 'PROPERTY':
|
||||||
|
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 draw(self, context, layout):
|
||||||
|
if not self.should_draw or not layout:
|
||||||
|
return
|
||||||
|
|
||||||
|
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)
|
||||||
|
if not layout:
|
||||||
|
return
|
||||||
|
if self.element_type == 'LABEL':
|
||||||
|
if self.display_name:
|
||||||
|
layout.label(text=self.display_name)
|
||||||
|
if self.element_type == 'ROW':
|
||||||
|
layout = layout.row()
|
||||||
|
if self.display_name:
|
||||||
|
layout.label(text=self.display_name)
|
||||||
|
if self.element_type == 'PROPERTY':
|
||||||
|
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)
|
||||||
|
|
||||||
|
for child in self.children:
|
||||||
|
child.draw(context, layout)
|
||||||
|
|
||||||
|
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',
|
||||||
|
)
|
||||||
|
|
||||||
|
if not self.display_name:
|
||||||
|
display_name = 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}:
|
||||||
|
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)
|
||||||
|
elif value_type in {int, float}:
|
||||||
|
if self.prop_is_custom:
|
||||||
|
# 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'
|
||||||
|
op_props = layout.operator(self.bl_idname, text=self.display_name, icon=op_icon)
|
||||||
|
feed_op_props(op_props, self.op_kwargs)
|
||||||
|
return op_props
|
||||||
|
|
||||||
|
|
||||||
|
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 = True
|
||||||
|
layout.use_property_decorate = False
|
||||||
|
|
||||||
|
rig = context.active_object # TODO
|
||||||
|
|
||||||
|
for elem in rig.cloudrig_ui:
|
||||||
|
if not elem.parent:
|
||||||
|
elem.draw(context, layout)
|
||||||
|
|
||||||
#######################################
|
#######################################
|
||||||
########### Rig Preferences ###########
|
########### Rig Preferences ###########
|
||||||
#######################################
|
#######################################
|
||||||
@ -2971,10 +3220,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,
|
||||||
@ -3043,6 +3294,7 @@ def register():
|
|||||||
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'}
|
||||||
|
Loading…
Reference in New Issue
Block a user