5
$\begingroup$

I am writing an addon that displays the mesh modifiers in a listbox, with the settings below the box.

The issue I have is with the active index for the listbox - I'm using a custom property because there is no internal 'active' property for modifiers. The problem I have is that when the last modifier is removed, the index reaches true zero (index size zero), and I get a panel crash plus a console message:

IndexError: bpy_prop_collection[index]:index 0 out of range, size 0

If I then re-add a modifier, the problem resolves and the panel(listbox) reappears.

How can I structure this so the listbox doesn't crash when the index reaches size 0? Here's the backbone of the script:

import bpy

bpy.types.Object.modifier_active_index = bpy.props.IntProperty()

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

    global num 
    global atype
    global aname
    global amod

    num = bpy.context.object.modifier_active_index

    ob = context.object
    obname = context.object.name
    amod = bpy.data.objects[obname].modifiers[num]
    amods = bpy.data.objects[obname].modifiers
    atype = bpy.data.objects[obname].modifiers[num].type
    aname = bpy.data.objects[obname].modifiers[num].name

    rows = 2

    row = layout.row()

    row.template_list("UI_UL_list", "", ob, "modifiers", ob, "modifier_active_index", rows=rows)

Edit:

class OBJECT_OT_modifier_move(bpy.types.Operator): #move active modifier down in stack
bl_idname = "object.modifier_action"
bl_label = "Modifier Action"

action = bpy.props.EnumProperty(
    items=(
        ('UP', "Up", ""),
        ('DOWN', "Down", ""),
        ('REMOVE', "Remove", ""),
        ('ADD', "Add","")
    )
)

def invoke(self, context, event):

    ob = context.object
    idx = ob.modifier_active_index

    if self.action == 'ADD':
          if bpy.ops.object.modifier_add(type = '') == {'FINISHED'}: #how do I call the menu with modifier.type enums
              ob.modifier_active_index = len(ob.modifiers) - 1
    try:
        mod = ob.modifiers[idx]
    except IndexError:
        pass
    else:
        if self.action == 'DOWN' and idx < len(ob.modifiers) - 1:
            if bpy.ops.object.modifier_move_down(modifier=mod.name) == {'FINISHED'}:
                ob.modifier_active_index += 1
        elif self.action == 'UP' and idx >= 1:
            if bpy.ops.object.modifier_move_up(modifier=mod.name) == {'FINISHED'}:
                ob.modifier_active_index -= 1                
        elif self.action == 'REMOVE':
            bpy.ops.object.modifier_remove(modifier=mod.name)
            if idx >= 1:
                ob.modifier_active_index -= 1


    return {"FINISHED"}
$\endgroup$
0

1 Answer 1

6
$\begingroup$

You simply need to handle the case of an invalid index or an empty list of modifiers.

You could for example test if len(ob.modifiers): ... if there are any, but I would rather catch all bad indices using try ... except IndexError:

import bpy

class OBJECT_PT_modifiers(bpy.types.Panel):
    bl_idname = "OBJECT_PT_modifiers"
    bl_label = "Modifiers"
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "object"

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

        rows = 2
        ob = context.object
        idx = ob.modifier_active_index

        try:
            mod = ob.modifiers[idx]
            layout.label("%s (%s)" % (mod.name, mod.type), icon="MODIFIER")
        except IndexError:
            layout.label("no modifier selected")

        row = layout.row()
        row.template_list("UI_UL_list", "modifiers", ob, "modifiers", ob, "modifier_active_index", rows=rows)

def register():
    bpy.utils.register_module(__name__)
    bpy.types.Object.modifier_active_index = bpy.props.IntProperty()

def unregister():
    bpy.utils.unregister_module(__name__)
    del bpy.types.Object.modifier_active_index

if __name__ == "__main__":
    register()

Note that an index of -1 is not considered bad, because it refers to the last item in a list in Python.

You shouldn't use global anywhere, it is certainly not needed. modifier_active_index is globally available for all objects, and you don't need globals inside the draw function to refer to the modifier.

Also note that if you use UI_UL_list, the second argument should be some unique string to prevent collisions with other UI_UL_list that may exist in the panel.

Edit: How to handle (re-)move actions while always showing the panel (even with no modifier)

import bpy

mod_icon_map = {m.identifier: m.icon for m in bpy.types.OBJECT_OT_modifier_add.bl_rna.properties['type'].enum_items}


