Now browsing the SCRAPBLOG weblog archives.

XPCOM サービスへの頻繁なアクセスを効率化するテクニック

拡張機能や XUL アプリにて、 JavaScript から特定の XPCOM サービスを頻繁に使用するケースがよくあります。そのような場合に処理やソースコードを効率化するためのテクニックをいくつか紹介します。ここでは、例として nsIObserverService を頻繁に利用するケースを想定します。なお、CcComponents.classes, CiComponents.interfaces への参照です。

方式1: 毎回 XPCOM サービスを呼び出す

特に工夫をしない場合、以下のように XPCOM サービスを利用するたびに毎回そのスコープ内で呼び出し手続きを行うことになります。

var MyExtension = {
    init: function() {
        var observerSvc = Cc["@mozilla.org/observer-service;1"].
                          getService(Ci.nsIObserverService);
        observerSvc.addObserver(...);
    },
    uninit: function() {
        var observerSvc = Cc["@mozilla.org/observer-service;1"].
                          getService(Ci.nsIObserverService);
        observerSvc.removeObserver(...);
    },
};

方式2: グローバル変数として定義

最初に XPCOM サービスの呼び出しを行い、グローバル変数 gObserverSvc として参照を保持します。
この方式ですと、スクリプトロード直後(実際に XPCOM サービスを使うタイミングよりも前)に XPCOM サービスの呼び出しが行われます。したがって、その XPCOM サービスを必ずしも使うとは限らないケースには向いていません。

var gObserverSvc = Cc["@mozilla.org/observer-service;1"].
                   getService(Ci.nsIObserverService);

var MyExtension = {
    init: function() {
        gObserverSvc.addObserver(...);
    },
    uninit: function() {
        gObserverSvc.removeObserver(...);
    },
};

方式3: 拡張機能専用オブジェクトのプロパティとして定義

グローバル変数として定義せずに、拡張機能専用オブジェクト MyExtensionobserverSvc プロパティとして定義します。
この方式ですと、実際に XPCOM サービスを使うタイミング(observerSvc プロパティを参照した時)に XPCOM サービスの呼び出しが行われます。逆に、その XPCOM サービスを使わない場合は無駄に XPCOM サービスの呼び出しが行われることがない、という利点があります。

var MyExtension = {
    get observerSvc() {
        return Cc["@mozilla.org/observer-service;1"].
               getService(Ci.nsIObserverService);
    },
    init: function() {
        this.observerSvc.addObserver(...);
    },
    uninit: function() {
        this.observerSvc.removeObserver(...);
    },
};

方式4: 一度取得した参照をキャッシュ

方式3は、XPCOM サービスを使う(observerSvc プロパティを参照する)たびに毎回 XPCOM サービスの呼び出しが行われるという欠点があります。一方、こちらの方式4では、 observerSvc プロパティが初めて参照された際に、取得した XPCOMサービスへの参照を _observerSvc プロパティ(先頭にアンダーバー付きのプライベート的なプロパティ)として保持し、二回目以降のサービス使用時は保持した参照を返します。

var MyExtension = {
    _observerSvc: null,
    get observerSvc() {
        if (!this._observerSvc) {
            this._observerSvc = Cc["@mozilla.org/observer-service;1"].
                                getService(Ci.nsIObserverService);
        }
        return this._observerSvc;
    },
    init: function() {
        this.observerSvc.addObserver(...);
    },
    uninit: function() {
        this.observerSvc.removeObserver(...);
    },
};

方式5: 一度取得した参照をキャッシュ(改良版)

方式4をさらに改良し、プロパティの数を節約したバージョンです。
observerSvc プロパティへの初回参照時に、プロパティ自身を XPCOM サービスへの参照へと置き換えます。
return a = b; という書き方により、 ab を代入して、さらに a の値を返します。

var MyExtension = {
    get observerSvc() {
        delete this.observerSvc;
        return this.observerSvc = Cc["@mozilla.org/observer-service;1"].
                                  getService(Ci.nsIObserverService);
    },
    init: function() {
        this.observerSvc.addObserver(...);
    },
    uninit: function() {
        this.observerSvc.removeObserver(...);
    },
};

方式6: 一度取得した参照をキャッシュ(クラス対応)

方式5のような単独のオブジェクトではなく、クラスのプロパティとする場合、理由はよくわかりませんが以下のように書く必要があるようです。

function MyExtensionClass() { ... }

MyExtensionClass.prototype = {
    get observerSvc() {
        var svc = Cc["@mozilla.org/observer-service;1"].
                  getService(Ci.nsIObserverService);
        this.__defineGetter__("observerSvc", function() svc);
        return svc;
    },
    init: function() {
        this.observerSvc.addObserver(...);
    },
    uninit: function() {
        this.observerSvc.removeObserver(...);
    },
};

方式7: XPCOMUtils.defineLazyServiceGetter を使う

XPCOMUtils.jsm という標準の JavaScript モジュールをインポートすると、方式5は以下のように defineLazyServiceGetter を使って書くことができます。ただし方式7は Firefox 3.6 (Gecko 1.9.2) 以降限定です。また、 browser.xul 内であれば Firefox 本体側ですでにモジュールがインポート済みなので、拡張機能側で改めてインポートする必要はありません。

Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");

var MyExtension = {
    init: function() {
        this.observerSvc.addObserver(...);
    },
    uninit: function() {
        this.observerSvc.removeObserver(...);
    },
};

XPCOMUtils.defineLazyServiceGetter(
    MyExtension, "observerSvc", "@mozilla.org/observer-service;1", "nsIObserverService"
);

方式8: Services.jsm を使う

Services.jsm という標準の JavaScript モジュールをインポートすると、ごく一部の XPCOM サービスへの参照を手軽に取得できます。 nsIObserverService であれば Services.obs で参照できます。実は Services.jsm モジュール内部では XPCOMUtils.defineLazyServiceGetter が使用されており、また、すべての拡張機能および Firefox 本体でシングルトンの参照を保持できるという JavaScript モジュールの特性を考えると、最も効率的な方式といえるでしょう。ただし方式8は Firefox 3.7 (Gecko 1.9.3) 以降限定です。また、 browser.xul 内であれば Firefox 本体側ですでにモジュールがインポート済みなので、拡張機能側で改めてインポートする必要はありません。

