popup.elによるポップアップメニュー、カスケードポップアップメニュー、ツールチップの実現

さまざまな現代的なインターフェースを実現できるようpulldown.elを大幅に拡張しました。それに伴って抽象度も上がったため、pulldown.elという名前は少し内容にそぐわなくなりました。そこでpulldown.elあらためpopup.elという名前で開発を続けていくことにしました*1マーケティング的にはあまりよろしくないと思いますが、主な利用者はauto-completeだけなのでまあいいでしょう。

成果物はいつも通りauto-completeのリポジトリから取得できます。

http://github.com/m2ym/auto-complete

さて、今回の拡張で何ができるようになったかですが、おそらく次のスクリーンショットを見れば一目瞭然かと思われます。


見ての通り、多階層ポップアップメニューとツールチップが実現できるようになっています。元々、これらの機能を実装するつもりはなかったのですが、auto-completeの機能拡張でいずれ必要になるので、結局実装してしまった次第です。

popup.elではポップメニューであれツールチップであれ、結局はpopup-create, popup-set-line, popup-draw, popup-deleteの一連の流れに従います。ただ、ユーザーがいちいちこの流れを記述するのは面倒なので、簡単に利用できるヘルパー関数が用意されています。ちなみに、auto-completeは少し複雑な流れになっているので自分でポップアップを管理しています。

ポップアップメニューを表示するにはpopup-menu関数を使います。詳しくはソースコードを見てもらうとして*2、基本的な使い方だけ紹介しておきます。

popup-menu関数は引数としてメニューとなるリストを渡します。

(popup-menu '(Foo Bar Baz))

これを評価すると次のようになります。

popup-menu関数を呼びだすと、内部では同期的なイベントループが開始されます。キーボードによるメニューの操作は全てこのイベントループで処理され、選択されたアイテムが関数の返り値になります。

次のようにscroll-barキーワードやmarginキーワードを指定しておくと、若干見やすいインターフェースになります。

(popup-menu '(Foo Bar Baz) :scroll-bar t :margin t)

多階層ポップアップメニューを表示するにはpopup-cascade-menu関数を使います。この関数はpopup-menu関数のラッパーですが、第一引数のリストを構造的に解釈して多階層ポップアップメニューとして表示します。

(popup-cascade-menu '(A
                      (B
                       B-1
                       B-2)
                      (C
                       (C-1
                        C-1-1
                        C-1-2)
                       C-2)
                      D))

リストの要素がconsセルの場合、メニューアイテムがそのconsセルのcar、子メニューのリストがそのconsセルのcdrになります。

スクリーンショットは冒頭に載せてあるので割愛します。

ツールチップを表示するにはpopup-tip関数を使います。この関数は第一引数にツールチップとして表示したい文字列を取ります。

(popup-tip "Hello world!")

文字列内に改行がある場合や、ある行がある幅より大きい場合*3、その行は折り返されます。

(popup-tip "First line\nSecond line\nThird line")

popup-menu関数のように内部でイベントループを開始したりはしませんが、read-eventで同期的にキー入力を待ちます。何らかのキー入力があった場合は、ツールチップを削除して従来の動作に委譲します。

おそらく真っ先に思いつく利用ケースは、ポイントしているシンボルのドキュメントをツールチップで表示することでしょう。次のコードはdefunのドキュメントをツールチップで表示するものです。

(popup-tip (documentation 'defun))

変数のドキュメントも引けるように次のような関数を定義します。

(defun doc (symbol)
  (or (ignore-errors (documentation symbol))
      (ignore-errors (documentation-property symbol 'variable-documentation))))

さらにポイントしているシンボルのドキュメントをツールチップで表示するコマンドを定義します。

(defun doc-at-point ()
  (interactive)
  (let* ((symbol (symbol-at-point))
         (doc (doc symbol)))
    (when (and doc (null popup-instances))
      (popup-tip doc :margin t))))

これを評価して、適当なシンボルの上でM-x doc-at-pointすると、そのシンボルのドキュメントがツールチップで表示されると思います。

さらに進んで、タイマーで自動的にドキュメントを表示するのも良いかもしれません。

(defvar doc-timer nil)
(defvar doc-delay 1.0)
(defvar doc-tip-point nil)

(defun doc-timer ()
  (when (and (not (eq doc-tip-point (point)))
             (not (minibufferp)))
    (setq doc-tip-point (point))
    (doc-at-point)))

(setq doc-timer (run-with-idle-timer doc-delay doc-delay 'doc-timer))

popup.elを利用することにより、これまで不可能だったインターフェースを実現できるようになります。是非ご利用ください。

*1:重複しそうな名前だけどググってもヒットしないので大丈夫でしょう

*2:ドキュメントはまだです、すみません

*3:ソースコードを見てください