10
$\begingroup$

I need the user to choose from a huge list of items. This is ugly and useless for lists longer than 100 items. I'm happy with the rest of the code. I just want a scrollable listbox really.

class my_panel(Panel):
      bl_label = "MYPANEL"
      bl_idname = "SCENE_PT_mypanel"
      bl_space_type = 'PROPERTIES'
      bl_context = 'scene'

      def draw(self,context):
           props = context.scene.myAddon
           self.layout.prop(props, 'jlist', text='jlist')



def updateJlist(self,context):
     t = []
     for i,v in enumerate(master_jlist):
          t.append((v.name,v.name,'',i)

     return t


jlist  = EnumProperty( items=updateJList) 
bpy.types.Scene.myAddon = jlist

I would also be happy with something like the material list. I can't seem to get UILayout.template_list, or UILayout.template_ID to work. Examples on the web refer to IDPropertyGroup which seems to be missing in recent blender builds.

$\endgroup$

4 Answers 4

14
$\begingroup$

To let the user explicitly select an item from a large set, I discourage a template_list - although there's a search filter.

1. Instead, I recommend the search popup:

import bpy


def item_cb(self, context):
    return [(str(i), "Item %i" % i, "") for i in range(100)]


class SimpleOperator(bpy.types.Operator):
    """Tooltip"""
    bl_idname = "object.simple_operator"
    bl_label = "Simple Object Operator"
    bl_property = "my_enum"

    my_enum = bpy.props.EnumProperty(items=item_cb)

    def execute(self, context):
        self.report({'INFO'}, "Selected: %s" % self.my_enum)
        return {'FINISHED'}

    def invoke(self, context, event):
        wm = context.window_manager
        wm.invoke_search_popup(self)
        return {'FINISHED'}


def register():
    bpy.utils.register_class(SimpleOperator)


def unregister():
    bpy.utils.unregister_class(SimpleOperator)


if __name__ == "__main__":
    register()

    # test call
    bpy.ops.object.simple_operator('INVOKE_DEFAULT')

2. Or nested menus if there's some kind of hierarical structure in your data. The example uses dynamically-created menus:

import bpy

class SimpleOperator(bpy.types.Operator):
    """Tooltip"""
    bl_idname = "object.report"
    bl_label = "Report"

    text = bpy.props.StringProperty()

    def execute(self, context):
        self.report({'INFO'}, self.text)
        return {'FINISHED'}

class SimpleCustomMenu(bpy.types.Menu):
    bl_label = "Simple Custom Menu"
    bl_idname = "OBJECT_MT_simple_custom_menu"

    def draw(self, context):
        layout = self.layout
        for i in range(6):
            layout.menu("OBJECT_MT_dyn_%d" % i)


def register():
    bpy.utils.register_class(SimpleCustomMenu)
    bpy.utils.register_class(SimpleOperator)


def unregister():
    bpy.utils.unregister_class(SimpleCustomMenu)
    bpy.utils.unregister_class(SimpleOperator)

if __name__ == "__main__":
    register()


    for i in range(6):
        idname = "OBJECT_MT_dyn_%d" % i

        def func(self, context):
            self.layout.label("Parent: " + self.bl_label)
            for j in range(10):
                option_text = "Option %i" % j
                props = self.layout.operator(SimpleOperator.bl_idname, text=option_text)
                props.text = "%s > %s" % (self.bl_label, option_text)


        opclass = type("OBJECT_MT_dyn_%d" % i,
                       (bpy.types.Menu, ),
                       {"bl_idname": idname, "bl_label": "Test %i" % i, "draw": func},
                       )
        bpy.utils.register_class(opclass)

    # The menu can also be called from scripts
    bpy.ops.wm.call_menu(name=SimpleCustomMenu.bl_idname)

3. A template_list() can be used to show a single line:

layout.template_list(..., rows=1)

You can force the built-in filter options to always show in the UIList subclass:

def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
    self.use_filter_show = True

4. Something hand-crafted: a StringProperty for a search phrase and as many buttons as there are matches. The number of matches should be limited (hard-limited in below code), you could implement pagination if necessary. You make make the layout adapt to the panel width if you want:

import bpy
import random

vowels = ["a", "e", "i", "o", "u"]

def fake_words():
    words = []
    for w in range(random.randint(1, 3)):
        for i, l in enumerate(range(random.randint(2, 7))):
            if i % 2 == 0:
                words.append(vowels[random.randrange(len(vowels))])
            else:
                words.append(chr(random.randint(97, 122)))
        words.append(" ")
    return "".join(words).title()

wordlist = [fake_words() for i in range(1000)]

class ReportSelected(bpy.types.Operator):
    bl_label = "Report"
    bl_idname = "wm.report_selected"

    text = bpy.props.StringProperty()

    def execute(self, context):
        self.report({'INFO'}, self.text)
        return {'FINISHED'}


class HelloWorldPanel(bpy.types.Panel):
    """Creates a Panel in the Object properties window"""
    bl_label = "Hello World Panel"
    bl_idname = "OBJECT_PT_hello"
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "object"

    def draw(self, context):
        layout = self.layout
        wm = context.window_manager
        search_phrase = wm.custom_search
        cols = context.region.width // 100
        matches_per_col = 10
        matches_max = matches_per_col * cols

        layout.prop(context.window_manager, "custom_search", text="", icon="VIEWZOOM")

        matches = [w for w in wordlist if search_phrase.lower() in w.lower()]

        col = layout.column(True)

        for i, w in enumerate(matches):
            if i >= matches_max:
                break

            if i % cols == 0:
                row = col.row(True)

            row.operator("wm.report_selected", text=w).text = w

        matches_more = len(matches) - matches_max
        if matches_more > 0:
            layout.label("+ %i matches" % matches_more)

def register():
    bpy.types.WindowManager.custom_search = bpy.props.StringProperty()
    bpy.utils.register_class(HelloWorldPanel)
    bpy.utils.register_class(ReportSelected)


def unregister():
    bpy.utils.unregister_class(HelloWorldPanel)
    bpy.utils.unregister_class(ReportSelected)
    del bpy.types.WindowManager.custom_search


if __name__ == "__main__":
    register()

I can think of many more solutions, like a grid of buttons with a letter of the alphabet each as a first step, and in the second, show only strings that start with the chosen letter (probably as buttons).

Or multiple dropdown menus, select a letter or category first, and let the second update based on the first, to show only the relevant entries.

$\endgroup$
7
  • $\begingroup$ I'm using a panel, not an operator. The second idea is interesting but I don't see how well this works for large lists of dynamically generated choices. I'm just trying to find the string in the list of strings. $\endgroup$
    – Ben L
    Apr 21, 2014 at 14:34
  • $\begingroup$ Added two more options, which can be used in a panel. $\endgroup$
    – CodeManX
    Apr 21, 2014 at 22:51
  • $\begingroup$ Interesting implimentations. I considered paginatation but it seemed like an unfair hack. I'm going with the UIList Subclass. The limitations are I can compound the list_items into a better string using ID + name + description, but I can't search against it. $\endgroup$
    – Ben L
    Apr 22, 2014 at 14:21
  • $\begingroup$ You can either implement your own filter_items function in the UIList subclass, or concat the search fields and set it as .name (which is searched against). You can draw ID, label and description or whatever, but leave out the .name property (implementation example added to my answer). $\endgroup$
    – CodeManX
    Apr 22, 2014 at 15:18
  • 1
    $\begingroup$ I moved additional template_list() examples to a new answer, as it's a bit off-topic. There's no need to update the .name property just for searching, you can implement your own searching and sorting in a proper way by implementing the filter_items() method. $\endgroup$
    – CodeManX
    Apr 22, 2014 at 23:49
7
$\begingroup$

More on template_list(), which is related to the specific problem:

To search in properties other than a PropertyGroup's .name, you can either write a custom filter function (see Ui List Template), or use a little hack: Concatenate all fields for search and use it as .name, but not draw it:

bl_info = {
    "name": "Collection Menu",
    "author": "CoDEmanX",
    "version": (1, 0),
    "blender": (2, 65, 0),
    "location": "",
    "description": "",
    "warning": "",
    "wiki_url": "",
    "category": ""}


import bpy

coll_data = (
    ("Option A", "The first option, can be found by searching '1' as well"),
    ("Option B", "You can find this by searching for 'roflmao'"),
    ("Some text", "Some more descriptive explanation"),
    ("Blabla", "Talking stuff...")
)

class COLL_UL_search(bpy.types.UIList):
    def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
        if self.layout_type in {'DEFAULT', 'COMPACT'}:
            # You should always start your row layout by a label (icon + text), or a non-embossed text field,
            # this will also make the row easily selectable in the list! The later also enables ctrl-click rename.
            # We use icon_value of label, as our given icon is an integer value, not an enum ID.
            # Note "data" names should never be translated!
            split = layout.split(0.3)
            split.label(item.label)
            split.label(item.description)

        elif self.layout_type in {'GRID'}:
            pass

class MyColl(bpy.types.PropertyGroup):
    #name = bpy.props.StringProperty()
    label = bpy.props.StringProperty()
    description = bpy.props.StringProperty()


class HelloWorldPanel(bpy.types.Panel):
    """Creates a Panel in the Object properties window"""
    bl_label = "Hello World Panel"
    bl_idname = "OBJECT_PT_hello"
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "scene"


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

        col = context.scene.col
        idx = context.scene.col_idx

        if idx >= len(col):
            #context.scene.col_idx = len(col) - 1
            text = "(index error)"
        else:
            text = col[idx].name

        layout.template_list("COLL_UL_search", "", context.scene, "col", context.scene, "col_idx")


def collhack(scene):
    bpy.app.handlers.scene_update_pre.remove(collhack)

    try:
        scene.col.clear()
    except:
        pass

    for i, (label, description) in enumerate(coll_data, 1):
        item = scene.col.add()
        item.label = label
        item.description = description
        item.name = " ".join((str(i), label, description))



def register():
    bpy.utils.register_module(__name__)
    bpy.types.Scene.col = bpy.props.CollectionProperty(type=MyColl)
    bpy.types.Scene.col_idx = bpy.props.IntProperty(default=0)

    bpy.app.handlers.scene_update_pre.append(collhack)


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


if __name__ == "__main__":
    register()

And here's a complex, editable UIList that searches numbers, labels and descriptions:

import bpy

class MESH_UL_mylist(bpy.types.UIList):
    def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, flt_flag):
        self.use_filter_show = True

        if self.layout_type in {'DEFAULT', 'COMPACT'}:
            split = layout.split(0.1)
            split.label("%i." % (index + 1))
            split = split.split(0.3)
            split.prop(item, "label", text="", emboss=False)
            split.prop(item, "description", text="", emboss=False)

        elif self.layout_type in {'GRID'}:
            pass

    # Called once to filter/reorder items.
    def filter_items(self, context, data, propname):

        col = getattr(data, propname)
        filter_name = self.filter_name.lower()

        flt_flags = [self.bitflag_filter_item if any(
                filter_name in filter_set for filter_set in (
                    str(i), item.label.lower(), item.description.lower()
                )
            )
            else 0 for i, item in enumerate(col, 1)
        ]

        if self.use_filter_sort_alpha:
            flt_neworder = [x[1] for x in sorted(
                    zip(
                        [x[0] for x in sorted(enumerate(col), key=lambda x: x[1].label)],
                        range(len(col))
                    )
                )
            ]
        else:
            flt_neworder = []

        return flt_flags, flt_neworder


