Comment implémenter un menu contextuel similaire à celui utilisé dans Magit


10

Question

Je voudrais créer une interface utilisateur sous forme de menu contextuel , menu contextuel similaire à celui utilisé dans Magit .

Caractéristiques

Définition de Popup

Une fenêtre contextuelle dans le contexte de cette question signifie une petite fenêtre temporaire qui contient une collection d'éléments de menu afin que l'utilisateur puisse sélectionner un et un seul de ces éléments.

Position sur l'écran

La fenêtre contextuelle peut apparaître dans n'importe quelle partie de l'écran, mais il est souhaitable qu'elle soit assez évidente et qu'elle apparaisse donc à côté de la fenêtre actuellement active.

Contenu du tampon contextuel

Les articles doivent être affichés sous forme de joli tableau. Assez est le contexte de la question signifie visuellement attrayant, cet effet peut être plus facilement obtenu en mettant des éléments de menu en rangées droites, voir complete--insert-stringpar exemple. Ce paragraphe sert de clarification supplémentaire, vous pouvez le faire à votre manière, cela ne rendra pas votre réponse incorrecte.

Sélection de l'élément de menu

La sélection devrait être effectuée en appuyant sur une seule touche ou, éventuellement avec une souris (bien que ce ne soit pas si important, les réponses contenant des propositions qui ne prennent pas en charge la souris sont légales). Si vous proposez une solution prenant en charge la souris, veuillez noter que l'utilisateur devrait pouvoir sélectionner un élément de menu de manière intuitive, c'est-à-dire en cliquant avec le bouton gauche sur le choix souhaité.

La souris NB peut être utilisée de nombreuses façons et d'autres manières d'indiquer un choix sont également les bienvenues.

Élimination de Popup

