2025-12-05

Emacsの中で動く仮想キーボードを作る

Emacs 30からmodifier-bar-modeというのが追加されました。これはツールバーにControl、Shift、Meta、Alt、Super、Hyperといった修飾キーに相当するボタンを追加するモードです。つまりツールバーでそれらのボタンを押してから通常のキーを押すと、その通常のキーがControlキーやらShiftキーやらと一緒に押したことになるわけです。これはおそらくAndroidの仮想キーボード(Emacsではオンスクリーンキーボードという呼び方で統一されていますしソフトウェアキーボードという呼び方も普通だと思いますが、ここでは仮想キーボードと呼ぶことにします)と併用するために追加されたのではないでしょうか。普通の仮想キーボードにはそういった修飾キーはありませんからね。

modifier-bar-mode
図1: modifier-bar-mode

私も今年Android版のEmacsを利用し始めましたが、このmodifier-bar-modeは当然試しました。

しかし単に修飾キーが欲しいだけならHacker's Keyboardのような豪華な仮想キーボードを使えば十分です。

このように手軽に押せるツールバー上のキーが欲しくなるシチュエーションは、日本語用の仮想キーボードを表示しているときや、そもそも仮想キーボードを非表示にしているときではないでしょうか。それならもっと色々なキーがツールバー上にあっても良いでしょう。

ということで作ったのがこちらのツールバー。

my-tool-bar-kbd-mode
図2: my-tool-bar-kbd-mode

modifier-bar-modeを真似た上で他にも様々なキーを追加しました。これで仮想キーボードをONにしたり切り替えたりしなくてもよく使うキーが押せるようになりました。

しかしこのツールバーを使っていると不満も出てきます。アルファベットがqwerty配列ではないので使いづらいです。キーのサイズも小さくて押しづらい。そして一番はやはり「もっと沢山キーが欲しいよ!」ということです。人間の欲望に限りはありません。

それならいっそのこと……というわけで、Emacsの中で動く仮想キーボードを作ることにしました(以前どこかで見たような気もしたのですが、ちょっと探したくらいでは見つからなかったんですよね)。

Emacs Virtual Keyboard(el-vkbd)

それでできたのがこちら。

misohena/el-vkbd: A software keyboard implemented in Emacs Lisp that runs inside Emacs.

次のスクリーンショットで上側に表示しているのがその仮想キーボードです。

vkbdと通常の仮想キーボードを両方表示
図3: vkbdと通常の仮想キーボードを両方表示

こうやって既存の仮想キーボードと併用することで日本語もフリック入力で打てますし切り替え無しでほぼ全てのキーが入力できます。(この配列は括弧がシフト無しで入力できるようになっています。素晴らしくないですか!?)

キー配列も数種類用意してあります。次のように特殊キーのみの配列を使えばmodifier-bar-modeの代わりとしても使えます。

vkbdは特殊キーのみの表示で通常の仮想キーボードと併用する(関係ないけどこの後ろのBASIC世代にはたまらなく懐かしくありません?)
図4: vkbdは特殊キーのみの表示で通常の仮想キーボードと併用する(関係ないけどこの後ろのBASIC世代にはたまらなく懐かしくありません?)

もちろん配列は自由にカスタマイズ可能です。標準的なカスタマイズ方法としては、 M-x customize-variable vkbd-layout-list というのを用意してあります。

仮想キーボードの表示場所はサイドウィンドウ、子フレーム、独立フレームから選べるようになっています(内部的にはcontainer-typeと呼んでいます)。最初は子フレームとして作ったのですが、後からサイドウィンドウとして表示できるようにもしました。結局同じフレームにウィンドウを分割して表示した方が使いやすいと思います。

子フレームとして表示している状態
図5: 子フレームとして表示している状態

仮想キーボードのタイトルバーにあるボタンは左から「閉じる」「メニュー」「10x7配列へ切替」「特殊キーのみ配列へ切替」「ネイティブ仮想キーボード無効化トグル(Android時のみ)」です。もちろんこれらタイトルバーの構成は自由にカスタマイズ可能です(M-x customize-group vkbd-title-bar)。

タイトルバーのボタン
図6: タイトルバーのボタン

