たまに18歳未満の人や心臓の弱い人にはお勧めできない情報が含まれることもあるかもしれない、甘くなくて酸っぱくてしょっぱいチラシの裏。RSSによる簡単な更新情報を利用したりすると、ハッピーになるかも知れませんしそうでないかも知れません。
の動向はもえじら組ブログで。
宣伝。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。 以下の特設サイトにて、単行本まんがでわかるLinux シス管系女子の試し読みが可能!
前のエントリではoperationHistory.jsの基本的な使い方を説明しましたが、次は、これを使うにあたって理解しておかないといけないポイントを解説しようと思います。
UIに対して行うアンドゥ可能にしたい操作の中には、他のアンドゥ可能にしたい操作を内部で呼び出すものがあるでしょう。例えばブックマークフォルダの内容をまとめてタブで開く時などに使われるtabbrowserのloadTabs()
メソッドは、新しい空のタブを開くaddTab()
メソッドを内部で呼び出しています。「新しいタブを開く」操作と「ブックマークフォルダの内容をまとめてタブで開く」操作をアンドゥ可能にする際は、これらのメソッドに対してそれぞれアンドゥ・リドゥの処理を定義することになります。
このように「アンドゥ可能な処理」同士が入れ子になっている時、operationHistoryは、それらすべてのアンドゥ可能な処理について、実行が始まった順番通りに履歴に登録を行います。例えば「loadTabs()
で3つのタブを開く」という場面では、以下のように処理が行われます。
loadTabs()
実行。アンドゥ可能な操作Aが始まる。
addTab()
実行。アンドゥ可能な操作Bが始まる。
addTab()
実行。アンドゥ可能な操作Cが始まる。
addTab()
実行。アンドゥ可能な操作Dが始まる。
この時の操作B~Dは、操作Aが完了する前に、操作Aの中から呼び出されています。そのため、これらに対応する履歴項目B'~D'は、完了していない操作Aに対応する履歴項目A'の子項目として登録されます。つまり、このような親子関係が形成されます。
他の履歴項目の子項目として登録された履歴項目は、親となる項目の一部として扱われます。子項目になった履歴項目は、アンドゥ可能な操作の履歴の一覧には登場せず、「最大100回までアンドゥ可能」といった場合、子項目の数はそのカウントに含まれないことになります。
この履歴項目A'に対してアンドゥを指示すると、operationHistoryは以下の順で処理を行います。
onUndo()
onUndo()
onUndo()
onUndo()
この時の実行順序は項目の登録時の逆順であることに注意して下さい。なお、後で解説しますが、遅延処理のための仕組みによってこの実行順序は保証されます。前の項目のonUndo()
が終わる前に次の項目のonUndo()
が始まるという事はありません。
逆に履歴項目A'に対してリドゥを指示すると、operationHistoryは以下の順で処理を行います。
onRedo()
onRedo()
onRedo()
onRedo()
今度は、履歴項目の登録順の通りであることに注意して下さい。こちらについても、実行順序はこの通りに保証されます。
操作をアンドゥできるようにする際は、操作の中から呼び出している別の操作がアンドゥ可能である場合、上記の実行順の事を念頭に置いてアンドゥ・リドゥ用の処理を記述する必要があります。例えば上記の例であれば、アンドゥの際は以下の順でアンドゥのための処理が進みます。
これを見ると、4番目の項目の時点ではもう何もする必要が無いということが分かります。もし4の時点で、3つのタブを開いたのでそのアンドゥ操作としてタブを3つ閉じようとしても、閉じる対象のタブが存在しないためエラーになってしまいます。他のアンドゥ可能な操作を内部で呼び出す操作については、アンドゥ・リドゥの内容が重複しないように注意してください。
Undo Tab Operationsの核であるoperationHistory.jsは、タブに限らずいろんな操作に対してアンドゥ・リドゥを実装しやすくするための汎用のライブラリとして設計しています(一応)。こいつの使い方を、自分の頭の中の整理も兼ねて少しずつ解説していこうと思います。
まず、読み込みの方法。以下のようにJavaScriptのファイルを読み込ませるだけでOKです。
<script src="lib/operationHistory.js"
type="application/javascript"/>
複数のアドオンでこのライブラリを読み込んでいる場合、最もリビジョンの新しい物が使われます。今後はAPIは変えないor後方互換を維持していくつもりなので、使う方はあんまり気にしないで使えるはずです。
operationHistoryの各機能には window['piro.sakura.ne.jp'].operationHistory
でアクセスできます。以下の説明ではサンプルコードを短くするために、 OH
という変数でこれを参照しているものとします。
var OH = window['piro.sakura.ne.jp']
.operationHistory;
operationHistoryを使って任意の処理をアンドゥ・リドゥ可能にしたい時は、その処理を以下のように実行するようにします。
OH.doOperation(function() {
// 任意の処理
});
doOperation()
の引数に関数を渡すと、それがその場で実行されます。実行時のthis
はOH
自身を指していますが、普通に分かりにくいんで、クロージャを使うなり何なりして好きなように書くといいと思います。
var MyAddon = {
myFeature : function() {
var self = this;
OH.doOperation(function() {
self.myInternalMethod();
});
},
myInternalMethod : function() {
// 何かの処理
}
};
で、これだけだとまだアンドゥ・リドゥはできません。doOperation()
に対して以下の引数をさらに指定してやる必要があります。
"MyAddonOperations"
とかそんな感じで好きに名前を付けて下さい。省略すると、履歴の対象のウィンドウが指定されている場合は "global"
、そうでなければ "window"
になります。"TabbarOperations"
という名前を指定してます。window
です。省略すると、ウィンドウ単位の履歴ではなく、クロスウィンドウな単一の履歴となります。が、動作が怪しいので今の所はウィンドウ単位での使い方だけ推奨しておきます。やり直し可能にしたい処理自体と合わせると、doOperation()
は最大で4つまでの引数を取るという事ですね。引数は全部型が違う(関数、文字列、DOMWindow、オブジェクト)ので、doOperation()
はそれらを受け取った後に、どれがどれなのかを自動的に判別します。なので引数はどの順番で指定しても構いません。
実際のアンドゥ・リドゥ処理は、履歴エントリになるオブジェクトのプロパティとして関数で定義します。
var entry = {
// 内部名
name : "undotab-addTab",
// メニュー等に表示する「やり直す処理の名前」
label : "タブを開く",
// アンドゥ時に実行される内容
onUndo : function(aParams) {
gBrowser.removeTab(this.tab);
},
// リドゥ時に実行される内容
onRedo : function(aParams) {
this.tab = gBrowser.addTab();
},
// 以下、任意のプロパティを好きなようにどうぞ
tab : null
};
OH.doOperation(
function() { // やり直し可能にする処理
entry.tab = gBrowser.addTab();
},
'MyAddonOperations', // 履歴名
window, // 処理対象ウィンドウ
entry // 履歴エントリ
);
onUndo
という名前のプロパティで関数を定義しておくとアンドゥ時にそれが実行されます。onRedo
はリドゥの時に実行されます。どちらもthis
は履歴エントリのオブジェクト自身になりますので、まあ見た通りで分かりやすいんじゃないかと思います。クロージャ使って書いても全然構いません。
name
、label
は、文字列で好きなように名前を付けて下さい。定義しなくても使えますが、デバッグの時にはあると便利ですし、DOMイベントを使った記法(後で解説します)の時には無いと困ります。
上記のようにして履歴に登録した処理は、以下のようにしてアンドゥ・リドゥできます。
// アンドゥ
OH.undo('MyAddonOperation', window);
// リドゥ
OH.redo('MyAddonOperation', window);
undo()
とredo()
の引数には、doOperation()
に対して指定したものと同じ履歴名と処理対象のウィンドウを渡します(こちらも引数の指定順は任意です)。ここではまだ「タブを開く操作」しか書いていませんが、同じ履歴名で「タブを閉じる操作」「タブを移動する操作」などに対してそれぞれアンドゥ・リドゥの処理を書いてやれば、線形にそれらをアンドゥ・リドゥできるようになります。
また、「戻る」「進む」のドロップダウンメニューで項目を指定してそこまで一気に飛ぶのと同じように、goToIndex()
で履歴項目のインデックスを指定してそこまで一気にアンドゥする・リドゥする事もできます。
// 現在の位置を得る
var history = OH.getHistory('MyAddonOperation', window);
var current = history.index;
OH.goToIndex(current-3, 'MyAddonOperation', window);
getHistory()
は、登録済みの履歴項目の全エントリを格納したオブジェクトを取得するメソッドです。それで取得したオブジェクトのindex
プロパティで現在のフォーカス位置を得られるので、上の例ではそこから3つ手前に飛ぶ事になります。
ここまでの説明で既に疑問に思った人もいると思いますが、例えばこんな場合。
function NewTab() {
var entry = {
name : "undotab-addTab",
label : "タブを開く",
onUndo : function(aParams) {
gBrowser.removeTab(this.tab);
},
onRedo : function(aParams) {
this.tab = gBrowser.addTab();
gBrowser.selectedTab = this.tab;
},
tab : null
};
OH.doOperation(
function() {
entry.tab = gBrowser.addTab();
gBrowser.selectedTab = entry.tab;
},
'MyAddonOperations',
window,
entry
);
}
function MoveTab(aTab) {
var entry = {
name : "undotab-moveTab",
label : "タブを移動する",
onUndo : function(aParams) {
gBrowser.moveTabTo(this.tab, this.oldPosition);
},
onRedo : function(aParams) {
gBrowser.moveTabTo(this.tab, this.newPosition);
},
tab : null
};
OH.doOperation(
function() {
entry.tab = aTab;
entry.oldPosition = aTab._tPos;
gBrowser.moveTabTo(aTab, 3);
entry.newPosition = aTab._tPos;
},
'MyAddonOperations',
window,
entry
);
}
NewTab()
を実行→それで開かれたタブに対してMoveTab()
を実行→アンドゥ→アンドゥ→リドゥ→リドゥ という順に操作すると、2つ目の履歴項目のリドゥ時にタブが見つからないせいでエラーになってしまいます。こうならないように、処理対象の要素は固有のIDなどで識別してやらないといけません。
operationHistoryにはそのために、要素に対して一意なIDを自動的に付与してそれを元に要素を検索する仕組みがあります。先の例を安全に書くと、以下のようになります。
function NewTab() {
var entry = {
name : "undotab-addTab",
label : "タブを開く",
onUndo : function(aParams) {
var tab = OH.getElementById(this.tab,
gBrowser.mTabContainer);
gBrowser.removeTab(tab);
},
onRedo : function(aParams) {
var tab = gBrowser.addTab();
OH.setElementId(tab, this.tab)
gBrowser.selectedTab = tab;
},
tab : null
};
OH.doOperation(
function() {
var tab = gBrowser.addTab();
entry.tab = OH.getElementId(tab);
gBrowser.selectedTab = tab;
},
'MyAddonOperations',
window,
entry
);
}
function MoveTab(aTab) {
var entry = {
name : "undotab-moveTab",
label : "タブを移動する",
onUndo : function(aParams) {
var tab = OH.getElementById(this.tab,
gBrowser.mTabContainer);
gBrowser.moveTabTo(tab, this.oldPosition);
},
onRedo : function(aParams) {
var tab = OH.getElementById(this.tab,
gBrowser.mTabContainer);
gBrowser.moveTabTo(tab, this.newPosition);
},
tab : null
};
OH.doOperation(
function() {
entry.tab = OH.getElementId(aTab);
entry.oldPosition = aTab._tPos;
gBrowser.moveTabTo(aTab, 3);
entry.newPosition = aTab._tPos;
},
'MyAddonOperations',
window,
entry
);
}
getElementId()
は、要素に一意なIDが付いていなければ新しいIDを生成て設定した上でそのIDを、既にIDが付いていればその値を、文字列として返します。IDは普通のid属性ではなく別の属性名で保存されるので、通常の動作を破壊することはありません。
getElementById()
は、そのID文字列をキーとして要素を検索するメソッドです。tabbrowserの場合はタブなどの内部の要素は普通のdocument.getElementById()
等では取得できないのですが、getElementById()
はID名の文字列以外に要素ノードを渡すと、その要素の子孫だけを検索するようになります。
ここでは、タブを開く操作のリドゥにおいてsetElementId()
も使用しています。これは、既に生成されたID文字列を新しく復元された要素に付与することで、その要素を元の要素の代わりとして参照できるようにするためです。
とりあえず、まずはこの辺だけ解説しておきます。
Undo Tab Operationsの開発を通じて実装をこねくり回してた汎用のアンドゥ・リドゥ用のライブラリだけど、最低限必要そうな一通りの機能を実装し終えた……と思う。
最初はもっとコンパクトになるかなと思ってたんだけど、なんだかんだで膨らんで35KBちょいになった(2010年1月11日現在)。ライブラリの使い方は……Undo Tab Operationsのソース読んで実際の使われ方を見た方が話が早いかも。
以下、ライブラリの使い方の説明じゃなくてただの苦労話です。
当初は、単純に以下のようにしようと思ってた。
しかし実際やってみるまでもなく、これだと考慮しないといけないケースがあまりに多くなりすぎる。例えばタブを開く操作だけでも、「タブバー上のボタン」「ブックマーク」「リンク」等々色々ある。それら1つ1つに対してアンドゥ・リドゥの処理を定義していくのはさすがに無理がある。また、例えば「タブバーのボタンで新しいタブを開く処理に対応する関数」に対してアンドゥ・リドゥの処理を定義したとして、その関数が他の処理の中から呼ばれないという保証はどこにもないわけで、アンドゥ用の処理がかぶったら、タブが2つも3つも開き直されたり、その逆に2つも3つも閉じられたりしかねない。これは危険すぎる。
という事くらいはすぐに思いついたので、次にこんな風に考えた。
gBrowser.addTab()
やgBrowser.moveTabTo()
などの基本的な関数それぞれに対してアンドゥ・リドゥの処理を定義する。gBrowser.loadOneTab()
(内部でaddTab()
とmoveTabTo()
を呼んでいる)のような関数の存在も考慮して、1つの「やり直し可能な処理の単位」の中で行われたやり直し可能な処理はすべて、トップレベルの履歴項目に子供としてぶら下げる。
gBrowser.swapBrowsersAndCloseOther()
は場合によっては元のウィンドウを閉じてしまうため、ウィンドウを開き直す→ロード完了を待ってからタブを開き直す、という風な事をしないといけない。ウィンドウが開かれるまでの間に下位の項目(タブを開く、タブを閉じる等)のアンドゥ処理が走るとおかしな事になる。なので、下位の項目のアンドゥ処理はキャンセルして、swapBrowsersAndCloseOther()
のアンドゥ処理の中で全部完結させるようにする。例外を設けたのは、「前の履歴項目のアンドゥ処理の完了を待ってから次の履歴項目のアンドゥ処理を始める」という風な、非同期処理を考慮した仕組みを当初備えていなかったせい。なんでその仕組みを先に作らなかったのかというと、作るのがめんどかったからの一言に尽きる。
で、このような仕様で開発を進めて、Undo Tab Operationsについてはとりあえず素のFirefox上でならまともに使えるようになってきたかなあと思ったので、マルチプルタブハンドラとの連携に着手し始めた。そしたら破綻した。
例えばマルチプルタブハンドラは、タブ1つだけのドラッグ&ドロップでの移動を検知して選択されたタブ全部をその近くに移動するようになってるけど、これをアンドゥ可能にしようと思うと、そのアンドゥの処理が始まる前にUndo Tab Operationsによって行われるmoveTabTo()
のアンドゥでタブの並び順が変わってしまうので、最終的なタブの並び順がグチャグチャになってしまう。手っ取り早く解決しようと思うと、Undo Tab Operationsによって行われるタブの移動のアンドゥ処理は全部キャンセルして、マルチプルタブハンドラ側で面倒を見てやった方が、書くのは簡単なわけです。
しかし、そんなことをそれぞれのアドオンがやり始めたら、絶対にどっかで考慮漏れが起こるわけですよ。Undo Tab Operationsとマルチプルタブハンドラだけだったら問題が起こらなくても、そこにツリー型タブや他のアドオンが加わってくると、互いにアンドゥ処理の優先権の取り合いになってしまうのは目に見えてる。
そういう未来が予想できてしまったので、ついに観念して、非同期処理に真面目に対応することにした。で、結局以下のようになった。
addTab()
、removeTab()
など、それぞれの基本的な関数に対してアンドゥ・リドゥの処理を提供する。あとついでに、関数オブジェクトをそのまま履歴項目に使うようにすると履歴項目が増える度にクロージャが増えていってメモリリークの温床になりそうだなあと思ったので、DOMのイベントを監視することでも同じ事ができるようにAPIを整備した。Undo Tab Operations 0.2.2010011001以降ではこっちの方法を使ってそれぞれのアンドゥ・リドゥ処理を実装してある。
処理待ちにはJSDeferredを使ってもよかったんだけど、他のライブラリには依存させたくなかったので、JavaScript 1.7以降のジェネレータ・イテレータと継続関数で実現してみた。イベントオブジェクトのaEvent.wait()
で処理待ち状態になって、aEvent.continue()
で処理の完了を通知するというスタイルにしてある。
クロージャを使っても使わなくても、処理対象のタブを特定するには一意なIDが無いとどうにもならないなあと思ったので、汎用の「要素ノードに自動生成でIDを付与する」とかの機能もライブラリに含めることにした。これを使うことで、閉じられたタブに対応する開き直されたタブを確実に取得できるようになってる。
実際使ってみるとなかなか妙な感じですね。タブをウィンドウ外にドロップしてタブを切り離し→Shift-Ctrl-Zで元に戻す→Shit-Ctrl-Yでまた切り離す なんてことができて、キモくて面白いです。
Undo Tab Operationsの開発を通じて実装をこねくり回してた汎用のアンドゥ・リドゥ用のライブラリだけど、最低限必要そうな一通りの機能を実装し終えた……と思う。
最初はもっとコンパクトになるかなと思ってたんだけど、なんだかんだで膨らんで35KBちょいになった(2010年1月11日現在)。ライブラリの使い方は……Undo Tab Operationsのソース読んで実際の使われ方を見た方が話が早いかも。
以下、ライブラリの使い方の説明じゃなくてただの苦労話です。
当初は、単純に以下のようにしようと思ってた。
しかし実際やってみるまでもなく、これだと考慮しないといけないケースがあまりに多くなりすぎる。例えばタブを開く操作だけでも、「タブバー上のボタン」「ブックマーク」「リンク」等々色々ある。それら1つ1つに対してアンドゥ・リドゥの処理を定義していくのはさすがに無理がある。また、例えば「タブバーのボタンで新しいタブを開く処理に対応する関数」に対してアンドゥ・リドゥの処理を定義したとして、その関数が他の処理の中から呼ばれないという保証はどこにもないわけで、アンドゥ用の処理がかぶったら、タブが2つも3つも開き直されたり、その逆に2つも3つも閉じられたりしかねない。これは危険すぎる。
という事くらいはすぐに思いついたので、次にこんな風に考えた。
gBrowser.addTab()
やgBrowser.moveTabTo()
などの基本的な関数それぞれに対してアンドゥ・リドゥの処理を定義する。gBrowser.loadOneTab()
(内部でaddTab()
とmoveTabTo()
を呼んでいる)のような関数の存在も考慮して、1つの「やり直し可能な処理の単位」の中で行われたやり直し可能な処理はすべて、トップレベルの履歴項目に子供としてぶら下げる。
gBrowser.swapBrowsersAndCloseOther()
は場合によっては元のウィンドウを閉じてしまうため、ウィンドウを開き直す→ロード完了を待ってからタブを開き直す、という風な事をしないといけない。ウィンドウが開かれるまでの間に下位の項目(タブを開く、タブを閉じる等)のアンドゥ処理が走るとおかしな事になる。なので、下位の項目のアンドゥ処理はキャンセルして、swapBrowsersAndCloseOther()
のアンドゥ処理の中で全部完結させるようにする。例外を設けたのは、「前の履歴項目のアンドゥ処理の完了を待ってから次の履歴項目のアンドゥ処理を始める」という風な、非同期処理を考慮した仕組みを当初備えていなかったせい。なんでその仕組みを先に作らなかったのかというと、作るのがめんどかったからの一言に尽きる。
で、このような仕様で開発を進めて、Undo Tab Operationsについてはとりあえず素のFirefox上でならまともに使えるようになってきたかなあと思ったので、マルチプルタブハンドラとの連携に着手し始めた。そしたら破綻した。
例えばマルチプルタブハンドラは、タブ1つだけのドラッグ&ドロップでの移動を検知して選択されたタブ全部をその近くに移動するようになってるけど、これをアンドゥ可能にしようと思うと、そのアンドゥの処理が始まる前にUndo Tab Operationsによって行われるmoveTabTo()
のアンドゥでタブの並び順が変わってしまうので、最終的なタブの並び順がグチャグチャになってしまう。手っ取り早く解決しようと思うと、Undo Tab Operationsによって行われるタブの移動のアンドゥ処理は全部キャンセルして、マルチプルタブハンドラ側で面倒を見てやった方が、書くのは簡単なわけです。
しかし、そんなことをそれぞれのアドオンがやり始めたら、絶対にどっかで考慮漏れが起こるわけですよ。Undo Tab Operationsとマルチプルタブハンドラだけだったら問題が起こらなくても、そこにツリー型タブや他のアドオンが加わってくると、互いにアンドゥ処理の優先権の取り合いになってしまうのは目に見えてる。
そういう未来が予想できてしまったので、ついに観念して、非同期処理に真面目に対応することにした。で、結局以下のようになった。
addTab()
、removeTab()
など、それぞれの基本的な関数に対してアンドゥ・リドゥの処理を提供する。あとついでに、関数オブジェクトをそのまま履歴項目に使うようにすると履歴項目が増える度にクロージャが増えていってメモリリークの温床になりそうだなあと思ったので、DOMのイベントを監視することでも同じ事ができるようにAPIを整備した。Undo Tab Operations 0.2.2010011001以降ではこっちの方法を使ってそれぞれのアンドゥ・リドゥ処理を実装してある。
処理待ちにはJSDeferredを使ってもよかったんだけど、他のライブラリには依存させたくなかったので、JavaScript 1.7以降のジェネレータ・イテレータと継続関数で実現してみた。イベントオブジェクトのaEvent.wait()
で処理待ち状態になって、aEvent.continue()
で処理の完了を通知するというスタイルにしてある。
クロージャを使っても使わなくても、処理対象のタブを特定するには一意なIDが無いとどうにもならないなあと思ったので、汎用の「要素ノードに自動生成でIDを付与する」とかの機能もライブラリに含めることにした。これを使うことで、閉じられたタブに対応する開き直されたタブを確実に取得できるようになってる。
実際使ってみるとなかなか妙な感じですね。タブをウィンドウ外にドロップしてタブを切り離し→Shift-Ctrl-Zで元に戻す→Shit-Ctrl-Yでまた切り離す なんてことができて、キモくて面白いです。
Undo Tab Operationsの開発を通じて実装をこねくり回してた汎用のアンドゥ・リドゥ用のライブラリだけど、最低限必要そうな一通りの機能を実装し終えた……と思う。
最初はもっとコンパクトになるかなと思ってたんだけど、なんだかんだで膨らんで35KBちょいになった(2010年1月11日現在)。ライブラリの使い方は……Undo Tab Operationsのソース読んで実際の使われ方を見た方が話が早いかも。
以下、ライブラリの使い方の説明じゃなくてただの苦労話です。
当初は、単純に以下のようにしようと思ってた。
しかし実際やってみるまでもなく、これだと考慮しないといけないケースがあまりに多くなりすぎる。例えばタブを開く操作だけでも、「タブバー上のボタン」「ブックマーク」「リンク」等々色々ある。それら1つ1つに対してアンドゥ・リドゥの処理を定義していくのはさすがに無理がある。また、例えば「タブバーのボタンで新しいタブを開く処理に対応する関数」に対してアンドゥ・リドゥの処理を定義したとして、その関数が他の処理の中から呼ばれないという保証はどこにもないわけで、アンドゥ用の処理がかぶったら、タブが2つも3つも開き直されたり、その逆に2つも3つも閉じられたりしかねない。これは危険すぎる。
という事くらいはすぐに思いついたので、次にこんな風に考えた。
gBrowser.addTab()
やgBrowser.moveTabTo()
などの基本的な関数それぞれに対してアンドゥ・リドゥの処理を定義する。gBrowser.loadOneTab()
(内部でaddTab()
とmoveTabTo()
を呼んでいる)のような関数の存在も考慮して、1つの「やり直し可能な処理の単位」の中で行われたやり直し可能な処理はすべて、トップレベルの履歴項目に子供としてぶら下げる。
gBrowser.swapBrowsersAndCloseOther()
は場合によっては元のウィンドウを閉じてしまうため、ウィンドウを開き直す→ロード完了を待ってからタブを開き直す、という風な事をしないといけない。ウィンドウが開かれるまでの間に下位の項目(タブを開く、タブを閉じる等)のアンドゥ処理が走るとおかしな事になる。なので、下位の項目のアンドゥ処理はキャンセルして、swapBrowsersAndCloseOther()
のアンドゥ処理の中で全部完結させるようにする。例外を設けたのは、「前の履歴項目のアンドゥ処理の完了を待ってから次の履歴項目のアンドゥ処理を始める」という風な、非同期処理を考慮した仕組みを当初備えていなかったせい。なんでその仕組みを先に作らなかったのかというと、作るのがめんどかったからの一言に尽きる。
で、このような仕様で開発を進めて、Undo Tab Operationsについてはとりあえず素のFirefox上でならまともに使えるようになってきたかなあと思ったので、マルチプルタブハンドラとの連携に着手し始めた。そしたら破綻した。
例えばマルチプルタブハンドラは、タブ1つだけのドラッグ&ドロップでの移動を検知して選択されたタブ全部をその近くに移動するようになってるけど、これをアンドゥ可能にしようと思うと、そのアンドゥの処理が始まる前にUndo Tab Operationsによって行われるmoveTabTo()
のアンドゥでタブの並び順が変わってしまうので、最終的なタブの並び順がグチャグチャになってしまう。手っ取り早く解決しようと思うと、Undo Tab Operationsによって行われるタブの移動のアンドゥ処理は全部キャンセルして、マルチプルタブハンドラ側で面倒を見てやった方が、書くのは簡単なわけです。
しかし、そんなことをそれぞれのアドオンがやり始めたら、絶対にどっかで考慮漏れが起こるわけですよ。Undo Tab Operationsとマルチプルタブハンドラだけだったら問題が起こらなくても、そこにツリー型タブや他のアドオンが加わってくると、互いにアンドゥ処理の優先権の取り合いになってしまうのは目に見えてる。
そういう未来が予想できてしまったので、ついに観念して、非同期処理に真面目に対応することにした。で、結局以下のようになった。
addTab()
、removeTab()
など、それぞれの基本的な関数に対してアンドゥ・リドゥの処理を提供する。あとついでに、関数オブジェクトをそのまま履歴項目に使うようにすると履歴項目が増える度にクロージャが増えていってメモリリークの温床になりそうだなあと思ったので、DOMのイベントを監視することでも同じ事ができるようにAPIを整備した。Undo Tab Operations 0.2.2010011001以降ではこっちの方法を使ってそれぞれのアンドゥ・リドゥ処理を実装してある。
処理待ちにはJSDeferredを使ってもよかったんだけど、他のライブラリには依存させたくなかったので、JavaScript 1.7以降のジェネレータ・イテレータと継続関数で実現してみた。イベントオブジェクトのaEvent.wait()
で処理待ち状態になって、aEvent.continue()
で処理の完了を通知するというスタイルにしてある。
クロージャを使っても使わなくても、処理対象のタブを特定するには一意なIDが無いとどうにもならないなあと思ったので、汎用の「要素ノードに自動生成でIDを付与する」とかの機能もライブラリに含めることにした。これを使うことで、閉じられたタブに対応する開き直されたタブを確実に取得できるようになってる。
実際使ってみるとなかなか妙な感じですね。タブをウィンドウ外にドロップしてタブを切り離し→Shift-Ctrl-Zで元に戻す→Shit-Ctrl-Yでまた切り離す なんてことができて、キモくて面白いです。
Undo Tab Operationsの開発を通じて実装をこねくり回してた汎用のアンドゥ・リドゥ用のライブラリだけど、最低限必要そうな一通りの機能を実装し終えた……と思う。
最初はもっとコンパクトになるかなと思ってたんだけど、なんだかんだで膨らんで35KBちょいになった(2010年1月11日現在)。ライブラリの使い方は……Undo Tab Operationsのソース読んで実際の使われ方を見た方が話が早いかも。
以下、ライブラリの使い方の説明じゃなくてただの苦労話です。
当初は、単純に以下のようにしようと思ってた。
しかし実際やってみるまでもなく、これだと考慮しないといけないケースがあまりに多くなりすぎる。例えばタブを開く操作だけでも、「タブバー上のボタン」「ブックマーク」「リンク」等々色々ある。それら1つ1つに対してアンドゥ・リドゥの処理を定義していくのはさすがに無理がある。また、例えば「タブバーのボタンで新しいタブを開く処理に対応する関数」に対してアンドゥ・リドゥの処理を定義したとして、その関数が他の処理の中から呼ばれないという保証はどこにもないわけで、アンドゥ用の処理がかぶったら、タブが2つも3つも開き直されたり、その逆に2つも3つも閉じられたりしかねない。これは危険すぎる。
という事くらいはすぐに思いついたので、次にこんな風に考えた。
gBrowser.addTab()
やgBrowser.moveTabTo()
などの基本的な関数それぞれに対してアンドゥ・リドゥの処理を定義する。gBrowser.loadOneTab()
(内部でaddTab()
とmoveTabTo()
を呼んでいる)のような関数の存在も考慮して、1つの「やり直し可能な処理の単位」の中で行われたやり直し可能な処理はすべて、トップレベルの履歴項目に子供としてぶら下げる。
gBrowser.swapBrowsersAndCloseOther()
は場合によっては元のウィンドウを閉じてしまうため、ウィンドウを開き直す→ロード完了を待ってからタブを開き直す、という風な事をしないといけない。ウィンドウが開かれるまでの間に下位の項目(タブを開く、タブを閉じる等)のアンドゥ処理が走るとおかしな事になる。なので、下位の項目のアンドゥ処理はキャンセルして、swapBrowsersAndCloseOther()
のアンドゥ処理の中で全部完結させるようにする。例外を設けたのは、「前の履歴項目のアンドゥ処理の完了を待ってから次の履歴項目のアンドゥ処理を始める」という風な、非同期処理を考慮した仕組みを当初備えていなかったせい。なんでその仕組みを先に作らなかったのかというと、作るのがめんどかったからの一言に尽きる。
で、このような仕様で開発を進めて、Undo Tab Operationsについてはとりあえず素のFirefox上でならまともに使えるようになってきたかなあと思ったので、マルチプルタブハンドラとの連携に着手し始めた。そしたら破綻した。
例えばマルチプルタブハンドラは、タブ1つだけのドラッグ&ドロップでの移動を検知して選択されたタブ全部をその近くに移動するようになってるけど、これをアンドゥ可能にしようと思うと、そのアンドゥの処理が始まる前にUndo Tab Operationsによって行われるmoveTabTo()
のアンドゥでタブの並び順が変わってしまうので、最終的なタブの並び順がグチャグチャになってしまう。手っ取り早く解決しようと思うと、Undo Tab Operationsによって行われるタブの移動のアンドゥ処理は全部キャンセルして、マルチプルタブハンドラ側で面倒を見てやった方が、書くのは簡単なわけです。
しかし、そんなことをそれぞれのアドオンがやり始めたら、絶対にどっかで考慮漏れが起こるわけですよ。Undo Tab Operationsとマルチプルタブハンドラだけだったら問題が起こらなくても、そこにツリー型タブや他のアドオンが加わってくると、互いにアンドゥ処理の優先権の取り合いになってしまうのは目に見えてる。
そういう未来が予想できてしまったので、ついに観念して、非同期処理に真面目に対応することにした。で、結局以下のようになった。
addTab()
、removeTab()
など、それぞれの基本的な関数に対してアンドゥ・リドゥの処理を提供する。あとついでに、関数オブジェクトをそのまま履歴項目に使うようにすると履歴項目が増える度にクロージャが増えていってメモリリークの温床になりそうだなあと思ったので、DOMのイベントを監視することでも同じ事ができるようにAPIを整備した。Undo Tab Operations 0.2.2010011001以降ではこっちの方法を使ってそれぞれのアンドゥ・リドゥ処理を実装してある。
処理待ちにはJSDeferredを使ってもよかったんだけど、他のライブラリには依存させたくなかったので、JavaScript 1.7以降のジェネレータ・イテレータと継続関数で実現してみた。イベントオブジェクトのaEvent.wait()
で処理待ち状態になって、aEvent.continue()
で処理の完了を通知するというスタイルにしてある。
クロージャを使っても使わなくても、処理対象のタブを特定するには一意なIDが無いとどうにもならないなあと思ったので、汎用の「要素ノードに自動生成でIDを付与する」とかの機能もライブラリに含めることにした。これを使うことで、閉じられたタブに対応する開き直されたタブを確実に取得できるようになってる。
実際使ってみるとなかなか妙な感じですね。タブをウィンドウ外にドロップしてタブを切り離し→Shift-Ctrl-Zで元に戻す→Shit-Ctrl-Yでまた切り離す なんてことができて、キモくて面白いです。
Undo Tab Operationsの開発を通じて実装をこねくり回してた汎用のアンドゥ・リドゥ用のライブラリだけど、最低限必要そうな一通りの機能を実装し終えた……と思う。
最初はもっとコンパクトになるかなと思ってたんだけど、なんだかんだで膨らんで35KBちょいになった(2010年1月11日現在)。ライブラリの使い方は……Undo Tab Operationsのソース読んで実際の使われ方を見た方が話が早いかも。
以下、ライブラリの使い方の説明じゃなくてただの苦労話です。
当初は、単純に以下のようにしようと思ってた。
しかし実際やってみるまでもなく、これだと考慮しないといけないケースがあまりに多くなりすぎる。例えばタブを開く操作だけでも、「タブバー上のボタン」「ブックマーク」「リンク」等々色々ある。それら1つ1つに対してアンドゥ・リドゥの処理を定義していくのはさすがに無理がある。また、例えば「タブバーのボタンで新しいタブを開く処理に対応する関数」に対してアンドゥ・リドゥの処理を定義したとして、その関数が他の処理の中から呼ばれないという保証はどこにもないわけで、アンドゥ用の処理がかぶったら、タブが2つも3つも開き直されたり、その逆に2つも3つも閉じられたりしかねない。これは危険すぎる。
という事くらいはすぐに思いついたので、次にこんな風に考えた。
gBrowser.addTab()
やgBrowser.moveTabTo()
などの基本的な関数それぞれに対してアンドゥ・リドゥの処理を定義する。gBrowser.loadOneTab()
(内部でaddTab()
とmoveTabTo()
を呼んでいる)のような関数の存在も考慮して、1つの「やり直し可能な処理の単位」の中で行われたやり直し可能な処理はすべて、トップレベルの履歴項目に子供としてぶら下げる。
gBrowser.swapBrowsersAndCloseOther()
は場合によっては元のウィンドウを閉じてしまうため、ウィンドウを開き直す→ロード完了を待ってからタブを開き直す、という風な事をしないといけない。ウィンドウが開かれるまでの間に下位の項目(タブを開く、タブを閉じる等)のアンドゥ処理が走るとおかしな事になる。なので、下位の項目のアンドゥ処理はキャンセルして、swapBrowsersAndCloseOther()
のアンドゥ処理の中で全部完結させるようにする。例外を設けたのは、「前の履歴項目のアンドゥ処理の完了を待ってから次の履歴項目のアンドゥ処理を始める」という風な、非同期処理を考慮した仕組みを当初備えていなかったせい。なんでその仕組みを先に作らなかったのかというと、作るのがめんどかったからの一言に尽きる。
で、このような仕様で開発を進めて、Undo Tab Operationsについてはとりあえず素のFirefox上でならまともに使えるようになってきたかなあと思ったので、マルチプルタブハンドラとの連携に着手し始めた。そしたら破綻した。
例えばマルチプルタブハンドラは、タブ1つだけのドラッグ&ドロップでの移動を検知して選択されたタブ全部をその近くに移動するようになってるけど、これをアンドゥ可能にしようと思うと、そのアンドゥの処理が始まる前にUndo Tab Operationsによって行われるmoveTabTo()
のアンドゥでタブの並び順が変わってしまうので、最終的なタブの並び順がグチャグチャになってしまう。手っ取り早く解決しようと思うと、Undo Tab Operationsによって行われるタブの移動のアンドゥ処理は全部キャンセルして、マルチプルタブハンドラ側で面倒を見てやった方が、書くのは簡単なわけです。
しかし、そんなことをそれぞれのアドオンがやり始めたら、絶対にどっかで考慮漏れが起こるわけですよ。Undo Tab Operationsとマルチプルタブハンドラだけだったら問題が起こらなくても、そこにツリー型タブや他のアドオンが加わってくると、互いにアンドゥ処理の優先権の取り合いになってしまうのは目に見えてる。
そういう未来が予想できてしまったので、ついに観念して、非同期処理に真面目に対応することにした。で、結局以下のようになった。
addTab()
、removeTab()
など、それぞれの基本的な関数に対してアンドゥ・リドゥの処理を提供する。あとついでに、関数オブジェクトをそのまま履歴項目に使うようにすると履歴項目が増える度にクロージャが増えていってメモリリークの温床になりそうだなあと思ったので、DOMのイベントを監視することでも同じ事ができるようにAPIを整備した。Undo Tab Operations 0.2.2010011001以降ではこっちの方法を使ってそれぞれのアンドゥ・リドゥ処理を実装してある。
処理待ちにはJSDeferredを使ってもよかったんだけど、他のライブラリには依存させたくなかったので、JavaScript 1.7以降のジェネレータ・イテレータと継続関数で実現してみた。イベントオブジェクトのaEvent.wait()
で処理待ち状態になって、aEvent.continue()
で処理の完了を通知するというスタイルにしてある。
クロージャを使っても使わなくても、処理対象のタブを特定するには一意なIDが無いとどうにもならないなあと思ったので、汎用の「要素ノードに自動生成でIDを付与する」とかの機能もライブラリに含めることにした。これを使うことで、閉じられたタブに対応する開き直されたタブを確実に取得できるようになってる。
実際使ってみるとなかなか妙な感じですね。タブをウィンドウ外にドロップしてタブを切り離し→Shift-Ctrl-Zで元に戻す→Shit-Ctrl-Yでまた切り離す なんてことができて、キモくて面白いです。
Undo Tab Operationsの開発を通じて実装をこねくり回してた汎用のアンドゥ・リドゥ用のライブラリだけど、最低限必要そうな一通りの機能を実装し終えた……と思う。
最初はもっとコンパクトになるかなと思ってたんだけど、なんだかんだで膨らんで35KBちょいになった(2010年1月11日現在)。ライブラリの使い方は……Undo Tab Operationsのソース読んで実際の使われ方を見た方が話が早いかも。
以下、ライブラリの使い方の説明じゃなくてただの苦労話です。
当初は、単純に以下のようにしようと思ってた。
しかし実際やってみるまでもなく、これだと考慮しないといけないケースがあまりに多くなりすぎる。例えばタブを開く操作だけでも、「タブバー上のボタン」「ブックマーク」「リンク」等々色々ある。それら1つ1つに対してアンドゥ・リドゥの処理を定義していくのはさすがに無理がある。また、例えば「タブバーのボタンで新しいタブを開く処理に対応する関数」に対してアンドゥ・リドゥの処理を定義したとして、その関数が他の処理の中から呼ばれないという保証はどこにもないわけで、アンドゥ用の処理がかぶったら、タブが2つも3つも開き直されたり、その逆に2つも3つも閉じられたりしかねない。これは危険すぎる。
という事くらいはすぐに思いついたので、次にこんな風に考えた。
gBrowser.addTab()
やgBrowser.moveTabTo()
などの基本的な関数それぞれに対してアンドゥ・リドゥの処理を定義する。gBrowser.loadOneTab()
(内部でaddTab()
とmoveTabTo()
を呼んでいる)のような関数の存在も考慮して、1つの「やり直し可能な処理の単位」の中で行われたやり直し可能な処理はすべて、トップレベルの履歴項目に子供としてぶら下げる。
gBrowser.swapBrowsersAndCloseOther()
は場合によっては元のウィンドウを閉じてしまうため、ウィンドウを開き直す→ロード完了を待ってからタブを開き直す、という風な事をしないといけない。ウィンドウが開かれるまでの間に下位の項目(タブを開く、タブを閉じる等)のアンドゥ処理が走るとおかしな事になる。なので、下位の項目のアンドゥ処理はキャンセルして、swapBrowsersAndCloseOther()
のアンドゥ処理の中で全部完結させるようにする。例外を設けたのは、「前の履歴項目のアンドゥ処理の完了を待ってから次の履歴項目のアンドゥ処理を始める」という風な、非同期処理を考慮した仕組みを当初備えていなかったせい。なんでその仕組みを先に作らなかったのかというと、作るのがめんどかったからの一言に尽きる。
で、このような仕様で開発を進めて、Undo Tab Operationsについてはとりあえず素のFirefox上でならまともに使えるようになってきたかなあと思ったので、マルチプルタブハンドラとの連携に着手し始めた。そしたら破綻した。
例えばマルチプルタブハンドラは、タブ1つだけのドラッグ&ドロップでの移動を検知して選択されたタブ全部をその近くに移動するようになってるけど、これをアンドゥ可能にしようと思うと、そのアンドゥの処理が始まる前にUndo Tab Operationsによって行われるmoveTabTo()
のアンドゥでタブの並び順が変わってしまうので、最終的なタブの並び順がグチャグチャになってしまう。手っ取り早く解決しようと思うと、Undo Tab Operationsによって行われるタブの移動のアンドゥ処理は全部キャンセルして、マルチプルタブハンドラ側で面倒を見てやった方が、書くのは簡単なわけです。
しかし、そんなことをそれぞれのアドオンがやり始めたら、絶対にどっかで考慮漏れが起こるわけですよ。Undo Tab Operationsとマルチプルタブハンドラだけだったら問題が起こらなくても、そこにツリー型タブや他のアドオンが加わってくると、互いにアンドゥ処理の優先権の取り合いになってしまうのは目に見えてる。
そういう未来が予想できてしまったので、ついに観念して、非同期処理に真面目に対応することにした。で、結局以下のようになった。
addTab()
、removeTab()
など、それぞれの基本的な関数に対してアンドゥ・リドゥの処理を提供する。あとついでに、関数オブジェクトをそのまま履歴項目に使うようにすると履歴項目が増える度にクロージャが増えていってメモリリークの温床になりそうだなあと思ったので、DOMのイベントを監視することでも同じ事ができるようにAPIを整備した。Undo Tab Operations 0.2.2010011001以降ではこっちの方法を使ってそれぞれのアンドゥ・リドゥ処理を実装してある。
処理待ちにはJSDeferredを使ってもよかったんだけど、他のライブラリには依存させたくなかったので、JavaScript 1.7以降のジェネレータ・イテレータと継続関数で実現してみた。イベントオブジェクトのaEvent.wait()
で処理待ち状態になって、aEvent.continue()
で処理の完了を通知するというスタイルにしてある。
クロージャを使っても使わなくても、処理対象のタブを特定するには一意なIDが無いとどうにもならないなあと思ったので、汎用の「要素ノードに自動生成でIDを付与する」とかの機能もライブラリに含めることにした。これを使うことで、閉じられたタブに対応する開き直されたタブを確実に取得できるようになってる。
実際使ってみるとなかなか妙な感じですね。タブをウィンドウ外にドロップしてタブを切り離し→Shift-Ctrl-Zで元に戻す→Shit-Ctrl-Yでまた切り離す なんてことができて、キモくて面白いです。
Undo Tab Operationsの開発を通じて実装をこねくり回してた汎用のアンドゥ・リドゥ用のライブラリだけど、最低限必要そうな一通りの機能を実装し終えた……と思う。
最初はもっとコンパクトになるかなと思ってたんだけど、なんだかんだで膨らんで35KBちょいになった(2010年1月11日現在)。ライブラリの使い方は……Undo Tab Operationsのソース読んで実際の使われ方を見た方が話が早いかも。
以下、ライブラリの使い方の説明じゃなくてただの苦労話です。
当初は、単純に以下のようにしようと思ってた。
しかし実際やってみるまでもなく、これだと考慮しないといけないケースがあまりに多くなりすぎる。例えばタブを開く操作だけでも、「タブバー上のボタン」「ブックマーク」「リンク」等々色々ある。それら1つ1つに対してアンドゥ・リドゥの処理を定義していくのはさすがに無理がある。また、例えば「タブバーのボタンで新しいタブを開く処理に対応する関数」に対してアンドゥ・リドゥの処理を定義したとして、その関数が他の処理の中から呼ばれないという保証はどこにもないわけで、アンドゥ用の処理がかぶったら、タブが2つも3つも開き直されたり、その逆に2つも3つも閉じられたりしかねない。これは危険すぎる。
という事くらいはすぐに思いついたので、次にこんな風に考えた。
gBrowser.addTab()
やgBrowser.moveTabTo()
などの基本的な関数それぞれに対してアンドゥ・リドゥの処理を定義する。gBrowser.loadOneTab()
(内部でaddTab()
とmoveTabTo()
を呼んでいる)のような関数の存在も考慮して、1つの「やり直し可能な処理の単位」の中で行われたやり直し可能な処理はすべて、トップレベルの履歴項目に子供としてぶら下げる。
gBrowser.swapBrowsersAndCloseOther()
は場合によっては元のウィンドウを閉じてしまうため、ウィンドウを開き直す→ロード完了を待ってからタブを開き直す、という風な事をしないといけない。ウィンドウが開かれるまでの間に下位の項目(タブを開く、タブを閉じる等)のアンドゥ処理が走るとおかしな事になる。なので、下位の項目のアンドゥ処理はキャンセルして、swapBrowsersAndCloseOther()
のアンドゥ処理の中で全部完結させるようにする。例外を設けたのは、「前の履歴項目のアンドゥ処理の完了を待ってから次の履歴項目のアンドゥ処理を始める」という風な、非同期処理を考慮した仕組みを当初備えていなかったせい。なんでその仕組みを先に作らなかったのかというと、作るのがめんどかったからの一言に尽きる。
で、このような仕様で開発を進めて、Undo Tab Operationsについてはとりあえず素のFirefox上でならまともに使えるようになってきたかなあと思ったので、マルチプルタブハンドラとの連携に着手し始めた。そしたら破綻した。
例えばマルチプルタブハンドラは、タブ1つだけのドラッグ&ドロップでの移動を検知して選択されたタブ全部をその近くに移動するようになってるけど、これをアンドゥ可能にしようと思うと、そのアンドゥの処理が始まる前にUndo Tab Operationsによって行われるmoveTabTo()
のアンドゥでタブの並び順が変わってしまうので、最終的なタブの並び順がグチャグチャになってしまう。手っ取り早く解決しようと思うと、Undo Tab Operationsによって行われるタブの移動のアンドゥ処理は全部キャンセルして、マルチプルタブハンドラ側で面倒を見てやった方が、書くのは簡単なわけです。
しかし、そんなことをそれぞれのアドオンがやり始めたら、絶対にどっかで考慮漏れが起こるわけですよ。Undo Tab Operationsとマルチプルタブハンドラだけだったら問題が起こらなくても、そこにツリー型タブや他のアドオンが加わってくると、互いにアンドゥ処理の優先権の取り合いになってしまうのは目に見えてる。
そういう未来が予想できてしまったので、ついに観念して、非同期処理に真面目に対応することにした。で、結局以下のようになった。
addTab()
、removeTab()
など、それぞれの基本的な関数に対してアンドゥ・リドゥの処理を提供する。あとついでに、関数オブジェクトをそのまま履歴項目に使うようにすると履歴項目が増える度にクロージャが増えていってメモリリークの温床になりそうだなあと思ったので、DOMのイベントを監視することでも同じ事ができるようにAPIを整備した。Undo Tab Operations 0.2.2010011001以降ではこっちの方法を使ってそれぞれのアンドゥ・リドゥ処理を実装してある。
処理待ちにはJSDeferredを使ってもよかったんだけど、他のライブラリには依存させたくなかったので、JavaScript 1.7以降のジェネレータ・イテレータと継続関数で実現してみた。イベントオブジェクトのaEvent.wait()
で処理待ち状態になって、aEvent.continue()
で処理の完了を通知するというスタイルにしてある。
クロージャを使っても使わなくても、処理対象のタブを特定するには一意なIDが無いとどうにもならないなあと思ったので、汎用の「要素ノードに自動生成でIDを付与する」とかの機能もライブラリに含めることにした。これを使うことで、閉じられたタブに対応する開き直されたタブを確実に取得できるようになってる。
実際使ってみるとなかなか妙な感じですね。タブをウィンドウ外にドロップしてタブを切り離し→Shift-Ctrl-Zで元に戻す→Shit-Ctrl-Yでまた切り離す なんてことができて、キモくて面白いです。
Undo Tab Operationsの開発を通じて実装をこねくり回してた汎用のアンドゥ・リドゥ用のライブラリだけど、最低限必要そうな一通りの機能を実装し終えた……と思う。
最初はもっとコンパクトになるかなと思ってたんだけど、なんだかんだで膨らんで35KBちょいになった(2010年1月11日現在)。ライブラリの使い方は……Undo Tab Operationsのソース読んで実際の使われ方を見た方が話が早いかも。
以下、ライブラリの使い方の説明じゃなくてただの苦労話です。
当初は、単純に以下のようにしようと思ってた。
しかし実際やってみるまでもなく、これだと考慮しないといけないケースがあまりに多くなりすぎる。例えばタブを開く操作だけでも、「タブバー上のボタン」「ブックマーク」「リンク」等々色々ある。それら1つ1つに対してアンドゥ・リドゥの処理を定義していくのはさすがに無理がある。また、例えば「タブバーのボタンで新しいタブを開く処理に対応する関数」に対してアンドゥ・リドゥの処理を定義したとして、その関数が他の処理の中から呼ばれないという保証はどこにもないわけで、アンドゥ用の処理がかぶったら、タブが2つも3つも開き直されたり、その逆に2つも3つも閉じられたりしかねない。これは危険すぎる。
という事くらいはすぐに思いついたので、次にこんな風に考えた。
gBrowser.addTab()
やgBrowser.moveTabTo()
などの基本的な関数それぞれに対してアンドゥ・リドゥの処理を定義する。gBrowser.loadOneTab()
(内部でaddTab()
とmoveTabTo()
を呼んでいる)のような関数の存在も考慮して、1つの「やり直し可能な処理の単位」の中で行われたやり直し可能な処理はすべて、トップレベルの履歴項目に子供としてぶら下げる。
gBrowser.swapBrowsersAndCloseOther()
は場合によっては元のウィンドウを閉じてしまうため、ウィンドウを開き直す→ロード完了を待ってからタブを開き直す、という風な事をしないといけない。ウィンドウが開かれるまでの間に下位の項目(タブを開く、タブを閉じる等)のアンドゥ処理が走るとおかしな事になる。なので、下位の項目のアンドゥ処理はキャンセルして、swapBrowsersAndCloseOther()
のアンドゥ処理の中で全部完結させるようにする。例外を設けたのは、「前の履歴項目のアンドゥ処理の完了を待ってから次の履歴項目のアンドゥ処理を始める」という風な、非同期処理を考慮した仕組みを当初備えていなかったせい。なんでその仕組みを先に作らなかったのかというと、作るのがめんどかったからの一言に尽きる。
で、このような仕様で開発を進めて、Undo Tab Operationsについてはとりあえず素のFirefox上でならまともに使えるようになってきたかなあと思ったので、マルチプルタブハンドラとの連携に着手し始めた。そしたら破綻した。
例えばマルチプルタブハンドラは、タブ1つだけのドラッグ&ドロップでの移動を検知して選択されたタブ全部をその近くに移動するようになってるけど、これをアンドゥ可能にしようと思うと、そのアンドゥの処理が始まる前にUndo Tab Operationsによって行われるmoveTabTo()
のアンドゥでタブの並び順が変わってしまうので、最終的なタブの並び順がグチャグチャになってしまう。手っ取り早く解決しようと思うと、Undo Tab Operationsによって行われるタブの移動のアンドゥ処理は全部キャンセルして、マルチプルタブハンドラ側で面倒を見てやった方が、書くのは簡単なわけです。
しかし、そんなことをそれぞれのアドオンがやり始めたら、絶対にどっかで考慮漏れが起こるわけですよ。Undo Tab Operationsとマルチプルタブハンドラだけだったら問題が起こらなくても、そこにツリー型タブや他のアドオンが加わってくると、互いにアンドゥ処理の優先権の取り合いになってしまうのは目に見えてる。
そういう未来が予想できてしまったので、ついに観念して、非同期処理に真面目に対応することにした。で、結局以下のようになった。
addTab()
、removeTab()
など、それぞれの基本的な関数に対してアンドゥ・リドゥの処理を提供する。あとついでに、関数オブジェクトをそのまま履歴項目に使うようにすると履歴項目が増える度にクロージャが増えていってメモリリークの温床になりそうだなあと思ったので、DOMのイベントを監視することでも同じ事ができるようにAPIを整備した。Undo Tab Operations 0.2.2010011001以降ではこっちの方法を使ってそれぞれのアンドゥ・リドゥ処理を実装してある。
処理待ちにはJSDeferredを使ってもよかったんだけど、他のライブラリには依存させたくなかったので、JavaScript 1.7以降のジェネレータ・イテレータと継続関数で実現してみた。イベントオブジェクトのaEvent.wait()
で処理待ち状態になって、aEvent.continue()
で処理の完了を通知するというスタイルにしてある。
クロージャを使っても使わなくても、処理対象のタブを特定するには一意なIDが無いとどうにもならないなあと思ったので、汎用の「要素ノードに自動生成でIDを付与する」とかの機能もライブラリに含めることにした。これを使うことで、閉じられたタブに対応する開き直されたタブを確実に取得できるようになってる。
実際使ってみるとなかなか妙な感じですね。タブをウィンドウ外にドロップしてタブを切り離し→Shift-Ctrl-Zで元に戻す→Shit-Ctrl-Yでまた切り離す なんてことができて、キモくて面白いです。