Cómo implementar un menú emergente similar al utilizado en Magit

10

Pregunta

Me gustaría crear una interfaz de usuario en forma de menú emergente , menú emergente similar al utilizado en Magit .

Caracteristicas

Definición de ventana emergente

La ventana emergente en el contexto de esta pregunta significa una pequeña ventana temporal que contiene una colección de elementos de menú para que el usuario pueda seleccionar uno y solo uno de estos elementos.

Posición en la pantalla

Se permite que la ventana emergente aparezca en cualquier parte de la pantalla, pero es deseable que sea bastante obvia y, por lo tanto, que aparezca junto a la ventana actualmente activa.

Contenido de Popup Buffer

Los artículos deben mostrarse en forma de bonita mesa. Bonito es el contexto de la pregunta significa visualmente atractivo, este efecto se puede lograr más fácilmente colocando los elementos del menú en filas rectas, ver complete--insert-stringpor ejemplo. Este párrafo sirve para aclaraciones adicionales, puede hacerlo a su manera, esto no hará que su respuesta sea incorrecta.

Selección de elemento de menú

Se espera que la selección se realice presionando una sola tecla u, opcionalmente, con un mouse (aunque no es tan importante, por lo que las respuestas que contienen proposiciones que no admiten mouse son legales). Si propone una solución compatible con el mouse, tenga en cuenta que el usuario debe poder seleccionar un elemento del menú de forma intuitiva, es decir, haciendo clic con el botón izquierdo en la opción deseada.

El mouse NB se puede usar de muchas maneras y también se aceptan formas alternativas para indicar una elección.

Eliminación de Popup

Una vez que el usuario ha seleccionado un elemento del menú de la manera descrita anteriormente, el búfer y, por lo tanto, su ventana deben eliminarse de la vista y eliminarse. La ventana que ha estado activa antes de la invocación del menú emergente debería volver a enfocarse (es decir, volverse activa).

Valor devuelto y argumentos

Preferiblemente, esta consecuencia de las acciones debería dar como resultado un objeto Lisp devuelto. El objeto Lisp puede ser:

  • nil- esto indica que el usuario ha abortado el menú emergente presionando C-go de alguna otra manera †.

  • string- la cadena (está permitido usar un símbolo) debe estar string-equal en una de las cadenas suministradas al menú emergente como colección de elementos reales.

Se aceptan formas alternativas para que el resto del programa conozca la elección del usuario o, posiblemente, su ausencia. Sin embargo, si no está claro de qué otra manera se puede realizar, pido a todos los que responden que improvisen y no me piden más aclaraciones sobre este aspecto.

Esto es todo por valor devuelto. En cuanto a los parámetros de entrada, deberían incluir al menos una colección de cadenas que representen posibles opciones (es decir, elementos de menú).

Respuestas Aceptables

La respuesta esperada puede ser de las siguientes formas:

  • Suficiente fragmento de código que permite al lector educado escribir una función como la descrita anteriormente; No se espera ni es necesario escribir toda la función de trabajo. Sin embargo, para evitar la incertidumbre (¿se pueden omitir partes considerables del código?), Debo tener en cuenta que las partes faltantes del fragmento deben describirse en el componente textual de la respuesta.

  • Un enlace a la biblioteca existente que implementa una funcionalidad similar. Para evitar la incertidumbre, debo tener en cuenta que similar en nuestro caso significa que la biblioteca se puede utilizar para crear ventanas emergentes (consulte la definición anterior) que tiene al menos 2 o 3 características descritas anteriormente. Si la biblioteca propuesta es diferente al punto en el que no se puede cumplir la condición previamente establecida, cada uno de estos casos se juzgará de forma independiente y siempre se votará si OP lo considera útil.

  • Descripción de las funciones integradas de Emacs o de terceros que se pueden utilizar para implementar cualquier característica descrita en la sección «Características», ver arriba. Para evitar la incertidumbre, por favor aclara cómo su respuesta puede ser útil para futuros lectores que deseen implementar emergente , menú emergente similar a la utilizada en Magit .


† Las formas alternativas de abortar el menú emergente pueden incluir lo siguiente (pero no limitado a esto):

  • haciendo clic fuera de la ventana del menú emergente;

  • Eliminación del búfer que contiene la ventana emergente sin hacer una elección.

Mark Karpov
fuente

Respuestas:

8

magit-popuptiene su propio manual ! Pero a menos que realmente desee establecer argumentos en la ventana emergente que luego se pasan a la acción invocada, probablemente sea mejor usarlos hydra.

También tenga en cuenta que no tengo la intención de seguir desarrollándome magit-popup. En cambio, escribiré un paquete similar desde cero. Hay demasiada complejidad accidental en la implementación actual. (Pero eso no significa que magit-popupsimplemente desaparecerá. Es probable que no vea muchas características nuevas, pero si la implementación actual hace lo que quiere, ¿por qué no usarla?)

O ya que usted también quiere hacerlo "desde cero", eche un vistazo read-char-choicey avance desde allí. Para implementar la parte visual, eche un vistazo a lvcuál es parte del repositorio de hidra.

tarsius
fuente
Para otros lectores: Tenga en cuenta que @tarsius ha seguido y escrito ese reemplazo para magit-popup. Se llama al nuevo paquete transient, y esto es lo que se usa en las versiones actuales de magit. Consulte magit.vc/manual/transient para obtener documentación.
Phil
5

Las hidras son bastante simples de escribir:

