(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.
Showing only changes of commit 3534faebd4 - Show all commits

View File

@ -14,6 +14,7 @@ from bpy.props import (
EnumProperty,
PointerProperty,
IntProperty,
CollectionProperty,
)
from bpy.types import (
bpy_struct,
@ -1426,6 +1427,254 @@ def unquote_custom_prop_name(prop_name: str) -> str:
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 ###########
#######################################
@ -2971,10 +3220,12 @@ def register_hotkey(
#######################################
classes = (
CloudRig_UIElement,
CloudRig_RigPreferences,
CloudRigBoneCollection,
CLOUDRIG_UL_collections,
CLOUDRIG_PT_settings,
CLOUDRIG_PT_custom_ui,
CLOUDRIG_PT_hotkeys_panel,
CLOUDRIG_PT_collections_sidebar,
CLOUDRIG_PT_collections_filter,
@ -3043,6 +3294,7 @@ def register():
bpy.types.Object.cloudrig_prefs = PointerProperty(
type=CloudRig_RigPreferences, override={'LIBRARY_OVERRIDABLE'}
)
bpy.types.Object.cloudrig_ui = CollectionProperty(type=CloudRig_UIElement)
bpy.types.BoneCollection.cloudrig_info = PointerProperty(
type=CloudRigBoneCollection, override={'LIBRARY_OVERRIDABLE'}