Components.utils.import("resource://gre/modules/Services.jsm");

var MyExtension = {
    init: function() {
        Services.obs.addObserver(...);
    },
    uninit: function() {
        Services.obs.removeObserver(...);
    },
};

方式9: 独自モジュール内で定義する

方式8に近いですが、拡張機能独自の JavaScript モジュールを作り、頻繁にアクセスする XPCOM サービスへの参照はそのモジュールのプロパティとして定義(その際に方式5や方式7を利用)してしまう手もあります。たくさんの XUL ウィンドウから XPCOM サービスへアクセスするような規模の大きい拡張機能に向いています。
また、そもそも拡張機能のメインプログラムがモジュール化されている場合は、そのモジュール内で方式5や方式7を使ってキャッシュを行うことで効率化が可能です。

私見

個人的には多くの場合は方式5で、グローバル変数を増やしても他への影響が少ない拡張機能独自のウィンドウ内で、それほど効率化を意識する必要の無い場面や単に面倒な場合は方式2、という感じです。最近の Firefox 本体のソースコードを見ると方式7,8が主流となりつつあるようですが、古いバージョンの Firefox にも対応しなければならない拡張機能としては、今のところは対応する Firefox のバージョンが限られてしまうのが難点です。

TOP

Jetpack SDK 0.3 コンテキストメニューAPIの使用例

先日 Jetpack SDK 0.3 がリリースされました。このバージョンのSDKではブラウザ上でのコンテキストメニューへ項目を追加するための context-menu API などが導入されており、少しずつですが実用的な機能を手軽にを開発できるようになってきました。この記事では context-menu API を実際に使用し、右クリックメニューから現在のページの短縮 URL を bit.ly で表示する機能を開発する手順を解説します。なお、 Jetpack SDK 自体の基本的な使い方については、 はじめての Jetpack SDK 0.2 を参照してください。

パッケージマニフェストの作成

Jetpack SDK 0.3 を展開したフォルダの下の packages フォルダ下に、これから作成するパッケージのルートフォルダである context-menu-example フォルダを作成し、その中に以下のような内容のファイル package.json を作成します。

{
    "id": "context-menu-example@xuldev.org",
    "name": "context-menu-example",
    "fullName": "Jetpack: Context Menu Example",
    "version": "0.1",
    "description": "An example for using the context menu API.",
    "author": "Gomita <gomita@xuldev.org>"
}

注意: Jetpack SDK 0.4 に対応させる場合はパッケージマニフェスト内の “id” プロパティを削除してください。

なお、 Jetpack SDK 0.2 から 0.3 でパッケージマニフェストの仕様が若干変更されており、 “name” プロパティの値はパッケージのルートフォルダのフォルダ名と同一であり、なおかつスペースを含まない値とする必要があります。また、 “name” プロパティは無くても構いません。一方、アドオンマネージャ上で表示するためのパッケージの名称は “fullName” プロパティとして宣言してください。詳細はSDK ドキュメント をご覧ください。

パッケージマニフェストを作成したら、いったんパッケージが正常に読み込まれることを確認します。 OS が Windows の場合、コマンドプロンプトから binactivate で Jetpack SDK 0.3 を活性化させ、 cfx docs コマンドで SDK ドキュメントを開き、「Package Reference」の下に「context-menu-example」が表示されることを確認してください。

プログラムの作成

ここからはプログラムの作成に取り掛かります。パッケージのルートフォルダ内にプログラム格納用の lib フォルダを作成し、さらにその下にメインプログラム main.js を作成してください。 exports.main メソッドには、以下のようにコンテキストメニューへ項目を追加する処理を記述します。

exports.main = function(options, callbacks) {
    var contextMenu = require("context-menu");
    var newItem = contextMenu.Item({
        label: "Shorten with bit.ly",
        onClick: handleClick
    });
    contextMenu.add(newItem);
};

コンテキストメニューへ新しい項目を追加するには、まず require 関数で context-menu API のモジュールを読み込み、 contextMenu.Item コンストラクタでメニュー項目のインスタンスを生成し、 contextMenu.add で実際に使用できるようにします。 contextMenu.Item コンストラクタの引数は、下表に挙げるようなプロパティを有するオブジェクトです。なお、 context-menu API の詳細は SDK ドキュメント を参照してください。

プロパティ 概要
label メニュー項目の表示文言。現在のところ日本語は使用不可。
data メニュー項目に関連付ける文字列。 xul:menuitem 要素の value 属性となる。
onClick メニュー項目をクリックしたときに呼び出されるコールバック関数。コールバック関数には contextObjitem の2つの引数が渡される。詳細は後述。
context メニュー項目を特殊な場面(画像上、リンク上などで右クリックしたとき)だけ表示させる場合に指定する。指定しない場合は純粋なページ上での右クリック時のみ表示される。

次に、 contextMenu.Item コンストラクタの引数オブジェクトの onClick プロパティに指定したコールバック関数 handleClick の内容を記述します。 exports.main の外側に以下の内容を追加してください。

function handleClick(contextObj, item) {
    var url   = contextObj.window.top.location.href;
    var title = contextObj.window.top.document.title;
    url   = encodeURIComponent(url);
    title = encodeURIComponent(title);
    require("tab-browser").addTab("http://bit.ly/?v=3&u=" + url + "&s=" + title);
}

onClick プロパティで指定したコールバック関数には contextObjitem の2つの引数が渡されます。前者は window, document, node という3つのプロパティを有する特殊なオブジェクトで、後者はクリックしたメニュー項目自体のインスタンスです。 contextObj.window プロパティはコンテキストメニューを開いた対象のページの DOM の window オブジェクトですので、 contextObj.window.location.href で現在のページの URL を取得することができます。ここでは、フレーム内部で右クリックした場合を考慮して window.top から最上位のフレームの URL およびタイトルを取得します。

