Now browsing the archives for the 'XPCOM' category.

nsIZipWriter を使ってフォルダ丸ごと圧縮

前提

・Firefox 21~24.0a1
・変数 srcDir は圧縮元フォルダの nsILocalFile オブジェクト
・変数 zipFile は圧縮先ファイルの nsILocalFile オブジェクト
srcDir の中身のファイルをすべて圧縮して新規のアーカイブ zipFile を生成する

nsIZipWriter インスタンス生成

はじめに nsIZipWriter インスタンスを生成し、 open メソッドで圧縮先ファイルを開く。
圧縮率はデフォルト(レベル6)とする。

// |zipFile| is a nsILocalFile object corresponding to the zip file
var zipWriter = Cc["@mozilla.org/zipwriter;1"].createInstance(Ci.nsIZipWriter);
const PR_WRONLY = 0x02;
const PR_CREATE_FILE = 0x08;
zipWriter.open(zipFile, PR_WRONLY | PR_CREATE_FILE);
var zipLevel = Ci.nsIZipWriter.COMPRESSION_DEFAULT;

フォルダ/ファイルのエントリ追加

srcDir を起点に、フォルダ内のファイルへ再帰的にアクセスする。

(function(dir) {
    var fileEnum = dir.directoryEntries;
    while (fileEnum.hasMoreElements()) {
        var file = fileEnum.getNext().QueryInterface(Ci.nsILocalFile);
        if (file.isDirectory()) {
            // [ToDo]            
            // go to sub folder recursively
            arguments.callee.call(this, file);
        }
        else if (file.isFile()) {
            // [ToDo]            
        }
    }
}).call(this, srcDir);
// |srcDir| is a nsILocalFile object corresponding to the folder where will be archived

変数 file がフォルダなら、 addEntryDirectory でエントリを追加する。第3引数 false だと即座に圧縮される。
変数 file がファイルなら、 addEntryFile でエントリを追加する。第4引数 false だと即座に圧縮される。
変数 file が通常のフォルダでもファイルでもなく、ショートカット(シンボリックリンク)などの場合は何もしない。

エントリ名は、圧縮元フォルダを起点にした圧縮元ファイルのパス。例えば、圧縮元ファイルのパスが「C:UsersHogeSourceDirectoryAAABBBccc.txt」なら、エントリ名は「AAA/BBB/ccc.txt」となる。フォルダの場合はエントリ名の末尾に「/」を付ける。

        var entry = file.path.substr(srcDir.path.length + 1).replace("", "/", "g");
        if (file.isDirectory()) {
            entry += "/";
            zipWriter.addEntryDirectory(entry, file.lastModifiedTime * 1000, false);
            // go to sub folder recursively
            arguments.callee.call(this, file);
        }
        else if (file.isFile()) {
            zipWriter.addEntryFile(entry, zipLevel, file, false);
        }

最後に圧縮先ファイルを閉じる。

zipWriter.close();

processQueue を使って後からまとめて圧縮

addEntryDirectory, addEntryFile の最後の引数を true にすると、その時点では圧縮されず、後で processQueue メソッド呼び出し時にまとめて圧縮される。

        if (file.isDirectory()) {
            entry += "/";
            zipWriter.addEntryDirectory(entry, file.lastModifiedTime * 1000, true);
            arguments.callee.call(this, file);
        }
        else if (file.isFile()) {
            zipWriter.addEntryFile(entry, zipLevel, file, true);
        }

processQueue の第1引数には nsIRequestObserver オブジェクトを渡す。
nsIRequestObserver は最初のファイル圧縮前に呼び出される onStartRequest と、最後のファイル圧縮後に呼び出される onStopRequest メソッドを持つ。

    zipWriter.processQueue({
        onStartRequest: function(aReuqest, aContext) {
        },
        onStopRequest: function(aRequest, aContext) {
            zipWriter.close();
        },
    }, null);

TOP

nsITransactionManager を使ったトランザクション管理

拡張機能やXULアプリにて、ユーザの操作に対する「元に戻す」「やり直し」機能を実装する際、 nsITransactionManager が便利です。例えば Firefox 本体ではブックマークの追加/削除/移動などが nsITransactionManager によってトランザクション管理されています。

基本形

ユーザがボタンをクリックすると、金額が加算されて合計金額がテキストボックスに表示されるような、簡単なXULアプリを作ってみます。

bank.xul
<?xml version="1.0"?>

<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>

<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
      title="Bank" onload="init();">

    <script type="application/x-javascript" src="bank.js" />

    <textbox id="total" />
    <hbox>
        <button label="Deposit $1"   oncommand="deposit(1);" />
        <button label="Deposit $10"  oncommand="deposit(10);" />
        <button label="Deposit $100" oncommand="deposit(100);" />
    </hbox>

