Now browsing the SCRAPBLOG weblog archives.

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

ファイル書き込み処理を別スレッドで行う

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

注意: この記事の内容は Firefox 3.6 以降で追加される新機能について触れています。

Firefox ではブラウズ中のセッション状態を保存するために、デフォルトで10秒に1回、JSON形式のデータをプロファイルフォルダ下の sessionstore.js へ書き出す処理を行っています。
しかし、 Firefox 3.5 まではこの処理が原因で YouTube の動画閲覧中にプチフリーズが頻発するといった現象が見られたようです。そこで、 Firefox 3.6 以降では、ファイル書き込み処理を別スレッドで行うことで、このプチフリーズが発生しないよう改善されることになりました (Bug 485976 – Move writing sessionstore.js off the main thread)。

この別スレッドでのファイル書き込み処理は nsIAsyncStreamCopier という XPCOM にて実装されていますが、 NetUtil.jsm という JavaScript モジュールをインポートすることで、拡張機能などから簡単に利用することができます。

サンプル

以下、別スレッドにてファイルへ文字列を書き出すサンプルを作ってみます。なお、ソースコード中の Cc, Ci は、それぞれ Components.classes, Components.interfaces への参照です。

最初に、 JavaScript モジュールをインポートします。当然インポートは最初に一度だけ行えば良く、ファイルへの書き出しを行うたびに行う必要はありません。

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

次に、書き出し先のファイル(nsILocalFile オブジェクト)を生成します。なお、変数 path の値は各自の環境に合わせて適宜修正してください。

var path = "C:***.txt";
var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
file.initWithPath(path);

次に、 nsISafeOutputStream によって安全にファイルへ出力するためのストリームを生成します。どういうことかと言うと、ファイル書き込み中は「test-1.txt」のような別名の一時ファイルへ書き込み、書き込みが完了したら本来の「test.txt」へ上書きすることで、ファイルが破損しにくい仕組みとなっています(参考)。

var ostream = Cc["@mozilla.org/network/safe-file-output-stream;1"].
              createInstance(Ci.nsIFileOutputStream);
ostream.init(file, -1, -1, 0);

次に、ファイルへ書き込む文字列から、入力用のストリームを生成します。

const TEST_DATA = "this is a test string";
var istream = Cc["@mozilla.org/io/string-input-stream;1"].
              createInstance(Ci.nsIStringInputStream);
istream.setData(TEST_DATA, TEST_DATA.length);

なお、日本語を含む文字列を UTF-8 エンコードでファイルへ書き出す場合、以下のように nsIScriptableUnicodeConverter を使って入力用ストリームを生成します。

const TEST_DATA = "これはテスト用文字列です";
var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
                createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
var istream = converter.convertToInputStream(TEST_DATA);

最後に、 NetUtil.asyncCopy を使い、入力用ストリームを出力用ストリームへコピーし、別スレッド上でファイルへの書き込みを行います。3番目の引数は別スレッドでのファイル書き込みが完了した際に呼び出されるコールバック関数です。

NetUtil.asyncCopy(istream, ostream, function(result) {
    if (Components.isSuccessCode(result))
        alert("ファイル書き込み成功");
});

今回のサンプルでは書き出す文字列が少ないため、別スレッドで処理が行われていることを体感できないと思います。そこで、以下のように長大な文字列を生成して試してみると、 Firefox がフリーズすることなくファイルへの書き出しが行われることが体感できるかと思います。ただし、 for ループ自体が重いため、ファイル書き出し前にフリーズが発生します。

var TEST_DATA = "";
// ループ回数を少しずつ増やしながら調整してください
for (var i = 0; i < 100; i++) {
    TEST_DATA += "this is a test string
";
}

リファレンス

TOP

ダウンロードマネージャに進捗状況を表示させつつダウンロードする

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

拡張機能にて、ある URL からファイルをダウンロードするには、 Downloading Files – MDC で解説されているように nsIWebBrowserPersist::saveURI を使うのが一般的です。この方法でダウンロードをすると、ダウンロードマネージャの UI 上に進捗状況が表示されず、バックグラウンド処理のような感じでダウンロードが進行します。

では、ダウンロードマネージャに進捗状況を表示させつつダウンロードするには、どうすれば良いのでしょうか?そのためには、 Firefox 3 以降で導入された nsIDownloadManager インタフェースの API を利用します。

ダウンロードマネージャ

ここからは例として Google のロゴ画像をダウンロードし、ローカルファイルとして保存する手順を解説します。なお、ソースコード中の Cc, Ci は、それぞれ Components.classes, Components.interfaces への参照です。

