Firefox拡張機能(extension)の作り方
Firefox拡張機能(extension)の作り方を説明します。
Firefox 拡張機能(extension)の作り方
Firefox 拡張機能とは
Firefox add-onの一種です。
add-onは次のように分類できます。
- plugin ...実体はexeやdll。C++で作成。素人にはお勧めしません
- 検索バー ...実体はXMLの設定ファイルのみ。見れば分かるので説明はしません
- スペルチェッカ ...日本語には無縁なので未調査(たぶんファイルを作るだけ)
- 拡張機能 ...実体はXML、JavaScriptとCSS。必要なら、C++で書くXPCOM。これから説明します
- テーマ ...拡張機能のサブセット。CSSのみの場合をテーマと呼びます
前提となる概念の説明
- chrome(クローム)
- XUL(ズール。"pronounced zool and it rhymes with cool")
chromeとは(1)
参考サイト
上記サイトの説明によれば「chromeはアプリケーションウィンドウのUI要素のセット」です。 しかし、この説明で意味が分かる人は奇跡的に勘が良い人でしょう。
chromeが何かを知るには、Mozillaアーキテクチャを理解する必要があります。
chromeとは(2)
Mozillaアーキテクチャはコア機能の上に各種chrome(とXPCOM実装コードのセット)が載ることで、様々なアプリケーションを実装しています。
Webブラウザのchrome(firefox) メーラのchrome(thunderbird) カレンダーのchrome(sunbird) その他(#4) \ | / コア機能(ネットワーク機能(#1)、レンダリング機能(#2)、JavaScriptインタプリタ(#3)、etc.) [based on XPCOM(#5)、NSPR、etc.]
- (#1) Necko
- (#2) Gecko
- (#3) SpiderMonkey
- (#4) ChatZilla, Composer, etc.
- (#5) (偉大なる)MS COMのパクリ技術
chromeとは(4)
それぞれの主な役割は
- XUL ...UIコンポーネント(ボタン、メニュー、ラベルなど)の配置を定義
- JavaScript ...UIコンポーネントのイベントハンドラを実装
- CSS ...UIコンポーネントのデザイン(レイアウト)を定義
chromeとは(5)
結局chromeは、Gecko(XULとCSSのインタプリタ)とSpiderMonkey(JavaScriptインタプリタ)を実行環境と見なした場合
- アプリケーションプログラムのGUI部分そのもの
です。
firefox拡張機能は、アプリケーションプログラムのひとつであるfirefoxに手をいれることです。
chrome URI
chrome://browser/content/browser.xul
- 'browser'の部分; パッケージ名(ローカルPC上で一意)
- 'content'の部分; 定義済みキーワード('content'、'locale'、'skin'のいずれか)
- browser.xulの部分; chromeパッケージ内でのファイル名(*)
- (*)パッケージ名とファイル名の対応はchrome.manifestファイルで宣言します(後述)
XULとは(1)
- XMLベースのGUI記述言語
HTMLで、GUIコントロールと呼べるものは、ボタン、テキストボックス、プルダウンメニューなど色々あります。 しかし、一般的なウィンドウシステム(MS-WindowsやGNOMEなど)が提供するGUIコントロールと比較すると、質、量ともに劣ります(元々の目的が異なるので当然ですが)。
XULは一般的なウィンドウシステムが提供するGUIコントロールと同等のGUIをXMLで記述することを目的としています。
XULとは(2)
- XULで定義されたGUIコントロール一覧
- http://developer.mozilla.org/en/docs/XUL_Reference
ウィンドウプログラミングに馴染みのある人が見れば、雰囲気が分かると思います。
XULとは(3)
- 以下の内容のmy.xulファイルを作成してfirefoxで開いてみます。
<?xml version="1.0"?> <?xml-stylesheet href="chrome://browser/skin/" type="text/css"?> <window id="my-xul" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> <hbox> <label value="this is a XUL page" id="my-label"/> <colorpicker type="button"/> </hbox> </window>
XULとは(4)
- firebugでmy.xulのDOM要素を操作してみます。
var l = $('my-label'); l.value += '...'; l.onclick = function () {alert('label clicked');}
XULとは(5)
GeckoはHTMLレンダリングエンジンと呼ばれることがありますが、正確にはXULレンダリングエンジンと呼ぶべきです。
Firefoxのメニューバー、ツールバー、ステータスバー、各種ダイアログボックス、すべてXULで記述して、Geckoがレンダリングしています。
XULとは(6)
FirefoxのXULファイルはbrowser.xulです。
MS-Windowsの場合のパスの例(jarを展開するとbrowser.xulファイルがあります)
c:/Program Files/Mozilla Firefox/chrome/browser.jar
XULとは(7)
- browser.xulの中身をエディタで見ます
- chrome://browser/content/browser.xul を開くとfirefoxがbrowser.xulをレンダリングします
XULとは(8)
chromeを新規に書き起こせば、GUIアプリケーションを作成可能です。 (MS-Windowsとの対比で言えば、VBやDelphiでGUIプログラミングをすることと等価の作業です。 GUIコントロールの配置をXULとCSSで記述し、ロジック(イベントハンドラ)の記述をJavaScriptで行います)
Firefox拡張機能と言った場合、既存のchrome(browser.xulがメインファイル)の書き換えを意味します。 これを実現するのがXULのoverlay機能です(詳細は後述)。
拡張機能開発の準備(1)
参考
拡張機能開発の準備(2)
開発用のfirefoxプロファイルを作成します。
- firefoxを終了
- firefox -ProfileManager を起動
- 適当な名前でプロファイルを作成 (e.g. dev)
- firefox -no-remote -P dev で起動します(次のbatファイルを作成すると楽)
@echo off : batch file for firefox extension development "C:\Program Files\Mozilla Firefox\firefox" -no-remote -P dev
拡張機能開発の準備(3)
about:configを開いて、以下の4つの値をtrueにします。
- javascript.options.showInConsole
- javascript.options.strict
- extensions.firebug.showChromeErrors
- extensions.firebug.showChromeMessages
拡張機能開発の準備(4)
about:configを開いて、次を追加します(右クリックメニュー/新規作成/真偽値)
- nglayout.debug.disable_xul_cache => true
Firefoxを再起動せずに拡張機能を試せます。 ただしFirefoxが異常に重くなるので、速いPCで無いと辛いです。
何もしない拡張機能を作って動かしてみる(1)
作業ディレクトリ(e.g. c:/cygwin/home/inoue/src/firefox/)の下に次のようなディレクトリ構成を作成します。 (このようなディレクトリをtemplateとして用意して、再利用することを勧めます)
以下、作業ディレクトリのベースを${WORK}と記述します。
${WORK}/my/chrome.manifest /install.rdf /chrome/content/my.xul
何もしない拡張機能を作って動かしてみる(2)
chrome.manifestの中身
content sample chrome/content/ overlay chrome://browser/content/browser.xul chrome://sample/content/my.xul
先頭カラムは定義済みキーワードです('content','locale','skin','overlay','style','override')。 先頭カラムによって、後続カラムの文法が決定します。
何もしない拡張機能を作って動かしてみる(3)
chrome.manifestの中身の説明
content sample chrome/content/
chromeパッケージ名(上記例では'sample')と、ファイルシステム(相対パス指定)の対応を定義します。 Firefoxにこのchromeパッケージをインストールすると(インストール方法は後述)、chrome://sample/content/my.xulがファイルシステム上の${WORK}/my/chrome/content/my.xulを指します。
overlay chrome://browser/content/browser.xul chrome://sample/content/my.xul
chrome://sample/content/my.xulでchrome://browser/content/browser.xul(Firefoxのchrome)をoverlayするように指示します(overlayの詳細は後述)。
何もしない拡張機能を作って動かしてみる(4)
install.rdfの中身 (コメントの無い部分はあまり気にしないで下さい)
<?xml version="1.0"?> <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#"> <Description about="urn:mozilla:install-manifest"> <em:id>{a58fa49f-acb8-43f8-b3b5-e69f552f6a7d}</em:id> <!-- 拡張機能ごとにUUIDを生成(*) --> <em:version>1.0</em:version> <!-- 拡張機能のバージョン --> <em:type>2</em:type> <!-- 2:拡張機能、4:テーマ、8:ロケール、16:プラグイン、32:... --> <!-- Target Application this extension can install into, with minimum and maximum supported versions. --> <em:targetApplication> <Description> <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> <!-- Firefox固有のUUID --> <em:minVersion>1.5</em:minVersion> <em:maxVersion>2.0.0.*</em:maxVersion> </Description> </em:targetApplication> <!-- Front End MetaData --> <!-- 拡張機能の説明(人間用) --> <em:name>my sample</em:name> <em:description>A test extension</em:description> <em:creator>inoue@ariel-networks.com</em:creator> <!--em:homepageURL>http://dev.ariel-networks.com/</em:homepageURL--> </Description> </RDF>
- (*)名前が被らなければ任意名でも可
何もしない拡張機能を作って動かしてみる(5)
my.xulの中身
<?xml version="1.0"?> <overlay id="sample" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> </overlay>
何もしない拡張機能を作って動かしてみる(6)
動かす方法
次のディレクトリ(ユーザ名と${id}の部分は環境依存します)
c:/Documents and Settings/inoue/Application Data/Mozilla/Firefox/Profiles/${id}/extensions/
に、次の名前のファイルを作成します。
c:/Documents and Settings/inoue/Application Data/Mozilla/Firefox/Profiles/${id}/extensions/{a58fa49f-acb8-43f8-b3b5-e69f552f6a7d}
カッコ内のuuidはinstall.rdfの/RDF/Description/em:idの値に対応します。
このファイルの中身に拡張機能の実体へのパスを書きます。
c:\cygwin\home\inoue\src\firefox\my\
Firefoxを起動すると、sample拡張機能(chrome://sample/content/my.xul)が有効になります。 (nglayout.debug.disable_xul_cacheがtrueであれば、新規ウィンドウを開くだけでOK)
XULのoverlay機能(1)
Firefox拡張機能の最初の一歩は、browser.xulをoverlayで書き換えることです。
XULのXML要素のid属性の値をキーにして、上書きを指定する場所を指定します。
browser.xulの調べ方
- browser.xulファイルの中身を読む
- chrome://browser/content/browser.xulを開いて、firebugで調査
XULのoverlayの実例(1)
例えば、browser.xul内のステータスバー定義は次のようになっています。
<statusbar class="chromeclass-status" id="status-bar" ondragdrop="nsDragAndDrop.drop(event, contentAreaDNDObserver);"> <statusbarpanel id="statusbar-display" flex="1"/> ...省略 </statusbarpanel> </statusbar>
my.xulでoverlayするには次のように書きます。
<overlay id="sample" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> <statusbar id="status-bar"> <statusbarpanel id="my-panel" label="Hello, Firefox extension"/> </statusbar> </overlay>
XULのoverlayの実例(2)
メニューバーへのメニュー追加をoverlayで書く例
<overlay id="sample" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> <menupopup id="menu_ToolsPopup"> <menuitem label='my menu' oncommand='alert("foobar")'/> </menupopup> </overlay>
メニューからコマンド実行の実例(1)
<menuitem label='my menu' oncommand='my_func()'/> <!-- JavaScript関数呼び出し -->
JavaScript関数定義の参照方法(1)
<overlay id="sample" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> <script type="application/x-javascript"><![CDATA[ function my_func() { alert('my-func'); } ]]></script> </overlay>
JavaScript関数定義の参照方法(2)
<overlay id="sample" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> <script type="application/x-javascript" src="my.js"/> <!-- 別ファイル --> </overlay>
メニューからコマンド実行の実例(2)
メニュー、ツールバー、キーボードショートカットから同じコマンドを呼ぶ場合、次のような方法が良いでしょう。
<overlay id="sample" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> <commandset id="mainCommandSet"> <command id='MyCommand' oncommand='alert("my command")'/> </commandset> <menupopup id="menu_ToolsPopup"> <menuitem label='my menu' command='MyCommand'/> </menupopup> </overlay>
拡張機能の(私的)分類
- ユーザ操作で動く拡張機能(メニュー、ツールバー、サイドバーから実行)
- 文書オープン時に動く拡張機能
- タイマードリブンで動く拡張機能
ユーザ操作で動く拡張機能の雛型
既出なので省略
ユーザ操作で動く拡張機能の実例
- メニューから実行して画像を削除する拡張機能
<overlay id="sample" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> <menupopup id="menu_ToolsPopup"> <menuitem label='hide images' oncommand='hideImages()'/> </menupopup> <script type="application/x-javascript"><![CDATA[ function hideImages() { var imgs = window.content.document.images; for (var i = 0, len = imgs.length; i < len; i++) { imgs[i].style.display = 'none'; } } ]]></script> </overlay>
- # この程度ならbookmarkletで充分ですが...
文書オープン時に動く拡張機能の雛型
<overlay id="sample" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> <script type="application/x-javascript" src="my.js" /> </overlay>
// my.js template var MyContLoad = { init: function() { window.removeEventListener("load", MyContLoad.init, false); // global initialization code window.addEventListener("DOMContentLoaded", MyContLoad.onContentLoad, false);//新規文書ごとに挙がるイベント }, onContentLoad: function() { // alert('load'); } }; window.addEventListener('load', MyContLoad.init, false); //新規ウィンドウごとに挙がるイベント
- このパターンはgreasemonkeyを使うのがお勧め (greasemonkey: このパターンのディスパッチ機能を提供する拡張機能)
タイマードリブンで動く拡張機能の雛型
<overlay id="sample" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> <script type="application/x-javascript" src="my.js" /> </overlay>
// my.js template var MyTimer = { init: function() { window.removeEventListener("load", MyTimer.init, false); setInterval(MyTimer.onTimer, 60 * 1000); // 1 min. }, onTimer: function() { // alert('load'); } }; window.addEventListener('load', MyTimer.init, false);
タイマードリブンで動く拡張機能の実例
- XMLHttpRequestでJSONデータを取得してステータスバーに結果を表示
var MyAirOne = { init: function() { MyAirOne.getFreqMinute(); window.removeEventListener("load", MyAirOne.init, false); MyAirOne.getHeadline(); setInterval(MyAirOne.getHeadline, MyAirOne.getFreqMinute() * 60 * 1000); }, getHeadline: function() { var req = new XMLHttpRequest(); req.open('get', 'http://localhost:6809/1/aircafe/get-headline-num', true); req.onreadystatechange = function(ev) { if (req.readyState == 4 && req.status == 200) { var data = eval(req.responseText); // ({count:$room-number, $room-id1:$count1, $room-id2:$count2, ... }) var count = data.count; if (count == 0) { document.getElementById('airone-statusbar-image').src = 'chrome://my-airone/content/airone-notice1.ico'; } else { document.getElementById('airone-statusbar-image').src = 'chrome://my-airone/content/airone-notice2.ico'; } } } req.send(null); }, getFreqMinute: function() { // default is 3min try { var prefManager = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch); var freq = prefManager.getIntPref('extensions.airone.checkfreq'); return freq > 0 ? freq : 3; } catch (e) { return 3; } }, openPrefs: function() { window.openDialog("chrome://my-airone/content/prefs.xul", 'Preferences', 'chrome,titlebar,toolbar,centerscreen,modal'); } }; window.addEventListener('load', MyAirOne.init, false);
その他の話題
- 国際化(メッセージ翻訳)
- XBL
- Preference
- ポップアップウィンドウ
国際化(chrome.manifest)
locale my ja-JP chrome/locale/ja-JP/
myというパッケージ名と相対ファイルパス(chrome/locale/ja-JP/)を結び付けます。
- ${WORK}/my/chrome/locale/ja-JP/my.propertiesをchrome://my/locale/my.propertiesで参照可能にします。
- ${WORK}/my/chrome/locale/ja-JP/my.dtdをchrome://my/locale/my.dtdで参照可能にします。
JavaScriptのメッセージ翻訳(2)
${WORK}/my/chrome/content/my.xul 抜粋
<script type="application/x-javascript" src="my.js" /> <stringbundleset id="stringbundleset"> <stringbundle id="my-msg-bundle" src="chrome://my/locale/my.properties" /> </stringbundleset>
JavaScriptのメッセージ翻訳(3)
${WORK}/my/chrome/content/my.js 抜粋
alert(document.getElementById('my-msg-bundle').getString('msg.hello'));
XULのメッセージ翻訳(2)
${WORK}/my/chrome/content/my.xul
<?xml version="1.0"?> <!DOCTYPE window SYSTEM "chrome://my/locale/my.dtd"> <overlay id="sample" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> <menupopup id="menu_ToolsPopup"> <menuitem label='&my.msg.hello;' oncommand='alert("&my.msg.hello;")'/> </menupopup> </overlay>
XBL(1)
- cssを仲介することで、XULのメタ言語のように振舞う(XULにadvice)
${WORK}/my/chrome/content/my.xul
<?xml version="1.0"?> <?xml-stylesheet href="my.css" type="text/css"?> <overlay id="sample" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> <menupopup id="menu_ToolsPopup"> <menuitem class="my"/> </menupopup> </overlay>
XBL(2)
${WORK}/my/chrome/content/my.xml
<?xml version="1.0"?> <bindings id="my-xbl" xmlns="http://www.mozilla.org/xbl" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:xbl="http://www.mozilla.org/xbl"> <binding id="my-binding"> <content> <children/> <label value='my xbl'/> </content> </binding> </bindings>
XBL(3)
${WORK}/my/chrome/content/my.css
menuitem.my { -moz-binding: url('chrome://my-xbl/content/my.xml#my-binding'); }
Preference(1)
// プリファレンス値の取得例 var prefManager = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch); var freq = prefManager.getIntPref('extensions.my.checkfreq'); //名前が被らないように extensions prefixで始めるのが良い // プリファレンス設定画面を出す例 window.openDialog("chrome://my/content/prefs.xul", 'Preferences', 'chrome,titlebar,toolbar,centerscreen,modal');
Preference(2)
${WORK}/my/chrome/content/prefs.xul
<?xml version="1.0"?> <?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> <prefwindow id="prefs" title="preferences" buttons="accept,cancel" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> <prefpane id="my-prefpane"> <preferences> <preference id="my-checkfreq" name="extensions.my.checkfreq" type="int" /> <!-- id属性が後から参照される --> </preferences> <hbox align="center"> <label control="checkfreq-field" value="frequency" /> <textbox id="checkfreq-field" preference="my-checkfreq" size="2" cols="2" /> <!-- ユーザに入力させるテキストボックス。preference属性の値が上記のid値を参照して --> </hbox> </prefpane> </prefwindow>
ポップアップウィンドウ
var alertsService = Components.classes["@mozilla.org/alerts-service;1"] .getService(Components.interfaces.nsIAlertsService); alertsService.showAlertNotification('chrome://my/content/my.png', 'popup title', 'popup content', true, '', null);