2
$\begingroup$

I'm trying to create a collection of items, that have dynamic EnumProperty (scene objects of given type):

import bpy

def enum_items(self, context):
    items = []
    for obj in context.scene.objects:
        if obj.type == 'MESH':
            items.append( (obj.name, obj.name, "") )
    return items

class SomePropertyGroupEntry(bpy.types.PropertyGroup):
    mesh_obj = bpy.props.EnumProperty(items=enum_items)
    val = bpy.props.IntProperty()

class SomePropertyGroup(bpy.types.PropertyGroup):
    collection = bpy.props.CollectionProperty(type=SomePropertyGroupEntry)
    index = bpy.props.IntProperty()

class SCENE_UL_list(bpy.types.UIList):
    def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
        if self.layout_type in {'DEFAULT', 'COMPACT'}:
            layout.prop(item, "name", text="")
            layout.prop(item, "mesh_obj", text="")
        elif self.layout_type in {'GRID'}:
            layout.alignment = 'CENTER'
            layout.label(text="", icon_value=icon)

class SCENE_OT_list_populate(bpy.types.Operator):
    bl_idname = "scene.list_populate"
    bl_label = "Populate list"

    def execute(self, context):
        context.scene.prop_group.collection.clear()
        for i in range(3):
            item = context.scene.prop_group.collection.add()
            item.name = "item"+str(i)
        return {'FINISHED'}

class SCENE_PT_list(bpy.types.Panel):
    bl_label = "UIList Panel"
    bl_idname = "SCENE_PT_list"
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "scene"

    def draw(self, context):
        self.layout.operator("scene.list_populate")
        self.layout.template_list("SCENE_UL_list", "", context.scene.prop_group, "collection", context.scene.prop_group, "index")

def register():
    bpy.utils.register_module(__name__)
    bpy.types.Scene.prop_group = bpy.props.PointerProperty(type=SomePropertyGroup)

def unregister():
    bpy.utils.unregister_module(__name__)
    del bpy.types.Scene.prop_group

if __name__ == "__main__":
    register()

It works as expected (properties are set and saved properly)...

enter image description here

...unless I add or delete a mesh object - EnumProperty values in collection items are shifted: enter image description here enter image description here

I'm guessing there is a problem with mesh object's enumeration... byt why? All objects have unique names, so items properties should be consistent despite the enumeration and mesh objects count in the scene... what am I missing here?

$\endgroup$
1
+100
$\begingroup$

The EnumProperty properties represent underlying integer values - 1, 2, 3, etc. - and the 'enum_items' function provides the conversion between the numbers and the textual description (in this case the names of the objects in the scene).

The problem is that the list returned by the 'enum_items' function is changing when meshes are added or removed from the scene so that the values held in the EnumProperty properties no longer corresponds with the same object. This means that as the sequence of objects changes, the corresponding enumeration value also changes, resulting in the values being reassigned.

The 'enum_items' list is currently being built directly from the context.scene.objects list and each item is appended to the list. When objects are added to the scene they are added to the start of the list of objects - hence they are appearing at the start of the list and this is shuffling the objects along the list - so what was in position 1 is now in position 2, what was in position 2 is now in position 3, and so on. A simple dirty fix to this would be to change the order of the list - by inserting each one at the beginning rather than appending them at the end. ie,

Change :

items.append( (obj.name, obj.name, "") )

to :

items.insert(0, (obj.name, obj.name, "") )

You should now find that adding new objects to the scene no longer upset the existing EnumProperties. However, deleting objects still presents a problem - unless it's always the 'newest' object that is deleted, although even then you have the problem that the EnumProperty still points to the same (empty) slot of the list (so if a new object is added again then it will be immediately populated into the same slot - rather than the slot being initially blank!).

To address this you can assign a unique identifier when adding items to the list (in which case it no longer matters on the order in the list since the uniqueid is used to maintain consistency regardless of the sequence) :

items.append( (obj.name, obj.name, "", <uniqueid>) )

This can be achieved as follows :

import bpy

# NEW CODE - Create a list to assign unique identifier to each mesh name
enum_items_store = []

def enum_items(self, context):

    items = []
    for obj in context.scene.objects:
        if obj.type == 'MESH':

            # NEW CODE
            # Scan the list of IDs to see if we already have one for this mesh
            maxid = -1
            id = -1
            found = False
            for idrec in enum_items_store:
                id = idrec[0]
                if id > maxid:
                    maxid = id
                if idrec[1] == obj.name:
                    found = True
                    break

            if not found:
                enum_items_store.append((maxid+1, obj.name))

            # AMENDED CODE - include the ID
            items.append( (obj.name, obj.name, "", id) )

    return items

class SomePropertyGroupEntry(bpy.types.PropertyGroup):
    mesh_obj = bpy.props.EnumProperty(items=enum_items)
    val = bpy.props.IntProperty()

class SomePropertyGroup(bpy.types.PropertyGroup):
    collection = bpy.props.CollectionProperty(type=SomePropertyGroupEntry)
    index = bpy.props.IntProperty()

class SCENE_UL_list(bpy.types.UIList):
    def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
        if self.layout_type in {'DEFAULT', 'COMPACT'}:
            layout.prop(item, "name", text="")
            layout.prop(item, "mesh_obj", text="")
        elif self.layout_type in {'GRID'}:
            layout.alignment = 'CENTER'
            layout.label(text="", icon_value=icon)

class SCENE_OT_list_populate(bpy.types.Operator):
    bl_idname = "scene.list_populate"
    bl_label = "Populate list"

    def execute(self, context):
        context.scene.prop_group.collection.clear()
        for i in range(3):
            item = context.scene.prop_group.collection.add()
            item.name = "item"+str(i)
        return {'FINISHED'}

class SCENE_PT_list(bpy.types.Panel):
    bl_label = "UIList Panel"
    bl_idname = "SCENE_PT_list"
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "scene"

    def draw(self, context):
        self.layout.operator("scene.list_populate")
        self.layout.template_list("SCENE_UL_list", "", context.scene.prop_group, "collection", context.scene.prop_group, "index")

def register():
    bpy.utils.register_module(__name__)
    bpy.types.Scene.prop_group = bpy.props.PointerProperty(type=SomePropertyGroup)

def unregister():
    bpy.utils.unregister_module(__name__)
    del bpy.types.Scene.prop_group

if __name__ == "__main__":
    register()

Note that mesh names are never removed from the list of IDs. This means that the unique identifier will only be re-used if a mesh is re-created or renamed to the same name as a previous mesh.

$\endgroup$
  • $\begingroup$ I'll accept it since it solves the problem. Would you say enumerating will be consistent when saving/reopening file on different computers? I've been thinking about using some sort of unique id produced from hashed obj name (hashlib), however that produces way too large ints and chopping them wouldn't guarantee unique id, also been thinking about looking for some unique property of an object, but I don't think there is any I could use. So I'm guessing the safest bet would be to create a CollectionProperty in the scene and strore id's there (and check against it on new object)? $\endgroup$ – kilbee Apr 27 '17 at 9:03
  • $\begingroup$ Thanks. I can't really comment on whether it will be consistent as I'm not really experienced in UI mods so must admit that I don't really know. However, the code doesn't currently store its state and would need to re-build the panel when Blender re-loads so I guess one solution would be to store the object name rather than enumeration id - and just re-generate new ids as required. $\endgroup$ – Rich Sedman Apr 27 '17 at 9:17

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.