ノートPCのタッチパッドでタブを切り替えるソフトウェアを作りました ～TouchpadGestures Advanced～

## Windows10のタッチパッドジェスチャ、最高ですよね！

Windows10の標準機能として、ノートPCのタッチパッドを3本指で右にスワイプすると、フォアグラウンドウィンドウを選択できるという機能があります。
<img width="700" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183224/a572c6e8-e38e-5bbf-57bb-041aae351fe6.png"></img>
やってることはショートカットキーでAlt+Tabと入力するのと同じなんですが、これがまあ使いやすい！何が使いやすいって、

- ウィンドウは最近使った順に並んでいるので、3本指でスワイプ直後に指を離すことで直前のウィンドウに戻れる
- Alt+Tabではお目当てのウィンドウに至るまで何回もTabキーを押さなきゃいけないところ、非常に高速かつ直感的にウィンドウを選択できる
- 基本的にウィンドウ選択をこのジェスチャで行うようにすれば、タスクバーを限界まで小さくしたり自動的に隠したりしてもウィンドウ選択に困らない

というわけで、非常に便利で愛用しておりました。ところが、これに慣れすぎてしまった結果、**「ブラウザとかエディタのタブもこんな感じのタッチパッドジェスチャで切り替えたいなあ……」** という思いが年々増していくのでした。

なので、自分で作りました。タッチパッドジェスチャでタブを切り替えるソフトウェアを！！

## TouchpadGestures Advanced
<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183224/052bc3b8-dec4-79e5-5d6b-0c285476f2c3.png"></img>
<iframe width="560" height="315" src="https://www.youtube.com/embed/UiN2GjRNuCY" title="YouTube video player" frameborder="0" allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

