WebExtensionsによるFirefox用の拡張機能で、キーボードショートカットの変更用UIを提供するライブラリ：ShortcutCustomizeUIMenuUI.js

（この記事は、Firefoxの従来型アドオン（XULアドオン）の開発経験がある人向けに、WebExtensionsでの拡張機能開発でのノウハウを紹介する物で、[所属会社のブログの2018年5月14日の記事](http://www.clear-code.com/blog/2018/5/14.html)の再掲です。）

従来型のアドオンでキーボードショートカットを実現する方法には、XULの`<key>`要素を使う方法と、JavaScriptでキーイベントを捕捉する方法の2通りがありました。WebExtensionsでは、前者は[`manifest.json`の`commands`でショートカットを定義する方法](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/manifest.json/commands)、後者は[content scriptで実装する方法](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Content_scripts)がそれぞれ対応します。ブラウザウィンドウのどこにフォーカスがあっても動作するキーボードショートカットを実現する方法としては、前者の方法が唯一の選択肢となります[^content_script]。

[^content_script]: 使用できるキーの種類などの面で自由度が高いのは後者ですが、実行のためには全てのページでユーザースクリプトを実行する権限（`<all_urls>`）が必要な上に、コンテンツ領域にフォーカスがある時でないとイベントを認識できない、addons.mozilla.orgのページやabout:addonsなどのFirefox自体が提供するページでは動作しない、などの欠点があります。

Firefox 60以降のバージョンでは`manifest.json`で定義されたキーボードショートカットを後から任意の物に変更できるようになったのですが、Google Chromeでは拡張機能が定義したキーボードショートカットを横断して制御できる設定画面をChrome自体が提供しているのに対し、[Firefox 60にはまだその機能がありません](https://bugzilla.mozilla.org/show_bug.cgi?id=1303384 "1303384 - UI for re-assigning an extension's command shortcut")。実際にキーボードショートカットを変更するためには、[`commands.update`という機能](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/commands/update)を使って各アドオンが自前で設定UIを提供する必要があります。

そこで、[Firefox本体で設定UIが提供されるようになる](https://bugzilla.mozilla.org/show_bug.cgi?id=1303384 "1303384 - UI for re-assigning an extension's command shortcut")までのつなぎとして、キーボードショートカットの変更のためのUIを提供する軽量なライブラリ [ShortcutCustomizeUI.js](https://github.com/piroor/webextensions-lib-shortcut-customize-ui)を開発しました。


# 使い方

このライブラリは、以下の2つの機能を含んでいます。

 * キーボードショートカットの設定用UIを生成する
 * 既定のキーボードショートカットを割り当て解除できるようにする

順番に説明してきます。


## キーボードショートカットの設定用UIを生成する

このライブラリは、キーボードショートカットの変更用UIとして機能するHTMLのコード片をDocumentFragmentとして生成する機能を含んでいます。まず、設定画面を提供するHTMLファイルに`ShortcutCustomizeUI.js`を読み込ませて下さい。

```html
<script type="application/javascript" src="./ShortcutCustomizeUI.js"></script>
```

このファイルを読み込むと`ShortcutCustomizeUI`というオブジェクトが利用可能になります。`ShortcutCustomizeUI.build()`を実行するとPromiseが返され、そのPromiseの解決後の値として、生成されたDocumentFragmentが得られます。後は、以下のようにして設定画面の中にDocumentFragmentを埋め込むだけです。

```html
ShortcutCustomizeUI.build().then(list => {
  document.getElementById('shortcuts').appendChild(list);
});
```

すると、以下のスクリーンショットのようなUIが使えるようになります。
![（実際に表示されたUIのスクリーンショット）](https://qiita-image-store.s3.amazonaws.com/0/61387/29672e33-b8aa-aae0-538a-8cd045d28e42.png)
各ショートカットにはコマンドの名前もしくは説明文がラベルとして表示され、行の右端のボタンをクリックすることで初期値に戻すこともできます。

 * このライブラリは`manifest.json`の`commands`で定義されている全てのコマンドを自動的に走査し、UIに列挙します。コマンド名を各言語に応じた翻訳で表示したい場合は、`manifest.json`自体の国際化のための機能を使用して下さい。
 * `ShortcutCustomizeUI.build()`に指定できるオプションの詳細については、[リポジトリ内のREADMEファイル](https://github.com/piroor/webextensions-lib-shortcut-customize-ui)を参照して下さい。
 * 実際にはショートカットとしては使えないキーの組み合わせも登録できる場合がありますが、その場合、そのショートカットは当然ですが機能しません。
 * キー名の入力欄にフォーカスがある状態で「Escape」キーを押すと、ショートカットの割り当てが解除されます。（ただし、後述する理由により、割り当て解除可能なショートカットを提供するには若干の準備が必要となります。）

Firefoxの組み込みのキーボードショートカットと衝突するショートカットを設定した場合、Firefoxの機能の方が優先的に動作します。アドオン側のショートカットを優先することは、現時点ではできません（[1320332 - Support overriding existing keybinding through WebExtensions](https://bugzilla.mozilla.org/show_bug.cgi?id=1320332)も参照して下さい）。




## 既定のキーボードショートカットを割り当て解除できるようにする

WebExtensionsでは通常、アドオンの既定のショートカットは[`manifest.json`の`commands`配下に`suggested_key`として記述します](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/commands)。しかし、素直にこの方法を使って定義した既定のショートカットには、[ユーザーが割り当て解除できない](https://bugzilla.mozilla.org/show_bug.cgi?id=1475043 "1475043 - Allow commands.update() to un-set a shortcut, disabling the command")という問題があります[^bug]。本ライブラリは、このWebExtensions APIの制限事項を回避して「既定のキーボードショートカットをユーザーが任意の割り当て解除する」という事を可能にするための機能を含んでいます。

[^bug]: Firefox 63やFirefox 64といった将来のバージョンで修正される可能性はありますが、セキュリティアップデートのみが提供されるFirefox ESR60ではこの問題は修正されない見込みです。

この機能を使うためには、まず下準備として`manifest.json`の書き換えが必要です。

```json
  "commands": {
    "_execute_browser_action": {
      "suggested_key": { "default": "F1" },
      "description": "__MSG_sidebarToggleDescription__"
    },
```

このように`suggested_key`で定義していた既定のショートカットを、以下のように`description`の中で半角丸括弧で囲う形で記述し直して下さい[^format]。

[^format]: このような書き方をする都合上、`suggested_key`では可能なプラットフォームごとの既定のショートカット割り当ては、このライブラリではできないという制限事項があります。あしからずご了承ください。

```json
  "commands": {
    "_execute_browser_action": {
      "description": "__MSG_sidebarToggleDescription__ (F1)"
    },
```

その上で、`ShortcutCustomizeUI.js`をバックグラウンドページで読み込み、*アドオンのインストール直後の1回だけ`ShortcutCustomizeUI.setDefaultShortcuts()`を実行して下さい*。例えば、`storage.local`を使うなら以下のようにします。

```javascript
(async () => {
  const SHORTCUTS_VERSION = 1;
  const configs = await browser.storage.local.get({ shortcutsVersion: 0 });
  switch (configs.shortcutsVersion) {
    case 0:
      ShortcutCustomizeUI.setDefaultShortcuts();
  }
  browser.storage.local.set({ shortcutsVersion: SHORTCUTS_VERSION });
})();
```

`ShortcutCustomizeUI.setDefaultShortcuts()`を実行すると、ライブラリが`manifest.json`の`description`に記述されたショートカットを自動的に収集し、ショートカットの初期値として自動設定します。このようにして動的に反映されたショートカットは、`browser.commands.reset()`で空の状態にリセットできるため、「割り当て解除可能な既定のショートカット」として振る舞う事になります。

アドオンの更新などで後からまたショートカットを追加したという場合には、`ShortcutCustomizeUI.setDefaultShortcuts()`を再実行するのではなく、以下のようにして、個別のコマンドに対し`ShortcutCustomizeUI.setDefaultShortcut()`を実行するようにして下さい。

```json
  "commands": {
    "_execute_browser_action": {
      "description": "__MSG_sidebarToggleDescription__ (F1)"
    },
    "newCommand": {
      "description": "__MSG_newCommand__ (Ctrl+Alt+PageUp)"
    },
```

```javascript
(async () => {
  const SHORTCUTS_VERSION = 2; // 初期化済みかどうか判定するバージョンを繰り上げる
  const configs = await browser.storage.local.get({ shortcutsVersion: 0 });
  switch (configs.shortcutsVersion) {
    case 0:
      ShortcutCustomizeUI.setDefaultShortcuts();
    case 1:
      ShortcutCustomizeUI.setDefaultShortcut('newCommand');
      ShortcutCustomizeUI.setDefaultShortcut('extraCommand');
  }
  browser.storage.local.set({ shortcutsVersion: SHORTCUTS_VERSION });
})();
```



# 改善のためのご協力のお願い

このアドオンが生成するUIにはキーボードの[モディファイアキー](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/manifest.json/commands#Shortcut_values)やその他のキー名が表示されますが、キーの表示名は言語によって異なる場合があります。そのため、[ライブラリ内で言語ごとの表示名](https://github.com/piroor/webextensions-lib-shortcut-customize-ui/blob/master/ShortcutCustomizeUI.js#L287)を定義していますが、現状ではごく一部の言語のみの対応に留まっています。

もし他の言語のことに詳しい方がいらっしゃいましたら、キーの表示名と内部名の対応表の追加にご協力をいただければ幸いです。


# まとめ

WebExetnsionsによるFirefox用アドオンに簡単にキーボードショートカット変更用のUIを追加できる軽量ライブラリである[ShortcutCustomizeUI.js](https://github.com/piroor/webextensions-lib-shortcut-customize-ui)について、その使い方を解説しました。[「以後確認しない」のようなチェックボックスを伴った確認ダイアログを表示する`RichConfirm.js`](https://qiita.com/piroor/items/588fcb9ef94f913a849d)や、[メニュー風のUIを提供する`MenuUI.js`](https://qiita.com/piroor/items/b7a3a983ba4bdd22bdb9)と併せて、Firefox用アドオンの開発にご活用いただければ幸いです。