Jak zaimplementować wyskakujące menu podobne do tego używanego w Magit

10

Pytanie

Chciałbym stworzyć interfejs użytkownika w postaci menu kontekstowego , menu kontekstowego podobnej do tej stosowanej w MAGIT .

funkcje

Definicja wyskakującego okienka

Wyskakujące okienko w kontekście tego pytania oznacza niewielkie tymczasowe okno, które zawiera zbiór elementów menu, dzięki czemu użytkownik może wybrać jeden i tylko jeden z tych elementów.

Pozycja na ekranie

Wyskakujące okienko może pojawiać się w dowolnej części ekranu, ale pożądane jest, aby było dość oczywiste i dlatego powinno się pojawiać obok aktualnie aktywnego okna.

Zawartość Popup Buffer

Przedmioty powinny być wyświetlane w formie ładnego stołu. Ładny jest kontekst pytania oznacza atrakcyjny wizualnie, efekt ten można najłatwiej osiągnąć, umieszczając elementy menu w prostych rzędach, patrz complete--insert-stringna przykład. Ten akapit służy dodatkowym wyjaśnieniom, możesz to zrobić na swój własny sposób, co nie spowoduje, że twoja odpowiedź będzie niepoprawna.

Wybór pozycji menu

Oczekuje się, że selekcja zostanie przeprowadzona przez naciśnięcie jednego klawisza lub, opcjonalnie, za pomocą myszy (chociaż nie jest to tak ważne, więc odpowiedzi zawierające propozycje, które nie obsługują myszy, są legalne). Jeśli zaproponujesz rozwiązanie, które obsługuje mysz, pamiętaj, że użytkownik powinien być w stanie wybrać element menu w intuicyjny sposób, to znaczy klikając lewym przyciskiem myszy żądany wybór.

Myszy NB można używać na wiele sposobów; mile widziane są również alternatywne sposoby wskazania wyboru.

Eliminacja wyskakującego okienka

Gdy użytkownik wybierze pozycję menu w sposób opisany powyżej, bufor, a tym samym jego okno, należy usunąć z widoku, a także zabić. Okno, które było aktywne przed wywołaniem menu podręcznego, powinno ponownie stać się aktywne (to znaczy stać się aktywne).

Zwrócona wartość i argumenty

Najlepiej, jeśli ta konsekwencja działań powinna spowodować zwrócenie obiektu Lisp. Obiektem Lisp może być:

  • nil- oznacza to, że użytkownik przerwał menu podręczne, naciskając C-glub w inny sposób †.

  • string- ciąg (dozwolone jest użycie symbolu) powinien być string-equal jednym z ciągów dostarczonych do menu podręcznego jako zbiór rzeczywistych elementów.

Dopuszczalne są alternatywne sposoby poinformowania reszty programu o wyborze użytkownika lub ewentualnie jego braku. Jeśli jednak nie jest jasne, jak inaczej można to zrobić, proszę wszystkich improwizujących o improwizację i nie proszę o dalsze wyjaśnienia tego aspektu.

To wszystko dla zwróconej wartości. Jeśli chodzi o parametry wejściowe, powinny one obejmować przynajmniej zbiór ciągów, które reprezentują możliwe wybory (tj. Pozycje menu).

Dopuszczalne odpowiedzi

Oczekiwana odpowiedź może mieć następujące formy:

  • Wystarczający fragment kodu, który pozwala wykształconemu czytelnikowi pisać funkcje takie jak opisano powyżej; napisanie całej funkcji roboczej nie jest oczekiwane ani konieczne. Aby jednak uniknąć niepewności (czy można pominąć znaczną część kodu?), Należy zauważyć, że brakujące części fragmentu kodu należy opisać w tekstowym składniku odpowiedzi.

  • Łącze do istniejącej biblioteki, która implementuje podobną funkcjonalność. Aby uniknąć niepewności, należy zauważyć, że podobnie w naszym przypadku oznacza, że ​​biblioteki można użyć do tworzenia wyskakujących okienek (patrz definicja powyżej), która ma co najmniej 2 lub 3 funkcje opisane powyżej. Jeśli proponowana biblioteka różni się od punktu, w którym nie można spełnić wcześniej określonego warunku, każdy taki przypadek będzie oceniany niezależnie i zawsze będzie oceniany, jeśli OP uzna to za przydatne.

  • Opis wbudowanych funkcji Emacsa lub innych firm, których można użyć do zaimplementowania dowolnej funkcji opisanej w sekcji «Funkcje», patrz wyżej. Niepewności uniknąć należy podać wyraźnie, jak Twoja odpowiedź może być przydatne dla przyszłych czytelników, którzy chcą zaimplementować popup , menu podręczne podobną do tej stosowanej w MAGIT .


