namespace ytLive.Models; /// /// Undo/redo for what the creator does in the preview: move, resize, reorder, add, remove, hide. /// /// Snapshots hold element REFERENCES, not copies. The model has a dozen subclasses /// with dozens of properties each; a deep clone would be a large, fragile mirror of every one of /// them, and it would silently miss the next field someone adds. Holding the live instances and /// recording the values the preview can change (geometry, visibility, order, and which elements a /// scene contains) means a new model property cannot break undo — it simply is not part of this /// stack's contract. /// /// Scope, stated plainly: this covers layout edits. Property edits (colour, opacity, /// border, font) are not captured yet — pressing Ctrl+Z after one of those restores the last /// layout change instead. That is a limitation, not a bug to discover later. /// /// Bounded: entries; the oldest falls off. Snapshots are /// references plus a handful of doubles, so the stack costs kilobytes, not megabytes. /// public sealed class PreviewHistory { public const int MaxDepth = 50; private readonly List _undo = new(); private readonly List _redo = new(); public bool CanUndo => _undo.Count > 0; public bool CanRedo => _redo.Count > 0; public int UndoDepth => _undo.Count; public int RedoDepth => _redo.Count; /// Record the layout as it is now, before an edit. Clears the redo branch — a new /// edit after an undo is a new future. public void Capture(IEnumerable scenes) { _undo.Add(LayoutState.From(scenes)); _redo.Clear(); if (_undo.Count > MaxDepth) _undo.RemoveAt(0); } /// Restore the previous layout. The current layout is pushed onto the redo stack, so /// redo is exact even after several undos. public bool Undo(IEnumerable scenes) { if (_undo.Count == 0) return false; var sceneList = scenes as IList ?? scenes.ToList(); _redo.Add(LayoutState.From(sceneList)); var previous = _undo[^1]; _undo.RemoveAt(_undo.Count - 1); previous.RestoreTo(sceneList); return true; } public bool Redo(IEnumerable scenes) { if (_redo.Count == 0) return false; var sceneList = scenes as IList ?? scenes.ToList(); _undo.Add(LayoutState.From(sceneList)); var next = _redo[^1]; _redo.RemoveAt(_redo.Count - 1); next.RestoreTo(sceneList); return true; } /// Dropped when the layout is reloaded from disk — undoing across a reload would /// resurrect a layout the creator never had. public void Clear() { _undo.Clear(); _redo.Clear(); } /// One scene's contents and geometry at a point in time. private sealed record SceneState(Scene Scene, SceneElement[] Order, ElementState[] Elements); private sealed record ElementState(SceneElement Element, double X, double Y, double Width, double Height, bool IsVisible); private sealed record LayoutState(SceneState[] Scenes) { public static LayoutState From(IEnumerable scenes) => new( scenes.Select(scene => new SceneState( scene, scene.Elements.ToArray(), scene.Elements.Select(element => new ElementState( element, element.X, element.Y, element.Width, element.Height, element.IsVisible)).ToArray())) .ToArray()); public void RestoreTo(IList scenes) { foreach (var state in Scenes) { // A scene removed from the layout since the snapshot is not restored — the // layout, not the history, owns which scenes exist. if (!scenes.Contains(state.Scene)) continue; foreach (var element in state.Elements) { element.Element.X = element.X; element.Element.Y = element.Y; element.Element.Width = element.Width; element.Element.Height = element.Height; element.Element.IsVisible = element.IsVisible; } var elements = state.Scene.Elements; // Membership AND order: this is what makes add / remove / reorder undo-able. if (elements.SequenceEqual(state.Order)) continue; elements.Clear(); foreach (var element in state.Order) elements.Add(element); } } } }