Як реалізувати спливаюче меню, схоже на те, яке використовується у Magit


10

Питання

Я б хотів створити інтерфейс користувача у вигляді спливаючого меню , спливаючого меню, подібного до використовуваного в Magit .

Особливості

Визначення спливаючого вікна

Спливаюче вікно в контексті цього питання означає невелике тимчасове вікно, яке містить колекцію пунктів меню, щоб користувач міг вибрати один і лише один із цих пунктів.

Позиція на екрані

Спливаюче вікно дозволяється відображати в будь-якій частині екрана, але бажано, щоб воно було досить очевидним, і, таким чином, воно повинно з’являтися біля активного вікна.

Зміст буфера Popup

Елементи повинні бути відображені у вигляді симпатичної таблиці. Досить те , що контекст питання означає візуально привабливий, цього ефекту можна найпростіше досягти, розміщуючи елементи меню в прямі рядки, див complete--insert-string. Цей пункт служить для додаткового роз'яснення, ви можете це зробити по-своєму, це не зробить вашу відповідь неправильною.

Вибір пункту меню

Очікується, що вибір буде здійснено одним натисканням клавіші або, за бажанням, мишкою (хоча це не так важливо, тому відповіді, що містять пропозиції, які не підтримують мишу, є законними). Якщо ви пропонуєте рішення, яке підтримує мишу, зауважте, що користувач повинен мати можливість інтуїтивно вибрати пункт меню, тобто клацанням лівою кнопкою на потрібному виборі.

NB миша може бути використана багатьма способами, і альтернативні способи вказувати на вибір також вітаються.

Ліквідація спливаючих вікон

Після того як користувач обрав пункт меню описаним вище способом, буфер і, таким чином, його вікно повинні бути видалені з виду, а також знищені. Вікно, яке було активним до виклику спливаючого меню, має знову зосередити увагу (тобто стати активним).

Повернене значення та аргументи

Переважно, цей наслідок дій повинен призводити до повернення об'єкта Lisp. Об'єктом Lisp може бути:

  • nil- це вказує на те, що користувач скасував спливаюче меню або натисканням, C-gабо іншим способом †.

  • string- рядок (дозволяється використовувати символ) повинен містити string-equal один із рядків, що надходять до спливаючого меню як колекцію фактичних елементів.

Альтернативні способи повідомити решту програми про вибір користувача, або, можливо, про його відсутність, є прийнятними. Однак, якщо незрозуміло, як це ще можна зробити, я прошу всіх відповідачів імпровізувати і не вимагати від мене подальшого роз'яснення цього аспекту.

Це все для повернутого значення. Що стосується вхідних параметрів, вони повинні принаймні включати колекцію рядків, які представляють можливий вибір (тобто пункти меню).

Прийнятні відповіді

Очікувана відповідь може мати такі форми:

  • Достатній фрагмент коду, який дозволяє освіченому читачеві записувати таку функцію, як описана вище; не передбачається і не потрібно писати всю робочу функцію. Однак, щоб уникнути невизначеності (чи можуть бути пропущені значні частини коду?), Слід зазначити, що відсутні фрагменти фрагмента повинні бути описані в текстовій складовій відповіді.

  • Посилання на існуючу бібліотеку, яка реалізує аналогічні функції. Щоб уникнути невизначеності, слід зазначити, що подібне в нашому випадку означає, що бібліотеку можна використовувати для створення спливаючого вікна (див. Визначення вище), який має щонайменше 2 або 3 функції, описані вище. Якщо запропонована бібліотека відрізняється від того, що раніше не може бути виконано заявлену умову, кожен такий випадок буде розглядатися незалежно і завжди буде апробовано, якщо ОП вважає це корисним.

  • Опис вбудованих функцій Emacs або сторонніх функцій, які можна використовувати для реалізації будь-якої функції, описаної в розділі «Особливості», див. Вище. Щоб уникнути невизначеності, будь ласка, чітко вкажіть, як ваша відповідь може бути корисною для майбутніх читачів, які хочуть реалізувати спливаюче , спливаюче меню, подібне до того, яке використовується у Magit .


† Альтернативні способи скасування спливаючого меню можуть включати такі (але не обмежуючись цим):

  • клацання поза вікном спливаючого меню;

  • вбивство буфера, що містить спливаюче вікно без вибору.

Відповіді:


8

magit-popupмає власну інструкцію ! Але якщо ви дійсно не хочете встановити аргументи у спливаючому віці, які потім передаються викликаній дії, вам, ймовірно, краще скористатися hydraнатомість.

Також зауважте, що я не збираюся надалі розвиватися magit-popup. Натомість я напишу подібний пакет з нуля. У нинішній реалізації є просто багато випадкових складностей. (Але це не означає, що magit-popupце просто зникне. Він, ймовірно, не побачить багато нових функцій, але якщо поточна реалізація робить те, що ви хочете, то чому б не використовувати її?)

Або оскільки ви теж хочете це зробити "з нуля", погляньте read-char-choiceі вирушайте звідти. Для реалізації візуальної частини погляньте, lvяка є частиною сховища гідри.


Для інших читачів: Зауважте, що @tarsius дотримувався та писав цю заміну для magit-popup. Новий пакет називається transient, і саме це використовується в сучасних версіях magit. Див. Документацію на magit.vc/manual/transient .
філз

