« while ループ条件中の in 演算子の不思議な挙動 | Jetpack SDK 0.7 の Clipboard API » |
nsITransactionManager を使ったトランザクション管理
拡張機能やXULアプリにて、ユーザの操作に対する「元に戻す」「やり直し」機能を実装する際、 nsITransactionManager が便利です。例えば Firefox 本体ではブックマークの追加/削除/移動などが nsITransactionManager によってトランザクション管理されています。
基本形
ユーザがボタンをクリックすると、金額が加算されて合計金額がテキストボックスに表示されるような、簡単なXULアプリを作ってみます。
<?xml version="1.0"?> <?xml-stylesheet href="chrome://global/skin/" type="text/css"?> <page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" title="Bank" onload="init();"> <script type="application/x-javascript" src="bank.js" /> <textbox id="total" /> <hbox> <button label="Deposit $1" oncommand="deposit(1);" /> <button label="Deposit $10" oncommand="deposit(10);" /> <button label="Deposit $100" oncommand="deposit(100);" /> </hbox> </page>
引き続き JavaScript で機能を実装します。合計金額を gTotalMoney
というグローバル変数に保持し、合計金額をテキストボックスに表示するための updateUI
関数を作っておきます。 onload イベント発生時に呼び出される init
関数では、 updateUI
で合計金額の初期値「$0」を表示します。
ボタンをクリックしたときに呼び出される deposit
関数では、引数に渡された金額を gTotalMoney
に加算した後、 updateUI
で合計金額の表示を更新します。
var gTotalMoney = 0; function init() { updateUI(); } function updateUI() { document.getElementById("total").value = "$" + gTotalMoney.toString(); } function deposit(aMoney) { gTotalMoney += aMoney; updateUI(); }
トランザクション管理
では、上記のアプリでボタンをクリックした後、その処理を元に戻す/やり直しできるようにします。
はじめに、一連のトランザクションを管理するための nsITransactionManager インスタンスであるグローバル変数 gTxnManager
およびその初期化処理を追加します。
var gTotalMoney = 0; var gTxnManager; function init() { gTxnManager = Components.classes["@mozilla.org/transactionmanager;1"]. createInstance(Components.interfaces.nsITransactionManager); updateUI(); }
トランザクション管理をして元に戻す/やり直し可能にするためには、個々の処理を nsITransaction インタフェースを実装したオブジェクトとして記述する必要があります。今回の場合は預金処理を表す DepositTxn
クラスを以下のような設計で作ります。
メンバ | 概要 |
---|---|
_money |
コンストラクタの引数に渡された金額を内部的に保持するためのプロパティ。 |
doTransaction |
このトランザクションを実行する際に呼び出されるメソッド。 合計金額へ _money 分だけ加算する。 |
undoTransaction |
このトランザクションを元に戻す際に呼び出されるメソッド。 合計金額から _money 分だけ減算する。 |
redoTransaction |
このトランザクションをやり直しする際に呼び出されるメソッド。 通常は doTransaction と同じ処理を実行すればよい。 |
merge isTransient |
詳細不明。 |
function DepositTxn(aMoney) { this._money = aMoney; } DepositTxn.prototype = { doTransaction : function() { gTotalMoney += this._money; }, undoTransaction: function() { gTotalMoney -= this._money; }, redoTransaction: function() this.doTransaction(), merge: function() false, get isTransient() false, };
deposit
関数を書き換えて、合計金額を直接変更する代わりに、預金処理トランザクションクラスのインスタンスを生成して gTxnManager
の doTransaction
メソッドへ渡します。こうすることで、内部的に DepositTxn
インスタンスの doTransaction
が呼び出されて合計金額への加算が行われ、なおかつその処理が元に戻す処理のスタックへ追加され、必要に応じて元に戻すことが可能となります。
function deposit(aMoney) {
var txn = new DepositTxn(aMoney);
gTxnManager.doTransaction(txn);
updateUI();
}
次に、アプリのUIへ元に戻す/やり直しするためのボタンを追加します。
<hbox> <button id="undoButton" label="Undo" oncommand="undo();" /> <button id="redoButton" label="Redo" oncommand="redo();" /> </hbox>
それぞれのボタン押下時の処理として、 nsITransactionManager の undoTransaction および redoTransaction メソッドを呼び出します。すると、元に戻す/やり直し処理のスタックの最後尾にあるトランザクション(nsITransaction インスタンス)の undoTransaction および redoTransaction メソッドが実行されます。その後、合計金額の表示を更新します。
function undo() { gTxnManager.undoTransaction(); updateUI(); } function redo() { gTxnManager.redoTransaction(); updateUI(); }
バッチ処理
例えば複数のブックマークを選択して削除した後に元に戻すと、複数のブックマークが一括して復元されます。このように、一連のトランザクションをバッチ処理として実行する場合の元に戻す/やり直しを実装します。
はじめに、アプリのUIへ複数の金額を一括して預金するためのボタンを追加します。
<button label="Deposit $1+$10+$100" oncommand="depositSet([1,10,100]);" />
depositSet
関数は引数に渡された金額の配列すべてについて合計金額へ加算します。このとき、 nsITransactionManager の beginBatch メソッドを呼んでから doTransaction で個々の金額についての預金処理を実行し、最後に endBatch を呼ぶようにします。これにより、一連の預金処理がグループ化され、元に戻す際に一括して元に戻す処理が実行されます。
function depositSet(aMoneys) { gTxnManager.beginBatch(); aMoneys.forEach(function(money) { var txn = new DepositTxn(money); gTxnManager.doTransaction(txn); }); gTxnManager.endBatch(); updateUI(); }
元に戻す/やり直しスタックのリセット
nsITransactionManager の clear メソッドによって、元に戻す/やり直しスタックをいったんリセットすることができます。このメソッドを使って預金処理を確定させるボタンを作ってみます。
<button id="completeButton" label="Complete" oncommand="complete();" />
function complete() { gTxnManager.clear(); }
元に戻す/やり直しスタック内の個数
nsITransactionManager の numberOfUndoItems, numberOfRedoItems プロパティで元に戻す/やり直しスタック内の個数を調べることができます。これを使って元に戻す/やり直し可能なときだけボタンを押下可能にするようにしてみます。
function updateUI() {
document.getElementById("total").value = "$" + gTotalMoney.toString();
var canUndo = gTxnManager.numberOfUndoItems > 0;
var canRedo = gTxnManager.numberOfRedoItems > 0;
function setDisabled(id, disabled) {
var elt = document.getElementById(id);
if (disabled)
elt.setAttribute("disabled", "true");
else
elt.removeAttribute("disabled");
}
setDisabled("undoButton", !canUndo);
setDisabled("redoButton", !canRedo);
setDisabled("completeButton", !canUndo && !canRedo);
}
トランザクションの監視
nsITransactionManager の AddListener に nsITransactionListener インタフェースを実装したオブジェクトを渡して、トランザクションを監視することができます。 RemoveListener で監視を終了するのを忘れずに。
<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
title="Bank" onload="init();" onunload="uninit();">
function init() { gTxnManager = Components.classes["@mozilla.org/transactionmanager;1"]. createInstance(Components.interfaces.nsITransactionManager); gTxnManager.AddListener(gTxnListener); updateUI(); } function uninit() { gTxnManager.RemoveListener(gTxnListener); }
nsITransactionListener は多くのメソッドを持ち、元に戻す/やり直し処理の直前/直後などにコールバック処理を設定することができます。今回は元に戻す処理の直前に確認のダイアログを表示し、「キャンセル」ボタンを押下することで中止できるようにします。以下のように willUndo
メソッドで true を返すと、処理を中止することができます。
var gTxnListener = { willDo: function() {}, didDo: function() {}, willUndo: function(aMgr, aTxn) { if (!window.confirm("Would you like to undo?")) return true; }, didUndo: function() {}, willRedo: function() {}, didRedo: function() {}, willBeginBatch: function() {}, didBeginBatch: function() {}, willEndBatch: function() {}, didEndBatch: function() {}, willMerge: function() {}, didMerge: function() {}, };