» アドオン開発

about:config を扱う拡張機能についての質問

はじめまして、こんにちは。
takataと言います。

自作でThunderbirdのアドオンを作ろうと考えています。
内容は、独自で作ったlocalパッケージを

Thunderbird >環境設定>設定エディタ

で書き換えて適応するのではなく、

1)ツールバーに表示された拡張機能のアイコンをクリック
2)ラジオボタン表示
3)言語選択→「OK」クリック
4)適応(再起動)

とう具合に行いたいと思っています。
ツールバーにアイコン表示、ラジオボタンののったダイアログ表示ということは
自分でできると思います。

分からない点は
1)about:configを扱うアドオンを作るにはどこをオーバレイすればいいか
2)言語選択後、自動でThunderbirdを再起動させる方法
です。

DOM Inspector を使って調べてみたのですが、
chrome://global/content/config.xul がページであること?
各項目がまとめてNodename が treechildren、id がconfigTreeBody であること
がわかりましたが、general.useragent.local 単独で扱う方法がわかりませんでした。

環境は
Mac OS X バージョン 10.5.8
Thunderbird バージョン 2.0.0.23

手元にある資料は
FIREFOX 3 HACKS (オライリージャパン発行)
Firefox拡張機能開発チュートリアル
Firefox 拡張機能開発 準備編
Firefox 拡張機能開発 入門編
です。

始めたばかりで、基本中の基本を聞いていたり、
的外れなことを言っているかもしれませんが、
どうかその場合でもアドバイスをいただけないでしょうか。
また、投稿場所がおかしかったりしてもご指摘お願いしたいです。

どうかよろしくお願いします。

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

注意: この記事の内容は 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\n";
}

リファレンス

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

拡張機能にて、ある 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 関数を使うケースの方が多いかもしれませんね。

関連ドキュメント

機能を他のアドオンからキャンセルできるようにする

このエントリは、一つ前のカスタムイベントの通知方法の紹介を踏まえた内容になっています。先に前のエントリを読んでからご覧になる事をお勧めします。

さて、前のエントリでは closeTabs() 関数が呼ばれた時にそれを通知する方法を解説しました。しかし前のエントリに書いた方法だけだと、以下のような制限があります。

  • イベントを通知する側から検知する側へ、一方通行でしか情報を送れない。

例えば、何か時間のかかる処理をするアドオンを開発していたとしましょう。

gBrowser.addEventListener(
  'ClosingTabs',
  function (aEvent) {
    // お、複数のタブが閉じられようとしているぞ!
    if (aEvent.tabs.some(function(aTab) {
          return aTab.hasAttribute('wait-for-a-while');
        })) {
      // 処理中のタブがある!
      // これは今閉じられると困るなあ。
      // でも、「やめて!」と伝える方法がない><
    }
  },
  false
);

このように、処理の中止を訴えたくてもイベントを発行した側である closeTabs() に対して値を返したりメッセージを送ったりできないので、指をくわえて見ていることしかできません。

こういうニーズに備えて、事前にキャンセルができるようなタイミングでカスタムイベントを通知する時は、キャンセルできるイベントとして通知するようにするとよいでしょう。

// イベント通知側
function closeTabs(aTabs) {
  var event = document.createEvent('Events');
  // 好きなイベント名を付ける。
  // この時、第3引数が true だと、キャンセルできるイベントになる。
  event.initEvent('ClosingTabs', true, true);
  event.tabs = aTabs;
  gBrowser.dispatchEvent(event);
  // イベントがキャンセルされていたら、処理を中断する。
  if (event.getPreventDefault())
    return;

  ...
}

// イベント検知側
gBrowser.addEventListener(
  'ClosingTabs',
  function (aEvent) {
    // お、複数のタブが閉じられようとしているぞ!
    if (aEvent.tabs.some(function(aTab) {
          return aTab.hasAttribute('wait-for-a-while');
        })) {
      // 処理中のタブがあるからやめて!
      aEvent.preventDefault();
    }
  },
  false
);

イベントを検知する側でイベントオブジェクトの preventDefault() メソッドを呼ぶと、getPreventDefault() の戻り値が false から true に変わります。なので、これを見ればイベント通知側からも、イベントがキャンセルされたかどうかを判別できるというわけです。

