2
$\begingroup$

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

$\endgroup$
1
  • 1
    $\begingroup$ This answer shows expanding panels. For menus this answer shows using operator_menu_enum to add a submenu. $\endgroup$
    – sambler
    Sep 20, 2017 at 11:09

2 Answers 2

1
$\begingroup$

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)
$\endgroup$
5
$\begingroup$

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)
$\endgroup$
2
  • $\begingroup$ 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? $\endgroup$ Sep 21, 2017 at 17:21
  • $\begingroup$ 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 $\endgroup$
    – brockmann
    Sep 21, 2017 at 18:44

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .