Changing the menu bar order of Emacs easy-menu menus

October 20, 2023

These days, Emacs has a menu bar, theoretically in both graphical and text modes (I turn it off in terminals). One of the things that you (I) can want to do as part of customizing things like MH-E is to add additional menu-bar menus with your own convenient entries. The easiest and most obvious way to define a new menu-bar menu in Emacs Lisp is with Easy Menu's easy-menu-define. Easy-menu-define is both easy to use and powerful, offering support for things like dynamic filtering and dynamic enabling of entire menus. However, for my purposes it has one limitation (I shouldn't call it a flaw), namely that easy-menu-define adds new menus to the front of the menu bar (either a mode specific menu bar or worse the global part of the menu bar). The cheerful advice is to define your easy menus in reverse order, but you can't really do this if you're extending an existing mode.

There are two ways around this; the bad way of Emacs crimes and the proper way, which has worked for me so far. Both ways start with the fact that menus are actually Emacs keymaps, especially including menus in the menu bar, which is itself tied up in keymaps; you add a menu to the menu bar by adding it to either the global keymap or the current major mode keymap under a special format of key names. The reason that easy-menu-define adds your menu to the front of the menu bar is that it winds up using define-key, and define-key adds the new key binding to the front of the keymap. If we want our new menu to be anywhere else in the menu bar, we need to get the easy-menu system to use define-key-after instead (either with or without an explicit thing to put our new menu after).

If we ignore defining a function that you can use to make a pop-up menu, what easy-menu-define does (more or less) is it creates a menu with easy-menu-create-menu, creates binding(s) from this menu with easy-menu-binding, and then sets the binding(s) into the keymaps you requested with define-key (making up a special 'key' name for the binding of the special form '[menu-bar <something>]', which the menu bar system will use to find all of the menu-bar menu entries). We can do this ourselves. First let's do the two easy-menu steps:

(defun cks/easy-menu-setup (menu-name menu-items)  
  (easy-menu-binding
   (easy-menu-create-menu menu-name menu-items)
   menu-name))

Here, menu-name is the user-friendly name of your menu, which with easy-menu-define you'd put as the first element of its menu argument, and menu-items are the elements of the menu, everything except the first element of easy-menu-define's menu argument. This function does everything short of defining the menu-bar 'key'.

(easy-menu-create-menu will sometimes return a keymap and sometimes return something else I don't fully understand, depending on whether you gave the menu any properties. easy-menu-binding handles everything.)

Provided with this function, we can define menus and put them into keymaps to make them appear, like so:

(define-key-after mh-folder-mode-map [menu-bar my-example]
 (cks/easy-menu-setup "Example"
  '(["First entry" (message "First")]
    ["Second entry" (message "Second")])))

(This is not proper Emacs Lisp indentation.)

This will put your new 'Example' menu at the end of the mode specific menus in MH-E's folder window. If you want, you can save the value returned from cks/easy-menu-setup in a let variable and use define-key-after to set it in multiple modes, for example to also set it in mh-show-mode-map (there are cautions here in the case of MH-E that are outside the scope of this entry, and also general cautions in that I'm not sure that reusing the same easy-menu-binding in multiple keymaps is correct, although it works for me).

The normal easy-menu-define code will make up the special key name from the title text of your menu, which may not be what you want. Since we're doing this by hand, we can be different. Note that this may affect your ability to use other easy-menu functions to modify the menu later (for example, easy-menu-add-item). I haven't tested this.

The necessary disclaimer is that while this works for me so far, I'm not sure it's either completely correct or the best way to do this. And it would be nice if there were general functions to shuffle the order of menu bar entries.

PS: What definitely doesn't work, although you might innocently think that it should, is extracting a menu-bar entry's keymap with '(lookup-key map [menu-bar your-name])', using define-key to remove it from the keymap, and adding it back with define-key-after. This will appear to work for simple easy-menu-define menus, but won't for menus with things like a :filter; you appear to get the post-filtered version of the menu and then things obviously go wrong.

Sidebar: The Emacs crimes way

The Emacs crimes way is to use advice-add to temporarily and conditionally turn define-key into define-key-after, because easy-menu-define calls define-key only once (well, once per map). This allows us to keep using all of the features of easy-define-menu, and looks like this:

(defvar cks/define-key-to-after nil)
(defun cks/define-key-to-after (oldfun keymap key def &optional remove)
  (if (and cks/define-key-to-after (not remove))
      (define-key-after keymap key def)
    (apply oldfun keymap key def remove)))

(advice-add 'define-key :around 'cks/define-key-to-after)

(let ((cks/define-key-to-after t))
  (easy-menu-define ... ))

(advice-remove 'define-key 'cks/define-key-to-after)

My view is that this is definitely full bore Emacs crimes, but seasoned Emacs Lisp people may have different views.

A more elaborate version that allows you to optionally specify what to put the binding after (by setting a non-t value for cks/define-key-to-after) is left as an exercise to the reader.

Sidebar: What I think easy-menu-create-menu is returning

Since I started at Lisp code and the output of '(pp ...)' for long enough, what I think easy-menu-create-menu is doing is that it returns either a keymap or a keymap and a set of properties. If it only has a keymap to return, it returns just the keymap. If it has two things to return, it returns an uninterned symbol that has the keymap attached as the 'function' value of the symbol and the menu properties attached as the 'menu-prop' property of the symbol. Easy-menu-binding detects if it's the second case and peels the two parts apart again, then reassembles them differently into something that can be passed to define-key to define a menu.

Easy-menu-create-menu uses extended menu item format, including especially for the top level item that represents your entire menu (and which may have, eg, your :filter on it).

(This entire sidebar may not make sense to future me, but at least I tried.)

Written on 20 October 2023.
« Using extra Firefox profiles to make my life better
Understanding dynamic menubar menus in GNU Emacs »

Page tools: View Source, Add Comment.
Search:
Login: Password:
Atom Syndication: Recent Comments.

Last modified: Fri Oct 20 23:22:56 2023
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.