diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 964a153c8b..da4e90fb66 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -18,6 +18,7 @@ using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Automation.Peers; using System.Diagnostics; +using Avalonia.Threading; namespace Avalonia.Controls { @@ -159,18 +160,41 @@ namespace Avalonia.Controls (o, v) => o.UndoLimit = v, unsetValue: -1); + /// + /// Defines the event. + /// public static readonly RoutedEvent CopyingToClipboardEvent = RoutedEvent.Register( nameof(CopyingToClipboard), RoutingStrategies.Bubble); + /// + /// Defines the event. + /// public static readonly RoutedEvent CuttingToClipboardEvent = RoutedEvent.Register( nameof(CuttingToClipboard), RoutingStrategies.Bubble); + /// + /// Defines the event. + /// public static readonly RoutedEvent PastingFromClipboardEvent = RoutedEvent.Register( nameof(PastingFromClipboard), RoutingStrategies.Bubble); + /// + /// Defines the event. + /// + public static readonly RoutedEvent TextChangedEvent = + RoutedEvent.Register( + nameof(TextChanged), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent TextChangingEvent = + RoutedEvent.Register( + nameof(TextChanging), RoutingStrategies.Bubble); + readonly struct UndoRedoState : IEquatable { public string? Text { get; } @@ -359,8 +383,8 @@ namespace Avalonia.Controls /// public double LineHeight { - get { return GetValue(LineHeightProperty); } - set { SetValue(LineHeightProperty, value); } + get => GetValue(LineHeightProperty); + set => SetValue(LineHeightProperty, value); } [Content] @@ -376,11 +400,19 @@ namespace Avalonia.Controls CaretIndex = CoerceCaretIndex(caretIndex, value); SelectionStart = CoerceCaretIndex(selectionStart, value); SelectionEnd = CoerceCaretIndex(selectionEnd, value); - if (SetAndRaise(TextProperty, ref _text, value) && IsUndoEnabled && !_isUndoingRedoing) + + var textChanged = SetAndRaise(TextProperty, ref _text, value); + + if (textChanged && IsUndoEnabled && !_isUndoingRedoing) { _undoRedoHelper.Clear(); SnapshotUndoRedo(); // so we always have an initial state } + + if (textChanged) + { + RaiseTextChangeEvents(); + } } } @@ -564,6 +596,27 @@ namespace Avalonia.Controls remove => RemoveHandler(PastingFromClipboardEvent, value); } + /// + /// Occurs asynchronously after text changes and the new text is rendered. + /// + public event EventHandler? TextChanged + { + add => AddHandler(TextChangedEvent, value); + remove => RemoveHandler(TextChangedEvent, value); + } + + /// + /// Occurs synchronously when text starts to change but before it is rendered. + /// + /// + /// This event occurs just after the property value has been updated. + /// + public event EventHandler? TextChanging + { + add => AddHandler(TextChangingEvent, value); + remove => RemoveHandler(TextChangingEvent, value); + } + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { _presenter = e.NameScope.Get("PART_TextPresenter"); @@ -1252,7 +1305,7 @@ namespace Avalonia.Controls if (text != null && _wordSelectionStart >= 0) { - var distance = caretIndex - _wordSelectionStart; + var distance = caretIndex - _wordSelectionStart; if (distance <= 0) { @@ -1539,11 +1592,39 @@ namespace Avalonia.Controls return text.Substring(start, end - start); } + /// + /// Raises both the and events. + /// + /// + /// This must be called after the property is set. + /// + private void RaiseTextChangeEvents() + { + // Note the following sequence of these events (following WinUI) + // 1. TextChanging occurs synchronously when text starts to change but before it is rendered. + // This occurs after the Text property is set. + // 2. TextChanged occurs asynchronously after text changes and the new text is rendered. + + var textChangingEventArgs = new TextChangingEventArgs(TextChangingEvent); + RaiseEvent(textChangingEventArgs); + + Dispatcher.UIThread.Post(() => + { + var textChangedEventArgs = new TextChangedEventArgs(TextChangedEvent); + RaiseEvent(textChangedEventArgs); + }, DispatcherPriority.Normal); + } + private void SetTextInternal(string value, bool raiseTextChanged = true) { if (raiseTextChanged) { - SetAndRaise(TextProperty, ref _text, value); + bool textChanged = SetAndRaise(TextProperty, ref _text, value); + + if (textChanged) + { + RaiseTextChangeEvents(); + } } else { diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index d39d964277..5d5ffcc381 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -64,7 +64,7 @@ namespace Avalonia.Controls return new TextInputMethodSurroundingText { - Text = lineText ?? "", + Text = lineText ?? "", AnchorOffset = anchorOffset, CursorOffset = cursorOffset }; diff --git a/src/Avalonia.Controls/TextChangedEventArgs.cs b/src/Avalonia.Controls/TextChangedEventArgs.cs new file mode 100644 index 0000000000..77c609f19b --- /dev/null +++ b/src/Avalonia.Controls/TextChangedEventArgs.cs @@ -0,0 +1,20 @@ +using Avalonia.Interactivity; + +namespace Avalonia.Controls +{ + /// + /// Provides data specific to a TextChanged event. + /// + public class TextChangedEventArgs : RoutedEventArgs + { + public TextChangedEventArgs(RoutedEvent? routedEvent) + : base (routedEvent) + { + } + + public TextChangedEventArgs(RoutedEvent? routedEvent, IInteractive? source) + : base(routedEvent, source) + { + } + } +} diff --git a/src/Avalonia.Controls/TextChangingEventArgs.cs b/src/Avalonia.Controls/TextChangingEventArgs.cs new file mode 100644 index 0000000000..4dedbc927b --- /dev/null +++ b/src/Avalonia.Controls/TextChangingEventArgs.cs @@ -0,0 +1,20 @@ +using Avalonia.Interactivity; + +namespace Avalonia.Controls +{ + /// + /// Provides data specific to a TextChanging event. + /// + public class TextChangingEventArgs : RoutedEventArgs + { + public TextChangingEventArgs(RoutedEvent? routedEvent) + : base (routedEvent) + { + } + + public TextChangingEventArgs(RoutedEvent? routedEvent, IInteractive? source) + : base(routedEvent, source) + { + } + } +}