Custom UI pulldown of PointerProperty

I’ve got an addon with a PropertyGroup added to bpy.types.Collection with many properties - most important of which are "type" (an enum) and "name" (a string).

This name must be different than the name blender uses for the collection due to various reasons.
The blender collections will be numerous and their names will be generated serially so names will look like ID_000001. However the addon’s name property will be human readable, and far more convenient a way to set pointers by.
These collections are used in various other UI PointerProperty fields and filtered using custom polling functions based on my custom "type" property (all of this is currently working quite well).

I’d like to make it so that instead of the PointerProperty showing the blender name (default behavior), it shows my custom property name instead, both once selected, and in the dropdown of options when setting the property. A dynamic enum might be the only way to do this, but it doesn’t feel like the most elegant solution, so I’m hoping someone has a better solution.

Thanks in advance.

Hello, I’m sorry but I have a bit of trouble visualizing your setup. Could you add a little diagram or add a bit more info, or some code maybe ? Cheers

Hopefully this gives a bit more info as to how my addon is structured…
This is some pseudo code as an example (typed from memory, so please just ignore typos if any)

# Custom property group
class my_addon_props(bpy.props.PropertyGroup):
    name: bpy.props.StringProperty(name='name')
    type: bpy.props.EnumProperty(name='type', items=[('group', 'group', ''), ('option', 'option', ''), ('variant', 'variant', '')])
    parent: bpy.props.PointerProperty(name='parent', type=bpy.types.Collection)

# Add property group to all collections
bpy.types.Collection.my_addon = my_addon_props()

Now say we create some collections. Each of these collections will be named in sequence following the pattern ID_###### (trust me there are reasons, hundreds of collections, and that enum above in reality is more like 10 types instead of 3).

On the Collections my_addon.parent property we want to select another collection from the data.
I have a custom polling function so that only certain collections will be visible to the parent field based on a couple criteria, such as the current collections’ my_addon.type value… so for example only the group type collections will be visible as a parent when viewing anything marked as an option type, and so on.
(And to reiterate all of this is working quite well already)

However the issue comes when you select this parent PointerProperty in the UI like the image below.

image
This listing shows the blender name for the collections, however it would be far more useful for the user to see the value of the my_addon.name property in this list.

I’m trying to do this without resorting to some custom runtime generated enum of the available options. I feel the conversion to / from enum, extra property, extra update function and storage of the pointer value would be less than ideal that way, and would be improved if this property could perhaps have a custom draw function to override the default display behavior.
This all might not be possible and I’ll have to go the enum route?
Hoping to get input from ppl with more bpy api experience than I have, as this is my first addon (converting some of our maya pipeline tools into blender :wink: )

Unfortunately, I don’t think it’s possible to change the display name of items in the search, and you do have to use an EnumProperty if you want to display a custom name.

However, you can make it quite succinct if you want, like this (the comments make it look longer than it really is :sweat_smile: ):

class CollectionSettings(PropertyGroup):

    # The custom name of this collection. Here it just appends "Custom" to the collection name
    # (The collection can be accessed with self.id_data)
    my_name: StringProperty(get=lambda self: self.id_data.name + "Custom")


class SceneSettings(PropertyGroup):

    # The actual collection pointer
    coll: PointerProperty(type=bpy.types.Collection, name="collection")

    # Get the an item for each collection, but using a custom name we have defined.
    def get_items(self, context):
        items = []
        for i, col in enumerate(bpy.data.collections):
            # Used to get the custom of the collection (Including the colors)
            icon_val = bpy.types.UILayout.icon(col)
            # Add this item with the custom display name
            items.append((col.name, col.my_settings.my_name, col.name, icon_val, i))
        return items

    # The enum property we use as a proxy for the collection pointer.
    coll_enum: EnumProperty(
        items=get_items,
        name="Collection",
        # Make sure the enum always displays the correct value
        # by making the get function always return the index of the currently selected collection
        # This means you can set the pointer from the api, and enum will update automatically.
        get=lambda self: list(bpy.data.collections).index(self.coll) if self.coll else -1,
        # Also set the pointer when the enum is set.
        set=lambda self, value: setattr(self, "coll", bpy.data.collections[value]),
    )


def register():
    bpy.types.Scene.my_settings = PointerProperty(type=SceneSettings)
    bpy.types.Collection.my_settings = PointerProperty(type=CollectionSettings)


def unregister():
    del bpy.types.Scene.my_settings
    del bpy.types.Collection.my_settings

What it looks like using the pointer:
image

What it looks like using the enum:
image

Unfortunately, this method means that you lose the ability to search, but hopefully that isn’t too big of a problem…

Hope that helps!

3 Likes

Thank you @Strike_Digital
This is certainly a good workaround for my issue.
I’m able to merge my polling logic with the enum items function, and the original pointer property keeps it’s link!!!
I’ve never seen an override of the get / set in any examples… and using the index number instead of a string to resolve the collection should work around the issues I foresaw using strings…
Thank you for that example!

One issue I see so far is I’m getting odd readouts during panel refreshes (on resize or mouse hover events for example).
In the image below you can see the left most column name gets replaced to an odd accented o character (every entry should read body_std)… same can happen to the center column too (2nd image).
They seem to go back to the correct value most of the time after a refresh or so, but still odd.
Any thoughts?

image
image

Most likely comes from the “known issue” described in the docs : https://docs.blender.org/api/current/bpy.props.html#bpy.props.EnumProperty

Dynamic enum items need a “hard” handle to work properly.

You can read more about it there https://blender.stackexchange.com/questions/216230/is-there-a-workaround-for-the-known-bug-in-dynamic-enumproperty

A simple workaround is to use a global variable

items = []  # Defined globally

    def get_items(self, context):
        global items
        items.clear()
        for i, col in enumerate(bpy.data.collections):
            # Used to get the custom of the collection (Including the colors)
            icon_val = bpy.types.UILayout.icon(col)
            # Add this item with the custom display name
            items.append((col.name, col.my_settings.my_name, col.name, icon_val, i))
        return items
2 Likes

The globally defined items seems to have fixed the display issues.
Thank you @Gorgious

Yeah, that bug is super annoying to figure out if you’ve never found it before.
If just keeping a global variable still doesn’t work ever, have a look at this solution, which helped me a lot:

Here’s the API documentation with some useful examples of using the get and set arguments for properties:
https://docs.blender.org/api/current/bpy.props.html#getter-setter-example
They can be very useful for things like making properties read only, or doing something to a value before it is stored.

2 Likes