[UI] Dynamic Drop Down Menu

Hello Pythoncoders !
As some of you already know , I recently wrote an addon about material searching. (see link below)

Unfortunately, the user must currently type in every material he wants to search which can be annoying after a while, so my idea was to built in a dynamic drop down menu which has all the material available.

The most work is already done, but i have a little problem my drop down menu is not dynamic ! If the user creates new materials then the list will not update ? Can someone help me out to make a DDDM out of my DDM ? : D

Here is a link to my experimental source code ( can be run without any error messages ) :
http://www.pasteall.org/47562/python
As you can see the DDM works quite well, but as i said it will not update if new materials will be created.
My idea was to write a timer that updates the DDM every 300 ms or somewhat, but i don’t know how to realize it!
I already opened the same topic on blendpolis.de a few days ago, but no one does know the solution.

I hope someone is able to help me !

Thanks in advance for any help!

greeting
-Black

PS: My Add On for those who are interested :
http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/3D_interaction/Material_Search

Maybe a prop_search? (the below code invokes a props dialog, but you can do the same in a panel)

import bpy

class SimpleOperator(bpy.types.Operator):
    """Tooltip"""
    bl_idname = "object.simple_operator"
    bl_label = "Simple Object Operator"
    bl_options = {'REGISTER', 'UNDO'}
    
    prop = bpy.props.StringProperty(name="Material", maxlen=63)
    mats = bpy.props.CollectionProperty(type=bpy.types.PropertyGroup)

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

    def execute(self, context):
        print(self.prop)
        return {'FINISHED'}
    
    def draw(self, context):
        layout = self.layout
        layout.prop_search(self, "prop", self, "mats", icon='MATERIAL')
        
    def invoke(self, context, event):
        self.mats.clear()
        for i in range(1, 7):
            self.mats.add().name = "Material %i" % i
        return context.window_manager.invoke_props_dialog(self)


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')


You can also use an EnumProp with update and items callbacks.
Here’s an example that uses a dynamic enumprop, although it is used for a search popup:

import bpy

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, 7):
            self.mats.add().name = "Material %i" % i
        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')


Hi, had a quick look and here’s some notes


def count_materials():
    var = bpy.data.materials
    x = str(var)
    mat_count = x[16:-22]
    max = int(mat_count)
    return max


max = len(bpy.data.materials)


            var = obj.material_slots.data.active_material
            var = str(var)
            Material = var[23:-3]


mat = obj.active_material
Material = mat.name

When you set up a property with bpy.types.Scene.xxxx = bpy.props.XxxxProperty you can access it via scene.xxxx rather than scene[‘xxxx’]. This way it will return the default value. Otherwise you need to initialise the property or it throws a bug.

Lastly a menu construct instead


import bpy
from bpy.props import StringProperty




def get_objs_with_mat(context, mat_name):
    ob_list = []
    for ob in context.scene.objects:
        mats = [s.name for s in ob.material_slots]
        if mat_name in mats:
            ob_list.append(ob.name)
    print("%d objects with material:%s" % (len(ob_list),mat_name))
    print(ob_list)
    
    
class SimpleOperator(bpy.types.Operator):
    """Print Material name to Console"""
    bl_idname = "object.simple_operator"
    bl_label = "Simple Object Operator"
    mat = StringProperty(default="")
    @classmethod
    def poll(cls, context):
        return bool(len(bpy.data.materials))


    def execute(self, context):
        print(self.mat)
        context.screen.search = self.mat
        get_objs_with_mat(context, self.mat)
        return {'FINISHED'}
    
class SimpleCustomMenu(bpy.types.Menu):
    bl_label = "Materials"
    bl_idname = "OBJECT_MT_simple_custom_menu"


    def draw(self, context):
        layout = self.layout
        for mat in bpy.data.materials:
            op = layout.operator("object.simple_operator", text=mat.name)
            op.mat = mat.name


def register():
    bpy.types.Screen.search = StringProperty(default="")
    bpy.utils.register_module(__name__)






def unregister():
    bpy.utils.register_module(__name__)




if __name__ == "__main__":
    register()


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



The layout


        row.menu("OBJECT_MT_simple_custom_menu", text=context.screen.search)

