たまに18歳未満の人や心臓の弱い人にはお勧めできない情報が含まれることもあるかもしれない、甘くなくて酸っぱくてしょっぱいチラシの裏。RSSによる簡単な更新情報を利用したりすると、ハッピーになるかも知れませんしそうでないかも知れません。
の動向はもえじら組ブログで。
宣伝。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。 以下の特設サイトにて、単行本まんがでわかるLinux シス管系女子の試し読みが可能!
※今(2009年11月24日03:22)見たらリンク先エントリおよびWikiのページからTree Style Tabの項目が消されていた。このエントリおよびリンク先のコメントを見た人にはさっぱり意味不明でワケが分からないと思うので説明しておくと、リンク先のページにはどちらもタブ系のAll-in-One型アドオンが列挙されていて、初出時はその中にツリー型タブ(Tree Style Tab)もあったのです。
多機能オールインワンを否定して、タブをツリー表示するという事に特化した単機能のアドオンとして作ってきたつもりなのに、こういう所で俎上に載せられることが非常に心外且つショックで、でも実態からそう見られてしまう部分があるのは事実なので、悲しい思いをした。
ツリー型タブにあるツリーと関係ない機能は、開発当時にその機能だけを提供する単機能アドオンを見つけられなかったからだったり、あるいは、ツリーとの緊密な連携を考慮しないと上手く働かなさそうだったりという理由からやむなく加えた物なので、外せるものならどんどん外していきたいというのが僕の正直な考えです。
で、今日探してみたら選択範囲のリンクをタブで開く機能に特化したアドオン「Selection Links」(紹介記事)を見つけて、このアドオンによってツリー型タブの持つ機能は完全に代替できる(実際にはTSTはリファラを渡すけどこのアドオンはリファラを渡さない等の違いがあるんだけどそこは気にしない)事が分かったので、当該機能をツリー型タブの次のバージョンから廃止することにしました。
あとついでに「新しいタブを開く」ボタンの表示・非表示を制御するオプションも廃止します。これはuserChrome.cssに以下のように書けば簡単に代替できるので。
.tabs-newtab-button {
display: none !important;
}
追記。意に反する紹介のされ方をした僕がキレて当てつけのために機能の削除を決意した、という風に解釈されるぜと忠告を受けたのでここにも明記しておきますが、今回の決定はそーいう下らない目的のためでは決してないです。機能の削除は「ユーザを困らせるため」ではなく、「何のためのアドオンなのかというコンセプトをはっきりさせることによってユーザ体験を向上するため(あと、コードを減らしてメンテナンス性を高めるため)」という、前向きな理由に基づいています。
確かに事実として関係ない機能まで取り込んで肥大化してしまっている場合はありますが、今回言及した表のようにそれを気付かせてくれる存在に出会えば、肥大化した部分はちゃんと切り捨てて本来あるべき姿に戻したい、と思っています。これは昨日今日で決めたようなことではなく、ツリー型タブ(およびその他の「TBE代替」のためのアドオン)を開発した当初からの方針です。
その辺の話に触れた過去のエントリがいくつかありますので、併せてご覧頂ければ幸いです。
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」に変更して下さい。
表題の件について、どーも実際に表示されてる内容とセッション情報とが食い違ってるケースがあるようだ。
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
というプロパティが使われるようになっていたので、それにあわせてサンプルコードを修正した。
ツリー型タブを使ってタブバーを左または右に表示してる時に、サイドバーをタブの下に表示したい(コンテンツ領域の左に「タブバー」と「サイドバー」が縦に並ぶようにしたい)、という要望を何度か受け取っている。
「タブをツリー表示する」という基本コンセプトとはあまり関係がない機能なので、機能として付け加えるつもりはない。しかし、実装するとしたらツリー型タブに激しく依存するはずなので、全くフォローしないという訳にもいかなさそう。
ということでuserChrome.cssでなんとかできないか考えてみた。
#sidebar-box {
bottom: 16px;
display: -moz-box;
position: fixed;
-moz-box-orient: vertical;
}
sidebarheader {
width: 208px;
}
#sidebar-box,
#sidebar {
width: 250px;
}
#sidebar {
height: 300px;
}
.tabbrowser-strip {
margin-bottom: 316px;
}
サイズ固定になってしまうけど、こうするとそれらしくなった(タブバーが左にあって、250ピクセルくらいの幅である場合)。
フレキシブルにやろうと思ったら、それなりのコードを書かなきゃいかんよなあ。誰かやってくんないかなあ。
追記。アドオンにしました。
ツリー型タブが入ってるとスターアイコンからブックマークの内容を編集できない、という報告を見て、そんなバカなこっちじゃちゃんと動いてるのに!と思って薄々そうなんじゃないかなあと思いながらよく報告を見てみたら、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()
メソッドが呼ばれてしまって文字列置換にならないので。
修正ついでに、ブックマーク項目の「親のタブ」を設定する機能について、もうちょっと自由に使えるように手を入れてみた。ツリー構造を書き換えるのと同時に、ツリーとして表示される時の順番に合わせてブックマークを自動的に並べ替えるようにした。
ツリー型タブ 0.8.2009073101/02で、「このツリーをブックマーク」や「すべてタブをブックマーク」した時に、ツリーの構造を含めてブックマークを保存するようにしてみた。だいぶ前から要望を受けてて、「確かにそうするべきだよなあ」とは思ってたんだけど、どうやって実現すればいいかで悩んでた。でもFirefox 2のサポートを切ったことによって、API経由でPlacesデータベースに色んな情報を簡単に保存できるようになったので、思い切って実装してみた。
他のアドオンからもこの機能を使えるように、APIを用意してある。複数のタブからブックマークを作成する場合、以下のように、PlacesUIUtils.showMinimalAddMultiBookmarkUI()
でブックマークの追加を行う前後にツリー型タブのAPIを呼んでやると、タブのツリー構造がブックマークに保存される。
var tabs = Array.slice(gBrowser.mTabContainer.childNodes);
var isTSTBookmarksTreeStructureAvailable = (
'TreeStyleTabBookmarksService' in window &&
'beginAddBookmarksFromTabs' in TreeStyleTabBookmarksService &&
'endAddBookmarksFromTabs' in TreeStyleTabBookmarksService
);
if (isTSTBookmarksTreeStructureAvailable)
TreeStyleTabBookmarksService.beginAddBookmarksFromTabs(tabs);
try {
PlacesUIUtils.showMinimalAddMultiBookmarkUI(tabs.map(function(aTab) { return aTab.linkedBrowser.currentURI; }));
}
catch(e) {
}
if (isTSTBookmarksTreeStructureAvailable)
TreeStyleTabBookmarksService.endAddBookmarksFromTabs();
このAPIはマルチプルタブハンドラでもさっそく使ってる。
やってることはどういう事かというと……
TreeStyleTabBookmarksService.beginAddBookmarksFromTabs()
の方では、ブックマークされる予定のタブのツリー構造をシリアライズして内部に保持した上で、ブックマークの監視を開始する。PlacesUIUtils.showMinimalAddMultiBookmarkUI()
で複数のブックマーク項目が新たに作成される。この時、TreeStyleTabBookmarksService
はブックマークの追加を監視していて、新しく作られたブックマークのIDを内部に保持する。TreeStyleTabBookmarksService.endAddBookmarksFromTabs()
の中で、追加されたブックマークと元になったタブとを対応させ、ツリー構造の情報(親のタブにあたるブックマーク項目はどれか、という情報)を、ブックマークのアノテーションとして保存する。この時、タブの数と作られたブックマークの数とが一致しない場合(ブックマークの追加がキャンセルされたとか、未知の機能によってタブと関係ないブックマークが同時に作成されたとか)は想定外のエラーということで、何もせず終了する。とりあえず一番簡単なやり方で実装してみたので、保存した後のブックマークの順番や親子関係をいじくり回すとちょっと変なことになる。一応、そんなに大きな問題は起こらないで見た目上は何となく自然な形に収まるように、と工夫はしてみたんだけど……どうだろう。
保存された「どのタブが親か?」という情報は、ブックマークのプロパティから編集できるようにしてある。親を付け替えられるようにしてみたけど、横着してるのでちょっと制限が厳しい。そのうち、親を付け替えたらそれに応じてブックマーク項目自体の親フォルダ内での位置も自動的に入れ替えるようにでもしてみようかなー。
掲示板で書いた事を改めてここにもまとめてみる。
この2つの要望にはどちらも応えるつもりはない、というよりも、安直に応えてはいけない種類の要望だと思ってる。
あらかじめ言っておくと、過去の「タブブラウザ拡張」ではこのどちらも応えていた。いや、要望に応えたと言うよりは多分、自分から進んでそうしてしまっていた。でも今の自分の考えでは、そうするべきではなかったと考えている。なので、もうしない。
そもそもを言えば、こういう要望が出てきてしまっているということが「失敗」の何よりの証拠だと僕は考えている。「何故、そういう要望が出るのか?」「どうしてそうして欲しいと思うのか?」そこを考えないといけないと思う。
前者の要望は、色んなアドオンがそういう項目を「ツール」メニューにどんどん加えていったらメニューが一杯になってしまう!ということを見過ごしている。絶対に、多分別の人が「ツールメニューの中の項目は使わないので、非表示にするオプションを加えて欲しい」って言ってくるだろう。そしたらまたそれに応えるの?
後者の要望は、タブの一覧のリストやサムネイル一覧などからそのタブを選んだという風な、「本当にそのタブにフォーカスしたかった場合」にはどうすればいいのか?ということを考慮に入れずに安直に対応すると、泥沼に嵌ってしまう。不用意にそのオプションをいじってしまった人が「フォーカスしたいタブにフォーカスしてくれない!」と(自分がそう設定したからだと言う事にも気づかず)「バグ報告」してくるだろうし、それにまた安直に応えて「じゃあ、そういうケースだけは特別に、閉じられたツリーの中のタブにもフォーカスできるようにしよう」なんて後手後手の対応を続けていたら、どこまでいってもきりがない。
そういう安直な対応を繰り返した果てにあるのが、かつてのタブブラウザ拡張であり、今のTab Mix Plusであると、僕は考えてる。「多機能で凄い、良い物だ」と人は言うけれども、今の僕にはこれらは「考えることを放棄し続けた結果、肥大化の一途を辿った末路だ」という風に見えてる。既にツリー型タブもそうなりつつあると僕は思ってるので、今後は「設定項目を減らしていく」方向にシフトしたいくらいだ。
アプリケーションを作る立場の人は、要望として口に出された言葉の裏にある本当のニーズを読み取る努力をしておかないと、最終的には自分で自分の首を絞めることになると思う。「その方が格好よさそう」とか「その方が賢そう」とかのあまり意味のない自己満足なこだわりだろ、という風に切り捨てないで、自分自身の身を守るための現実的な対策のひとつとして、実践してほしいなと思う。
そしてその結果、それをエンドユーザとして使う立場に僕がなった時に、またハッピーになれるわけだ。
つまり「情けは人のためならず」とか「自業自得」とかそういう話。