BeInteractive!

あっという間に今年も半分が終わろうとしていますが、皆様如何がお過ごしでしょうか。

最近、仕事でエディタやツール的なものを作ることが多いのですが、いざ作ってみて思うのはイマドキ Cmd+Z が利かない (=アンドゥ/取り消しが出来ない) のはどーなのよ。ってことです。実際、世の FLASHer が Cmd+Enter の前に Cmd+S を押してしまうように、無意識に Cmd+Z を押してる場面も少なくないです。一個前にやっていた仕事ではアンドゥをつけなかったのを微妙に後悔していたりします (後から組み込むのは結構大変…)。

というわけで、今やってる仕事ではアンドゥを付けようと思って、さー UndoManager を書くぞと書き始めたら、補完に flashx.undo.UndoManager っていう、何か既に出来る子っぽいクラスが居るので、3秒で自分で作ったクラスは消しました。オブジェクト指向の奥義、再利用ってヤツです。

調べてみると、flashx.undo.UndoManager は、TextLayoutFramework に含まれているクラスのようです。Flex SDK 4 であれば、"frameworks/projects/textLayout/src/flashx/undo/UndoManager.as" に元のファイルを見つけることができます。たぶん、新しいテキストエンジンで色々やるために自前で実装する必要があったんでしょうね。

アンドゥ/リドゥを実現するためには、行った「操作」を覚えておいて、その操作に対する「復元」と「再実行」ができなければなりません。言い換えると、「復元」と「再実行」が出来る「操作」を表現するクラスが必要です。このようなクラスがあれば、操作を行う度に、このクラスのインスタンスを生成して溜め込んでおくことで、必要な時に「復元」を呼び出して、操作を取り消すことができます。所謂 Command パターンですね。

flashx.undo パッケージにも、このような Command クラスを実装するための、flashx.undo.IOperation というインターフェイスが存在します。定義されているメソッドはふたつで、perfomUndo() と perfomRedo() です。それぞれ、これが呼び出された時に「復元」と「再実行」を行えってことです。例えば何かドット絵エディタ的なものを作っているとして、「BitmapData のあるピクセルに色をセットする」という操作に対する IOperation インターフェイスの実装クラスは次のようなものになるでしょう。ちなみに、折角「操作」をクラス化するんですから、操作の「実行」もこのクラスに任せたいので、IOperation クラスを継承して execute() というメソッドを定義した IExecutableOperation を用意して、それを実装しています。

public interface IExecutableOperation extends IOperation
{
    function execute():void;
}

public class SetPixelOperation implements IExecutableOperation
{
    public function SetPixelOperation(bitmapData:BitmapData, x:uint, y:uint, color:uint)
    {
        _bitmapData = bitmapData;
        _x = x;
        _y = y;
        _color = color;
    }

    private var _bitmapData:BitmapData;
    private var _x:uint;
    private var _y:uint;
    private var _color:uint;
    private var _oldColor:uint;

    // 操作の呼び出し
    public function execute():void
    {
        // 元の色を覚えておく
        _oldColor = _bitmapData.getPixel32(_x, _y);
        // 色をセット
        _bitmapData.setPixel32(_x, _y, _color);
    }

    // 操作の取り消し
    public function performUndo():void
    {
        // 元の色に戻す
        _bitmapData.setPixel32(_x, _y, _oldColor);
    }

    // 操作のやり直し
    public function performRedo():void
    {
        // 色をセットする
        _bitmapData.setPixel32(_x, _y, _color);
    }
}

このようにすることで、ある操作に対する「実行」と「復元」と「再実行」の責任をひとつのクラス内にまとめることができました。このクラスを使用して、例えば次のようなコードを書くことで、取り消しが可能な操作を実現することができます。

// クリックされた時のハンドラ
private function clickHandler(e:MouseEvent):void
{
    // カーソル位置の色を選択されている色に変更する操作を生成
    var operation:IExecutableOperation = new SetPixelOperation(_bitmapData, stage.mouseX, stage.mouseY, _selectedColor);
    // 操作を実行
    operation.execute();
    // 取り消し可能な操作として UndoManager に登録
    _undoManager.pushUndo(operation);
}

_undoManager は、もちろん flashx.undo.UndoManager クラスのインスタンスです。こうしておくと、例えば Cmd+Z が押された際に _undoManager の undo() メソッドを呼び出せば、直前の操作を取り消すことが出来ます。勿論、更に undo() メソッドを呼び出せば、それ以前の操作もどんどん取り消されていきます。

ただし、ここで一つ問題があります。普通、undo() をしたら redo() を出来ることが期待されますが、この UndoManager は自動的には redo() をできるようにはしてくれません。この部分は自分で書く必要があるようです。なので、最終的に以下のような OperationManager というクラスを作りました。このようなクラスを一つ作っておくと便利でしょう。

public class OperationManager
{
    private static var _sharedOperationManager:OperationManager;
   
    /**
     * 共有されている OperationManager クラスのインスタンスを返します.
     */
    public static function get sharedOperationManager():OperationManager
    {
        if (_sharedOperationManager == null) {
            _sharedOperationManager = new OperationManager();
        }
        return _sharedOperationManager;
    }
   