ただし、Firefox 3.6 以前では UIEvents として生成・初期化したイベントでなければ getPreventDefault() が使えないという問題があります。Firefox 3.5 以前でも正常に動作するようにするには、以下のようなハックを行う必要があります。

function closeTabs(aTabs) {
  var event = document.createEvent('Events');
  event.initEvent('ClosingTabs', true, true);
  event.tabs = aTabs;
  gBrowser.dispatchEvent(event);
  ensureEventCancelable(event); // ←ここで問題を解消
  if (event.getPreventDefault())
    return;

  ...
}

function ensureEventCancelable(aEvent) {
  // Gecko 1.9.2 ( Firefox 3.6 )以降なら何もしない
  if (aEvent.getPreventDefault) return;

  // 元の関数を保存
  aEvent.__original__preventDefault = aEvent.preventDefault;
  aEvent.__canceled = false;
  aEvent.preventDefault = function() {
    this.__original__preventDefault();
    // キャンセルされたかどうかのフラグを独自に立てる
    this.__canceled = true;
  };
  aEvent.getPreventDefault = function() {
    return this.__canceled;
  };
}

……というこのハックを見るとバレバレなのですが、実はイベントオブジェクトのプロパティを介しても情報のやりとりはできます。ただ、preventDefault() はリンクのクリックによるページ遷移をキャンセルするといった場合でも使われる、W3C の DOM Level2 Events の仕様にも含まれる広く知られたメソッドなので、それと同じ方法でイベントをキャンセルできるようにしておいた方が、イベントを検知するアドオンを作る人にとっては分かりやすいでしょう。

nsIObserverService など他の方法を使うと、クロスウィンドウでイベントを通知するなどのより高度なこともできます。とりあえず、まずはウィンドウ内で完結するイベントの仕組みから始めてみて、限界を感じたら別の方法を検討してみてください。

機能が呼び出されたことを他のアドオンに通知する

あなたはアドオンを作る時、他のアドオンと連携しやすいようにするということを意識しているでしょうか?

例えば、複数のタブをまとめて一気に閉じる機能を設けるために、タブの配列を受け取ってそれらをすべて閉じる closeTabs() という関数を定義するとしましょう。

function closeTabs(aTabs) {
  gBrowser.dispatchEvent(event);
  aTabs.forEach(function(aTab) {
    gBrowser.removeTab(aTab);
  });
}

この機能を提供するアドオンの作者であるあなたにとっては、これ以上特に何も気にする事はありませんよね。

では、他のアドオン作者の人が、あなたのアドオンと連係して動作するアドオンを開発する場面を考えてみましょう。この関数が呼ばれたという事を他のアドオンから検知するにはどうすればよいでしょうか?

Firefox のユーザの多くは、複数のアドオンを組み合わせて使っています。アドオンを公開していると、「あなたのこのアドオンはとても素晴らしい! ところで自分は○○というアドオンを使ってるんだけど、あなたのアドオンの機能からもこの○○の機能を利用できるようにならないだろうか?」という風な要望が寄せられることもあります。私はそういう要望を受け取って初めてそのアドオンの存在を知ることが多いのですが、それでは連携してみようと思ってそのアドオンのソースコードを覗いてみて、途方に暮れてしまうことが少なくありません。そういう場合で一番多いのが、「このアドオンのこの機能が呼び出される直前に、何らかの処理を割り込ませたい。しかし、その方法がない。」というケースです。

もちろん、方法が全くないわけではない場合がほとんどです。例えばたいていの場合、以下のようにすれば任意の処理を割り込ませることができます。

eval('window.closeTabs = '+
     window.closeTabs.toSource().replace(
       '{',
       '{ AnotherAddon.onTabsClosed(); '
     )
);

しかし、このように eval() を使って関数を書き換えたり、あるいは関数全体を置き換えたりする方法は、同じ事をやろうとするアドオンが沢山存在すると破綻してしまいます。また、書き換え(置き換え)対象の関数の定義が変わった時にも、期待通りに動かなくなる可能性があります。

