Come implementare il menu a comparsa simile a quello usato in Magit


10

Domanda

Vorrei creare un'interfaccia utente sotto forma di menu popup , menu popup simile a quello utilizzato in Magit .

Caratteristiche

Definizione di Popup

Popup nel contesto di questa domanda significa una piccola finestra temporanea che contiene una raccolta di voci di menu in modo che l'utente possa selezionare solo una di queste voci.

Posizione sullo schermo

Il popup può apparire in qualsiasi parte dello schermo, ma è auspicabile che sia abbastanza ovvio e quindi dovrebbe apparire accanto alla finestra attualmente attiva.

Contenuto del buffer popup

Gli oggetti dovrebbero essere visualizzati sotto forma di una bella tabella. Abbastanza è il contesto della domanda significa visivamente accattivante, questo effetto può essere ottenuto più facilmente mettendo le voci di menu in file diritte, vedi complete--insert-stringad esempio. Questo paragrafo serve per ulteriori chiarimenti, puoi farlo a modo tuo, questo non renderà la tua risposta errata.

Selezione della voce di menu

La selezione dovrebbe essere eseguita premendo un solo tasto o, facoltativamente, con un mouse (anche se non è così importante, quindi le risposte contenenti proposizioni che non supportano il mouse sono legali). Se proponi una soluzione che supporti il ​​mouse, tieni presente che l'utente dovrebbe essere in grado di selezionare una voce di menu in modo intuitivo, ovvero facendo clic con il pulsante sinistro del mouse sulla scelta desiderata.

NB Il mouse può essere utilizzato in molti modi e sono anche ben accetti modi alternativi per indicare una scelta.

Eliminazione del popup

Una volta che l'utente ha selezionato una voce di menu nel modo sopra descritto, il buffer e quindi la sua finestra dovrebbero essere eliminati dalla vista e uccisi. La finestra che è stata attiva prima dell'invocazione del menu a comparsa dovrebbe essere nuovamente attiva (ovvero, diventare attiva).

Valore e argomenti restituiti

Preferibilmente, questa conseguenza delle azioni dovrebbe comportare la restituzione di un oggetto Lisp. L'oggetto Lisp può essere:

  • nil- indica che l'utente ha interrotto il menu popup premendo C-go in qualche altro modo †.

  • string- stringa (è consentito utilizzare un simbolo) dovrebbe essere string-equal una delle stringhe fornite al menu popup come raccolta di oggetti reali.

Sono accettabili modi alternativi per far conoscere al resto del programma la scelta dell'utente o, eventualmente, la sua assenza. Tuttavia, se non è chiaro in che altro modo può essere eseguita, chiedo a tutti i rispondenti di improvvisare e non chiedermi ulteriori chiarimenti su questo aspetto.

Questo è tutto per il valore restituito. Per quanto riguarda i parametri di input, dovrebbero almeno includere una raccolta di stringhe che rappresentano possibili scelte (ovvero voci di menu).

Risposte accettabili

La risposta attesa può essere delle seguenti forme:

  • Snippet di codice sufficiente che consente al lettore istruito di scrivere funzioni come quella sopra descritta; non è previsto né necessario scrivere l'intera funzione di lavoro. Tuttavia, per evitare incertezza (è possibile omettere parti considerevoli di codice?), Devo notare che le parti mancanti dello snippet dovrebbero essere descritte nella componente testuale della risposta.

  • Un collegamento alla libreria esistente che implementa funzionalità simili. Per evitare incertezza, dovrei notare che simile nel nostro caso significa che la libreria può essere utilizzata per creare popup (vedere la definizione sopra) che abbia almeno 2 o 3 funzioni sopra descritte. Se la libreria proposta è diversa dal punto in cui non è possibile soddisfare la condizione precedentemente dichiarata, ciascuno di questi casi verrà valutato in modo indipendente e sarà sempre valutato se OP lo ritiene utile.

  • Descrizione delle funzioni integrate di Emacs o di terze parti che possono essere utilizzate per implementare qualsiasi funzione descritta nella sezione «Funzionalità», vedere sopra. Per evitare incertezza, indica chiaramente come la tua risposta può essere utile per i futuri lettori che desiderano implementare un menu popup , simile a quello utilizzato in Magit .


† I modi alternativi per interrompere il menu popup possono includere i seguenti (ma non limitato a questi):

  • facendo clic all'esterno della finestra del menu popup;

  • uccisione del buffer contenente il popup senza fare una scelta.

Risposte:


8

magit-popupha il suo manuale ! Ma a meno che tu non voglia effettivamente impostare argomenti nel popup che vengano poi passati all'azione invocata, probabilmente stai meglio usando hydrainvece.

