Add-on SDKでできない事をやってみよう -UI編- 本日のメニュー 1. Add-on SDKのおさらい 2. サードパーティ製のライブラリを   見てみよう 3. 実際にやってみよう Add-on SDKのおさらい SDKで用意されている標準ライブラリ 高レベルAPI "addon-kit" addon-kit - Add-on SDK Documentation https://addons.mozilla.org/en-US/developers/docs/sdk/latest/packages/addon-kit/index.html ・ページの読み込みの監視、ページの内容の変更 ・選択範囲の監視 ・クリップボードの操作 ・コンテキストメニューへの機能の追加 ・キーボードショートカットの定義 ・アドオンバー(ステータスバー)へのアイコンの追加 ・ポップアップでの通知、パネルの表示 ・タブ操作の監視 ・ウィンドウ操作の監視 など。普通にやりたくなるようなことはだいたいできるようになってる。 addon-kitのAPIではできない事? ・サイドバーの操作 ・サイドバーパネルの追加 ・カスタマイズ可能な  ツールバーボタンの追加 など サードパーティ製ライブラリがいくつかある。 https://builder.addons.mozilla.org/search/?type=l これを組み込めば、addon-kit APIではできないことでもできるようになる。 それでもできない事をしたい時は? 低レベルのAPI api-utils - Add-on SDK Documentation https://addons.mozilla.org/en-US/developers/docs/sdk/latest/packages/api-utils/index.html ・独自のイベントモデル ・Base64 ・JavaScriptでオブジェクト指向をもっとガッツリ など、addon-kitやサードパーティ製ライブラリを構築するために使われている基礎技術色々。 これらを使ったり、あるいはもっと基礎的なAPI(DOM、CSS、js-ctypes)を使ったりする。 ここで、Firefoxの設計についておさらい Web標準と言われる技術と、Firefox独自の技術との両方から成り立っている。 FirefoxのUIやXPCOMの部分は不安定 (ここでいう不安定とは、バージョンアップでAPIが変わってしまうとかそういう意味。 クラッシュするとかそういう話では無い) Add-on SDKは、Firefoxの上に安定したAPIのレイヤを作る。 そのため、Firefoxより下の層(特に、XPCOMとFirefoxそのもの)の仕様変更を基本的には考えなくて良い。 もし将来FirefoxがXULを捨てても、この層で差異が吸収される……はず!? その代償として、抽象化された、APIで機能が提供されている範囲でしか開発できないという制限がある。 大体の場合については標準のaddon-kitでできるはずだが、addon-kitで機能が提供されていない範囲のことをやるには、SDKより下の層に触る必要がある。 そうすると、不安定な物を自分で扱わないといけなくなる。 実際にどういう事をしないといけないのか、サードパーティ製ライブラリを例に見てみよう。 library - Sidebar r. 26 by dandonkulous - Add-on Builder https://builder.addons.mozilla.org/package/25177/revision/26/ Firefoxに組み込みのサイドバーの横に新しくサイドバーを追加する。 サンプルを動かしてみる アンインストール、無効化も試してみる 要点 ・独自のUIをFirefoxのウィンドウに追加している ・api-utilsのunloadを使って、アンインストールされたり無効化されたりした時に追加したUIを削除したりしている アンインストールや終了の監視は、SDKのライブラリがやってくれている。 ウィンドウを開いたことの監視も、SDKのライブラリでやってくれている。 問題は、開かれたウィンドウの中でやる処理。 これはDOMを使ってFirefoxをいじっている。なので、Firefoxの変更の影響を受けやすい。 SDKではできない事を大まかに分類すると、 ・UIの変更(SDKで機能が提供されていない部分の変更) ・もっと低レベルの処理(Web標準のAPIも提供されていない、ブラウザの基本機能に強く結び付いた機能。独自プロトコルへの対応など。) ・一番低レベルの処理(プラットフォームに強く結び付いた機能。ハードウェアの機能を直接叩く、など。) この3種類に分けられる。 ・UIの変更  →Firefoxのバージョンに強く依存し、新バージョンに追従するため継続的なアップデートが欠かせない。 ・もっと低レベルの処理  →Firefoxのバージョンに強く依存し、新バージョンに追従するため継続的なアップデートが欠かせない。   プラットフォームの違いにも依存する。 ・一番低レベルの処理  →OSのバージョン、ハードウェアの種類に強く依存。   OSにライブラリがあるかどうかなどにも依存する。 いずれの場合も、茨の道になる事は覚悟しないといけない。 余談 ハードウェアを直接叩くというのは、今後は自力でやらないといけない部分は減ってくるはず Web API経由でできるようにMozillaも他のベンダも色々仕様を提案してる。 B2G、Firefox OSでも、そういったWeb APIの利用を前提にしている。 いずれの場合も必要なこと。 アンインストールや終了の監視→ここが肝心!!! require('unload').when(function(event){ if (event == 'uninstall' || event == 'upgrade' || event == 'disable') { sidebars.instances.forEach(function(object){ object.destroy(); }) } }); ライブラリは、アンインストール時や終了時に後始末をする。これがルール。 後始末をしないとメモリリークを引き起こす。 後始末、とは? ・UI要素を追加した物は削除する ・イベントリスナやオブザーバなど、登録した物は登録を解除する ・プロパティなど、参照を追加した物は参照を削除する ・ハードウェア寄りの処理についても、終了処理を行う。 UIの変更 UI要素を追加したり、既存のUI要素の見た目を変えたり、ユーザの操作に対して反応したり。 例:タブバーの上でホイールスクロールしたらタブのフォーカスを切り替える これがゴール。なんとかしてこれを実現したい。 前提知識として、 ・FirefoxはGUIの部分がJavaScriptで作られてる ・なので、Webアプリの開発で使われてる技術をかなりの部分でそのまま流用できる 検索してWeb系の技術情報が見つかったら、それをFirefoxのGUIをいじるのにも使えるかもしれない。 必要なこと: ・マウスのホイールスクロールを検知する ・ホイールスクロールをトリガーにして、タブのフォーカスを切り替える タブバーのスクロールを監視する機能は、SDKのライブラリには無い。 やり方をどうにかして見つけないといけない。 とりあえずぐぐってみます。 ホイール スクロール イベント javascript - Google 検索 https://www.google.co.jp/search?q=%E3%83%9B%E3%82%A4%E3%83%BC%E3%83%AB%E3%82%B9%E3%82%AF%E3%83%AD%E3%83%BC%E3%83%AB+%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88&ie=utf-8&oe=utf-8&aq=t&rls=org.mozilla:ja:official&hl=ja&client=firefox#hl=ja&client=firefox&hs=q0O&rls=org.mozilla:ja%3Aofficial&sclient=psy-ab&q=%E3%83%9B%E3%82%A4%E3%83%BC%E3%83%AB+%E3%82%B9%E3%82%AF%E3%83%AD%E3%83%BC%E3%83%AB+%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88+JavaScript&oq=%E3%83%9B%E3%82%A4%E3%83%BC%E3%83%AB+%E3%82%B9%E3%82%AF%E3%83%AD%E3%83%BC%E3%83%AB+%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88+JavaScript&gs_l=serp.3...8921.12104.0.12443.10.10.0.0.0.0.199.1051.7j3.10.0...0.0...1c.1vWcAy7ZKsw&pbx=1&bav=on.2,or.r_gc.r_pw.r_qf.&fp=47634e37946a33fc&biw=922&bih=729 JavaScriptでマウスホイールイベントを扱い、スクロールも停止する方法:phpspot開発日誌 http://phpspot.org/blog/archives/2006/08/javascript_23.html なんかよくわかんないけど、DOMMouseScrollっていうのが鍵っぽい またぐぐってみましょう。 dommousescroll mdn - Google 検索 https://www.google.co.jp/search?q=DOMMouseScroll&ie=utf-8&oe=utf-8&aq=t&rls=org.mozilla:ja:official&hl=ja&client=firefox#hl=ja&client=firefox&hs=TN4&rls=org.mozilla:ja%3Aofficial&sclient=psy-ab&q=DOMMouseScroll+MDN&oq=DOMMouseScroll+MDN&gs_l=serp.3..0i30.4200.6163.0.7495.4.4.0.0.0.0.200.571.0j3j1.4.0...0.0...1c.mR8Ju8GJ2BE&pbx=1&bav=on.2,or.r_gc.r_pw.r_qf.&fp=47634e37946a33fc&biw=922&bih=729 Gecko 固有の DOM Event | Mozilla Developer Network https://developer.mozilla.org/ja/docs/Gecko-Specific_DOM_Events このイベントを監視して、それに応じて何かやれば良さそう? ほんとにそうなのか、Firefoxの中で使えるのかどうか調べてみる http://mxr.mozilla.org/mozilla-central/search?string=DOMMouseScroll 一杯ヒットするので、多分使える。 次、タブを切り替える。 タブのフォーカスを切り替えるAPIなら、SDKにある。 https://addons.mozilla.org/en-US/developers/docs/sdk/latest/packages/addon-kit/tabs.html ……が、これは使わないことにします。 何故か? addon-kitのAPIはaddon-kitのレイヤのAPIだけで使う事を想定されているので、それより下のレイヤと行ったり来たりすると訳が分からなくなる。 下のレイヤと相性がいいのは、api-utilsのtab-browser。 https://addons.mozilla.org/en-US/developers/docs/sdk/latest/packages/api-utils/tab-browser.html ・が持ってるプロパティ  http://mxr.mozilla.org/mozilla-central/source/browser/base/content/tabbrowser.xml#45  ・tabContainer()が持ってるメソッド   http://mxr.mozilla.org/mozilla-central/source/toolkit/content/widgets/tabbox.xml#514   ・advanceSelectedTab()の使い方 とりあえず技術的には可能そうということまで分かった DOMのスクロールイベントを捕捉するには →DOMに触らないといけない。  また、ウィンドウが開かれたり閉じられたりというのを監視しないといけない。 →tab-browserのTrackerのonTrack/onUntrackでできる 実際やってみる。 var TabBrowser = require('tab-browser'); new TabBrowser.Tracker({ onTrack: function (aTabBrowser) { console.log('track '+aTabBrowser); }, onUntrack: function(aTabBrowser) { console.log('untrack '+aTabBrowser); } }); ちなみに、タブ周り以外(ツールバーなど)の方に手を出したいなら、tab-browserではなくwindow-utilsを使うとよい。 var WindowUtils = require('window-utils'); new WindowUtils.WindowTracker({ onTrack: function (aDOMWindow) { console.log('track '+aDOMWindow); }, onUntrack: function(aDOMWindow) { console.log('untrack '+aDOMWindow); } }); DOMの要素ノードが取れたら、イベントの監視。 DOM Level2のaddEventListener/removeEventListenerで監視する。 どのイベント型を監視すればよいか? →今(Firefox ESR 10とFirefox 14)だとDOMMouseScrollイベント。 detailでスクロールの方向とスクロールした量を取得できる。 実際やってみる。 var TabBrowser = require('tab-browser'); new TabBrowser.Tracker({ onTrack: function (aTabBrowser) { console.log('track '+aTabBrowser); var target = aTabBrowser.tabContainer; target.addEventListener('DOMMouseScroll', function(aEvent) { console.log(aEvent.type+': '+aEvent.detail+ '('+aEvent.timeStamp+')'); }, true); }, onUntrack: function(aTabBrowser) { console.log('untrack '+aTabBrowser); } }); メモリリークを防ぐためにちゃんとイベントリスナの登録の解除もしておく (こういうケースだとほんとは省略してもいいっぽいけど、初期化処理と終了処理は必ず対にして書くというルールを身に付けておくために、やっておくのが望ましい) var listeners = []; var TabBrowser = require('tab-browser'); new TabBrowser.WindowTracker({ onTrack: function (aTabBrowser) { console.log('track '+aTabBrowser); var target = aTabBrowser.tabContainer; var listener = { tabBrowser: aTabBrowser, target: target, callback: function(aEvent) { console.log(aEvent.type+': '+aEvent.detail+ '('+aEvent.timeStamp+')'); } }; listener.target.addEventListener('DOMMouseScroll', listener.callback, true); }, onUntrack: function(aTabBrowser) { console.log('untrack '+aTabBrowser); listeners.some(function(aListener, aIndex) { if (aListener.tabBrowser == aTabBrowser) { aListener.target.removeEventListener('DOMMouseScroll', listener.callback, true); listeners.splice(aIndex, 1); return true; } }); } }); アンインストール、無効化などの時についてもちゃんとハンドルする require('unload').when(function(aEventType){ if (aEventType == 'uninstall' || aEventType == 'upgrade' || aEventType == 'disable') { console.log('unloading by '+aEventType); listeners.forEach(function(aListener) { aListener.target.removeEventListener('DOMMouseScroll', listener.callback, true); }) listeners = []; } }); ここまででやった内容を「ライブラリ」にする main.js→tabbarscroll.js exportsに移す exports.tabbarscroll = function(aCallback) { ... callback: function(aEvent) { aCallback(aEvent); } ... }; これで、 require('tabbarscroll').tabbarScroll(function(aEvent) { console.log(aEvent.type+': '+aEvent.detail+ '('+aEvent.timeStamp+')'); }); と書けるようになった。 タブの切り替え require('tabbarscroll') .tabbarscroll(function(aTabBrowser, aEvent) { var direction = aEvent.detail < 0 ? -1 : 1; aTabBrowser.tabContainer.advanceSelectedTab(direction, true); }); イベントのキャンセル (そのままだとFirefox自身のイベントハンドラがタブバーをスクロールしてしまうので、それを禁止する) aEvent.stopPropagation(); if (aEvent.stopImmediatePropagation) aEvent.stopImmediatePropagation(); aEvent.preventDefault(); スロットリング (そのままだとちょっとホイールを回しただけで大量のイベントが発生→1つずつフォーカスを移すのが難しくなるので、反応する頻度を敢えて落とす) var timers = require('timers'); var lastTimer = null; require('tabbarscroll') .tabbarscroll(function(aTabBrowser, aEvent) { console.log(aEvent.detail); var direction = aEvent.detail < 0 ? -1 : 1; if (lastTimer) timers.clearTimeout(lastTimer); lastTimer = timers.setTimeout(function() { lastTimer = null; aTabBrowser.tabContainer.advanceSelectedTab(direction, true); }, 100); aEvent.stopPropagation(); if (aEvent.stopImmediatePropagation) aEvent.stopImmediatePropagation(); aEvent.preventDefault(); }); 必要な知識 ・DOMのイベント  ・addEventListener, removeEventListener  ・キャプチャリングフェーズとバブリングフェーズ  ・preventDefault ・DOMMouseScrollイベント  ・detailプロパティ ・MozMousePixelScrollイベントとDOMMouseScrollとの関係  https://developer.mozilla.org/en-US/docs/DOM/DOM_event_reference/DOMMouseScroll    実際には、このケースだと自分でやる必要は無い。(tabsが自分でMozMousePixelScrollをキャンセルしているので)  http://mxr.mozilla.org/mozilla-central/source/toolkit/content/widgets/scrollbox.xml#472 ・Firefox 17以降、wheelイベントへの移行も視野に入れる  ・http://www.d-toybox.com/studio/weblog/show.php?mode=single;id=2012081600 ・が持ってるプロパティ  http://mxr.mozilla.org/mozilla-central/source/browser/base/content/tabbrowser.xml#45  ・tabContainer()が持ってるメソッド   http://mxr.mozilla.org/mozilla-central/source/toolkit/content/widgets/tabbox.xml#514   ・advanceSelectedTab()の使い方 このまとめを見ても分かる通り、すでにFirefox 17以降で古くなる事が確定している技術というのがある。 SDKではこういうのは隠蔽されてるから影響ないけど、SDKのライブラリを使わずに直接Firefoxの中に触り始めると、こういう変更の影響をいつも気にしないといけなくなる。