Menu - Select next item via operator

I have been trying to figure this out for a while with no luck. What I am trying to do is to create an operator which selects the next menu item.

For example, (template) ui_menu_simple.py:
(This example is using presets but the question is for menus in general)

image

If you have ‘123’ selected:
image

Instead of clicking on the dropdown and then manually selecting ‘456’, I want to create an operator that goes to the next option in the list. How do you control the menu’s selection with an operator?

image — ?

ui_menu_simple.py: (with test operator)

import bpy
from bpy.types import Operator, Menu
from bl_operators.presets import AddPresetBase


class OBJECT_MT_display_presets(Menu):
    bl_label = "Object Display Presets"
    preset_subdir = "object/display"
    preset_operator = "script.execute_preset"
    draw = Menu.draw_preset


class AddPresetObjectDisplay(AddPresetBase, Operator):
    '''Add a Object Display Preset'''
    bl_idname = "camera.object_display_preset_add"
    bl_label = "Add Object Display Preset"
    preset_menu = "OBJECT_MT_display_presets"

    # variable used for all preset values
    preset_defines = [
        "obj = bpy.context.object"
    ]

    # properties to store in the preset
    preset_values = [
        "obj.display_type",
        "obj.show_bounds",
        "obj.display_bounds_type",
        "obj.show_name",
        "obj.show_axis",
        "obj.show_wire",
    ]

    # where to store the preset
    preset_subdir = "object/display"
    

# Help!
class SelectNext(Operator):
    bl_idname = "select.nextmenuitem"
    bl_label = ""
    bl_description = ""
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        
        return{'FINISHED'}    


# Display into an existing panel
def panel_func(self, context):
    layout = self.layout

    row = layout.row(align=True)
    row.menu(OBJECT_MT_display_presets.__name__, text=OBJECT_MT_display_presets.bl_label)
    row.operator("select.nextmenuitem",text="",icon="TRIA_RIGHT")
    row.operator(AddPresetObjectDisplay.bl_idname, text="", icon='ZOOM_IN')
    row.operator(AddPresetObjectDisplay.bl_idname, text="", icon='ZOOM_OUT').remove_active = True


classes = (
    OBJECT_MT_display_presets,
    AddPresetObjectDisplay,
    SelectNext,
)


def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    bpy.types.OBJECT_PT_display.prepend(panel_func)


def unregister():
    for cls in classes:
        bpy.utils.unregister_class(cls)
    bpy.types.OBJECT_PT_display.remove(panel_func)


if __name__ == "__main__":
    register()

Blender’s menu preset framework is nasty. Unless you are on constraints that force you to use a menu, suggest using an EnumProperty.

Anyway, here’s an example of how your operator could look like.

class SelectNext(Operator):
    bl_idname = "select.nextmenuitem"
    bl_label = ""
    bl_description = ""
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        from operator import itemgetter, indexOf
        import os, re

        preset_subdir = AddPresetObjectDisplay.preset_subdir
        path = bpy.utils.preset_paths(preset_subdir)

        if path:
            path = bpy.path.native_pathsep(path[0])
            presets = []

            # Fetch files as tuple: ("Hello World", "path/to/presets/hello_world.py")
            for f in os.listdir(path):
                if f.endswith(".py"):
                    presets.append((bpy.path.display_name(f), os.path.join(path, f)))

            if presets:
                # Sort method ripped from ``Menu.path_menu`` in bpy_types.py.
                presets.sort(
                    key=lambda file_path:
                    tuple(int(t) if t.isdigit() else t for t in re.split(r"(\d+)", file_path[0].lower()))
                )
                active_preset = bpy.path.display_name(OBJECT_MT_display_presets.bl_label)
                try:
                    next_index = indexOf(map(itemgetter(0), presets), active_preset) + 1
                except ValueError:  # Name not in presets. Probably the initial name.
                    next_index = 0

                next_filepath = presets[next_index % len(presets)][1]
                bpy.ops.script.execute_preset(filepath=next_filepath, menu_idname="OBJECT_MT_display_presets")
                return {'FINISHED'}
        return{'CANCELLED'}    
1 Like

That is quite nasty indeed, wow. Thank you very much, it works perfectly.

As you also mentioned EnumProperties, that is another one I’ve been trying to tackle. From what I can tell the operator you’ve just provided is specific to presets. So how would you achieve this for EnumProperties too?

I have a slightly modified version of your menu from this post.

image
menutest.blend (803.5 KB)

1 Like

Yes. Especially when presets are python files - they get executed without any checks.

With EnumProperty I was referring to the display implementation of your presets. You still would need a CollectionProperty to store the presets, and the machinery to dump and load a preset.

The general idea could be:

  • Scene.display_presets (CollectionProperty) to store each preset
  • A New Preset operator that dumps the current display settings to Scene.display_presets
  • Object.display_preset (EnumProperty) to assign a preset per object
  • An update callback for the EnumProperty that applies the preset to the object

