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);