</page>

引き続き JavaScript で機能を実装します。合計金額を gTotalMoney というグローバル変数に保持し、合計金額をテキストボックスに表示するための updateUI 関数を作っておきます。 onload イベント発生時に呼び出される init 関数では、 updateUI で合計金額の初期値「$0」を表示します。
ボタンをクリックしたときに呼び出される deposit 関数では、引数に渡された金額を gTotalMoney に加算した後、 updateUI で合計金額の表示を更新します。

bank.js
var gTotalMoney = 0;

function init() {
    updateUI();
}

function updateUI() {
    document.getElementById("total").value = "$" + gTotalMoney.toString();
}

function deposit(aMoney) {
    gTotalMoney += aMoney;
    updateUI();
}

トランザクション管理

では、上記のアプリでボタンをクリックした後、その処理を元に戻す/やり直しできるようにします。

はじめに、一連のトランザクションを管理するための nsITransactionManager インスタンスであるグローバル変数 gTxnManager およびその初期化処理を追加します。

bank.js
var gTotalMoney = 0;
var gTxnManager;

function init() {
    gTxnManager = Components.classes["@mozilla.org/transactionmanager;1"].
                  createInstance(Components.interfaces.nsITransactionManager);
    updateUI();
}

トランザクション管理をして元に戻す/やり直し可能にするためには、個々の処理を nsITransaction インタフェースを実装したオブジェクトとして記述する必要があります。今回の場合は預金処理を表す DepositTxn クラスを以下のような設計で作ります。

メンバ 概要
_money コンストラクタの引数に渡された金額を内部的に保持するためのプロパティ。
doTransaction このトランザクションを実行する際に呼び出されるメソッド。
合計金額へ _money 分だけ加算する。
undoTransaction このトランザクションを元に戻す際に呼び出されるメソッド。
合計金額から _money 分だけ減算する。
redoTransaction このトランザクションをやり直しする際に呼び出されるメソッド。
通常は doTransaction と同じ処理を実行すればよい。
merge
isTransient
詳細不明。
function DepositTxn(aMoney) {
    this._money = aMoney;
}

DepositTxn.prototype = {
    doTransaction  : function() { gTotalMoney += this._money; },
    undoTransaction: function() { gTotalMoney -= this._money; },
    redoTransaction: function() this.doTransaction(),
    merge: function() false,
    get isTransient() false,
};

deposit 関数を書き換えて、合計金額を直接変更する代わりに、預金処理トランザクションクラスのインスタンスを生成して gTxnManagerdoTransaction メソッドへ渡します。こうすることで、内部的に DepositTxn インスタンスの doTransaction が呼び出されて合計金額への加算が行われ、なおかつその処理が元に戻す処理のスタックへ追加され、必要に応じて元に戻すことが可能となります。

function deposit(aMoney) {
    var txn = new DepositTxn(aMoney);
    gTxnManager.doTransaction(txn);
    updateUI();
}

次に、アプリのUIへ元に戻す/やり直しするためのボタンを追加します。

bank.xul
    <hbox>
        <button id="undoButton" label="Undo" oncommand="undo();" />
        <button id="redoButton" label="Redo" oncommand="redo();" />
    </hbox>

それぞれのボタン押下時の処理として、 nsITransactionManager の undoTransaction および redoTransaction メソッドを呼び出します。すると、元に戻す/やり直し処理のスタックの最後尾にあるトランザクション(nsITransaction インスタンス)の undoTransaction および redoTransaction メソッドが実行されます。その後、合計金額の表示を更新します。

bank.js
function undo() {
    gTxnManager.undoTransaction();
    updateUI();
}

function redo() {
    gTxnManager.redoTransaction();
    updateUI();
}

バッチ処理

例えば複数のブックマークを選択して削除した後に元に戻すと、複数のブックマークが一括して復元されます。このように、一連のトランザクションをバッチ処理として実行する場合の元に戻す/やり直しを実装します。

はじめに、アプリのUIへ複数の金額を一括して預金するためのボタンを追加します。

bank.xul
        <button label="Deposit $1+$10+$100" oncommand="depositSet([1,10,100]);" />

depositSet 関数は引数に渡された金額の配列すべてについて合計金額へ加算します。このとき、 nsITransactionManager の beginBatch メソッドを呼んでから doTransaction で個々の金額についての預金処理を実行し、最後に endBatch を呼ぶようにします。これにより、一連の預金処理がグループ化され、元に戻す際に一括して元に戻す処理が実行されます。