Optionally you could store the display presets in AddonPreferences, or by file as json, plain text, primitive python values which are safe to read. It depends on how you want presets to be stored and if they should be accessed regardless of blend file and/or userprefs.

If you’re unsure how to set up these, feel free to let me know and I’ll provide an example.

1 Like

I have a very basic use case for the presets, so the existing way is pretty much sufficient for me. Though I appreciate the explanation and offer.

Though regarding the menu from my second post, can you at least point me in the right direction? Like even what to search? I’ve exhausted all ideas and search terms, I cannot figure that one out. I’m trying to do the same thing there too.

You can use the same approach as in my previous reply.

  1. Get the enum items list
  2. Get the currently selected preset
  3. Get the index of the preset in the list, then increment by 1
  4. Do a modulo on the new index so it wraps to zero instead of going out of bounds.
  5. Access the enum list by the new index and get the first element.
class SelectNext(Operator):
    bl_idname = "select.nextmenuitem"
    bl_label = ""
    bl_description = ""
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        from operator import itemgetter, indexOf
        enum = generate_enum(None, context)
        if enum:
            selected = context.scene.TestBool.menu_object_selected
            try:
                next_index = indexOf(map(itemgetter(0), enum), selected) + 1
            except ValueError:
                next_index = 0
            context.scene.TestBool.menu_object_selected = enum[next_index % len(enum)][0]
            return{'FINISHED'}
        return {'CANCELLED'}
1 Like

Thank you kindly once again – your help is greatly appreciated. I’m learning, slowly… :slight_smile:

Sorry – I’ve just realized that only changes the name in the menu, it does not run the operator. I have the StringProperty there just to display the name of the currently selected menu item.

How to get this to run the operator with the new item?

Oh right!

Well, I prepared the whole thing for you just in case. I didn’t test it thoroughly, though.
This uses the CollectionProperty and EnumProperty as suggested earlier.

from bpy.props import EnumProperty, CollectionProperty
import bpy, ast


# Display property names used to dump and load settings.
display_properties = (
    "show_name", "show_axis", "show_wire", "show_all_edges", "show_texture_space",
    "display.show_shadows", "show_in_front", "color", "display_type", "show_bounds",
    "display_bounds_type"
)


class HelloWorldPanel(bpy.types.Panel):
    bl_label = "Hello World Panel"
    bl_idname = "OBJECT_PT_hello"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'

    def draw(self, context):
        if context.object:
            layout = self.layout
            row = layout.row()
            row.prop(context.object, "display_preset", text="")
            row.operator("object.add_display_preset", text="", icon="PRESET_NEW")
            layout.operator("select.nextmenuitem",text="Next",icon="TRIA_RIGHT")
    
            # Draw the object's display settings.
            bpy.types.OBJECT_PT_display.draw(self, context)


class SelectNext(bpy.types.Operator):
    bl_idname = "select.nextmenuitem"
    bl_label = ""
    bl_description = ""
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        # The first element is None, skip it.
        presets = list(get_presets(None, context))[1:]

        selected = context.object.display_preset
        next_index = context.scene.display_presets.find(selected) + 1
        
        context.object.display_preset = presets[next_index % len(presets)][0]
        return{'FINISHED'}


# Dump an object's display settings as string.
def dump_display_values(obj):
    values = []
    for name in display_properties:
        value = obj.path_resolve(name)
        if not isinstance(value, (type(None), bool, int, float, str)):
            value = tuple(value)  # For Color, Vector types.
        values.append((name, value))
    return repr(values)


# A preset for the Scene.display_prests CollectionProperty
class DisplayPreset(bpy.types.PropertyGroup):
    values: bpy.props.StringProperty()     # Preset values as string.


# Add preset operator
class OBJECT_OT_add_display_preset(bpy.types.Operator):
    bl_idname = "object.add_display_preset"
    bl_label = "New"
    name: bpy.props.StringProperty(default="New Preset")

    def execute(self, context):
        display_presets = context.scene.display_presets

        name = self.name
        while name in display_presets:
            name, *num = name.rsplit(".", 1)
            if num and num[0].isdigit():
                name = f"{name}.{(int(num[0]) + 1):03}"
            else:
                name = name + ".001"

        preset = context.scene.display_presets.add()
        preset.values = dump_display_values(context.object)
        preset.name = name
        context.object.display_preset = name
        return {'FINISHED'}

    def invoke(self, context, event):
        # Show text input when calling the operator.
        return context.window_manager.invoke_props_dialog(self)


# For Object.display_presets EnumProperty. ``None`` is the default.
def get_presets(_, context):
    yield from [None] + [(p.name, p.name, "") for p in context.scene.display_presets]


