Blender_bevy_components_wor.../tools/bevy_components/components/ui.py
Mark Moissette 1353e14802
feat(bevy_components): qol improvements (#164)
* closes #163 
* closes #153 
* closes #154 
* feat(bevy_components): added tools for diagnostics/ finding & replacing invalid & unregistered components
   * added ui for listing invalid & unregistered components
   * added boilerplate & functionality for component renaming/replacing
   * injection of invalid status & message in case the conversion did not work well
   * added deletion of components individual & bulk
   * added handling of wrong string for unit structs : allows detection of more wrong values for components
   * added progress bars for bulk operators
   * added docs for new features
   * added tests
   * added small "attempt to fix" button for unit struct uis in case they are invalid
* feat(bevy_components): added progress indicators for from/to custom properties
* various other minor ui tweaks for workflow improvement
2024-03-07 16:29:04 +01:00

225 lines
11 KiB
Python

import json
import bpy
from ..registry.operators import COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_CURRENT
from .metadata import do_object_custom_properties_have_missing_metadata
from .operators import AddComponentOperator, CopyComponentOperator, Fix_Component_Operator, RemoveComponentOperator, GenerateComponent_From_custom_property_Operator, PasteComponentOperator, Toggle_ComponentVisibility
def draw_propertyGroup( propertyGroup, layout, nesting =[], rootName=None):
is_enum = getattr(propertyGroup, "with_enum")
is_list = getattr(propertyGroup, "with_list")
#nesting = nesting + [current_short_name] # we need this convoluted "nested path strings " workaround so that operators working on a given
# item in our components hierarchy can get the correct propertyGroup by STRINGS because of course, we cannot pass objects to operators...sigh
# if it is an enum, the first field name is always the list of enum variants, the others are the variants
field_names = propertyGroup.field_names
#print("")
#print("drawing", propertyGroup, nesting, "component_name", rootName)
#type_name = getattr(propertyGroup, "type_name", None)#propertyGroup.type_name if "type_name" in propertyGroup else ""
#print("type name", type_name)
#print("name", propertyGroup.name, "name2", getattr(propertyGroup, "name"), "short_name", getattr(propertyGroup, "short_name", None), "nesting", nesting)
if is_enum:
subrow = layout.row()
display_name = field_names[0] if propertyGroup.tupple_or_struct == "struct" else ""
subrow.prop(propertyGroup, field_names[0], text=display_name)
subrow.separator()
selection = getattr(propertyGroup, field_names[0])
for fname in field_names[1:]:
if fname == "variant_" + selection:
subrow = layout.row()
display_name = fname if propertyGroup.tupple_or_struct == "struct" else ""
nestedPropertyGroup = getattr(propertyGroup, fname)
nested = getattr(nestedPropertyGroup, "nested", False)
#print("nestedPropertyGroup", nestedPropertyGroup, fname, nested)
if nested:
draw_propertyGroup(nestedPropertyGroup, subrow.column(), nesting + [fname], rootName )
# if an enum variant is not a propertyGroup
break
elif is_list:
#print("show list", propertyGroup, dict(propertyGroup), propertyGroup.type_name)
item_list = getattr(propertyGroup, "list")
item_type = getattr(propertyGroup, "type_name_short")
list_index = getattr(propertyGroup, "list_index")
box = layout.box()
split = box.split(factor=0.9)
list_column, buttons_column = (split.column(),split.column())
list_column = list_column.box()
for index, item in enumerate(item_list):
row = list_column.row()
draw_propertyGroup(item, row, nesting, rootName)
icon = 'CHECKBOX_HLT' if list_index == index else 'CHECKBOX_DEHLT'
op = row.operator('generic_list.select_item', icon=icon, text="")
op.component_name = rootName
op.property_group_path = json.dumps(nesting)
op.selection_index = index
#various control buttons
buttons_column.separator()
row = buttons_column.row()
op = row.operator('generic_list.list_action', icon='ADD', text="")
op.action = 'ADD'
op.component_name = rootName
op.property_group_path = json.dumps(nesting)
row = buttons_column.row()
op = row.operator('generic_list.list_action', icon='REMOVE', text="")
op.action = 'REMOVE'
op.component_name = rootName
op.property_group_path = json.dumps(nesting)
buttons_column.separator()
row = buttons_column.row()
op = row.operator('generic_list.list_action', icon='TRIA_UP', text="")
op.action = 'UP'
op.component_name = rootName
op.property_group_path = json.dumps(nesting)
row = buttons_column.row()
op = row.operator('generic_list.list_action', icon='TRIA_DOWN', text="")
op.action = 'DOWN'
op.component_name = rootName
op.property_group_path = json.dumps(nesting)
else:
for fname in field_names:
subrow = layout.row()
nestedPropertyGroup = getattr(propertyGroup, fname)
nested = getattr(nestedPropertyGroup, "nested", False)
display_name = fname if propertyGroup.tupple_or_struct == "struct" else ""
if nested:
layout.separator()
layout.label(text=display_name) # this is the name of the field/sub field
layout.separator()
draw_propertyGroup(nestedPropertyGroup, subrow.column(), nesting + [fname], rootName )
else:
subrow.prop(propertyGroup, fname, text=display_name)
subrow.separator()
class BEVY_COMPONENTS_PT_ComponentsPanel(bpy.types.Panel):
bl_idname = "BEVY_COMPONENTS_PT_ComponentsPanel"
bl_label = "Components"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Bevy Components"
bl_context = "objectmode"
bl_parent_id = "BEVY_COMPONENTS_PT_MainPanel"
@classmethod
def poll(cls, context):
return (context.object is not None)
def draw(self, context):
object = context.object
layout = self.layout
# we get & load our component registry
registry = bpy.context.window_manager.components_registry
available_components = bpy.context.window_manager.components_list
registry_has_type_infos = registry.has_type_infos()
if object is not None:
row = layout.row(align=True)
row.prop(available_components, "list", text="Component")
row.prop(available_components, "filter",text="Filter")
# add components
row = layout.row(align=True)
op = row.operator(AddComponentOperator.bl_idname, text="Add", icon="ADD")
op.component_type = available_components.list
row.enabled = available_components.list != ''
layout.separator()
# paste components
row = layout.row(align=True)
row.operator(PasteComponentOperator.bl_idname, text="Paste component ("+bpy.context.window_manager.copied_source_component_name+")", icon="PASTEDOWN")
row.enabled = registry_has_type_infos and context.window_manager.copied_source_object != ''
layout.separator()
# upgrate custom props to components
upgradeable_customProperties = registry.has_type_infos() and do_object_custom_properties_have_missing_metadata(context.object)
if upgradeable_customProperties:
row = layout.row(align=True)
op = row.operator(GenerateComponent_From_custom_property_Operator.bl_idname, text="generate components from custom properties" , icon="LOOP_FORWARDS")
layout.separator()
components_in_object = object.components_meta.components
for component_name in sorted(dict(object)) : # sorted by component name, practical
if component_name == "components_meta":
continue
# anything withouth metadata gets skipped, we only want to see real components, not all custom props
component_meta = next(filter(lambda component: component["name"] == component_name, components_in_object), None)
if component_meta == None:
continue
component_invalid = getattr(component_meta, "invalid")
invalid_details = getattr(component_meta, "invalid_details")
component_visible = getattr(component_meta, "visible")
single_field = False
# our whole row
box = layout.box()
row = box.row(align=True)
# "header"
row.alert = component_invalid
row.prop(component_meta, "enabled", text="")
row.label(text=component_name)
# we fetch the matching ui property group
root_propertyGroup_name = registry.get_propertyGroupName_from_shortName(component_name)
if root_propertyGroup_name:
propertyGroup = getattr(component_meta, root_propertyGroup_name, None)
if propertyGroup:
# if the component has only 0 or 1 field names, display inline, otherwise change layout
single_field = len(propertyGroup.field_names) < 2
prop_group_location = box.row(align=True).column()
if single_field:
prop_group_location = row.column(align=True)#.split(factor=0.9)#layout.row(align=False)
if component_visible:
if component_invalid:
error_message = invalid_details if component_invalid else "Missing component UI data, please reload registry !"
prop_group_location.label(text=error_message)
draw_propertyGroup(propertyGroup, prop_group_location, [root_propertyGroup_name], component_name)
else :
row.label(text="details hidden, click on toggle to display")
else:
error_message = invalid_details if component_invalid else "Missing component UI data, please reload registry !"
row.label(text=error_message)
# "footer" with additional controls
if component_invalid:
if root_propertyGroup_name:
propertyGroup = getattr(component_meta, root_propertyGroup_name, None)
if propertyGroup:
unit_struct = len(propertyGroup.field_names) == 0
if unit_struct:
op = row.operator(Fix_Component_Operator.bl_idname, text="", icon="SHADERFX")
op.component_name = component_name
row.separator()
op = row.operator(RemoveComponentOperator.bl_idname, text="", icon="X")
op.component_name = component_name
row.separator()
op = row.operator(CopyComponentOperator.bl_idname, text="", icon="COPYDOWN")
op.source_component_name = component_name
op.source_object_name = object.name
row.separator()
#if not single_field:
toggle_icon = "TRIA_DOWN" if component_visible else "TRIA_RIGHT"
op = row.operator(Toggle_ComponentVisibility.bl_idname, text="", icon=toggle_icon)
op.component_name = component_name
#row.separator()
else:
layout.label(text ="Select an object to edit its components")