Undo / Redo on JavaFX

今回は、JavaFX で Undo / Redo してみようと思います。
Undo / Redo は ちょっとまともなアプリケーションを作ろうとすると どうしても避けられない 機能です。
JavaFX で Undo / Redo する方法としては

  1. swing の UndoManager を使う
  2. 自分でがんばる
等がありますが、今回はもちろん後者でがんばる...

自分で Undo / Redo を実装する場合、まずはどの様に現在の情報を保存しておくかを決めないと...
保存する情報は次の2つ...

  1. 状態
  2. 手続 (手順)

これらは DB に例えるなら...
前者は スナップショットをテーブルやファイルに保存しておくようなイメージ。
状態そのものをデータとして保存しておきます。
後者は Redo ログ の様な感じ。
データそのものではなく、元に戻すための手順を保存しておきます。

どちらを使うかは状況次第?
もちろん、組み合わせて使うのもありですが...
今回は後者で実装することに...

なぜ、後者なのかというと JavaFX には

  1. 関数型の変数
  2. クロージャ
という Java にはない強力な言語仕様があるからです。
これらを利用すれば、簡単に元に戻すための手順を保存しておくことができます。

まずは 今回紹介する Undo / Redo を実際に試してみましょう。

では、早速 実装ですが...
今回は undo, redo が可能な Drag and Drop を実装してみましょう。
まずは

  1. undo, redo の手順を保持するためのクラス Undoable
  2. Undoable を管理するためのクラス UndoManager
の2つのクラスを用意します。ソースは こちら
本来であれば Command パターンの方がよいのかも...
でも 今回は undo, redo がメインと言うことで...

次は、以下のように undo, redo の手順を示す関数を保存します。

var umgr = UndoManager { limit: 100 }

Draggable {
    node: node

    var translateX: Number;
    var translateY: Number;

    // Drag 前の位置を保存
    onStart: function(e) {
        translateX = node.translateX;
        translateY = node.translateY;
    }

    // undo / redo の手順を保存
    onDone: function(e) {
        undoables.add(Undoable {

            // undo の手順を示す関数を生成するブロック
            undo: {
                def _translateX = translateX;
                def _translateY = translateY;
                function(): Void {
                    node.translateX = _translateX;
                    node.translateY = _translateY;
                }
            }

            // redo の手順を示す関数を生成するブロック
            redo: {
                def _translateX = node.translateX;
                def _translateY = node.translateY;
                function(): Void {
                    node.translateX = _translateX;
                    node.translateY = _translateY;
                }
            }
        });
    }
}

Draggable は 『Drag and Drop on JavaFX Part 1』 で紹介したものに onStart と onDone を追加しています。
onStart は Drag が開始される前に実行されるコールバック関数で Drag する前の位置を保存しています。
onDone は Drag が終了した後に実行されるコールバック関数で undo / redo の手順を保存しています。

undo の手順を保存するコードは

undo: {
    def _translateX = translateX;
    def _translateY = translateY;
    function(): Void {
        node.translateX = _translateX;
        node.translateY = _translateY;
    }
}
たったこれだけですが
  1. ブロック
  2. 関数型の変数
  3. クロージャ
JavaFX の特徴を最大限利用しています。
ちなみに、ブロックとは "{" と "}" で囲んだ式の固まりで 全てブロックのスコープ内で評価され、最後の式の結果のみが戻り値として返されます。
これは 以下のように無名関数を評価するのとほぼ等価です。
undo: (function() {
    def _translateX = translateX;
    def _translateY = translateY;
    function(): Void {
        node.translateX = _translateX;
        node.translateY = _translateY;
    }
})();
今回の場合、関数オブジェクトがブロックから返され、関数型の変数 undo にセットされます。

ここで大事なポイントですが、関数内で利用したい値は ブロック内の変数に格納しておくこと。
と言うのも、関数が評価されるタイミングは 関数の宣言時ではなく UndoManager#undo() がコールされたときだからです。 ちなみにこういうヤツを巷ではクロージャと呼んでいます。
もし、以下のように関数の中で 外部からアクセス可能な変数を直接参照してしまったら、関数が評価されるころには その変数の内容は すでに期待しているものではないでしょう。
undo: function(): Void {
    node.translateX = translateX;
    node.translateY = translateY;
}

後は 実際に undo, redo するためのボタンを用意するだけです。
以下のようにとっても簡単です。
undoable, redoable はそれぞれ undo, redo できるかどうかを表す変数で、これを Button#disable に bind しておけば 実際に undo, redo できる時だけボタンを有効にできるのです。なんて楽なんだろう。
undo(), redo() 関数は UndoManager に保存してある 手順を元に undo, redo します。

Button {
    text: "Undo"
    disable: bind not umgr.undoable
    action: umgr.undo
}
Button {
    text: "Redo"
    disable: bind not umgr.redoable
    action: umgr.redo
}

どうでしょうか?
JavaFX なら とっても簡単に Undo / Redo が実現できることがわかっていただけたのではないでしょうか?