Wandering Thoughts archives

2023-10-21

Understanding dynamic menubar menus in GNU Emacs

Suppose, not hypothetically, that you don't just want to add a new menu to Emacs' menu bar, but that you want to dynamically determine what's in this menu. You have two options, one simple to understand and use but involving magic and one complicated but with less magic. The simple option is an easy menu with a :filter. An easy-menu filter function has a simple calling convention and a simple usage. It's called with a list of the menu entries you initially specified in easy-menu-define (or the equivalent if you did it yourself), and it returns a list of what menu entries should be included, in exactly the same format. The easy-menu menu entry format is also quite powerful and expressive, letting you do things like bind menu entries to expressions, not just functions.

(To put the conclusion first, I suggest that you stick to easy-menu for dynamic menus unless you have a compelling reason otherwise.)

The more complex but less magical way is to use Emacs' standard menu system with extended menu items. To understand what we need to do, we need to understand a number of things about Emacs menus and the menu-bar. To start with, menus are actually keymaps, and keymaps themselves are just specially formatted lists. Also, an actual keymap only describes the mapping for a single character at a time. Although you can define multi-key sequences all at once for convenience, a sequence like 'C-x 5 f' is actually three entries in three keymaps; there's an entry for C-x in the global keymap, an entry for '5' in the C-x keymap, and an entry for 'f' in the 'C-x 5' keymap. If you define a multi-key sequence and the necessary intermediate keymaps don't yet exist, Emacs creates them for you. All of this is true for the menu-bar and for menus. When you define a 'key' binding for '[menu-bar my-example]' to add a new menu-bar entry, there's a '[menu-bar]' keymap that Emacs is inserting a 'my-example' entry into, which has your 'my-example' keymap and other information.

To implement a dynamic menu with standard Emacs menus, we generally need to use the :filter property of extended menu items. However, this is confusing to understand because it's documented in terms of an individual menu entry, not a menu:

This property provides a way to compute the menu item dynamically. The property value filter-fn should be a function of one argument; when it is called, its argument will be real-binding. The function should return the binding to use instead.

A menu on the menu-bar is a keymap, which is to say its 'real-binding' is a keymap. Here is a non-dynamic starting point:

(defvar test-menu-map (make-sparse-keymap "Test"))
(define-key-after test-menu-map [entry] '("Entry" . end-of-buffer))
(define-key-after test-menu-map [disabled]
  '(menu-item "Disabled" beginning-of-buffer :enable nil))

(define-key-after (current-global-map) [menu-bar test-menu]
  (cons "Test" test-menu-map))

(This sort of follows the example in Menu Bar. The functions I'm binding are random; I have to bind something, and this way I can see visible effects from a specific menu entry.)

Since the real-binding is a keymap, a :filter function will be passed the keymap and needs to return another keymap that will describe all of the menu entries. Since keymaps are a list, we can append additional menu entries to the keymap and return that (and I'll do this in my example). And to specify the :filter property, we need to set our menu-bar 'key' using the extended menu item format, instead of the simple one. Assuming that we have a my-generate-menu function, setting up the menu looks like this:

(define-key-after (current-global-map) [menu-bar test-menu]
  (list 'menu-item "Test" test-menu-map
        :filter 'my-generate-menu))

Now we get into a bit more work, because our my-generate-menu function must return a keymap that has entries in the internal keymap formats for menu entries, as covered in the format of keymaps, and this is not quite the format you give to define-key. If we dump our test-menu-map to see how the entries actually look, we will get this slightly transformed version:

(keymap "Test"
   (entry "Entry" . end-of-buffer)
   (disabled menu-item "Disabled" beginning-of-buffer :enable nil))

(The 'entry' and 'disabled' are the key name symbols we gave to define-key-after.)

The menu entries our filter function will add need to be in the same format. So a functional (although hard-coded) filter function looks like this:

(defun my-generate-menu (orig-binding)
  (append orig-binding
     '((new-one menu-item "New one" forward-word)
       (new-simple "Simple one" . backward-word)
       (new-disabled menu-item "Disabled one" next-line :enable nil))))

There are a variety of options for how your filtering function could work. If you want to make an entirely dynamic menu, you could probably have nothing in the initial test-menu-map keymap, entirely ignore the orig-binding argument to the filter function, and just create a new keymap and define menu items in it in your filter function (then return it). This would save you from getting the formatting details right for each type of keymap entry; define-key or define-key-after would worry about that for you.

(Or you could completely create your keymap by hand, since it's just a list and Emacs Lisp has plenty of options for creating lists.)

If you want to put things at the start of the menu while preserving the fixed entries at the end, things get trickier because the lists that are keymaps must start with the 'keymap' symbol. You'll need to add your entries after that symbol but before any existing bindings, or build your own keymap list. As we see above, appending entries is (somewhat) easier.

(Easy-menu's menu :filter is implemented using the standard Emacs menu item :filter, but it does various transformations in the process.)

Having gone through the entire exercise of working out how to do this with standard Emacs menu facilities, my considered opinion is that I'm going to stick with easy-menu for dynamic menus (and for non-dynamic ones too). Easy-menu has easier to use dynamic filtering and easier to use menu entries, and I'm not going to bet against its overall efficiency either. I think that easy-menu's ability to bind menu entries to expressions instead of just functions is especially useful for dynamic menus; in the single dynamic menu I built, I wanted to build a a bunch of entries of the form 'do fixed thing to <this>'. In the standard Emacs menu facilities, I'd have been building a lot of lambdas. In easy-menu, easy-menu did it for me and I think with more efficiency.

(I wound up digging into this once I learned enough to understand that easy-menu's :filter must be converting menu items from the easy-menu format to the true keymap format, which made me wonder if directly using Emacs's menu item :filter property would be more efficient. Now hopefully I can stop poking into Emacs corners.)

PS: You (I) may someday find yourself wanting to use some of the keymap-* functions on menus and menu entries. These functions take key names in string form, not in the '[menu-bar test-menu]' form that define-key does. In string form, this is written as "<menu-bar> <test-menu>", because internally the menu and menu-bar names are treated as (pretend) function keys (and Emacs represents function keys as symbols, cf). You can see this by evaluating, for example, '(kbd "<menu-bar> <file>")'.

Emacs is sometimes weirdness all the way down, partly because it has a very long history.

Sidebar: A function that reports its (menu) invocation

If you're testing standard Emacs menus, where you can only bind a function (instead of an expression), you may want an interactive function that reports how it was invoked, so you can bind it to lots of menu entries and still get useful feedback. There are probably several ways to get this, but here is what I came up with:

(defun cks/report-path ()
  (interactive)
  (message "Invoked via: %s" (this-command-keys)))

(In my examples above I bound a random set of movement functions.)

programming/EmacsDynamicMenubarMenus written at 23:00:40; Add Comment


Page tools: See As Normal.
Search:
Login: Password:
Atom Syndication: Recent Pages, Recent Comments.

This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.