Permanently expanded submenu in pie menu

Hello guys!

Is it possible to add submenu (from another custom class) in pie menu is a such way as to make it always expanded (without howering over it). Or maybe it possible with popup menu as submenu? (For example, there is “Expand Popup Dialog” checkbox in Pie Menu Editor addon).

And second question is how to insert panels in pie menu? Again, it is possible with Pie Menu Editor but I want to know how do it by myself, without addons.
2020-09-18 18_20_09-Blender

import bpy
from bpy.types import Menu, Operator

class CUSTOM_MT_menu(Menu):
    bl_label = "Main Menu"
    bl_idname = "CUSTOM_MT_menu"

    def draw(self, context):
        layout = self.layout
        pie = layout.menu_pie()

        pie.menu("CUSTOM_MT_submenu", text='SubMenu')
        
        pie.operator("object.custom_operator", text='Operator')
        
        col = pie.column()
        col.operator("mesh.primitive_cube_add", text = "Cube", icon = "MESH_CUBE")
        col.operator("mesh.primitive_cube_add", text = "Cube", icon = "MESH_CUBE")
        col.operator("mesh.primitive_cube_add", text = "Cube", icon = "MESH_CUBE")
        

class CUSTOM_MT_submenu(Menu):
    bl_label = "Sub Menu"
    bl_idname = "CUSTOM_MT_submenu"

    def draw(self, context):
        layout = self.layout        
        
        layout.operator("mesh.primitive_cube_add", text = "Cube", icon = "MESH_CUBE")
        layout.operator("mesh.primitive_cube_add", text = "Cube", icon = "MESH_CUBE")
        layout.operator("mesh.primitive_cube_add", text = "Cube", icon = "MESH_CUBE")
        
        
class CUSTOM_OT_operator(Operator):
    bl_idname = "object.custom_operator"
    bl_label = "Operator"
    
    def execute(self, context):
        return {'FINISHED'}

    def invoke(self, context, event):
        wm = context.window_manager
        return wm.invoke_props_dialog(self, width=70)

    def draw(self, context):
        layout = self.layout        

        layout.operator("mesh.primitive_cube_add", text = "Cube", icon = "MESH_CUBE")
        layout.operator("mesh.primitive_cube_add", text = "Cube", icon = "MESH_CUBE")
        layout.operator("mesh.primitive_cube_add", text = "Cube", icon = "MESH_CUBE")          


classes = (    
    CUSTOM_MT_menu,
    CUSTOM_MT_submenu,
    CUSTOM_OT_operator
    )

register, unregister = bpy.utils.register_classes_factory(classes)

if __name__ == "__main__":
    register()

    bpy.ops.wm.call_menu_pie(name="CUSTOM_MT_menu")

If you just want the menu items expanded, just like your manual column of operators:

col1 = pie.column()
col1.menu_contents("CUSTOM_MT_submenu")

Panels, like menus are just draw functions. You can funnel them through any layout.

You can use something like this to pass as override layout:

class Layout:
    def __init__(self, layout):
        self.layout = layout
col2 = pie.column()
override = Layout(col2)
bpy.types.OBJECT_PT_display.draw(override, context)
1 Like

So simple! Thank you! I wish blender docs would be provided with more examples instead of this :laughing:

menu_contents
Parameters
**menu**  ( *string* *,*  *(* *never None* *)* ) – Identifier of the menu

If you don’t mind, can you please explain second part in more detail and where I can read about ? I tried to insert
DATA_PT_display and for some reason it doesn’t work (with OBJECT_PT_display eveything is ok). And what if I want to insert another panels? I have to use something like this, right?

another_column = pie.column()
override = Layout(another_column)
bpy.types.ANOTHER_CLASS.draw(override, context)

There aren’t any docs that I know of that really explain this. I’ve just been at it for too long.

When a draw method is called with the two arguments self and context, generally self can be anything as long as it provides an attribute called layout.

With the override, I simply made a generic instance with one attribute, layout, set to the argument it was created with:

col2 = pie.column()
override = Layout(col2)

Maybe you’re wondering why we can pass something like a column as a layout?

