Como implementar o menu pop-up semelhante ao usado no Magit

10

Questão

Gostaria de criar uma interface de usuário na forma de menu pop-up , menu pop-up semelhante ao usado no Magit .

Características

Definição de pop-up

O pop-up no contexto desta pergunta significa uma pequena janela temporária que contém uma coleção de itens de menu para que o usuário possa selecionar um e apenas um desses itens.

Posição na tela

É permitido que o pop-up apareça em qualquer parte da tela, mas é desejável que seja bastante óbvio e, portanto, apareça ao lado da janela ativa no momento.

Conteúdo do buffer pop-up

Os itens devem ser exibidos em forma de tabela bonita. Pretty é contexto da pergunta significa visualmente atraente, esse efeito pode ser alcançado mais facilmente colocando itens de menu em linhas retas, veja, complete--insert-stringpor exemplo. Este parágrafo serve para esclarecimentos adicionais. Você pode fazê-lo à sua maneira. Isso não tornará sua resposta incorreta.

Seleção do item de menu

Espera-se que a seleção seja realizada pressionando uma única tecla ou, opcionalmente, com um mouse (embora não seja tão importante, portanto, as respostas que contêm proposições que não suportam o mouse são legais). Se você propõe uma solução que suporta mouse, observe que o usuário deve poder selecionar um item de menu de maneira intuitiva, ou seja, clicando com o botão esquerdo na opção desejada.

O mouse NB pode ser usado de várias maneiras e maneiras alternativas para indicar uma escolha também são bem-vindas.

Eliminação de pop-up

Depois que o usuário seleciona um item de menu da maneira descrita acima, o buffer e, portanto, sua janela devem ser eliminados da visualização e eliminados. A janela que estava ativa antes da chamada do menu pop-up deve obter o foco (ou seja, tornar-se ativo) novamente.

Valor retornado e argumentos

De preferência, essa consequência de ações deve resultar em um objeto Lisp retornado. O objeto Lisp pode ser:

  • nil- isso indica que o usuário cancelou o menu pop-up pressionando C-gou de alguma outra maneira †.

  • string- string (é permitido usar um símbolo) deve ser string-equal uma das strings fornecidas no menu pop-up como coleção de itens reais.

Formas alternativas de informar o restante do programa sobre a escolha do usuário ou, possivelmente, sua ausência, são aceitáveis. No entanto, se não estiver claro de que outra forma isso pode ser executado, peço a todos os respondentes que improvisem e não me peçam mais esclarecimentos sobre esse aspecto.

Isso é tudo para o valor retornado. Quanto aos parâmetros de entrada, eles devem incluir pelo menos a coleção de strings que representam possíveis opções (ou seja, itens de menu).

Respostas Aceitáveis

A resposta esperada pode ter as seguintes formas:

  • Fragmento de código suficiente que permite que o leitor instruído escreva funções como as descritas acima; não é esperado ou necessário escrever toda a função de trabalho. No entanto, para evitar incertezas (partes consideráveis ​​do código podem ser omitidas?), Devo observar que partes ausentes do trecho devem ser descritas no componente textual da resposta.

  • Um link para a biblioteca existente que implementa funcionalidade semelhante. Para evitar incertezas, devo observar que semelhante em nosso caso significa que a biblioteca pode ser usada para criar pop-up (veja a definição acima) que possui pelo menos 2 ou 3 recursos descritos acima. Se a biblioteca proposta for diferente do ponto em que a condição declarada anteriormente não puder ser atendida, cada um desses casos será julgado de forma independente e sempre será votado se o OP considerar útil.

  • Descrição das funções internas do Emacs ou de terceiros que podem ser usadas para implementar qualquer recurso descrito na seção «Recursos», veja acima. Para evitar incertezas, indique claramente como sua resposta pode ser útil para futuros leitores que desejam implementar pop-up , menu pop-up semelhante ao usado no Magit .


† Formas alternativas de abortar o menu pop-up podem incluir o seguinte (mas não limitado a estes):

  • clicar fora da janela do menu pop-up;

  • morte de buffer contendo o pop-up sem fazer uma escolha.

Mark Karpov
fonte

Respostas:

8

magit-popuptem seu próprio manual ! Mas, a menos que você realmente deseje definir argumentos no pop-up que são passados ​​para a ação invocada, provavelmente será melhor usá-lo hydra.

Observe também que não pretendo continuar desenvolvendo magit-popup. Em vez disso, vou escrever um pacote semelhante do zero. Existe muita complexidade acidental na implementação atual. (Mas isso não significa que magit-popupsimplesmente desaparecerá. Provavelmente não haverá muitos recursos novos, mas se a implementação atual fizer o que você deseja, por que não usá-la?)

Ou já que você também deseja fazê-lo "do zero", dê uma olhada read-char-choicee vá a partir daí. Para implementar a parte visual, dê uma olhada no lvque faz parte do repositório hydra.

tarso
fonte
Para outros leitores: Observe que @tarsius seguiu e escreveu esse substituto para magit-popup. O novo pacote é chamado transiente é isso que é usado nas versões atuais do magit. Veja magit.vc/manual/transient para documentação.
phils
5

