Как реализовать всплывающее меню, подобное тому, которое используется в Magit

10

Вопрос

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

особенности

Определение всплывающих окон

Всплывающее окно в контексте этого вопроса означает небольшое временное окно, которое содержит коллекцию пунктов меню, чтобы пользователь мог выбрать один и только один из этих элементов.

Положение на экране

Допускается показ всплывающего окна в любой части экрана, но желательно, чтобы оно было достаточно очевидным, и поэтому оно должно отображаться рядом с текущим активным окном.

Содержимое Popup Buffer

Элементы должны отображаться в виде красивой таблицы. Pretty - контекст вопроса означает визуально привлекательный, этого эффекта легче всего достичь, поместив пункты меню в прямые строки, см., complete--insert-stringНапример. Этот параграф служит для дополнительного разъяснения, вы можете сделать это по-своему, это не сделает ваш ответ неверным.

Выбор пункта меню

Ожидается, что выбор будет выполнен одним нажатием клавиши или, возможно, мышью (хотя это не так важно, поэтому ответы, содержащие предложения, которые не поддерживают мышь, являются законными). Если вы предлагаете решение, которое поддерживает мышь, обратите внимание, что пользователь должен иметь возможность выбирать пункт меню интуитивно понятным способом, то есть, щелкая левой кнопкой мыши по желаемому выбору.

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

Устранение Popup

Как только пользователь выбрал пункт меню способом, описанным выше, буфер и, следовательно, его окно должны быть исключены из вида, а также уничтожены. Окно, которое было активным до вызова всплывающего меню, должно снова получить фокус (то есть стать активным).

Возвращаемое значение и аргументы

Предпочтительно, это следствие действий должно привести к возвращению объекта Lisp. Объект Лиспа может быть:

  • nil- это означает, что пользователь отменил всплывающее меню, нажав C-gили каким-либо другим способом †.

  • string- строка (разрешено использовать символ) должна указывать string-equal на одну из строк, представленных во всплывающем меню, как набор фактических элементов.

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

Это все для возвращаемого значения. Что касается входных параметров, они должны по крайней мере включать коллекцию строк, которые представляют возможные варианты выбора (то есть пункты меню).

Приемлемые ответы

Ожидаемый ответ может иметь следующие формы:

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

  • Ссылка на существующую библиотеку, которая реализует аналогичные функции. Чтобы избежать неопределенности, я должен отметить, что подобное в нашем случае означает, что библиотека может использоваться для создания всплывающего окна (см. Определение выше), которое имеет как минимум 2 или 3 функции, описанные выше. Если предлагаемая библиотека отличается от точки, в которой ранее указанное условие не может быть выполнено, каждый такой случай будет оцениваться независимо и всегда будет поддерживаться, если OP сочтет это полезным.

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


† Альтернативные способы отмены всплывающего меню могут включать следующее (но не ограничиваясь этим):

  • щелкнув за пределами окна всплывающего меню;

  • уничтожение буфера, содержащего всплывающее окно, без выбора.

Марк Карпов
источник

Ответы:

8

magit-popupимеет собственное руководство ! Но если вы на самом деле не хотите устанавливать во всплывающем окне аргументы, которые затем передаются вызываемому действию, вам, вероятно, лучше использовать hydraвместо этого.

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

Или, так как вы тоже хотите сделать это "с нуля", посмотрите read-char-choiceи оттуда. Для реализации визуальной части взгляните, lvкакая часть является хранилищем гидры.

Tarsius
источник
Для других читателей: обратите внимание, что @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если вы хотите, чтобы всплывающее окно появлялось в другой части экрана.

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

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

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

Марк Карпов
источник
4
Я думаю, что, возможно, вопрос, который у вас есть в голове, хорош; записанный вами вопрос довольно неясен, я не понимаю, как кто-то мог знать, что вы были после такого ответа.
npostavs
1

Вот стороннее решение, использующее Сосульки .

  • Ваш код всплывает в окне с кандидатами, аккуратно оформленными в виде меню. Как это сделать, смотрите ниже.

  • Вы ставите перед каждым пунктом меню уникальный символ. Например, a, b, c ... или 1, 2, 3 ... Пользователь нажимает на символ, чтобы выбрать элемент.

  • Вы связываете несколько переменных вокруг вызова 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(а также его смысл). Этот пакет предназначен, чтобы помочь новичкам обнаружить привязки Emacs.


Ссылки

YoungFrog
источник
2
@ Марк Я не совсем уверен, почему вы приняли этот ответ. В нем вообще не упоминаются маленькие строительные блоки, которые, если я правильно помню, это то, что вы были после. Это также не правильно: makey - это не форк magit-popup, это форк его предшествующего режима magit-key-mode. Он наследует большинство недостатков, которые были устранены в magit-popup. Таким образом, вы гораздо лучше используете magit-popup или hydra (или пишете свои).
Tarsius
@tarsius Оценить правильность ответа сложно. Если вы знаете, что это неправильно, не стесняйтесь редактировать его. Я сделал редактирование, я все еще не уверен на 100%, что сейчас это правильно. Я также не упомянул «дефицит», который он наследует, потому что я не знаю, что это такое.
YoungFrog
0

На основе ответа @ Drew (Icicles), с изменениями, позволяющими отделить реализацию меню от данных меню, чтобы можно было определить несколько меню, каждое со своим собственным: (content, prompt, default).

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

Общий всплывающее окно.

(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)
ideasman42
источник