1
$\begingroup$

Does anybody know how to make accelerator keys to work in Blender 4.0 ? Can it be fixed by an add-on?

They 'replaced' accelerator keys with a search bar. I read UI dev discussion and I think their conclusion was to include a settings option for that, but I don't see anything in settings about that.

I used AK for everything. Search bar requires more typing, for me it's a big downgrade, possibly a deal breaker.

$\endgroup$
4
  • $\begingroup$ There's a Blender YT video here explaining the Blender 4.0 Keymap Updates. Some of the menus still have the accelerator keys, but I don't believe there's an option to bring them all back and I doubt it would be possible to write an addon as they've presumably been removed in the source code. $\endgroup$
    – John Eason
    Nov 15, 2023 at 11:18
  • $\begingroup$ Its relatively simple to write an addon that adds global shortcuts for adding certain modifiers. Other than that, it may also be possible to write an addon that just builds a static menu of all the conventional modifiers into a list, and lets Blender assign automatic keyboard shortcuts, that will hopefully match the existing ones $\endgroup$ Nov 15, 2023 at 11:54
  • $\begingroup$ Ok. Fair enough. Off you go then! :^)) $\endgroup$
    – John Eason
    Nov 15, 2023 at 11:57
  • $\begingroup$ @JohnEason It was actually a whole lot simpler than I though $\endgroup$ Nov 15, 2023 at 12:53

1 Answer 1

1
$\begingroup$

Accelerator keys have been removed from Blender 4.0 in favour of direct searching in menus by typing for a few reasons.

  • Scales better for large number of items
  • Allows for custom entries like the new custom modifier or nodes groups
  • Behaves better for dynamic content generation from libraries and assets

Well, it was actually simpler than I thought it'd be, for the single case of modifiers at least.

I was planing to summon a list of all the "static" modifiers from Blender RNA than build a traditional static menu from it, but as it seems, the legacy menu is still hidden away in Blender source code, albeit removed from visible panels. At least for the time being, until they clean house this should work.

enter image description here

We only need to add it back to the modifiers panel. This doesn't aim to remove the new search menu, as it still serves the purpose of adding Geometry Nodes based custom modifiers which the legacy doesn't cover.

For the 3D View Add menu all I could muster at the moment was a brute force solution of replicating the menus with searching disabled. It is a bit inelegant but should do the trick for now if nothing changes.

# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#n
# ##### END GPL LICENSE BLOCK #####

bl_info = {
    "name": "Classic Add Menus with Accelerator",
    "author": "Duarte Farrajota Ramos",
    "version": (1, 1),
    "blender": (4, 00, 0),
    "location": "Properties > Modifiers, 3D View > Add Menu",
    "description": "Brings back classic add modifier and add object menus with accelarator keys",
    "warning": "",
    "doc_url": "http://duarteramos.pt/addons.html",
    "category": "UI",
}

import bpy
from bpy.utils import register_class
from bpy.types import (
    Header,
    Menu,
    Panel,
    )

from bpy.app.translations import (
    pgettext_iface as iface_,
    pgettext_tip as tip_,
    contexts as i18n_contexts,
)

class VIEW3D_MT_mesh_add_classic(Menu):
    bl_idname = "VIEW3D_MT_mesh_add_classic"
    bl_label = "Mesh"
    ##bl_options = {'SEARCH_ON_KEY_PRESS'}

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

        layout.operator_context = 'INVOKE_REGION_WIN'

        layout.operator("mesh.primitive_plane_add", text="Plane", icon='MESH_PLANE')
        layout.operator("mesh.primitive_cube_add", text="Cube", icon='MESH_CUBE')
        layout.operator("mesh.primitive_circle_add", text="Circle", icon='MESH_CIRCLE')
        layout.operator("mesh.primitive_uv_sphere_add", text="UV Sphere", icon='MESH_UVSPHERE')
        layout.operator("mesh.primitive_ico_sphere_add", text="Ico Sphere", icon='MESH_ICOSPHERE')
        layout.operator("mesh.primitive_cylinder_add", text="Cylinder", icon='MESH_CYLINDER')
        layout.operator("mesh.primitive_cone_add", text="Cone", icon='MESH_CONE')
        layout.operator("mesh.primitive_torus_add", text="Torus", icon='MESH_TORUS')

        layout.separator()

        layout.operator("mesh.primitive_grid_add", text="Grid", icon='MESH_GRID')
        layout.operator("mesh.primitive_monkey_add", text="Monkey", icon='MESH_MONKEY')

        layout.template_node_operator_asset_menu_items(catalog_path="Add")


