Help with scripting a menu needed

i want to make a menu for some things in a list (no blender-objects).

i used bpy.types.menu but i have some problems there (see image)


i have the problem, that the menu/list-items go to the right side after the 20th item.
i also want to add a search-field for it.
(on the right side of the image you can see, how i want my menu)

is there a possibility to change this?

import bpy



class CustomMenu(bpy.types.Menu):
    bl_label = "RubberDuck's New Bad Ass Menu" #panel name displayed in header
    bl_idname = "OBJECT_MT_custom_menu"
  
    def draw(self, context):
        layout = self.layout










class SimpleOperator(bpy.types.Operator):
    """Tooltip"""
    bl_idname = "object.simple_operator"
    bl_label = "Simple Object Operator"
    bl_options = {'REGISTER', 'UNDO'}
    bl_property = "enumprop"
    
    def item_cb(self, context):
        return [(mat.name, mat.name, '') for mat in self.mats]
    
    mats = bpy.props.CollectionProperty(type=bpy.types.PropertyGroup)
    enumprop = bpy.props.EnumProperty(items=item_cb)#(('1','One',''),('2','Two','')))


    @classmethod
    def poll(cls, context):
        return (context.active_object is not None and
                context.active_object.type == 'MESH')    


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


    def invoke(self, context, event):
        self.mats.clear()
        for i in range(1, 40): #seaching between 1,40. Try switch between 1,5 and you understand the resualt
            self.mats.add().name = "Badlamshade_says_change_this %i" % i #change this Badlamshade_says_change_this
        context.window_manager.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')
def draw_item(self, context):
    layout = self.layout
    layout.menu(CustomMenu.bl_idname)
    




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


    # lets add ourselves to the main header
    bpy.types.INFO_HT_header.append(draw_item)




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


    bpy.types.INFO_HT_header.remove(draw_item)


if __name__ == "__main__":
    register()


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



I believe search popup is what you are looking for. This works as is when you hit run script; you’ll have to play around with it to your fancy.

That dropdown type is a template_ID() and only available for built-in ID types.

I suggest to use a prop_search() on a custom Collection. There’s also invoke_search_popup(), but it needs an operator (thus a button to start it in a panel).

import bpy

enum_items = (
    ('FOO', 'Foo', ''),
    ('BAR', 'Bar', '')
)

class SearchOp(bpy.types.Operator):
    bl_idname = "wm.invoke_search_popup"
    bl_label = "Invoke Search Popup"
    bl_property = "enumprop"
    
    enumprop = bpy.props.EnumProperty(items=enum_items)
    
    def execute(self, context):
        self.report({'INFO'}, self.enumprop)
        context.scene.enum_string = self.enumprop
        context.region.tag_redraw()
        return {'FINISHED'}
    
    def invoke(self, context, event):
        context.window_manager.invoke_search_popup(self)
        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

        obj = context.object

        col = layout.column()
        s = context.scene.enum_string
        col.operator(SearchOp.bl_idname, s if s else SearchOp.bl_label)
        
        col.prop_search(context.scene, "coll_string", context.scene, "coll")

def populate_coll(scene):
    bpy.app.handlers.scene_update_pre.remove(populate_coll)
    scene.coll.clear()
    for identifier, name, description in enum_items:
        scene.coll.add().name = name

def register():
    bpy.utils.register_module(__name__)
    
    bpy.types.Scene.enum = bpy.props.EnumProperty(items=enum_items)
    
    bpy.types.Scene.coll = bpy.props.CollectionProperty(
        type=bpy.types.PropertyGroup
    )
    
    bpy.types.Scene.enum_string = bpy.props.StringProperty()
    bpy.types.Scene.coll_string = bpy.props.StringProperty()
    
    bpy.app.handlers.scene_update_pre.append(populate_coll)


def unregister():
    bpy.utils.unregister_module(__name__)
    del bpy.types.Scene.enum
    del bpy.types.Scene.coll
    del bpy.types.Scene.string


