Amazing UILists in Blender with Custom Filtering

For this tuto­r­i­al you need to know the basics of mak­ing UI lists with Python in Blender, luck­i­ly some­one has already made a tuto­r­i­al about that!

In the last episode we learned how to make a UIList and place it in a pan­el. It even had sort­ing and search­ing for free. So what’s left to do? Build our own sort­ing and fil­ter­ing of course!

Advanced UILists in Blender

This can be done by adding a few more meth­ods to our pre­vi­ous UIList. Let’s dive in.

The filter_items() method is where the mag­ic hap­pens. This func­tion needs to return two lists to fil­ter and order the items. If we don’t want to fil­ter or change the order, we just return two emp­ty lists and leave every­thing else up to Blender. 

    def filter_items(self, context, data, propname):
        """Filter and order items in the list."""

        filtered = []
        ordered = []

        return filtered, ordered

One of the first things you want to do though is get your col­lec­tion. This is just a mat­ter of using getattr() on the data parameter.

        items = getattr(data, propname)

Filtering

You know you are tread­ing unchart­ed ter­ri­to­ry in Blender when the Python API looks sus­pi­cious­ly like C. And fil­ter­ing here is just the case.

First we have to cre­ate a list of 32bit inte­ger bit­flags that has the same length as our col­lec­tion of items. Lucky the UIList class gives us a bitflag_filter_item prop­er­ty that we can use right away.

When we want to fil­ter out a spe­cif­ic item in the list we have to flip the bit­flag for that item. First we have to get the com­ple­ment of the bit­flag, this is done with the ~ oper­a­tor. We can then flip it by com­bin­ing it with its com­ple­ment using the log­i­cal “bit­wise and” oper­a­tor, which is & in Python. We can sim­pli­fy this to &= ~bitflag. That’s all the bit­flag­gin’ we need to know but if you want to learn more about bit­flags in Python, check out this wiki article.

The code looks like this:

# Initialize with all items visible
filtered = [self.bitflag_filter_item] * len(items)

# Filtering out the first item
filtered[0] &= ~self.bitflag_filter_item

The base class also gives us a handy func­tion to fil­ter by name (AKA search­ing).

helpers = bpy.types.UI_UL_list
filtered = helpers.filter_by_name(self.filter_name,
                                  self.bitflag_filter_item,
                                  items, "name", reverse=False)

Yes, we get search for free. Reversing the search (with the but­ton) also seems to work, even if we always pass False to the “reverse” para­me­ter. Note that the bitlfag is only tak­ing one out of the 32 pos­si­ble bits, so you can use the oth­er 31 for your own needs. 

Ordering

Ordering is sim­i­lar to fil­ter­ing, except we play with indices. We find out the orig­i­nal indices by enu­mer­at­ing the orig­i­nal items list, then we change the num­bers to give the objects new indices.

ordered = [index for index, item in enumerate(item)]

Now we need to remap the indices. Watch out, this gave me a lot of trou­ble ini­tial­ly. For some rea­son my brain just refused to under­stand it. Remapping the indices is done by plac­ing a new index num­ber on the index you want to change. Like this:

Original indices, new indices and the resulting order in the UIList

So if we want the first object to move to the sec­ond posi­tion, we pass 2 as the first item in the list.

Note that the new indices list must have the same length as the items list. If it has more or less, Blender will crash. Indices also have to be unique, we can’t have some­thing like [0, 0, 1] or Blender will also crash. And you won’t believe what hap­pens if you pass a neg­a­tive num­ber (spoil­er: Blender will crash).

To be fair, I believe all those crash­es hap­pen for per­for­mance rea­sons. This fil­ter method is called very often, and check­ing the lists every time would be too expensive.

UIList also offers a cou­ple of func­tions for sort­ing. The first one lets you sort by name, while the oth­er one takes the items and a func­tion that returns an index. The func­tions it takes can be any­thing that Python’s sort() would accept.

helpers = bpy.types.UI_UL_list