Hydras são bem simples de escrever:

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

O Hydra é uma interface centralizada no teclado e, em sua forma básica, não é mais difícil do que easy-menu-define(embutida). E é bastante extensível se você quiser fazer coisas mais complexas.

Basta olhar para essa interface do twitter, a janela inferior é uma Hydra personalizada, não muito mais difícil de escrever do que a acima:

hydra-twittering.png

O código para isso está disponível no wiki , junto com muitos outros exemplos.

abo-abo
fonte
Ótimo, certamente encontrarei tempo para aprender mais sobre isso!
Mark Karpov
1

Após algumas pesquisas, encontrei um idioma que pode ser usado para criar uma janela na parte inferior (ou mesmo em qualquer lugar) da janela atualmente ativa. Isso por si só tem efeito de janela auxiliar temporária:

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

Você pode brincar um pouco com o actionargumento de with-current-buffer-windowse deseja que o pop-up apareça em diferentes partes da tela.

A função Sair é descrita na sequência de documentos de with-current-buffer-window, pode ser usada para obter informações do usuário. Como o @tarsius sugeriu, read-char-choiceé um bom candidato para experimentar.

O próprio buffer pop-up pode ser gerado como qualquer outro buffer. Ainda estou pensando nos botões, porque o usuário pode usar o mouse para selecionar um item no menu pop-up, não apenas o teclado. No entanto, isso requer um esforço adicional se você quiser fazê-lo bem.

Se o seu pop-up for um pouco ad-hoc e você não precisar dele mais de uma vez, poderá sair com esta solução. Além disso, dê uma olhada completion--insert-strings. Achei bastante útil como um exemplo de como gerar linhas bonitas de itens de menu, você pode até usá-lo inalterado se não precisar de algo especial.

Mark Karpov
fonte
4
Eu acho que talvez a pergunta que você tenha em sua cabeça seja boa; a pergunta que você escreveu não é clara, não vejo como alguém poderia saber que você estava depois de uma resposta como essa.
npostavs 27/07/2015
1

Aqui está uma solução de terceiros, usando Sincelos .

  • Seu código abre uma janela com os candidatos organizados ordenadamente como um menu. Para saber como, veja abaixo.

  • Você prefixa cada item de menu com um caractere exclusivo. Por exemplo, a, b, c ... ou 1, 2, 3 ... O usuário pressiona o caractere para escolher o item.

  • Você vincula algumas variáveis ​​ao redor de uma chamada completing-read. Você passa uma lista dos itens de menu. Opcionalmente, você especifica um dos itens como o padrão, escolhido se o usuário clicar RET.

    As ligações de variáveis ​​informam completing-readpara:

    • Mostrar cada item de menu em uma linha separada
    • Mostrar o menu imediatamente
    • Atualize-o imediatamente quando um usuário pressionar uma tecla
    • Retorne imediatamente, se a entrada do usuário corresponder apenas a um item
    • Mostre a opção padrão no prompt.
  • Você pode deixar os itens do menu tão sofisticados quanto desejar (não mostrado).

    • rostos
    • imagens
    • anotações
    • várias linhas por item
    • mouse-3 menu pop-up, para fazer qualquer coisa com o 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)))

Você também pode criar menus verdadeiramente de múltipla escolha , ou seja, menus em que um usuário pode escolher vários itens ao mesmo tempo (escolha um subconjunto das opções possíveis).

Desenhou
fonte
1

Você também pode estar interessado em olhar para o pacote makey. Ele tem o objetivo de fornecer funcionalidades semelhantes às magit-popupe é uma bifurcação do predecessor de magit-popup( magit-key-mode, mencionado no comentário) de quando ainda não era um pacote disponível separadamente magit.

Deixe-me mencionar também discover: esse é um exemplo de como usar makey(e também a sua razão de ser). Esse pacote visa ajudar os recém-chegados a descobrir as ligações do emacs.


Referências

YoungFrog
fonte
2
@ Mark Não sei ao certo por que você aceitou esta resposta. Ele não menciona pequenos blocos de construção que, se bem me lembro, é o que você buscava. Também não está correto: makey não é um fork do magit-popup, é um fork do seu antecessor magit-key-mode. Ele herda a maioria dos déficits que foram tratados no magit-popup. Então você é muito melhor usando magit-popup ou hydra (ou escrevendo o seu próprio).
tarsius
@ tarso Julgar a exatidão de uma resposta pode ser difícil. Se você souber que não está correto, fique à vontade para editá-lo. Fiz uma edição, ainda não tenho 100% de certeza de que está correta agora. Também não mencionei os "déficits" herdados porque não sei o que são.
YoungFrog
0

O Guide-Key funciona bem para mim em todos os prefixos configurados.

Netsu
fonte
0

Com base na resposta de @ Drew (Sincelos), com modificações para separar a implementação do menu dos dados do menu - para que vários menus possam ser definidos, cada um com o seu: (conteúdo, prompt, padrão).

Opcionalmente, ele usa o ivy (quando disponível), que possui recursos mais avançados, como poder ativar itens enquanto mantém o menu aberto.

Pop-up 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))))))

Exemplo de uso, atribua algumas ações à 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
fonte