bank.js
function depositSet(aMoneys) {
    gTxnManager.beginBatch();
    aMoneys.forEach(function(money) {
        var txn = new DepositTxn(money);
        gTxnManager.doTransaction(txn);
    });
    gTxnManager.endBatch();
    updateUI();
}

元に戻す/やり直しスタックのリセット

nsITransactionManager の clear メソッドによって、元に戻す/やり直しスタックをいったんリセットすることができます。このメソッドを使って預金処理を確定させるボタンを作ってみます。

bank.xul
        <button id="completeButton" label="Complete" oncommand="complete();" />
bank.js
function complete() {
    gTxnManager.clear();
}

元に戻す/やり直しスタック内の個数

nsITransactionManager の numberOfUndoItems, numberOfRedoItems プロパティで元に戻す/やり直しスタック内の個数を調べることができます。これを使って元に戻す/やり直し可能なときだけボタンを押下可能にするようにしてみます。

bank.js
function updateUI() {
    document.getElementById("total").value = "$" + gTotalMoney.toString();
    var canUndo = gTxnManager.numberOfUndoItems > 0;
    var canRedo = gTxnManager.numberOfRedoItems > 0;
    function setDisabled(id, disabled) {
        var elt = document.getElementById(id);
        if (disabled)
            elt.setAttribute("disabled", "true");
        else
            elt.removeAttribute("disabled");
    }
    setDisabled("undoButton", !canUndo);
    setDisabled("redoButton", !canRedo);
    setDisabled("completeButton", !canUndo && !canRedo);
}

トランザクションの監視

nsITransactionManager の AddListener に nsITransactionListener インタフェースを実装したオブジェクトを渡して、トランザクションを監視することができます。 RemoveListener で監視を終了するのを忘れずに。

bank.xul
<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
      title="Bank" onload="init();" onunload="uninit();">
bank.js
function init() {
    gTxnManager = Components.classes["@mozilla.org/transactionmanager;1"].
                  createInstance(Components.interfaces.nsITransactionManager);
    gTxnManager.AddListener(gTxnListener);
    updateUI();
}

function uninit() {
    gTxnManager.RemoveListener(gTxnListener);
}

nsITransactionListener は多くのメソッドを持ち、元に戻す/やり直し処理の直前/直後などにコールバック処理を設定することができます。今回は元に戻す処理の直前に確認のダイアログを表示し、「キャンセル」ボタンを押下することで中止できるようにします。以下のように willUndo メソッドで true を返すと、処理を中止することができます。

var gTxnListener = {
    willDo: function() {},
    didDo: function() {},
    willUndo: function(aMgr, aTxn) {
        if (!window.confirm("Would you like to undo?"))
            return true;
    },
    didUndo: function() {},
    willRedo: function() {},
    didRedo: function() {},
    willBeginBatch: function() {},
    didBeginBatch: function() {},
    willEndBatch: function() {},
    didEndBatch: function() {},
    willMerge: function() {},
    didMerge: function() {},
};

TOP

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

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 に投稿した記事と同内容です。

注意: この記事の内容は 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

Webページの拡大率を取得する

拡張機能などから、ブラウザのコンテンツエリアに表示された Web ページの拡大率(画像も含めたズームイン・ズームアウトによる拡大率)を調べる。
以下、 browser.xul へオーバーレイした状態を想定。

Firefox 3.5 以前

nsIMarkupDocumentViewer::fullZoom で取得可能。

var zoomFactor = gBrowser.mCurrentBrowser.markupDocumentViewer.fullZoom;

Firefox 3.6 以降

nsIDOMWindowUtils::screenPixelsPerCSSPixel でも取得可能。

var win = window.content;
var winUtils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
var zoomFactor = winUtils.screenPixelsPerCSSPixel;

両者の値の違い

Firefox 3.6 以降の screenPixelsPerCSSPixel で取得できる値は、厳密にはズームイン・ズームアウト機能の拡大率ではなく、 CSS での1ピクセルがスクリーン上で何ピクセルとして表示されるかを示した値である。そして、両者の値はなぜか微妙に異なる。 Firefox 3.5 で Firefox 3.6 とほぼ同等の値を取得するにはどうすれば良いか? All-in-One Gestures が面白いやり方で実践していた。

var doc = window.content.document;
var div = doc.createElementNS("http://www.w3.org/1999/xhtml", "DIV");
div.style.position = "absolute";
div.style.top = "100000px";
doc.body.appendChild(div);
var zoomFactor = (doc.getBoxObjectFor(div).screenY - doc.getBoxObjectFor(doc.body).screenY) / div.offsetTop;
doc.body.removeChild(div);

