emacsのtooltip作成用のライブラリのposframe.elの使いかたを調べていた。
経緯
元々の経緯として、補完のinterfaceにivyを検討していたのだけれど、どうも候補選択のUIは現在のカーソル位置からのtooltipのような形で出て欲しいかなと思ったりしていた。
現状はcompany-modeを使っていたのだけれど。ivyでできないかなーと思ったりしていた。その過程でivy-posframeを知った。
このivy-posframeが内部で利用しているライブラリがposframe。
posframe?
posframeはtooltip的なものなどを作成するために使えそうなUIライブラリ的なもの。ちょっと風変わりだと思ったのはchild-frame(おそらくchild processに倣ってこの呼び方)を作成してそれを提供する点。
Posframe can pop a posframe at point, this posframe is a child-frame with its root window's buffer.
frame? window?
その前にemacsでのframeが何かということについて説明を書いておく。
- window -- 1frame内の表示領域のこと
- frame -- windowを持った画面のこと。一般的なアプリケーションでのウィンドウがemacsでのframeに対応している。
例えば、C-x 2
やC-x 3
で1つのアプリケーションウィンドウを水平や垂直に分割したときに、分割された個々の表示領域のことはwindowと呼ぶ。
一方frameはM-x new-frame
でアプリケーションウィンドウを新しく開いたりしたときに生成されたそのウィンドウ(パネル)を意味するもののことを指している。
絵にすると以下の様な感じ。下の図は2つのframeを表示したときのこと。
frame 1 -------------------- |window 1 | -------------------- |window 2| window 3| -------------------- |window 4 | -------------------- frame 2 -------------------- | | | |window 1| window 2| | | | --------------------
詳しくはこのあたりを読む
elisp的なframeの扱い
画面上の背景や文字色等はframeのparameterとして格納されている。現在アクティブなframeはselected-frame
を使って取れる。
(selected-frame) ;; => #<frame emacs@localhost 0x13bfc60> (frame-parameter (selected-frame) 'foreground-color) ;; => "#f8f8f2"
ちなみにnilを渡した時にも、現在アクティブなframeな情報を返してくれる。一気に全てのparametersを取りたいときには代わりにframe-parameters
を使う。
(frame-parameter nil 'background-color) ;; => "#282a36"
そうそう、良い機会だったのでついでにemacsのcolor-themeをdracula-themeに変更した(doom-draculaは使っていない)。先程の例の背景色などはdracula-themeのもの。
frameがwindowを所有するので、frame-list
で取り出したframeが所持するwindowをwindow-list
で取り出せる。
(frame-list) ;; => (#<frame *scratch* 0x6998890> #<frame *scratch* 0x13bfc60>) (window-list (first (frame-list))) ;; => (#<window 207 on *scratch*>) (window-list (second (frame-list))) ;; => (#<window 118 on *scratch*> #<window 212 on posframe.el>)
windowの削除などは、おそらくふつうにemacsを使っているなら息を吸う様に使っているであろうC-x 0
とC-x 1
にbindされている(delete-windows
, delete-other-windows
)。
frameの削除はdelete-frame
やdelete-other-frame
を使う。delete-other-frame
はC-x 5 1
という馴染みのないキーにbindされているらしい。
frameを隠すこととframeを削除すること
先程、frameの削除については紹介したがもう一つframeに対する操作がある。それは表示・非表示にすること。このあたりはmake-frame-invisible
などでできる
;; 引数を省略しても良い (make-frame-invisible (selected-frame))
詳しくはこのあたり
ようやくframe,windowの説明が終わった。
posframe
posframeは先程説明したframeを使ってtooltip的な機能を表現したモノのこと。とりあえずposframe-show
を覚えれば良い。
渡したbufferの内容をtooltipとして表示する。なので、複数の場所に表示したい場合には渡すbufferを変える必要がある。引数としてバッファ名を渡すこともできる。
;; need: package-install posframe (require 'posframe) (posframe-show "*sample buffer*" :string "hello")
バッファ名を文字列で渡し、その内容を":string"オプションで指定した例。
ちょっとtooltipっぽくないかもしれない。borderを追加してみる。
作られたchild-frameはstickyで付いてくる(ディスプレイ上の座標を指定しての表示なのでそれはそう)。
tooltipを消したい
tooltip(child-frame)は消されるまでずっと表示され続ける。消す方法を調べる必要がありそう。
tooltipを全部消したい場合には posframe-delete-all
を使えば良い。
ちなみにchild-frameかどうかは、そのframeがpostframe-buffer
というparameterを持っているかどうかで確認できる。
(cl-loop for f in (frame-list) when (frame-parameter f 'posframe-buffer) collect f );; => (#<frame 0x4cc9030> #<frame 0x9414f50>)
個別に消したい場合には、以下の様なhookが追加されているので、対応するbufferをただkillするだけでも消せるといえば消せる。
(defun posframe-auto-delete () "Auto delete posframe when its buffer is killed. This function is used by `kill-buffer-hook'." (posframe-delete-frame (current-buffer))) (add-hook 'kill-buffer-hook #'posframe-auto-delete)
そんなわけでtimerなどと合わせれば以下の様な形でN秒後にtooltipを消すということもできる。
;; 3秒後に消える (lexical-let ((bufname "*me*")) (posframe-show bufname :internal-border-width 20 :string "hello!!" ) (run-at-time "3 sec" nil #'(lambda () (kill-buffer (get-buffer bufname)) )) )
ただちょっと待って欲しい。
消すことと非表示にすること
この記事の途中でframeの削除とは別にframeの非表示についても説明していた。ふつうに考えてtooltipを表示するたびに毎回frameを作ったり消したりしているのでは効率が悪いかもしれない。
幸いposframe-show
は:timeout
というオプションを持っており、これを使えばN秒後の非表示を実現できる。
;; 3秒後に非表示にする(隠す) (lexical-let ((bufname "*me*")) (posframe-show bufname :internal-border-width 20 :string "hello!!" :timeout 3 ) )
裏側では先程説明したmake-frame-invisible
などが使われている。
その他対応しているオプション
その他色々なオプションが指定できるので後で調整したい場合にはこの辺のオプションの挙動などを調べることになりそう(describe-functionなどで調べていく感じ)。
(cl-defun posframe-show (posframe-buffer &key string position poshandler width height min-width min-height x-pixel-offset y-pixel-offset left-fringe right-fringe internal-border-width internal-border-color font foreground-color background-color respect-header-line respect-mode-line face-remap initialize no-properties keep-ratio override-parameters timeout refresh ...
色の選択(ivyとivy-posframeとの表示の違い)
ちなみにposframe-show
に渡す色の選択には、冒頭のivyファミリー(?)のcounselのcounsel-colors-emacs
がそれなりに便利だった。
(counsel-colors-emacs)
こちらは通常のivyの表示
一時的にivy-posframeを使う(colors-emacsではなくcolors-webというものもある)。
(let ((ivy-display-function #'ivy-posframe-display-at-point)) (counsel-colors-web))
(他にもivy-posframe-displayで始まるtooltipの表示位置の計算が異なる関数が色々ある)
;; (apropos "ivy-posframe-display-") ivy-posframe-display-at-frame-bottom-left Function: (not documented) ivy-posframe-display-at-frame-bottom-window-center Function: (not documented) ivy-posframe-display-at-frame-center Function: (not documented) ivy-posframe-display-at-point Function: (not documented) ivy-posframe-display-at-window-bottom-left Function: (not documented) ivy-posframe-display-at-window-center Function: (not documented)
まじめに設定する場合には以下の様な形(再起動以外に設定を取り除く方法がないあたり微妙ではある)。
(require 'ivy) (require 'ivy-posframe) (ivy-mode 1) (add-to-list 'ivy-display-functions-alist '(complete-symbol . ivy-posframe-display-at-point)) (ivy-posframe-enable)
ivy-display-function
に特定の関数を1つだけ設定する方法もあるけれど、正直それは微妙だと思う。けっこう状況によってtooltip的に表示されてほしいかminibuffer的に表示されてほしいか変わる気がするし。
posframeの使いかた
posframeによるtooltipはけっこう手軽で良さそうだと思ったのだけれど。まじめにUIを作ろうと思った時に結局発生する座標管理がだるいなーと思ったし。それを何らかのdocumentでという風になったらDOMの再発明だなーという感じになったりした。
とはいえminibufferに表示されるよりtooltip的な見た目の方が見やすいことはたしかで、ちょっとしたインジケーター的なものとかposframeで作ってみたりしても良いのかもしれないと思ったりした。
そんなわけでなるべくposframeの仕組み自体が後でわかるように記事にしてみた。