2

enter image description here

Hi, I´m aiming to create panels with menus and submenus in the TOOLS area type just like in the picture to the LEFT. Blender pops up a shortcut menu (with shift+a) and I see it´s got arrows pointing to a submenu populated with options.

I saw the py demos on the text editor, but none of those make a main panel nesting sub panels. I want to be able to contract/expand the panel and thus reveal or hide the subPanels. The 2nd level of menu branching will

Marty Fouts
  • 33,070
  • 10
  • 35
  • 79
Pierre Schiller
  • 2,954
  • 1
  • 11
  • 34

2 Answers2

5

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".

enter image description here

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)

enter image description here

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)

brockmann
  • 12,613
  • 4
  • 50
  • 93
  • Great code. It works wonderfully. Is there a way to offset the first menu by 100+ pixels in X from where the mouse clicks? or is there any chance to open the menu pop up in a specific coordinate relative to % of the screen space? – Pierre Schiller Sep 21 '17 at 17:21
  • Not that I know of. Even if that's possible, it doesn't makes sense from the user point of view IMHO. However, that's might worth a new question @PierreSchiller – brockmann Sep 21 '17 at 18:44
1

To use the script from this answer in version 2.8 or later, a few small changes are necessary, mostly to how registration is done. Here's an updated version, tested with 2.93.5:

bl_info = {
    "name": "Add-on Template",
    "description": "",
    "author": "",
    "version": (0, 0, 1),
    "blender": (2, 83, 0),
    "location": "3D View",
    "warning": "", # used for warning icon and text in addons panel
    "wiki_url": "",
    "tracker_url": "",
    "category": "Development"
}

import bpy

------------------------------------------------------------------------

custom menus

------------------------------------------------------------------------

class OBJECT_MT_CustomMenu(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 OBJECT_MT_CustomSubMenu(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 OBJECT_MT_CustomSubSubMenu(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')    


------------------------------------------------------------------------

my tool in objectmode

------------------------------------------------------------------------

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 = "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.label(text='properties')
    layout.prop(mytool, "my_int")
    layout.prop(mytool, "my_enum", text="")
    layout.label(text='big red button')
    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", ""),
           ]
    )

------------------------------------------------------------------------

register and unregister the classes

------------------------------------------------------------------------

classes = [ OBJECT_MT_CustomMenu, OBJECT_MT_CustomSubMenu, OBJECT_MT_CustomSubSubMenu, OBJECT_PT_my_panel, MySettings, ]

def register(): for c in classes: bpy.utils.register_class(c) bpy.types.Scene.my_tool = bpy.props.PointerProperty(type=MySettings)

def unregister(): for c in classes: bpy.utils.unregister_class(c) bpy.utils.unregister_class(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)

Marty Fouts
  • 33,070
  • 10
  • 35
  • 79