† Alternatywne sposoby przerwania menu podręcznego mogą obejmować następujące (ale nie tylko):

  • kliknięcie poza wyskakującym oknem menu;

  • zabicie bufora zawierającego wyskakujące okienko bez dokonywania wyboru.

Mark Karpov
źródło

Odpowiedzi:

8

magit-popupma własną instrukcję ! Ale chyba, że ​​faktycznie chcesz ustawić argumenty w wyskakującym okienku, które są następnie przekazywane do wywołanej akcji, prawdopodobnie lepiej jest użyć hydrazamiast tego.

Zauważ też, że nie zamierzam dalej się rozwijać magit-popup. Zamiast tego napiszę podobny pakiet od zera. Obecne wdrożenie jest po prostu zbyt przypadkowe. (Ale to nie znaczy, że magit-popupto po prostu zniknie. Prawdopodobnie nie zobaczy wielu nowych funkcji, ale jeśli bieżąca implementacja robi to, co chcesz, to dlaczego jej nie użyć?)

Lub, ponieważ ty też chcesz to zrobić „od zera”, spójrz read-char-choicei stamtąd. Aby zaimplementować część wizualną, zobacz, lvktóra część jest repozytorium hydry.

Tarsjusz
źródło
Dla innych czytelników: Zauważ, że @tarsius przeszedł i napisał ten zamiennik magit-popup. Wywoływany jest nowy pakiet transient, który jest używany w aktualnych wersjach magit. Zobacz magit.vc/manual/transient dla dokumentacji.
phils
6

Hydry są dość proste do napisania:

(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 to interfejs zorientowany na klawiaturę, aw swojej podstawowej formie nie jest trudniejszy niż easy-menu-define(wbudowany). Jest to dość rozszerzalne, jeśli chcesz sprawić, by robiło bardziej złożone rzeczy.

Wystarczy spojrzeć na ten interfejs Twittera, dolne okno to niestandardowa Hydra, niewiele trudniejsza do napisania niż powyższa:

hydra-twittering.png

Kod tego jest dostępny na wiki wraz z wieloma innymi przykładami.

abo-abo
źródło
Świetnie, na pewno znajdę czas, aby dowiedzieć się więcej na ten temat!
Mark Karpov
1

Po kilku badaniach znalazłem idiom, którego można użyć do utworzenia okna na dole (lub w dowolnym miejscu) aktualnie aktywnego okna. To samo ma wpływ na tymczasowe okno pomocnicze:

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

Możesz zagrać trochę z actionargumentem, with-current-buffer-windowczy chcesz, aby wyskakujące okienko pojawiało się w innej części ekranu.

Funkcja Quit jest opisana w dokumencie doc-string with-current-buffer-window, można jej użyć do uzyskania danych wejściowych od użytkownika. Jak sugerował @tarsius, read-char-choicejest dobrym kandydatem do eksperymentowania.

Sam bufor wyskakujący może być generowany tak jak każdy inny bufor. Nadal myślę o przyciskach, ponieważ użytkownik może użyć myszy, aby wybrać element w menu podręcznym, a nie tylko klawiaturę. Wymaga to jednak dodatkowego wysiłku, jeśli chcesz to zrobić dobrze.

Jeśli twoje wyskakujące okienko jest nieco ad-hoc i nie będziesz go potrzebować więcej niż raz, możesz uciec od tego rozwiązania. Zobacz także completion--insert-strings. Uznałem, że jest to całkiem przydatne jako przykład generowania ładnych rzędów pozycji menu, możesz nawet używać go bez zmian, jeśli nie potrzebujesz czegoś specjalnego.

Mark Karpov
źródło
4
Myślę, że może pytanie, które masz w głowie, jest dobre; pytanie, które zapisałeś, jest raczej niejasne, nie rozumiem, jak ktokolwiek mógł wiedzieć, że jesteś po takiej odpowiedzi.
npostavs
1

Oto rozwiązanie innej firmy, wykorzystujące Icicles .

  • Twój kod wyskakuje w oknie z kandydatami uporządkowanymi jako menu. Jak to zrobić, patrz poniżej.

  • Każdą pozycję menu poprzedza się unikalnym znakiem. Np. A, b, c ... lub 1, 2, 3 ... Użytkownik naciska znak, aby wybrać element.

  • Wiążesz kilka zmiennych wokół wywołania do completing-read. Przekazujesz mu listę swoich pozycji menu. Opcjonalnie możesz określić jeden z elementów jako domyślny, wybrany, jeśli użytkownik tylko trafi RET.

    Powiązania zmienne nakazują completing-read:

    • Pokaż każdą pozycję menu w osobnym rzędzie
    • Pokaż menu od razu
    • Zaktualizuj go natychmiast, gdy użytkownik naciśnie klawisz
    • Wróć natychmiast, jeśli dane wejściowe użytkownika odpowiadają tylko jednemu elementowi
    • Pokaż domyślny wybór w wierszu polecenia.
  • Możesz sprawić, by elementy menu były tak fantazyjne, jak chcesz (nie pokazano).

    • twarze
    • obrazy
    • adnotacje
    • wiele wierszy na element
    • mouse-3 menu podręczne, aby zrobić cokolwiek z przedmiotem
(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)))