5

Писати гідри досить просто:

(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 - це інтерфейс, орієнтований на клавіатуру, і в своїй базовій формі це не складніше, ніж easy-menu-define(вбудований). І це досить розширюється, якщо ви хочете змусити його робити більш складні речі.

Подивіться на цей щебетаючий інтерфейс, нижнє вікно - це власна гідра, не набагато складніше написати, ніж це вище:

hydra-twittering.png

Код для цього доступний на вікі , разом із набагато більше прикладів.


Чудово, я обов'язково знайду час, щоб дізнатися більше про цю річ!
Марк Карпов

1

Після деяких досліджень я виявив ідіому, яку можна використати для створення вікна внизу (або взагалі будь-де) активного вікна. Це саме по собі має ефект тимчасового, допоміжного вікна:

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

Ви можете трохи пограти з actionаргументом, with-current-buffer-windowякщо ви хочете, щоб спливаюче вікно з’являлося в іншій частині екрана.

Функція виходу описана в doc-string of with-current-buffer-window, вона може використовуватися для отримання даних від користувача. Як запропонував @tarsius, read-char-choiceце хороший кандидат для експериментів.

Сам буфер спливаючих вікон може бути створений так само, як і будь-який інший буфер. Я все ще думаю про кнопки там, тому що користувач міг використовувати мишу, щоб вибрати пункт у спливаючому меню, а не лише клавіатуру. Однак для цього потрібні додаткові зусилля, якщо ви хочете зробити це добре.

Якщо ваше спливаюче вікно трохи спеціальне, і вам це буде потрібно не один раз, ви можете піти з цього рішення. Також погляньте completion--insert-strings. Я вважаю це досить корисним як приклад того, як генерувати гарні рядки елементів меню, ви навіть можете використовувати його без змін, якщо вам не потрібно щось особливе.


4
Я думаю, можливо, питання, яке у вас в голові, добре; запитання, яке ви записали, є досить незрозумілим, я не бачу, як хто-небудь міг знати, що ви були після такої відповіді.
npostavs

1

Ось стороннє рішення з використанням Icicles .

  • Ваш код спливає вікно з кандидатами, розташованими акуратно як меню. Про те, як дивіться нижче.

  • Ви додасте до кожного елемента меню унікальний символ. Наприклад, a, b, c ... або 1, 2, 3 ... Користувач натискає на char, щоб вибрати предмет.

  • Ви зв'язуєте кілька змінних навколо дзвінка completing-read. Ви передаєте йому список елементів меню. Ви необов'язково вказуєте один із елементів, який буде за замовчуванням, вибраний, якщо користувач просто натискає RET.

    Змінні прив'язки повідомляють completing-read:

    • Показати кожен пункт меню в окремому рядку
    • Покажіть меню негайно
    • Оновіть його негайно, коли користувач натисне клавішу
    • Поверніться негайно, якщо введення користувача відповідає лише одному елементу
    • Покажіть вибір за замовчуванням у підказці.
  • Ви можете зробити пункти меню настільки вигадливими, як хочете (не показано).

    • обличчя
    • образи
    • анотації
    • кілька рядків на предмет
    • mouse-3 спливаюче меню, щоб зробити щось із цим пунктом
(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)))

Ви також можете створити по - справжньому меню з множинним вибором , тобто меню, де користувач може одночасно вибрати декілька елементів (вибрати підмножину можливих варіантів).


1

Можливо, вам також буде цікаво подивитися пакет makey. Він призначений для надання подібних функціональних можливостей magit-popup, і це вилка попередника magit-popup( magit-key-modeзгаданого в коментарі) від того часу, коли він ще не був пакетом, доступним окремо від magit.

Дозвольте також зазначити discover: це приклад того, як користуватися makey(а також його raison d'être). Цей пакет призначений для того, щоб допомогти новачкам відкрити прив’язки emacs.


Список літератури


2
@ Марк Я не зовсім впевнений, чому ви прийняли цю відповідь. Це зовсім не згадує про невеликі будівельні блоки, які, якщо я правильно пам’ятаю, це те, що ви були після. Це також не правильно: makey не є форкою magit-popup, це вилка попереднього режиму magit-key. Він успадковує більшість дефіцитів, які з тих пір були вирішені в magit-popup. Тож вам набагато краще використовувати magit-popup або hydra (або писати власну).
Тарсій

@tarsius Судити про правильність відповіді може бути важко. Якщо ви знаєте, що це неправильно, сміливо редагуйте це. Я зробив правки, я все ще не на 100% впевнений, що це правильно. Я також не згадував про "дефіцит", який він успадковує, бо не знаю, що вони.
YoungFrog


0

На основі відповіді @ Drew (Icicles), з модифікаціями, щоб відокремити реалізацію меню від даних меню - так можна було визначити декілька меню, кожне зі своїх: (зміст, підказка, за замовчуванням).

Тут необов'язково використовується плющ (якщо є), який має більш вдосконалені функції, такі як можливість активувати елементи, зберігаючи меню відкритим.

Родове спливаюче вікно.

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

Приклад використання, призначте кілька дій клавіші 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)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.