(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 es una interfaz centrada en el teclado y, en su forma básica, no es más difícil que easy-menu-define(incorporada). Y es bastante extensible si quieres que haga cosas más complejas.

Solo mire esta interfaz de Twitter, la ventana inferior es una Hydra personalizada, no mucho más difícil de escribir que la anterior:

hydra-twittering.png

El código para esto está disponible en la wiki , junto con muchos más ejemplos.

abo-abo
fuente
¡Genial, ciertamente encontraré tiempo para aprender más sobre esto!
Mark Karpov
1

Después de algunas investigaciones, he encontrado un idioma que se puede usar para crear una ventana en la parte inferior (o de hecho en cualquier lugar) de la ventana actualmente activa. Esto mismo tiene efecto de ventana auxiliar temporal:

(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)
     ;; …
     )))

Puede jugar un poco con el actionargumento de with-current-buffer-windowsi desea que la ventana emergente aparezca en diferentes partes de la pantalla.

La función Salir se describe en la cadena de documentos de with-current-buffer-window, se puede usar para obtener información del usuario. Como sugirió @tarsius, read-char-choicees un buen candidato para experimentar.

El búfer emergente en sí se puede generar como cualquier otro búfer. Todavía estoy pensando en botones allí, porque el usuario podría usar su mouse para seleccionar un elemento en el menú emergente, no solo el teclado. Sin embargo, esto requiere un esfuerzo adicional si desea hacerlo bien.

Si su ventana emergente es un poco ad-hoc y no la necesitará más de una vez, puede salirse con la suya. Además, eche un vistazo a completion--insert-strings. Lo encontré bastante útil como un ejemplo de cómo generar filas bonitas de elementos de menú, incluso puede usarlo sin modificaciones si no necesita algo especial.

Mark Karpov
fuente
44
Creo que tal vez la pregunta que tienes en la cabeza es buena; la pregunta que escribió no está clara, no veo cómo alguien podría haber sabido que estaba después de una respuesta como esta.
npostavs
1

Aquí hay una solución de terceros, usando Icicles .

  • Su código abre una ventana con los candidatos ordenados como un menú. Para saber cómo, ver más abajo.

  • Prefijas cada elemento del menú con un personaje único. Por ejemplo, a, b, c ... o 1, 2, 3 ... El usuario toca el carácter para elegir el elemento.

  • Vincula algunas variables alrededor de una llamada a completing-read. Le pasa una lista de los elementos de su menú. Opcionalmente, puede especificar que uno de los elementos sea el predeterminado, elegido si el usuario solo presiona RET.

    Los enlaces variables le dicen completing-reada:

    • Mostrar cada elemento del menú en una fila separada
    • Mostrar el menú inmediatamente
    • Actualícela inmediatamente cuando un usuario presione una tecla
    • Regrese de inmediato, si la entrada del usuario coincide con un solo elemento
    • Mostrar la opción predeterminada en la solicitud.
  • Puede hacer que los elementos del menú sean tan elegantes como desee (no se muestran).

    • caras
    • imágenes
    • anotaciones
    • múltiples líneas por artículo
    • mouse-3 menú emergente, para hacer algo con el elemento
(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)))

También puede crear menús verdaderamente de opción múltiple , es decir, menús en los que un usuario puede elegir varios elementos al mismo tiempo (elija un subconjunto de las opciones posibles).

Dibujó
fuente
1

También podría estar interesado en mirar el paquete makey. Su objetivo es proporcionar una funcionalidad similar magit-popupy es una bifurcación del predecesor de magit-popup( magit-key-modemencionado en el comentario) de cuando aún no era un paquete disponible por separado magit.

Permítanme mencionar también discover: ese es un ejemplo de cómo usar makey(y también su razón de ser). Ese paquete está destinado a ayudar a los recién llegados a descubrir enlaces de emacs.


Referencias

YoungFrog
fuente
2
@ Mark No estoy muy seguro de por qué aceptaste esta respuesta. No menciona en absoluto los pequeños bloques de construcción, que, si mal no recuerdo, es lo que buscabas. Tampoco no es correcto: makey no es una bifurcación de magit-popup, es una bifurcación de su predecesor magit-key-mode. Hereda la mayoría de los déficits que desde entonces se han abordado en magit-popup. Entonces, es mucho mejor usar magit-popup o hydra (o escribir el suyo).
tarsius
@tarsius Juzgar la exactitud de una respuesta puede ser difícil. Si sabe que no es correcto, no dude en editarlo. Hice una edición, todavía no estoy 100% seguro de que sea correcta ahora. Tampoco mencioné los "déficits" que hereda porque no sé cuáles son.
YoungFrog
0

Guide-Key funciona bien para mí para todos los prefijos configurados.

Netsu
fuente
0

Basado en la respuesta de @ Drew (Icicles), con modificaciones para separar la implementación del menú de los datos del menú, por lo que se pueden definir múltiples menús, cada uno con su propio: (contenido, solicitud, predeterminado).

Opcionalmente, utiliza hiedra (cuando está disponible), que tiene características más avanzadas como la posibilidad de activar elementos mientras se mantiene abierto el menú.

Popup genérico.

(defun custom-popup (my-prompt default-index my-content)
  "Pop up menu
Takes args: my-prompt, default-index, my-content).
Where the content is any number of (string, function) pairs,
each representing a menu item."
  (cond
    ;; Ivy (optional)
    ((fboundp 'ivy-read)
      (ivy-read my-prompt my-content
        :preselect
        (if (= default-index -1) nil 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 (completing-read
                     my-prompt my-content nil t nil nil
                     (nth default-index my-content))))
        (pcase-let ((`(,_text . ,action) (assoc choice my-content)))
          (funcall action))))))

Ejemplo de uso, asigne algunas acciones a la tecla f12:

(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 my-global-utility-menu-def "Select: ", 1))

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