» アドオン開発

XMLHttpRequest サンプルコード

XMLHttpRequest を処理する簡易 Wrapper サンプルコード。

var myextension = {
	// get html contents and do something
	httpGet: function(url, callback) {
		var req = new XMLHttpRequest();
		req.onreadystatechange = this.bindFunction(this, function(e) {
			if (req.readyState == 4) {
				if (req.status == 200) {
					if (typeof callback == "function") {
						callback(req.responseText, req.status, req);
					}
				}
				else {
					var message = "HTTP Request Failed! - url: " + url;
					if (this.debug && typeof this.debug.warn == "function") {
						this.debug.warn(message);
					}
					else if (Firebug && Firebug.Console && typeof Firebug.Console.log == "function") {
						Firebug.Console.log(message);
					}
					else {
						window.alert(message);
					}
				}
			}
		});
		req.open("GET", url, true);
		req.send(null);
		return req;
	},
	// workaround until we can use ECMA 5th bind method
	// we can this function to specify 'this' for callback functions
	bindFunction: function(thisobj, func) {
		return function() func.apply(thisobj, arguments);
	}
}
myextension.httpGet("", function(data, status, req) {
	alert(data);
});

このコードは 拡張機能デバッグイ ンターフェイスや Firebug などと組み合わせて使えるようにしたり、コールバック関数で this が維持されない問題についての対応を含めたサンプルですが、拡張機能固有の話ではないので必要な機能に応じて適当に Web サイト用のコードを好きなように使えば OK です。

ユーザ設定インターフェイス

ユーザ設定操作用のインターフェイスのサンプルコードです

var myextension = {
	// preference interface
	prefs: {
		_service: Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefService),
		setBranch: function(branch) {
			this._service = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefService).getBranch(branch);
		},
		getPref: function(name) {
			switch(this._service.getPrefType(name)) {
				case this._service.PREF_INVALID:
					return null;
				case this._service.PREF_STRING:
					return decodeURIComponent(escape(this._service.getCharPref(name)));
				case this._service.PREF_INT:
					return this._service.getIntPref(name);
				case this._service.PREF_BOOL:
					return this._service.getBoolPref(name);
				default:
					return null;
			}
		},
		setPref: function(name, val) {
			switch(typeof val) {
				case "string":
					this._service.setCharPref(name, unescape(encodeURIComponent(val)));
					break;
				case "number":
					this._service.setIntPref(name, parseInt(val));
					break;
				case "boolean":
					this._service.setBoolPref(name, val);
					break;
				default:
					this.log("cannot set pref - val: " + val);
					return null
			}
			return true;
		},
		get datasource()    this.getPref("datasource"),
		set datasource(val) this.setPref("datasource", val),
	}
	onLoad: function() {
		var mypref = this.prefs.getPref("extensions.myextension.mypref");
		this.prefs.setBranch("extensions.myextension.");
		var anotherpref = this.prefs.getPref("anotherpref");
	}
}
window.addEventListener("load", function() { myextension.onLoad() }, false);

prefs オブジェクトを自分の拡張機能専用オブジェクトのプロパティとして定義し、prefs.getPref() と prefs.setPref() メソッドを使います。設定の型は設定時の引数に応じて自動判別されるので、引数の型には注意してください。

また、prefs.setBranch() メソッドを使えば共通する接頭辞部分を省略できるようになります。Firefox 本体や他の拡張機能の設定を操作する予定がない場合、誤って自分の拡張機能以外の設定を書き換えてしまわないようにするためにも、ブランチを設定しておくことをオススメします。

拡張機能デバッグインターフェイス

拡張機能開発時によく使うログや表明 (assertion) などのためのインターフェイスのサンプルコードです

