たまに18歳未満の人や心臓の弱い人にはお勧めできない情報が含まれることもあるかもしれない、甘くなくて酸っぱくてしょっぱいチラシの裏。RSSによる簡単な更新情報を利用したりすると、ハッピーになるかも知れませんしそうでないかも知れません。
の動向はもえじら組ブログで。
宣伝。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。 以下の特設サイトにて、単行本まんがでわかるLinux シス管系女子の試し読みが可能!
Firefoxでは、タブがセッション情報も伴って復元された時に、SSTabRestoring
とSSTabRestored
という2つのイベントが発行される。イベントのoriginalTarget
はいずれも復元されたタブの要素で、SSTabRestoring
はセッション復元処理が走ったけれどもタブの読み込みは完了していない段階、SSTabRestored
はタブの読み込みが完了した段階で発行される。
これらのイベントが発行される場面は、3つある。
この3つの場合を、特に1とそれ以外とを判別したい、その方法を考えてみたよ、というのがこのエントリの主題です。
例えばツリー型タブでは、タブの親子関係が変更された時のインデントの変更やツリーの折りたたみ時にアニメーション効果を適用している。しかしながら、Firefoxのセッション復元時に複数のタブのツリー構造を一気に変更する場合には、いちいちアニメーションしてたら重くてしょうがない。なので、この時だけはアニメーション効果を適用しないようにしたいと僕は思った。
しかしながら、前述のSSTabRestoring
とSSTabRestored
からは、そのイベントが発行されたのが上記の3つの場面のうちいずれなのかが分からない。どの場面でイベントが発行されたのかを判別するには、他の情報も参照する必要がある。
実は1の場合については、セッションが復元される時にObserverServiceに登録したオブザーバに対してsessionstore-windows-restored
というメッセージが通知される。なので、これを監視すればいいんじゃないか?と、最初のうちは考えてた。
でも、話はそう単純には済まなかった。実際にイベントを監視してみると、セッション復元時には以下のような順番でイベントが発行されていることが分かった。
SSTabRestoring
イベントが発行される。sessionstore-windows-restored
が通知される。SSTabRestoring
イベントが順番に発行される。SSTabRestored
イベントが読み込みの終わった物から発行される。3と4はこの通りにならないこともあって、あるタブのSSTabRestoring
が発行されてすぐに読み込みが完了したタブについては、次のタブのSSTabRestoring
が発行される前にSSTabRestored
が発行される場合もある。
一番致命的なのは、複数タブのセッション復元が始まったことは通知されるのに、全部のタブのセッション復元が終わったことは通知されないという点だ。
sessionstore-windows-restored
が通知されたらその後に発行されたSSTabRestoring
は全部ウィンドウ単位のセッション復元の一部なんだな、と判断してしまうと、例えば10個のタブを開いたウィンドウのセッションを復元した後、1つタブを開いて、閉じて、開き直した時、そのタブまで「ウィンドウ単位のセッション復元の一部」と誤爆してしまう。SSTabRestored
が発行されるまでの間にSSTabRestoring
が発行されたタブ」がウィンドウ単位のセッション復元なんだな、と判断してしまうと、後続のタブのSSTabRestoring
よりも前にSSTabRestored
が発行された時に、セッション復元が終わってないのに終わったと見なされてしまう。という具合で、「ウィンドウ単位でのセッション復元の終わり」がいつなのかが分からないと、誤爆しまくりで全然役に立たない。
また、重ねて困ったことに、sessionstore-windows-restored
の通知はsubjectがnull
なので、どのウィンドウでセッション復元が開始されたのかすらオブザーバ側からは分からない。別のウィンドウでセッション復元がはじまった時に来た通知を見て「これからこのウィンドウで開き直されるタブは全部、ウィンドウ単位のセッション復元の一部なんだな」と判断してしまってはいけない。
dump()
をコードの中に埋め込んで調べたところ、nsSessionStore.js内の各処理とイベントは、以下のような順番で起こっているらしいということが分かった。
SSTabRestoring
が発火sessionstore-windows-restored
が通知されるSSTabRestoring
が発火×タブの個数分SSTabRestored
が発火×タブの個数分SSTabRestored
が発火したら、すべてのタブの復元が完了このうちnsSessionStore::restoreHistoryPrecursor()やnsSessionStore::restoreHistory()やnsSessionStore::restoreDocument_proxy()は、タブを1つ復元するだけの時にも使われてる。
よくコードをよく読むと、nsSessionStore::restoreHistoryPrecursor()の中で「復元するタブの数だけ新しくタブを開く」「それらのタブのtab.linkedBrowser.parentNode.__SS_data._tabStillLoading
をtrue
にセットする」という処理が行われて、その後でsessionstore-windows-restored
がオブザーバに通知されていた。(このtab.linkedBrowser.parentNode.__SS_data._tabStillLoading
は、タブの復元が完了した段階でundefined
になる。)
ということは、sessionstore-windows-restored
が通知された時にウィンドウ内の全部のタブを調べて、tab.linkedBrowser.parentNode.__SS_data._tabStillLoading
がtrue
であるタブが2つ以上ある(復元待ちのタブが複数ある)なら、そのウィンドウでこれからウィンドウ単位のセッション復元が行われようとしている、と考えてよいわけだ。
で、SSTabRestored
が発行される度に復元待ちのタブの数を確認して、復元待ちのタブの数が0になったら、ウィンドウ単位でのセッション復元が終わったと判断できる。
厳密には、「ウィンドウ単位でのセッションの復元中に別途タブを1個だけ復元した」という場合にもそのタブが「ウィンドウ単位のセッション復元の一環で復元された」と見なされてしまうので、この方法にも穴はある。たくさんのタブのツリー構造を一気に変更した時にアニメーションでクソ重くなるのを避けたい、という僕の目的ではこれで必要十分なので、これ以上は追求してない。
以上をコンパクトにまとめると、こんな感じになる。
var ObserverService = Cc['@mozilla.org/observer-service;1']
.getService(Ci.nsIObserverService);
var observer = {
restoringWindow : false,
getRestoringTabsCount : function() {
return Array.slice(gBrowser.mTabContainer.childNodes)
.filter(function(aTab) {
var owner = aTab.linkedBrowser;
var data = owner.parentNode.__SS_data;
return data && data._tabStillLoading;
}).length;
},
observe : function(aSubject, aTopic, aData) {
if (aTopic == 'sessionstore-windows-restored')
this.restoringWindow = this.getRestoringTabsCount() > 1;
},
handleEvent : function(aEvent) {
switch (aEvent.type) {
case 'load':
window.removeEventListener('load', this, false);
window.addEventListener('unload', this, false);
gBrowser.addEventListener('SSTabRestoring', this, false);
gBrowser.addEventListener('SSTabRestored', this, false);
return;
case 'unload':
ObserverService.removebserver(this, 'sessionstore-windows-restored');
window.removeEventListener('unload', this, false);
gBrowser.removeEventListener('SSTabRestoring', this, false);
gBrowser.removeEventListener('SSTabRestored', this, false);
return;
case 'SSTabRestoring':
this.onTabRestoring(aEvent);
return;
case 'SSTabRestored':
this.onTabRestored(aEvent);
return;
}
},
onTabRestoring : function(aEvent) {
var tab = aEvent.originalTarget;
if (this.restoringWindow) {
// ウィンドウ単位でのセッション復元時の処理
}
else {
// タブが個別に開き直された時の処理
}
},
onTabRestored : function(aEvent) {
if (this.restoringWindow)
this.restoringWindow = this.getRestoringTabsCount() > 0;
}
};
window.addEventListener('load', observer, false);
ObserverService.addObserver(observer, 'sessionstore-windows-restored', false);
セッションストアAPIを使ってタブにいろんな情報を紐付けて、その情報に基づいてあれこれしたい人には、役に立つんじゃないでしょうか。(でもそんな変なことやってる人はほとんどいないんだろうなー)
I would like to see a shortcut assigned to show/hide the tab bar with the next update. That would be very useful since I reckon, since I have to click every time I want to show/hide it, which every time I want to read some thing on the web, which is way too frequent.
次のアップデートで、タブバーの表示・非表示の切り替えのためのキーボードショートカットを追加して欲しいです。私が思うにそれはきっととても便利でしょう。Webで何かを読もうと思う度に、タブバーの表示・非表示を切り替えるために今は(タブバーを?)その都度クリックしていますが、この操作が頻繁すぎて面倒です。
I'm very sorry, currently I have no idea to add new shortcut to show/hide tab bar. Instead, if you press "Ctrl" key for a while, collapsed tab bar will be expanded while you press the key. I hope it helps you.
If you like, you can call internal methods to show/hide tab bar from keyboard shortcuts defined by keyconfig or KeySnail. For example:
// toggle mode shown <=> hidden (shown <=> shrunken)
TreeStyleTabBrowserAutoHide.toggleMode();
// set to "shown"
TreeStyleTabService.setTreePref('tabbar.autoHide.mode',
TreeStyleTabBrowserAutoHide.prototype.kMODE_DISABLED);
// set to "hidden"
TreeStyleTabService.setTreePref('tabbar.autoHide.mode',
TreeStyleTabBrowserAutoHide.prototype.kMODE_HIDE);
// set to "shrunken"
TreeStyleTabService.setTreePref('tabbar.autoHide.mode',
TreeStyleTabBrowserAutoHide.prototype.kMODE_SHRINK);
Note: These examples work on Tree Style Tab 0.8.2009100101 or later.
すみません、今の所タブバーの表示・非表示を切り替えるショートカットを加える予定はないです。その代わり、Ctrlキーを長押しすると、非表示になっていたタブバーがCtrlキーを押している間表示されるという機能があります。これが助けになることを願っています。
お好みで、タブバーの表示・非表示を切り替える内部的な機能をkeyconfigやKeySnailで定義されたショートカットから呼ぶこともできます。例は上記の通りです(ツリー型タブ0.8.2009100101以降で動作します)。
In Tree Style Tab extension, would it be possible to make the background (dark gray blank area) display an image? Maybe with a userstyle or script...
ツリー型タブで、タブの背景(暗いグレーの空白の領域)に画像を表示できませんか? ユーザースタイルシートかスクリプトを使えばできると思うんですが……
On Firefox 3.6, you'll be able to make the background color of the tabbar transparent, like following CSS in the userChrome.css:
tabbrowser[treestyletab-style][treestyletab-mode]
.tabs-stack,
tabbrowser[treestyletab-style][treestyletab-mode]
.tabbrowser-tabs,
tabbrowser[treestyletab-style][treestyletab-tabbar-position]
.tabbrowser-tabs {
background: transparent !important;
}
On Firefox 4:
:root[treestyletab-style][treestyletab-tabbar-position]
#appcontent,
:root[treestyletab-style][treestyletab-tabbar-position]
tabbrowser,
:root[treestyletab-style][treestyletab-tabbar-position]
.tabbrowser-strip {
background: transparent !important;
}
Another way, you can get transparent tab bar with a secret preference "extensions.treestyletab.tabbar.style.aero". Go to "about:config" and turn it to "true".
上記のようなCSSをuserChrome.cssに書くと、タブバーの背景色を透明にできます。1つ目はFirefox 3.6用、2つ目はFirefox 4用の指定です。
また別の方法として、隠し設定「extensions.treestyletab.tabbar.style.aero」でもタブバーを透明にできます。「about:config」を開いて、この設定の値を「true」に変更して下さい。
XUL/Migemo 0.12.xで、機能を他のアドオンとかから呼び出すためのAPIを刷新してみたよ。
古いAPI(はてなブックマーク拡張とかが使ってくれてるやつ)は僕自身が色々よく分からないまま作った物だったために、引数を文字列で渡さないといけないとか戻り値も正規表現オブジェクトではなく文字列だとか、色々と使い勝手が悪かったと思う。メインウィンドウ以外では呼び出す時にいちいちXPConnect使わないといかんし。
新しいAPIは、それに比べたらものっそシンプルになった。XPConnect使わなくてもmigemo.getRegExp('hoge')
とか書くだけで使えるし、戻り値もすぐに使える正規表現オブジェクトが返ってくるし。互換性を保つために旧APIはそのまま残してあるので、旧APIを使ってるアドオンが動かなくなるということはないけど、今後は使うなら新APIの方を使うのがお勧めです。
で、このAPIはWebページ内のスクリプトでもCPUの使用率を取得できるようにするAPIを提供する例のアドオンの技術の応用なので、Webページ内のスクリプトからもXUL/Migemoの機能を利用できてしまいます。
ただしスクリプトの実行権限の関係で、上記のmigemo.getRegExp('hoge')
のような、正規表現オブジェクトを直に受け取る機能は使えません。代わりにJavaScript/Migemo互換の、正規表現のソース文字列を返すAPI migemo.query('hoge')
などを使う必要があります。
XUL/Migemo 0.12.2以降が入ってる環境でこのページを表示してれば、以下のデモを試せるはず。
ページ内検索系のメソッドもあるんだけど、多分使いでがなさそうなので解説は用意してないです。
すでに上でもリンクしてるけど、エンジンごとページ内に埋め込めるJavaScript/Migemoという実装もあるから、XUL/Migemoを入れてるFirefoxユーザでないと使えないこのAPIってなんか意味あんの?と言われそう。深くはツッコまないでください。
以下、補足事項。
当初このエントリでは、「Webページ上のスクリプトからもmigemo.getRegExp('hoge')
のようにして正規表現を取得して利用できる」という風な書き方をしてたけど、これは大間違いだった。戻り値の正規表現オブジェクトが生成された実行コンテキストがUniversalXPConnect特権のある場所なのに対して、呼び出し元は特権のない普通のWebページ上であるため、それぞれの権限が違うので本来ならその正規表現の各メソッドは実行できないのが正しい。
ただ、Gecko 1.9.1(Firefox 3.5)以前のバージョンにはバグがあって、上記のようなセキュリティのチェックが働かないために、戻り値の正規表現の各メソッドを呼べてしまう状態になっていた。
このバグはGecko 1.9.2(Firefox 3.6)以降ではすでに修正済みで、実際、Trunk等で戻り値の正規表現オブジェクトのメソッドを呼ぼうとすると、その場で処理が中断されて Error: RegExp.prototype.toString called on incompatible ChromeObjectWrapper というエラーがエラーコンソール上に出力される。
ということで、JavaScript/Migemo互換のAPIとして正規表現オブジェクトのソース文字列だけを返すような機能を0.12.2で新たに加えた。文字列や数値などのプリミティブ値に対してはセキュリティのチェックが行われないようなので、こっちはGecko 1.9.2以降でもWeb上で使える。
Tab Progress BarのプログレスバーがFirefox 3.7のモックアップ風なのを見て「羨ましい!」と思ったのでInformational Tabのデフォルトスタイルをそのように変えてみた。一応、設定で今まで通り(ラベルの下に表示)にも戻せる。
で、やるならとことんやってみっか!と一念発起して、モックアップにあるような光るプログレスバーを再現しようと頑張ってみた。 伸びるバーの部分は背景画像で、ぽわーんと光った感じは-moz-box-shadowを使ってるので、Firefox 3.5じゃないと期待通りには見えない。
XUL/MigemoがTrunk(3.7a1pre)で動かなくなっていた件について原因を調べてた。
エラーメッセージに見覚えがあるなあと思って検索したら、前に書いたエントリがヒットした。
一個だけ躓いた所として、XPCOMコンポーネントの新しいメソッドをIDL定義に追加して引数にnsISelectionController型のオブジェクトを渡すようにしていた所、XPIDLでのコンパイルは通るんだけど実際に使う時に
NS_ERROR_XPC_CANT_GET_PARAM_IFACE_INFO
というエラーが出てにっちもさっちもいかなくなってしまった。ダメ元で、引数の型をnsISupportsにして受け取り側でQueryInterfaceするようにしてみたところ、ちゃんと動いてくれた。一体何だったんだろうこれは。
ああ、前にも詰まってたところだったか……この時は結局「理由」が分からないままで、とりあえず動くようにはなったからということでそれ以上は調べなかったんだよね。
改めて検索してみたら、似たような問題にぶち当たった人がいたようだった。で、やっと何が問題の原因だったのかが分かった。
新しいXPCOMコンポーネントを定義する時に、インターフェースも新しく定義する場合、XPIDLを使ってインターフェースを定義してやらないといけない。
[scriptable, uuid(4aca3120-ae38-11de-8a39-0800200c9a66)]
interface xmIXMigemoFileAccess : nsISupports
{
AString getAbsolutePath(in AString filePath);
AString getRelativePath(in AString filePath);
AString getExistingPath(in AString absoluteOrRelativePath);
AString readFrom(in nsIFile file, in ACString encoding);
nsIFile writeTo(in nsIFile file, in AString content, in ACString encoding);
};
この時、メソッドの引数や返り値の型として、AStringやlongのようなプリミティブ型(?)だけでなく、nsIFile
のように他のインターフェースを使うこともできる。
この時気をつけないといけないのが、インターフェースには2つの識別子があるということ。1つは、上記の例でいえばinterface xmIXMigemoFileAccess
という部分で定義されているインターフェース名「xmIXMigemoFileAccess」、もう一つは[scriptable, uuid(4aca3120-ae38-11de-8a39-0800200c9a66)]
という部分で定義されているインターフェースID(IID)「4aca3120-ae38-11de-8a39-0800200c9a66」だ。
今回は、XUL/Migemoのコンポーネントの機能のうち、nsIDOMRangeやnsIDOMWindowを値の型として使っていた部分でエラーが起こっていた。
interface xmIXMigemoTextUtils : nsISupports
{
(略)
AString range2Text(in nsIDOMRange range);
(略)
具体的にはこの辺。どうも、Firefox 3.5から3.7a1preまでの間のどこかの時点で、nsIDOMRangeやnsIDOMWindowのIIDが変更されたらしい。XPIDLのコンパイル(.xptファイルの生成)には成功しても、そのあと3.7a1preのFirefoxが.xptを解釈する時に、IIDの方でnsIDOMRangeやnsIDOMWindowのインターフェース定義を探すために、「こんなIIDのインターフェースは定義されてないよ! インターフェースの情報が見つからないよ!」というエラーになっていたようだ。
とぴあさんに色々教えてもらった。元々、XPCOMの元になった(?)COMの世界つまりC++の世界では、インターフェースの識別にはIIDを使うのが原則というかIIDこそが本来の識別子で、nsIDOMRangeとかの名前はそれへの参照に過ぎないということなのだそうな。
インターフェースの内容が変化した時(メソッドの追加等)には、「古いインターフェースの定義が消されて、別のIIDで新たなインターフェースが定義された」というような扱いになるようだ。「IIDが変わった」と前述しているけれども、プログラム的には「IIDが変わっただけで同じインターフェース」なのではなく「全くの別物」という扱いだから、全く互換性は保証されない……というわけ。
なお、互換性を維持したままインターフェースに新しい機能を追加するためには、現在使われているインターフェースの定義はそのまま残して、それを継承した新しいインターフェースを定義する必要があるということになる。「nsIPrefBranch2」とか「nsIGlobalHistory3」などがそれにあたる。
nsIDOMRangeやnsIDOMWindowのIIDが3.7a1preのものと同じになっている新しいSDKを使って.xptファイルを作り直してやれば、3.7a1preでもXUL/Migemoが動くようになるはず。でも、そうすると今度はFirefox 3.5以下で動かなくなる。それは困る。
無難な解決策は、値の型として使うインターフェースを、古いFirefoxから新しいFirefoxまでの間でずっと変わっていないインターフェースにするということ。nsISupports(すべてのXPCOMの基底インターフェース)ならほぼ確実に使える。
interface xmIXMigemoTextUtils : nsISupports
{
(略)
AString range2Text(in nsISupports range);
(略)
このようにインターフェースの定義を変えた上で、実装の方で受け取った値をQueryInterface()
してやる。
range2Text : function(aRange) {
aRange.QueryInterface(Ci.nsIDOMRange);
var doc = aRange.startContainer;
(略)
こうすると、メソッドを呼ぶ時に、DOMのRangeオブジェクトをそのまま引数に渡せる状態を維持できる。
JavaScriptのレイヤからはIIDではなくヒューマン・リーダブルなインターフェース名だけを使うのが一般的なのだけれども、こうしておけばXPCOMのフレームワークが自動的に「新しいIIDのnsIDOMRangeインターフェース」を参照してくれる。実際にインターフェースで定義されている内容には変更が起こっているかもしれないから、確実な動作は保証できなくなるけれども、当該のインターフェースが「既にある機能はなくなったり変更されたりせず、新しい機能が追加されていくだけ」という傾向があるのなら、おおむね問題なく動作し続けてくれると考えられる。
とぴあさんが調べてくれたのだけれども、今回のトラブルの原因になったnsIDOMRangeは定義の頭の方に@status FROZEN
と書かれていて、本来であれば、IIDが変わることもなければメソッドやプロパティなどのインターフェース定義の内容が変わることもない、安心して永続的に使えるインターフェースだったはず……のようだ。
それが、Bug 396392 – Support for getClientRects and getBoundingClientRect in DOM Rangeに提出されたパッチでメソッドが追加されると同時にIIDも変更されてしまった。本当は、これはあってはならない事態らしい。当該バグのコメントでもnsIDOMRangeのIIDは元に戻して、変更はnsIDOMNSRange(Geckoの独自拡張の機能が色々定義されているインターフェース)に対して行うべきと書かれている。おそらく近いうちに、nsIDOMRangeのIIDは元の物に戻されて、XUL/Migemoが動かなくなってしまった問題も解消されるものと思われる。
個人的な感覚としては、インターフェースに変化が無くても実装が変わって挙動も変わりました……なんて事がMozillaではザラにあるので、インターフェースの部分でだけ「ちょっとでも変化があったらIIDは別の物! インターフェースとしても別物!」という風に厳密に区別しても意味なくね? と思う。ぶっちゃけ、「安心して使えるAPI」なんてのはMozillaの世界じゃリップサービスに過ぎないと思ってる。(とぴあさんには、それはプロジェクトのマネジメントがマズイという別のレイヤの問題だよねと言われた。)
あと、現在のFirefox(Gecko 1.9以降)では、自分で新しくインターフェースを定義してXPCOMコンポーネントを作る必要はほとんど無いと言っていい。JavaScriptでコンポーネントを定義してJavaScriptだけから使うのであれば、JavaScriptコードモジュールを使えばよくなった。また、起動直後に処理を行うような場合なんかには相変わらずXPCOMコンポーネントの定義が必要だけど、それは既存のインターフェース(nsISupportsやnsIObserver)だけでも事足りる。XPIDLが必要になるのは、JavaScriptで書かれた機能をC++のレイヤから呼び出したいような時だけだ。普通に開発する分には、こんな事で悩む必要は今や全くない。という事に気がついて、今更になって激しい徒労感を感じている。
表題の件について、どーも実際に表示されてる内容とセッション情報とが食い違ってるケースがあるようだ。
Firefoxのタブとかのセッション情報はJSONっぽい文字列で保存されてて、最初はJSON整形で読みやすい形にして調べようと思ってたけど、めんどすぎたので、以下のようなスクリプトでツリー構造の所だけ可視化してみた。
var sv = gBrowser.treeStyleTab;
var session = sv.SessionStore.getWindowState(window);
eval('session = '+session);
var result = [];
session.windows[0].tabs.forEach(function(aInfo) {
var entry = aInfo.entries[aInfo.entries.length-1];
var item = {
label : entry.title+' / '+entry.url,
id : aInfo.extData[sv.kID],
children : (aInfo.extData[sv.kCHILDREN] || '').split('|'),
parent : (aInfo.extData[sv.kPARENT] || ''),
items : []
};
var bullet = '*';
var tab = sv.getTabById(item.id);
if (tab.getAttribute(sv.kPARENT) != item.parent) {
item.label += '\n<WRONG PARENT>';
bullet = '?';
}
if (tab.getAttribute(sv.kCHILDREN) != item.children.join('|')) {
item.label += '\n<WRONG CHILDREN>';
bullet = '?';
}
item.label = item.label.replace(/^/gm, ' ').replace(/^./, bullet);
var current, index;
if (result.some(function(aItem) {
if (!aItem) return false;
if (aItem.items.some(arguments.callee)) return true;
current = aItem;
index = aItem.children.indexOf(item.id);
return index > -1;
})) {
if (current.items.length <= index) {
while (current.items.length < index) current.items.push(null);
current.items.push(item);
}
else {
current.items[index] = item;
}
}
else if (result.some(function(aItem) {
if (!aItem) return false;
if (aItem.items.some(arguments.callee)) return true;
current = aItem;
return aItem.id == item.id;
})) {
current.items.push(item);
}
else {
result.push(item);
}
});
var string = result.map(function(aItem) {
var children = aItem.items.map(arguments.callee).join('\n');
return children ?
aItem.label+'\n'+children.replace(/^/gm, ' ') :
aItem.label ;
}).join('\n')+'\n';
alert(string);
で、調べてみたら、やっぱりツリー構造がおかしい。ツリー表示はタブの属性値の方に基づいて行われてて、その属性値をnsISessionStoreのsetTabValue()
とdeleteTabValue()
でセッションの方にミラーしてるんだけど、ミラーされてるはずの値が期待通りにミラーされてないようだ。
追記。
……nsSessionStore.jsを読んでたら原因が分かった。
setTabValue()
では内部で最後にsaveStateDelayed()
を呼んでいるため、変更がファイル(プロファイルフォルダ内のsessionstore.js)にすぐ書き出される。deleteTabValue()
ではsaveStateDelayed()
が呼ばれてないために、他の処理の中でsaveStateDelayed()
が呼ばれるまでは変更がファイルに書き出されない。deleteTabValue()
だけで情報をミラーしたつもりでいると、ゴミ情報が残ったままになってしまうことが多々ある。そのゴミ情報が邪魔をして、期待通りにツリー構造が復元されなくなってしまっている。deleteTabValue()
する前にsetTabValue()
で空の値をセットして強制的にセッション情報を書き出させるようにしてみたところ、上記のスクリプトで調査してもセッション情報との間でのツリー構造の不整合は検出されなくなった。
結論:deleteTabValue()
マジ使えねえ……
追記。
それでも全然駄目だった。Firefoxがセッション情報をファイルに書き出す時、読み込み中であるというフラグが立っている(Firefox 3.5以前ではtab.linkedBrowser.parentNode.__SS_data._tabStillLoading
、Firefox 3.6以降ではtab.linkedBrowser.__SS_data._tabStillLoading
がtrue
である)タブについてはキャッシュされた情報を書き出すようになってるのに、このフラグがタブの内容の読み込み完了後も立ちっぱなしになってるせいで、常にキャッシュされた古い情報が書き出されてしまい、setTabValue()
で設定された新しい値が無視されてしまう。
結論:nsISessionStore/nsSessionStore.jsは腐ってる……
まとめると、以下のようなメソッドを使うようにしてやれば色々と幸せになれそうです。
setTabValue : function(aTab, aKey, aValue) {
if (!aValue) return this.deleteTabValue(aTab, aKey);
try {
this.checkCachedSessionDataExpiration(aTab);
this.SessionStore.setTabValue(aTab, aKey, aValue);
}
catch(e) {
}
return aValue;
},
deleteTabValue : function(aTab, aKey) {
try {
this.checkCachedSessionDataExpiration(aTab);
this.SessionStore.setTabValue(aTab, aKey, '');
this.SessionStore.deleteTabValue(aTab, aKey);
}
catch(e) {
}
},
checkCachedSessionDataExpiration : function(aTab) {
var data = aTab.linkedBrowser.__SS_data || // Firefox 3.6-
aTab.linkedBrowser.parentNode.__SS_data; // -Firefox 3.5
if (data &&
data._tabStillLoading &&
aTab.getAttribute('busy') != 'true' &&
aTab.linkedBrowser.__SS_restoreState != 1)
data._tabStillLoading = false;
},
2010年1月29日追記。Firefox 3.6以降とFirefox 3.5以前でフラグが保存される場所が少し違っていたので、その旨を修正した。
2010年9月27日追記。Firefox 4 の最適セッションリストア(原文)の影響によって、まだ実際にはセッションが復元されていないタブなのに、ビジー状態でなくなっているというケースがあり得るようになった。そのため、aTab.getAttribute('busy')
だけでビジー状態を判別すると、これからセッションを復元して欲しい・読み込み中のタブであるにも関わらず_tabStillLoading
をfalseにしてしまい、セッションが復元されなくなってしまうという問題が起こっていた。なので、タブの属性値と併せてaTab.linkedBrowser.__SS_restoring
も確認するようにサンプルコードを修正した。
2010年12月6日追記。aTab.linkedBrowser.__SS_restoring
が廃止されてaTab.linkedBrowser.__SS_restoreState
というプロパティが使われるようになっていたので、それにあわせてサンプルコードを修正した。
trunk gBrowserのloadOneTabとaddTabの引数が変わった - alice0775のファイル置き場 - Yahoo!ジオシティーズを見て初めて知ったけど、TrunkでgBrowser.addTab()
とgBrowser.loadOneTab()
の仕様が変わったようだ。
すでに追加されている「現在のタブの隣に新しいタブを開く」機能は、リファラが渡されていれば現在のタブの隣に、そうでなければタブバーの右端にタブを開くという挙動になっている。これに対し、リファラを渡さなくても現在のタブの隣に新しいタブを開けるようにしたい、という要望が出た(当然と言えば当然だ)。
それを実現するには、普通に考えると、gBrowser.addTab()
とgBrowser.loadOneTab()
の引数でそういう挙動を指定できるようにしてやらないといけない。しかしどっちのメソッドもすでに多数の引数を受け付けるようになってて(現状でもすでに6個ある!)、これ以上引数を増やすのってどうなん? と。関数の引数が多いのは悪い設計の典型例だ。こういう場面ではハッシュなりなんなりを使うのが定石ですわな。そこで件のバグが立ったと。
最初に提出されたパッチは、引数リストにさらにaRelatedToCurrent
を加えつつ、各引数に対応する値をプロパティに持つオブジェクトを2番目の引数として渡した時はそっちを使うようにするという風になってる。これだけ見ると「また引数増やすのかよ、しかも新方式(ハッシュによる指定)もサポートするのかよ。マンドクセ。」と思うところだけど、2番目に提出されたパッチでは引数の数の方は変更が無くて、aRelatedToCurrent
に相当する引数を指定したい時はハッシュを使わなければならないようになってる。実際にチェックインされた内容は後者のパッチの通りだ。
この事から、今後は新方式のAPI(新しいタブの挙動はgBrowser.addTab()
とgBrowser.loadOneTab()
の第2引数でハッシュで指定する)が標準となり、旧方式のAPI(gBrowser.addTab()
とgBrowser.loadOneTab()
に沢山の引数を渡す)はあくまで後方互換性のためにのみ残されている、という風に考えることができる。
参考までに、新旧それぞれの書き方を示しておこう。
// new API
var newTab = gBrowser.addTab('http://www.example.com/', {
referrerURI : referrer, // nsIURI
charset : 'Shift_JIS',
postData : null,
inBackground : true,
allowThirdPartyFixup : false,
relatedToCurrent : false
});
// old API
var newTab = gBrowser.addTab(
'http://www.example.com/',
referrer, // nsIURI
'Shift_JIS',
null, // postData
true, // inBackgorund
false // allowThirdPartyFixup
);
本当だったらもっと早く、Firefox 3.0になる前の時点でこういう事は済ませておくべきだったんだろうと思う(そのための「メジャーバージョン」でしょ?)。でもまあ、いつかはやらなきゃいけないことだ。新しい引数が追加されるというタイミングは、移行のいいきっかけではある。
ツリー型タブが入ってるとスターアイコンからブックマークの内容を編集できない、という報告を見て、そんなバカなこっちじゃちゃんと動いてるのに!と思って薄々そうなんじゃないかなあと思いながらよく報告を見てみたら、Tab Mix Plusと組み合わせた状態であると書いてあり、やっぱり……と思いながら両方を入れてみたら確かに問題が再現した。まあここまではよくある話。
エラーが起こっている箇所はエラーコンソールのメッセージから容易に特定できたんだけど、でもどう見てもそこでエラーになるはずがないという箇所でエラーになっていた。具体的には1つ前のエントリに書いたcreateContextualFragment()
の所。色々条件を変えて調べてみたら、どんな簡単なソースを渡した場合でもcreateContextualFragment()
の返り値が常にnullになっているようだった(Firefox 3.0.13でのテスト結果)。で、さらに条件を変えながら色々試して分かったのは、そもそもツリー型タブと組み合わせなくても、Tab Mix Plusが入ってるだけでcreateContextualFragment()
が全然使い物にならなくなる(Firefox 3.0.xや3.5では常にnullが返り、Trunkでは常に例外が発生する)ということだった。
さすがにこれは何かおかしいと思って、エラーコンソールに表示されるエラーをよく見ると、XMLのパースエラーで「属性が二重に定義されている」とメッセージが出ている。それでピンと来てTab Mix Plusのソースを見てみたら、怪しい記述を見つけた。オーバーレイ用のXULドキュメントで、idがmain-windowであるwindow要素のオーバーレイ内容にXML名前空間宣言が含まれている、というものだ。もちろんこれはXML的に全く問題がないはずの記述なのだけれども、まさかと思いながらそこを書き換えてみたら、Tab Mix PlusがあってもcreateContextualFragment()
が失敗しなくなった。それで、ここが原因だと確信が持てたということで、Bugzillaにバグとして報告してみた。
条件がややこしい上に、一体どこが一番悪いのか分からなかったんだけど、一番表面上のトリガーになってるように見えてるのがDOM Traversal-Rangeだったので、そこのバグとして報告してある。
この問題を回避しようと思ったら、Tab Mix PlusのオーバーレイでXML名前空間宣言を書くのは本当のルート要素だけという風に書き換えるのが一番手っ取り早いんだけど……できれば本体(Gecko)の方を直してほしい所ではある。
先週1週間は夏休み取って家に缶詰でずっともえじら組のマンガ描いてたんだけど、その間大量にバグ報告が来てたのをずっと見て見ぬふりしてたのを今週になってやっと修正した。
ブックマークフォルダの内容をタブで開けなくなるという問題はFirefox 3.0.xでのみ発生する問題で、原因はJavaScriptコードモジュールのPlacesUtilsにFirefox 3.5から追加された機能をそうとは知らずに使ってしまっていたせいだった。
あとブックマーク周りの変更が結構ボロボロだったのをだいぶ直した。特にスターアイコンのことは自分であんまり使わないからすっかり忘れてて、直すのに難儀した。Firefox自身がeditBookmarkOverlay.xulを動的に読み込んでいて、そのeditBookmarkOverlay.xulに対してツリー型タブがオーバーレイを適用しているために問題が……とか、とてもバッドノウハウくさい。結局、XULオーバーレイでどうこうするのは諦めてJavaScriptで動的にDOM要素を生成して挿入することにした。
var range = document.createRange();
range.selectNodeContents(container);
range.collapse(false);
range.insertNode(range.createContextualFragment(<![CDATA[
<row align="center" id="treestyletab-parent-row">
<label id="treestyletab-parent-label"
control="treestyletab-parent-menulist"/>
<menulist id="treestyletab-parent-menulist"
flex="1"
oncommand="TreeStyleTabBookmarksServiceEditable.onParentChange();">
<menupopup id="treestyletab-parent-popup">
<menuseparator id="treestyletab-parent-blank-item-separator"/>
<menuitem id="treestyletab-parent-blank-item"
value=""/>
</menupopup>
</menulist>
</row>
]]>.toString().replace(/^\s*|\s*$/g, '').replace(/>\s+</g, '><')));
range.detach();
こんな感じにしておけば、XULの「タグを書くだけでUIを作れる」という利点をそれほど殺さなくても済む……と思う。E4XのXMLオブジェクトを生成した物を既存のDOMツリーに直接組み込むことができれば話は早いんだけど、そういうことは無理っぽいので、createContextualFragment()
にしてる。ここではE4XのCDATAマーク区間をヒアドキュメント代わりに使ってるんだけど、文字列置換でタグの間の空白文字を消してるのと、toString()
をわざわざ書いていることに注意が必要。前者を忘れると要素ノードの間にいちいちテキストノードが生成されてややこしいことになるし、後者をを忘れるとStringクラスの物ではなくXMLオブジェクト自身の方のreplace()
メソッドが呼ばれてしまって文字列置換にならないので。
修正ついでに、ブックマーク項目の「親のタブ」を設定する機能について、もうちょっと自由に使えるように手を入れてみた。ツリー構造を書き換えるのと同時に、ツリーとして表示される時の順番に合わせてブックマークを自動的に並べ替えるようにした。