class OBJECT_OT_modifier_move(bpy.types.Operator): #move active modifier down in stack
    bl_idname = "object.modifier_action"
    bl_label = "Modifier Action"

    action = bpy.props.EnumProperty(
        items=(
            ('UP', "Up", ""),
            ('DOWN', "Down", ""),
            ('REMOVE', "Remove", ""),
        )
    )

    def invoke(self, context, event):

        ob = context.object
        idx = ob.modifier_active_index
        try:
            mod = ob.modifiers[idx]
        except IndexError:
            pass
        else:
            if self.action == 'DOWN' and idx < len(ob.modifiers) - 1:
                if bpy.ops.object.modifier_move_down(modifier=mod.name) == {'FINISHED'}:
                    ob.modifier_active_index += 1
            elif self.action == 'UP' and idx >= 1:
                if bpy.ops.object.modifier_move_up(modifier=mod.name) == {'FINISHED'}:
                    ob.modifier_active_index -= 1                
            elif self.action == 'REMOVE':
                bpy.ops.object.modifier_remove(modifier=mod.name)
                if idx >= 1:
                    ob.modifier_active_index -= 1


        return {"FINISHED"}


class MODIFIER_UL_listtype(bpy.types.UIList): #custom UIList type for modifiers

    def draw_item(self, context, layout, data, item, active_data, active_propname, index):

        modifier = item

        if self.layout_type in {'DEFAULT', 'COMPACT'}:

            layout.prop(modifier, "name", text="", emboss=False, icon=mod_icon_map[modifier.type])

            icon = 'RESTRICT_RENDER_OFF' if item.show_render else 'RESTRICT_RENDER_ON'
            layout.prop(item, "show_render", text="", icon=icon, emboss=False)

            icon = 'RESTRICT_VIEW_OFF' if item.show_viewport else 'RESTRICT_VIEW_ON'
            layout.prop(item, "show_viewport", text="", icon=icon, emboss=False)

            icon = 'EDITMODE_HLT' if item.show_in_editmode else 'OBJECT_DATAMODE'
            layout.prop(item, "show_in_editmode", text="", icon=icon, emboss=False)

        elif self.layout_type in {'GRID'}:
            layout.alignment = 'CENTER'
            layout.label("", icon=mod_icon_map[modifier.type])


class OBJECT_PT_Modifiers(bpy.types.Panel): #panel draw class
    """Creates a Panel in the Object properties window"""
    bl_label = "Modifiers"
    bl_idname = "OBJECT_PT_Modifiers"
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "object"


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

        ob = bpy.context.object
        rows = 2

        row = layout.row()
        row.template_list("MODIFIER_UL_listtype", "", ob, "modifiers", ob, "modifier_active_index", rows=rows)
        col = row.column(align=True)
        col.operator("object.modifier_add", icon='ZOOMIN', text="")
        col.operator("object.modifier_action", icon='ZOOMOUT', text="").action = 'REMOVE'
        col.separator()
        col.operator("object.modifier_action", icon='TRIA_UP', text="").action = 'UP'
        col.operator("object.modifier_action", icon='TRIA_DOWN', text="").action = 'DOWN'


def register():
    bpy.utils.register_module(__name__)
    bpy.types.Object.modifier_active_index = bpy.props.IntProperty()


def unregister():
    bpy.utils.unregister_module(__name__)
    del bpy.types.Object.modifier_active_index


if __name__ == "__main__":
    register()

Remarks:

  • Creating a lookup dict is way better for icons
  • Move up, move down and remove actions can be handled in a single operator
  • Don't do ob.modifiers[ob.modifier_active_index] anywhere without range change or try/except construct
  • Check the result of modifier_move_up() and modifier_move_down(), as it may fail (MultiRes can't be below a non-deforming modifier)
  • All classes can be (un-)registered at once with (un-)register_module()
  • Use consistent indentation (4 spaces for every level)
$\endgroup$
7
  • $\begingroup$ thanks, that try statement catches the error, but I still have a problem withe the listbox and the rest of the panel contents disappearing upon index error - the globals were simply a sloppy way to test some other classes, fixed now: $\endgroup$
    – russcript
    Commented Jul 20, 2014 at 19:41
  • $\begingroup$ Ive tried everything I could think of, but couldn't resolve the disappearing panel issue, here's the script, if you add a modifier and remove it, the panel should disappear. If you re-add a modifier, it will reappear. dropbox.com/s/dseda2xpf761qd1/ModListPaneltest $\endgroup$
    – russcript
    Commented Jul 20, 2014 at 19:51
  • $\begingroup$ You're trying to access an invalid element again without range check or try/catch. See my edited answer for a revised version of your script. $\endgroup$
    – CodeManX
    Commented Jul 20, 2014 at 21:08
  • $\begingroup$ thanks a million, I'm obviously still learning a lot, I couldn't get that icon lookup to work when I tried it; must have been doing somethin wrong. thanks for all the addditional tips $\endgroup$
    – russcript
    Commented Jul 21, 2014 at 11:39
  • $\begingroup$ sorry to return here; if I want to add the modifier_add operator to the actions in the OBJECT_OT_modifier_move class, how do I call the modifier.type enum menu as an operator? see code snip in the edited OP for example. the reason I want to add modifiers with a custom operator is so I can switch the indx to the new modifier after adding it. $\endgroup$
    – russcript
    Commented Jul 21, 2014 at 13:24

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .