4
$\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$
4
$\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$
  • $\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 Jul 20 '14 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 Jul 20 '14 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 Jul 20 '14 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 Jul 21 '14 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 Jul 21 '14 at 13:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.