class VIEW3D_MT_curve_add_classic(Menu):
    bl_idname = "VIEW3D_MT_curve_add_classic"
    bl_label = "Curve"
    ##bl_options = {'SEARCH_ON_KEY_PRESS'}

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

        layout.operator_context = 'INVOKE_REGION_WIN'

        layout.operator("curve.primitive_bezier_curve_add", text="Bezier", icon='CURVE_BEZCURVE')
        layout.operator("curve.primitive_bezier_circle_add", text="Circle", icon='CURVE_BEZCIRCLE')

        layout.separator()

        layout.operator("curve.primitive_nurbs_curve_add", text="Nurbs Curve", icon='CURVE_NCURVE')
        layout.operator("curve.primitive_nurbs_circle_add", text="Nurbs Circle", icon='CURVE_NCIRCLE')
        layout.operator("curve.primitive_nurbs_path_add", text="Path", icon='CURVE_PATH')

        layout.separator()

        layout.operator("object.curves_empty_hair_add", text="Empty Hair", icon='CURVES_DATA')
        layout.operator("object.quick_fur", text="Fur", icon='CURVES_DATA')

        experimental = context.preferences.experimental
        if experimental.use_new_curves_tools:
            layout.operator("object.curves_random_add", text="Random", icon='CURVES_DATA')


class VIEW3D_MT_surface_add_classic(Menu):
    bl_idname = "VIEW3D_MT_surface_add_classic"
    bl_label = "Surface"
    ##bl_options = {'SEARCH_ON_KEY_PRESS'}

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

        layout.operator_context = 'INVOKE_REGION_WIN'

        layout.operator("surface.primitive_nurbs_surface_curve_add", text="Nurbs Curve", icon='SURFACE_NCURVE')
        layout.operator("surface.primitive_nurbs_surface_circle_add", text="Nurbs Circle", icon='SURFACE_NCIRCLE')
        layout.operator("surface.primitive_nurbs_surface_surface_add", text="Nurbs Surface", icon='SURFACE_NSURFACE')
        layout.operator("surface.primitive_nurbs_surface_cylinder_add",
                        text="Nurbs Cylinder", icon='SURFACE_NCYLINDER')
        layout.operator("surface.primitive_nurbs_surface_sphere_add", text="Nurbs Sphere", icon='SURFACE_NSPHERE')
        layout.operator("surface.primitive_nurbs_surface_torus_add", text="Nurbs Torus", icon='SURFACE_NTORUS')


class VIEW3D_MT_edit_metaball_context_menu_classic(Menu):
    bl_label = "Metaball"

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

        layout.operator_context = 'INVOKE_REGION_WIN'

        # Add
        layout.operator("mball.duplicate_move")

        layout.separator()

        # Modify
        layout.menu("VIEW3D_MT_mirror")
        layout.menu("VIEW3D_MT_snap")

        layout.separator()

        # Remove
        layout.operator_context = 'EXEC_REGION_WIN'
        layout.operator("mball.delete_metaelems", text="Delete")


class VIEW3D_MT_metaball_add_classic(Menu):
    bl_idname = "VIEW3D_MT_metaball_add_classic"
    bl_label = "Metaball"
    ##bl_options = {'SEARCH_ON_KEY_PRESS'}

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

        layout.operator_context = 'INVOKE_REGION_WIN'
        layout.operator_enum("object.metaball_add", "type")


class VIEW3D_MT_armature_add_classic(Menu):
    bl_idname = "VIEW3D_MT_armature_add_classic"
    bl_label = "Armature"
    #bl_options = {'SEARCH_ON_KEY_PRESS'}

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

        layout.operator_context = 'EXEC_REGION_WIN'
        layout.operator("object.armature_add", text="Single Bone", icon='BONE_DATA')