Another useful way to make an updated list is with a standard py property


import bpy


def get_objs_with_mat(self):
    ob_list = []
    for ob in self.scene.objects:
        mats = [s.name for s in ob.material_slots]
        if self.screen.search in mats:
            ob_list.append(ob.name)
    print("%d objects with material:%s" % (len(ob_list),self.screen.search))
    return ob_list
    
bpy.types.Context.ob_matlist = property(get_objs_with_mat)

Here is the output from the console


>>> C.screen.search = D.materials['Material'].name
>>> C.screen.search
'Material'


>>> C.ob_matlist
5 objects with material:Material
['Cube.004', 'Cube.003', 'Cube.002', 'Cube.001', 'Cube']




PS… just noticed I used screen.search rather than scene.search… no reason… tired.

Thank you for your answers !

@CoDEmanX,
Thank you! I tried to analyse your both examples, but i could’nt figure out how to use this information in my code so that the Drop Down Menu is dynamic, i try it again tomorrow. Could you or someone else explain to me how the “prop_search” works? An example would be nice :slight_smile: I’ve never used it, i have just a week of experience with blender-python, so excuse my question pls.

@batFINGER
Thank you so much! I learned alot from your answer and i don’t even read all of it yet. Now i don’t need the whole function “count_material” anymore. Thank you for all the tips, with these information everything will be easier :slight_smile: I will read the rest tomorrow, its too late and im too tired now. Thx!

PS: Unfortunately your way to improve:

        var = obj.material_slots.data.active_material
        var = str(var)
        Material = var[23:-3]

Does not work. It says ‘Object has no attribute “name”’

PS. Thank you for fixing a bug in my Code!! I replaced every scn.[‘Search’] with scn.Search, now there is no bug anymore! :slight_smile:

Yes the object could have no active material. Always check for against None.



mat = obj.active_material
if mat is not None:
    mat_name = mat.name

Otherwise you can be pretty sure that all blenddata objects have a name.

Here’s an example of a dynamic enum and of prop_search. It would be nice to combine both, but it is not allowed to update the required CollectionProperty (prop_search accepts this only) in the enum items-callback. You could use an operator / button the user would have to click, to get around this update problem (click=manual update). But would probably be better to open a search popup instead, if one has to click a button anyway.

import bpy


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
        layout.prop(context.scene, "dyn_list")

        layout.prop_search(context.scene, "mat", bpy.data, "materials")

def items_cb(self, context):
    # not allowed to dynamically add items to a CollectionProperty here :(
    
    mats = []    
    for mat in bpy.data.materials:
        mats.append((mat.name, mat.name, mat.name)) # id, label, description
            
    return mats

def update_cb(self, context):
    print("Updated:", self.dyn_list)

def register():
    bpy.utils.register_class(HelloWorldPanel)
    
    bpy.types.Scene.dyn_list = \
        bpy.props.EnumProperty(items=items_cb, update=update_cb)
        
    bpy.types.Scene.mat = bpy.props.StringProperty()

def unregister():
    bpy.utils.unregister_class(HelloWorldPanel)
    del bpy.types.Scene.dyn_list
    del bpy.types.Scene.mat


if __name__ == "__main__":
    register()


Sorry, new here. Do I have to make a post before I can post a new thread?
Python Menu Related…?

THX for your Answers!
Alright ! Cool, almost everything works now as i wanted :slight_smile: THANK YOU SO MUCH! CoDEmanX :wink: but there is one little thing left that makes trouble, maybe you guys can help me out. The function “update_cb” does not work. My AddOn will not work as expected because of that.

I can choose now a Material from the dynamic drop down list but i want the text from the search input to be updated as well. I don’t know why the update_cb does not work ? The search input text will just update if i press the search/refresh button.

Would be nice if someone could have a look over my new Code. And pls tell me if you find something to improve :slight_smile:

http://www.pasteall.org/47582/python

Thank you batFINGER!
I had to edit your code a bit, but now it works :):


mat = obj.active_material
if mat is not None:
     Material = mat.name
else:
         Material = ""

But the code is now even longer compared to my previous one, is it faster your way?