class MyColl(bpy.types.PropertyGroup):
    #name = bpy.props.StringProperty()
    label = bpy.props.StringProperty()
    description = bpy.props.StringProperty()


class HelloWorldPanel(bpy.types.Panel):
    """Creates a Panel in the Object properties window"""
    bl_label = "Hello World Panel"
    bl_idname = "OBJECT_PT_hello"
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "scene"


    def draw(self, context):
        layout = self.layout
        layout.template_list("MESH_UL_mylist", "", context.scene, "col", context.scene, "col_idx")

# Demo data
coll_data = (
    ("Option A", "The first option, can be found by searching '1' as well"),
    ("Option B", "You can find this by searching for 'roflmao'"),
    ("Some text", "Some more descriptive explanation"),
    ("Blabla", "Talking stuff...")
)

def register():
    bpy.utils.register_module(__name__)
    bpy.types.Scene.col = bpy.props.CollectionProperty(type=MyColl)
    bpy.types.Scene.col_idx = bpy.props.IntProperty(default=0)

    # Add demo data
    scene = bpy.context.scene
    #scene.col.clear()
    if 1: #len(scene.col) < 1:
        for label, description in coll_data:
            item = scene.col.add()
            item.label = label
            item.description = description


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


if __name__ == "__main__":
    register()