引き続き取得した URL およびページのタイトルを encodeURIComponent 関数でエンコードし、 bit.ly の URL へパラメータとして付加してタブで開きます。ここで、タブで開く処理には Jetpack SDK 0.3 で導入された tab-browser API を使用します。現時点では tab-browser API は未完成のためか SDK ドキュメントには仕様が記載されていませんが、 addTab メソッドによって引数で渡した URL を新しいタブで開いたりすることが可能です。

動作確認

以上で実装は完了です。 cfx run -a firefox コマンドで動作確認しましょう。なお、 Jetpack SDK 0.3 から追加された機能として、以下のように -P オプションで指定したパスのプロファイルディレクトリから Firefox を起動して動作確認をすることができます。毎回クリーンなプロファイルを使用するのではなく、一定の動作確認専用プロファイルを使用したい場合に便利です。

cfx run -a firefox -P "%appdata%MozillaFirefoxProfilesjetpack.testpilot"

Jetpack SDK 0.4 では頻繁に使用するオプションを local.json に記述して呼び出すことが可能です(詳細)。

XPI インストーラ生成

上記手順で開発したパッケージを配布するには、 cfx xpi コマンドで XPI インストーラを生成します。生成したインストーラを Add-ons for Firefox にアップロードして公開することも可能なようです。なお、この手順で生成したインストーラは、将来的には Firefox の再起動無しにインストール・アンインストールが可能となる予定ですが、現時点では通常の拡張機能同様に再起動が必要となります。

TOP

xul:textbox のプレースホルダー文字列

modest に投稿した記事と同内容です。

注意: この記事は Firefox 3.7 での新機能について触れています。

HTML のテキストボックス

Firefox 3.7a5pre では HTML 5 の仕様である placeholder 属性 が実装されており、以下のようにしてテキストボックスにヒント用文字列を表示させることができます。

<input type="text" placeholder="Your Name">

XUL のテキストボックス

一方、 XUL の textbox 要素では Firefox 3.0 にてすでに同様の機能が emptytext 属性として実装済みですが、 Firefox 3.7a5pre では placeholder 属性でもヒント用文字列を表示することができます。互換性維持のため emptytext 属性も引き続き利用可能ですので、 Firefox 3.6 と 3.7 両対応の拡張機能などでは emptytext 属性を使用したほうが良いでしょう。

<textbox emptytext="Your Name" />
<textbox placeholder="Your Name" />

また、以下のように xul:textbox 要素の emptyText (大文字小文字に注意)および placeholder プロパティを使って JavaScript で動的にヒント用文字列をセットすることも可能です。

document.getElementById(...).emptyText = "Your Name";
document.getElementById(...).placeholder = "Your Name";

TOP

nsIEventListenerService でDOMイベントリスナを列挙する

modest に投稿した記事と同内容です。

nsIEventListenerService というXPCOMサービスを使うと、 XUL や HTML ドキュメント内のある要素に対してどんなDOMイベントリスナが追加されているかを調べることができます。以下は、 browser.xul にてFirefoxの「ホーム」ボタンに追加されたイベントリスナをエラーコンソールに列挙するサンプルです。

var els = Cc["@mozilla.org/eventlistenerservice;1"].
          getService(Ci.nsIEventListenerService);
var infos = els.getListenerInfoFor(document.getElementById("home-button"), {});
infos.forEach(function(info) {
    Application.console.log(info.type + " => " + info.toSource());
});

nsIEventListenerService の getListenerInfoFor メソッドは、引数で渡した要素のイベントリスナの情報を、 nsIEventListenerInfo オブジェクトの配列として返します。さらに、各 nsIEventListenerInfo オブジェクトについて、 type プロパティでイベントリスナの種類(click, keypress, mousedown など)を調べたり、 JavaScript のリスナであれば toSource() で内容を文字列化したりできます。ただし、 nsIEventListenerInfo オブジェクトはイベントリスナそのものではないので、 getListenerInfoFor で取得したイベントリスナを removeEventListener で削除する、といったことはできません。あくまでもデバッグ用です。

なお、 nsIEventListenerService は Firefox 3.6 (Gecko 1.9.2) 以降で利用可能です。

TOP

一定時間ドラッグオーバーし続けたら処理を実行する

modest に投稿した記事と同内容です。

拡張機能(XULアプリ)にて、一定時間ドラッグオーバーし続けたときに何らかの処理を実行したい、例えばツールバーに配置したボタン上にブラウザタブを3秒間ドラッグオーバーし続けたら、そのボタンをクリックしたものとみなして処理を実行したいとします。

これは、HTML5のドラッグ&ドロップAPIを使い、ドラッグオーバーし続けた際に dragover イベントが繰り返し発生する特性を利用すると、以下のように実装可能です。

以下は、ボタン上に何かを3秒間ドラッグオーバーし続けると、テキストボックスに現在時刻を表示するサンプルです。なお、サンプルコード全量はこちらに置いてあります。 chrome 権限は不要ですので、ダウンロードして拡張子を.xulにしてFirefoxで開けば、動作確認可能です。

XUL:

<button label="Drag something over here for 3 seconds."
        ondragenter="MyExtension.handleDragEvent(event);"
        ondragover="MyExtension.handleDragEvent(event);"
        oncommand="this.nextSibling.value += new Date() + '
';" />
<textbox multiline="true" flex="1" />

JavaScript:

var MyExtension = {

    _dragStartTime: null,

    handleDragEvent: function(event) {
        event.preventDefault();
        switch (event.type) {
            case "dragenter": 
                // ドラッグオーバー開始時、ドラッグオーバー開始時刻をセット
                this._dragStartTime = Date.now();
                break;
            case "dragover": 
                // ドラッグオーバー中、ドラッグオーバー開始時刻からの経過時間を調べる
                if (this._dragStartTime && Date.now() - this._dragStartTime > 3000) {
                    // 3秒以上経過したら、ドラッグ開始時刻をリセットし、処理を実行する
                    this._dragStartTime = null;
                    event.target.doCommand();
                }
                break;
        }
    }

};