When you write something like:

row = layout.row()

row is now an instance derived from layout. A branch of the original, with an inherent horizontal drawing direction, but still a bpy.types.UILayout. Same with layout.column(), layout.box(), layout.box().box().column().split(). The return from chaining those is still a layout.

So, as you pass the override to the panel’s draw function, when the draw function does

layout = self.layout

self now points to the override. And self.layout becomes the column col2 we passed as override.

When something doesn’t work, generally it’s a good idea to look in the system console. The python error messages are extremely helpful. It’s likely the panel has more requirements, or that the context passed doesn’t provide enough data.

Yes. There’s surely other ways to do it, but the gist is to call the panel’s draw with your own layout.

2 Likes

No, i didn’t get this thing. :slightly_smiling_face:

class Layout:
    def __init__(self, layout):
        self.layout = layout

As I understand it’s internal class. Everything else is pretty clear. Thanks again!

File "X:\Blender\blender-2.83.6-windows64\2.83\scripts\addons\_test_pie_panels.py", line 37, in draw
    bpy.types.DATA_PT_display.draw(override, context)
  File "X:\Blender\blender-2.83.6-windows64\2.83\scripts\startup\bl_ui\properties_data_armature.py", line 83, in draw
    arm = context.armature
AttributeError: 'Context' object has no attribute 'armature'

line 37 is

bpy.types.DATA_PT_display.draw(override, context)

A class not inheriting from other classes is just a generic class. The reason we define a Layout class is because it needs to hold an attribute other functions can programmatically access. We’re literally just smacking a .layout onto a thingy and tell the draw function, “hey, take this object as your self. It’s got a layout you can access”.

I’m assuming you’re running the script from the text editor. Context is dynamic. The available members depend on the area you are currently in. When you call the panel in the viewport, things like context.armature will be available.

1 Like

It was my first thought, but no. I even created and installed test addon but error the same and only with armature panels.

import bpy
from bpy.types import Menu

bl_info = {
    "name": "Test",
    "description": "Viewport Bone Views",
    "author": "",
    "version": (0, 1, 0),
    "blender": (2, 80, 0),
    "location": "3D View",
    "warning": "",
    "doc_url": "",
    "category": "Pie Menu"
}


class CUSTOM_MT_menu(Menu):
    bl_label = "Main Menu"
    bl_idname = "CUSTOM_MT_menu"

    def draw(self, context):
        layout = self.layout
        pie = layout.menu_pie()

        col1 = pie.column()
        col1.menu_contents("CUSTOM_MT_submenu")

        col2 = pie.column()
        override = Layout(col2)
        bpy.types.OBJECT_PT_display.draw(override, context)

        col4 = pie.column()
        override = Layout(col4)
        bpy.types.SCENE_PT_scene.draw(override, context)

        col3 = pie.column()
        override = Layout(col3)
        bpy.types.DATA_PT_display.draw(override, context)


class CUSTOM_MT_submenu(Menu):
    bl_label = "Sub Menu"
    bl_idname = "CUSTOM_MT_submenu"

    def draw(self, context):
        layout = self.layout

        layout.operator("mesh.primitive_cube_add",
                        text="Cube", icon="MESH_CUBE")
        layout.operator("mesh.primitive_cube_add",
                        text="Cube", icon="MESH_CUBE")
        layout.operator("mesh.primitive_cube_add",
                        text="Cube", icon="MESH_CUBE")


class Layout:
    def __init__(self, layout):
        self.layout = layout


classes = (
    CUSTOM_MT_menu,
    CUSTOM_MT_submenu,
)

addon_keymaps = []


def register():
    for cls in classes:
        bpy.utils.register_class(cls)

    wm = bpy.context.window_manager
    kc = wm.keyconfigs.addon
    if kc:
        km = kc.keymaps.new(name='3D View', space_type='VIEW_3D')
        kmi = km.keymap_items.new('wm.call_menu_pie', 'F2', 'PRESS')
        kmi.properties.name = "CUSTOM_MT_menu"
        addon_keymaps.append((km, kmi))


