You can declare as many menus as you like and concatenate them by calling each menu within the previous menu by layout.menu(menu_identifier)
.
You can even automatically draw a button to call your menu without declaring any extra operator by layout.operator("wm.call_menu").name="menu_identifier"
.
The layout code is ripped from How to create a custom UI? and combined with Templates > Python > UI Menu template (Text Editor).
bl_info = {
"name": "Add-on Template",
"description": "",
"author": "",
"version": (0, 0, 1),
"blender": (2, 80, 0),
"location": "3D View > Tools",
"category": "Development"
}
import bpy
# ------------------------------------------------------------------------
# Menus
# ------------------------------------------------------------------------
class CUSTOM_MT_Menu(bpy.types.Menu):
bl_label = "First Menu"
bl_idname = "CUSTOM_MT_Menu"
def draw(self, context):
layout = self.layout
layout.label(text="Hello First Menu!", icon='WORLD_DATA')
# call the second custom menu using bl_idname attribute
layout.menu(CUSTOM_MT_SubMenu.bl_idname, icon="COLLAPSEMENU")
# OR use the name of the class
#layout.menu(CUSTOM_MT_SubMenu.__name__, icon="COLLAPSEMENU")
# OR just pass the class name as string
#layout.menu("CUSTOM_MT_SubMenu", icon="COLLAPSEMENU")
# use an operator enum property to populate a sub-menu
layout.operator_menu_enum("object.select_by_type",
property="type",
text="Select All Objects by Type...",
)
class CUSTOM_MT_SubMenu(bpy.types.Menu):
bl_label = "Sub Menu"
bl_idname = "CUSTOM_MT_SubMenu" # Optional
def draw(self, context):
layout = self.layout
layout.label(text="Hello Second Menu!", icon='WORLD_DATA')
# call another menu
layout.operator("wm.call_menu", text="Unwrap").name = "VIEW3D_MT_uv_map"
# just for fun call the first one again
layout.menu(CUSTOM_MT_SubSubMenu.__name__, icon="COLLAPSEMENU")
class CUSTOM_MT_SubSubMenu(bpy.types.Menu):
bl_label = "Sub Sub Menu"
bl_idname = "CUSTOM_MT_SubSubMenu" # Optional
def draw(self, context):
layout = self.layout
layout.label(text="Hello Third Menu!", icon='WORLD_DATA')
# ------------------------------------------------------------------------
# Panel (Object Mode)
# ------------------------------------------------------------------------
class OBJECT_PT_CustomPanel(bpy.types.Panel):
bl_label = "My Panel"
bl_idname = "OBJECT_PT_custom_panel"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Tools"
bl_context = "objectmode"
'''
@classmethod
def poll(self,context):
return context.object is not None
'''
def draw(self, context):
layout = self.layout
scene = context.scene
mytool = scene.my_tool
layout.prop(mytool, "my_int")
layout.prop(mytool, "my_enum", text="")
layout.operator("wm.call_menu", text="Call My Menu").name = CUSTOM_MT_Menu.__name__
# Like above, you can also just pass the class name as string
#layout.operator("wm.call_menu", text="Call My Menu").name = "CUSTOM_MT_Menu"
# ------------------------------------------------------------------------
# Settings
# ------------------------------------------------------------------------
class CUSTOM_PG_Settings(bpy.types.PropertyGroup):
my_int: bpy.props.IntProperty(
name = "Int Value",
description="A integer property",
default = 23,
min = 10,
max = 100
)
my_enum: bpy.props.EnumProperty(
name="Dropdown:",
description="Apply Data to attribute.",
items=[ ('OP1', "Option 1", ""),
('OP2', "Option 2", ""),
('OP3', "Option 3", ""),
]
)
# ------------------------------------------------------------------------
# Registration
# ------------------------------------------------------------------------
classes = (
CUSTOM_MT_Menu,
CUSTOM_MT_SubMenu,
CUSTOM_MT_SubSubMenu,
OBJECT_PT_CustomPanel,
CUSTOM_PG_Settings
)
def register():
from bpy.utils import register_class
for cls in classes:
register_class(cls)
bpy.types.Scene.my_tool = bpy.props.PointerProperty(type=CUSTOM_PG_Settings)
def unregister():
from bpy.utils import unregister_class
for cls in reversed(classes):
unregister_class(cls)
del bpy.types.Scene.my_tool
if __name__ == "__main__":
register()
# The menu can also be called from scripts
#bpy.ops.wm.call_menu(name=CUSTOM_MT_Menu.bl_idname)
Blender 2.7x (from the original answer)
bl_info = {
"name": "Add-on Template",
"description": "",
"author": "",
"version": (0, 0, 1),
"blender": (2, 70, 0),
"location": "3D View > Tools",
"warning": "", # used for warning icon and text in addons panel
"wiki_url": "",
"tracker_url": "",
"category": "Development"
}
import bpy
# ------------------------------------------------------------------------
# Menus
# ------------------------------------------------------------------------
class MyCustomMenu(bpy.types.Menu):
bl_label = "First Menu"
bl_idname = "OBJECT_MT_custom_menu"
def draw(self, context):
layout = self.layout
layout.label(text="Hello First Menu!", icon='WORLD_DATA')
# call the second custom menu
layout.menu("OBJECT_MT_sub_menu", icon="COLLAPSEMENU")
# use an operator enum property to populate a sub-menu
layout.operator_menu_enum("object.select_by_type",
property="type",
text="Select All Objects by Type...",
)
class MyCustomSubMenu(bpy.types.Menu):
bl_label = "Sub Menu"
bl_idname = "OBJECT_MT_sub_menu"
def draw(self, context):
layout = self.layout
layout.label(text="Hello Second Menu!", icon='WORLD_DATA')
# call another menu
layout.operator("wm.call_menu", text="Unwrap").name = "VIEW3D_MT_uv_map"
# just for fun call the first one again
layout.menu("OBJECT_MT_sub_sub_menu", icon="COLLAPSEMENU")
class MyCustomSubSubMenu(bpy.types.Menu):
bl_label = "Sub Sub Menu"
bl_idname = "OBJECT_MT_sub_sub_menu"
def draw(self, context):
layout = self.layout
layout.label(text="Hello Third Menu!", icon='WORLD_DATA')
# ------------------------------------------------------------------------
# Panel
# ------------------------------------------------------------------------
class OBJECT_PT_my_panel(bpy.types.Panel):
bl_idname = "OBJECT_PT_my_panel"
bl_label = "My Panel"
bl_space_type = "VIEW_3D"
bl_region_type = "TOOLS"
bl_category = "Tools"
bl_context = "objectmode"
'''
@classmethod
def poll(self,context):
return context.object is not None
'''
def draw(self, context):
layout = self.layout
scene = context.scene
mytool = scene.my_tool
layout.prop(mytool, "my_int")
layout.prop(mytool, "my_enum", text="")
layout.operator("wm.call_menu", text="Call My Menu").name = "OBJECT_MT_custom_menu"
# ------------------------------------------------------------------------
# Settings
# ------------------------------------------------------------------------
class MySettings(bpy.types.PropertyGroup):
my_int = bpy.props.IntProperty(
name = "Int Value",
description="A integer property",
default = 23,
min = 10,
max = 100
)
my_enum = bpy.props.EnumProperty(
name="Dropdown:",
description="Apply Data to attribute.",
items=[ ('OP1', "Option 1", ""),
('OP2', "Option 2", ""),
('OP3', "Option 3", ""),
]
)
# ------------------------------------------------------------------------
# Registration
# ------------------------------------------------------------------------
def register():
bpy.utils.register_module(__name__)
bpy.types.Scene.my_tool = bpy.props.PointerProperty(type=MySettings)
def unregister():
bpy.utils.unregister_module(__name__)
del bpy.types.Scene.my_tool
if __name__ == "__main__":
register()
# The menu can also be called from scripts
# bpy.ops.wm.call_menu(name=CustomMenu.bl_idname)
operator_menu_enum
to add a submenu. $\endgroup$