$\endgroup$
1
  • $\begingroup$ very cool. self.filter_name is the key here. $\endgroup$
    – Ben L
    Apr 23, 2014 at 14:06
2
$\begingroup$

This is what I found:

template_ID seems to only work with ID types which you can't create without writing C code.

prop_search will let you do a search box with a selection dropdown although not the same as the template_ID.

template_list should be implimented like this

layout.template_list("UI_UL_list", '', props, <collection prop>, props, <int prop>)

But, template_list is large and can't be minimized to a single line. So one might need trigger an event after the item is chosen to hide that ui element.

$\endgroup$
2
$\begingroup$

@CodeManX thank you for this. I happened to use it so I remade for 2.80+

bl_info = {
    'name': 'Search Browser',
    'description': 'Search Browser',
    'author': 'CodeManX, Kabu',
    'version': (0, 0, 1),
    'blender': (2, 80, 0),
    'location': 'View3D > Sidebar > Search Browser',
    'description': 'A Search Browser',
    'warning': '',
    'wiki_url': '',
    'tracker_url': '',
    'category': 'Object'
}

import bpy

class SEARCHBROWSER_UL_mylist(bpy.types.UIList):
    def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, flt_flag):
        self.use_filter_show = True

        if self.layout_type in {'DEFAULT', 'COMPACT'}:
            split = layout.split(factor=0.1)
            split.label(text="%i." % (index + 1))
            split = split.split(factor=0.3)
            split.prop(item, "label", text="", emboss=False)
            split.prop(item, "description", text="", emboss=False)

        elif self.layout_type in {'GRID'}:
            pass

    # Called once to filter/reorder items.
    def filter_items(self, context, data, propname):

        col = getattr(data, propname)
        filter_name = self.filter_name.lower()

        flt_flags = [self.bitflag_filter_item if any(
                filter_name in filter_set for filter_set in (
                    str(i), item.label.lower(), item.description.lower()
                )
            )
            else 0 for i, item in enumerate(col, 1)
        ]

        if self.use_filter_sort_alpha:
            flt_neworder = [x[1] for x in sorted(
                    zip(
                        [x[0] for x in sorted(enumerate(col), key=lambda x: x[1].label)],
                        range(len(col))
                    )
                )
            ]
        else:
            flt_neworder = []

        return flt_flags, flt_neworder


