Adding an Operator to the Context Menu of a UIList

Hello,
How may I add an operator to the Context Menu of a UI List?

Context menus seem to be defined per Spaces (e.g. NODE_MT_context_menu in the below code). Could this be set for the menu of a region? Thank you!

def draw_menu(self, context):
    layout = self.layout
    layout.separator()
    layout.operator("node.duplicate_move", text="My new context menu item")

def register():
    bpy.types.NODE_MT_context_menu.append(draw_menu)

def unregister():
    bpy.types.NODE_MT_context_menu.remove(draw_menu)

I’m not aware of a way to do this, typically uilist context menus are shown via a button to the right of the list (including all of the uilists native to Blender)

Thank you for the reply!

You can override the context menus for buttons and ui list items.
See Add Operator to Right Click Menu for Operators - #2 by iceythe

Here’s an example that adds a label to the context menu when a vertex group is right clicked.

import bpy


class WM_MT_button_context(bpy.types.Menu):
    bl_label = ""
    # Leave empty for compatibility.
    def draw(self, context): pass


# Your draw function.
def draw(self, context):
    if hasattr(context, "button_pointer"):
        print("Clicked: ", context.button_pointer)

        if isinstance(context.button_pointer, bpy.types.VertexGroups):
            layout = self.layout
            layout.label(text="I'm in vertex groups context menu!")

    if hasattr(context, "button_prop"):
        print("Property:", context.button_prop.identifier)

    if hasattr(context, "ui_list") and context.ui_list is not None:
        print("UI List: ", context.ui_list.bl_idname)


if __name__ == "__main__":
    # Register menu only if it doesn't already exist.
    rcmenu = getattr(bpy.types, "WM_MT_button_context", None)
    if rcmenu is None:
        bpy.utils.register_class(WM_MT_button_context)
        rcmenu = WM_MT_button_context

    # Retrieve a python list for inserting draw functions.
    draw_funcs = rcmenu._dyn_ui_initialize()
    draw_funcs.append(draw)

@iceythe Thank you for this! It works with Vertex Groups as “Clicked” prints <bpy_struct, VertexGroups at 0x000001AC0DAF3908>

However, when I right click in my UIList, it prints <bpy_struct, Scene("Scene") at 0x000001AC0DBC2048>

Have I set something up incorrectly, context-wise, in my UIList/Panel?

Thank you

If your ui list is at context.scene.mylist, then context.button_pointer points to context.scene, so that seems correct.

The draw function prints a few things from context, if they exist:

context.button_pointer: The object you registered your ui list on. The scene in this case.
context.button_prop: The name of your ui list e.g mylist in context.scene.mylist.
context.ui_list: The ui list object itself, if Blender finds it. Otherwise None.

Using these values, and assuming the context holds any of these, you should be able to build a predicate in the draw function, so that your custom entry only is drawn when the right click menu is invoked on your ui list specifically.

Examples

def draw(self, context):
    ui_list = getattr(context, "ui_list", None)
    if ui_list is not None and ui_list == context.scene.mylist:
        # Draw operator here
def draw(self, context):
    if hasattr(context, "button_pointer"):
        if getattr(context.button_pointer, "mylist"):
            # Draw operator here

You have been a wonderful help as always, @iceythe . Thank you so much!

Hi @iceythe,
Thanks a lot for all these code examples, really useful.

To go further, I have an additional question. Indeed I would like to get the item that is exactly under the mouse in the ui_list when the right click is done to call the menu. This is to set the current item as being the one we clicked on (cause in practice the right clicked item do not automatically become the selected one).

Thanks to button_prop we get the selected item, but according to where we click it may not be the one below the click.

So do you know if there is a way to get this exact clicked item?

Thanks a lot for your help!

To my limited knowledge and testing I don’t think there is. I’m sure others will just point out the fact that just like anything else in Blender (especially object operations) it’s all context based therefore it makes sense that it requires an explicit selection of an object, or item in this case, in order to extract appropriate information out of it. I would in turn argue against that claim, since we can detect context changes by just hovering over different UI sections of Blender, hence why would right clicking require prior explicit selection, if that makes sense?

Ohhh, and here’s another post on stack exchange where a user claims that it’s possible to access information of the item being right clicked, which just creates a lot of confusion and misinformation around it.

In the end, just like you, I would also like to definitely know if it’s possible or not.

And it turns out there is an elegant way of doing this, at least for custom made UI lists.
First and foremost, in the UI list’s draw_item method store the item in a dynamically created and globally accessible variable via layout.contex_pointer_set(item, “arbitrary_name”) and then create a custom drawing method that will be appended to a built-in UI_MT_button_context_menu.
This custom drawing method will handle the layout or GUI content added to the right click context menu.