実際に Firefox 1.5 以前の古いバージョンの Firefox では、tabbrowser 要素の addTab()removeTab() といったメソッドを置き換えないと、「新しいタブが開かれた」「タブが閉じられた」「タブが移動された」といった場面で任意の処理を行う事はできませんでした(※厳密に言うと、間接的なやり方で実現できない事もなかったのですが……)。そのため、タブブラウズ機能に関係するアドオンを複数インストールするとそれらが衝突してまともに動かないということもざらにありました。

他のアドオン作者をこういう風に困らせてしまわないように、他のアドオンと連携しやすいアドオンを作る方法はないのでしょうか? それがこのエントリのテーマです。

複数のアドオン同士を連携しやすくする方法はいくつかありますが、その 1 つとして、DOM Level2 Events の仕組みを使う方法があります。例えば次のようにすると、前出の closeTabs() が呼ばれた時に「複数のタブを閉じようとしていますよ!」というイベントを通知することができます。

function closeTabs(aTabs) {
  // 新しいイベントオブジェクトを作る
  var event = document.createEvent('Events');
  // 好きなイベント名を付ける。
  // 後の引数はとりあえず true, false と書いておけばいい。
  event.initEvent('ClosingTabs', true, false);
  // イベントオブジェクトに任意のプロパティを持たせて
  // いろんな情報を送れる。
  event.tabs = aTabs; // 閉じられようとしているタブ
  // 任意の DOM 要素ノードの dispatchEvent() メソッドで、
  // イベントの通知を開始!
  gBrowser.dispatchEvent(event);

  ...
}

このようにカスタムイベントを通知するようにしておけば、他のアドオンの作者は以下のようにして closeTabs() が呼ばれたことを簡単に検知できます。

gBrowser.addEventListener(
  'ClosingTabs',
  function (aEvent) {
    // お、複数のタブが閉じられようとしているぞ!
    var tabs = aEvent.tabs;
    var labels = tabs.map(function(aTab) { return aTab.label; });
    alert(labels.join('\n')+
          '\n-----'+
          '\n以上の'+tabs.length+'個のタブがこれから閉じられます');
  },
  false
);

Firefox 2 以降でも、これと同じ方法で、タブが開かれたり閉じられたりといったイベントを検知できます。つまり、Firefox 本体と同様の処理をあなたのアドオンにも持たせるということですね。

「他のアドオンと連携する事なんて興味ないよ」と思うかもしれませんが、このテクニックは他のアドオンとの連携以外でも役に立ちます。

例えば closeTabs() を使う機能にさらに色々便利な機能を増やしたくなった時、closeTabs() の中にその都度コードを書き加えていっていると、そのうち関数がどんどん長くなってメンテナンスが大変になってきます。何百行、何千行という長い関数の中でうまく動かない部分がでてくると、原因箇所の特定も、バグの修正も困難です。

でも上記のようにカスタムイベントを定義してあれば、機能を増やしたくなった時は、そのイベントを監視するイベントリスナを新しく定義するだけで済みます。機能ごとに細かく関数を分ければメンテナンス性も高まります。

カスタムイベントは、他のアドオンとの連携にもあなた自身がアドオンをメンテナンスする手間の削減にも役立つ一石二鳥なテクニックです。ぜひ活用してみて下さい。

クリックされたタブやボタンを確実に取得する方法

タブの上での特殊な操作をトリガーとして発動する機能を作りたい時は、イベントが発生した要素を取得する方法にも気を配りましょう。

例えば「ダブルクリックされたタブを閉じる」機能を実現する場合、単純に考えるとこのようになるでしょう。

gBrowser.mTabContainer.addEventListener(
  'dblclick',
  function(aEvent) {
    var tab = aEvent.originalTarget;
    if (tab && tab.localName == "tab")
      gBrowser.removeTab(tab);
  },
  false
);

しかし、これは実際には期待通りに動きません。例えばタブの中の label 要素や image 要素でイベントが発生した場合、aEvent.originalTarget は tab 要素ではなくなってしまうので、この条件ではマッチしないことになります。なのでもう少し工夫が必要です。