タイマーを用いた実装方式

ドラッグオーバー開始時(dragenter イベント発生時)に setTimeout で一定時間後に処理を実行するためのタイマーを設定し、ドラッグオーバー終了時(dragleave イベント発生時)に clearTimeout でタイマーを解除する、という実装方式ももちろん可能です。

XUL:

<button id="myButton"
        label="Drag something over here for 3 seconds."
        ondragenter="MyExtension.handleDragEvent(event);"
        ondragleave="MyExtension.handleDragEvent(event);"
        oncommand="this.nextSibling.value += new Date() + '
';" />
<textbox multiline="true" flex="1" />

JavaScript:

var MyExtension = {
    _dragOverTimer: null,
    handleDragEvent: function(event) {
        event.preventDefault();
        switch (event.type) {
            case "dragenter": 
                // dragenterイベントが二回連続で発生した場合への対策
                if (this._dragOverTimer)
                    return;
                // ドラッグオーバー開始時にタイマーを設定
                this._dragOverTimer = setTimeout(function() {
                    document.getElementById("myButton").doCommand();
                }, 3000);
                break;
            case "dragleave": 
                // ドラッグオーバー終了時にタイマーを解除
                clearTimeout(this._dragOverTimer);
                this._dragOverTimer = null;
                break;
        }
    }
};

TOP

はじめての Jetpack SDK 0.2

先日 Mozilla Labs のサイトにて Jetpack SDK の新バージョンである SDK 0.2 が公開されました。SDK 0.2 では、 SDK 0.1 で見つかった Windows 上での不具合などが修正されています。 SDK 0.2 は依然として API は充実しておらず、実用的な機能を手軽に作ることはできませんが、 SDK を用いた開発の雰囲気を一通り味わうことができます。

この記事では SDK 0.2 による開発環境のセットアップから始め、 SDK 0.2 を使用して実際に簡単な機能を開発するための手順を解説します。なお、 OS は Windows を前提としますが、おおよその手順は他の OS でも大差無いと思います。

Python インストール

Jetpack SDK を動作させるには Python のインストールが必要となります。インストール方法は OS によって異なると思いますが、 Windows の場合、 Python のサイトから「Python 2.6.5 Windows installer」をダウンロードし、ウィザードに従ってインストールを実施します。ここでは、インストール先を「C:Python26」とします。

インストール完了後、コマンドラインにて「python」コマンドを有効にするため、 Windows のユーザー環境変数の変数「Path」へ値「C:Python26」を追加(すでに別の値が存在する場合は「;」で区切って追加)し、 Python インストール先フォルダへのパスを通します。スタートメニューの「ファイル名を指定して実行」で「cmd」と入力してコマンドプロンプトを起動し、「python -V」と入力して「Python 2.6.5」と出力されることを確認します。

C:>python -V
Python 2.6.5

なお、 Jetpack SDK Docs には Python 2.5 以上のバージョンが必要と記載されていますが、現在のところ Python 3.0.1 には対応していないようです。また、 Windows の場合は「Windows 用拡張モジュール」が必要と記載されていますが、実際はインストールしなくても問題ないようです。

Jetpack SDK セットアップ

次に、 Jetpack SDK のセットアップを行います。 Mozilla Labs のサイトから Jetpack SDK 0.2 のパッケージをダウンロードし、お好みの位置へ展開します。ここでは、「C:jetpack-sdk-0.2」へ展開するものとします。

Jetpack SDK を使用する際は、毎回最初に「活性化」させる必要があります。コマンドプロンプトを起動し、 Jetpack SDK パッケージ展開先フォルダへ移動し、「binactivate」と入力します。

C:jetpack-sdk-0.2>binactivate
Welcome to the Jetpack SDK. Run 'cfx docs' for assistance.
(C:jetpack-sdk-0.2) C:jetpack-sdk-0.2>

引き続き、「cfx docs」コマンドを入力して SDK ドキュメントをブラウザで表示します。SDK ドキュメントはポート8888を待ち受けポートとしたローカルのWebサーバ上で表示されます。

(C:jetpack-sdk-0.2) C:jetpack-sdk-0.2>cfx docs
One moment.
Opening web browser to http://127.0.0.1:8888.

パッケージのフォルダ構成

Jetpack SDK で開発する機能の単位をパッケージと呼びます。ここからは、単純な hello-world パッケージを作成する手順へと移りますが、その前に hello-world パッケージが最終的にどのようなフォルダ構成となるかを、下表に示します。

フォルダ/ファイル 概要
フォルダjetpack-sdk-0.2 Jetpack SDK 展開先フォルダ
フォルダpackages パッケージ格納フォルダ
フォルダhello-world パッケージのルートフォルダ
ファイルpackage.json マニフェストファイル
ファイルREADME.md ドキュメントファイル
フォルダlib プログラム格納フォルダ
ファイルmain.js メインプログラム
ファイルsimple-dialog.js 自作ライブラリ

Jetpack SDK を展開したフォルダの下の「packages」フォルダ内に個々のパッケージのルートフォルダがあり、その下には「package.json」という名前のマニフェストファイルがあります。「README.md」はパッケージの詳細を記述するためのドキュメントファイルで、必要に応じて配置します。「lib」フォルダ内には、パッケージのメインプログラムや自作ライブラリのプログラムを格納します。

パッケージの作成

それでは、「C:jetpack-sdk-0.2packages」フォルダ下に hello-world パッケージ用の「hello-world」フォルダを作成します。次に、パッケージのルートフォルダ内にマニフェストファイル「package.json」を作成します。マニフェストファイルにはパッケージに関するメタ情報を JSON 形式で記述します。拡張機能を作成したことのある方であれば、インストールマニフェスト「install.rdf」に近いものと考えてください。ここでは、以下のような内容を記述します。

