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 @@
+