gBrowser.mTabContainer.addEventListener(
  'dblclick',
  function(aEvent) {
    var target = aEvent.originalTarget;
    var parent = target.parentNode;
    if (target && target.localName == "tab")
      gBrowser.removeTab(target);
    else if (parent && parent.localName == "tab")
      gBrowser.removeTab(parent);
  },
  false
);

素の Firefox での利用のみを前提とするなら、これでも十分かもしれません。

しかし実際には、ユーザはそのアドオンを他のアドオンと組み合わせて使うかもしれません。タブの中の label 要素にさらに子要素を追加するアドオンや、tab 要素のバインディングを変更するアドオンと組み合わせた時、このコードでは期待通りに動かなくなります。また、-moz-border-image がサポートされていなかった頃からあるテーマの中にも、バインディングを使ってタブの外観を変えている物はたくさんあります。イベントが発生したノードとその直上の親ノードだけを調べてタブかどうかを判別するというやり方は、この手の変化に対し非常に脆弱です。(直上の親に限らず、「N 階層上の祖先」や「N 番目の子ノード」という調べ方でハードコーディングしてしまうやり方自体がそもそも変化に弱いということです。)

このような場面では、DOM3 XPathSelectors API のように抽象的な指定でノードを取得する仕組みを使うことをお勧めします。

例えば上記の例では、特定のノードを起点として祖先方向に検索を行える DOM3 XPath を使うと問題を解決できます。

gBrowser.mTabContainer.addEventListener(
  'dblclick',
  function(aEvent) {
    var tab = document.evaluate(
                'ancestor-or-self::*[local-name()="tab"][1]',
                aEvent.originalTarget,
                null,
                XPathResult.FIRST_ORDERED_NODE_TYPE,
                null
              ).singleNodeValue;
    if (tab) gBrowser.removeTab(tab);
  },
  false
);

DOM3 XPath では、XPath 式で検索対象のノードを指定します。ここでは ancestor-or-self という指定で「起点ノードもしくはその祖先」の要素ノードを検索して、その中でローカル名が「tab」である1番目の要素(※ XPath 式では N 番目の項目を指定する時、添字は 0 ではなく 1 から始まります)を取得しています。

アドオンを開発する時、特にそれがタブやツールバーなど他のアドオンによって色々と変更される可能性が高い箇所に対して機能を追加するのであれば、Firefox の素の状態からある程度の変化が加わっている可能性を見越して、柔軟に対応できるようにしておくとよいでしょう。また、自分のアドオンが他のアドオンに対して悪影響を及ぼさないよう、加える変化はなるべく少なくする事も大切です。どちらにしても、Firefox のセールスポイントが豊富なアドオンである以上、アドオンの作者は、自分のアドオンが他のアドオンと組み合わせて使われることを前提にして、なるべく衝突の可能性が低くなるように、また、衝突しても容易に対応できるような設計にしておくように気を遣う必要があります(←自戒を込めて)。

画面の描画内容を一時的にロックする方法

Firefox のウィンドウ内の描画を一旦停止して、処理を行った後で改めて再描画させる、という事をしたいと思った事はないでしょうか。Firefox 3(Gecko 1.9)以降であれば、これは以下のコードで実現できます。

var baseWindow = window.top
          .QueryInterface(Ci.nsIInterfaceRequestor)
          .getInterface(Ci.nsIWebNavigation)
          .QueryInterface(Ci.nsIDocShell)
          .QueryInterface(Ci.nsIBaseWindow);
baseWindow.setPosition(window.innerWidth, window.innerHeight); // これで画面の描画が止まる

gBrowser.addTab(); // これによって起こる変化は画面上に現れない
gBrowser.addTab(); // この変化も画面上に現れない
gBrowser.addTab(); // 同上

baseWindow.setPosition(0, 0); // ここでやっと描画が再開される

具体的な利用例を1つ挙げてみます。

CSS のプロパティを動的に変更して UI 要素の表示を変える事はよくあると思いますが、その際、複数の UI 要素の表示を調整するために順番に処理を行っていると、最初の状態から最終的な状態に至るまでの間の中間状態が描画されてしまって、画面がチラつく事があります。