var myextension = {
	// debug interface
	debug: {
		// https://developer.mozilla.org/en/NsIConsoleService
		_consoleservice: Components.classes["@mozilla.org/consoleservice;1"].getService(Components.interfaces.nsIConsoleService),
		_Cc_scripterror: Components.classes["@mozilla.org/scripterror;1"],
		_Ci_scripterror: Components.interfaces.nsIScriptError,
		enabled: false, // set true to output
		noFirebug: false, // don't show object in Firebug
		prefix: "",  // prefix string
		createScripterror: function() this._Cc_scripterror.createInstance(this._Ci_scripterror),
		// log for Firebug with existence check
		logFirebug: function(x) this.enabled && !this.noFirebug && Firebug && Firebug.Console && Firebug.Console.log(x),
		logFirebugOnlyObject: function(x) typeof x == "object" && x != null && this.logFirebug(x),
		// log/warn/error in console
		log: function(message) {
			if (this.enabled) {
				this._consoleservice.logStringMessage(this.prefix+message);
				this.logFirebugOnlyObject(message);
			}
		},
		warn: function(message) {
			if (this.enabled) {
				var stack = Components.stack.caller;
				var error = this._Cc_scripterror.createInstance(this._Ci_scripterror);
				error.init(this.prefix+message, stack.filename, null, stack.lineNumber, null, this._Ci_scripterror.warningFlag, null);
				this._consoleservice.logMessage(error);
				this.logFirebugOnlyObject(message);
			}
		},
		error: function(message) {
			if (this.enabled) {
				var stack = Components.stack.caller;
				var error = this._Cc_scripterror.createInstance(this._Ci_scripterror);
				error.init(this.prefix+message, stack.filename, null, stack.lineNumber, null, this._Ci_scripterror.errorFlag, null);
				this._consoleservice.logMessage(error);
				this.logFirebugOnlyObject(message);
			}
		},
		// debug with exception (error objects)
		exception: function(error) {
			if (this.enabled) {
				Components.utils.reportError(error);
				this.logFirebugOnlyObject(error);
			}
		},
		stack: function(error) {
			if (this.enabled) {
				if (error instanceof Error) {
					this._consoleservice.logStringMessage(this.prefix+error.stack);
				}
				else {
					error = new Error();
					var callerstack = error.stack.replace(/^.*\n.*\n/, "");
					this._consoleservice.logStringMessage(this.prefix+callerstack);
				}
				this.logFirebugOnlyObject(error);
			}
		},
		// alert and assert
		alert: function(message) {
			if (this.enabled) {
				window.alert(this.prefix + message);
				this.logFirebugOnlyObject(message);
			}
		},
		assert: function(cond, message) {
			var failed = this.enabled && !cond;
			if (failed) {
				var message = this.prefix+message;
				var stack = Components.stack.caller;
				var error = this._Cc_scripterror.createInstance(this._Ci_scripterror);
				error.init(message, stack.filename, null, stack.lineNumber, null, this._Ci_scripterror.errorFlag, null);
				this._consoleservice.logMessage(error);
				window.alert(message);
				this.logFirebugOnlyObject(message);
			}
			return !failed;
		}
	},
	onLoad: function() {
		this.debug.enabled = true;
		this.debug.prefix = "myextension debug:\n";
		this.debug.log("myextension inited!");
	}
}
window.addEventListener("load", function() { myextension.onLoad() }, false);

debug オブジェクトを自分の拡張機能専用オブジェクトのプロパティとして定義し、debug.log() などのメソッドを使います。debug.prefix に文字列を設定しておけば、コンソールのメッセージすべての先頭にその文字列が表示されるので検索しやすくなります。引数がオブジェクトの場合、Firebug がインストールされていれば Firebug のコンソールにも出力します。

上記の例では直接 true を代入していますが、debug.enabled プロパティはユーザ設定から読み込むようにすれば、開発環境だけでログを出力するといった使い方もできます。

より詳しい解説の追加などはまた後日…

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

注意:このトピックは 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」にて適用するのもよいかもしれません。

拡張アンインストール時になんらかの処理を行う

 古くから拡張を作られている方で拡張アンインストール時になんらかの処理を行うために、ObserverService の通知を利用されている方も少なくないかと思います。またあるいは FUEL を利用して簡便に拡張のアンインストールを検出している方も少なくないと思います。
 これらの2つの方法が使えなくなる予定になっています。最新の方法ではアドオンマネージャを使って、拡張の削除を検出するようになっています。

 拙作 Hütte Nippon では、ほぼサンプル通り以下のようなコードになっています。

