Firefox拡張機能(extension)の作り方Firefox拡張機能(extension)の作り方を説明します。 Firefox 拡張機能(extension)の作り方Firefox 拡張機能とはFirefox add-onの一種です。 add-onは次のように分類できます。
前提となる概念の説明
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.]
chromeとは(4)それぞれの主な役割は
chromeとは(5)結局chromeは、Gecko(XULとCSSのインタプリタ)とSpiderMonkey(JavaScriptインタプリタ)を実行環境と見なした場合
です。 firefox拡張機能は、アプリケーションプログラムのひとつであるfirefoxに手をいれることです。 chrome URIchrome://browser/content/browser.xul
XULとは(1)
HTMLで、GUIコントロールと呼べるものは、ボタン、テキストボックス、プルダウンメニューなど色々あります。しかし、一般的なウィンドウシステム(MS-WindowsやGNOMEなど)が提供するGUIコントロールと比較すると、質、量ともに劣ります(元々の目的が異なるので当然ですが)。 XULは一般的なウィンドウシステムが提供するGUIコントロールと同等のGUIをXMLで記述することを目的としています。 XULとは(2)
ウィンドウプログラミングに馴染みのある人が見れば、雰囲気が分かると思います。 XULとは(3)
<?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)
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)
XULとは(8)chromeを新規に書き起こせば、GUIアプリケーションを作成可能です。(MS-Windowsとの対比で言えば、VBやDelphiでGUIプログラミングをすることと等価の作業です。GUIコントロールの配置をXULとCSSで記述し、ロジック(イベントハンドラ)の記述をJavaScriptで行います) Firefox拡張機能と言った場合、既存のchrome(browser.xulがメインファイル)の書き換えを意味します。これを実現するのがXULのoverlay機能です(詳細は後述)。 拡張機能開発の準備(2)開発用のfirefoxプロファイルを作成します。
@echo off: batch file for firefox extension development"C:\Program Files\Mozilla Firefox\firefox" -no-remote -P dev 拡張機能開発の準備(3)about:configを開いて、以下の4つの値をtrueにします。
拡張機能開発の準備(4)about:configを開いて、次を追加します(右クリックメニュー/新規作成/真偽値)
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の調べ方
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>
文書オープン時に動く拡張機能の雛型<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 templatevar 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); //新規ウィンドウごとに挙がるイベント
タイマードリブンで動く拡張機能の雛型<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 templatevar 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);タイマードリブンで動く拡張機能の実例
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);その他の話題
国際化(chrome.manifest)locale my ja-JP chrome/locale/ja-JP/ myというパッケージ名と相対ファイルパス(chrome/locale/ja-JP/)を結び付けます。
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)
${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.cssmenuitem.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); |