Here’s a really simple example of such implementation:

#TESTED IN BLENDER 4.1

import bpy
from bpy.props import IntProperty, StringProperty, CollectionProperty, PointerProperty
from bpy.types import Operator, Panel, UIList, PropertyGroup

# ---------------------------
# Property Group for List Items
# ---------------------------
class PersonItem(PropertyGroup):
    name: StringProperty(name="Name", default="John Doe")
    age: IntProperty(name="Age", default=30, min=0, max=120)
    birth_year: IntProperty(name="Birth Year", default=1990, min=1900, max=2020)

# ---------------------------
# Custom UI List
# ---------------------------
class PERSON_UL_List(UIList):
    def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
        # Set context pointer for right-click
        layout.context_pointer_set("person_item", item)
        
        # Display item info
        row = layout.row()
        row.alignment = 'LEFT'
        row.prop(item, "name", text="", emboss=False)
        row.separator()
        row.prop(item, "age", text="Age:", emboss=False)
        row.separator()
        row.prop(item, "birth_year", text="Year of Birth:", emboss=False)

# ---------------------------
# Right-Click Context Menu
# ---------------------------
def draw_context_menu(self, context):
    item = context.person_item
#    item = get_attr(context, "person_item", None) #alternative
    layout = self.layout
    
    layout.separator()
    op = layout.operator("object.print_list_item", text="Print Item Details")
    op.name = item.name
    op.age = item.age
    op.birth_year = item.birth_year
# ---------------------------
# Operator to Print Item Data
# ---------------------------
class OBJECT_OT_PrintListItem(Operator):
    bl_idname = "object.print_list_item"
    bl_label = "Print List Item"
    
    name: StringProperty(default="")
    age: IntProperty(default=0)
    birth_year: IntProperty(default=2025)
    
    def execute(self, context):
        print(f"\nItem Details:")
        print(f"Name: {self.name}")
        print(f"Age: {self.age}")
        print(f"Birth Year: {self.birth_year}")
        print(f"------------------")
        
        return {'FINISHED'}

# ---------------------------
# Panel to Display the List
# ---------------------------
class OBJECT_PT_CustomList(Panel):
    bl_label = "People List"
    bl_idname = "OBJECT_PT_custom_list"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = 'Tool'

    def draw(self, context):
        layout = self.layout
        scene = context.scene
        
        row = layout.row()
        row.template_list(
            "PERSON_UL_List", 
            "", 
            scene, 
            "people_list", 
            scene, 
            "people_list_index"
        )
        
        col = row.column(align=True)
        col.operator("object.add_person", icon='ADD', text="")
        col.operator("object.remove_person", icon='REMOVE', text="")

# ---------------------------
# Add/Remove Operators
# ---------------------------
class OBJECT_OT_AddPerson(Operator):
    bl_idname = "object.add_person"
    bl_label = "Add Person"
    
    def execute(self, context):
        person = context.scene.people_list.add()
        person.name = f"Person {len(context.scene.people_list)}"
        person.age = 20 + len(context.scene.people_list)
        person.birth_year = 2000 - len(context.scene.people_list)
        return {'FINISHED'}

class OBJECT_OT_RemovePerson(Operator):
    bl_idname = "object.remove_person"
    bl_label = "Remove Person"
    
    def execute(self, context):
        index = context.scene.people_list_index
        context.scene.people_list.remove(index)
        context.scene.people_list_index = min(
            max(0, index - 1),
            len(context.scene.people_list) - 1
        )
        return {'FINISHED'}

def add_generic_items():
    for i in range(6):
        person = bpy.context.scene.people_list.add()
        person.name = f"Person {i+1}"
        person.age = 30 - i
        person.birth_year = 1995 + i

# ---------------------------
# Registration
# ---------------------------
classes = (
    PersonItem,
    PERSON_UL_List,
    OBJECT_OT_PrintListItem,
    OBJECT_PT_CustomList,
    OBJECT_OT_AddPerson,
    OBJECT_OT_RemovePerson,
)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    
    bpy.types.Scene.people_list = CollectionProperty(type=PersonItem)
    bpy.types.Scene.people_list_index = IntProperty()
    

    
    # Register context menu
    bpy.types.UI_MT_button_context_menu.append(draw_context_menu)

def unregister():
    # Unregister context menu
    if draw_context_menu in bpy.types.UI_MT_button_context_menu._dyn_ui_initialize():
        bpy.types.UI_MT_button_context_menu.remove(draw_context_menu)
    
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)
    
    del bpy.types.Scene.people_list
    del bpy.types.Scene.people_list_index

if __name__ == "__main__":
    register()
    add_generic_items()