Making a dynamic dropdown Menu in a Panel

Hello All,
I have been having some trouble with drawing a menu for a user to select an option.

class BasicMenu(bpy.types.Menu):
     def draw(self, context):
            bone_path = obj.pose.bones["bone-name"]
            for y, _ in bone_path.items()
                print(y) #This prints the expected string
                self.layout.prop(bone_path, f"{y}", text=f"{y}")
            
#Returns error: rna_uiItemR: property not found: Bone-name.prop-name

In the example menu I have above I am able to draw a menu, but i am not able to make a selection. Instead I am able to edit each entry, i assume because im using layout.prop.

How do I make a menu where a user can select a property, and then I can store that reference (in I assume a pointer property) for later use by an operator?

To see this in a full context you can checkout this operator file on my github where I am working on a fork of Rhubarb Lipsync that spits out integers instead of shape keys.

Maybe you want to explore Enum properties if you want the user to be able to select items in a list ?

https://docs.blender.org/api/current/bpy.props.html#bpy.props.EnumProperty

and

https://docs.blender.org/api/current/bpy.types.UILayout.html#bpy.types.UILayout.prop_enum

2 Likes

Ok I will continue to learn about Enum Properties, I have seen those docs before but I have trouble understanding until can see an example that uses it. I knew I could make a list with enum but i don’t understand how to use it for user selections.

Is this script a good example of a proper usage? import bpyclass MyProperties(bpy.types.PropertyGroup): my - Pastebin.com found from this: video

Also I can dynamically grow and shrink the list using .add and .remove right?

I just started to understand properties, and I find it pretty awkward. But ok, here I do my best to explain it. (So it could be I am rambling).

first you can do some imports llke:

import bpy
from bpy.props import EnumProperty, PointerProperty
from bpy.type import Panel, Operator, PropertyGroup

A property group is a group of properties, a class. You have only 1 property, but if you use more, then you use a PropertyGroup. And a PropertyGroup looks like this:

class TemplProperties(bpy.types.PropertyGroup):
          
     my_enum : bpy.props.EnumProperty(name = "Dimensions", description = "Choose format", 
     items = (
              ('Profile Picture', "400x400", "Profile Picture"), # value, dropdown-value, tiptool
              ('Header', "1500x500", "Header"),                  # instead value try operator
              ('Post Image', "1200 x 675", "Post Image")  
              
              ) )

You can make this enum dynamic, I guess, by using python. Or, you can use also a UIList but that could be quite mind-boggling to use. Better start like this first.

Then in the register/unregister functions, you add this property to an object, winman, scene (or other things I am not aware off). Here I use the scene to store the enum:

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    bpy.types.Scene.scene_propname = PointerProperty(type=TemplProperties)        

def unregister():
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)
    del bpy.types.Scene.scene_propname

Some explanation:
scene_propname: I could name it anything I want. But we will use this name elsewhere, in the operator, and in the panel class. Notice that it is called here a PointerProperty (don’t ask me why, but it seems that is how it works). TemplProperties is the name of the PropertyGroup class.

How to use the enum in an operator?

class TEMPL_OT_operator(Operator):
    """ tooltip goes here """
    bl_idname = "nmssm.operator"
    bl_label = "Label of operator"
    bl_options = {"REGISTER", "UNDO"}

    @classmethod
    def poll(cls, context):
        return context.mode == "OBJECT"

    def execute(self, context):
        print(context.scene.scene_propname.my_enum)        
        
        return {'FINISHED'}

You see here scene_propname followed by .my_enum.

How to use the enum in the panel?

class TEMPL_PT_rendersettings(Panel):
    """ Tooltip """

    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_label = "Templlabel"
    bl_category = "Templ"
    bl_options = {"DEFAULT_CLOSED"}

    def draw(self, context):
        layout = self.layout
        scene = context.scene
        prop_one = scene.scene_propname
        layout.prop(prop_one, "my_enum")                 
        box = layout.box()

        row = box.row()
        row.operator("nmssm.operator", icon="PREVIEW_RANGE")
        row = box.row()
# Recap (panel)
# - Make a convenience variable:
#   scene = context.scene
#   prop_one = scene.scene_propname
# - layout.prop(prop_one, "my_enum")

To complete the script, don’t forget to add the classes list:

classes = [
    TemplProperties,
    TEMPL_OT_operator,
    TEMPL_PT_rendersettings,
]

And the code to run it as script at the bottom:

if __name__ == '__main__':
    register()
1 Like

There are 2 things i would remark on regarding that specific video (after a quick scrub through)

~ 8:10 in the video when removing the reference to the pointer that line should be at the same level of indentation as the for statement above it. Having it at the indentation level he has shown attempts to delete the pointer multiple times instead of just once. Also, (~7:30) when creating the pointer reference it should be at the level of indentation of the for loop not inside the loop.

Next because a predefined enumerated property requires 3 fields. You can make better use of those fields as long as you are aware that only the 1st field is unique.

python - How do I change text on a panel row or hide it depending on the value of an EnumProperty? - Blender Stack Exchange

Note in this old example while functional ideally I should have used the command string as the identifier and the icon string as the description simply to better ensure a unique command.

Nezumi72/Multifunction_operator: Blender 2.9x example panel utilizing a single operator to perform different functions based on enumerated list value. Based on https://blenderartists.org/t/how-to-structure-add-on-code/1288322/2. (github.com)

1 Like

Ok, this is a great breakdown thank you for taking the time. It is the plain English explanation I have needed to understand how this actually works.

Thank you and I will report back with my results!

Hey thank you for writing this and walking me thru some of the issues in the video. Also for these links which are really great examples