ダミーの DIV 要素を、 Web ページの document.body を基点とした上から100000px(CSS上のピクセル)の絶対位置に配置し、 body と DIV 要素のスクリーン上のY座標の値を getBoxObjectFor で取得し、両者の差を100000で割ることで算出している。 getBoxObjectFor を使用しているので、もちろん Firefox 3.5 以下限定である。

関連記事

SCRAPBLOG : Firefox 3 のフルページズーム使用時はスクリーン上でのピクセル量とCSS上でのピクセル量は一致しない

TOP

Firefox を再起動する

Firefox 3 以前

Firefox を再起動するとき、 Firefox 3 までは以下のようなコードを書く必要があった。

var appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup);
appStartup.quit(appStartup.eRestart | appStartup.eAttemptQuit);

ただし、上記コードでは Firefox 終了直前に「開いているタブを保存して次回起動時に復元しますか?」の確認ダイアログを表示する設定になっていた場合などに、ダイアログを表示せずに強制的に終了してしまう欠点がある。そのような場合でもちゃんとダイアログを表示させるようにするためには、 chrome://mozapps/content/extensions/extensions.js の restartApp 関数のように少々長いコードを書く必要があった。

Firefox 3.5 以降

FUELextIApplication インタフェースに追加された restart メソッドで実現可能となった。

Application.restart();

Application 定数が未定義の JS XPCOM 内では、以下のようにする。

var fuelApp = Cc["@mozilla.org/fuel/application;1"].getService(Ci.fuelIApplication);
fuelApp.restart();

また、 restart メソッドの戻り値で、実際に再起動の処理を開始するか、あるいはユーザによってキャンセルされたかを判別可能。

var ret = Application.restart();
alert(ret ? "再起動します。" : "再起動はユーザによってキャンセルされました。");

なお、 Firefox を終了する処理についても、 FUEL で実現可能となった。

Application.quit();

TOP

openDialog の引数で指定したウィンドウ名を nsIWindowMediator で使用する

ルート要素に windowtype 属性がセットされていないウィンドウを、以下のように “test_window” という名前付きで開く。

window.openDialog("chrome://...", "test_window", "chrome,all");

引き続き nsIWindowMediator を使ってそのウィンドウの名前からウィンドウを取得しようとしても、できない。

var winMediator = Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);
var win = winMediator.getMostRecentWindow("test_window");
// |win| is null

そこで、以下のようにウィンドウを開いた直後に window.name から取得したウィンドウ名を windowtype 属性へセットしてやる必要がある。

var win = window.openDialog("chrome://...", "test_window", "chrome,all");
win.document.documentElement.setAttribute("windowtype", win.name);

ただしモーダルダイアログを開く場合、ウィンドウを開く側ではなく、開かれたウィンドウ側で上記処理を行う必要がある。

TOP

nsIConsoleService

// サービス取得
var consoleSvc = Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService);

// エラーコンソールへの出力内容を消去
consoleSvc.reset();

// #1 Components.utils.reportErrorによるのエラーメッセージ出力
Components.utils.reportError("error #1");

// #2 nsIConsoleService.logStringMessageによるエラーメッセージ出力
consoleSvc.logStringMessage("error #2");

// #3 nsIConsoleService.logMessageによるエラーメッセージ出力
var err = Cc["@mozilla.org/scripterror;1"].createInstance(Ci.nsIScriptError);
err.init("error #3", null, null, null, null, err.errorFlag, null);
consoleSvc.logMessage(err);

// エラーコンソールへの出力内容を列挙
var msgs = {};
consoleSvc.getMessageArray(msgs, {});
msgs.value.forEach(function(msg) {
    dump("---
");
    if (msg instanceof Ci.nsIScriptError) {
        // [JavaScript Error]
        msg.QueryInterface(Ci.nsIScriptError);
        dump(
            msg + "
" + 
            "errorMessage => " + msg.errorMessage + "
" + 
            "sourceName   => " + msg.sourceName   + "
" + 
            "sourceLine   => " + msg.sourceLine   + "
" + 
            "lineNumber   => " + msg.lineNumber   + "
" + 
            "columnNumber => " + msg.columnNumber + "
" + 
            "flags        => " + msg.flags        + "
" + 
            "category     => " + msg.category     + "
"
        );
    }
    else {
        // [xpconnect wrapped nsIConsoleMessage]
        dump(
            msg + "
" + 
            "message     => " + msg.message + "
"
        );
    }
});

リファレンス

nsIConsoleService – MDC
nsIConsoleService.idl
nsIConsoleMessage.idl
nsIScriptError.idl

TOP