class VIEW3D_MT_light_add_classic(Menu):
    bl_idname = "VIEW3D_MT_light_add_classic"
    bl_context = i18n_contexts.id_light
    bl_label = "Light"
    #bl_options = {'SEARCH_ON_KEY_PRESS'}

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

        layout.operator_context = 'INVOKE_REGION_WIN'
        layout.operator_enum("object.light_add", "type")


class VIEW3D_MT_lightprobe_add_classic(Menu):
    bl_idname = "VIEW3D_MT_lightprobe_add_classic"
    bl_label = "Light Probe"
    #bl_options = {'SEARCH_ON_KEY_PRESS'}

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

        layout.operator_context = 'INVOKE_REGION_WIN'
        layout.operator_enum("object.lightprobe_add", "type")


class VIEW3D_MT_camera_add_classic(Menu):
    bl_idname = "VIEW3D_MT_camera_add_classic"
    bl_label = "Camera"
    #bl_options = {'SEARCH_ON_KEY_PRESS'}

    def draw(self, _context):
        layout = self.layout
        layout.operator_context = 'EXEC_REGION_WIN'
        layout.operator("object.camera_add", text="Camera", icon='OUTLINER_OB_CAMERA')


class VIEW3D_MT_volume_add_classic(Menu):
    bl_idname = "VIEW3D_MT_volume_add_classic"
    bl_label = "Volume"
    bl_translation_context = i18n_contexts.id_id
    #bl_options = {'SEARCH_ON_KEY_PRESS'}

    def draw(self, _context):
        layout = self.layout
        layout.operator("object.volume_import", text="Import OpenVDB...", icon='OUTLINER_DATA_VOLUME')
        layout.operator("object.volume_add", text="Empty",
                        text_ctxt=i18n_contexts.id_volume,
                        icon='OUTLINER_DATA_VOLUME')


class VIEW3D_MT_grease_pencil_add_classic(Menu):
    bl_idname = "VIEW3D_MT_grease_pencil_add_classic"
    bl_label = "Grease Pencil"
    #bl_options = {'SEARCH_ON_KEY_PRESS'}

    def draw(self, _context):
        layout = self.layout
        layout.operator("object.grease_pencil_add", text="Empty", icon='EMPTY_AXIS').type = 'EMPTY'
        layout.operator("object.grease_pencil_add", text="Stroke", icon='STROKE').type = 'STROKE'
        layout.operator("object.grease_pencil_add", text="Suzanne", icon='MONKEY').type = 'MONKEY'