こんな感じで動きます。OSはWindows10のみ対応、ブラウザはFirefoxとChromium系(ChromeとVivaldiは動作確認済)に対応しています。アプリケーション本体は[GitHubのリリースページからダウンロード](https://github.com/kamektx/TouchpadGestures_Advanced/releases)でき、また専用のブラウザアドオンは[Chrome用がここ](https://chrome.google.com/webstore/detail/webextension-for-touchpad/iheedacijllmhlfbahgjekdjmcpelkjk)、そして[Firefox用がここ](https://addons.mozilla.org/ja/firefox/addon/touchpadgestures-advanced/)からインストールできます。

## インストール方法

- インストーラーを[GitHubのリリースページからダウンロード](https://github.com/kamektx/TouchpadGestures_Advanced/releases)します。
- `setup.zip`を解凍します。
- `setup.exe`を実行します。.Net Core 3.1ランタイムがPCにインストールされていない場合、管理者権限が必要になります。
- **Windowsのデフォルトのタッチパッドジェスチャ設定を変更します。スタートボタンを押し、歯車マークの設定を開き、「デバイス」に行き、「タッチパッド」タブを開き、「関連設定」にある「ジェスチャの詳細な構成」を開き、「三本指ジェスチャの構成」で「上」と「下」を「何もしない」にします。** 「右」は「アプリの切り替え」にしておくことをオススメします。
- 専用のブラウザアドオンを[Chrome用はここ](https://chrome.google.com/webstore/detail/webextension-for-touchpad/iheedacijllmhlfbahgjekdjmcpelkjk)、[Firefox用はここ](https://addons.mozilla.org/ja/firefox/addon/touchpadgestures-advanced/)からインストールします。

## 使い方

### ブラウザの場合

<img width="250" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183224/d8ad03f5-7229-d5ac-a006-d37f47c74b85.png"></img>
3本指で下にスワイプすると……
<img width="700" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183224/2b0dae62-d8ca-21e4-1892-7eb8bcde2e73.png"></img>
タブ選択画面が出現！3本指を動かして選択、指を離すと確定・タブ遷移

### エディタなどの場合

<img width="250" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183224/808475ca-cb4d-1ae1-3f77-72e3eb218383.png"></img>
3本指で上下にスワイプすると……
<img width="700" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183224/7bf8833e-48e9-6312-c295-f28822fd392c.png"></img>
仮想的にCtrl (+ Shift) + Tab を入力しタブ切り替え！

## 開発中の機能

- 設定画面
  - 設定画面を作るのが面倒くさくて未作成なのですが、 `%APPDATA%/local/TouchpadGestures_Advanced` 内のjsonファイルを書き換えたり追加することで将来的な設定変更を可能にしています。

## 開発までの道のり

最初はブラウザのタブ選択機能をブラウザのアドオンとして開発しようと試みました。ところが、僕のメインブラウザであるFirefoxでは**アドオンからタッチパッドの詳細なデータを取得できない** ということが判明しました(Chromeは[HID API](https://developer.chrome.com/docs/extensions/reference/hid/)があるので取得できるかも)。非常に困った。

そんなとき、ローカルにインストールしたプログラムとブラウザアドオン間で通信するための[NativeMessaging API](https://developer.mozilla.org/ja/docs/Mozilla/Add-ons/WebExtensions/Native_messaging)なるAPIが存在すると知りました。これを使えばいけるかもしれない……

こうして、**ウィンドウ表示及びタッチパッド情報取得を行うメインプログラム、ブラウザとメインプログラム間のインターフェイスとなるサブプログラム、そして専用のブラウザアドオン** というソフトウェア構造を考え、実装しました。

## ソフトウェアの構造

![スクリーンショット 2021-05-20 084841.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183224/cf169a28-cfe4-8a8a-a834-ad27c341b851.png)
以上の画像の通り、C#・C++・TypeScriptと**3つもの言語を用いる** こととなりました。特にメインのプログラムで使用したWPFは未経験のライブラリであったため、学習にかなり時間を費やしました。

さて、なぜローカル側のプログラムが2つ必要だったのか。それは、**NativeMessaging APIではブラウザから新規プロセスとしてローカルのプログラムが立ち上がり、ブラウザを閉じるとローカルのプログラムも強制終了される** という制約があったからです。どうせならブラウザだけでなくエディタ等のアプリケーションにおいてもタブ切り替えをしたかったし、またChromeとFirefoxの両方とも起動しているような状況にも対応したい。よって、ブラウザとNativeMessagingするサブのプログラム(以下NMC.exe[^1])はインターフェイスとしての役割に徹し、他にメインのプログラム(以下TGA.exe)を実行しておくというのが最適だろうと判断しました。

[^1]: `NativeMessaging_Cliant.exe`の略。どうやらNativeMessagingでは本当はブラウザがCliantでローカル側がHostと呼ばれるらしいですが、その誤りに気づいた頃にはだいぶ開発が進んでいて、名称変更が面倒臭すぎたのでそのままにしています。

## タッチパッド詳細データの取得

さて、どうにかしてタッチパッドの詳細データを取得しなくては何も始まらないのですが、これが大変でした。情報が少ない中、[Raw Input](https://docs.microsoft.com/en-us/windows/win32/inputdev/raw-input)と[hidpi.h](https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/hidpi/)なるガチガチのWindows32 APIを利用すればよいということが分かりました。自プロセスのウィンドウをRaw Inputに登録すると、そのウィンドウのWindowsイベントループにHIDデバイス入力の発生とその入力のIDが通知されるようになり、そのHID入力IDを`hidpi.h`内の関数に渡すことで、詳細なHID情報が取得できるというものです。

ところが、TGA.exeはC#で開発されており、C言語ベースである`hidpi.h`内の複雑怪奇なWindows32 APIを[PInvoke](https://pinvoke.net/)を利用して叩くのは困難極まります。よって、C++でこれらのAPIを叩いて取得したHID情報をJSONデータにして返してくれる関数を含むDLLを自作し、メインプログラムのC#ではそれをDLL Importして利用することにしました。

## プロセス間のデータ受け渡し

まずNMC.exeがブラウザから立ち上げられるたびにTGA.exeを起動させ[^2]、立ち上がったTGA.exeでは他にTGA.exeが立ち上がっているかどうかMutexにより判断し、立ち上がっている場合そのTGA.exeは即終了します。次にNMC.exeは256bit乱数→Base32エンコードにより生成されたKeyと自分のPIDをレジストリに書き込みます。また `%APPDATA%/local/TouchpadGestures_Advanced/NMC` ディレクトリにKeyを名前とするフォルダを作成し、その中にTGA.exeに渡す用のJSONファイルとTGA.exeから受け取る用のJSONファイルを生成します。その後NMC.exeは初期化終了を伝えるOSイベントを発行し、それをTGA.exeは購読していて、すぐさまそのNMC.exeを監視下に加えます。

<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183224/f9ff94f5-bb54-8244-8a18-61a0910f1b92.png"></img>
実際のレジストリ。
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183224/87cf59e7-6bfd-5e08-091b-331aba93501f.png"></img>
実際のフォルダ。`sending_object.json`がブラウザ→NMC.exe→TGA.exeと渡す用のJSON、`for_receiving.json`がTGA.exe→NMC.exe→ブラウザと受け取る用のJSON。

ブラウザからウィンドウ・タブ状況を示すJSONがNMC.exeに送られてくると、NMC.exeはそれを渡す用のJSONファイルに上書きします。TGA.exeではそのJSONファイルの書き込みイベントを監視していて、変更があれば読み取ってJSONをデシリアライズします。同様にTGA.exeではタブ選択データ等を受け取る用JSONファイルに書き込み、NMC.exeはその書き込みイベントを監視していて、変更があればすぐさまブラウザにそれを送信します。

ブラウザからNMC.exeにはJSONファイルの他にも、スクリーンショット画像データ・ファビコン画像データ・ファビコンURL・スクリーンショット撮影エラーなどが送られてきます。画像データはBase64でエンコードされており、これをバイナリにデコードしてフォルダ内に画像ファイルとして生成します。Chromeではファビコンは画像データではなくURLで送られてくるので、仕方なくCurlでデータを取得します。さらにファビコン画像は多種多様な形式が存在し、WPFで表示できる形式ではなかったりもするので、ImageMagickLibraryですべてpngに変換しています。またChromeではスタートページやアドオン管理ページなどがスクリーンショットできない制限があるので、その場合Windows32 APIでChromeウィンドウごとスクリーンショットの撮影を行います。Chrome対応は本当に大変でした。

TGA.exeでは1分に一度、NMC.exeの起動状況をPIDから判別し、すでにプロセスが終了していた場合、 `%APPDATA%/local/TouchpadGestures_Advanced/NMC/%KEY%` ディレクトリを消去、レジストリも消去し、監視を終了します。

[^2]: [Job Objects](https://docs.microsoft.com/en-us/windows/win32/procthread/job-objects)という仕組みにより本来ブラウザからNMC.exeが強制終了されると自動的に子プロセスであるTGA.exeも終了させられてしまうのですが、[CreateProcessW](https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw)関数の`dwCreationFlags`引数に`CREATE_BREAKAWAY_FROM_JOB`定数を指定することで、これを回避しています。

## NativeMessagingの標準入出力通信における再現性のないバグ

NativeMessaging通信では標準入出力で通信を行うのですが、ごくまれにブラウザからのデータの読み取りに失敗し、以降通信が不可能になるというバグが発生しました。このバグは再現性に乏しく、かなり悩まされました。

NativeMessagingでは、ブラウザから送られる標準入力の最初の4バイトが続けて送られてくるデータのバイト数のバイナリとなっています。このバグの原因は、**この4バイトの中にASCIIでEOFを示す0x03等の特殊文字が含まれると標準入力の読み取りを終了してしまう**というものでした。

解決策は、C言語関数の`_setmode( _fileno( stdin ), _O_BINARY )`を一度実行しておくことで、標準入力をバイナリとして扱うというものでした。参考になれば幸いです。

## Q. ところでEdgeでタッチパッドジェスチャによるタブ選択、できるよね？？

あらかた開発し終わってから気付きました。やられた、と思いました。