{
    "id": "helloworld@xuldev.org",
    "version": "0.1",
    "description": "This is my first package.",
    "author": "Gomita <gomita@xuldev.org>"
}

注意: Jetpack SDK 0.4 に対応させる場合はパッケージマニフェスト内の “id” プロパティを削除してください。

"id" プロパティはすべての拡張機能および Jetpack パッケージを一意に識別するための文字列で、一般的にはメールアドレスのような形式にします。拡張機能のインストールマニフェストの <em:id> タグに相当します。

次に、さきほどブラウザで開いた SDK ドキュメントのページを更新し、「Package Reference」に「hello-world」が追加されたことを確認してください。

プログラムの作成

引き続き、 hello-world パッケージへメインプログラムを追加して、動作できるようにします。パッケージのルートフォルダの下に「lib」フォルダを作成します。「lib」フォルダ内にメインプログラムである「main.js」ファイルを作成し、以下のような内容を記述してください。

exports.main = function(options, callbacks) {
    console.log("Hello, World!");
};

メインプログラムは「main」という名前のひとつのモジュールとなっており、 CommonJS 形式の exports.main = ... という記法によって main プロパティのみをモジュール外部からアクセス可能にします。また、 console.log は Jetpack 標準のグローバル関数のひとつで、 Jetpack SDK のコマンドプロンプトへデバッグ用文字列を出力します。

なお、現時点では console.log("こんにちは"); のように日本語を記述しても、正常に動作しません。将来的に提供されるはずのローカライズ用APIを利用することになるはずです。

テスト実行

メインプログラムを作成したら、さっそくテスト実行してみます。テスト実行するには SDK のコマンドプロンプトへ「cfx run -a firefox」コマンドを入力します。「cfx run」コマンドへ「-a firefox」オプションを付加することで、新規の Firefox プロファイルへ先ほど作成したパッケージのみがインストールされた状態で Firefox が起動します。

(C:jetpack-sdk-0.2) C:jetpack-sdk-0.2>cd packageshello-world

(C:jetpack-sdk-0.2) C:jetpack-sdk-0.2packageshello-world>cfx run -a firefox
info: Hello, World!
OK
Total time: 1.531000 seconds
Program terminated unsuccessfully.

Firefox 起動後、コマンドプロンプトに「info: Hello, World!」と表示されることを確認してください。Firefox のウィンドウをすべて閉じると、テスト実行も終了します。

標準ライブラリの使用

ここからは、 Jetpack 標準ライブラリのひとつである timer ライブラリを使用して、さきほどのプログラムを少し変更してみます。 timer ライブラリはタイマー関連の処理をひとまとめにしたモジュールで、 DOM の window.setTimeout, window.clearTimeout などとほぼ同等の機能を提供します。ライブラリの詳細を調べるには、 SDK ドキュメントを参照してください。なお、 SDK ドキュメントには記載されていませんが、 timer.setInterval, timer.clearInterval も利用可能です。

メインプログラム内でライブラリをインポートして利用可能にするには、 CommonJS 形式の require 関数を使用します。メインプログラム「main.js」を下記のように修正してください。

var timer = require("timer");

exports.main = function(options, callbacks) {
    timer.setInterval(function() {
        console.log(new Date().toLocaleTimeString());
    }, 1000);
};

修正後、「cfx run -a firefox」コマンドでテスト実行し、以下のように SDK のコマンドプロンプトへ1秒おきに現在時刻が出力されることを確認してください。

(C:jetpack-sdk-0.2) C:jetpack-sdk-0.2packageshello-world>cfx run -a firefox
info: 10:37:21
info: 10:37:22
info: 10:37:23
info: 10:37:24
info: 10:37:25

自作ライブラリの作成

ここからは、 Jetpack 標準ライブラリには無い機能を、自作ライブラリとして作成する手順に移ります。拡張機能や Jetpack パッケージ内でファイル読み書きなどの高度な処理を行う場合、 XPCOM と呼ばれるコンポーネントを呼び出す必要があります。 Jetpack には、 XPCOM を使用する高度な処理はモジュール化してメインプログラムから切り離すという設計思想があります。今のところ、ライブラリだけでなくメインプログラム内でも制限なく XPCOM を利用可能ですが、もしかすると将来的にはメインプログラム内では XPCOM 使用不可となる可能性があります。したがって、 XPCOM を使用する処理は極力ライブラリとして実装した方がいいと思われます。

ここでは、 DOM の window.alert のような単純なモーダルダイアログを表示する simple-dialog ライブラリを実装してみます。 Jetpack のプログラムのコンテクストには DOM でおなじみの windowdocument といったオブジェクトが無いため、 window.alert 関数も使用できません。このようなコンテクストでダイアログを表示するためには、 nsIPromptService という XPCOM を使用します(参考)。まず、パッケージのルートフォルダ下の「lib」フォルダ内に「simple-dialog.js」ファイルを作成します。メインプログラム同様にライブラリは exports.メソッド名 = function(...) { ... }; のような CommonJS 形式で実装していきます。

simple-dialog ライブラリには下表の2つのメソッドを実装します。

メソッド 概要
alert(text) 引数 text のラベルとOKボタンを有する警告ダイアログを表示する。 DOM の window.alert と同等。
confirmYesNo(text) 引数 text のラベルと「はい」「いいえ」ボタンを有する確認ダイアログを表示する。メソッドの戻り値は真偽値で、ユーザが「はい」ボタン押下時は true を、「いいえ」ボタン押下時は false を返す。

「simple-dialog.js」には、以下のように記述します。

var promptSvc = Cc["@mozilla.org/embedcomp/prompt-service;1"].
                getService(Ci.nsIPromptService);

exports.alert = function(text) {
    promptSvc.alert(null, "[Jetpack]", text);
};

exports.confirmYesNo = function(text) {
    var pos = promptSvc.confirmEx(
        null, "[Jetpack]", text, promptSvc.STD_YES_NO_BUTTONS,
        null, null, null, null, {}
    );
    return pos == 0;
};