def unregister():
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)

    wm = bpy.context.window_manager
    kc = wm.keyconfigs.addon
    if kc:
        for km, kmi in addon_keymaps:
            km.keymap_items.remove(kmi)
    addon_keymaps.clear()

Right. The issue wasn’t the viewport, but the properties editor. context.armature doesn’t exist outside of properties, so you must generate a new context and manually add armature to it.

Good news is, the procedure is same as with the layout override. Adding a check to emulate a poll is a good idea since drawing the panel yourself circumvents the original one. The bad news is, well, there are no bad news.


class Context(dict):
    def __init__(self, context=None, **kwargs):
        super().__init__()
        self.__dict__ = self

        if context is not None:
            self.update(context.copy())
        self.update(**kwargs)

Then draw the armature panel like this:


        if context.object.type == 'ARMATURE':
            context_ = Context(context, armature=context.object.data)
            col3 = pie.column()
            override = Layout(col3)
            bpy.types.DATA_PT_display.draw(override, context_)

1 Like

Awesome! Thank you so much!

And last(I hope) question is how to add expanded pop up menu(like in Pie Menu Editor, without OK button) into pie menu?

I tried

col4 = pie.column()
        override = Layout(col4)
        bpy.types.CUSTOM_OT_operator(override, context)

but

  File "X:\Blender\blender-2.83.6-windows64\2.83\scripts\addons\_test_pie_panels.py", line 43, in draw
    bpy.types.CUSTOM_OT_operator(override, context)
AttributeError: 'RNA_Types' object has no attribute 'CUSTOM_OT_operator'

location: <unknown location>:-1


Could you show a screenshot of what it should look like in PME?

It’s a bit unclear, since you’re calling it a popup menu, which generally does not come with an OK button.

You’re right. It’s initially operator but in PME it’s called ‘Popup Dialog’.

Popup Dialog

Pie Menu with same Popup Dialog (expand Popup Dialog checkbox switched on in Pie Menu options)

Pie Menu with same Popup Dialog (expand Popup Dialog checkbox switched off)

Ah, you’re trying to draw the operator’s layout?

Custom operators no longer go into bpy.types. Instead you reference the class itself:

col4 = pie.column()
        override = Layout(col4)
        CUSTOM_OT_operator(override, context)
1 Like

Doesn’t work :frowning_face:

  File "X:\Blender\blender-2.83.6-windows64\2.83\scripts\addons\_test_pie_panels.py", line 44, in draw
    CUSTOM_OT_operator(override, context)
TypeError: bpy_struct.__new__(type): expected a single argument

With single argument

col4 = pie.column()
override = Layout(col4)
CUSTOM_OT_operator(override)
  File "X:\Blender\blender-2.83.6-windows64\2.83\scripts\addons\_test_pie_panels.py", line 44, in draw
    CUSTOM_OT_operator(override)
TypeError: bpy_struct.__new__(type): type 'CUSTOM_OT_operator' is not a subtype of bpy_struct

Oops.

col4 = pie.column()
        override = Layout(col4)
        CUSTOM_OT_operator.draw(override, context)
1 Like

Much obliged!

One more question :slightly_smiling_face:
Whenever I call Set Roll, Extrude, Duplicate etc. from Armature menu (internal class VIEW3D_MT_edit_armature), everything works.
But if I call from my custom menu

layout.operator("transform.transform", text="Set Roll").mode = 'BONE_ROLL'
or
layout.operator("armature.extrude_move")
or
layout.operator("armature.duplicate_move")

nothing happens (in case of Extrude or Duplicate pressing G key helps a little) At the same time
layout.operator("armature.subdivide", text="Subdivide"),
layout.operator("armature.switch_direction")

and some other works fine. Why is that?

Not easy to tell without seeing the code. Did you at one point set operator_context to EXEC_DEFAULT? Interactive operators require INVOKE_DEFAULT.

1 Like

Yes, exactly.

with INVOKE_DEFAULT it works…
But is it possible to use EXEC_DEFAULT and INVOKE_DEFAULT in the same menu?

