diff --git a/src/Perspex.Base/Perspex.Base.csproj b/src/Perspex.Base/Perspex.Base.csproj index 4d6504d520..b0f6d928ff 100644 --- a/src/Perspex.Base/Perspex.Base.csproj +++ b/src/Perspex.Base/Perspex.Base.csproj @@ -85,6 +85,7 @@ + diff --git a/src/Perspex.Base/Utilities/WeakTimer.cs b/src/Perspex.Base/Utilities/WeakTimer.cs new file mode 100644 index 0000000000..f8901a1d74 --- /dev/null +++ b/src/Perspex.Base/Utilities/WeakTimer.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Perspex.Threading; + +namespace Perspex.Utilities +{ + public class WeakTimer + { + public interface IWeakTimerSubscriber + { + bool Tick(); + } + + private readonly WeakReference _subscriber; + private DispatcherTimer _timer; + + public WeakTimer(IWeakTimerSubscriber subscriber) + { + _subscriber = new WeakReference(subscriber); + _timer = new DispatcherTimer(); + + _timer.Tick += delegate { OnTick(); }; + _timer.Start(); + } + + private void OnTick() + { + IWeakTimerSubscriber subscriber; + if (!_subscriber.TryGetTarget(out subscriber) || !subscriber.Tick()) + Stop(); + } + + public TimeSpan Interval + { + get { return _timer.Interval; } + set { _timer.Interval = value; } + } + + public void Start() => _timer.Start(); + + public void Stop() => _timer.Stop(); + + + public static WeakTimer StartWeakTimer(IWeakTimerSubscriber subscriber, TimeSpan interval) + { + var timer = new WeakTimer(subscriber) {Interval = interval}; + timer.Start(); + return timer; + } + + } +} diff --git a/src/Perspex.Controls/Perspex.Controls.csproj b/src/Perspex.Controls/Perspex.Controls.csproj index 9270658e51..f9a4a3ad84 100644 --- a/src/Perspex.Controls/Perspex.Controls.csproj +++ b/src/Perspex.Controls/Perspex.Controls.csproj @@ -148,6 +148,7 @@ + @@ -173,6 +174,10 @@ + + ..\..\packages\JetBrains.Annotations.9.2.0\lib\portable-net4+sl5+netcore45+wpa81+wp8+MonoAndroid1+MonoTouch1\JetBrains.Annotations.PCL328.dll + True + ..\..\packages\Rx-Core.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Core.dll diff --git a/src/Perspex.Controls/TextBox.cs b/src/Perspex.Controls/TextBox.cs index 34ad050d0a..7eddecf0f7 100644 --- a/src/Perspex.Controls/TextBox.cs +++ b/src/Perspex.Controls/TextBox.cs @@ -17,7 +17,7 @@ using Perspex.Metadata; namespace Perspex.Controls { - public class TextBox : TemplatedControl + public class TextBox : TemplatedControl, UndoRedoHelper.IUndoRedoHost { public static readonly PerspexProperty AcceptsReturnProperty = PerspexProperty.Register("AcceptsReturn"); @@ -49,7 +49,22 @@ namespace Perspex.Controls public static readonly PerspexProperty UseFloatingWatermarkProperty = PerspexProperty.Register("UseFloatingWatermark"); + struct UndoRedoState : IEquatable + { + public string Text { get; } + public int CaretPosition { get; } + + public UndoRedoState(string text, int caretPosition) + { + Text = text; + CaretPosition = caretPosition; + } + + public bool Equals(UndoRedoState other) => ReferenceEquals(Text, other.Text) || Equals(Text, other.Text); + } + private TextPresenter _presenter; + private UndoRedoHelper _undoRedoHelper; static TextBox() { @@ -73,6 +88,7 @@ namespace Perspex.Controls ScrollViewer.HorizontalScrollBarVisibilityProperty, horizontalScrollBarVisibility, BindingPriority.Style); + _undoRedoHelper = new UndoRedoHelper(this); } public bool AcceptsReturn @@ -90,7 +106,12 @@ namespace Perspex.Controls public int CaretIndex { get { return GetValue(CaretIndexProperty); } - set { SetValue(CaretIndexProperty, value); } + set + { + SetValue(CaretIndexProperty, value); + if (_undoRedoHelper.IsLastState && _undoRedoHelper.LastState.Text == Text) + _undoRedoHelper.UpdateLastState(); + } } public int SelectionStart @@ -173,6 +194,7 @@ namespace Perspex.Controls Text = text.Substring(0, caretIndex) + input + text.Substring(caretIndex); CaretIndex += input.Length; SelectionStart = SelectionEnd = CaretIndex; + _undoRedoHelper.DiscardRedo(); } } @@ -189,7 +211,7 @@ namespace Perspex.Controls { return; } - + _undoRedoHelper.Snapshot(); HandleTextInput(text); } @@ -223,6 +245,16 @@ namespace Perspex.Controls Paste(); } + break; + case Key.Z: + if (modifiers == InputModifiers.Control) + _undoRedoHelper.Undo(); + + break; + case Key.Y: + if (modifiers == InputModifiers.Control) + _undoRedoHelper.Redo(); + break; case Key.Left: MoveHorizontal(-1, modifiers); @@ -524,5 +556,15 @@ namespace Perspex.Controls return i; } + + UndoRedoState UndoRedoHelper.IUndoRedoHost.UndoRedoState + { + get { return new UndoRedoState(Text, CaretIndex); } + set + { + Text = value.Text; + SelectionStart = SelectionEnd = CaretIndex = value.CaretPosition; + } + } } } diff --git a/src/Perspex.Controls/Utils/UndoRedoHelper.cs b/src/Perspex.Controls/Utils/UndoRedoHelper.cs new file mode 100644 index 0000000000..9de2ca9b35 --- /dev/null +++ b/src/Perspex.Controls/Utils/UndoRedoHelper.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Perspex.Utilities; + +namespace Perspex.Controls.Utils +{ + class UndoRedoHelper : WeakTimer.IWeakTimerSubscriber where TState : IEquatable + { + private readonly IUndoRedoHost _host; + + public interface IUndoRedoHost + { + TState UndoRedoState { get; set; } + } + + + + private readonly LinkedList _states = new LinkedList(); + + [NotNull] + private LinkedListNode _currentNode; + + public int Limit { get; set; } = 10; + + public UndoRedoHelper(IUndoRedoHost host) + { + _host = host; + _states.AddFirst(_host.UndoRedoState); + _currentNode = _states.First; + WeakTimer.StartWeakTimer(this, new TimeSpan(0, 0, 1)); + + } + + public void Undo() + { + _host.UndoRedoState= (_currentNode = _currentNode?.Previous ?? _currentNode).Value; + } + + public bool IsLastState => _currentNode.Next == null; + + public void UpdateLastState(TState state) + { + _states.Last.Value = state; + } + + public void UpdateLastState() + { + _states.Last.Value = _host.UndoRedoState; + } + + public TState LastState => _currentNode.Value; + + public void DiscardRedo() + { + //Linked list sucks, so we are doing this + while (_currentNode.Next != null) + _states.Remove(_currentNode.Next); + } + + public void Redo() + { + _host.UndoRedoState = (_currentNode = _currentNode?.Next ?? _currentNode).Value; + } + + public void Snapshot() + { + var current = _host.UndoRedoState; + if (!_currentNode.Value.Equals(current)) + { + if(_currentNode.Next != null) + DiscardRedo(); + _states.AddLast(current); + _currentNode = _states.Last; + if(_states.Count > Limit) + _states.RemoveFirst(); + } + } + + bool WeakTimer.IWeakTimerSubscriber.Tick() + { + Snapshot(); + return true; + } + } +} diff --git a/src/Perspex.Controls/packages.config b/src/Perspex.Controls/packages.config index 7d2a813bf7..26d3282b7a 100644 --- a/src/Perspex.Controls/packages.config +++ b/src/Perspex.Controls/packages.config @@ -1,5 +1,6 @@  +