EnumProperty callback problems - a better workaround

A frequent use case of EnumProperties in Blender addons is to display a dropdown with a choice of scene objects. Of course which objects are available in a scene is only known when invoking the addon so a fixed list of choices is of no use here. For this scenario a callback option is provided. The callback can be any function that returns a list of choices. Unfortunately there are a number of severe bugs associated with using a callback: if your python code doesn't keep a reference to the items in the list it returns, Blender may crash. This bug is documented and a workaround suggested but this doesn't solve the problem completely. Have a look at the code below:
import bpy
from bpy.props import EnumProperty

available_objects = []

def availableObjects(self, context):
 available_objects.clear()
 for ob in bpy.data.objects:
  name = ob.name
  available_objects.append((name, name, name))
 return available_objects
 
class TestCase(bpy.types.Operator):
 
 bl_idname = "object.testcase"
 bl_label = "TestCase"
 bl_options = {'REGISTER', 'UNDO'}

 objects = EnumProperty(name="Objects", items = availableObjects)
if we hover the mouse over the second or third option we see that the description shows garbage, i.e. the description part of another item!
The really annoying part is that even if we make a full copy of the object's name (with ob.name[:]) the problem isn't solved. So probably the internal callback code trashes memory even outside its own allocated memory.
def availableObjects(self, context):
 available_objects.clear()
 for ob in bpy.data.objects:
  name = ob.name[:] # a copy, not just a reference
  available_objects.append((name, name, name))
 return available_objects
A way around this is to let the callback just return a list that is filled with values outside the callback code, for example in the code thst is executed when the user clicks on the menu item that selects the addon:
available_objects = []

def availableObjectsInit(self, context):
 available_objects.clear()
 for ob in bpy.data.objects:
  name = ob.name[:] # a copy, not just a reference
  available_objects.append((name, name, name))
 return available_objects
 
class TestCase(bpy.types.Operator):

        ... stuff omitted ... 

 objects = EnumProperty(name="Objects",
                    items = lambda self,context: available_objects)

  
def menu_func(self, context):
 availableObjectsInit(self, context)
 self.layout.operator(TestCase.bl_idname, 
                             text="TestCase",icon='PLUGIN')

def register():
 bpy.utils.register_module(__name__)
 bpy.types.VIEW3D_MT_object.append(menu_func)
This is usable even if the addon changes the number of objects. The only downside is that the code isn't reentrant because in its present form does not guard thread access to the global variable but as far as I know the Blender UI runs as part of a single Python interpreter so there's only one thread, which renders this point moot. The other issue is that it looks ugly, but as long as it works that's a minor issue :-) Note: you might wonder why we need the lambda here but that's because if we would point to just the list here, the list would be empty at the point where the EnumProperty was defined and apparently it makes a copy of that list so it would stay empty.

No comments:

Post a Comment