10

Question

I would like to create user interface in form of popup menu, popup menu similar to that used in Magit.

Features

Definition of Popup

Popup in context of this question means little temporary window that contains collection of menu items so that user can select one and only one of these items.

Position on the Screen

The popup is allowed to appear in any part of the screen, but it's desirable that it should be quite obvious and thus it should appear beside currently active window.

Contents of Popup Buffer

Items should be displayed in form of pretty table. Pretty is context of the question means visually appealing, this effect can be most easily achieved by putting menu items into straight rows, see complete--insert-string for example. This paragraph serves for additional clarification, you can do it in your own way, this won't render your answer incorrect.

Selection of Menu Item

The selection is expected to be performed by a single key-pressing or, optionally with a mouse (although it's not that important so answers containing propositions that don't support mouse are legal). If you propose solution that supports mouse, please note that user should be able to select a menu item in an intuitive way, that is, by left-button clicking on desired choice.

N.B. mouse can be used in many ways and alternative ways to indicate a choice are also welcomed.

Elimination of Popup

Once user has selected a menu item in a way described above, the buffer and thus its window should be eliminated from view as well as killed. The window that has been active before invocation of the popup menu should get focus (that is, become active) again.

Returned Value and Arguments

Preferably, this consequence of actions should result in a Lisp object returned. The Lisp object can be either:

  • nil — this indicates that user has aborted the popup menu either by pressing C-g or in some other way †.

  • string — string (it's allowed to use a symbol) should be string-equal to one of the strings supplied to the popup menu as collection of actual items.

Alternative ways to let the rest of program know user's choice, or, possibly, its absence, are acceptable. However, if it's not clear how else it can be performed I ask all answerers improvise and don't ask me for further clarification of this aspect.

This is all for returned value. As for input parameters, they should at least include collection of strings which represent possible choices (that is, menu items).

Acceptable Answers

Expected answer can be of the following forms:

  • Sufficient code snippet that allows educated reader to write function like that described above; it's not expected or necessary to write entire working function. However, to avoid uncertainty (can considerable parts of code be omitted?), I should note that missing parts of the snippet should be described in textual component of answer.

  • A link to existing library that implements similar functionality. To avoid uncertainty, I should note that similar in our case means that the library can be used to create popup (see definition above) that has at least 2 or 3 features described above. If proposed library is different to the point where the previously stated condition cannot be met, each such case will be judged independently and will be always upvoted if OP deems it useful.

  • Description of built-in Emacs functions or third-party ones that can be used to implement any feature described in section «Features», see above. To avoid uncertainty, please state clearly how your answer can be useful for future readers who want to implement popup, popup menu similar to that used in Magit.


† Alternative ways to abort the popup menu may include the following (but not limited to these):

  • clicking outside of popup menu window;

  • killing of buffer containing the popup without making a choice.

0

7 Answers 7

10

magit-popup has it's own manual! But unless you actually want to set arguments in the popup which are then passed to the invoked action, you are probably better off using hydra instead.

Also note that I don't intend to further develop magit-popup. Instead I will write a similar package from scratch. There's just to much accidental complexity in the current implementation. (But that doesn't mean that magit-popup will simply vanish. It likely won't see many new features, but if the current implementation does what you want, then why not use it?)

Or since you too want to do it "from scratch", have a look at read-char-choice and go from there. To implement the visual part have a look at lv which is part of the hydra repository.

1
  • 1
    For other readers: Note that @tarsius has followed through and written that replacement for magit-popup. The new package is called transient, and this is what is used in current versions of magit. See magit.vc/manual/transient for documentation.
    – phils
    Commented Mar 21, 2020 at 22:49
7

Hydras are pretty simple to write:

(defhydra hydra-launcher (:color blue :columns 2)
   "Launch"
   ("h" man "man")
   ("r" (browse-url "http://www.reddit.com/r/emacs/") "reddit")
   ("w" (browse-url "http://www.emacswiki.org/") "emacswiki")
   ("s" shell "shell")
   ("q" nil "cancel"))

(global-set-key (kbd "C-c r") 'hydra-launcher/body)

Hydra is a keyboard-centric interface, and in its basic form, it's not harder than easy-menu-define (built-in). And it's quite extensible if you want to make it do more complex things.

Just look at this twittering interface, the bottom window is a custom Hydra, not much harder to write than the one above:

hydra-twittering.png

The code for this is available on the wiki, along with a lot more examples.

1
  • Great, I'll certainly find time to learn more about this thing! Commented Jul 28, 2015 at 12:48
1

After some research I've found an idiom that can be used to create a window at the bottom (or indeed anywhere) of currently active window. This itself has effect of temporary, auxiliary window:

(let ((buffer (get-buffer-create "*Name of Buffer*")))
  (with-current-buffer buffer
    (with-current-buffer-window
     ;; buffer or name
     buffer
     ;; action (for `display-buffer')
     (cons 'display-buffer-below-selected
           '((window-height . fit-window-to-buffer)
             (preserve-size . (nil . t))))
     ;; quit-function
     (lambda (window _value)
       (with-selected-window window
         (unwind-protect
             ;; code that gets user input
           (when (window-live-p window)
             (quit-restore-window window 'kill)))))
     ;; Here we generate the popup buffer.
     (setq cursor-type nil)
     ;; …
     )))

You can play a bit with action argument of with-current-buffer-window if you want the popup to appear in different part of the screen.

Quit function is described in doc-string of with-current-buffer-window, it can be used to get input from user. As @tarsius suggested, read-char-choice is a good candidate for experimenting.

The popup buffer itself can be generated just like any other buffer. I'm still thinking of buttons there, because user could use his mouse to select an item in the popup menu, not just keyboard. However, this requires additional effort if you want to do it well.

If your popup is a bit ad-hoc and you won't need it more than once, you can get away with this solution. Also, take a look at completion--insert-strings. I found it quite useful as an example of how to generate pretty rows of menu items, you can even use it unaltered if you don't need something special.

1
  • 4
    I think maybe the question you have in your head is good; the question you wrote down is rather unclear, I don't see how anyone could have known you were after an answer like this.
    – npostavs
    Commented Jul 27, 2015 at 9:34
1

Here is a 3rd-party solution, using Icicles.

  • Your code pops up a window with the candidates arranged neatly as a menu. For how, see below.

  • You prefix each menu item with a unique character. E.g., a, b, c... or 1, 2, 3... The user hits the char to choose the item.

  • You bind a few variables around a call to completing-read. You pass it a list of your menu items. You optionally specify one of the items to be the default, chosen if the user just hits RET.

    The variable bindings tell completing-read to:

    • Show each menu item on a separate row
    • Show the menu immediately
    • Update it immediately when a user hits a key
    • Return immediately, if the user input matches only one item
    • Show the default choice in the prompt.
  • You can make the menu items as fancy as you want (not shown).

    • faces
    • images
    • annotations
    • multiple lines per item
    • mouse-3 popup menu, to do anything with the item
(defvar my-menu '(("a: Alpha It"   . alpha-action)
                  ("b: Beta It"    . beta-action)
                  ("c: Gamma It"   . gamma-action)
                  ("d: Delta It"   . delta-action)
                  ("e: Epsilon It" . epsilon-action)
                  ("f: Zeta It"    . zeta-action)
                  ("g: Eta It"     . eta-action)
                  ("h: Theta It"   . theta-action)
                  ("i: Iota It"    . iota-action)
                  ("j: Kappa It"   . kappa-action)
                  ("k: Lambda It"  . lambda-action)))

(defun my-popup ()
  "Pop up my menu.
User can hit just the first char of a menu item to choose it.
Or s?he can click it with `mouse-1' or `mouse-2' to select it.
Or s?he can hit RET immediately to select the default item."
  (interactive)
  (let* ((icicle-Completions-max-columns               1)
         (icicle-show-Completions-initially-flag       t)
         (icicle-incremental-completion-delay          0.01)
         (icicle-top-level-when-sole-completion-flag   t)
         (icicle-top-level-when-sole-completion-delay  0.01)
         (icicle-default-value                         t)
         (icicle-show-Completions-help-flag            nil)
         (choice  (completing-read "Choose: " my-menu nil t nil nil
                                   "b: Beta It")) ; The default item.
         (action  (cdr (assoc choice my-menu))))

    ;; Here, you would invoke the ACTION: (funcall action).
    ;; This message just shows which ACTION would be invoked.
    (message "ACTION chosen: %S" action) (sit-for 2)))

You can also create truly multiple-choice menus, that is, menus where a user can choose multiple items at the same time (choose a subset of the possible choices).

0
1

You might also be interested in looking at the package makey. It is intended to provide similar functionality as magit-popup, and it is a fork of the predecessor of magit-popup (magit-key-mode, mentionned in the comment) from when it was not yet a package available separately from magit.

Let me also mention discover : that is an example of how to use makey (and also its raison d'être). That package is meant to help newcomers discover emacs bindings.


References

2
  • 2
    @Mark I am not quite sure why you accepted this answer. It does not at all mention small building blocks, which, if I remember correctly, is what you were after. It's also isn't not correct: makey isn't a fork of magit-popup, it's a fork of its predecessor magit-key-mode. It inherits most of the deficits that have since been addressed in magit-popup. So you are much better of using magit-popup or hydra (or writing your own).
    – tarsius
    Commented Sep 2, 2015 at 21:50
  • @tarsius Judging the correctness of an answer can be hard. If you know it isn't correct, feel free to edit it. I did an edit, I'm still not 100% sure it is correct now. I also didn't mention the "deficits" it inherits because I don't know what they are.
    – YoungFrog
    Commented Sep 7, 2015 at 7:29
0

Guide-Key work nice for me for all configured prefixes.

0

Based on @Drew's answer (Icicles), with modifications to separate menu implementation from menu data - so multiple menu's could be defined, each with their own: (content, prompt, default).

This optionally uses ivy (when available), which has more advanced features such as being able to activate items while keeping the menu open.

Generic popup.

(defun custom-popup (prompt default-index content)
  "Pop up menu
Takes args: prompt, default-index, content).
Where the content is any number of (string, function) pairs,
each representing a menu item."
  (cond
   ;; Ivy (optional)
   ((fboundp 'ivy-read)
    (ivy-read
     prompt content
     :preselect
     (cond
      ((= default-index -1)
       nil)
      (t
       default-index))
     :require-match t
     :action
     (lambda (x)
       (pcase-let ((`(,_text . ,action) x))
         (funcall action)))
     :caller #'custom-popup))

   ;; Fallback to completing read.
   (t
    (let ((choice
           (funcall completing-read-function
                    prompt
                    content
                    nil
                    t
                    nil
                    nil
                    (nth default-index content))))
      (pcase-let ((`(,_text . ,action) (assoc choice content)))
        (funcall action))))))

Example use, assign some actions to the f12 key:

(defvar my-global-utility-menu-def
  ;; (content, prompt, default_index)
  '(("Emacs REPL" . ielm)
    ("Spell Check" . ispell-buffer)
    ("Delete Trailing Space In Buffer" . delete-trailing-whitespace)
    ("Emacs Edit Init" . (lambda () (find-file user-init-file)))))

(defun my-global-utility-popup ()
  "Pop up my menu. Hit RET immediately to select the default item."
  (interactive)
  (custom-popup "Select: " 1 my-global-utility-menu-def))

(global-set-key (kbd "<f12>") 'my-global-utility-popup)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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