1~2行目は、 nsIPromptService を呼び出す処理です。なお、 Cc, Ci はそれぞれ Components.classes, Components.interfaces への参照であり、 Jetpack 標準のグローバル変数として定義済みです。4~6行目は simple-dialog ライブラリの alert メソッドの実装で、 nsIPromptService の alert メソッドを使って警告ダイアログを表示します。8~14行目は simple-dialog ライブラリの confirmYesNo メソッドの実装で、 nsIPromptService の confirmEx メソッドを使って「はい」「いいえ」ボタン付きの確認ダイアログを表示します。 nsIPromptService の confirmEx メソッドはユーザが押したボタンの番号(「はい」は 0、「いいえ」は 1)を返す仕様ですので、 confirmEx メソッドの戻り値が 0 なら confirmYesNo メソッドは true を返すようにします。

自作ライブラリの使用

先ほど作成した simple-dialog ライブラリをメインプログラムで呼び出し、正常に動作するかを確認します。「main.js」を下記のように修正してください。

var simpleDialog = require("simple-dialog");

exports.main = function(options, callbacks) {
    var adult = simpleDialog.confirmYesNo("Are you over 18 years old?");
    if (adult) {
        simpleDialog.alert("Welcome!");
    }
    else {
        simpleDialog.alert("Good bye!");
    }
};

「cfx run -a firefox」コマンドでテスト実行し、以下のように「はい」「いいえ」ボタン付き確認ダイアログが表示されることを確認してください。また、「はい」「いいえ」それぞれのボタン押下時に適切な警告ダイアログが表示されることを確認してください。

オフライン状態監視機能の実装

ここからは、 hello-world パッケージを修正して、もう少し実用的な機能を実装してみます。 Jetpack 標準ライブラリのひとつである observer-service ライブラリを使い、 Firefox のオンライン/オフライン状態の変化を監視する機能を実装します。

Firefox の内部ではアプリケーションに関する色々なイベントを nsIObserverService という XPCOM によってオブザーバへ通知しています。 Firefox がオフライン状態になったとき、トピック名「network:offline-status-changed」の通知が送信されます。この通知を受けて何らかの処理を実行するには、 observer-service ライブラリの add メソッドを使用します。add メソッドの第1引数には監視する通知のトピック名、第2引数には通知を受けた際に実行するコールバック関数を設定します。コールバック関数には、2つの引数が渡されますが、オフライン状態が変化した際には、第2引数に「online」または「offline」の文字列が渡されます。ここでは、この文字列の値を調べて、状況に応じた内容のダイアログを simple-dialog ライブラリを使って表示させます。

var simpleDialog = require("simple-dialog");
var observer = require("observer-service");

exports.main = function(options, callbacks) {
    observer.add("network:offline-status-changed", function(sbj, data) {
        if (data == "online") {
            simpleDialog.alert("Firefox is now online.");
        }
        else if (data == "offline") {
            simpleDialog.alert("Firefox is now offline.");
        }
    });
};

「cfx run -a firefox」コマンドでテスト実行し、 Firefox 起動後に [ファイル] → [オフライン作業] のチェックボックスをオフにし、以下のようなダイアログが表示されることを確認してください。

ドキュメントの作成

各パッケージについての詳細なドキュメントを追加することで、 SDK ドキュメントの各パッケージのリンクをクリックしたときに表示されるようになります。ドキュメントを追加するには、パッケージのルートフォルダの下に「README.md」ファイルを作成してください。「README.md」は以下のように markdown という記法にて記述します。

This is my *first* package.

* foo
* bar
* baz

「cfx docs」コマンドで SDK ドキュメントをブラウザで表示し、「hello-world」のリンクをクリックするとパッケージのメタ情報とともにドキュメントの内容が整形表示されることを確認してください。

インストーラの作成

これまで作成してきたような Jetpack のパッケージから、一般的な Firefox 拡張機能と同等の XPI インストーラ形式を作成することができます。 XPI インストーラを作成するには、 SDK のコマンドプロンプトでパッケージのルートフォルダへ移動し、「cfx xpi」コマンドを入力します。

(C:jetpack-sdk-0.2) C:jetpack-sdk-0.2packageshello-world>cfx xpi
Exporting extension to hello-world.xpi.

すると、「hello-world.xpi」というファイル名の XPI インストーラが生成されます。これを適当な Firefox のウィンドウへドラッグ&ドロップして、通常の拡張機能としてインストールされることを確認してください。

TOP

Firefox 3.7 でのナビゲーションツールバーのアイコン画像サイズ

modest に投稿した記事と同内容です。

注意:このトピックは Firefox 3.7 での仕様変更について触れています。また、 Windows 版のデフォルトテーマを前提としており、他のOSについては未確認です。他のOSについての情報求みます。

Firefox 3.7 では、ナビゲーションツールバーに配置するボタン(戻る・進む・更新・ホームなど)のアイコン画像サイズが下表のように変わるようです。なお、下表の「小さいアイコン」とは、「ツールバーのカスタマイズ」で「小さいアイコンを使用」オプションを有効にしている場合、あるいはブックマークツールバー上にボタンを配置した場合のアイコンを意味します。

Firefox 3.6 Firefox 3.7
通常アイコン 24×24ピクセル 18×18ピクセル
小さいアイコン 16×16ピクセル 18×18ピクセル

拡張機能にてナビゲーションツールバーにボタンを追加している場合、この仕様変更の影響を受けるようです。ここでは、例として、拡張機能にて下記のような XUL オーバーレイによってナビゲーションツールバーにボタンを追加するとします。

<toolbarpalette id="BrowserToolbarPalette">
  <toolbarbutton id="myaddon-button"
                   class="toolbarbutton-1 chromeclass-toolbar-additional"
                   label="My Addon" />
</toolbarpalette>

Firefox 3.6 までは、一般的には以下のようなスタイルシートによって通常アイコンと小さいアイコンのスタイルを別々に定義します(参考)。なお、「largeicon.png」は24×24ピクセルの画像、「smallicon.png」は16×16ピクセルの画像とします。