    /**
     * 新しい OperationManager クラスのインスタンスを生成します.
     */
    public function OperationManager()
    {
        _undoManager = new UndoManager();
    }
   
    // 内部で使用する UndoManager
    private var _undoManager:UndoManager;
   
    /**
     * アンドゥが可能であれば true, そうでなければ false を返します.
     */
    public function get canUndo():Boolean
    {
        return _undoManager.canUndo();
    }
   
    /**
     * リドゥが可能であれば true, そうでなければ false を返します.
     */
    public function get canRedo():Boolean
    {
        return _undoManager.canRedo();
    }
   
    /**
     * アンドゥを実行して直前の操作を取り消します.
     */
    public function undo():void
    {
        // アンドゥできる状態でなければ何もしない
        if (!canUndo) {
            return;
        }
       
        // アンドゥ対象の操作を取得
        var undoOperation:IOperation = _undoManager.peekUndo();
        // アンドゥを実行
        _undoManager.undo();
        // リドゥできるように登録
        _undoManager.pushRedo(undoOperation);
    }
   
    /**
     * アンドゥを実行して直前に取り消した操作を再実行します.
     */
    public function redo():void
    {
        // リドゥできる状態でなければ何もしない
        if (!canRedo) {
            return;
        }
       
        // リドゥ対象の操作を取得
        var redoOperation:IOperation = _undoManager.peekRedo();
        // リドゥを実行
        _undoManager.redo();
        // アンドゥできるように登録
        _undoManager.pushUndo(redoOperation);
    }
   
    /**
     * 指定された操作を実行し、取り消し可能なよう登録します.
     *
     * @param    operation    実行する操作
     */
    public function execute(operation:IExecutableOperation):void
    {
        // 操作を実行
        operation.execute();
       
        // アンドゥできるように登録
        _undoManager.pushUndo(operation);
        // 新しい操作を実行した際はリドゥはもういらない
        _undoManager.clearRedo();
    }
}

このクラスを使用する場合、先ほどのコードは次のようになります。

// クリックされた時のハンドラ
private function clickHandler(e:MouseEvent):void
{
    // カーソル位置の色を選択されている色に変更する操作を生成
    var operation:IExecutableOperation = new SetPixelOperation(_bitmapData, stage.mouseX, stage.mouseY, _selectedColor);
    // 操作を実行
    OperationManager.sharedOperationManager.execute(operation);
}

自分で operation の execute() メソッドを呼び出す代わりに、OperationManager に渡して実行をしてもらいます。このあと、OperationManager の undo() を呼び出せば、操作が取り消されます。続けて redo() を呼び出せば、取り消した操作が再実行されます。何度か undo() を呼び出して、redo() を呼び出さずに execute() で新しい操作を実行すると、undo() した分の redo() はクリアされて再実行できなくなります (大抵のアプリケーションを観察するとこのような実装になっているのが分かるでしょう)。めでたく、いい感じにアンドゥ/リドゥが実装出来ました。

ちなみに、AIR でメニュー項目と関連づける場合は、次のようにすると、アンドゥ/リドゥ可能かによってメニューの有効/無効も切り替えることができます。

// アンドゥメニュー
var undoMenu:NativeMenuItem = new NativeMenuItem('Undo');
undoMenu.label = '元に戻す';
undoMenu.keyEquivalent = 'z'; // Cmd+Z or Ctrl+Z
undoMenu.addEventListener(Event.DISPLAYING, willDisplayUndoMenuHandler);
undoMenu.addEventListener(Event.SELECT, undoMenuSelectHandler);

// リドゥメニュー
var redoMenu:NativeMenuItem = new NativeMenuItem('Redo');
redoMenu.label = 'やり直し';
redoMenu.keyEquivalent = 'Z'; // Cmd+Shift+Z or Ctrol+Shift+Z
redoMenu.addEventListener(Event.DISPLAYING, willDisplayRedoMenuHandler);
redoMenu.addEventListener(Event.SELECT, redoMenuSelectHandler);

// メニューが表示される直前に呼ばれる
private function willDisplayUndoMenuHandler(e:Event):void
{
    var undoMenu:NativeMenuItem = e.target as NativeMenuItem;
    undoMenu.enabled = OperationManager.sharedOperationManager.canUndo;
}

// メニューが表示される直前に呼ばれる
private function willDisplayRedoMenuHandler(e:Event):void
{
    var redoMenu:NativeMenuItem = e.target as NativeMenuItem;
    redoMenu.enabled = OperationManager.sharedOperationManager.canRedo;
}

// メニューが選択されると呼ばれる
private function undoMenuSelectHandler(e:Event):void
{
    OperationManager.sharedOperationManager.undo();
}

// メニューが選択されると呼ばれる
private function redoMenuSelectHandler(e:Event):void
{
    OperationManager.sharedOperationManager.redo();
}

それではまた!

この記事へのトラックバック

トラックバックはありません。

TrackBack URL:

http://www.be-interactive.org/trackback.php?id=535

この記事へのコメント

コメントはありません。

コメント書き込み:

カテゴリ

タグ

アーカイブ