What I am noticing in both these examples you and @anon72338821 added is that neither use (bpy.types.Menu): which originally is how I thought this worked.

Also credit to @Gorgious because Enum Property seemed to be the term that was missing from my searches,

Material slots, for example, use a Menu (that triangle on the right):

class MATERIAL_MT_context_menu(Menu):
    bl_label = "Material Specials"

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

        layout.operator("material.copy", icon='COPYDOWN')
        layout.operator("object.material_slot_copy")
        layout.operator("material.paste", icon='PASTEDOWN')
        layout.operator("object.material_slot_remove_unused")

And then in a Panel class they use it like:
col.menu("MATERIAL_MT_context_menu", icon='DOWNARROW_HLT', text="")

But you were asking:

How do I make a menu where a user can select a property, and then I can store that reference (in I assume a pointer property) for later use by an operator?

So for that, you use an Enum Property.
If you want to make it dynamic, there might also be a way with getters and setters. But I have no experience with that:
https://docs.blender.org/api/current/bpy.props.html#getter-setter-example

Theres also an example of custom getter and setter in a link @nezumi.blend provided so im going that route because in my case it does need to be dynamically set.

And you are correct this is ultimately what i needed. Thank again so much for sharing ur knowledge, again will report back with the finished stuff

UPDATE: Ive got the defined list enum prop working thanks all for the help. Ive got the basics of it working.

Im looking at the blender docs for getter and setter for enum now havent fully figured it out yet so thats my next problem to solve.

1 Like

this should help you define a dynamic getter https://blender.stackexchange.com/questions/10910/dynamic-enumproperty-by-type-of-selection

also do note the documentatin advises you to keep a reference to the dynamic enum list otherwise you might experience weird things. One solution is to make it global

something along the lines of :

items = []
def my_settings_callback(scene, context):
    global items
    items.clear()
    items.extend([
        ('LOC', "Location", ""),
        ('ROT', "Rotation", ""),
        ('SCL', "Scale", ""),
    ])

    ob = context.object
    if ob is not None:
        if ob.type == 'LIGHT':
            items.append(('NRG', "Energy", ""))
            items.append(('COL', "Color", ""))

    return items

So I understand how this code snippet works, I actually stumbled across the same stack exchange post when I was trying to make my list dynamic. I was able to make it work this in print statements in the console, but I wasn’t able to draw that list in a dropdown menu with selections.

In this example the start of your the enum list is defined as items = [] and in the solution provided by @anon72338821 I see it is defined in a property group. This is the solution I think i need as I want to pass the input to an operator.

I also want the list to dynamically change depending on the context of what is selected.

So if I defined my enum similar to this

class TemplProperties(bpy.types.PropertyGroup):
          
     proplist : bpy.props.EnumProperty(name = "proplist", items = ( )

bpy.types.Object.customprops= bpy.props.PointerProperty(type=TemplProperties)

So in the example you provided they are using items.append
So I tried something similar to:

class MyPanel(bpy.types.Panel):
    def draw(self, context):
        obj = bpy.context.object
        props = obj.customprops.bl_rna.properties["proplist"]
        bone_path = obj.pose.bones["bone-name"]
        for y, x in bone_path.items():
            props.append(f"{y}", f"{y}", f"{y}")
        enum = items[props.proplist]

Now when test this code line by line in the blender python console I get this error

AttributeError: 'EnumProperty' object has no attribute 'append'

So I assume .append doesn’t work for enum props, or my syntax is bad. In the doc I was able to find a set function in the enum docs. So its my usage that is wrong.

Based on my reading from the docs what I think I need to do is: I need to create a new operator similar to .append give variables to my enum prop for me and then call that operator in my FOR statement. Which i what I meant by custom getter/setter.

I did some research and found this post Add/remove EnumProperty items? - Coding / Python Support - Blender Artists Community which clearly explained the disconnect I have with this quote

So essentially I have my iterable variable in my bone_path I have my Enum prop in my group I am only not understanding the some function to generate the enum items.

Will do more reading when I can, i think the solution is in that post i just linked so i just need to break it down for myself.

Try with this ?

items_handle = []
def get_items(self, context):
    global items_handle
    items_handle.clear()
    obj = context.object
    props = obj.customprops.proplist
    bone_path = obj.pose.bones["bone-name"]
    for y, x in bone_path.items():
        items_handle.append(f"{y}", f"{y}", f"{y}")
    return items_handle

class TemplProperties(bpy.types.PropertyGroup):
     proplist : bpy.props.EnumProperty(name="Prop List", items=get_items)

bpy.types.Object.customprops = bpy.props.PointerProperty(type=TemplProperties)

class MyPanel(bpy.types.Panel):
    def draw(self, context):
        self.layout.prop(context.object.customprops, "proplist")
1 Like

So essentially I have my iterable variable in my bone_path I have my Enum prop in my group I am only not understanding the some function to generate the enum items.

I made simple addon example, hope it help you.

1 Like

Hey thanks so much for this, I am going look thru what you’ve made here and see if I can get a better understanding of how this is suppose to work. Much Appreciated! Just at first glance I think i this will help me and I hope I can integrate it into what im doing.

I am hoping to use a loop to automatically generate the list items so this example you provided is really great first steps towards that.

ok so major update here @IIIFGIII saved the day with that explanation. It pushed me over the edge and now I understand how this works, and I was able to test an example on my side. Thanks so much for the help with this.

I have a much better understanding of this now. I am now able to understand what @Gorgious was getting at. I am also able to read the current enum selection using bpy.context.object.my_custom_propgrp.get('my_enum_prop') I have to clean up my code a lot before its human readable.