2024-01-07

Emacs Widget Libraryについて調べる

Emacs Widget LibraryというのはEmacsのテキストバッファ内に(データ入力フォームみたいな)UIを構築するためのライブラリです。

customizeのUIなんかで使われているアレです。defcustomの:typeで指定しているのはまさにこのwidgetのtypeだったりします。他にもEmacsの様々なところで使われていますし、私のel-easydrawではプロパティ一覧編集の画面に使われていたりもします。

Emacs Lispを書いていると度々必要になるものなので、前々からこのEmacs Widget Libraryについてはちょくちょく調べています。

基本的な部分については一応マニュアルがあります。

The Emacs Widget Library

手っ取り早く動く例が欲しいのであれば、Programming Exampleがあります。この例は一つ一つのwidgetの挙動を細かく調べるには大きすぎるので、私は次のようなコードで色々試しています。

url-linkの例:

(pop-to-buffer (generate-new-buffer "*Widget Example*"))
(widget-create 'url-link
               :tag "GNUのWebサイト"
               "https://www.gnu.org/")
(widget-setup)

push-buttonの例:

(pop-to-buffer (generate-new-buffer "*Widget Example*"))
(widget-create 'push-button
               :action (lambda (widget &optional _event)
                         (message "押された value=%s" (widget-value widget)))
               :tag "押して"
               "何かの値")
(use-local-map widget-keymap) ;; これが無いと押せない。linkは押せるのに
(widget-setup)

listの例:

(pop-to-buffer (generate-new-buffer "*Widget Example*"))
(widget-create 'list
               :tag "何かの固定長固定型のリスト"
               :notify (lambda (widget &rest args)
                         (message "通知 value=%s" (widget-value widget)))
               '(const "何かの固定値")
               '(boolean :tag "何かの真偽値" :on "有効" :off "無効")
               '(text :tag "好きな複数行文字列" :value "hogehoge"))
(use-local-map widget-keymap)
(widget-setup)

widget-keymapには次のものが割り当てられています:

  • ボタンを押すために必要なコマンド(RET, down-mouse-1等)
  • 前後のwidgetへ移動するコマンド(TAB, S-TAB等)

widget-setupは(現在のバッファについて)次のことを行います:

  • 一部widgetの最終初期化処理
  • UNDO情報のクリア
  • 変更を監視するフックの追加 (変更をしかるべき所に通知するのが主な目的ですが、同時にフィールド以外の所でテキスト編集ができなくなります)

そして肝心要のwidget-createは次のことを行います:

  1. widgetオブジェクトの作成
  2. バッファへの挿入

widget-createの引数は (type &rest args) です。 type はwidgetの種類を表すシンボルです。残りの引数列 argstype ごとに定められた形の引数列です。一般的には、キーワード引数が0個以上続いた後にキーワードが付かない値が0個以上続く形になっています。

widgetの type (種類)に何があるのかは、マニュアルのBasic TypesSexp Typesに載っているほか、M-x widget-browseの補完候補で分かります。または次のコードで(現在ロードされている)一覧を得ることも出来ます。

(let ((syms))
  (mapatoms (lambda (s) (when (get s 'widget-type) (push s syms))) obarray)
  syms)
(boolean checklist widget-browse documentation-link group natnum emacs-commentary-link float visibility coding-system custom-manual tree-widget-icon regexp checkbox c-symbol-list choice other toggle variable cons integer editable-field bibtex-entry-alist tree-widget-nohandle-guide flycheck-minimum-level alist option mule-input-method-string charset plist info-link sexp c-integer-or-nil radio-button-choice character tree-widget-no-handle lazy emacs-library-link flycheck-highlighting-style custom-browse-group-tag my-attribute-list tree-widget-no-guide glyphless-char-display-method tree-widget-empty-icon custom-face-edit tree-widget-leaf-icon custom-face tree-widget-open-icon tree-widget string custom-group-visibility custom-icon link repeat list function-link hook my-attribute-plist buffer-predicate custom-browse-variable-tag variable-link custom-magic fringe-bitmap custom-comment number text directory vector documentation-string custom-group-link custom-visibility custom-browse-face-tag choice-item radio-button symbol bibtex-field-alist custom-group face restricted-sexp custom-variable custom key c-extra-types-widget tree-widget-end-guide custom-face-all function custom-browse-visibility custom-display face-link color tree-widget-close-icon tree-widget-guide item const file editable-list function-item set insert-button key-sequence radio default file-link variable-item menu-choice delete-button push-button url-link c-const-symbol tree-widget-handle)

基本的なwidgetは wid-edit.el で定義されているので、それを見るのが一番手っ取り早いでしょう。例えば url-link はwid-edit.el内で次のように定義されています。

(define-widget 'url-link 'link
  "A link to a web page."
  :action 'widget-url-link-action)

このコードによって、url-linkタイプは、linkタイプの:actionプロパティを'widget-url-link-actionに変更したものと定義されます。

これは良くあるオブジェクト指向プログラミング言語におけるクラスの定義と意味的にはほとんど同じです。linkが継承元クラスで、:actionというメソッドをオーバーライドすることに相当します。:actionはプロパティですが、その値は常に関数として(widget-applyで)呼び出されるのでメソッドのようなものです。:actionに指定された関数はwidgetが押されたときに呼び出されます。:actionに指定されているwidget-url-link-action関数の中身が (browse-url (widget-value widget)) となっているので、url-linkを押すとwidgetの現在値に設定されたURLがbrowse-url関数で開かれるわけです。

ちなみに継承元であるlinkは:

(define-widget 'link 'item
  "An embedded link."
  :button-prefix 'widget-link-prefix
  :button-suffix 'widget-link-suffix
  :follow-link 'mouse-face
  :keymap widget-link-keymap
  :help-echo "Follow the link."
  :format "%[%t%]")

さらにその継承元であるitemは:

(define-widget 'item 'default
  "Constant items for inclusion in other widgets."
  :convert-widget 'widget-value-convert-widget ;; 引数列をwidgetオブジェクトへ変換する
  :value-create 'widget-item-value-create ;; :formatの%v部分として、:valueの値をprincでバッファへ挿入する
  :value-delete 'ignore
  :value-get 'widget-value-value-get ;; :valueの値をそのまま返す
  :match 'widget-item-match
  :match-inline 'widget-item-match-inline
  :action 'widget-item-action ;; 自分自身に対して :notify を呼び出す
  :format "%t\n")

さらにさらにその継承元であるdefaultは:

(define-widget 'default nil
  "Basic widget other widgets are derived from."
  :value-to-internal (lambda (_widget value) value)
  :value-to-external (lambda (_widget value) value)
  :button-prefix 'widget-button-prefix
  :button-suffix 'widget-button-suffix
  :completions-function #'widget-default-completions
  :create 'widget-default-create ;; :format等に従ってテキストやオーバーレイなどをバッファへ挿入する。
  :indent nil
  :offset 0
  :format-handler 'widget-default-format-handler ;; :formatに知らないエスケープ文字がある場合に呼ばれる
  :button-face-get 'widget-default-button-face-get
  :mouse-face-get 'widget-default-mouse-face-get
  :sample-face-get 'widget-default-sample-face-get
  :delete 'widget-default-delete ;; バッファからwidgetの全テキストを削除する
  :copy 'identity
  :value-set 'widget-default-value-set
  :value-inline 'widget-default-value-inline
  :value-delete 'ignore ;; :value-createで挿入したものを削除する
  ;; :value-create で :format の %v 部分をバッファへ挿入する
  :default-get 'widget-default-default-get
  :menu-tag-get 'widget-default-menu-tag-get
  :validate #'ignore ;; widgetの現在値が正当かチェックする
  :active 'widget-default-active
  :activate 'widget-specify-active
  :deactivate 'widget-default-deactivate
  :mouse-down-action #'ignore
  :action 'widget-default-action
  :notify 'widget-default-notify
  :prompt-value 'widget-default-prompt-value)

と定義されており、このように継承関係を辿っていくことが出来ます。

(ちなみに、実装的にはJavaScriptのprototypeチェーンを使った継承に似ています。prototypeの代わりにシンボルの'widget-typeプロパティが使われていますが)

プロパティの一覧については、後からdefine-widget部分に載っていないプロパティが動的に追加・参照されることも多々あるのでこれだけでは完全ではありませんが、ある程度参考にはなるでしょう。

新しいwidgetタイプを作る時に使うプロパティについてはDefining New Widgetsに説明がありますが、これだけで分かる人もあまりいないと思うので wid-edit.el を見て各widget-*関数からどのようにそれらのプロパティが参照されているかを確認した方が早いでしょう。

様々なタイミングでどのような処理がされているのかいくらでも書くことが出来ますが、キリがないのでとりあえずこの辺で。

最後にwidgetタイプをクラスに見立ててクラス図を描いたのを載せておきます。

Basic Typesに書いてあるもの:

2024-01-07-classes-basic.png

Sexp Typesに書いてあるもの:

2024-01-07-classes-sexp.png

Basic Typesに書いてあるものの詳細:

2024-01-07-classes-basic-detailed.png

※(関数以外の)デフォルト値を変更しているだけのものは // を付けて残してあります。