メニューからは「キー配列の変更」「フレームタイプ変更(ウィンドウ、子フレーム、独立フレーム)」「サイドウィンドウの方向(ウィンドウ表示時のみ)」「カスタマイズバッファの表示」が選べます。

キーボードメニュー
図7: キーボードメニュー

メニューやボタンから選んだ状態は即座に反映され、デフォルトでは ~/.emacs.d/vkbd ファイルに保存され次回起動時には同じ状態からスタートします(保存先は M-x customize-variable vkbd-global-keyboard-user-data-storage で変更可能。項目毎にどこに保存するかをカスタマイズできる過度に複雑な仕組みをご用意しました)。

Android環境では通常の仮想キーボードを無効化するボタンも付けました。それを押すと通常の仮想キーボードは表示されなくなり、もう一度押すと元の設定に戻ります(どのようなときに仮想キーボードを表示するかは元々Emacsの設定である程度制御できます)。

通常の仮想キーボードを無効化すると、完全にこの仮想キーボードだけでEmacsを操作することも可能です。

vkbdのみ使用
図8: vkbdのみ使用

また、vkbd-replace-osk-modeというグローバルマイナーモードも作りました。これは通常の仮想キーボード(Emacsではオンスクリーンキーボードと呼びます)をできるだけこの仮想キーボードで置き換えるモードです。Emacsでは(設定によりますが)画面をタッチしたときに、そのタッチした場所が編集可能なら自動的に仮想キーボードを表示するしくみがあります。他にもミニバッファからの入力を開始したときなど、仮想キーボードを自動的に立ち上げるタイミングが複数あります。vkbd-replace-osk-modeを有効にすると、それらのタイミングで通常の仮想キーボードは出ずに代わりにこの仮想キーボードが出現します。

もちろん100% Emacs Lispで実装されていますので、動作はAndroidに限りません。デスクトップ上でも使用できます。

Windowsで使う
図9: Windowsで使う

マウスから手を離したくない、キーボードが遠い、といった時には使ってみても良いかもしれません。(私はあまり使わないと思いますが)

実装について

実装の一番のキモはやはりどうやってマウス/タッチイベントをキー入力に変換するのかという点でしょう。

このプロジェクトはmodifier-bar-modeに端を発していますから、その方法を参考にするのが自然な流れでした。

それはinput-decode-mapという変数を使うことです。

Translation Keymaps (GNU Emacs Lisp Reference Manual) (ayatakesi氏の日本語訳)

相変わらずマニュアルを読んでも頭に入ってこないので色々試して理解したこと:

  • input-decode-mapというキーマップがある。
  • input-decode-mapにはキーシーケンスに対してコマンドでは無く変換関数を割り当てる。
  • (read_key_sequence 中に)イベント(キーやマウス、タッチ等)が発生したらその組み合わせ(キーシーケンス)に対応するinput-decode-mapに割り当てられた変換関数が呼ばれる。
  • 変換関数はpromptという引数一つだけを受け取る。これは元々は read_key_sequence の引数。ほとんどの場合無視して良いらしい。
  • 変換対象のイベントはcurrent-key-remap-sequence変数から取得できる。その値はベクトルで、基本的にキーマップに設定した(変換関数を呼び出すトリガーとなった)キーシーケンスと同じものが入っているはずなので必ずしも参照しなくてよいが、マウスやタッチイベントでは座標等の細かい情報はここからしか入手できない。
  • 変換関数は変換後のイベントをベクトルで返す。
    • 空ベクトル([])を返すと変換元のイベントは無かったことになるみたい。
    • nilを返すと変換無しとなり、何も変換せずに元のイベントを返すのと同じになるみたい。
    • 返すベクトルに沢山のイベントを詰め込んでも、それが全部使われるわけではない。あくまで1回のキーシーケンス読み取りに使われる分だけが処理対象。