/* 通常アイコン */
#myaddon-button {
    list-style-image: url("chrome://myaddon/skin/largeicon.png");
}

/* 小さいアイコン */
toolbar[iconsize="small"] #myaddon-button {
    list-style-image: url("chrome://myaddon/skin/smallicon.png");
}

このとき、 Firefox 3.6 では通常アイコン・小さいアイコンともに本来の画像サイズできれいに表示されますが、 Firefox 3.7 では通常アイコンは「largeicon.png」を本来のサイズである24×24ピクセルから18×18ピクセルへと縮小され、小さいアイコンは「smallicon.png」を本来のサイズである16×16ピクセルを18×18ピクセルへ拡大されます。下表のように、小さいアイコンの表示がきれいでなくなる傾向があります。

Firefox 3.6 Firefox 3.7
通常アイコン
小さいアイコン

Firefox 3.7 でも本来の画像サイズでアイコンをきれいに表示したい場合、いくつかの方法があるかと思いますが、ここでは Firefox 3.7 以上専用のスタイルシートを別途追加する方式を解説します。

Firefox が特定のバージョンの場合に限り、指定した XUL に対してスタイルシートを適用したい場合、以下のようにクロムマニフェストの「style」命令へ「appversion」フラグをセットします。なお、ツールバーボタン用のスタイルシートは、「browser.xul」(ブラウザウィンドウ)と「customizeToolbar.xul」(ツールバーのカスタマイズウィンドウ)の両方に適用させます。

# apply stylesheet if Firefox 3.7a or later
style  chrome://browser/content/browser.xul  chrome://myaddon/skin/fx37.css  appversion>=3.7a
style  chrome://global/content/customizeToolbar.xul  chrome://myaddon/skin/fx37.css  appversion>=3.7a

拡張機能の skin パッケージに含めるFirefox 3.7 以上専用のスタイルシート「fx37.css」 には以下のような内容を記述します。 xul:toolbarbutton 要素自体に画像を設定するのではなく、内部の匿名 xul:image 要素に対して画像およびサイズを設定します。

/* 通常アイコン・小さいアイコン共通 */
#myaddon-button > .toolbarbutton-icon {
    list-style-image: url("chrome://myaddon/skin/smallicon.png");
    width: 16px;
    height: 16px;
}

これにより、以下のように通常アイコン・小さいアイコンともに「smallicon.png」が本来の16×16ピクセルできれいに表示されます。

Firefox 3.7
通常アイコン
小さいアイコン

Firefox 標準のツールバーボタンと同じ18×18ピクセルの画像を Firefox 3.7 以降用のアイコン画像として別途作成し、上記「fx37.css」にて適用するのもよいかもしれません。

TOP

Shifting from Jetpack Prototype to Jetpack Reboot

The Japanese version is also available at modest.

Recently Mozilla Labs Jetpack site was updated and Jetpack Reboot was finally revealed to the public. However, not a few people might think that “What’s Jetpack Reboot? How does it differ from the old Jetpack?”.

Briefly speaking, the old Jetpack (Jetpack Prototype) which has being developed as a Firefox extension, will disappear in the near future, and now it is being replaced to the new Jetpack (Jetpack Reboot) as a brand-new SDK to build new type of extensions.

This post explains the difference between Jetpack Prototype and Jetpack Reboot with three points of view: concepts, architecture and development.

(1) Concepts

There’s no much difference between the basic concepts of both types of Jetpack. Jetpack Reboot has the following concepts as the superiority of extensions utilizing Jetpack Reboot SDK in comparing with the existing commonly-distributed extensions:

  • Web developers can easily develop in their familiar languages: HTML, JavaScript and CSS
  • Rapid development using rich API library to ease our debug and maintenance
  • A robust security model to keep users away from their security risk
  • No need to restart Firefox when installing and uninstalling

(2) Architecture

See the image below which describes the architecture of both types of Jetpack.

In Jetpack Prototype, you need to install Jetpack “extension” which consists of a runtime and API library, and each small program called Jetpack Feature works on Jetpack extension. There seemed to be a plan to integrate Jetpack extension into Firefox. However, it may cause a problem that the integration prevents the agile development of the evolving API library. Additionally, since all features share one API library, it may cause a compatibility problem if each feature uses a different version of API.

Meanwhile in Jetpack Reboot, Firefox doesn’t hold the API related to Jetpack, and each feature becomes a package including the API and bootloader. This architecture enables the agile API development and the API included in each package are not interfered even if they have differing versions. We can distribute the package as an XPI installer same as the existing XUL-based extension. So, we can install and uninstall it in a same way of the existing extension. Additionally, in the future the Firefox extensions manager will be improved so that we will be able to install and uninstall the packages which have Jetpack bootloader without restarting Firefox.

(3) Development

Shifting from Jetpack Prototype to Jetpack Reboot changes the style of extension development. In Jetpack Prototype, we can easily build a feature by making single JavaScript program. Meanwhile in Jetpack Reboot, we need to utilize a development environment called Jetpack SDK and make a package of JavaScript program and JSON-formatted manifest file in the appropriate folder structure. The current version of SDK 0.1 is a command line tool made with Python language. Although the new development style seems to be more likely for the advanced users than the old one, there is a plan to make it easier with a GUI development environment called FlightDeck which works on your browser.

Note that the current SDK doesn’t have rich API to build working extensions like Jetpack Features, since it is in an early stage. I will post a step-by-step tutorial to make a package utilizing the SDK on Windows 7 before too long.

TOP

タブ切り替えパネル風の半透明ポップアップ

modest に投稿した記事と同内容です。

Tab Flick 拡張機能 のポップアップは、 Firefox 3.6 以降の Ctrl+Tab によるタブ切り替えパネルのような半透明の見た目となっています。

ここでは、そこに至るまでの実装の経緯を記しました。なお、簡単のためコードの一部は実際とは異なるものとなっています。

第1段階