まず、ダウンロード元のURLから、 nsIURI オブジェクトを生成します。

var sourceURL = "http://www.google.com/intl/en_ALL/images/logo.gif";
var ioSvc = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
var sourceURI = ioSvc.newURI(sourceURL, null, null);

次に、保存先ファイルのパスから、 URL が file:// 形式の nsIURI オブジェクトを生成します。
なお、変数 targetPath にセットするファイルパスは、各自の環境に合わせて適宜修正してください。

var targetPath = "C:***.gif";
var targetFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
targetFile.initWithPath(targetPath);
var targetURI = ioSvc.newFileURI(targetFile);

次に、ダウンロードを行うための nsIWebBrowserPersist のインスタンスを生成します。 persistFlags プロパティには、お好みに応じてフラグを設定してください。今回は、保存先ファイルがすでに存在する場合は上書きするフラグ、キャッシュを使わずに最新のデータをダウンロードするフラグ、 gzip 圧縮などがされている場合に自動で展開するフラグの3つを設定します。

var persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"].
              createInstance(Ci.nsIWebBrowserPersist);
persist.persistFlags = Ci.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES |
                       Ci.nsIWebBrowserPersist.PERSIST_FLAGS_BYPASS_CACHE |
                       Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;

いよいよ、今回の要となる nsIDownloadManager のサービスを呼び出し、 addDownload メソッドによってダウンロードマネージャへ新しいエントリを追加します。 addDownload メソッドの個々の引数についての説明は、下記コード中のコメントを参照ください。

var dlMgr = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
var dl = dlMgr.addDownload(
    Ci.nsIDownloadManager.DOWNLOAD_TYPE_DOWNLOAD,    // ダウンロードマネージャ上での表示形式
    sourceURI,    // ダウンロード元の nsIURI オブジェクト
    targetURI,    // 保存先ファイルの nsIURI オブジェクト
    null,    // ダウンロードマネージャ上での表示名。 null なら保存先ファイル名となる。
    null,    // nsIMIMEInfo オブジェクト。詳細不明だが null で問題なし。
    Math.round(Date.now() * 1000),    // ダウンロード開始時刻。現在時刻を指定すればよい。
    null,    // 一時ファイルを作ってダウンロードする際に nsILocalFile を指定する。
    persist    // 先ほど生成した nsIWebBrowserPersist オブジェクトを渡す。
);

addDownload メソッドの戻り値は、ダウンロードマネージャにより管理される個々のエントリに対応した nsIDownload オブジェクトとなっています。この nsIDownload オブジェクトから nsIWebBrowserProgressListener インタフェースを呼び出して以下のようにすると、 nsIWebBrowserPersist 側のダウンロード進捗状況の変化がダウンロードマネージャ側へ伝わるようになります。

persist.progressListener = dl.QueryInterface(Ci.nsIWebProgressListener);

最後に、 nsIWebBrowserPersist::saveURI メソッドを実行し、実際にダウンロードの処理を開始させます。なお、 saveURI の引数にダウンロード元の nsIURI オブジェクトと保存先ファイルの nsILocalFile オブジェクトを渡す必要がありますが、それぞれ nsIDownload オブジェクトの source, targetFile プロパティから参照可能です。もちろん、これまでの一連の処理で登場した変数 sourceURI, targetFileの2つを渡しても構いません。

persist.saveURI(dl.source, null, null, null, null, dl.targetFile);

以上のような手順でダウンロードマネージャと連携しつつダウンロードした場合、単に nsIWebBrowserPersist::saveURI を使ってダウンロードした場合とは異なり、ダウンロード中に Firefox を終了させても再起動時に自動的にレジュームが開始されるというメリットがあります。したがって、巨大なファイルをダウンロードするような拡張機能では利用価値の高い方法となるかもしれません。

saveURL ヘルパー関数

ここまでかなり長いコードを書いてダウンロードマネージャに進捗状況を表示させつつダウンロードする方法を解説しましたが、実はブラウザウィンドウ (browser.xul) のように chrome://global/content/contentAreaUtils.js が読み込まれているウィンドウ内であれば、 saveURL というヘルパー関数を使って以下のようにいとも簡単に実現可能です。

saveURL("http://www.google.com/intl/en_ALL/images/logo.gif", "logo.gif", null, true, true, null);

5番目の引数を false に変えることでファイル選択ダイアログを表示させることなどもできますし、実際の拡張機能ではこの saveURL 関数を使うケースの方が多いかもしれませんね。

関連ドキュメント

TOP