# If our "items" collection was a collection of objects, we could sort by
# name like this.
ordered = helpers.helper_funcs.sort_items_by_name(items, 'name')

# If the objects in our items collection had a custom int property, we could use it
# to sort them too.
ordered = helpers.helper_funcs.sort_items_helper(items, lambda o: o.myaddon.myprop, True)

Drawing a custom filter/sort UI

Adding all this func­tion­al­i­ty is only one half of the puz­zle. We still need to build a UI for it.

Custom UIs for UIlists in Blender

To cre­ate a UI we can over­ride the draw_filter(self, context, layout) method, this is clas­sic Blender UI code. Pretty much any­thing that goes in a pan­el works here.

    def draw_filter(self, context, layout):
        """UI code for the filtering/sorting/search area."""

        layout.separator()
        col = layout.column(align=True)

        row = col.row(align=True)
        row.prop(self, 'filter_name', text='', icon='VIEWZOOM')
        row.prop(self, 'use_filter_invert', text='', icon='ARROW_LEFTRIGHT')

Of course if you don’t have any extra con­trols, it’s bet­ter to not over­ride this method and just have the reg­u­lar UI. If you want to com­plete­ly remove any fil­ter­ing UI you can make this an emp­ty func­tion and just return. That won’t remove the lit­tle tri­an­gle tog­gle but no UI will be drawn when it’s toggled.

Final code

This final code takes over from the one in the pre­vi­ous tuto­r­i­al. You can find anoth­er exam­ple in the API docs.

import bpy
from bpy.props import StringProperty, IntProperty, CollectionProperty, BoolProperty
from bpy.types import PropertyGroup, UIList, Operator, Panel


class ListItem(PropertyGroup):
    """Group of properties representing an item in the list."""

    name: StringProperty(
           name="Name",
           description="A name for this item",
           default="Untitled")

    random_prop: StringProperty(
           name="Any other property you want",
           description="",
           default="")


class MY_UL_List(UIList):
    """Demo UIList."""

    # Filter by the value of random_prop
    filter_by_random_prop: StringProperty(default='')

    # Invert the random property filter
    invert_filter_by_random: BoolProperty(default=False)

    # Order by random prop
    order_by_random_prop: BoolProperty(default=False)


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

        # We could write some code to decide which icon to use here...
        custom_icon = 'OBJECT_DATAMODE'

        # Make sure your code supports all 3 layout types
        if self.layout_type in {'DEFAULT', 'COMPACT'}:
            layout.label(text=item.name, icon = custom_icon)

        elif self.layout_type in {'GRID'}:
            layout.alignment = 'CENTER'
            layout.label(text='', icon = custom_icon)


    def draw_filter(self, context, layout):
        """UI code for the filtering/sorting/search area."""

        layout.separator()
        col = layout.column(align=True)

        row = col.row(align=True)
        row.prop(self, 'filter_by_random_prop', text='', icon='VIEWZOOM')
        row.prop(self, 'invert_filter_by_random', text='', icon='ARROW_LEFTRIGHT')


    def filter_items(self, context, data, propname):
        """Filter and order items in the list."""

        # We initialize filtered and ordered as empty lists. Notice that 
        # if all sorting and filtering is disabled, we will return
        # these empty. 

        filtered = []
        ordered = []
        items = getattr(data, propname)

        # Filter
        if self.filter_by_random_prop:

            # Initialize with all items visible
            filtered = [self.bitflag_filter_item] * len(items)

            for i, item in enumerate(items):
                if item.random_prop != self.filter_by_random_prop:
                    filtered[i] &= ~self.bitflag_filter_item


        # Invert the filter
        if filtered and self.invert_filter_by_random:
            show_flag = self.bitflag_filter_item & ~self.bitflag_filter_item

            for i, bitflag in enumerate(filtered):
                if bitflag == filter_flag:
                    filtered[i] = self.bitflag_filter_item
                else:
                    filtered[i] &= ~self.bitflag_filter_item


        # Order by the length of random_prop
        if self.order_by_random_prop:
            sort_items = bpy.types.UI_UL_list.helper_funcs.sort_items_helper
            ordered = sort_items(items, lambda i: len(i.random_prop), True)


        return filtered, ordered