(function(){
  let Cc=Components.classes;
  let Ci=Components.interfaces;
  let tm;
  function rmStringValue(pentry){
    try {
      Cc["@mozilla.org/preferences-service;1"]
        .getService(Ci.nsIPrefBranch).deleteBranch(pentry);
    }catch(e){
      return false;
    }
    return true;
  };
  function clearProfile(){
    rmStringValue('extensions.weather.prefecture');
    rmStringValue('extensions.weather.region');
    rmStringValue('extensions.weather@avidya.ne.jp.install-event-fired');
  };
  function init(){
    window.clearTimeout(tm);

    let listener = {
      onUninstalling: function(addon) {
        if (addon.id == "weather@avidya.ne.jp") {
          clearProfile();
        }
      },
      onOperationCancelled: function(addon) {
        if (addon.id == "weather@avidya.ne.jp") {
          beingUninstalled = (addon.pendingOperations & AddonManager.PENDING_UNINSTALL) != 0;
        }
      }
    }

    Components.utils.import("resource://gre/modules/AddonManager.jsm");
    AddonManager.addAddonListener(listener);
  }
  tm=window.setTimeout(init,500);
})();

 なお、Hütte Nippon のライセンスを以前から MIT License に変更しています。コピーして使うなり、参考にするなり自由にご利用ください。
 また Hütte Nippon の全ソースコードは github にて公開しています。

FUEL の仕様変更予定 (Application.getExtensions)

Firefox の拡張機能を簡単に書けるようにするためのライブラリ FUEL には拡張機能の情報を取得するメソッドが存在しますが、今度 Jetpack 対応なども含めて Extension Manager 周りが大きく書き直されるのに伴い、一点だけ仕様変更が予定されています。

拡張機能の情報を得る処理はこれまで同期処理となっていましたが、Firefox 高速化のためなどに様々な処理が非同期化されているのと同じく、Extension Manager 周りのコードが非同期処理に変更されます。この影響により、FUEL のメソッド 1 つだけが変更されることになります。

例えば以下のように Application.extensions.get() メソッドを用いたコードは変更が必要になります:

alert(Application.extensions.get("my-addon@foo.com").version);

このようなコードであれば次のように Application.getExtensions() メソッドに callback 関数を渡す形に書き換えることになります:

Application.getExtensions(function(extensions) {
  alert(extensions.get("my-addon@foo.com").version);
});

詳しくは Dave さんのブログを参照してください:

http://www.oxymoronical.com/blog/2010/03/How-were-breaking-some-extensions-in-the-near-future

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

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;" />

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

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

拡張機能パッケージスクリプト

拡張機能のを公開するために xpi パッケージを作るのは面倒なので、普通は xpi パッケージ作成用のビルドスクリプトを使います。

ここでは Ant のビルドスクリプトとして書いたものを公開すると共に、他のパッケージ作成用スクリプトを公開しているページを紹介します。皆さんの開発環境や好みに応じて選んでご利用ください。

Ant スクリプト

ダウンロード: build.xml
動作環境: Apache Ant 1.7 以降

このファイルを拡張機能の開発用ディレクトリ(chrome.manifest ファイルと install.rdf ファイルのあるディレクトリ)にコピーし、そのディレクトリで ant コマンドを(引数なしで)実行すると xpi パッケージが作成されます。

build.xml ファイルと異なるディレクトリのファイルを xpi にするにはコマンドラインで src.dir プロパティを指定してください。

ant -Dsrc.dir=path/to/your/extension/dir

ビルド時に生成される一時ファイルディレクトリを残したくない場合は次のコマンドを実行してください。

ant xpi clean

このスクリプトで生成されるパッケージの特徴:

  • jar ファイルを使わない開発ディレクトリから jar ファイルを用いた標準的な xpi パッケージを生成
  • それに伴い chrome.manifest を書き換えたものを xpi パッケージに含める
  • jar ファイルは無圧縮、xpi は圧縮という標準的な圧縮パターンになる
  • skin ディレクトリの下に classic ディレクトリがある標準形に統一される
  • .svn などの ant の defaultexcludes 対象ファイルやディレクトリは除外される

一般的な拡張機能のディレクトリ構成で、content パッケージが単一の拡張機能を想定しています。その他のディレクトリ構成やパッケージ構成である場合はビルドできません。必要に応じてビルドスクリプトを書き換えてご利用ください。

