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 '17 at 11:09
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$
4
$\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 '17 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 '17 at 18:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.