RedoUndo

Undo/Redoの仕組み

基本概念

NSUndoManager2つのスタックを持っています:

┌──────────────┐  ┌──────────────┐
│  Undoスタック │  │  Redoスタック │
├──────────────┤  ├──────────────┤
│  操作3       │  │              │
│  操作2       │  │              │
│  操作1       │  │              │
└──────────────┘  └──────────────┘

ステップ1: 塗りつぶし実行

// ユーザーが塗りつぶしを実行
performFloodFill(at: point)

内部処理:

private func performFloodFill(at point: CGPoint) {
    let oldImage = drawing_image  // ← 現在の画像を保存
    
    // 塗りつぶし処理
    guard let filled = floodFill(...) else { return }
    
    // Undo登録(重要!)
    registerUndoForFloodFill(oldImage: oldImage, newImage: filled)
    
    // 画像を更新
    drawing_image = filled
    drawing_layer.contents = filled
}

ステップ2: Undo登録

private func registerUndoForFloodFill(
    oldImage: CGImage?,
    newImage: CGImage?
) {
    undoManager?.registerUndo(withTarget: self) { target in
        target.restoreDrawingImage(oldImage)
    }
    undoManager?.setActionName("塗りつぶし")
}

この時点でのスタック:

Undoスタック:
┌─────────────────────────────────┐
│ 操作: restoreDrawingImage(旧画像) │  ← NEW!
└─────────────────────────────────┘

Redoスタック:
┌─────────────────────────────────┐
│ (空)                             │
└─────────────────────────────────┘

現在の状態: 新画像

ステップ3: Undoを実行(Cmd+Z)

ユーザーが Cmd+Z を押すと:

// システムが自動的に呼び出す
undoManager?.undo()

内部で起こること:

  1. Undoスタックから操作を取り出す
  2. その操作を実行: restoreDrawingImage(oldImage)
private func restoreDrawingImage(_ image: CGImage?) {
    let currentImage = drawing_image  // ← 現在(新画像)を保存
    
    // 画像を元に戻す
    drawing_image = image  // 旧画像に戻す
    drawing_layer.contents = image
    
    // ここがポイント!Redoのために新画像を登録
    undoManager?.registerUndo(withTarget: self) { target in
        target.restoreDrawingImage(currentImage)  // 新画像
    }
}

この時点でのスタック:

Undoスタック:
┌─────────────────────────────────┐
│ (空)                             │
└─────────────────────────────────┘

Redoスタック:
┌─────────────────────────────────┐
│ 操作: restoreDrawingImage(新画像) │  ← NEW!
└─────────────────────────────────┘

現在の状態: 旧画像(元に戻った)

ステップ4: Redoを実行(Cmd+Shift+Z)

ユーザーが Cmd+Shift+Z を押すと:

// システムが自動的に呼び出す
undoManager?.redo()

内部で起こること:

  1. Redoスタックから操作を取り出す
  2. その操作を実行: restoreDrawingImage(newImage)
private func restoreDrawingImage(_ image: CGImage?) {
    let currentImage = drawing_image  // ← 現在(旧画像)を保存
    
    // 画像を新しいものに
    drawing_image = image  // 新画像に戻す
    drawing_layer.contents = image
    
    // またUndoのために登録
    undoManager?.registerUndo(withTarget: self) { target in
        target.restoreDrawingImage(currentImage)  // 旧画像
    }
}

この時点でのスタック:

Undoスタック:
┌─────────────────────────────────┐
│ 操作: restoreDrawingImage(旧画像) │  ← NEW!
└─────────────────────────────────┘

Redoスタック:
┌─────────────────────────────────┐
│ (空)                             │
└─────────────────────────────────┘

現在の状態: 新画像(やり直した)

完全なフロー図

初期状態: 画像A
    ↓
塗りつぶし実行
    ↓
[登録] Undo: 画像Aに戻す
    ↓
現在の状態: 画像B
    ↓
Cmd+Z (Undo)
    ↓
[実行] 画像Aに戻す
[登録] Redo: 画像Bに進む
    ↓
現在の状態: 画像A
    ↓
Cmd+Shift+Z (Redo)
    ↓
[実行] 画像Bに進む
[登録] Undo: 画像Aに戻す
    ↓
現在の状態: 画像B

複数操作がある場合

3回塗りつぶした場合

操作1: 白 → 赤
操作2: 赤 → 青
操作3: 青 → 緑

