宣伝。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。 以下の特設サイトにて、単行本まんがでわかるLinux シス管系女子の試し読みが可能!
Tokyo WebExtensions Meetup #3で、標題の通りの発表をしました。スライドはQiitaにありますが、こちらにもクロスポストしておきます。
タブの複数選択機能が入った事で「選択」という言葉が多義的になってしまったので、まずその点を整理します。 WebExtensions APIにおいては、「選択」という言葉で表されうる状態に以下の2つがあります。
「selected tab」という表現は紛らわしいので、このエントリでは使いません。
Firefox 63でabout:config
でbrowser.tabs.multiselect
をtrue
にすると試せます。Chromeでも同じ操作ができます。
WebExtensionsベースのアドオンにとっては、このタブの複数選択機能に対して2つの関わり方があります。
それぞれ順番に紹介します。 なお、基本的にはChromeの拡張機能でも同様のAPIが使えます。
タブの選択状態は、browser.tabs.Tab.highlighted
で表されます。
true
true
browser.tabs.*()
等のAPIで返ってくるタブのオブジェクトはすべてこの情報を持ちます。
highlightedなタブは、browser.tabs.query()
の条件にhighlighted: true
を指定すれば収集できます。
const highlightedTabs = await browser.tabs.query({
currentWindow: true,
highlighted: true
});
if (highlightedTabs.length > 1) {
// 複数タブ選択時の処理
for (const tab of highlightedTabs) {
// 個々の選択されたタブの処理
if (tab.active) {
// 選択されていて、且つアクティブなタブの処理
}
}
}
1つのウィンドウの中にhighlightedなタブが2つ以上ある場合は、「タブが複数選択されている」と判断できます。
以上の事を踏まえると、「指定されたタブが選択範囲の一部であれば選択状態のすべてのタブに対して処理をして、そうでなければ指定のタブ単独に対して処理をする」という事をやるには、以下のようなユーティリティ関数を作るとよいという事が言えるでしょう。
async function getMultiselectedTabs(tab) {
// 与えられたタブが選択されている時は
// 同じウィンドウの選択されたタブを一緒に返す
if (tab.highlighted)
return browser.tabs.query({
windowId: tab.windowId,
highlighted: true
});
else // そうでなければ与えられたタブのみを返す
return [tab];
}
コンテキストメニューのコマンドだと「複数選択されたタブ以外の上でコンテキストメニューが開かれる」という場合があり得るので、そのケースをちゃんと考慮する必要があります。
先のユーティリティ関数を使えば、タブが複数選択されている場合とそうでない場合の実装を容易に共通化できます。
コンテキストメニューのコマンドの場合、リスナにはコンテキストメニューが開かれた対象のタブのオブジェクトが渡ってくるので、それを手がかりにhighlightedなタブを収集できます。
browser.menus.onClick.addListener(async (info, tab) => {
switch (info.menuItemId) {
case 'context_reloadTab':
reloadTab(tab);
break;
...
}
});
こういう実装になっていたのであれば、先のユーティリティ関数を使って
browser.menus.onClick.addListener(async (info, tab) => {
const tabs = await getMultiselectedTabs(tab);
switch (info.menuItemId) {
case 'context_reloadTab':
// ここが複数のタブを受け付けるようになっていればよい
reloadTabs(tabs);
break;
...
}
});
このようにすれば、もう「複数タブ選択機能に対応完了」という事になります。
キーボードショートカットが呼び出された場面では、アクティブなタブは不明なので自分で調べる必要があります。 後はコンテキストメニューの場合と同様です。
browser.commands.onCommand.addListener(async command => {
const activeTab = (await browser.tabs.query({
active: true,
currentWindow: true
}))[0];
switch (command) {
case 'reloadTab':
reloadTab(activeTab);
break;
...
}
});
こうだったのであれば、
browser.commands.onCommand.addListener(async command => {
const activeTab = (await browser.tabs.query({
active: true,
currentWindow: true
}))[0];
const tabs = await getMultiselectedTabs(activeTab);
switch (command) {
case 'reloadTab':
// ここが複数のタブを受け付けるようになっていればよい
reloadTabs(tabs);
break;
...
}
});
こう書き換えれば、めでたく「タブの複数選択機能に対応完了」です。
ここまでは選択されたタブを相手に何かする話ですが、今度はAPI経由で任意のタブを選択する話です。
タブの選択状態を変更する方法は2通りあります。
browser.tabs.update()
での選択・選択解除タブのhighlighted
プロパティの値を変更すれば、タブの選択状態を変更できます。
browser.tabs.update(tab.id, {
highlighted: true
});
ただし、前述の「アクティブなタブは常にhighlighted」という仕様のため、この方法でhighlightedにしたタブは同時にアクティブになってしまうという副作用があります。
じゃあ非アクティブな(バックグラウンドの)タブをアクティブにせず選択することはできないのか?という話なのですが、Firefoxでは以下のようにすればできます。
browser.tabs.update(tab.id, {
highlighted: true,
active: false
});
active: false
を併用するというのがポイントです。
こういう事ができたらいいよねとBugzillaで提案したら、すんなり要望が通りました。言ってみるものですね。
(要望は僕はFirefoxのBugzillaにしか出していないので、Chromeではできないと思います。誰かフィードバックしたらできるようになるかも。)
タブの選択解除も、highlighted
の値の変更でできます。
browser.tabs.update(tab.id, {
highlighted: false
});
アクティブなタブ以外にhighlightedなタブが存在しない状態でこれをやると、前述の「アクティブなタブは常にhighlighted」という仕様のために、操作が無視されます。
他にhighlightedなタブが存在している場合、アクティブなタブの選択状態を解除すると、入れ替わる形で他の選択済みタブのどれか1つがアクティブになります。
browser.tabs.highlight()
での選択・選択解除browser.tabs.update()
を使う方法はタブを1つ1つ指定しないといけませんが、複数のタブの選択状態をまとめてがばっと設定することもできます。
browser.tabs.highlight({
windowId: tab.windowId,
tabs: tabs.map(tab => tab.index)
});
こうすると、tabs
で指定されたタブが選択され、それ以外のタブは非選択になります。
選択対象のタブはIDの配列ではなくindex
の配列で指定する必要がある事に注意して下さい。
このメソッドを使うと、指定されたタブの中にアクティブなタブが含まれる場合でも、tabs
で指定されたタブの1番目のタブが常にアクティブになります。
アクティブなタブを切り替えないようにするためには、以下のようにしてアクティブなタブを配列の先頭に持ってきておく必要があります。
const activeTabs = selectTabs.splice(selectTabs.findIndex(tab => tab.active), 1);
selectTabs = activeTabs.concat(selectTabs);
browser.tabs.highlight({
windowId: selectTabs[0].windowId,
tabs: selectTabs.map(tab => tab.index)
});
ウィンドウのIDの指定が必須で、且つタブはIDではなくindex
での指定なので、論理的に1回の操作で1つのウィンドウに対しての操作しか行えません。
複数のウィンドウについてタブの選択状態を変えたければ、ウィンドウの数だけ操作を繰り返す必要があります。
ちなみに、複数のウィンドウがある時にそれぞれで個別にタブの選択状態を設定する事もできます(1つ目のウィンドウではタブが2つ選択され、2つ目のウィンドウではタブが3つ選択されている、というような状態も普通に作れます)。
tabs
で指定されなかったタブが非選択になるということは、以下のようにすれば選択を全解除できると思えるかもしれません。
browser.tabs.highlight({
windowId: tab.windowId,
tabs: []
});
が、これはエラーになります。 何故かというと、前述の「アクティブなタブは必ずhighlightedになる」「highlightedなタブが1つも存在しない状態はあり得ない」という仕様があるからです。 なので、最低1つは必ずタブを指定しないといけません。
browser.tabs.highlight({
windowId: tab.windowId,
tabs: [tab.index]
});
アクティブなタブを指定すれば、アクティブなタブ以外の選択が解除されるという結果になります。 アクティブでないタブを指定すれば、そのタブがアクティブになると同時に他のタブの選択が解除されます。
タブのactive
の状態変化はbrowser.tabs.onUpdated
では通知されませんが、それと同様に、highlighted
の変化もbrowser.tabs.onUpdated
では通知されません。
ではどうすればよいかというと、browser.tabs.onHighlighted
にリスナを登録する事でタブの選択状態の変化を検知できます。
browser.tabs.onHighlighted.addListener(async highlightInfo => {
const allTabs = await browser.tabs.query({
windowId: highlightInfo.windowId
});
const highlightedTabs = allTabs.filter(tab => highlightInfo.tabIds.includes(tab.id));
const unhighlightedTabs = allTabs.filter(tab => !highlightInfo.tabIds.includes(tab.id));
// 新しいタブの選択状態に基づいて何かする処理
});
browser.tabs.onHighlighted
の通知は、browser.tabs.highlight()
の呼び出し1回に対して1回通知されますが、browser.tabs.update()
を使った場合は個々のタブの状態が変わるごとに通知されます。
大量のタブの選択状態が一気にbrowser.tabus.update()
で変更されると、めちゃめちゃ遅くなります。
なので、以下のようにスロットリングする(一定時間内で何度も呼ばれた場合、最後の1回だけ実行するようにする)必要があります。
const timers = new Map();
browser.tabs.onHighlighted.addListener(async highlightInfo => {
let timer = timers.get(highlightInfo.windowId);
if (timer)
clearTimeout(timer);
timer = setTimeout(() => {
timers.delete(highlightInfo.windowId);
// 新しいタブの選択状態に基づいて何かする処理
}, 150);
timers.set(highlightInfo.windowId, timer);
});
ということで、タブの複数選択機能の導入と同時にWebExtensions APIからもその情報を扱えるようになったわけですが、それの一体何が嬉しいのでしょうか。
というのがポイントです。
WebExtensionsではアドオン同士の名前空間が分かれているので、タブに関連する情報でAPIの仕様に含まれていない物は、browser.runtime.sendMessage()
で頑張って持ち回る必要がありました。
しかし、この複数選択の状態はAPIの仕様に含まれているため、あるアドオンが選択状態を変更すれば、その結果は他のアドオンにも共有されます。
この事により、タブに関わるアドオン同士が暗黙的に連携できる余地が広がるという効果があります。
前述した通り、「タブが選択されている場合はすべての選択済みタブを対象にする」という変更を加えるだけで、複数タブ選択の恩恵を受けられるようになります。例えば以下のようなことができます。
未対応のアドオンがあればプルリクエストしていきましょう。 対応のための変更の仕方は、この資料の前半で書いた通りです。
一方、タブを選択する事に特化したアドオンという物も作れます。 タブを選択するUIを提供するだけで、便利な機能は他のアドオンが提供してくれるというわけです。
Firefox 64以降ではコンテキストメニューを介した暗黙的な連携も可能になるので、「タブ選択UIを提供するだけのアドオン」の利用価値がさらに高まります。
今までは、タブ周りで何か便利な機能を提供しようと思うと、各アドオン同士が明示的に連携し合わなければアドオンの機能同士を組み合わせられませんでした。 しかし明示的に連携するという事は、アドオン同士の結合度合いが高まってしまうという事です。
あるいは、分割が不能な場合、1つのアドオンにあの機能もこの機能も内包させなくてはならないという事にもなります。これはプロダクトの際限なき肥大化に繋がります。
「タブを選択するUIを提供するアドオン」と「選択されたタブをまとめて処理するアドオン」を分けて開発できることで、単機能のアドオン同士がお互いに疎結合の状態を保ったまま暗黙的に連携し合えるようになった。これが、このタブ複数選択APIの重要なポイントというわけです。
ということで、
という発表をしましたという話でした。
の末尾に2020年11月30日時点の日本の首相のファミリーネーム(ローマ字で回答)を繋げて下さい。例えば「noda」なら、「2018-11-01_webextensions-multiselect-tabs.trackbacknoda」です。これは機械的なトラックバックスパムを防止するための措置です。
writeback message: Ready to post a comment.