例えば、普段はタブバーを非表示にしておき、ポインタが近づいた時だけタブバーを表示する、という機能は以下のように実装できます。

var TabbarAutoHide = {
  hide : function() {
    gBrowser.mStrip.collapsed = true;
    // show()でずらした表示位置を元に戻す。
    let style = gBrowser.mPanelContainer.style;
    style.setProperty('position', '', '');
    style.setProperty('margin-bottom', '', '');
    style.setProperty('height', '', '');
    // 一旦画面外まで大きくずらしているのは、その範囲を強制的に再描画させるため。
    let viewer = gBrowser.markupDocumentViewer;
    viewer.move(window.outerWidth, window.outerHeight);
    viewer.move(0, 0);
  },
  show : function() {
    gBrowser.mStrip.collapsed = false;
    let tabHeight = gBrowser.mStrip.boxObject.height
    let panelHeight = gBrowser.mPanelContainer.boxObject.height + tabHeight;
    // コンテンツ表示領域をずらす分だけ下に空白ができてしまうので、それをごまかす
    let style = gBrowser.mPanelContainer.style;
    style.setProperty('position', 'relative', 'important');
    style.setProperty('margin-bottom', -tabHeight+'px', 'important');
    style.setProperty('height', panelHeight+'px', 'important');
    // コンテンツ表示領域の表示位置をタブバーの分だけ上にずらす
    let viewer = gBrowser.markupDocumentViewer;
    viewer.move(window.outerWidth, window.outerHeight);
    viewer.move(0, -tabHeight);
  }
};

しかしこのままだと、タブバーの表示・非表示が切り替わる瞬間にコンテンツ表示領域がガタガタと揺れるように見えてしまってとても不格好です。

こういう時に、冒頭で紹介したテクニックが役に立ちます。

var TabbarAutoHide = {
  get baseWindow() {
    return window.top
             .QueryInterface(Ci.nsIInterfaceRequestor)
             .getInterface(Ci.nsIWebNavigation)
             .QueryInterface(Ci.nsIDocShell)
             .QueryInterface(Ci.nsIBaseWindow);
  },
  hide : function() {
    this.baseWindow.setPosition(window.innerWidth, window.innerHeight);
    gBrowser.mStrip.collapsed = true;
    ...
    viewer.move(0, 0);
    this.baseWindow.setPosition(0, 0);
  },
  show : function() {
    this.baseWindow.setPosition(window.innerWidth, window.innerHeight);
    gBrowser.mStrip.collapsed = false;
    ...
    viewer.move(0, -tabHeight);
    this.baseWindow.setPosition(0, 0);
  }
};

このように、複数回の再描画が行われそうな部分の前後で描画の一時停止と再開を行えば、ユーザにとっては煩わしいだけの画面上のチラつきを減らすことができます。

ただ、複数の関数(メソッド)でそれぞれ独自に描画の停止/再開を行っていると、停止→停止→再開→再開 のような順で処理が行われてしまうため、せっかく見えないようにしたはずの処理中の画面が見えてしまうことがあります。このようなトラブルを防ぐには、呼び出しの深さをカウントするなどの方法で、実際に描画を停止/再開させるケースを判別する必要があります。この点だけに絞って最小構成でライブラリ化した stopRendering.js という物を作りましたので、ぜひ使ってみて下さい。window['piro.sakura.ne.jp'].stopRendering.stop() で描画が停止され、window['piro.sakura.ne.jp'].stopRendering.start() で再開されます。

なお、この方法の発見に至った経緯も公開していますので、アドオン開発がどういう風に行われているのかの一端を覗いてみたい方は見てみるといいかもしれません。(これはかなり特殊なケースですが……)

※このエントリでは当初 nsIContentViewer の hide() メソッドと show() メソッドを使った方法を紹介していましたが、この方法には弊害があることが判明したため、nsIBaseWindow の setPosition() メソッドを使った方法に訂正しました。訂正前のエントリをご覧になった方はご注意下さい。訂正前の方法の弊害は自サイトの新しいエントリに詳しく記載しています。