var = str(obj.material_slots.data.active_material)
Material = var[23:-3]

Edit: or even shorter:


Material = str(obj.material_slots.data.active_material)[23:-3]

Thanks in Advance for any help !

greeting
-Black

Ok, here’s some code that should be directly usable for your addon:

# Create the collection on material update

def update_materal(self, context):
    
    self.index = -1
    
    obs = self.objects # CollectionProperty
    obs.clear()
    
    ob_list = []
    
    for ob in context.scene.objects:
        if ob.type != 'MESH':
            continue
        
        for mat in ob.data.materials:
            if mat is not None and mat.name == self.material and ob not in ob_list:
                item = obs.add()
                item.name = ob.name
                ob_list.append(ob)

This is called if user picks a material from the prop_search:

def update_index(self, context):
    
    try:
        ob_name = self.objects[self.index].name
    except IndexError:
        print("Material Object Search: Bad objects list index")
        return
    
    ob = context.scene.objects.get(ob_name)
    if ob is not None:
        #jump_to_object(...)

The data structure to store the relevant data:


class MaterialObjectSearch(bpy.types.PropertyGroup):
    material = StringProperty(update=update_materal)
    objects = CollectionProperty(type=bpy.types.PropertyGroup)
    index = IntProperty(update=update_index)

Note the callbacks, StringProp callback will call update_material (see above) and IntProperty if user clicks on an item in the template_list (causes a change of the index!)

And my suggestion for the panel code:


        scn = context.scene
        
        layout.prop_search(scn.Black_myProps, "material", bpy.data, "materials", text="")

        layout.template_list("UI_UL_list", "MaterialObjectSearch", 
                             scn.Black_myProps, "objects", 
                             scn.Black_myProps, "index")

Does that work for your addon? Not sure if it behaves like you expect…

Thank you so much for your help !!! :slight_smile:

greetings
-Black

Both, the material field and the list, are in fact searchable:

If you find a piece of useful code and take it over, I would be happy about by name mentioned in the addon somewhere :slight_smile:

Thats perfect CoDEmanX!!! I did not knew that it’s possible to input something into the Drop Down List, if i would knew this earlier… Thank you so much for your help!! :slight_smile:

greetings
-Edward

Nice Addon!
It would be nice if the wildcard filter at the bottom updated dynamically on the fly when typing into it.
It seems to only update when clicking off of it(losing focus) or hitting Enter/Return.

Where do you set scene.dyn_list? after uncommenting it it works ok


>>> C.scene.dyn_list = 'MMMM'
Traceback (most recent call last):
  File "<blender_console>", line 1, in <module>
TypeError: bpy_struct: item.attr = val: enum "MMMM" not found in ('Material', 'Material.001', 'Material.002')


>>> C.scene.dyn_list = 'Material.001'
UPDATE BEGIN
UPDATE END

However as you have found out … you can’t change the items of an enum prop once registered.

Oh and on the other bit of code you didn’t like, you could always use the ternary operator, eg


Material = mat.name if mat is not None else ""

Which is surely easier to follow than slicing strings.

Ahh thx! i see. I thought that this update function will work for this list (see picture)
sorry. Do you know if it is possible to call an update function for this list ? I couldn’t figure out yet how to call an update function with this list.

mat = obj.active_material
Material = mat.name if mat is not None else “”

Thank you ! It’s still longer then my code, but it might be easier to understand

However as you have found out … you can’t change the items of an enum prop once registered.
That’s not true, EnumProperty can take a function as “items” parameter and will call it back whenever it needs an update. An enum can’t be used for the material selection however, since the prop_search wants a collection, and those don’t accept callbacks. To search “live” in the object list, you need to use this:

# Panel draw code
          row.prop(wm.mat_ob_search, "search", text="", icon="VIEWZOOM")
          props = row.operator("wm.context_set_string", text="", icon="X")
         props.data_path = "window_manager.mat_ob_search.search"
         props.value = ""

  # ...  
def update_search(self, context):
     update_material(self, context)

# ...     
search = StringProperty(description="Search phrase for object list", update=update_search)

The built-in list filter doesn’t support live updates, and even if you relaced the filter with custom python code, it won’t trigger (only once as you leave the field).