class PIE_MT_custom_parent(Menu):
    bl_label = "Custom Parenting Pie"

    def draw(self, context):
        layout = self.layout

        # layout.operator_context = 'EXEC_DEFAULT'
        layout.operator_context = 'INVOKE_DEFAULT'

        pie = layout.menu_pie()
        pie.label(text='')
        if context.mode == 'EDIT_ARMATURE':

            # LEFT
            pie.operator(
                "armature.parent_clear", text="Disconnect", icon='RADIOBUT_OFF').type = 'DISCONNECT'

            # RIGHT
            pie.operator(
                "armature.parent_set", text="Offset", icon='RADIOBUT_ON').type = "OFFSET"

            # BOTTOM
            pie.operator(
                "armature.subdivide", text="Subdivide", icon='POINTCLOUD_DATA')

            # TOP
            column = pie.column()
            column.menu('VIEW3D_MT_bone_options_toggle', text='Bone Opts')
            # column.menu('VIEW3D_MT_edit_armature_roll', text='Bone Roll')
            column.operator("armature.extrude_move")
            column.operator("transform.transform",
                            text="Set Roll").mode = 'BONE_ROLL'
            column.operator("armature.duplicate_move")

            # TOP LEFT
            pie.operator(
                "armature.parent_clear", text="Clear Parrent", icon='RADIOBUT_OFF').type = 'CLEAR'

            # TOP RIGHT
            pie.operator(
                "armature.parent_set", text="Prnt Connected", icon='RADIOBUT_ON').type = 'CONNECTED'

            # BOTTOM LEFT
            pie.operator(
                "armature.switch_direction", text="Flip", icon='DECORATE_OVERRIDE')

            # BOTTOM RIGHT
            pie.operator(
                "armature.align", text="Align", icon='CENTER_ONLY')

Yes, you can!

Setting operator_context sets the current state for subsequently defined operators. The already defined operators keep the previous state when they were instanced.

1 Like

Awesome! Thank you!

Another question :blush:
How to insert custom enum operator into pie menu ? Just class referencing doesn’t work

Traceback (most recent call last):
  File "\ui_pie_enum_operator.py", line 16, in draw
  File "\ui_pie_enum_operator.py", line 40, in draw
TypeError: UILayout.prop(): error with argument 1, "data" -  Function.data expected a AnyType type, not Layout
import bpy
from bpy.types import Menu, Operator

class VIEW3D_MT_PIE_template(Menu):

    bl_label = "Select Mode"

    def draw(self, context):
        layout = self.layout
        

        pie = layout.menu_pie()
#        pie.operator('wm.template_operator')
        col = pie.column()
        override = Layout(col)
        TMP_OT_tmp_operator.draw(override, context)

        

class TMP_OT_tmp_operator(Operator):
    bl_label = 'tmp'
    bl_idname = 'wm.template_operator'
    
    preset_enum: bpy.props.EnumProperty(
        name= '',
        description= 'Select an option',
        items= [
            ('op1', 'Cube',''),
            ('op2', 'Sphere','')
        ]
    )
    
    def invoke(self, context, event):
        wm = context.window_manager
        return wm.invoke_props_dialog(self)
    
    
    def draw(self, context):
        layout = self.layout
        layout.prop(self, 'preset_enum')
        
    
    def execute(self, context):
        
        if self.preset_enum == 'op1':
            bpy.ops.mesh.primitive_cube_add()
        if self.preset_enum == 'op2':
            bpy.ops.mesh.primitive_uv_sphere_add()

        
        return {'FINISHED'}  

class Layout:
    def __init__(self, layout):
        self.layout = layout
    

def register():
    bpy.utils.register_class(VIEW3D_MT_PIE_template)
    bpy.utils.register_class(TMP_OT_tmp_operator)


def unregister():
    bpy.utils.unregister_class(VIEW3D_MT_PIE_template)
    bpy.utils.unregister_class(TMP_OT_tmp_operator)


if __name__ == "__main__":
    register()

    bpy.ops.wm.call_menu_pie(name="VIEW3D_MT_PIE_template")

Am i to understand correctly that enum operators are displayed as dropdown menus only in panels, but in pie menus only as popup menus?