Une fois que l'utilisateur a sélectionné un élément de menu de la manière décrite ci-dessus, le tampon et donc sa fenêtre doivent être supprimés de la vue et supprimés. La fenêtre qui était active avant l'invocation du menu contextuel devrait redevenir active (c'est-à-dire redevenir active).

Valeur retournée et arguments

De préférence, cette conséquence des actions devrait entraîner le retour d'un objet Lisp. L'objet Lisp peut être soit:

  • nil- cela indique que l'utilisateur a abandonné le menu contextuel en appuyant sur C-gou d'une autre manière †.

  • string- chaîne (il est permis d'utiliser un symbole) doit être string-equal à l'une des chaînes fournies au menu contextuel en tant que collection d'éléments réels.

D'autres manières de faire savoir au reste du programme le choix de l'utilisateur, ou, éventuellement, son absence, sont acceptables. Cependant, si ce n'est pas clair comment cela peut être effectué, je demande à tous les répondeurs d'improviser et ne me demande pas de clarification supplémentaire sur cet aspect.

C'est tout pour la valeur retournée. Quant aux paramètres d'entrée, ils devraient au moins inclure une collection de chaînes qui représentent des choix possibles (c'est-à-dire des éléments de menu).

Réponses acceptables

La réponse attendue peut prendre les formes suivantes:

  • Extrait de code suffisant permettant au lecteur averti d'écrire une fonction comme celle décrite ci-dessus; il n'est pas prévu ni nécessaire d'écrire toute la fonction de travail. Cependant, pour éviter toute incertitude (des parties considérables du code peuvent-elles être omises?), Je dois noter que les parties manquantes de l'extrait doivent être décrites dans la composante textuelle de la réponse.

  • Un lien vers une bibliothèque existante qui implémente des fonctionnalités similaires. Pour éviter toute incertitude, je dois noter que similaire dans notre cas signifie que la bibliothèque peut être utilisée pour créer une fenêtre contextuelle (voir la définition ci-dessus) qui a au moins 2 ou 3 fonctionnalités décrites ci-dessus. Si la bibliothèque proposée est différente au point où la condition énoncée précédemment ne peut pas être remplie, chacun de ces cas sera jugé indépendamment et sera toujours voté si OP le juge utile.

  • Description des fonctions Emacs intégrées ou tierces qui peuvent être utilisées pour implémenter toute fonctionnalité décrite dans la section «Fonctionnalités», voir ci-dessus. Pour éviter l' incertitude de, s'il vous plaît indiquer clairement comment votre réponse peut être utile pour les futurs lecteurs qui souhaitent mettre en œuvre pop - up , menu contextuel semblable à celui utilisé dans Magit .


† Les autres méthodes pour abandonner le menu contextuel peuvent inclure les suivantes (mais sans s'y limiter):

  • cliquer en dehors de la fenêtre du menu contextuel;

  • suppression du tampon contenant le popup sans faire de choix.

Réponses:


8

magit-popupa son propre manuel ! Mais à moins que vous ne vouliez réellement définir des arguments dans la fenêtre contextuelle qui sont ensuite passés à l'action invoquée, il vaut probablement mieux utiliser à la hydraplace.

Notez également que je n'ai pas l'intention de développer davantage magit-popup. Au lieu de cela, j'écrirai un package similaire à partir de zéro. Il y a juste trop de complexité accidentelle dans l'implémentation actuelle. (Mais cela ne signifie pas que magit-popupcela disparaîtra simplement. Il ne verra probablement pas beaucoup de nouvelles fonctionnalités, mais si l'implémentation actuelle fait ce que vous voulez, alors pourquoi ne pas l'utiliser?)

Ou puisque vous aussi vous voulez le faire "à partir de zéro", jetez un œil read-char-choiceet allez-y. Pour implémenter la partie visuelle, regardez lvqui fait partie du référentiel hydra.


Pour les autres lecteurs: Notez que @tarsius a suivi et écrit ce remplacement pour magit-popup. Le nouveau package est appelé transient, et c'est ce qui est utilisé dans les versions actuelles de magit. Voir magit.vc/manual/transient pour la documentation.
phils

5

Les hydres sont assez simples à écrire:

(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 est une interface centrée sur le clavier, et dans sa forme de base, ce n'est pas plus difficile que easy-menu-define(intégré). Et c'est assez extensible si vous voulez lui faire faire des choses plus complexes.

Il suffit de regarder cette interface Twitter, la fenêtre du bas est une Hydra personnalisée, pas beaucoup plus difficile à écrire que celle ci-dessus:

hydra-twittering.png

Le code pour cela est disponible sur le wiki , avec beaucoup plus d'exemples.


Super, je vais certainement trouver le temps d'en savoir plus sur cette chose!
Mark Karpov

1

Après quelques recherches, j'ai trouvé un idiome qui peut être utilisé pour créer une fenêtre en bas (ou même n'importe où) de la fenêtre actuellement active. Cela a lui-même l'effet d'une fenêtre auxiliaire temporaire:

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

Vous pouvez jouer un peu avec l' actionargument de with-current-buffer-windowsi vous voulez que le popup apparaisse dans différentes parties de l'écran.

La fonction Quit est décrite dans doc-string de with-current-buffer-window, elle peut être utilisée pour obtenir des entrées de l'utilisateur. Comme l'a suggéré @tarsius, read-char-choiceest un bon candidat pour l'expérimentation.

Le tampon popup lui-même peut être généré comme n'importe quel autre tampon. Je pense toujours aux boutons, car l'utilisateur peut utiliser sa souris pour sélectionner un élément dans le menu contextuel, pas seulement le clavier. Cependant, cela nécessite des efforts supplémentaires si vous voulez bien le faire.

Si votre popup est un peu ad-hoc et que vous n'en aurez pas besoin plus d'une fois, vous pouvez vous en sortir avec cette solution. Jetez également un œil à completion--insert-strings. Je l'ai trouvé très utile comme exemple de la façon de générer de jolies lignes d'éléments de menu, vous pouvez même l'utiliser sans modification si vous n'avez pas besoin de quelque chose de spécial.


4
Je pense que la question que vous vous posez est peut-être bonne; la question que vous avez notée n'est pas claire, je ne vois pas comment quelqu'un aurait pu savoir que vous étiez après une réponse comme celle-ci.
npostavs

1

Voici une solution tierce, utilisant Icicles .

  • Votre code fait apparaître une fenêtre avec les candidats soigneusement organisés sous forme de menu. Pour savoir comment, voir ci-dessous.

  • Vous préfixez chaque élément de menu avec un caractère unique. Par exemple, a, b, c ... ou 1, 2, 3 ... L'utilisateur frappe le caractère pour choisir l'élément.

  • Vous liez quelques variables autour d'un appel à completing-read. Vous lui passez une liste de vos éléments de menu. Vous pouvez éventuellement spécifier l'un des éléments comme valeur par défaut, choisi si l'utilisateur vient de frapper RET.

    Les liaisons de variables indiquent completing-read:

    • Afficher chaque élément de menu sur une ligne distincte
    • Afficher le menu immédiatement
    • Mettez-le à jour immédiatement lorsqu'un utilisateur frappe une clé
    • Retournez immédiatement, si l'entrée utilisateur ne correspond qu'à un seul élément
    • Affichez le choix par défaut dans l'invite.
  • Vous pouvez rendre les éléments de menu aussi fantaisie que vous le souhaitez (non illustré).

    • visages
    • images
    • annotations
    • plusieurs lignes par article
    • mouse-3 menu contextuel, pour faire quoi que ce soit avec l'élément
(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)))

Vous pouvez également créer des menus à choix vraiment multiples , c'est-à-dire des menus où un utilisateur peut choisir plusieurs éléments en même temps (choisissez un sous-ensemble des choix possibles).


1

Vous pourriez également être intéressé à regarder le package makey. Il est destiné à fournir des fonctionnalités similaires à magit-popup, et il s'agit d'un fork du prédécesseur de magit-popup( magit-key-mode, mentionné dans le commentaire) à partir du moment où il n'était pas encore un package disponible séparément de magit.

Permettez-moi également de mentionner discover: c'est un exemple d'utilisation makey(et aussi sa raison d'être). Ce package est destiné à aider les nouveaux arrivants à découvrir les liaisons emacs.


Références


2
@Mark Je ne sais pas trop pourquoi vous avez accepté cette réponse. Il ne mentionne pas du tout de petits blocs de construction, ce qui, si je me souviens bien, est ce que vous recherchiez. Ce n'est pas non plus correct: makey n'est pas un fork de magit-popup, c'est un fork de son prédécesseur magit-key-mode. Il hérite de la plupart des déficits qui ont depuis été corrigés dans magit-popup. Il vaut donc mieux utiliser magit-popup ou hydra (ou écrire le vôtre).
tarsius

@tarsius Il peut être difficile de juger de l'exactitude d'une réponse. Si vous savez que ce n'est pas correct, n'hésitez pas à le modifier. J'ai fait un montage, je ne suis toujours pas sûr à 100% que c'est correct maintenant. Je n'ai pas non plus évoqué les "déficits" dont il hérite car je ne sais pas de quoi il s'agit.
YoungFrog


0

Basé sur la réponse de @ Drew (Icicles), avec des modifications pour séparer la mise en œuvre du menu des données de menu - de sorte que plusieurs menus pourraient être définis, chacun avec ses propres: (contenu, invite, par défaut).

Cela utilise facultativement le lierre (lorsqu'il est disponible), qui a des fonctionnalités plus avancées telles que la possibilité d'activer des éléments tout en gardant le menu ouvert.

Popup générique.

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

Exemple d'utilisation, attribuez quelques actions à la touche 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)
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.