Undoスタック:
┌──────────────┐
│ 青 → 緑 を戻す  │  ← 最新
│ 赤 → 青 を戻す  │
│ 白 → 赤 を戻す  │  ← 最古
└──────────────┘

現在: 緑

Undoを1回実行:

Undoスタック:
┌──────────────┐
│ 赤 → 青 を戻す  │
│ 白 → 赤 を戻す  │
└──────────────┘

Redoスタック:
┌──────────────┐
│ 青 → 緑 を進む  │
└──────────────┘

現在: 青

さらにUndoを実行:

Undoスタック:
┌──────────────┐
│ 白 → 赤 を戻す  │
└──────────────┘

Redoスタック:
┌──────────────┐
│ 青 → 緑 を進む  │
│ 赤 → 青 を進む  │
└──────────────┘

現在: 赤

重要な仕組み

1. 新しい操作をするとRedoスタックがクリアされる

状態: 白 → 赤 → 青
Undoを2回実行
↓
状態: 白
Redoスタック: [赤に進む, 青に進む]

ここで新しい塗りつぶし(緑)を実行
↓
Redoスタックがクリア!
状態: 白 → 緑
Undoスタック: [白 → 緑 を戻す]
Redoスタック: []  ← 空になる

2. registerUndo内でregisterUndoする理由

private func restoreDrawingImage(_ image: CGImage?) {
    let currentImage = drawing_image
    
    drawing_image = image
    drawing_layer.contents = image
    
    // ここでまたregisterUndoする!
    undoManager?.registerUndo(withTarget: self) { target in
        target.restoreDrawingImage(currentImage)
    }
}

理由: Undo/Redo を何度でも行き来できるようにするため

Undo実行時:
  - 操作を実行する
  - その逆操作をRedoスタックに登録

Redo実行時:
  - 操作を実行する
  - その逆操作をUndoスタックに登録

undoManagerはどこから来る?

// NSResponder(NSViewの親クラス)が持っている
override var undoManager: UndoManager? {
    return window?.undoManager  // ← ウィンドウが管理
}

自動的に接続される:

  • メニューの「編集 > 取り消す」
  • ショートカット Cmd+Z
  • ショートカット Cmd+Shift+Z

デバッグ用コード

private func registerUndoForFloodFill(
    oldImage: CGImage?,
    newImage: CGImage?
) {
    print("📝 Undo登録: 塗りつぶしを元に戻す")
    
    undoManager?.registerUndo(withTarget: self) { target in
        print("🔙 Undo実行: 画像を復元")
        target.restoreDrawingImage(oldImage)
    }
    undoManager?.setActionName("塗りつぶし")
}

private func restoreDrawingImage(_ image: CGImage?) {
    let currentImage = drawing_image
    
    print("🔄 画像を変更")
    drawing_image = image
    drawing_layer.contents = image
    
    print("📝 Undo登録: 逆操作")
    undoManager?.registerUndo(withTarget: self) { target in
        print("🔙 Undo実行: 逆の復元")
        target.restoreDrawingImage(currentImage)
    }
}

出力例:

[塗りつぶし実行]
📝 Undo登録: 塗りつぶしを元に戻す

[Cmd+Z]
🔙 Undo実行: 画像を復元
🔄 画像を変更
📝 Undo登録: 逆操作

[Cmd+Shift+Z]
🔙 Undo実行: 逆の復元
🔄 画像を変更
📝 Undo登録: 逆操作

まとめ

タイミング何が起こる?どこに登録?
操作実行registerUndoを呼ぶUndoスタック
Undo実行Undoスタックから取り出して実行<br>→ 逆操作をRedoスタックに登録Redoスタック
Redo実行Redoスタックから取り出して実行<br>→ 逆操作をUndoスタックに登録Undoスタック
新規操作Redoスタックをクリア

キーポイント:

  • registerUndoの中でregisterUndoすることで無限にUndo/Redoできる
  • システムが自動的にundo()/redo()を呼んでくれる
  • メニューやショートカットは自動で有効化される

setActionName()の役割

メニューに表示される操作名を設定するためのメソッドです。

なぜ最後に書くのか?

直前に登録したregisterUndo操作に名前をつけるためです。

簡単なまとめ

setActionName()は直前のregisterUndoに名前をつけるメソッド

なぜ最後?

  • 直前の操作に名前をつける仕様だから
  • 先に書いても効果がない

必須ではないが、UX向上のために推奨されています!

リセット

CanvasNSView にリセットメソッドを追加:

コメント

タイトルとURLをコピーしました