まず、 browser.xul にオーバーレイし、 #mainPopupSet をマージポイントとして新しい xul:panel 要素を追加します。

<popupset id="mainPopupSet">
    <panel id="tabFlickPanel" style="width: 200px; height: 200px;" />
</popupset>

openPopup あるいは openPopupAtScreen メソッドでこの xul:panel 要素を開くと、当然見た目はシンプルなポップアップとなります。

第2段階

次に、 xul:panel 要素に対して KUI-panel クラスを指定します。 KUI-panel クラスは browser.xul にて読み込まれているスタイルシート (browser.css) にて定義されています。したがって、 browser.xul へオーバーレイした XUL 内であれば、特にスタイルを定義することなく利用可能となります。

    <panel id="tabFlickPanel" class="KUI-panel" style="width: 200px; height: 200px;" />

これで Windows XP などでの Ctrl+Tab によるタブ切り替えパネルと同じ、黒い半透明の角丸ポップアップとなります。
KUI-panel クラスのスタイルは Firefox 3.5 に同梱されたスタイルシート (browser.css) でも定義済みですので、 Firefox 3.5 でも有効となります。

第3段階

さらに、 xul:panel 要素へ以下のような内容のスタイルシートを適用します。

#tabFlickPanel:-moz-system-metric(windows-compositor) {
    background: transparent;
    -moz-appearance: -moz-win-glass;
    -moz-border-radius: 0;
    border: none;
}

すると、 Windows Vista または Windows 7 で Windows Aero が有効な場合、Aero Glass 効果のある半透明のポップアップとなります。

第4段階

しかし、ここでひとつ問題が生じます。詳しい理由はわかりませんが、ポップアップの右下角に余計な枠線が表示されてしまいます。
これを解決するには、なぜか xul:panel 要素の collapsed を以下のようにして切り替えてあげる必要があります。

    <panel id="tabFlickPanel" class="KUI-panel" style="width: 200px; height: 200px;"
          collapsed="true"
          onpopupshown="this.collapsed = false;"
          onpopuphiding="this.collapsed = true;" />

これでようやく解決しました。

この方法は裏技的なものですので、拡張機能などでご利用の際はご注意ください。

TOP

ブラウザウィンドウのサムネイルを描画する

modest に投稿した記事と同内容です。

タブカタログ拡張機能のようにWebページのサムネイル画像を表示する拡張機能の多くは、 html:canvas 要素の二次元描画コンテクストの drawWindow メソッドへWebページの window オブジェクトなどを引数で渡してサムネイルの描画を行っています(canvas を使って図形を描く – MDC)。

この drawWindow メソッドの引数にWebページの window オブジェクトではなく、ブラウザウィンドウの ChromeWindow オブジェクトを渡すことで、ブラウザウィンドウのサムネイル画像を描画することも可能です。しかし、以下のようにブラウザタブ内に表示されたWebページまでは描画されず、背景色で塗りつぶされたようになってしまいます。

この問題を解決するには、ブラウザウィンドウのサムネイルの上に、Web ページのサムネイルを重ねて描画する必要があります。この記事では、その方式を紹介します。

XUL

ブラウザウィンドウのサムネイル描画用の html:canvas 要素を生成します。

<html:canvas id="testCanvas" />

JavaScript

最初にサムネイルの拡大/縮小率を定めておきます。ここでは50%に縮小することとします。

const scale = 0.5;

サムネイル描画の対象とするブラウザウィンドウとして、 nsIWindowMediator を使って直近のブラウザウィンドウの ChromeWindow オブジェクトを取得します。

var win = Components.classes["@mozilla.org/appshell/window-mediator;1"].
          getService(Components.interfaces.nsIWindowMediator).
          getMostRecentWindow("navigator:browser");

簡便のため、 html:canvas 要素への参照を取得しておきます。

var canvas = document.getElementById("testCanvas");

ブラウザウィンドウのサイズに合わせ、 html:canvas 要素のサイズを調整します。

var w = win.innerWidth;
var h = win.innerHeight;
canvas.width  = w * scale;
canvas.height = h * scale;

二次元描画コンテクストを取得し、ブラウザウィンドウのサムネイルを描画します。

var ctx = canvas.getContext("2d");
ctx.save();
ctx.scale(scale, scale);
ctx.drawWindow(win, 0, 0, w, h, "rgb(255,255,255)");

引き続き、Webページのサムネイルを描画しますが、その前に原点を左上(座標0, 0)から移動させる必要があります。原点を移動させるためには、二次元描画コンテクストの translate メソッドを使います。移動量は、ブラウザウィンドウに対する現在のブラウザ(xul:browser 要素)の位置から算出します。

var rect = win.gBrowser.mCurrentBrowser.getBoundingClientRect();
ctx.translate(rect.left, rect.top);

最後にWebページのサムネイルを重ねて描画します。なお、 win.contentwin に対応するブラウザウィンドウの現在のタブに表示されたWebページの window オブジェクトを表します。

w = win.content.innerWidth  || win.content.document.documentElement.clientWidth;
h = win.content.innerHeight || win.content.document.documentElement.clientHeight;
ctx.drawWindow(win.content, win.content.scrollX, win.content.scrollY, w, h, "rgb(255,255,255)");
ctx.restore();

以上のコードに適当に肉付けして実行してみると、以下のようにブラウザタブ内のWebページも含めてブラウザウィンドウのサムネイルが描画されます。

注意

今回はブラウザウィンドウ内の現在のブラウザタブのWebページだけが見えている前提となっていますが、分割ブラウザ (Split Browser)を使っている場合、この限りではありません。そのような場合にも対応させるには、さらなる工夫が必要となります。

補足

元々筆者はブラウザウィンドウのサムネイルを描画するために、ブラウザウィンドウ用とWebページ用の2つの html:canvas 要素を xul:stack 要素で重ねる方式を考え付きましたが、Piroさんのアドバイスにより、この記事で紹介したように原点を移動させつつ1個の html:canvas 要素に対して2回 drawWindow メソッドを使用する方式が可能なことがわかりました。

TOP