というわけで、まずはinput-decode-map[down-mouse-1] やら [touchscreen-begin] やらに対して独自の変換関数を登録(例えば (define-key input-decode-map [down-mouse-1] #'my-translate-event) みたいにして)。そして変換関数は、変換元イベントが仮想キーボードのキー上で発生したのであればそのキーに対応するキーイベントを生成して返します(実際には押し下げから離すまでの待ち、修飾キーの適用、キーリピートなど様々な処理がこの間に必要になります)。もし仮想キーボードと関係ない場所のイベントであれば他に悪影響が及ばないように速やかにnilを返します。

実際にやってみるとC-xの後に down-mouse-1 が来ないで mouse-1 は来るとか frame-switch イベントが邪魔とかよく分からない挙動が色々とありましたが、どうにかうまくマウス/タッチからキーイベントを生成することが出来ました。

しかしこれだけでは入力できないシチュエーションがチラホラありました。それもそのはず。input-decode-mapread_key_sequence 関数からしか使われないからです。その他の入力関数を使用している場合はinput-decode-mapによる変換は行われません。その他の入力関数とはread-eventread-charread-char-exclusiveのことです。

このあたりのEmacsの入力関数には何があるのかということは前回図解しました。

よく分かるEmacs Lisp入力関数関連図 | Misohena Blog

つまり入力関数には read_filtered_event 系(read-eventread-charread-char-exclusive)と read_key_sequence 系の2系統があり、その上に read_char という関数があるという構造でした。

read_char のレベルで何かイベントを変換するような仕組みがあれば良かったのですが、私には見つけられませんでした(長いのでろくに読んでいません)。

そこでread-eventread-charread-char-exclusiveadvice-addで:around adviceを仕掛けて動作を上書きし無理矢理返ってきたイベントを変換することに。read-charread-char-exclusiveは元の挙動ではマウス/タッチイベントが返ってこないので、read-eventを呼んで返ってきたイベントが文字で無ければリトライしたりエラーにするといった方法で対処しました。要するにread-charread-char-exclusive、そこから呼ばれる read_filtered_event が内部でやることを、Emacs Lispレベルで再現して対処しました。それなりにうまく再現できたと思いますが、不安は残るので(特にtext-conversionの切り替えまわり)仮想キーボードが表示されていない場合はちゃんとadviceを取り除くようにしてあります。

これでほとんどのケースで仮想キーボードに対するマウス/タッチイベントをキーイベントに変換できるようになりました。

これらはできるだけ表示方法とは切り離して実装してあります。なので表示部分だけをごっそり別なものに入れ替えることも理論上は可能です。その入れ替えるための仕組みも一応styleという名称ですでに入っています。ただし、その実装は現在のところテキストで表示するvkbd-text01-styleという一種類のみになっています。SVGで表示するスタイルも作りたいなと思ったのですが、思いのほかテキストだけで十分綺麗な物が出来てしまったので、作る意欲はどこかへ飛んで行ってしまいました。

Emacs Lisp実装の仮想キーボードの限界

現段階でもうまく入力できないシチュエーションがいくつか確認できています。

一つは仮想キーボードを専用フレーム(子フレームまたは独立フレーム)で表示している場合。この時、仮想キーボードをクリックするとフォーカスが仮想キーボードを表示しているフレームに移り switch-frame イベントが発生します。そして様々なフレーム切り替え処理が行われます。その影響なのか詳しくは調べていませんが、そのタイミングで入力受け付け状態が終了してしまうプログラムがいくつかありました。transient.el(Magit等)やset-transient-map(text-scale-adjust等)が典型的です。ウィンドウ表示にするとフレーム遷移が発生しないので問題が発生しません。基本的に生成したキーイベントは入力対象のバッファ、ウィンドウ、フレームに選択やフォーカスを戻してから返さないといけないので、必然的にフレーム選択やフォーカスの遷移は打鍵毎に2回生じることになります。別フレームで表示していると他にも様々な問題があります。基本的にフォーカスが当たっていないフレームではマウスイベントが発生しないようなので、キーリピート中のmouse upイベントが発生せずキーが押しっぱなしになってしまうという問題もありました(従って現状ではキーリピートはウィンドウ表示時のみ可能です)。

もう一つはウィンドウ表示時の問題。現在ウィンドウ表示時はサイドウィンドウを使用して仮想キーボードを表示しますが、既存のプログラムの中にはサイドウィンドウを使うものが数多くあります。例えばtransient.el(Magit等)はキーメニューをデフォルトでは下側にサイドウィンドウとして表示します。which-key-modeもヘルプを下側にサイドウィンドウとして表示していました。もし下側に仮想キーボードを表示していると、それらがサイドウィンドウを表示したときに消されてしまい、キー入力が出来なくなってしまいます。どちらもキーを受け付ける専用の状態の時に起こるので、その時にキーが打てないのは困ります。現在はサイドウィンドウのスロット番号にランダムな数字を入れておいて完全に消されないようにしていますが、それでも一部が隠れてしまい狙ったキーが入力できないときがあります。私はサイドウィンドウをあまり積極的に使っていないので、仮想キーボードの配置場所を上側に移すことで回避できました。上下両方に何かのサイドウィンドウを表示している人は困るかもしれません。

他にもたまにキーを押したつもりがマウスイベントとして解釈されてエラーが発生する場合があるような気がします。

いくつか問題はありますが、とは言え実用的にはほとんど困らない程度にはなかったかなと思っています。

2025-11-29

よく分かるEmacs Lisp入力関数関連図

Emacsのキー入力関数ってなんか似たような名前が多くてすぐに理解できませんよね。調べてだいたい分かったつもりになっていても、しばらくしたら忘れている自信があります。なので図にしておきました。自分のために。

read_charread_key_sequenceread_key_sequence_vsread-key-sequenceread-key-sequence-vectorread_filtered_eventread-eventread-charread-char-exclusivecommand_looprecursive_edit_1read_minibufread-from-minibuffercompleting-read-defaultcompleting-readread-multiple-choiceread-keyread-char-choiceread-quoted-charx-popup-dialoglread.ckeyboard.csubr.elminibuf.c/minibuffer.elrmc.elsimple.elcompleting-read-functionmenu.c

Emacs Lisp リファレンスマニュアルで言うと「Reading Input (GNU Emacs Lisp Reference Manual)」に書いてある関数のことです。

Reading Input (GNU Emacs Lisp Reference Manual)

大別すると read_filtered_event を介して直接的に一つのイベントを読み取る関数群と、 read_key_sequence を介してキーマップによって決まる一続きのキー列を読み取る関数群とに分けられるようです。普段バッファの中で使っているのは後者ですね。コマンドループを通じて read_key_sequence を呼び出しています。それだけに read_key_sequence の方が複雑で難しいです。

read_filtered_event 系には三つの関数がありますが、文字入力イベントのみに限定するバージョン(read-charread-char-exclusive)と全てのイベントを読み取るバージョン(read-event)に分かれます。 read-charread-char-exclusive の違いは、文字以外のイベントが来たときにエラーにするか、排除して続行するかの違いです。

  • read_filtered_event
    • 文字のみ
      • 文字以外でエラー : read-char
      • 文字以外は無視 : read-char-exclusive
    • 全て : read-event

read-charread-char-exclusive は、 read-event に比べると次の処理が加わっています。

  • text-conversionの無効化と復元 (Androidの場合IMEによって直接バッファを書き替える仕組みが存在します)
  • 非文字イベント発生時のエラー(read-char)またはリトライ(read-char-exclusive)
  • switch-frameイベントの遅延 (非文字イベントだが特別扱い)
  • イベントタイプシンボルの文字コード化 (例えばtabを9にします)
  • 修飾キービットの正規化 (主にshiftとcontrolの処理です。例えば25ビット目(?\S-\0)が立っていてベース文字がアルファベット小文字なら大文字にしてビットを消します。control(26ビット目)が立っている文字を制御文字へ変換したりもします)

当然ですが read_filtered_event 系関数にはキーマップは作用しません。

read_filtered_eventread_key_sequence に共通な処理は read_char の中に色々入っています。 unread-command-events の処理とかキーマクロの再現に関するものとか。

2025-11-13

タッチスクリーンで慣性スクロールする

Android版のEmacsを使っているとすぐに慣性スクロールが無いことに気がつくでしょう。つまり画面をスワイプしてスクロールするときに、弾くように指を離したらそのまましばらく勢いでスクロールが継続して欲しいわけです。これが無いと指を離すたびに「ピタッ」とスクロールが止まるので、何画面分もスクロールしなければならないときにとても疲れます。

これは慣性スクロールを実装しなきゃダメかな……と憂鬱になりながら少し調べてみたところ、pixel-scroll.elpixel-scroll-*-momentum という名前の変数や関数が存在することに気がつきました。喜び勇んですぐに pixel-scroll-precision-use-momentumt にして、 pixel-scroll-precision-mode を有効にしてみましたが……あれ、何も変わりません。うーん、どうなっているんだろう。

Googleで検索したら次のredditの投稿が見つかりました。

How to config to enable pixel scroll precision momentum-based scrolling on Android? : r/emacs

おおー、素晴らしい!

というわけで次の設定をしたらちゃんと慣性が働くようになりました。

;; タッチによるスクロールをピクセル単位にする(必要?)
(setq touch-screen-precision-scroll t)

;; 慣性スクロールを有効にする
;; https://www.reddit.com/r/emacs/comments/1mtouxh/how_to_config_to_enable_pixel_scroll_precision/
(defun touch-scroll-momentum (_dx dy)
  (pixel-scroll-accumulate-velocity (- dy)))
(advice-add 'touch-screen-handle-scroll :before 'touch-scroll-momentum)
;; (2025-11-14追記:長押しスクロールが効かなくなってしまったので修正)
;;(keymap-global-set "<touchscreen-end>" 'pixel-scroll-start-momentum)
(defun my-touch-screen-handle-touch:scroll-end (event &rest _)
  (when (eq (car event) 'touchscreen-end)
    (pixel-scroll-start-momentum event)))
(advice-add 'touch-screen-handle-touch
            :before ;;afterではダメ
            'my-touch-screen-handle-touch:scroll-end)

(setq pixel-scroll-precision-use-momentum t)
(pixel-scroll-precision-mode)

ただ、私の環境だとorg-mode文書のスクロールはかなりカクつきます。org-modernやphscrollで色々凝ったことをしているせいかもしれませんが。

2025-11-12 ,

Emacsのツールバーをカスタマイズする

Android版のEmacsのためにツールバーをカスタマイズしました。

修正前の状態は次図の通り。

ツールバー修正前
図1: ツールバー修正前

今回はあくまで一番上の標準的なツールバー(tool-bar-mode)のお話しです。その下にあるキー入力用のバーは無視してください(これもそのうち何とかしなきゃいけませんが(2025-12-09追記:何とかしました))。

上図はデフォルトの状態なのですが、どのボタンが何をするか分かるでしょうか。私は正直よく分かっていませんでした。一番左からファイル新規作成、ファイルを開く、ディレクトリを開く、バッファを閉じる、セーブ、アンドゥ、カット、コピー、ペースト、インクリメンタル検索となっています。

一番問題なのは最初の「ファイル新規作成、ファイルを開く、ディレクトリを開く」の部分です。これって実際に何をするか分かりますか? Emacsでは通常全てfind-fileで行うものだと思います。実際に割り当てられているコマンドは左から、find-file、menu-find-file-existing、diredとなっています。find-fileだけでいいじゃん! (だいたい新規作成なら既存のファイルを選んだら「上書きしますか?」と聞かなければいけませんよ。しかし当然ですがfind-fileはそんなことはしません)(2025-12-10追記: このあたりはOSごとのファイル・ディレクトリ選択ダイアログによって状況が異なるかもしれません。Androidではダイアログが実装されておらず単にミニバッファからファイルやディレクトリを選択する流れになります。MS-Windowsではfind-fileで出現するダイアログではディレクトリが選択出来ません。たしかMacだと既存のファイルを選択するときの挙動も異なっていたような)

それとバッファを閉じるための×が押しにくいんですよね。一番左に配置しましょう。

カット、コピーは長押しのコンテキストメニューでやっているのでここには要らないかなーと思います。ペーストくらいは残しておいても良いかな? この辺りはまた後で変えるかも。

他にもいくつかよく使う操作をツールバーのボタンにしたいと思います。

それと全体的にアイコンが小さすぎません? いや、これはデフォルトフォントサイズの設定に合わせて変わってしまっています。私はデフォルトフォントを少し小さめにしてしまったので、ツールバーのボタンも一緒に小さくなって押しづらくなってしまいました。そしてツールバーのボタンのサイズだけを調整する方法が見当たりません。tool-barフェイスの:heightを変更してみてもこれはテキストのみにしか効果が無いらしくアイコンのサイズは変わりませんでした。うーん困った。

で、色々やってみた結果がこちら。

ツールバー修正後
図2: ツールバー修正後

ボタンの数が減ってシンプルで分かりやすくなりました。ボタンのサイズも大きくなって押しやすくなりました。

一番右に追加したボタンはbeginning-of-bufferとend-of-bufferです。長いファイルの途中にいるときにタッチによるスクロールだけで大きく移動するのは大変です。かといってM-<やM->を押すのもなかなか面倒なので追加してみました。

作成したコードは次のようになりました。

;; 項目のカスタマイズ(既存項目の削除、並び順の変更)
;; tool-bar-mapを直接変更します。
;; tool-bar-setupが呼び出された後じゃないと正しく動作しません。

(defun my-tool-bar-map--customize-items (map)
  "tool-bar-mapの項目をカスタマイズします。"
  ;; new-file、open-file、diredは一つに統合する
  ;; (OSによってはファイル選択ダイアログで狙ったファイルやディレクトリ
  ;; が選択出来ない場合もあるが)
  (define-key map [find-file]
              (list 'menu-item "Open file" #'find-file
                    :image (tool-bar--image-expression "open")))
  ;; 不要なアイコンを削除
  ;;   - new-file、open-file、diredは新しく定義し直す
  ;;   - cut、copy、pasteはメニューのEditの押しやすい位置にあるから不要
  (dolist (key '(new-file open-file dired cut copy paste))
    ;; 削除は (define-key map (vector key) nil t) でもよいが、remove引
    ;; 数はEmacs29が必要
    (setf (alist-get key (cdr map) nil t) nil))
  ;; separator-2と3を削除
  (setf (alist-get 'separator-2 (cdr map) nil t) nil)
  (setf (alist-get 'separator-3 (cdr map) nil t) nil)
  ;; kill-bufferをツールバーの先頭に移動
  ;; kill-bufferの右にseparator
  (let ((item (assq 'kill-buffer (cdr map))))
    (setf (alist-get 'kill-buffer (cdr map) nil t) nil)
    (setf (alist-get 'separator-0 (cdr map) nil t) nil)
    (setcdr map (cons item
                      (cons
                       (list 'separator-0 "--")
                       (cdr map))))))

;; 項目の追加

(defconst my-tool-bar-map--additional-items
  '(;; 2025-12-10追記: org-captureボタンを追加
    (org-capture "Org Capture" org-capture "<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" width=\"24\" height=\"24\" viewBox=\"0 0 240 240\"><path d=\"M102 40H138V102H200V138H138V200H102V138H40V102H102Z\" fill=\"#444\" stroke=\"none\" /></svg>")
    (top "Top" beginning-of-buffer "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" version=\"1.1\"><path stroke=\"none\" d=\"M4 2H20V4H12L18 10H14V22H10V10H6L12 4H4Z\" fill=\"#444\" /></svg>")
    (bottom "Bottom" end-of-buffer "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" version=\"1.1\"><path stroke=\"none\" d=\"M4 22H20V20H12L18 14H14V2H10V14H6L12 20H4Z\" fill=\"#444\" /></svg>")))

(defun my-tool-bar--add-item (map id text cmd image-data)
  ;; 2025-12-10修正: define-key-afterを使うように修正
  (define-key-after map (vector id)
    (list 'menu-item text cmd
          :image
          (list 'quote
                (create-image image-data 'svg t :scale 'default)))))

(defun my-tool-bar-map--customize-additional (map)
  (dolist (spec my-tool-bar-map--additional-items)
    (apply #'my-tool-bar--add-item map spec)))

;; アイコンサイズの調整
;; image descriptorに:scaleを無理矢理追加して調整します。

(defconst my-tool-bar--icon-scale 2.8) ;; ★要調整

(defun my-tool-bar--adjust-image-scale (image)
  (when image
    (setf (plist-get (cdr image) :scale) my-tool-bar--icon-scale))
  image)

(defun my-tool-bar--adjust-map-image-scale (map)
  (dolist (item (cdr map))
    (when-let* ((image (plist-get (cddddr item) :image)))
      (setf (plist-get (cddddr item) :image)
            (list 'my-tool-bar--adjust-image-scale image)))))

(defun my-tool-bar-map--customize-icon-size (map)
  (my-tool-bar--adjust-map-image-scale map))

;; 初期化

(defun my-tool-bar-map--customize ()
  (let ((map (default-value 'tool-bar-map)))
    (my-tool-bar-map--customize-items map)
    (my-tool-bar-map--customize-additional map)
    (my-tool-bar-map--customize-icon-size map)))

(if (and (boundp 'tool-bar-map) (cdr-safe (default-value 'tool-bar-map)))
    ;; すでにtool-bar-mapが初期化されているときは更新
    (progn
      (my-tool-bar-map--customize)
      (tool-bar--flush-cache)
      (force-mode-line-update))
  ;; まだの時は初期化されるまで待つ
  (advice-add 'tool-bar-setup :after #'my-tool-bar-map--customize))

Emacsのツールバーはカスタマイズ性が悪いですね。どうせみんな使ってないんでしょう? いや、私もPC上では使っていませんが。まさかこの期に及んでツールバーをカスタマイズすることになるとは思いませんでした。

(2025-11-13:追記)org-mode時の折りたたみ操作もやりづらいので、org-mode用の項目も追加しました。

(defvar my-org-tool-bar-map
  (let ((map (make-sparse-keymap)))
    (define-key-after map [separator-org-1] menu-bar-separator)
    (define-key-after map [fold]
      `(menu-item
        "Fold" my-org-fold-current-subtree
        :help "Fold current subtree"
        :image (create-image "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" version=\"1.1\"><path d=\"M12 14 6 20H18\" fill=\"#444\" stroke=\"none\" /><path d=\"M12 10 18 4H6\" fill=\"#444\" stroke=\"none\" /></svg>" 'svg t :scale 'default)))
    map))

(defun my-org-tool-bar-map ()
  ;; make-composed-keymapは機能しない。
  ;; :imageの部分が展開されないから。(See: `tool-bar-make-keymap-1')
  ;; (make-composed-keymap (default-value 'tool-bar-map)
  ;;                       my-org-tool-bar-map)
  (let ((map (copy-keymap (default-value 'tool-bar-map))))
    (set-keymap-parent map my-org-tool-bar-map)
    map))

(defun my-org-fold-current-subtree ()
  "現在のサブツリーを折りたたみます。
ポイントが見出しにあり、その見出しがすでに折りたたまれている場合は、それ
を含む一つ上のサブツリーを折りたたみます。"
  (interactive)
  (if (org-at-heading-p)
      (if (org-fold-core-folded-p (pos-eol))
          ;; 折りたたまれている見出し上にいる場合
          (progn
            (outline-up-heading 1)
            (outline-hide-subtree))
        ;; 折りたたまれていない見出し上にいる場合
        (outline-hide-subtree)
        (unless (org-fold-core-folded-p (pos-eol))
          ;; 折りたためなかった場合、一つ上を試す
          ;; (空のエントリーの場合は折りたためない)
          (outline-up-heading 1)
          (outline-hide-subtree)))
    ;; 見出し以外にいる場合
    (outline-previous-heading)
    (outline-hide-subtree)))

(defun my-org-tool-bar-setup ()
  (setq-local tool-bar-map (my-org-tool-bar-map)))

(add-hook 'org-mode-hook 'my-org-tool-bar-setup)

org-modeの折りたたみ操作はキー操作においても常々不満があります。見出し上なら単に TAB で折りたためますが、エントリーの中では C-c C-p TAB としなければなりませんし、見出しの上でそれを含むサブツリーを折りたたむには C-c C-u TAB としなければならなかったりします。上に書いた my-org-fold-current-subtree コマンドは、どこであっても概ね狙った通りに折りたたむ(閉じる)ことが出来ます。