class VIEW3D_MT_add_classic(Menu):
    bl_label = "Add"
    bl_translation_context = i18n_contexts.operator_default
    #bl_options = {'SEARCH_ON_KEY_PRESS'}

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

        if layout.operator_context == 'EXEC_REGION_WIN':
            layout.operator_context = 'INVOKE_REGION_WIN'
            layout.operator("WM_OT_search_single_menu", text="Search...", icon='VIEWZOOM').menu_idname = "VIEW3D_MT_add"
            layout.separator()

        # NOTE: don't use 'EXEC_SCREEN' or operators won't get the `v3d` context.

        # NOTE: was `EXEC_AREA`, but this context does not have the `rv3d`, which prevents
        #       "align_view" to work on first call (see #32719).
        layout.operator_context = 'EXEC_REGION_WIN'

        # layout.operator_menu_enum("object.mesh_add", "type", text="Mesh", icon='OUTLINER_OB_MESH')
        layout.menu("VIEW3D_MT_mesh_add_classic", icon='OUTLINER_OB_MESH')

        # layout.operator_menu_enum("object.curve_add", "type", text="Curve", icon='OUTLINER_OB_CURVE')
        layout.menu("VIEW3D_MT_curve_add_classic", icon='OUTLINER_OB_CURVE')
        # layout.operator_menu_enum("object.surface_add", "type", text="Surface", icon='OUTLINER_OB_SURFACE')
        layout.menu("VIEW3D_MT_surface_add_classic", icon='OUTLINER_OB_SURFACE')
        layout.menu("VIEW3D_MT_metaball_add_classic", text="Metaball", icon='OUTLINER_OB_META')
        layout.operator("object.text_add", text="Text", icon='OUTLINER_OB_FONT')
        if context.preferences.experimental.use_new_point_cloud_type:
            layout.operator("object.pointcloud_add", text="Point Cloud", icon='OUTLINER_OB_POINTCLOUD')
        layout.menu("VIEW3D_MT_volume_add_classic", text="Volume", text_ctxt=i18n_contexts.id_id, icon='OUTLINER_OB_VOLUME')
        if context.preferences.experimental.use_grease_pencil_version3:
            layout.menu("VIEW3D_MT_grease_pencil_add_classic", text="Grease Pencil", icon='OUTLINER_OB_GREASEPENCIL')
        else:
            layout.operator_menu_enum(
                "object.gpencil_add",
                "type",
                text="Grease Pencil",
                icon='OUTLINER_OB_GREASEPENCIL')

        layout.separator()

        if VIEW3D_MT_armature_add_classic.is_extended():
            layout.menu("VIEW3D_MT_armature_add_classic", icon='OUTLINER_OB_ARMATURE')
        else:
            layout.operator("object.armature_add", text="Armature", icon='OUTLINER_OB_ARMATURE')

        layout.operator("object.add", text="Lattice", icon='OUTLINER_OB_LATTICE').type = 'LATTICE'

        layout.separator()

        layout.operator_menu_enum("object.empty_add", "type", text="Empty",
                                  text_ctxt=i18n_contexts.id_id,
                                  icon='OUTLINER_OB_EMPTY')
        layout.menu("VIEW3D_MT_image_add_classic", text="Image", icon='OUTLINER_OB_IMAGE')

        layout.separator()

        layout.menu("VIEW3D_MT_light_add_classic", icon='OUTLINER_OB_LIGHT')
        layout.menu("VIEW3D_MT_lightprobe_add_classic", icon='OUTLINER_OB_LIGHTPROBE')

        layout.separator()

        if VIEW3D_MT_camera_add_classic.is_extended():
            layout.menu("VIEW3D_MT_camera_add_classic", icon='OUTLINER_OB_CAMERA')
        else:
            VIEW3D_MT_camera_add_classic.draw(self, context)

        layout.separator()

        layout.operator("object.speaker_add", text="Speaker", icon='OUTLINER_OB_SPEAKER')

        layout.separator()

        layout.operator_menu_enum("object.effector_add", "type", text="Force Field", icon='OUTLINER_OB_FORCE_FIELD')

        layout.separator()

        has_collections = bool(bpy.data.collections)
        col = layout.column()
        col.enabled = has_collections

        if not has_collections or len(bpy.data.collections) > 10:
            col.operator_context = 'INVOKE_REGION_WIN'
            col.operator(
                "object.collection_instance_add",
                text="Collection Instance..." if has_collections else "No Collections to Instance",
                icon='OUTLINER_OB_GROUP_INSTANCE',
            )
        else:
            col.operator_menu_enum(
                "object.collection_instance_add",
                "collection",
                text="Collection Instance",
                icon='OUTLINER_OB_GROUP_INSTANCE',
            )


class VIEW3D_MT_image_add_classic(Menu):
    bl_label = "Add Image"
    #bl_options = {'SEARCH_ON_KEY_PRESS'}

    def draw(self, _context):
        layout = self.layout
        layout.operator("object.load_reference_image", text="Reference", icon='IMAGE_REFERENCE')
        layout.operator("object.load_background_image", text="Background", icon='IMAGE_BACKGROUND')



#############################################################################################################################
## Registration
classes = (
            VIEW3D_MT_mesh_add_classic,
            VIEW3D_MT_curve_add_classic,
            VIEW3D_MT_surface_add_classic,
            VIEW3D_MT_metaball_add_classic,
            VIEW3D_MT_armature_add_classic,
            VIEW3D_MT_light_add_classic,
            VIEW3D_MT_lightprobe_add_classic,
            VIEW3D_MT_camera_add_classic,
            VIEW3D_MT_volume_add_classic,
            VIEW3D_MT_add_classic,
        )

# Draw modifiers menu
def drawmodifiers(self, context):
    layout = self.layout
    layout.operator_menu_enum("object.modifier_add", "type")

