// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; using System.Linq; using FlaxEngine.Collections; namespace FlaxEditor.History { /// /// Controller for handling stack manipulations in history and reverse buffers. /// public sealed class HistoryStack { private int _historyActionsLimit; private readonly CircularBuffer _historyActions; private readonly CircularBuffer _reverseActions; /// /// Initializes a new instance of the class. /// /// The history actions limit. public HistoryStack(int historyActionsLimit = 1000) { if (historyActionsLimit < 1) throw new ArgumentOutOfRangeException(); _historyActionsLimit = historyActionsLimit; _historyActions = new CircularBuffer(_historyActionsLimit); _reverseActions = new CircularBuffer(_historyActionsLimit); _historyActions.OnItemOverflown += OnItemOverflown; _reverseActions.OnItemOverflown += OnItemOverflown; } private void OnItemOverflown(object sender, CircularBuffer.ItemOverflownEventArgs e) { // Dispose item to prevent leaks e.Item.Dispose(); } /// /// Gets the history actions limit. /// /// /// The history actions limit. /// public int HistoryActionsLimit { get => _historyActionsLimit; set { if (value < 1) throw new ArgumentOutOfRangeException(); if (_historyActionsLimit == value) return; // Cache actions var history = _historyActions.ToArray(); var reverse = _reverseActions.ToArray(); // Resize buffers _historyActionsLimit = value; _historyActions.Clear(_historyActionsLimit); _reverseActions.Clear(_historyActionsLimit); // Add actions back for (int i = 0; i < _historyActionsLimit && i < history.Length; i++) { _historyActions.PushBack(history[i]); } for (int i = 0; i < _historyActionsLimit && i < reverse.Length; i++) { _reverseActions.PushBack(reverse[i]); } // Cleanup remaining actions for (int i = _historyActionsLimit; i < history.Length; i++) { history[i].Dispose(); } for (int i = _historyActionsLimit; i < reverse.Length; i++) { reverse[i].Dispose(); } } } /// /// Gets the history count. /// /// /// The history count. /// public int HistoryCount => _historyActions.Count; /// /// Gets the reverse count. /// /// /// The reverse count. /// public int ReverseCount => _reverseActions.Count; /// /// Adds new history element at top of history stack, and drops reverse stack /// /// Item to add public void Push(IHistoryAction item) { _historyActions.PushFront(item); ClearReverse(); } /// /// Gets top-most item in history stack /// /// Found element or null public IHistoryAction PeekHistory() { return _historyActions.Count == 0 ? null : _historyActions[_historyActions.Count - 1]; } /// /// Gets top-most item in reverse stack /// /// Found element or null public IHistoryAction PeekReverse() { return _reverseActions.Count == 0 ? null : _reverseActions[_reverseActions.Count - 1]; } /// /// Gets top-most item in history stack, and removes it from history stack. Adds forgot element in reverse stack. /// /// Found element or null public IHistoryAction PopHistory() { var item = PeekHistory(); if (item == null) return null; _historyActions.PopFront(); _reverseActions.PushFront(item); return item; } /// /// Gets top-most item in reverse stack, and removes it from reverse stack. Adds forgot element in history stack. /// /// Found element or null public IHistoryAction PopReverse() { var item = PeekReverse(); if (item == null) return null; _reverseActions.PopFront(); _historyActions.PushFront(item); return item; } /// /// Gets element at given index from top of history stack, and adds all skipped elements to reverse stack /// /// If skipElements is bigger, then amount of elements in history, returns null, clears history and pushes all to reverse stack /// Amount of elements to skip from history stack /// >Found element or null public IHistoryAction TravelBack(int skipElements) { if (skipElements <= 0) { throw new ArgumentOutOfRangeException(nameof(skipElements), "argument cannot be smaller or equal to 0"); } if (_historyActions.Count - skipElements <= 0) { foreach (var historyAction in _historyActions) { _reverseActions.PushFront(historyAction); } var result = _historyActions.Back(); ClearHistory(); return result; } // Iterate all but one elements to skip. Last element is handled exclusively for (int i = 0; i < skipElements - 1; i++) { PopHistory(); } return PopHistory(); } /// /// Gets element at given index from top of reverse stack, and adds all skipped elements to history stack /// /// If skipElements is bigger, then amount of elements in reverse, returns null, clears reverse and pushes all to history stack /// Amount of elements to skip from reverse stack /// >Found element or null public IHistoryAction TravelReverse(int skipElements) { if (skipElements <= 0) { throw new ArgumentOutOfRangeException(nameof(skipElements), "skipElement cannot be smaller or equal to 0"); } if (_reverseActions.Count - skipElements <= 0) { foreach (var reverseAction in _reverseActions.Reverse()) { _historyActions.PushFront(reverseAction); } ClearReverse(); return PeekHistory(); } // iterate all but one elements to skip. Last element is handled exclusively for (int i = 0; i < skipElements - 1; i++) { PopReverse(); } return PopReverse(); } /// /// Clears whole history (back and front). /// public void Clear() { ClearHistory(); ClearReverse(); } private void ClearHistory() { if (_historyActions.Count > 0) { var actions = _historyActions.ToArray(); for (int i = 0; i < actions.Length; i++) actions[i].Dispose(); _historyActions.Clear(); } } private void ClearReverse() { if (_reverseActions.Count > 0) { var actions = _reverseActions.ToArray(); for (int i = 0; i < actions.Length; i++) actions[i].Dispose(); _reverseActions.Clear(); } } } }