Help with custom UI stuff

Is it possible to script a custom UI feature where you have a list of items, and you can add or subtract from the list using [+] or [-] buttons?

I would like each item to be able to store information.

Thanks in advance!

Yes you can, with a custom UIList, template_list and a few operators:

import bpy
from bpy.props import *

sire_items = (
    ('s', "Scale", ""),
    ('i', "Inset", ""),
    ('r', "Rotate", ""),
    ('e', "Extrude", "")
)

sire_items_dict = {}
for identifier, label, description in sire_items:
    sire_items_dict[identifier] = label


class SireStep(bpy.types.PropertyGroup):
    name = StringProperty()
    
    trans = EnumProperty(items=sire_items) # default 's' required?
    
    val = FloatProperty(min=0.001, soft_max=1000, default=1.0, precision=3)
    
    rot = EnumProperty(items=(
        ('cw', 'CW', ''),
        ('ccw', 'CCW', '')),
        name="Rotation" # name required if enum expanded, or labels will be missing
    )
    
    other = EnumProperty(items=(
        ('in', 'In', ''),
        ('out', 'Out', '')),
        name="Other"
    )
    
class Sire(bpy.types.PropertyGroup):
    type_add = EnumProperty(
        items=sire_items,
        name = "some name",
        default = 's'
    )
    
    steps = CollectionProperty(type=SireStep, name="Sire Step")
    index = IntProperty() # min/max/default?


class SIRE_UL_steplist(bpy.types.UIList):
    
    def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
        
        # draw_item must handle the three layout types... Usually 'DEFAULT' and 'COMPACT' can share the same code.
        if self.layout_type in {'DEFAULT', 'COMPACT'}:

            row = layout.row(align=True)
            row.label(item.name)
            
            row.prop(item, "val", text="")
            
            sub = row.row(align=True)
            sub.scale_x = 0.4
            
            if item.name == sire_items_dict['r']:
                sub.prop(item, "rot", expand=True)
            else:
                sub.prop(item, "other", expand=True) # if not expanded, use text=""
            
        # 'GRID' layout type should be as compact as possible (typically a single icon!).
        elif self.layout_type in {'GRID'}:
            layout.alignment = 'CENTER'
            layout.label("", icon_value=icon)


# ------ operator 0 ------ add item to collection
class VIEW3D_OT_sire_step_add(bpy.types.Operator):
    bl_idname = 'view3d.sire_step_add'
    bl_label = 'Add'
    bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}

    @classmethod
    def poll(cls, context):
        return (context.active_object and 
                context.active_object.type == 'MESH' and 
                context.mode == 'EDIT_MESH')

    def execute(self, context):
        steps = context.scene.sire.steps

        step = steps.add()
        step.name = sire_items_dict[context.scene.sire.type_add]
        
        context.scene.sire.index = len(steps) - 1
        
        return {'FINISHED'}


# ------ operator 1 ------ remove item from collection
class VIEW3D_OT_sire_step_remove(bpy.types.Operator):
    bl_idname = 'view3d.sire_step_remove'
    bl_label = 'Remove'
    bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}

    @classmethod
    def poll(cls, context):
        return (context.active_object and
                context.active_object.type == 'MESH' and 
                context.mode == 'EDIT_MESH' and
                len(context.scene.sire.steps) > 0)
        
    def execute(self, context):
        idx = context.scene.sire.index
        move_idx = len(context.scene.sire.steps) - 1 == idx
        context.scene.sire.steps.remove(idx)
        if move_idx:
            context.scene.sire.index -= 1
        
        return {'FINISHED'}
    
    
# ------ operator 2 ------ move item up
class VIEW3D_OT_sire_step_moveup(bpy.types.Operator):
    bl_idname = 'view3d.sire_step_moveup'
    bl_label = 'Move up'
    bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}

    @classmethod
    def poll(cls, context):
        return (context.active_object and
                context.active_object.type == 'MESH' and
                context.mode == 'EDIT_MESH' and
                len(context.scene.sire.steps) > 0 and
                context.scene.sire.index > 0)

    def draw(self, context):
        layout = self.layout
        
    def execute(self, context):
        idx = context.scene.sire.index
        context.scene.sire.steps.move(idx, idx-1)
        context.scene.sire.index -= 1

        return {'FINISHED'}


# ------ operator 3 ------ move item down
class VIEW3D_OT_sire_step_movedown(bpy.types.Operator):
    bl_idname = 'view3d.sire_step_movedown'
    bl_label = 'Move down'
    bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}

    @classmethod
    def poll(cls, context):
        return (context.active_object and 
                context.active_object.type == 'MESH' and
                context.mode == 'EDIT_MESH' and
                len(context.scene.sire.steps) > 0 and
                context.scene.sire.index < len(context.scene.sire.steps) - 1)

    def execute(self, context):
        idx = context.scene.sire.index
        context.scene.sire.steps.move(idx, idx+1)
        context.scene.sire.index += 1
        
        return {'FINISHED'}


class VIEW3D_PT_sire_steplist(bpy.types.Panel):
    """Tooltip"""
    bl_label = "SIRE"
    bl_idname = "VIEW3D_PT_sire_steplist"
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "scene"

    def draw(self, context):
        layout = self.layout

        layout.prop(context.scene.sire, "type_add", expand=True)

        row = layout.row()
        row.template_list("SIRE_UL_steplist", "", context.scene.sire, "steps", context.scene.sire, "index", rows=4, maxrows=10)
        
        col = row.column(align=True)
        col.operator("view3d.sire_step_add", text="", icon="ZOOMIN")
        col.operator("view3d.sire_step_remove", text="", icon="ZOOMOUT")
        
        col.separator()
        col.operator('view3d.sire_step_moveup', text='', icon='TRIA_UP')
        col.operator('view3d.sire_step_movedown', text='', icon='TRIA_DOWN')
            

def register():
    bpy.utils.register_module(__name__)
    bpy.types.Scene.sire = PointerProperty(type=Sire)

def unregister():
    bpy.utils.unregister_module(__name__)
    del bpy.types.Scene.sire


if __name__ == "__main__":
    register()

RUn that script, select a mesh object and go to editmode, see scene tab in properties editor.

Lol wow!

I hope you copy-pasted this, and didn’t code for 3 hours for my sake :slight_smile:
Thanks! I’ll definitely check it out!

copied from something I coded for another thread on this forum :wink: