Yearly Archives: 2024

2024-01-16

Webカラーwidgetを作る(Emacs Widget Libraryについて調べる2)

前回の続き。

試しにWeb用のカラーコードを入力するためのwidgetを定義してみましょう。

実はEmacs用のカラーコードを入力するためのwidgetは既にあります。

wid-edit.el 4053行目

(define-widget 'color 'editable-field
  "Choose a color name (with sample)."
  :format "%{%t%}: %v (%{sample%})\n"
  :value-create 'widget-color-value-create
  :size (1+ (apply #'max 13 ; Longest RGB hex string.
                   (mapcar #'length (defined-colors))))
  :tag "Color"
  :value "black"
  :completions (defined-colors)
  :sample-face-get 'widget-color-sample-face-get
  :notify 'widget-color-notify
  :match #'widget-color-match
  :validate #'widget-color-validate
  :action 'widget-color-action)

なので、これをちょっと変えてやればすぐに実現出来ます。

;; 実装にはedraw-color.elやedraw-color-picker.elの助けを借ります。
;; https://github.com/misohena/el-easydraw/blob/master/edraw-color.el
;; https://github.com/misohena/el-easydraw/blob/master/edraw-color-picker.el
(require 'edraw-color)
(require 'edraw-color-picker)

(define-widget 'my-web-color 'editable-field
  "Choose a web color (with sample)."
  :value "black"
  ;; タグ: 値 サンプル
  :format "%{%t%}: %v %{      %}\n"
  ;; デフォルトのタグ(%t部分)
  :tag "Color"
  ;; 値部分の作成(%v部分)
  :value-create 'my-widget-web-color-value-create
  :size 26 ;; rgba(255,255,255,1.2345)くらいが収まる長さにしておく
  ;; 補完候補(C-M-i (M-TAB)で補完候補を出せる)
  :completions (mapcar #'car edraw-color-web-keywords)
  ;; 見本部分のface
  :sample-face-get 'my-widget-web-color-sample-face-get
  ;; 見本の更新
  :notify 'my-widget-web-color-notify
  ;; 値の検証
  :match #'my-widget-web-color-match
  :validate #'my-widget-web-color-validate
  ;; ミニバッファからの入力
  :action 'my-widget-web-color-action)

(defun my-widget-web-color-value-create (widget)
  ;; 値を表す部分(:formatの%v部分)をバッファ上に作成します。

  ;; editable-fieldとしての部分を作成。
  (widget-field-value-create widget)
  (widget-insert " ")
  ;; その後にChooseボタンを追加。
  (widget-create-child-and-convert
   widget 'push-button
   :tag " Choose " :action 'my-widget-web-color--choose-action)
  (widget-insert " "))

(defun my-widget-web-color--choose-action (widget &optional _event)
  ;; Chooseボタンが押されたら呼び出されます。
  ;; カラーピッカーで色を入力してもらい、それを親widgetの値として設定します。
  (let* ((wp (widget-get widget :parent))
         (old-color (widget-value wp))
         (new-color (edraw-color-picker-read-color nil old-color)))
    (widget-value-set wp new-color)))

(defun my-widget-web-color-sample-face-get (widget)
  ;; 見本部分(:formatの%{から%}までの間)に適用するfaceを返します。
  (let ((color (condition-case nil
                   (edraw-color-from-string (widget-value widget))
                 (error (widget-get widget :value)))))
    (if color
        (list (cons 'background-color (edraw-to-string-hex (edraw-change-a color 1.0)))) ;; 半透明が表現できないので無理矢理不透明にします。本当はSVGで市松模様背景付きのサンプルを作りたい所。
      'default)))

(defun my-widget-web-color-action (widget &optional event)
  "Prompt for a color."
  ;; フィールド上でRETを押したときに呼び出されます。
  ;; ミニバッファから色を入力してもらい、それをwidgetの値として設定します。
  (let* ((old-color (widget-value widget))
         (new-color (edraw-color-picker-read-color nil old-color)))
    (when new-color
      (widget-value-set widget new-color)
      (widget-setup)
      (widget-apply widget :notify widget event))))

(defun my-widget-web-color-notify (widget child &optional event)
  "Update the sample, and notify the parent."
  ;; 何かイベントが発生したときに呼び出されます。
  ;; たいていの場合テキストが変化したときなので、サンプルのfaceを更新します。
  (overlay-put (widget-get widget :sample-overlay)
               'face (widget-apply widget :sample-face-get))
  (widget-default-notify widget child event))

(defun my-widget-web-color-match (_widget value)
  ;; WIDGETにVALUEを設定可能なら非nilを返します。
  (and (stringp value)
       (or (assoc value edraw-color-web-keywords)
           (string-match edraw-color-string-patterns-re value))))

(defun my-widget-web-color-validate (widget)
  ;; WIDGETの現在の値が正当かチェックします。
  ;; エラーがなければnilを返します。
  (let ((value (widget-value widget)))
    (unless (my-widget-web-color-match widget value)
      (widget-put widget :error (format "Invalid color: %S" value))
      widget)))

試しに表示させてみましょう。

(pop-to-buffer (generate-new-buffer "*Widget Example*"))
(widget-insert "\n")
(widget-create 'my-web-color
               :tag "色"
               "black")
(use-local-map widget-keymap)
(widget-setup)

実行すると次図のようになり、Chooseボタンを押すとカラーピッカー付きで色を入力できます。選択した色もwidgetの右側にサンプルとして表示されます。

Webカラーwidgetを表示させてChooseボタンを押したところ
図1: Webカラーwidgetを表示させてChooseボタンを押したところ

ここで定義したwidgetはdefcustomの:type部分に指定することも出来ます。

(defcustom my-hogehoge-color "#ff0000"
  "My Hogehoge HTML color."
  :type 'my-web-color)

(defun my-hogehoge-html ()
  (format "<span style=\"color: %s\">hogehoge</span>" my-hogehoge-color))

M-x customize-variableでmy-hogehoge-colorを選ぶと次図のようになります。ボタンのスタイルが変わっているのが面白いですね。私はCorfuを入れているので、C-M-iではこのように補完候補が表示されます。

my-hogehoge-colorをcustomizeで変更するところ
図2: my-hogehoge-colorをcustomizeで変更するところ

次は構造を持った値を編集するような複雑なwidgetを作りたいところです。

2024-01-16 ,

Emacs Lisp要素へのorg-modeリンクをエクスポートする

以前Emacs Lisp要素へのリンクをorg-modeに追加するというのを書きました。[[elisp-function:create-image][create-image]]のような形式でEmacs Lispの要素(関数・変数・face)へのリンクを書けるようにするものです。そしてC-c C-oでその要素の定義箇所へジャンプできるようにしました。

しかしエクスポートには対応していませんでした。そのようなリンクを含むOrg文書をHTMLとしてエクスポートしても、意味のあるリンクにはなりません。

元々Emacs内でソースコードを追いかけるときのメモのために用意したのでエクスポートする必要は無かったのですが、こうしたブログに書くときに正しくエクスポートできると便利なことが多々あります。

シンボルから定義の場所を探す

まず要素のシンボルから定義の場所(ファイルと行番号)を割り出します。次のようなコードでできるようです。

(defun my-elisp-element-file-line (symbol finder)
  (ignore-errors
    (let* ((buf-point (funcall finder symbol))
           ;;@todo バッファやポイントの状態を元に戻す。ただし、開きっぱなしの方が連続して変換するときは効率が良い。
           (buffer (car buf-point))
           (point (cdr buf-point)))
      (with-current-buffer buffer
        (cons (expand-file-name (buffer-file-name)) ;;abs-file
              (line-number-at-pos point t)))))) ;;line

(defun my-elisp-function-file-line (symbol)
  (my-elisp-element-file-line symbol #'find-function-noselect))

(defun my-elisp-variable-file-line (symbol)
  (my-elisp-element-file-line symbol #'find-variable-noselect))

(defun my-elisp-face-file-line (symbol)
  (my-elisp-element-file-line symbol
                              (lambda (symbol)
                                (find-definition-noselect symbol 'defface))))

例えば次のように使います。

(my-elisp-function-file-line 'create-image)
("c:/略/share/emacs/29.1/lisp/image.el" . 484)

find-function-noselect関数等を使うとその副作用でバッファが開きっぱなしになったり、もし既に開いていた場合はポイントが動いてしまったりするのですが、とりあえず放っておきます。開きっぱなしの方が連続的に沢山のリンクをエクスポートするときに速いという面はあると思うので。

ファイルと行番号からURLを求める

次にその場所を指すURLを求めるのですが、そのURLはローカルファイルへのURLではなく、世界中から参照できるWorld Wide Web上のURLでなければなりません。でなければこういったブログで使うことはできません。

幸いほとんどのソースコードはWeb上に存在するものです。

EmacsにバンドルされているファイルへのURLを求める

Emacsのソースコードは https://git.savannah.gnu.org/cgit/emacs.git/tree/lisp/ から参照できます。

例えばEmacs 29.1時点でのimage.el内の484行目は https://git.savannah.gnu.org/cgit/emacs.git/tree/lisp/image.el?h=emacs-29.1#n484 で参照できます。

従って次のような関数を作れば:

(defun my-path-globalize-emacs (abs-file line)
  "Create a URL to a file bundled with Emacs."
  (let ((dirs `((,lisp-directory
                 . "https://git.savannah.gnu.org/cgit/emacs.git/tree/lisp/")
                (,find-function-C-source-directory
                 . "https://git.savannah.gnu.org/cgit/emacs.git/tree/src/"))))
    (cl-loop for (dir . url) in dirs
             when (and dir (string-prefix-p (expand-file-name dir) abs-file))
             return
             (concat
              url
              (file-relative-name abs-file dir)
              "?h=emacs-" emacs-version
              (when line (format "#n%d" line))))))

次のような式でローカルファイルへのパスと行番号からWeb上でのURLを作成できます。

(my-path-globalize "c:/略/share/emacs/29.1/lisp/image.el" 484)
"https://git.savannah.gnu.org/cgit/emacs.git/tree/lisp/image.el?h=emacs-29.1#n484"

package.elで管理されているファイルへのURLを求める

package.elで管理しているファイルについてもある程度URLを求めることが出来るようです。

(defvar my-path-globalize-package-globalizers
  '(my-path-globalize-package-org
    my-path-globalize-package-github))

(defun my-path-globalize-package (abs-file line)
  "Create a URL to the file managed by package.el."
  (when-let* ((pkg-name-desc (my-path-globalize-package-find abs-file))
              (pkg-name (car pkg-name-desc))
              (pkg-desc (cdr pkg-name-desc))
              (pkg-dir (package-desc-dir pkg-desc))
              (rel-file (file-relative-name abs-file pkg-dir)))
    (seq-some (lambda (globalizer)
                (funcall globalizer rel-file line pkg-name pkg-desc))
              my-path-globalize-package-globalizers)))

(defun my-path-globalize-package-find (abs-file)
  ;;@todo package-user-dirで足切りする? package-directory-listも?
  (cl-loop for (name . descs) in package-alist
           for desc = (cl-loop for desc in descs
                               for dir = (package-desc-dir desc)
                               when (and dir
                                         (string-prefix-p
                                          (expand-file-name dir) abs-file))
                               return desc)
           when desc
           return (cons name desc)))

(defun my-path-globalize-package-org (rel-file line pkg-name pkg-desc)
  (when (eq pkg-name 'org)
    (when-let* ((pkg-extras (package-desc-extras pkg-desc))
                (pkg-commit (alist-get :commit pkg-extras)))
      (concat
       "https://git.savannah.gnu.org/cgit/emacs/org-mode.git/tree/lisp/"
       rel-file
       "?id=" pkg-commit
       (when line (format "#n%d" line))))))

(defun my-path-globalize-package-github (rel-file line _pkg-name pkg-desc)
  (when-let* ((pkg-extras (package-desc-extras pkg-desc))
              (pkg-url (alist-get :url pkg-extras))
              (pkg-commit (alist-get :commit pkg-extras)))
    (when (string-match "\\(https?://github\\.com/[^/]+/[^/]+\\)" pkg-url)
      (concat
       (match-string 1 pkg-url) "/blob/" pkg-commit "/" rel-file
       (when line (format "#L%d" line))))))

パッケージの紹介Webサイト(?)がGitHubの場合は、GitHubにあるソースコードを参照するようにしてみました。また、Org-modeは特別に処理しています。他にも必要に応じてルールを追加する必要があるでしょう。

例えば vertico-mode の場所:

(my-path-globalize-package (expand-file-name "~/.emacs.d/elpa/vertico-20231229.1740/vertico.el") 741)
"https://github.com/minad/vertico/blob/93f709d71e8908617a21ca469fd60123f5037ae4/vertico.el#L741"

org-link-parametersの場所:

(my-path-globalize-package (expand-file-name "~/.emacs.d/elpa/org-9.6.14/ol.el") 92)
"https://git.savannah.gnu.org/cgit/emacs/org-mode.git/tree/lisp/ol.el?id=58c91cbf9f2510700fbbdaaa166efcb1a5582cf7#n92"

両方まとめる

とりあえずこの二つをまとめて次のようにすることで:

(defvar my-path-globalizers
  '(my-path-globalize-emacs
    my-path-globalize-package))

(defun my-path-globalize (file line)
  "Return a URL on the World Wide Web that points to the LINE of the FILE.

FILE is a path to a local file that has the same content on the WWW.

LINE is a line number starting from 1."
  (let ((abs-file (expand-file-name file)))
    (seq-some (lambda (globalizer) (funcall globalizer abs-file line))
              my-path-globalizers)))

(my-path-globalize ファイル名 行番号)でローカルにあるEmacs Lispファイルの場所をWeb上のURLへ変換できるようになりました。

これを使ってシンボルから直接Web URLを求める関数も作成できます。

(defun my-elisp-face-web-url (symbol)
  (my-elisp-element-web-url symbol
                            (lambda (symbol)
                              (find-definition-noselect symbol 'defface))))

(defun my-elisp-variable-web-url (symbol)
  (my-elisp-element-web-url symbol #'find-variable-noselect))

(defun my-elisp-function-web-url (symbol)
  (my-elisp-element-web-url symbol #'find-function-noselect))

(defun my-elisp-element-web-url (symbol finder)
  (when-let ((file-line (my-elisp-element-file-line symbol finder)))
    (my-path-globalize (car file-line) (cdr file-line))))

org-modeでエクスポートする

後はorg-modeのリンクタイプに設定してやるだけです。

エクスポート用の関数を用意し:

(defun my-org-elisp-export-function (path desc format)
  (my-org-elisp-export-element path desc format #'my-elisp-function-web-url))

(defun my-org-elisp-export-variable (path desc format)
  (my-org-elisp-export-element path desc format #'my-elisp-variable-web-url))

(defun my-org-elisp-export-face (path desc format)
  (my-org-elisp-export-element path desc format #'my-elisp-face-web-url))

(defun my-org-elisp-export-element (path desc format path-converter)
  (or (when (eq format 'html)
        (when-let ((url (funcall path-converter (intern path))))
          (format "<a href=\"%s\">%s</a>" url (or desc path))))
      ;; pathが解決できないときはリンクにしない。
      (or desc path)))

org-link-parametersへ登録してやります:

(org-link-set-parameters
 "elisp-function"
 :follow (lambda (name) (find-function (intern name)))
 :export #'my-org-elisp-export-function)

(org-link-set-parameters
 "elisp-variable"
 :follow (lambda (name) (find-variable (intern name)))
 :export #'my-org-elisp-export-variable)

(org-link-set-parameters
 "elisp-face"
 :follow (lambda (name) (find-face-definition (intern name)))
 :export #'my-org-elisp-export-face))

テスト

試しに次にいろんな要素へのリンクを書いてみました。

Orgソース:

- Emacsバンドル
  - [[elisp-function:create-image]] (lisp/image.el)
  - [[elisp-function:url-retrieve]] (lisp/url/url.el ←階層あり)
  - [[elisp-function:widget-get]] (src/fns.c)
  - [[elisp-variable:truncate-lines]] (src/buffer.c)
  - [[elisp-face:font-lock-comment-face]] (lisp/font-lock.el)
- package.el
  - GitHub
    - [[elisp-function:vertico-mode][vertico-mode]]
  - org-mode
    - [[elisp-function:org-mode][org-mode]]
    - [[elisp-variable:org-link-parameters][org-link-parameters]]

注意点:再エクスポートで同じURLが生成されない問題

URLは現在実行している環境で使われているバージョンのソースコードを参照しています。文書を書いている時点のソースコードを指し示すのが最も無難だろうとの判断によるものですが、不都合なこともあるかもしれません。特に文書を書いてからしばらく経った後、Emacsやパッケージをバージョンアップした後に再度エクスポートした場合、同じURLが生成されないという問題があります。これが嫌なのであれば、大人しく通常のhttp(https)リンクを使うべきでしょう。上の仕組みを流用してそのようなhttpリンクを生成するようなコマンドも容易に作成できることでしょう。

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

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