#############################################################################################################################
# Registration

# Store keymaps here to access after registration
addon_keymaps = []

def register():
    for cls in classes:
        register_class(cls)
    # Append button to the properties Window > Modifier Tab
    bpy.types.DATA_PT_modifiers.prepend(drawmodifiers)
    bpy.types

#Keymaps###########################################################
    
    wm = bpy.context.window_manager
    kc = wm.keyconfigs.addon
    
    if not kc:
        print('No keyconfig path found. Adding new')
        return

    #3D View Global
    km = kc.keymaps.new(name = "3D View", space_type = "VIEW_3D")
    kmi = km.keymap_items.new("wm.call_menu", 'A', value = "PRESS", shift = True, ctrl = True)
    kmi.properties.name = "VIEW3D_MT_add_classic"

def unregister():
    for cls in reversed(classes):
        unregister_class(cls)
    bpy.types.DATA_PT_modifiers.remove(drawmodifiers)

if __name__ == "__main__":
    register()

Just copy the code into a plain text file, save it in your computer with the extension `.py. Install it as a regular addon from Edit > Preferences > Addons.

Activate it with the check box after installation, and save preferences if you disabled auto-saving.

It registers by default ⇧ Shift + Ctrl + A for the "classic" Add menu with key accelerators.

You can replace the default ⇧ Shift + A entry or add any other key with the classic menu from Edit > Preferences > Keymap. Then add a new key entry.

Set the operator to wm.call_menu, and the and the name to VIEW3D_MT_add_classic.


In the meantime I found out how it can be done programmatically, though I don't currently have the time actually implement it all.

You could theoretically make it work by inspecting all classes of menus in all files found at Blender\4.0\scripts\startup\bl_ui, the ones of interest will be like space_view3d.py for the 3D view, space_node.py for the node editor among others.

In them you will have to look for the Add menu definitions which will be named something along the convention of class ****_MT_add(bpy.types.Menu): where **** is the name of the space like NODEor 3DVIEW, and a suffix like mesh, curve. Example class VIEW3D_MT_mesh_add(Menu): is the add mesh menu in the 3D View.

If you notice each has a property bl_options = {'SEARCH_ON_KEY_PRESS'} which makes it searchable. What we want in this case would be to remove it from all menus.

To go about it one would have to gather a list of all the relevant menu classes, which should be relatively easy to find all listed at the bottom of each file under class registration, and remove this bl_options from all of them. Setting it to Nonewon't suffice.

The easiest method would then be to iterate through all menu classes in a for loop, unregister them, and re-register them all again but without bl_options. This is necessary because without re-registration Blender won't pick up the changes.

It is a moderate amount of work, but seems more maintainable than the previous solution. Another option would be to register new menu classes instead, but that would require also making the new classes call the new sub menus, which would be a lot more involved.

I don't have the time or inclination to implement it at the moment, only tested it from the console and it seems to work, so it should in theory work from an addon as well, though I didn't actually test it.

$\endgroup$
14
  • $\begingroup$ Well done! Now what about the main Add Objects menu! :^) $\endgroup$
    – John Eason
    Nov 15, 2023 at 14:06
  • $\begingroup$ That one is probably not as trivial, it was actually changed from the menu design side. Would probably have to be rebuilt from the ground $\endgroup$ Nov 15, 2023 at 14:52
  • $\begingroup$ Yes indeed. That's the one which would give me slight issues because I use Shift-A > M > C to add a cube for instance. It only means remembering to do Shift-A >cu instead in 4.0 though which is the same number of keypresses, just different ones... $\endgroup$
    – John Eason
    Nov 15, 2023 at 15:51
  • $\begingroup$ @JohnEason A bit of a "brute force" solution, can't spare the time for something more elegant at the moment, but seems to do the trick from my limited testing $\endgroup$ Nov 15, 2023 at 20:31
  • 1
    $\begingroup$ OK, I get it. It's a shame the devs didn't make it an option, but it is what it is. Thank you very much for your quick responses and your amazing code. I have marked your answer as a solution. Fantastic job Duarte! $\endgroup$
    – MikeTS
    Nov 16, 2023 at 10:12

You must log in to answer this question.

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