class SEARCHBROWSER_mycoll(bpy.types.PropertyGroup):    
    label : bpy.props.StringProperty()
    description : bpy.props.StringProperty()


class SEARCHBROWSER_PT_mypanel(bpy.types.Panel):
    """Creates a Panel in the Properties window"""
    bl_label = "Search Browser"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = "Search Browser"


    def draw(self, context):
        layout = self.layout
        layout.template_list("SEARCHBROWSER_UL_mylist", "", context.scene, "col", context.scene, "col_idx")

        # Demo data
        col_data = (
            ("Option A", "The first option, can be found by searching '1' as well"),
            ("Option B", "You can find this by searching for 'roflmao'"),
            ("Some text", "Some more descriptive explanation"),
            ("Blabla", "Talking stuff...")
        )

        # Add demo data
        col = context.scene.col
        if len(col) < 1:
            for label, description in col_data:
                item = col.add()
                item.label = label
                item.description = description


def register():
    bpy.utils.register_class(SEARCHBROWSER_mycoll)
    bpy.utils.register_class(SEARCHBROWSER_UL_mylist)
    bpy.utils.register_class(SEARCHBROWSER_PT_mypanel)
    
    bpy.types.Scene.col = bpy.props.CollectionProperty(type=SEARCHBROWSER_mycoll)
    bpy.types.Scene.col_idx = bpy.props.IntProperty(default=0)


def unregister():
    del bpy.types.Scene.col_idx
    del bpy.types.Scene.col

    bpy.utils.unregister_class(SEARCHBROWSER_PT_mypanel)
    bpy.utils.unregister_class(SEARCHBROWSER_UL_mylist)
    bpy.utils.unregister_class(SEARCHBROWSER_mycoll)


if __name__ == "__main__":
    register()

HTH

$\endgroup$
1
  • $\begingroup$ hey thanks for the code, but what does it search? The list is empty. $\endgroup$
    – yarun can
    Aug 30, 2021 at 2:54

You must log in to answer this question.

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