Skip to content

Commit

Permalink
Merge pull request #6023 from Deadpikle/feature/textbox-undo-adjustments
Browse files Browse the repository at this point in the history
TextBox: Add IsUndoEnabled and UndoLimit
  • Loading branch information
maxkatz6 authored and danwalmsley committed Jun 18, 2021
1 parent bfc85bb commit 0911059
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 27 deletions.
112 changes: 87 additions & 25 deletions src/Avalonia.Controls/TextBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,19 +119,31 @@ public class TextBox : TemplatedControl, UndoRedoHelper<TextBox.UndoRedoState>.I
AvaloniaProperty.Register<TextBox, bool>(nameof(RevealPassword));

public static readonly DirectProperty<TextBox, bool> CanCutProperty =
AvaloniaProperty.RegisterDirect<TextBox, bool>(
nameof(CanCut),
o => o.CanCut);
AvaloniaProperty.RegisterDirect<TextBox, bool>(
nameof(CanCut),
o => o.CanCut);

public static readonly DirectProperty<TextBox, bool> CanCopyProperty =
AvaloniaProperty.RegisterDirect<TextBox, bool>(
nameof(CanCopy),
o => o.CanCopy);

public static readonly DirectProperty<TextBox, bool> CanPasteProperty =
AvaloniaProperty.RegisterDirect<TextBox, bool>(
nameof(CanPaste),
o => o.CanPaste);
AvaloniaProperty.RegisterDirect<TextBox, bool>(
nameof(CanPaste),
o => o.CanPaste);

public static readonly StyledProperty<bool> IsUndoEnabledProperty =
AvaloniaProperty.Register<TextBox, bool>(
nameof(IsUndoEnabled),
defaultValue: true);

public static readonly DirectProperty<TextBox, int> UndoLimitProperty =
AvaloniaProperty.RegisterDirect<TextBox, int>(
nameof(UndoLimit),
o => o.UndoLimit,
(o, v) => o.UndoLimit = v,
unsetValue: -1);

struct UndoRedoState : IEquatable<UndoRedoState>
{
Expand Down Expand Up @@ -218,7 +230,7 @@ public int CaretIndex
value = CoerceCaretIndex(value);
SetAndRaise(CaretIndexProperty, ref _caretIndex, value);
UndoRedoState state;
if (_undoRedoHelper.TryGetLastState(out state) && state.Text == Text)
if (IsUndoEnabled && _undoRedoHelper.TryGetLastState(out state) && state.Text == Text)
_undoRedoHelper.UpdateLastState();
}
}
Expand Down Expand Up @@ -316,7 +328,7 @@ public string Text
SelectionEnd = CoerceCaretIndex(SelectionEnd, value);
CaretIndex = CoerceCaretIndex(caretIndex, value);

if (SetAndRaise(TextProperty, ref _text, value) && !_isUndoingRedoing)
if (SetAndRaise(TextProperty, ref _text, value) && IsUndoEnabled && !_isUndoingRedoing)
{
_undoRedoHelper.Clear();
}
Expand All @@ -329,7 +341,7 @@ public string SelectedText
get { return GetSelection(); }
set
{
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
if (string.IsNullOrEmpty(value))
{
DeleteSelection();
Expand All @@ -338,7 +350,7 @@ public string SelectedText
{
HandleTextInput(value);
}
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
}
}

Expand Down Expand Up @@ -446,6 +458,36 @@ public bool CanPaste
private set { SetAndRaise(CanPasteProperty, ref _canPaste, value); }
}

/// <summary>
/// Property for determining whether undo/redo is enabled
/// </summary>
public bool IsUndoEnabled
{
get { return GetValue(IsUndoEnabledProperty); }
set { SetValue(IsUndoEnabledProperty, value); }
}

public int UndoLimit
{
get { return _undoRedoHelper.Limit; }
set
{
if (_undoRedoHelper.Limit != value)
{
// can't use SetAndRaise due to using _undoRedoHelper.Limit
// (can't send a ref of a property to SetAndRaise),
// so use RaisePropertyChanged instead.
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();
}
}

protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
_presenter = e.NameScope.Get<TextPresenter>("PART_TextPresenter");
Expand All @@ -465,6 +507,15 @@ protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T>
UpdatePseudoclasses();
UpdateCommandStates();
}
else if (change.Property == IsUndoEnabledProperty && change.NewValue.GetValueOrDefault<bool>() == false)
{
// from docs at
// https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled:
// "Setting this property to false clears the undo stack.
// 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();
}
}