class LIST_OT_NewItem(Operator):
    """Add a new item to the list."""

    bl_idname = "my_list.new_item"
    bl_label = "Add a new item"

    def execute(self, context):
        context.scene.my_list.add()

        return{'FINISHED'}


class LIST_OT_DeleteItem(Operator):
    """Delete the selected item from the list."""

    bl_idname = "my_list.delete_item"
    bl_label = "Deletes an item"

    @classmethod
    def poll(cls, context):
        return context.scene.my_list

    def execute(self, context):
        my_list = context.scene.my_list
        index = context.scene.list_index

        my_list.remove(index)
        context.scene.list_index = min(max(0, index - 1), len(my_list) - 1)

        return{'FINISHED'}


class LIST_OT_MoveItem(Operator):
    """Move an item in the list."""

    bl_idname = "my_list.move_item"
    bl_label = "Move an item in the list"

    direction: bpy.props.EnumProperty(items=(('UP', 'Up', ""),
                                              ('DOWN', 'Down', ""),))

    @classmethod
    def poll(cls, context):
        return context.scene.my_list

    def move_index(self):
        """ Move index of an item render queue while clamping it. """

        index = bpy.context.scene.list_index
        list_length = len(bpy.context.scene.my_list) - 1  # (index starts at 0)
        new_index = index + (-1 if self.direction == 'UP' else 1)

        bpy.context.scene.list_index = max(0, min(new_index, list_length))

    def execute(self, context):
        my_list = context.scene.my_list
        index = context.scene.list_index

        neighbor = index + (-1 if self.direction == 'UP' else 1)
        my_list.move(neighbor, index)
        self.move_index()

        return{'FINISHED'}


class PT_ListExample(Panel):
    """Demo panel for UI list Tutorial."""

    bl_label = "UI_List Demo"
    bl_idname = "SCENE_PT_LIST_DEMO"
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "scene"

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

        row = layout.row()
        row.template_list("MY_UL_List", "The_List", scene,
                          "my_list", scene, "list_index")

        row = layout.row()
        row.operator('my_list.new_item', text='NEW')
        row.operator('my_list.delete_item', text='REMOVE')
        row.operator('my_list.move_item', text='UP').direction = 'UP'
        row.operator('my_list.move_item', text='DOWN').direction = 'DOWN'

        if scene.list_index >= 0 and scene.my_list:
            item = scene.my_list[scene.list_index]

            layout.row().prop(item, 'name')
            layout.row().prop(item, 'random_prop')


def register():

    bpy.utils.register_class(ListItem)
    bpy.utils.register_class(MY_UL_List)
    bpy.utils.register_class(LIST_OT_NewItem)
    bpy.utils.register_class(LIST_OT_DeleteItem)
    bpy.utils.register_class(LIST_OT_MoveItem)
    bpy.utils.register_class(PT_ListExample)

    bpy.types.Scene.my_list = CollectionProperty(type = ListItem)
    bpy.types.Scene.list_index = IntProperty(name = "Index for my_list",
                                             default = 0)


def unregister():

    del bpy.types.Scene.my_list
    del bpy.types.Scene.list_index

    bpy.utils.unregister_class(ListItem)
    bpy.utils.unregister_class(MY_UL_List)
    bpy.utils.unregister_class(LIST_OT_NewItem)
    bpy.utils.unregister_class(LIST_OT_DeleteItem)
    bpy.utils.unregister_class(LIST_OT_MoveItem)
    bpy.utils.unregister_class(PT_ListExample)


if __name__ == "__main__":
    register()

I hope you found this post use­ful. Some parts can be tricky but once you get them, adding more fea­tures
is easy. Check out the Python tag here if you are in the mood for more Blender script­ing tuto­ri­als.

All the posts you can read
TutorialsBlender, Python12.06.2020