Personal tools
You are here: Home 原稿・資料 ワークス、アリエル共同勉強会 Firefox拡張機能(extension)の作り方
Document Actions

Firefox拡張機能(extension)の作り方

Firefox拡張機能(extension)の作り方を説明します。

Firefox 拡張機能(extension)の作り方

Firefox 拡張機能とは

Firefox add-onの一種です。

add-onは次のように分類できます。

  • plugin ...実体はexeやdll。C++で作成。素人にはお勧めしません
  • 検索バー ...実体はXMLの設定ファイルのみ。見れば分かるので説明はしません
  • スペルチェッカ ...日本語には無縁なので未調査(たぶんファイルを作るだけ)
  • 拡張機能 ...実体はXML、JavaScriptとCSS。必要なら、C++で書くXPCOM。これから説明します
  • テーマ ...拡張機能のサブセット。CSSのみの場合をテーマと呼びます

開発環境

  • firefox2
  • firebug
  • 普通に使えるエディタ

以下、MS-Windowsを仮定していますが、他のOSでもファイルパス以外は同じです。

前提となる概念の説明

  • 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とは(3)

chromeの実体は主に次の要素からなります。

  • XUL
  • JavaScript
  • CSS

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機能です(詳細は後述)。

拡張機能開発の準備(2)

開発用のfirefoxプロファイルを作成します。

  1. firefoxを終了
  2. firefox -ProfileManager を起動
  3. 適当な名前でプロファイルを作成 (e.g. dev)
  4. 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')。 先頭カラムによって、後続カラムの文法が決定します。

参照
http://developer.mozilla.org/en/docs/Chrome_Registration

何もしない拡張機能を作って動かしてみる(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>
  • (*)名前が被らなければ任意名でも可
参照
http://developer.mozilla.org/ja/docs/install.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
  • ポップアップウィンドウ

国際化(メッセージ翻訳)

  • JavaScript
  • XUL

# 文字コードはutf8

国際化(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のメッセージ翻訳(1)

${WORK}/my/chrome/locale/ja-JP/my.properties
msg.hello=こんにちは

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のメッセージ翻訳(1)

${WORK}/my/chrome/locale/ja-JP/my.dtd
<!ENTITY my.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);

Copyright(C) 2001 - 2006 Ariel Networks, Inc. All rights reserved.