private void UpdateCommandStates()
Expand Down Expand Up @@ -551,7 +602,10 @@ private void HandleTextInput(string input)
SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex));
CaretIndex += input.Length;
ClearSelection();
_undoRedoHelper.DiscardRedo();
if (IsUndoEnabled)
{
_undoRedoHelper.DiscardRedo();
}
}
}

Expand All @@ -570,10 +624,10 @@ public async void Cut()
var text = GetSelection();
if (text is null) return;

_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
Copy();
DeleteSelection();
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
}

public async void Copy()
Expand All @@ -591,9 +645,9 @@ public async void Paste()

if (text is null) return;

_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
HandleTextInput(text);
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
}

protected override void OnKeyDown(KeyEventArgs e)
Expand Down Expand Up @@ -638,7 +692,7 @@ protected override void OnKeyDown(KeyEventArgs e)
Paste();
handled = true;
}
else if (Match(keymap.Undo))
else if (Match(keymap.Undo) && IsUndoEnabled)
{
try
{
Expand All @@ -652,7 +706,7 @@ protected override void OnKeyDown(KeyEventArgs e)

handled = true;
}
else if (Match(keymap.Redo))
else if (Match(keymap.Redo) && IsUndoEnabled)
{
try
{
Expand Down Expand Up @@ -752,7 +806,7 @@ protected override void OnKeyDown(KeyEventArgs e)
break;

case Key.Back:
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
{
SetSelectionForControlBackspace();
Expand All @@ -776,13 +830,13 @@ protected override void OnKeyDown(KeyEventArgs e)
CaretIndex -= removedCharacters;
ClearSelection();
}
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();

handled = true;
break;

case Key.Delete:
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
{
SetSelectionForControlDelete();
Expand All @@ -804,17 +858,17 @@ protected override void OnKeyDown(KeyEventArgs e)
SetTextInternal(text.Substring(0, caretIndex) +
text.Substring(caretIndex + removedCharacters));
}
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();

handled = true;
break;

case Key.Enter:
if (AcceptsReturn)
{
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
HandleTextInput(NewLine);
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
handled = true;
}

Expand All @@ -823,9 +877,9 @@ protected override void OnKeyDown(KeyEventArgs e)
case Key.Tab:
if (AcceptsTab)
{
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
HandleTextInput("\t");
_undoRedoHelper.Snapshot();
SnapshotUndoRedo();
handled = true;
}
else
Expand Down Expand Up @@ -1251,5 +1305,13 @@ UndoRedoState UndoRedoHelper<UndoRedoState>.IUndoRedoHost.UndoRedoState
ClearSelection();
}
}

private void SnapshotUndoRedo()
{
if (IsUndoEnabled)
{
_undoRedoHelper.Snapshot();
}
}
}
}
11 changes: 9 additions & 2 deletions src/Avalonia.Controls/Utils/UndoRedoHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ public interface IUndoRedoHost

private LinkedListNode<TState> _currentNode;

/// <summary>
/// Maximum number of states this helper can store for undo/redo.
/// If -1, no limit is imposed.
/// </summary>
public int Limit { get; set; } = 10;

public UndoRedoHelper(IUndoRedoHost host)
Expand Down Expand Up @@ -54,7 +58,10 @@ public bool TryGetLastState(out TState _state)
public bool HasState => _currentNode != null;
public void UpdateLastState(TState state)
{
_states.Last.Value = state;
if (_states.Last != null)
{
_states.Last.Value = state;
}
}

public void UpdateLastState()
Expand Down Expand Up @@ -86,7 +93,7 @@ public void Snapshot()
DiscardRedo();
_states.AddLast(current);
_currentNode = _states.Last;
if (_states.Count > Limit)
if (Limit != -1 && _states.Count > Limit)
_states.RemoveFirst();
}
}
Expand Down

0 comments on commit 0911059

Please sign in to comment.