Mozilla勉強会(#modest)に参加しました

Mozilla 勉強会 « Mozilla Developer Street (modest)

Firefox3 Hacksにサインをいただきました

http://www.geocities.jp/teruakigemma/modest/firefox3.png

Firefox 3 Hacks —Mozillaテクノロジ徹底活用テクニック

Firefox 3 Hacks —Mozillaテクノロジ徹底活用テクニック

ノベルティをいただきました

  • クリアーファイル! レア物とのこと。

http://www.geocities.jp/teruakigemma/modest/novelty0.png

  • ノートパソコン用バッグ! Firefoxのアイコン付き! これはすごい

http://www.geocities.jp/teruakigemma/modest/novelty1.png

  • ステッカー、ボールペン、携帯ストラップ、ネックストラップなどなど!

ありがとうありがとう!

JetpackやMozillaの現状など、とても勉強になりました。次回もぜひお願いします。

Mozilla勉強会で発表してきました

変態Vimperator使いのteramakoです。こんにちは

Mozilla 勉強会 « Mozilla Developer Street (modest)にて発表してきました。

内容としては

かなり異色の部類になるかと思われる開発者向けのfeatureの紹介となってます。

jetpack feature installerに関してはローカルのJetpack FeatureファイルをインストールするFeature – hogehogeで紹介していたこともあり、id:con_mameさんに先に紹介されたり、懇親会で「使っていますよ」と言ってくれる人がいたりと嬉しい誤算がありつつ、なかなか楽しい勉強会でした。

Jetpackに関しては皆いろいろ思うところがあるようで、それなりに議論できたのかなと思います。

私自身それなり思うところがあるわけですが、Jetpackは今後、GoogleChromeのChromeExtensionと比較され続けるかと思います。ChromeExtensinと比較して優位性を示せるかが課題ではないでしょうか。本心としてはTwitter / teramakoで書いた通りなのですが・・・。

開発者を募集しているようです

話はちょっとそれますが、JetpackのMLでWe’re looking for a Jetpack API Developer! – mozilla-labs-jetpack | Google グループなんてのが流れてます。ちょっと条件が厳しいですが・・・、自信のある方は応募してみると現状を打開できるかも!? です。

さらに余談

そうそう、懇親会で私がLightweight Language Television (LLTV)にて発表したものが話題(XULとcanvasを使って画面いっぱいにSLを走らせるというもの)にあがり、デモったのですが、その時の資料はSL command – LLTV にあります。資料はHTML+JavaScriptで出来ていますので、該当ソースを抜きとれば貴方のページでSLを走らせることができますよ :) デモで使ったコードのソースは/lang/javascript/vimperator-plugins/trunk/sl.js – CodeRepos::Share – Tracです(注:Vimperatorのプラグインとして動作するように作られているので通常では動きません)。

自分のサイトで配布しているアドオンを AMO に登録する

Mozilla Add-ons Blog で伝えられているとおり、 Add-ons for Firefox のサイト(以下AMO)に、自分のサイトで配布しているアドオンを登録することができるようになりました。

どういう意味かといいますと、 通常 AMO にアドオンを登録するには、アドオンのファイルをアップロードして承認を得てようやく公開という手順を踏む必要がありますが、今後は新たに、アドオンの配布は自分のサイトで行いつつも紹介ページのみを AMO に掲載するという運用が可能となったということです。

新しい方式で登録したアドオンは、通常のアドオンと同様に紹介ページにスクリーンショットを掲載したり、レビューやタグ付け、コレクションへの追加などもできます。逆に、インストーラのファイルを AMO 上に置いたり、バージョン情報を更新したりすることはできません。また、アドオンマネージャから検索することもできません。

さっそく私も FoxAge2ch という拡張機能を新しい方式で AMO に登録してみました。

foxage2ch-on-amo

FoxAge2ch は2ちゃんねるユーザ向けの拡張機能=ほぼ日本人限定ですので、 AMO での配布はふさわしくないと思い、自分のサイトで配布していますが、今後は AMO から探し出すことも可能となります。

ただし、なぜか AMO の検索バーに「foxage2ch」と入力しても「該当するアドオンはありません。」と表示されてしまいました。なぜか「検索オプション」でバージョンとプラットフォームを「すべて」にして検索したら、無事にヒットしました。