From ca4dee94fd2ab4884031072da4a92c9f00a5fcd2 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Tue, 8 Jun 2021 18:00:04 -0400 Subject: [PATCH 1/5] Remove timer from undo helper, improve u/r for textbox --- src/Avalonia.Controls/TextBox.cs | 57 ++++++++++++------- src/Avalonia.Controls/Utils/UndoRedoHelper.cs | 11 +--- 2 files changed, 40 insertions(+), 28 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 1bee15bccd..31ec12c22f 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -31,7 +31,7 @@ namespace Avalonia.Controls public static KeyGesture PasteGesture { get; } = AvaloniaLocator.Current .GetService()?.Paste.FirstOrDefault(); - + public static readonly StyledProperty AcceptsReturnProperty = AvaloniaProperty.Register(nameof(AcceptsReturn)); @@ -117,7 +117,7 @@ namespace Avalonia.Controls public static readonly StyledProperty RevealPasswordProperty = AvaloniaProperty.Register(nameof(RevealPassword)); - + public static readonly DirectProperty CanCutProperty = AvaloniaProperty.RegisterDirect( nameof(CanCut), @@ -135,7 +135,7 @@ namespace Avalonia.Controls public static readonly StyledProperty IsUndoEnabledProperty = AvaloniaProperty.Register( - nameof(IsUndoEnabled), + nameof(IsUndoEnabled), defaultValue: true); public static readonly DirectProperty UndoLimitProperty = @@ -174,6 +174,10 @@ namespace Avalonia.Controls private string _newLine = Environment.NewLine; private static readonly string[] invalidCharacters = new String[1] { "\u007f" }; + private int _selectedTextChangesMadeSinceLastUndoSnapshot; + private bool _hasDoneSnapshotOnce; + private const int _maxCharsBeforeUndoSnapshot = 7; + static TextBox() { FocusableProperty.OverrideDefaultValue(typeof(TextBox), true); @@ -202,7 +206,8 @@ namespace Avalonia.Controls horizontalScrollBarVisibility, BindingPriority.Style); _undoRedoHelper = new UndoRedoHelper(this); - + _selectedTextChangesMadeSinceLastUndoSnapshot = 0; + _hasDoneSnapshotOnce = false; UpdatePseudoclasses(); } @@ -331,6 +336,7 @@ namespace Avalonia.Controls if (SetAndRaise(TextProperty, ref _text, value) && IsUndoEnabled && !_isUndoingRedoing) { _undoRedoHelper.Clear(); + SnapshotUndoRedo(); // so we always have an initial state } } } @@ -341,7 +347,6 @@ namespace Avalonia.Controls get { return GetSelection(); } set { - SnapshotUndoRedo(); if (string.IsNullOrEmpty(value)) { DeleteSelection(); @@ -350,7 +355,6 @@ namespace Avalonia.Controls { HandleTextInput(value); } - SnapshotUndoRedo(); } } @@ -422,7 +426,7 @@ namespace Avalonia.Controls get { return _newLine; } set { SetAndRaise(NewLineProperty, ref _newLine, value); } } - + /// /// Clears the current selection, maintaining the /// @@ -480,11 +484,13 @@ namespace Avalonia.Controls var oldValue = _undoRedoHelper.Limit; _undoRedoHelper.Limit = value; RaisePropertyChanged(UndoLimitProperty, oldValue, value); - } + } // from docs at // https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled: // "Setting UndoLimit clears the undo queue." _undoRedoHelper.Clear(); + _selectedTextChangesMadeSinceLastUndoSnapshot = 0; + _hasDoneSnapshotOnce = false; } } @@ -515,6 +521,8 @@ namespace Avalonia.Controls // Therefore, if you disable undo and then re-enable it, undo commands still do not work // because the undo stack was emptied when you disabled undo." _undoRedoHelper.Clear(); + _selectedTextChangesMadeSinceLastUndoSnapshot = 0; + _hasDoneSnapshotOnce = false; } } @@ -577,23 +585,25 @@ namespace Avalonia.Controls { return; } - + input = RemoveInvalidCharacters(input); - + if (string.IsNullOrEmpty(input)) { return; } - + _selectedTextChangesMadeSinceLastUndoSnapshot++; + SnapshotUndoRedo(ignoreChangeCount: false); + string text = Text ?? string.Empty; int caretIndex = CaretIndex; int newLength = input.Length + text.Length - Math.Abs(SelectionStart - SelectionEnd); - + if (MaxLength > 0 && newLength > MaxLength) { input = input.Remove(Math.Max(0, input.Length - (newLength - MaxLength))); } - + if (!string.IsNullOrEmpty(input)) { DeleteSelection(); @@ -696,6 +706,7 @@ namespace Avalonia.Controls { try { + SnapshotUndoRedo(); _isUndoingRedoing = true; _undoRedoHelper.Undo(); } @@ -830,7 +841,6 @@ namespace Avalonia.Controls CaretIndex -= removedCharacters; ClearSelection(); } - SnapshotUndoRedo(); handled = true; break; @@ -858,7 +868,6 @@ namespace Avalonia.Controls SetTextInternal(text.Substring(0, caretIndex) + text.Substring(caretIndex + removedCharacters)); } - SnapshotUndoRedo(); handled = true; break; @@ -868,7 +877,6 @@ namespace Avalonia.Controls { SnapshotUndoRedo(); HandleTextInput(NewLine); - SnapshotUndoRedo(); handled = true; } @@ -879,7 +887,6 @@ namespace Avalonia.Controls { SnapshotUndoRedo(); HandleTextInput("\t"); - SnapshotUndoRedo(); handled = true; } else @@ -889,6 +896,10 @@ namespace Avalonia.Controls break; + case Key.Space: + SnapshotUndoRedo(); // always snapshot in between words + break; + default: handled = false; break; @@ -1306,11 +1317,19 @@ namespace Avalonia.Controls } } - private void SnapshotUndoRedo() + private void SnapshotUndoRedo(bool ignoreChangeCount = true) { if (IsUndoEnabled) { - _undoRedoHelper.Snapshot(); + if (ignoreChangeCount || + !_hasDoneSnapshotOnce || + (!ignoreChangeCount && + _selectedTextChangesMadeSinceLastUndoSnapshot >= _maxCharsBeforeUndoSnapshot)) + { + _undoRedoHelper.Snapshot(); + _selectedTextChangesMadeSinceLastUndoSnapshot = 0; + _hasDoneSnapshotOnce = true; + } } } } diff --git a/src/Avalonia.Controls/Utils/UndoRedoHelper.cs b/src/Avalonia.Controls/Utils/UndoRedoHelper.cs index 7374f20a0c..fd1ca54b57 100644 --- a/src/Avalonia.Controls/Utils/UndoRedoHelper.cs +++ b/src/Avalonia.Controls/Utils/UndoRedoHelper.cs @@ -7,7 +7,7 @@ using Avalonia.Utilities; namespace Avalonia.Controls.Utils { - class UndoRedoHelper : WeakTimer.IWeakTimerSubscriber where TState : struct, IEquatable + class UndoRedoHelper { private readonly IUndoRedoHost _host; @@ -31,7 +31,6 @@ namespace Avalonia.Controls.Utils public UndoRedoHelper(IUndoRedoHost host) { _host = host; - WeakTimer.StartWeakTimer(this, TimeSpan.FromSeconds(1)); } public void Undo() @@ -61,7 +60,7 @@ namespace Avalonia.Controls.Utils if (_states.Last != null) { _states.Last.Value = state; - } + } } public void UpdateLastState() @@ -103,11 +102,5 @@ namespace Avalonia.Controls.Utils _states.Clear(); _currentNode = null; } - - bool WeakTimer.IWeakTimerSubscriber.Tick() - { - Snapshot(); - return true; - } } } From 2c215d1b3917af0c31e55cb22e2ba07db203b535 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Tue, 8 Jun 2021 18:02:29 -0400 Subject: [PATCH 2/5] Make sure to snapshot undo before delete selected --- src/Avalonia.Controls/TextBox.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 31ec12c22f..23c0d70f20 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -349,6 +349,8 @@ namespace Avalonia.Controls { if (string.IsNullOrEmpty(value)) { + _selectedTextChangesMadeSinceLastUndoSnapshot++; + SnapshotUndoRedo(ignoreChangeCount: false); DeleteSelection(); } else From 11138b1fc5937bb268460b36dd259a64c6791caf Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Tue, 8 Jun 2021 18:05:02 -0400 Subject: [PATCH 3/5] Remove extra snapshots after cut, paste --- src/Avalonia.Controls/TextBox.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 23c0d70f20..dad2401921 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -639,7 +639,6 @@ namespace Avalonia.Controls SnapshotUndoRedo(); Copy(); DeleteSelection(); - SnapshotUndoRedo(); } public async void Copy() @@ -659,7 +658,6 @@ namespace Avalonia.Controls SnapshotUndoRedo(); HandleTextInput(text); - SnapshotUndoRedo(); } protected override void OnKeyDown(KeyEventArgs e) From 2fe25f8acf3567ad1869942eb20c6bc068b44387 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Tue, 8 Jun 2021 18:48:12 -0400 Subject: [PATCH 4/5] Fix equality for UndoRedoState In UndoRedoHelper, the .Equals call in Snapshot() did not call Equals(UndoRedoState other). It called the default Equals(object obj) implementation. This could throw of the undo redo snapshotting. Fix that by overriding Equals(object obj). --- src/Avalonia.Controls/TextBox.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index dad2401921..edd268a86a 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -157,6 +157,13 @@ namespace Avalonia.Controls } public bool Equals(UndoRedoState other) => ReferenceEquals(Text, other.Text) || Equals(Text, other.Text); + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (obj.GetType() != GetType()) return false; + return Equals((UndoRedoState)obj); + } } private string _text; From f92804ff05b6361a0e355ad894e59c453927c6c7 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Tue, 8 Jun 2021 18:57:18 -0400 Subject: [PATCH 5/5] Improve UndoRedoState.Equals(object obj) impl --- src/Avalonia.Controls/TextBox.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index edd268a86a..b89bd22038 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -158,12 +158,9 @@ namespace Avalonia.Controls public bool Equals(UndoRedoState other) => ReferenceEquals(Text, other.Text) || Equals(Text, other.Text); - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (obj.GetType() != GetType()) return false; - return Equals((UndoRedoState)obj); - } + public override bool Equals(object obj) => obj is UndoRedoState other && Equals(other); + + public override int GetHashCode() => Text.GetHashCode(); } private string _text;