- この投稿は個人サイトとのクロスポストです。
- この記事はFirefox 64での改善についての話で、Firefox ESR60は対象外です。
朗報があります。2016年6月にWebExtensionsへ一本化の方針が示された時に出した要望であるBug 1280347 - Add ability to provide custom HTML elements working as alias of existing Firefox UI items, especially tabsが、最近ようやく解決されました。
何故これが朗報なのでしょうか? アドオンのXULからWebExtensionsへの移行の話をおさらいしてみましょう。
XULアドオン同士は暗黙的に連携できた
いにしえの時代、XUL製アドオンは同じ名前空間の上で動作し、Firefox自体のUI要素や挙動を自由に変更する事ができました。
旧ツリー型タブの「ツリー表示されたタブ」はFirefox自体のタブバーそのもので、他のアドオンによって提供されるタブ関係の機能もそのまま利用できました。
ツリー型タブの移行の話に書いた通り、このようにXULアドオン同士は暗黙的に連携する事ができました。これは他のアプリの拡張機能の仕組みに対する大きな利点でしたが、このような単一の名前空間で多数のアドオンが動作するというモデルは混乱の元でもあり、そこにはセキュリティ上のリスクもありました。
WebExtensionsのアドオンは分離され、他の物と連携するための特別な努力が必要となった
他方で、WebExtensions APIは個々のアドオンを別々の名前空間に分離するように設計されています。
他のアドオンが追加したコンテキストメニュー項目はツリー型タブのコンテキストメニューでは利用できません。何故かというと、ツリー型タブのコンテキストメニューはサイドバー内でHTMLとCSSを使って構築された、偽物のメニューに過ぎないからです。そのため、偽コンテキストメニューはサイドバーのフレームからはみ出す事もできず、表示幅が狭く使いにくい物となってしまっていました。
他のアドオンとの互換性の問題に対する回避策として、僕は他のアドオンからbrowser.runtime.sendMessage()
経由で呼び出す事のできる公開のAPI群をツリー型タブに実装しました。
ツリー型タブの独自のAPIはアドオン同士の連携を部分的に可能にしましたが、ツリー型タブのAPIを明示的に呼び出すためのコードを追加する必要がありました。いくつかのアドオンの開発者の方は実際にこれを使ってくれて、また、僕自身もいくつかのアドオンにプルリクエストを送りました。しかし、アドオンの総数はあまりに多いため、将来的な事も考えると、このやり方には限界がありました。
このような辛い状況を打破する、Firefox 64での改良点とは?
Firefox 64では、ポップアップまたはサイドバーのパネルにおいて、他のタブまたはブックマーク関係のアドオンが追加した項目を含むコンテキストメニューを表示できるようになりました。これはつまり、追加のコンテキストメニューを提供するWebExtensionsベースのアドオンが、特別な対策無しにツリー型タブと協調動作できるようになったという事を意味します。
どのようにすればそういったアドオン同士の連携が可能になるのでしょうか? WebExtensionsのアドオンにおける独自のコンテキストメニュー実装の基本をおさらいしてみましょう。
ユーザーが右クリック(またはそれ以外の、コンテキストメニューを開くための何らかの操作)をした時には、対象のDOM要素からcontextmenu
というDOMイベントが発火し、既定のコンテキストメニューが表示されます。
このイベントはpreventDefault()
メソッドを呼ぶ事でキャンセルでき、そうすると既定のコンテキストメニューもまたキャンセルされるようになっています。その後はアドオンの側で好きな事ができるため、ツリー型タブはここで、Firefoxのタブのコンテキストメニューを模したメニュー風のUIを勝手に描画するという事をしていました。長らく、これがWebExtensionsのアドオンが独自のコンテキストメニューを提供するための一般的な方法でした。(他のアプローチとして、HTML5の<menu>要素およびその関連要素を使う方法もありましたが、僕は、アドオンの中で使うのには色々と問題が多いという印象を受けました。)
Firefox 64以降では、新たにbrowser.menus.overrideContext()
というAPIが導入されます。
このメソッドはcontextmenu
イベントのハンドラから呼ぶ事ができ、その際に、開かれようとしているコンテキストメニューに対する適切な「コンテキスト(文脈)」を指定できるようになっています:
この時、既定のコンテキストメニューは指定されたコンテキストに合わせた内容に切り替わります。この時表示されるメニューは偽物ではなくFirefox自身により提供される本物のメニューなので、他のアドオンによって追加されたコンテキストメニュー項目も利用できますし、サイドバーやポップアップのパネルの領域からはみ出す事もできます。
どうすればこの機能を使えるのか?
この機能を使うにあたっては、以下の3つのステップが必要となります。
- 新しい権限として
menus.overrideContext
を追加する -
browser.menus.overrideContext()
でコンテキストを指定する -
browser.menus.create()
で、パラメータにviewTypes
を指定してメニュー項目を追加する
様々な場合についてコンテキストメニューの追加の仕方を解説した別の記事を別途用意しましたので、この記事では一般的な話のみに限定して説明します。
新しい権限menus.overrideContext
の追加
新設のAPIであるbrowser.menus.overrideContext()
は、そのアドオンがmenus.overrideContext
というこれまた新設の権限を持っている時にだけ呼び出し可能になります。
そのためには、単純にmanifest.json
のpermissions
のリストにmenus.overrideContext
を加えればOKです。アドオンをFirefox 63またはそれ以前のバージョンにも対応させなくてはいけない場合でも問題ありません。というのは、古いFirefoxは未知の権限を単純に無視するからです(実際に、Firefox ESR60画素のような動作になる事を確認済みです)。なので、この権限をoptional_permissions
配下に置いておき、インストール後に権限を取得するためにbrowser.permissions.request({ permissions: ['menus.overrideContext'] })
を実行する、というようなめんどくさい事をする必要はありません。
browser.menus.overrideContext()
にコンテキストを指定する
以下は、独自のUI上でタブのコンテキストメニューを開く時のコードの例です:
document.addEventListener('contextmenu', event => {
const tab = event.target.closest('.tab');
if (tab && typeof browser.menus.overrideContext == 'function') {
// タブに対応する要素の上でコンテキストメニューが開かれた場合、
// 「指定のタブの上でタブのコンテキストメニューが開かれた」もの
// であると指定する。
browser.menus.overrideContext({
context: 'tab',
tabId: parseInt(tab.dataset.id)
});
}
else {
// そうでないなら、メニューを開かない。
event.preventDefault();
}
}, { capture: true });
同様に、以下は独自のUI上でブックマークのコンテキストメニューを開く時のコードの例です:
document.addEventListener('contextmenu', event => {
const item = event.target.closest('.bookmark');
if (item && typeof browser.menus.overrideContext == 'function') {
// ブックマークに対応する要素の上でコンテキストメニューが開かれた場合、
// 「指定のブックマークの上でブックマークにのコンテキストメニューが
// 開かれた」ものであると指定する。
browser.menus.overrideContext({
context: 'bookmark',
bookmarkId: parseInt(item.dataset.id)
});
}
else {
// そうでないなら、メニューを開かない。
event.preventDefault();
}
}, { capture: true });
しかしながら、これだけだと他のアドオンによって追加された項目だけが表示されるという、ずいぶん貧相なメニューを目にする事になります。
既定のコンテキストメニューの項目が無いのはバグだ!と思うかもしれませんが、これは仕様です。パッチを書いた人はこのように説明しています:
Firefoxの既定のメニュー項目を取り込む事は目的外です、何故なら既定のメニュー項目のラベルは常に適切な意味をなすとは限らないからです。例えば、「右側のタブを閉じる」は縦型のタブバーを提供する拡張機能においてはおかしな意味になるでしょう。
拡張機能用のAPIで再実装できないメニュー項目がある場合は、それらがどのように対応されるべきであるかケースバイケースで判断する事になります。
メニュー項目をviewTypes
パラメータを伴って追加する
以上の事から、もしFirefoxの既定のコンテキストメニューと互換性のあるメニューを提供したいのであれば、それらのメニュー項目は自分自身で実装し直さなくてはなりません。
(それだけでなく、最上位のメニュー項目として他に任意の項目を追加する事もできます。)
ご存じの通り、browser.menus.create()
で追加された複数のメニュー項目は自動的にサブメニュー配下にまとまるようになっています。しかし、これには例外があります。browser.menus.overrideContext()
を呼び出したアドオン自身が追加したメニュー項目だけは、サブメニューにまとまらずに最上位に列挙されるようになっているのです。そのため、Firefoxの既定のメニュー項目を真似た項目やそれ以外の任意の項目を、何の心配もなく最上位のメニュー項目として表示させる事ができます。
また、browser.menus.create()
の新しいパラメータであるviewTypes
が、ポップアップパネルやサイドバーパネルの上でだけ表示されて欲しい項目を追加する時に役立ちます。
browser.menus.create({
id: 'context_reloadTab',
title: browser.i18n.getMessage('context_reloadTab_title'),
type: 'normal',
contexts: ['tab'],
viewTypes: ['sidebar'], // これが重要!!
documentUrlPatterns: [`moz-extension://${location.host}/*`] // これも重要!!
});
viewTypes
は以下のうちいずれか1つ以上の値を含む配列です:
-
popup
: ポップアップパネル上で開かれたコンテキストメニュー用 -
sidebar
: サイドバー領域上で開かれたコンテキストメニュー用 -
tab
:Firefox本体のタブバー上コンテンツ領域上で開かれたコンテキストメニュー用
「タブのコンテキストメニュー」を意味するviewTypes
の値は存在しない、という事には注意が必要です。「ポップアップパネルでだけ表示されず、サイドバーとタブバーでは表示される」という結果を得たい場合にviewTypes:["tab","sidebar"]
と指定するのは間違いで、これだと「ポップアップパネルとタブバーでは表示されず、サイドバーとコンテンツ領域では表示される」という結果になってしまいます。意図通りの結果を得るためには、viewTypes
を指定せずに、browser.menus.onShown
のリスナーに渡されるinfo
のviewType
の値に基づいて、項目の表示・非表示をbrowser.menus.update()
のvisible
オプションで動的に切り替える必要があります。
contexts
とviewTypes
の両方を同時に指定した場合、その項目は両方の条件が満たされる時だけ表示されるようになります。
documentUrlPatterns
の指定は、あなたのアドオンが最上位のメニュー項目として追加した項目が、他のアドオンによって提供されたポップアップパネルやサイドバーパネル上では表示されないようにするために必要です。Bug 1498896によって、documentUrlPatterns
はポップアップパネルおよびサイドバーパネル自体のURLに基づいて項目の表示・非表示を制御するために使えるようになっています。
あなたのアドオンが提供するポップアップパネルやサイドバーパネル以外でだけ表示したいメニュー項目がある場合はどうすればよいでしょうか? 例えばマルチプルタブハンドラがそういう事をしています。マルチプルタブハンドラのポップアップパネル上では独自の最上位のメニュー項目が表示されますが、それ以外の場面では「選択されたタブ」というサブメニューの下に項目が置かれるようになっています。サブメニューのラベルがアドオン自体の名前と同じになっていて構わないのであれば、難しく考える必要はありません。単にviewTypes
を指定せずにメニュー項目を追加するだけでOKです。しかし、アドオンの名前と違うラベル文字列をグループ化用のサブメニューに使いたい場合には、最上位のメニュー項目用の項目とは別にそのような項目を定義し、表示・非表示を個別に制御する必要があります。
あなたのアドオンのポップアップパネルまたはサイドバーパネル上でのみ表示されて欲しいメニュー項目は、viewTypes
とdocumentUrlPatterns
の併用で簡単に定義できます。その一方で、あなたのアドオンのポップアップパネルまたはサイドバーパネル上でのみ非表示になって欲しい項目を定義するのはちょっと面倒です。それをするには、項目の表示・非表示をbrowser.menus.update()
のvisible
オプションで動的に切り替える必要があります。どのようにやればいいのか、マルチプルタブハンドラの当該コミットから重要箇所を抜き出してみます:
-
ポップアップパネルまたはサイドバーパネルの開閉状態をトラッキングするためのコードを追加します。バックグラウンドスクリプト側:
let gIsPanelOpen = false; browser.runtime.onConnect.addListener(port => { if (!/^connection-from-my-panel:/.test(port.name)) return; gIsPanelOpen = true; port.onDisconnect.addListener(() => { gIsPanelOpen = false; }); });
ポップアップパネルまたはサイドバーパネル側:
browser.runtime.connect({ // 接続名は一意である必要がある事に注意! name: `connection-from-my-panel:${Date.now()}` });
-
メニュー項目の表示状態を更新するコードをバックグラウンドスクリプトに追加します:
if (typeof browser.menus.overrideContext == 'function') { let gLastVisible = true; browser.menus.onShown.addListener(async () => { const visible = !gIsPanelOpen; if (gLastVisible == visible) return; await browser.menus.update('id-of-the-item-you-want-to-hide-in-your-panel', { visible }); gLastVisible = visible; browser.menus.refresh(); }); }
viewTypes
はFirefox 64以降、visible
はFirefox 63以降で追加された機能である事に注意が必要です。あなたのアドオンをFirefox 63以前のバージョンでも使えるようにしたい場合、これらのオプションを伴ってメニュー項目を追加・更新するコードはFirefox 64以降でのみ実行されるようにしておく必要があります。これは、例のようにtypeof browser.menus.overrideContext == 'function'
の結果がtrue
である場合にのみ実行するようにすればOKです。
既定のコンテキストメニュー項目の機能の再現
タブのコンテキストメニューの既定の機能のうちのほとんどは、WebExtensions APIで再現する事ができます。すべての機能の再現は大変ですが、ツリー型タブでは実際にそれをやっています。
コンテナータブ用に色分けされたSVGアイコンを自分で用意して、それをXPIパッケージの中に含める必要があるという事に注意して下さい。残念な事に、Firefox自体に組み込まれたSVGアイコンを直接参照してカラー表示することは、セキュリティ上の制限により不可能となっています。
また、今の所WebExtensionsのAPIが不足しているために、「タブを端末へ送信」は再現できないというのも残念なところです。
まとめ
Firefox 64でできるようになった事:
- タブやブックマークについて、他のアドオンによって追加された追加のコンテキストメニューを含む、独自のコンテキストメニューを開けるようになりました。
アドオン同士の暗黙的な連携が、部分的にですが再び可能になりました。 -
browser.menus.overrideContext()
を呼んだアドオン自身は、複数のコンテキストメニュー項目を最上位のメニュー項目として表示させる事ができます。
(それらは自動的にはサブメニュー配下にグループ化されません。)
依然としてできない事:
- コンテキストメニューを
mouseover
などの任意のタイミングで開く事はできません。この機能はあくまでcontextmenu
イベントが発行される場面でのみ使えます。 - 他のアドオンによって追加されたコンテキストメニュー項目を制御したり挙動を変えたりする事はできません。そのような事をしたい場合には依然として、メッセージベースの独自のAPIを使って連携する必要があります。
- Firefox自体が提供する既定のコンテキストメニュー項目を引用する事はできません。それらを使いたければ、自分で再実装する必要があります。
Firefoxのアドオンの仕組みがXULからWebExtensionsに移行するにあたって、多くの利点が失われ、FirefoxがChromeと同じになってしまったと言う人もいました。しかしながら、Firefoxはそのカスタマイズ性を少しずつ取り戻しつつあります。Firefoxはこの記事で述べたような独自の改良を試みている場合があるため、FirefoxのWebExtensions APIの発展の様子を引き続き注視していく必要がある、というのが僕の見解です。