Si noti inoltre che non intendo sviluppare ulteriormente magit-popup. Invece scriverò un pacchetto simile da zero. C'è solo molta complessità accidentale nell'attuale implementazione. (Ma ciò non significa che magit-popupsvanirà semplicemente. Probabilmente non vedrà molte nuove funzionalità, ma se l'implementazione attuale fa quello che vuoi, perché non usarla?)

O poiché anche tu vuoi farlo "da zero", dai un'occhiata read-char-choicee vai da lì. Per implementare la parte visiva dare un'occhiata a lvquale fa parte del repository hydra.


Per altri lettori: Nota che @tarsius ha seguito e scritto quel sostituto per magit-popup. Si chiama il nuovo pacchetto transiente questo è ciò che viene utilizzato nelle versioni correnti di magit. Vedere magit.vc/manual/transient per la documentazione.
phils,

5

Gli hydra sono piuttosto semplici da scrivere:

(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 è un'interfaccia incentrata sulla tastiera e, nella sua forma di base, non è più difficile di easy-menu-define(integrato). Ed è abbastanza estensibile se vuoi farlo fare cose più complesse.

Guarda questa interfaccia twitter, la finestra in basso è una Hydra personalizzata, non molto più difficile da scrivere rispetto a quella sopra:

Hydra-twittering.png

Il codice per questo è disponibile sul wiki , insieme a molti altri esempi.


Fantastico, troverò sicuramente il tempo per saperne di più su questa cosa!
Mark Karpov,

1

Dopo alcune ricerche ho trovato un linguaggio che può essere usato per creare una finestra nella parte inferiore (o addirittura ovunque) della finestra attualmente attiva. Questo stesso ha effetto di una finestra ausiliaria temporanea:

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

Puoi giocare un po 'con l' actionargomento with-current-buffer-windowse vuoi che il popup appaia in diverse parti dello schermo.

La funzione di uscita è descritta nella stringa di documenti di with-current-buffer-window, può essere utilizzata per ottenere input dall'utente. Come suggerito da @tarsius, read-char-choiceè un buon candidato per sperimentare.

Il buffer popup stesso può essere generato come qualsiasi altro buffer. Sto ancora pensando ai pulsanti lì, perché l'utente potrebbe usare il mouse per selezionare un elemento nel menu popup, non solo la tastiera. Tuttavia, ciò richiede uno sforzo aggiuntivo se si desidera farlo bene.

Se il tuo popup è un po 'ad hoc e non ti servirà più di una volta, puoi cavartela con questa soluzione. Inoltre, dai un'occhiata completion--insert-strings. L'ho trovato abbastanza utile come esempio di come generare belle file di voci di menu, puoi persino usarlo inalterato se non hai bisogno di qualcosa di speciale.


4
Penso che forse la domanda che hai in testa sia buona; la domanda che hai scritto non è abbastanza chiara, non vedo come qualcuno avrebbe potuto sapere che stavi cercando una risposta come questa.
npostavs,

1

Ecco una soluzione di terze parti, utilizzando Icicles .

  • Il tuo codice apre una finestra con i candidati disposti ordinatamente come menu. Per come vedere di seguito.

  • Si antepone a ciascuna voce di menu un carattere unico. Ad esempio, a, b, c ... o 1, 2, 3 ... L'utente preme il carattere per scegliere l'elemento.

  • Associa alcune variabili a una chiamata completing-read. Gli passi un elenco delle voci del tuo menu. Puoi facoltativamente specificare uno degli elementi come predefinito, scelto se l'utente ha appena raggiunto RET.

    I collegamenti variabili indicano completing-reada:

    • Mostra ogni voce di menu su una riga separata
    • Mostra subito il menu
    • Aggiornalo immediatamente quando un utente preme un tasto
    • Ritorna immediatamente, se l'input dell'utente corrisponde a un solo elemento
    • Mostra la scelta predefinita nel prompt.
  • Puoi rendere le voci di menu fantasiose come desideri (non mostrate).

    • facce
    • immagini
    • annotazioni
    • più righe per articolo
    • mouse-3 menu popup, per fare qualsiasi cosa con l'oggetto
(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)))

Puoi anche creare menu a scelta multipla , ovvero menu in cui un utente può scegliere più elementi contemporaneamente (scegli un sottoinsieme delle possibili scelte).


1

Potresti anche essere interessato a guardare il pacchetto makey. Ha lo scopo di fornire funzionalità simili come magit-popup, ed è un fork del predecessore di magit-popup( magit-key-mode, menzionato nel commento) da quando non era ancora un pacchetto disponibile separatamente da magit.

Vorrei anche menzionare discover: questo è un esempio di come usare makey(e anche la sua ragion d'essere). Questo pacchetto ha lo scopo di aiutare i nuovi arrivati ​​a scoprire i binding di emacs.


Riferimenti


2
@Mark Non sono del tutto sicuro del motivo per cui hai accettato questa risposta. Non menziona affatto i piccoli blocchi di costruzione, che, se ricordo bene, è ciò che stavi cercando. Inoltre non è corretto: makey non è un fork di magit-popup, è un fork della sua precedente modalità magit-key. Eredita la maggior parte dei deficit che da allora sono stati affrontati nel popup magit. Quindi sei molto meglio di usare magit-popup o hydra (o scrivere il tuo).
tarsius,

@tarsius Giudicare la correttezza di una risposta può essere difficile. Se sai che non è corretto, sentiti libero di modificarlo. Ho fatto una modifica, non sono ancora sicuro al 100% che sia corretto ora. Inoltre non ho menzionato i "deficit" che eredita perché non so quali siano.
YoungFrog


0

Basato sulla risposta di @ Drew (Icicles), con modifiche per separare l'implementazione del menu dai dati del menu - in modo da poter definire più menu, ognuno con il proprio: (contenuto, prompt, impostazione predefinita).

Questo opzionalmente utilizza l'edera (se disponibile), che ha funzionalità più avanzate come la possibilità di attivare gli elementi mantenendo aperto il menu.

Popup generico.

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

Esempio di utilizzo, assegnare alcune azioni al tasto 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)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.