2
$\begingroup$

So I was making an addon with a dynamic EnumProperty and apparently ran into a known bug when trying to use Expand = True for this property in the panel. Although everything works fine when not using Expand = True, for my addon, it is essential to have the options expanded and not collapsed in a drop-down menu. The bug can be found in the documentation:

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

enter image description here

This is the most compact example I could think of (add a few objects to the scene to see the bug in effect):

import bpy

def getEnumStuff(self, context):
    return [((obj.name, obj.name, "")) for obj in bpy.context.selectable_objects]

class OBJECT_PT_example_panel(bpy.types.Panel):
    bl_category = "Example"
    bl_label = "Example"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_context = "objectmode"

    def draw(self, context):
        layout = self.layout
        col = layout.column()
        col.prop(context.scene, "stuff", expand = True)
        
classes = (
    OBJECT_PT_example_panel,)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    bpy.types.Scene.stuff = bpy.props.EnumProperty(
        name = "stuff", 
        items = getEnumStuff,
        description = "")

def unregister():
    for cls in classes:
        bpy.utils.unregister_class(cls)

if __name__ == "__main__":
    register()

I tried a lot of things, such as making the list global before returning it from the getEnumStuff function and copying the name strings in order to have them point to a different address than the original object names. Nothing I tried improved the results so far.

The question is, is there even a workaround for this bug? Is there another way to have the options expanded in the panel while still being able to select them (not only printing the name)?

$\endgroup$

2 Answers 2

4
$\begingroup$

The Warning in the documentation states the EnumProperty should have a permanent handle on the items.

There is a known bug with using a callback, Python must keep a reference to the strings returned by the callback or Blender will misbehave or even crash.

One way I see, without resorting to a global variable, is to add an attribute to the getEnumStuff function. See relevant PEP 232. And then "manually" update the values before drawing the col.prop drawing call.

By the way, if you pass the context in a function, you should not use bpy.context afterwards but rather the context parameter because there could be a mismatch between the two, resulting in an incorrect context error.

import bpy


def getEnumStuff(context):
    getEnumStuff.enum_stuffs = [((obj.name, obj.name, "")) for obj in context.selectable_objects]
getEnumStuff.enum_stuffs = []


class OBJECT_PT_example_panel(bpy.types.Panel):
    bl_category = "Example"
    bl_label = "Example"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_context = "objectmode"

    def draw(self, context):
        layout = self.layout
        col = layout.column()
        getEnumStuff(context) # Update enum items before drawing them
        col.prop(context.scene, "stuff", expand = True)
        
        
classes = (
    OBJECT_PT_example_panel,)


def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    bpy.types.Scene.stuff = bpy.props.EnumProperty(
        name = "stuff", 
        items = lambda self, context: getEnumStuff.enum_stuffs,
        description = "")


def unregister():
    for cls in classes:
        bpy.utils.unregister_class(cls)


if __name__ == "__main__":
    register()

Result :

enter image description here

$\endgroup$
1
$\begingroup$

For an EnumProperty on a bpy.types.Object, I had text corruption with storing the items array in a global. A global string cache worked better for me.

STRING_CACHE = {}
def intern_enum_items(items):
    def intern_string(s):
        if not isinstance(s, str):
            return s
        global STRING_CACHE
        if s not in STRING_CACHE:
            STRING_CACHE[s] = s
        return STRING_CACHE[s]
    return [tuple(intern_string(s) for s in item) for item in items]


def getEnumStuff(self, context):
    # make your items like normal here
    items = ...

    # call intern_enum_items before you return
    return intern_enum_items(items)

The downside is the strings in STRING_CACHE will never be GCed (sort of the opposite of the original problem). For most people though, I doubt the cache will grow large enough for it to become an issue.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.