Amazing UILists in Blender with Custom Filtering
For this tutorial you need to know the basics of making UI lists with Python in Blender, luckily someone has already made a tutorial about that!
In the last episode we learned how to make a UIList and place it in a panel. It even had sorting and searching for free. So what’s left to do? Build our own sorting and filtering of course!
This can be done by adding a few more methods to our previous UIList. Let’s dive in.
The filter_items()
method is where the magic happens. This function needs to return two lists to filter and order the items. If we don’t want to filter or change the order, we just return two empty lists and leave everything 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 collection. This is just a matter of using getattr()
on the data parameter.
items = getattr(data, propname)
Filtering
You know you are treading uncharted territory in Blender when the Python API looks suspiciously like C. And filtering here is just the case.
First we have to create a list of 32bit integer bitflags that has the same length as our collection of items. Lucky the UIList class gives us a bitflag_filter_item
property that we can use right away.
When we want to filter out a specific item in the list we have to flip the bitflag for that item. First we have to get the complement of the bitflag, this is done with the ~
operator. We can then flip it by combining it with its complement using the logical “bitwise and” operator, which is &
in Python. We can simplify this to &= ~bitflag
. That’s all the bitflaggin’ we need to know but if you want to learn more about bitflags 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 function to filter by name (AKA searching).
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 button) also seems to work, even if we always pass False
to the “reverse” parameter. Note that the bitlfag is only taking one out of the 32 possible bits, so you can use the other 31 for your own needs.
Ordering
Ordering is similar to filtering, except we play with indices. We find out the original indices by enumerating the original items list, then we change the numbers 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 trouble initially. For some reason my brain just refused to understand it. Remapping the indices is done by placing a new index number on the index you want to change. Like this:
So if we want the first object to move to the second position, 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 something like [0, 0, 1] or Blender will also crash. And you won’t believe what happens if you pass a negative number (spoiler: Blender will crash).
To be fair, I believe all those crashes happen for performance reasons. This filter method is called very often, and checking the lists every time would be too expensive.
UIList also offers a couple of functions for sorting. The first one lets you sort by name, while the other one takes the items and a function that returns an index. The functions it takes can be anything 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 functionality is only one half of the puzzle. We still need to build a UI for it.
To create a UI we can override the draw_filter(self, context, layout)
method, this is classic Blender UI code. Pretty much anything that goes in a panel 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 controls, it’s better to not override this method and just have the regular UI. If you want to completely remove any filtering UI you can make this an empty function and just return
. That won’t remove the little triangle toggle but no UI will be drawn when it’s toggled.
Final code
This final code takes over from the one in the previous tutorial. You can find another example 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 useful. Some parts can be tricky but once you get them, adding more features
is easy. Check out the Python tag here if you are in the mood for more Blender scripting tutorials.