if __name__ == "__main__":
    register()


(Properties Editor, Object tab)

CoDEmanX, your code gives a good result, i first method (with the search-field) is good.
it has only one problem:

when i select something then it fills the search-field with the selected value.
after that i have to remove the words in the search-field to see all.

I’m pretty sure that’s handled in the native UI code, nothing you can do in python to change behavior

is there really no possibility?
when i start the script first in a blend file, nothing is selected in the menu.
isn’t it possible to reset this when clicking on the button?

or is there a better way to do this?

Well you can use the prop_search instead, which also allows for filtering (just start typing!)

thank you.

i did not realize that i can search by typing something into the field :D.

there is only a small thing left.
i want to access the data of the list.
everytime i select something i want to print the selected(here from the example: the first value >> FOO and the second one Foo) into a label

Just access the coll_item property:

import bpy

coll_items = (
    'Foo',
    'Bar',
    'Monkey',
)

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

        col = layout.column()
        col.prop_search(context.scene, "coll_item", context.scene, "coll", icon='OBJECT_DATA', text="")
        col.label("Selected: %s" % context.scene.coll_item)


def populate_coll(scene):
    bpy.app.handlers.scene_update_pre.remove(populate_coll)
    scene.coll.clear()
    for item in coll_items:
        scene.coll.add().name = item
        

def register():
    bpy.utils.register_module(__name__)
    
    bpy.types.Scene.coll = bpy.props.CollectionProperty(
        type=bpy.types.PropertyGroup
    )
    
    bpy.types.Scene.coll_item = bpy.props.StringProperty()
    
    bpy.app.handlers.scene_update_pre.append(populate_coll)


def unregister():
    bpy.utils.unregister_module(__name__)
    del bpy.types.Scene.coll
    del bpy.types.Scene.coll_item


if __name__ == "__main__":
    register()

If you need to execute code as the user selects an entry, use an update callback:

import bpy

coll_items = (
    'Foo',
    'Bar',
    'Monkey',
)
        

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

        col = layout.column()
        col.prop_search(context.scene, "coll_item", context.scene, "coll", icon='OBJECT_DATA', text="")
        col.label("Selected: %s" % context.scene.coll_item)


def populate_coll(scene):
    bpy.app.handlers.scene_update_pre.remove(populate_coll)
    scene.coll.clear()
    for item in coll_items:
        scene.coll.add().name = item

def coll_update(self, context):
    coll_item = context.scene.coll_item
    
    if coll_item:
        context.window_manager.popup_menu(
            lambda self, context: self.layout.label(
                coll_item,
                icon='OBJECT_DATA'
            )
        )

def register():
    bpy.utils.register_module(__name__)
    
    bpy.types.Scene.coll = bpy.props.CollectionProperty(
        type=bpy.types.PropertyGroup
    )
    
    bpy.types.Scene.coll_item = bpy.props.StringProperty(
        update=coll_update
    )
    
    bpy.app.handlers.scene_update_pre.append(populate_coll)


def unregister():
    bpy.utils.unregister_module(__name__)
    del bpy.types.Scene.coll
    del bpy.types.Scene.coll_item


if __name__ == "__main__":
    register()

good.

but now i have the last question for now.

when i use the example
enum_items = (
(‘FOO’, ‘Foo’, ‘’),
(‘BAR’, ‘Bar’, ‘’)
)

i see “Foo”/“Bar” in the menu.

what is the difference between the first value (“FOO”) and the second (“foo”) and how can i access the first one too?

it’s not used for the prop_search / CollectionProperty approach.

With EnumProperty, the first tuple item is stored in the prop instance. You can look up the other values by creating a dict.

enum_items = (
    ('FOO', 'Foo', ''),
    ('BAR', 'Bar', '')
)

enum_items_dict = {identifier: name for identifier, name, description in enum_items}

enum_items_dict['BAR'] # Bar

Solved. can be closed now