# Apply selected preset via update callback in Object.display_preset EnumProperty.
def apply_preset(self, context):
    if obj := context.object:
        preset_name = obj.display_preset
        if preset_name in context.scene.display_presets:
            preset = context.scene.display_presets[preset_name]
            for datapath, value in ast.literal_eval(preset.values):
                o = obj
                if "." in datapath:
                    sub, datapath = datapath.rsplit(".", 1)
                    o = getattr(obj, sub)
                setattr(o, datapath, value)


if __name__ == "__main__":
    bpy.utils.register_class(DisplayPreset)
    bpy.utils.register_class(OBJECT_OT_add_display_preset)
    bpy.utils.register_class(HelloWorldPanel)
    bpy.utils.register_class(SelectNext)

    bpy.types.Scene.display_presets = CollectionProperty(type=DisplayPreset)
    bpy.types.Object.display_preset = EnumProperty(
        items=get_presets, default=0, update=apply_preset)

1 Like

Oh no – the preset operator worked perfectly from your first reply, there was no issue.

The one which did not work was the enum for menutest.blend. How to get this to run the operator with the new item?

class SelectNext(Operator):
    bl_idname = "select.nextmenuitem"
    bl_label = ""
    bl_description = ""
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        from operator import itemgetter, indexOf
        enum = generate_enum(None, context)
        if enum:
            selected = context.scene.TestBool.menu_object_selected
            try:
                next_index = indexOf(map(itemgetter(0), enum), selected) + 1
            except ValueError:
                next_index = 0
            context.scene.TestBool.menu_object_selected = enum[next_index % len(enum)][0]
            return{'FINISHED'}
        return {'CANCELLED'}

(And either way thanks very much for the preset script, it is good)

1 Like

See:

The machinery I was referring to is:

  • An Add Preset operator which when clicked grabs the current values and stores them to a collection of presets.

  • An EnumProperty on the object which lists the available presets.

  • The enum needs an update callback that applies the selected preset’s values back to the object.

For this you can’t just use TestBool, because only stores a single string. You need a CollectionProperty that holds collection preset entries to be able to store multiple strings (as presets).

All of this is present in the above script.

1 Like

The menutest.blend is not used for presets. In that one, the list is populated by the contents of a collection named “Obj_Collection”. And when you select a menu item, it runs an operator which hides objects the other objects and unhides the selected object:

menutest

So this was totally separate. TestBool was only used to display the name of the selected item in the menu:
image
TestBool wasn’t intended to control anything, so in this example we can completely ignore TestBool.

So clicking the select.nextmenuitem operator only changes the name of the menu. How to get it to actually run the operator with the change?

Oh, so just the callback for when the property is changed?

You need to define a function and pass it to the StringProperty as a keyword update=callback.
This makes the function run whenever menu_object_selected is changed.

def callback(self, context):
    print("menu_object_selected changed to", context.scene.TestBool.menu_object_selected)

class TestBool(PropertyGroup):
    menu_object_selected : StringProperty(
        name="",
        description="",
        default="",
        update=callback  # <-- Add this
        )
1 Like

No – TestBool is not used for this at all, just ignore TestBool completely.

In the .gif from my previous post, I am manually selecting each item after the other. That’s what I want select.nextmenuitem to do, so that I can quickly click through the list to the next option.

Edited .blend without any propertygroups:
menutest_2.blend (802.4 KB)

In your last blend file you’re using an operator enum. This doesn’t actually store the value you assign, so there’s no way for the SelectNext operator to cycle the current value. It needs to be a persistent property stored somewhere in your scene, like your TestBool was.

Edit:

I guess you could do something like this where the last value you selected is stored on the operator class itself.


class TEST_OT_Object_Select_Menu(bpy.types.Operator):
    bl_idname = "test.object_menu_op"
    bl_label = ""

    action: bpy.props.EnumProperty(items=generate_enum)
    last_value = ""

    def execute(self, context):        
        __class__.last_value = self.action
        for obj in bpy.data.collections["Obj_Collection"].all_objects[:]:
            obj.hide_viewport = True
            obj.hide_render = True        
        bpy.data.objects[self.action].hide_viewport = False
        bpy.data.objects[self.action].hide_render = False
        return {'FINISHED'}

class SelectNext(Operator):
    bl_idname = "select.nextmenuitem"
    bl_label = ""
    bl_description = ""
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        from operator import itemgetter, indexOf
        enum = generate_enum(None, context)
        if enum:
            selected = TEST_OT_Object_Select_Menu.last_value
            try:
                next_index = indexOf(map(itemgetter(0), enum), selected) + 1
            except ValueError:
                next_index = 0
            item = enum[next_index % len(enum)][0]
            bpy.ops.test.object_menu_op(action=item)
            return{'FINISHED'}
        return {'CANCELLED'}

1 Like

Yes!!! That is exactly what I was trying to do, but I didn’t know how to explain it. This works perfectly. Thank you once again. I owe you a beer, if you message me your paypal address I will send one your way.

1 Like