Możesz także stworzyć naprawdę wielokrotne menu , to znaczy menu, w których użytkownik może wybrać wiele elementów jednocześnie (wybierz podzbiór możliwych wyborów).

Rysował
źródło
1

Możesz być także zainteresowany spojrzeniem na pakiet makey. Ma on zapewniać podobną funkcjonalność jak magit-popupi jest rozwidleniem poprzednika magit-popup( magit-key-modewspomnianego w komentarzu) z czasów, gdy nie był to jeszcze pakiet dostępny osobno magit.

Pozwól mi także wspomnieć discover: to jest przykład użycia makey(a także jego racji bytu). Ten pakiet ma pomóc początkującym odkryć wiązania emacs.


Bibliografia

YoungFrog
źródło
2
@ Mark Nie jestem pewien, dlaczego zaakceptowałeś tę odpowiedź. W ogóle nie wspomina o małych cegiełkach, które, o ile dobrze pamiętam, są tym, o co ci chodziło. To także nie jest poprawne: makey nie jest widelcem wyskakującego okienka, jest rozwidleniem swojego poprzedniego trybu magit-key. Dziedziczy większość braków, które zostały usunięte w wyskakującym oknie magit. Lepiej więc jest używać magit-popup lub hydry (lub pisać własne).
tarsjusz
@tarsius Ocena poprawności odpowiedzi może być trudna. Jeśli wiesz, że to nie jest poprawne, możesz je edytować. Zrobiłem edycję, wciąż nie jestem w 100% pewien, że jest teraz poprawny. Nie wspomniałem również o „deficytach”, które dziedziczy, ponieważ nie wiem, czym one są.
YoungFrog,
0

Guide-Key działa miło dla wszystkich skonfigurowanych prefiksów.

Netsu
źródło
0

Na podstawie odpowiedzi @ Drew (Icicles), z modyfikacjami oddzielającymi implementację menu od danych menu - można więc zdefiniować wiele menu, każde z własnym: (treść, monit, domyślnie).

Opcjonalnie korzysta z bluszczu (jeśli jest dostępny), który ma bardziej zaawansowane funkcje, takie jak możliwość aktywacji przedmiotów przy zachowaniu otwartego menu.

Ogólne wyskakujące okienko.

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

Przykład użycia, przypisz niektóre akcje do klawisza 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
źródło