参考までにビルド処理の流れを書いておきます:

  1. chrome.manifest の content 行からパッケージ名 <packagename> を取得
  2. install.rdf から拡張機能のバージョン <version> を取得
  3. chrome/content ディレクトリのファイルを temp/jar/content/<packagename> にコピー
  4. chrome/locale/<ab-CD> ディレクトリのファイルを temp/jar/locale/<ab-CD>/<packagename> にコピー
  5. chrome/skin/classic ディレクトリが存在する場合 temp/jar/skin/classic/<packagename> にコピー
  6. chrome/skin/ ディレクトリのファイルを temp/jar/skin/classic/<packagename> にコピー
  7. temp/jar ディレクトリのファイルから <packagename>.jar ファイルを生成
  8. chrome.manifest を jar ファイルを使ったパスに合わせて書き換えたものを生成
  9. chrome/* ディレクトリ以外のファイルと <packagename>.jar から <packagename>-<version>.xpi を生成

シェルスクリプト

Piro さんが利用しているビルドスクリプトが以下の URL で公開されています:

http://www.cozmixng.org/repos/piro/make-xpi/trunk/

http://www.cozmixng.org/repos/piro/make-xpi/trunk/makexpi.sh

Windows であれば Cygwin などをインストールしてご利用ください。

Windows 用パッチスクリプト

Ren’s Bookmark「拡張機能開発に役立つ(かもしれない)バッチファイル」でバッチファイルが解説付きで公開されています:

http://ren-bookmark.jugem.jp/?eid=42

ブラウザウィンドウのサムネイルを描画する(改訂版)

タブカタログ拡張機能のように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 メソッドを使用する方式が可能なことがわかりました。

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

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

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

この問題を解決するには、ブラウザウィンドウのサムネイルの上に、Web ページのサムネイルを重ねて描画する必要がありそうです。ここでは、 Tab Flick 拡張機能で実際に使っている、 html:canvas 要素を2個重ねる方式を紹介します。

XUL

ブラウザウィンドウのサムネイル描画用とWebページのサムネイル描画用の、2つの html:canvas 要素を xul:stack 要素で重ねます。

<stack>
	<html:canvas id="outerCanvas" /><!-- ブラウザウィンドウ用 -->
	<html:canvas id="innerCanvas" /><!-- Webページ用 -->
</stack>

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");

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

var oCanvas = document.getElementById("outerCanvas");
var iCanvas = document.getElementById("innerCanvas");

ブラウザウィンドウに対する現在のブラウザ(xul:browser 要素)の位置の相対関係から、Webページ用の html:canvas 要素の位置を調整します。

var rect = win.gBrowser.mCurrentBrowser.getBoundingClientRect();
iCanvas.style.left = Math.round(rect.left * scale) + "px";
iCanvas.style.top  = Math.round(rect.top  * scale) + "px";

この時点で2つの html:canvas 要素に枠線を付加して表示してみると、以下のようになっています。
なお、この時点では html:canvas 要素のサイズはまだ調整されていません。

次の処理へ移る前に、あらかじめ関数を作っておきます。引数で指定したウィンドウのサムネイルを、指定した html:canvas 要素に描画する関数です。

// @param aWindow ウィンドウのオブジェクト
// @param aCavas  html:canvas要素
// @param aScale  サムネイルの拡大/縮小率
function drawPreviewForWindow(aWindow, aCanvas, aScale) {
	var w = aWindow.innerWidth  || aWindow.document.documentElement.clientWidth;
	var h = aWindow.innerHeight || aWindow.document.documentElement.clientHeight;
	aCanvas.width  = Math.round(w * aScale);
	aCanvas.height = Math.round(h * aScale);
	var ctx = aCanvas.getContext("2d");
	ctx.clearRect(0, 0, aCanvas.width, aCanvas.height);
	ctx.save();
	ctx.scale(aScale, aScale);
	ctx.drawWindow(aWindow, aWindow.scrollX, aWindow.scrollY, w, h, "rgb(255,255,255)");
	ctx.restore();
}

先ほど作った関数を使い、ブラウザウィンドウと、そのブラウザウィンドウの現在のタブに表示されたWebページの両方のサムネイルを描画します。なお、 win.contentwin に対応するブラウザウィンドウの現在のタブに表示されたWebページの window オブジェクトを表します。

drawPreviewForWindow(win,         oCanvas, scale);
drawPreviewForWindow(win.content, iCanvas, scale);

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

なお、最終的に2つの html:canvas 要素は以下のような位置関係・サイズになっています。

別の方式…1個の html:canvas 要素で実現可能か?

今回は2つの html:canvas 要素を重ねる方式を紹介しましたが、別の方式として、1個の html:canvas 要素に対して2回 drawWindow メソッドを使用してブラウザウィンドウとWebページの2つのサムネイルを重ねて描画する方式も思いつきました。しかし、筆者が調べた限りでは、 drawWindow メソッドは必ず html:canvas 要素の左上位置(座標 0, 0)から描画を開始するため、Webページのサムネイルを正しい位置に重ねることができないようでした。したがって、別の方式は断念しました。