如何实现类似于Magit中使用的弹出菜单


10

我想以弹出菜单的形式创建用户界面,该弹出菜单类似于Magit中使用的弹出菜单

特征

弹出窗口的定义

在此问题的上下文中,弹出窗口意味着很少有包含菜单项集合的临时窗口,以便用户可以选择其中一项,并且只能选择其中一项。

在屏幕上的位置

允许弹出窗口出现在屏幕的任何部分,但是希望它应该非常明显,因此应该出现在当前活动的窗口旁边。

弹出缓冲区的内容

项目应以漂亮的表格形式显示。问题的上下文很漂亮,这意味着视觉上很吸引人,可以通过将菜单项置于直行中来轻松实现此效果complete--insert-string,例如参见 。本段用于进一步说明,您可以按照自己的方式进行操作,这不会使您的答案不正确。

菜单项的选择

选择可以通过一次按键或使用鼠标(尽管不是很重要,因此包含不支持鼠标的命题的答案是合法的)来执行。如果您提出支持鼠标的解决方案,请注意,用户应该能够以直观的方式选择菜单项,即,通过左键单击所需的选项。

NB鼠标可以以多种方式使用,也欢迎使用其他方式表示选择。

消除弹出窗口

一旦用户以上述方式选择了菜单项,就应从缓冲区中消除缓冲区并因此将其窗口从视图中清除并杀死。调用弹出菜单之前已处于活动状态的窗口应再次获得焦点(即变为活动状态)。

返回值和参数

优选地,动作的这种结果应该导致返回Lisp对象。Lisp对象可以是:

  • nil—这表示用户已通过按C-g或以其他方式† 终止了弹出菜单。

  • string—字符串(允许使用符号)应string-equal 位于作为实际项目集合提供给弹出菜单的字符串之一上。

让程序的其余部分知道用户的选择,或者可能没有用户选择的替代方法是可以接受的。但是,如果不清楚如何执行其他操作,请立即让所有答题者即兴回答,也不要要求我进一步说明这方面。

这都是为了返回值。至于输入参数,它们至少应包括代表可能选择(即菜单项)的字符串集合。

可接受的答案

预期答案可以采用以下形式:

  • 足够的代码段,使受过教育的读者可以编写如上所述的功能;不需要或不需要编写整个工作功能。但是,为了避免不确定性(可以省略大量代码吗?),我应该注意,应在答案的文本部分中描述代码段的缺失部分。

  • 指向实现类似功能的现有库的链接。为避免不确定性,我应该注意,在我们的情况下,类似意味着该库可用于创建具有至少2个或3个上述功能的弹出窗口(请参见上面的定义)。如果提议的库与无法满足先前陈述的条件不同,则将对每个此类情况进行独立判断,并且如果OP认为有用,将始终予以驳回。

  • 内置Emacs功能或第三方功能的描述,这些功能可用于实现“功能”部分中描述的任何功能,请参见上文。为了避免不确定性,请注明你的答案可以很清楚如何是谁想要实现未来的读者有用的弹出类似于Magit使用弹出式菜单


†中止弹出菜单的替代方法可能包括以下几种(但不仅限于这些):

  • 在弹出菜单窗口之外单击;

  • 杀死包含弹出窗口的缓冲区而没有做出选择。

Answers:


8

magit-popup自己的手册!但是,除非您实际上想在弹出窗口中设置参数,然后将其传递给调用的操作,否则最好使用hydra代替。

另请注意,我无意进一步发展magit-popup。相反,我将从头开始编写类似的程序包。当前实施中只是偶然的复杂性。(但这并不意味着它magit-popup会消失。它可能不会看到很多新功能,但是如果当前的实现可以实现您想要的功能,那么为什么不使用它呢?)

或者,由于您也想“从头开始”进行操作,因此请看一看read-char-choice并从那里开始。要实现可视部分,请看一下lvhydra存储库中的哪一部分。


对于其他读者:请注意,@ 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(内置)难。如果您想使其做更复杂的事情,它是相当可扩展的。

只需看一下这个twittering界面,底部的窗口是自定义的Hydra,编写起来并不比上面的难得多:

hydra-twittering.png

Wiki上提供了此代码以及更多示例。


太好了,我当然会抽出时间来学习更多有关此内容的信息!
Mark Karpov 2015年

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

这是使用Icicles的第三方解决方案。

  • 您的代码会弹出一个窗口,其中候选者的排列整齐地作为菜单。有关方法,请参见下文。

  • 您为每个菜单项添加一个唯一字符。例如,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-popupmagit-key-mode尚未在注释中提到的)以前的分支(从尚不能从单独获得的软件包开始)magit

我还要提到discover:这是一个使用方法的示例makey(及其存在的理由)。该软件包旨在帮助新手发现emacs绑定。


参考文献


2
@Mark我不太确定您为什么接受这个答案。它根本没有提到小的构建基块,如果我没记错的话,这就是您所追求的。这也不是不正确的:makey不是magit-popup的分支,而是其前身magit-key-mode的分支。它继承了自从magit-popup以来已解决的大多数缺陷。因此,使用magit-popup或hydra(或编写自己的)会更好。
tarsius

@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)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.