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

私も今年Android版のEmacsを利用し始めましたが、このmodifier-bar-modeは当然試しました。
しかし単に修飾キーが欲しいだけならHacker's Keyboardのような豪華な仮想キーボードを使えば十分です。
このように手軽に押せるツールバー上のキーが欲しくなるシチュエーションは、日本語用の仮想キーボードを表示しているときや、そもそも仮想キーボードを非表示にしているときではないでしょうか。それならもっと色々なキーがツールバー上にあっても良いでしょう。
ということで作ったのがこちらのツールバー。

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.
次のスクリーンショットで上側に表示しているのがその仮想キーボードです。

こうやって既存の仮想キーボードと併用することで日本語もフリック入力で打てますし切り替え無しでほぼ全てのキーが入力できます。(この配列は括弧がシフト無しで入力できるようになっています。素晴らしくないですか!?)
キー配列も数種類用意してあります。次のように特殊キーのみの配列を使えばmodifier-bar-modeの代わりとしても使えます。

もちろん配列は自由にカスタマイズ可能です。標準的なカスタマイズ方法としては、 M-x customize-variable vkbd-layout-list というのを用意してあります。
仮想キーボードの表示場所はサイドウィンドウ、子フレーム、独立フレームから選べるようになっています(内部的にはcontainer-typeと呼んでいます)。最初は子フレームとして作ったのですが、後からサイドウィンドウとして表示できるようにもしました。結局同じフレームにウィンドウを分割して表示した方が使いやすいと思います。

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

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

メニューやボタンから選んだ状態は即座に反映され、デフォルトでは ~/.emacs.d/vkbd ファイルに保存され次回起動時には同じ状態からスタートします(保存先は M-x customize-variable vkbd-global-keyboard-user-data-storage で変更可能。項目毎にどこに保存するかをカスタマイズできる過度に複雑な仕組みをご用意しました)。
Android環境では通常の仮想キーボードを無効化するボタンも付けました。それを押すと通常の仮想キーボードは表示されなくなり、もう一度押すと元の設定に戻ります(どのようなときに仮想キーボードを表示するかは元々Emacsの設定である程度制御できます)。
通常の仮想キーボードを無効化すると、完全にこの仮想キーボードだけでEmacsを操作することも可能です。

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

マウスから手を離したくない、キーボードが遠い、といった時には使ってみても良いかもしれません。(私はあまり使わないと思いますが)
実装について
実装の一番のキモはやはりどうやってマウス/タッチイベントをキー入力に変換するのかという点でしょう。
このプロジェクトは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-mapは read_key_sequence 関数からしか使われないからです。その他の入力関数を使用している場合はinput-decode-mapによる変換は行われません。その他の入力関数とはread-event、read-char、read-char-exclusiveのことです。
このあたりのEmacsの入力関数には何があるのかということは前回図解しました。
よく分かるEmacs Lisp入力関数関連図 | Misohena Blog
つまり入力関数には read_filtered_event 系(read-event、read-char、read-char-exclusive)と read_key_sequence 系の2系統があり、その上に read_char という関数があるという構造でした。
read_char のレベルで何かイベントを変換するような仕組みがあれば良かったのですが、私には見つけられませんでした(長いのでろくに読んでいません)。
そこでread-event、read-char、read-char-exclusiveにadvice-addで:around adviceを仕掛けて動作を上書きし無理矢理返ってきたイベントを変換することに。read-char、read-char-exclusiveは元の挙動ではマウス/タッチイベントが返ってこないので、read-eventを呼んで返ってきたイベントが文字で無ければリトライしたりエラーにするといった方法で対処しました。要するにread-charやread-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もヘルプを下側にサイドウィンドウとして表示していました。もし下側に仮想キーボードを表示していると、それらがサイドウィンドウを表示したときに消されてしまい、キー入力が出来なくなってしまいます。どちらもキーを受け付ける専用の状態の時に起こるので、その時にキーが打てないのは困ります。現在はサイドウィンドウのスロット番号にランダムな数字を入れておいて完全に消されないようにしていますが、それでも一部が隠れてしまい狙ったキーが入力できないときがあります。私はサイドウィンドウをあまり積極的に使っていないので、仮想キーボードの配置場所を上側に移すことで回避できました。上下両方に何かのサイドウィンドウを表示している人は困るかもしれません。
他にもたまにキーを押したつもりがマウスイベントとして解釈されてエラーが発生する場合があるような気がします。
いくつか問題はありますが、とは言え実用的にはほとんど困らない程度にはなかったかなと思っています。