From 86324dca7786b19c3794f3f5129b1fd7bc7a4c61 Mon Sep 17 00:00:00 2001 From: Tom Edwards Date: Wed, 8 Mar 2023 21:46:21 +0100 Subject: [PATCH] Convert Text properties to StyledProperty --- .../AutoCompleteBox.Properties.cs | 12 +- .../AutoCompleteBox/AutoCompleteBox.cs | 3 +- .../Documents/InlineCollection.cs | 8 +- src/Avalonia.Controls/MaskedTextBox.cs | 135 +++--- .../Presenters/TextPresenter.cs | 109 ++--- src/Avalonia.Controls/SelectableTextBlock.cs | 105 ++--- src/Avalonia.Controls/TextBlock.cs | 66 +-- src/Avalonia.Controls/TextBox.cs | 433 +++++++++--------- .../TextBoxTextInputMethodClient.cs | 2 +- src/Avalonia.Controls/Utils/UndoRedoHelper.cs | 4 +- .../Diagnostics/Controls/FilterTextBox.cs | 29 +- .../Styling/SetterTests.cs | 55 ++- .../TextBlockTests.cs | 2 +- 13 files changed, 453 insertions(+), 510 deletions(-) diff --git a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.Properties.cs b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.Properties.cs index ecbe01d8b7..93b057e176 100644 --- a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.Properties.cs +++ b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.Properties.cs @@ -87,12 +87,10 @@ namespace Avalonia.Controls /// Identifies the property. /// /// The identifier for the property. - public static readonly DirectProperty TextProperty = - TextBlock.TextProperty.AddOwnerWithDataValidation( - o => o.Text, - (o, v) => o.Text = v, + public static readonly StyledProperty TextProperty = + TextBlock.TextProperty.AddOwner(new(string.Empty, defaultBindingMode: BindingMode.TwoWay, - enableDataValidation: true); + enableDataValidation: true)); /// /// Identifies the property. @@ -317,8 +315,8 @@ namespace Avalonia.Controls /// control. public string? Text { - get => _text; - set => SetAndRaise(TextProperty, ref _text, value); + get => GetValue(TextProperty); + set => SetValue(TextProperty, value); } /// diff --git a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs index 9a949e31d4..2d074ef7fa 100644 --- a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs @@ -198,7 +198,6 @@ namespace Avalonia.Controls private bool _isDropDownOpen; private bool _isFocused = false; - private string? _text = string.Empty; private string? _searchText = string.Empty; private AutoCompleteFilterPredicate? _itemFilter; @@ -1275,7 +1274,7 @@ namespace Avalonia.Controls if ((userInitiated ?? true) && Text != value) { _ignoreTextPropertyChange++; - Text = value; + SetCurrentValue(TextProperty, value); callTextChanged = true; } diff --git a/src/Avalonia.Controls/Documents/InlineCollection.cs b/src/Avalonia.Controls/Documents/InlineCollection.cs index fe9f5e64a8..39750d3672 100644 --- a/src/Avalonia.Controls/Documents/InlineCollection.cs +++ b/src/Avalonia.Controls/Documents/InlineCollection.cs @@ -91,11 +91,11 @@ namespace Avalonia.Controls.Documents public override void Add(Inline inline) { - if (InlineHost is TextBlock textBlock && !string.IsNullOrEmpty(textBlock._text)) + if (InlineHost is TextBlock textBlock && !string.IsNullOrEmpty(textBlock.Text)) { - base.Add(new Run(textBlock._text)); + base.Add(new Run(textBlock.Text)); - textBlock._text = null; + textBlock.ClearTextInternal(); } base.Add(inline); @@ -113,7 +113,7 @@ namespace Avalonia.Controls.Documents { if (InlineHost is TextBlock textBlock && !textBlock.HasComplexContent) { - textBlock._text += text; + textBlock.Text += text; } else { diff --git a/src/Avalonia.Controls/MaskedTextBox.cs b/src/Avalonia.Controls/MaskedTextBox.cs index d397a581dc..8fc9477dc7 100644 --- a/src/Avalonia.Controls/MaskedTextBox.cs +++ b/src/Avalonia.Controls/MaskedTextBox.cs @@ -15,9 +15,8 @@ namespace Avalonia.Controls public static readonly StyledProperty AsciiOnlyProperty = AvaloniaProperty.Register(nameof(AsciiOnly)); - public static readonly DirectProperty CultureProperty = - AvaloniaProperty.RegisterDirect(nameof(Culture), o => o.Culture, - (o, v) => o.Culture = v, CultureInfo.CurrentCulture); + public static readonly StyledProperty CultureProperty = + AvaloniaProperty.Register(nameof(Culture), CultureInfo.CurrentCulture); public static readonly StyledProperty HidePromptOnLeaveProperty = AvaloniaProperty.Register(nameof(HidePromptOnLeave)); @@ -32,26 +31,49 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(Mask), string.Empty); public static readonly StyledProperty PromptCharProperty = - AvaloniaProperty.Register(nameof(PromptChar), '_'); + AvaloniaProperty.Register(nameof(PromptChar), '_', coerce: CoercePromptChar); - public static readonly DirectProperty ResetOnPromptProperty = - AvaloniaProperty.RegisterDirect(nameof(ResetOnPrompt), o => o.ResetOnPrompt, (o, v) => o.ResetOnPrompt = v); + public static readonly StyledProperty ResetOnPromptProperty = + AvaloniaProperty.Register(nameof(ResetOnPrompt), true); - public static readonly DirectProperty ResetOnSpaceProperty = - AvaloniaProperty.RegisterDirect(nameof(ResetOnSpace), o => o.ResetOnSpace, (o, v) => o.ResetOnSpace = v); + public static readonly StyledProperty ResetOnSpaceProperty = + AvaloniaProperty.Register(nameof(ResetOnSpace), true); - private CultureInfo? _culture; + private bool _ignoreTextChanges; - private bool _resetOnPrompt = true; + static MaskedTextBox() + { + PasswordCharProperty.OverrideMetadata(new('\0', coerce: CoercePasswordChar)); + } - private bool _ignoreTextChanges; + private static char CoercePasswordChar(AvaloniaObject sender, char baseValue) + { + if (!MaskedTextProvider.IsValidPasswordChar(baseValue)) + { + throw new ArgumentException($"'{baseValue}' is not a valid value for PasswordChar."); + } + var textbox = (MaskedTextBox)sender; + if (textbox.MaskProvider is { } maskProvider && baseValue == maskProvider.PromptChar) + { + // Prompt and password chars must be different. + throw new InvalidOperationException("PasswordChar and PromptChar values cannot be the same."); + } - private bool _resetOnSpace = true; + return baseValue; + } - static MaskedTextBox() + private static char CoercePromptChar(AvaloniaObject sender, char baseValue) { - PasswordCharProperty - .OverrideDefaultValue('\0'); + if (!MaskedTextProvider.IsValidInputChar(baseValue)) + { + throw new ArgumentException($"'{baseValue}' is not a valid value for PromptChar."); + } + if (baseValue == sender.GetValue(PasswordCharProperty)) + { + throw new InvalidOperationException("PasswordChar and PromptChar values cannot be the same."); + } + + return baseValue; } public MaskedTextBox() { } @@ -59,6 +81,9 @@ namespace Avalonia.Controls /// /// Constructs the MaskedTextBox with the specified MaskedTextProvider object. /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", + "AVP1012:An AvaloniaObject should use SetCurrentValue when assigning its own StyledProperty or AttachedProperty values", + Justification = "These values are being explicitly provided by a constructor parameter.")] public MaskedTextBox(MaskedTextProvider maskedTextProvider) { if (maskedTextProvider == null) @@ -87,8 +112,8 @@ namespace Avalonia.Controls /// public CultureInfo? Culture { - get => _culture; - set => SetAndRaise(CultureProperty, ref _culture, value); + get => GetValue(CultureProperty); + set => SetValue(CultureProperty, value); } /// @@ -131,15 +156,6 @@ namespace Avalonia.Controls /// public MaskedTextProvider? MaskProvider { get; private set; } - /// - /// Gets or sets the character to be displayed in substitute for user input. - /// - public new char PasswordChar - { - get => GetValue(PasswordCharProperty); - set => SetValue(PasswordCharProperty, value); - } - /// /// Gets or sets the character used to represent the absence of user input in MaskedTextBox. /// @@ -154,16 +170,8 @@ namespace Avalonia.Controls /// public bool ResetOnPrompt { - get => _resetOnPrompt; - set - { - SetAndRaise(ResetOnPromptProperty, ref _resetOnPrompt, value); - if (MaskProvider != null) - { - MaskProvider.ResetOnPrompt = value; - } - - } + get => GetValue(ResetOnPromptProperty); + set => SetValue(ResetOnPromptProperty, value); } /// @@ -171,16 +179,8 @@ namespace Avalonia.Controls /// public bool ResetOnSpace { - get => _resetOnSpace; - set - { - SetAndRaise(ResetOnSpaceProperty, ref _resetOnSpace, value); - if (MaskProvider != null) - { - MaskProvider.ResetOnSpace = value; - } - - } + get => GetValue(ResetOnSpaceProperty); + set => SetValue(ResetOnSpaceProperty, value); } Type IStyleable.StyleKey => typeof(TextBox); @@ -190,7 +190,7 @@ namespace Avalonia.Controls { if (HidePromptOnLeave == true && MaskProvider != null) { - Text = MaskProvider.ToDisplayString(); + SetCurrentValue(TextProperty, MaskProvider.ToDisplayString()); } base.OnGotFocus(e); } @@ -225,11 +225,11 @@ namespace Avalonia.Controls var index = GetNextCharacterPosition(CaretIndex); if (MaskProvider.InsertAt(item, index)) { - CaretIndex = ++index; + SetCurrentValue(CaretIndexProperty, ++index); } } - Text = MaskProvider.ToDisplayString(); + SetCurrentValue(TextProperty, MaskProvider.ToDisplayString()); e.Handled = true; return; } @@ -279,7 +279,7 @@ namespace Avalonia.Controls { if (HidePromptOnLeave && MaskProvider != null) { - Text = MaskProvider.ToString(!HidePromptOnLeave, true); + SetCurrentValue(TextProperty, MaskProvider.ToString(!HidePromptOnLeave, true)); } base.OnLostFocus(e); } @@ -326,15 +326,6 @@ namespace Avalonia.Controls } else if (change.Property == PasswordCharProperty) { - if (!MaskedTextProvider.IsValidPasswordChar(PasswordChar)) - { - throw new ArgumentException("Specified character value is not allowed for this property.", nameof(PasswordChar)); - } - if (MaskProvider != null && PasswordChar == MaskProvider.PromptChar) - { - // Prompt and password chars must be different. - throw new InvalidOperationException("PasswordChar and PromptChar values cannot be the same."); - } if (MaskProvider != null && MaskProvider.PasswordChar != PasswordChar) { UpdateMaskProvider(); @@ -342,17 +333,23 @@ namespace Avalonia.Controls } else if (change.Property == PromptCharProperty) { - if (!MaskedTextProvider.IsValidInputChar(PromptChar)) + if (MaskProvider != null && MaskProvider.PromptChar != PromptChar) { - throw new ArgumentException("Specified character value is not allowed for this property."); + UpdateMaskProvider(); } - if (PromptChar == PasswordChar) + } + else if (change.Property == ResetOnPromptProperty) + { + if (MaskProvider != null && change.GetNewValue() is { } newValue) { - throw new InvalidOperationException("PasswordChar and PromptChar values cannot be the same."); + MaskProvider.ResetOnPrompt = newValue; } - if (MaskProvider != null && MaskProvider.PromptChar != PromptChar) + } + else if (change.Property == ResetOnSpaceProperty) + { + if (MaskProvider != null && change.GetNewValue() is { } newValue) { - UpdateMaskProvider(); + MaskProvider.ResetOnSpace = newValue; } } else if (change.Property == AsciiOnlyProperty && MaskProvider != null && MaskProvider.AsciiOnly != AsciiOnly @@ -390,7 +387,7 @@ namespace Avalonia.Controls if (CaretIndex < Text?.Length) { - CaretIndex = GetNextCharacterPosition(CaretIndex); + SetCurrentValue(CaretIndexProperty, GetNextCharacterPosition(CaretIndex)); if (MaskProvider.InsertAt(e.Text!, CaretIndex)) { @@ -399,7 +396,7 @@ namespace Avalonia.Controls var nextPos = GetNextCharacterPosition(CaretIndex); if (nextPos != 0 && CaretIndex != Text.Length) { - CaretIndex = nextPos; + SetCurrentValue(CaretIndexProperty, nextPos); } } @@ -434,8 +431,8 @@ namespace Avalonia.Controls { if (provider != null) { - Text = provider.ToDisplayString(); - CaretIndex = position; + SetCurrentValue(TextProperty, provider.ToDisplayString()); + SetCurrentValue(CaretIndexProperty, position); } } diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 089a2c1168..e4167425cd 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -9,17 +9,13 @@ using Avalonia.VisualTree; using Avalonia.Layout; using Avalonia.Media.Immutable; using Avalonia.Controls.Documents; -using Avalonia.Input.TextInput; -using Avalonia.Data; namespace Avalonia.Controls.Presenters { public class TextPresenter : Control { - public static readonly DirectProperty CaretIndexProperty = - TextBox.CaretIndexProperty.AddOwner( - o => o.CaretIndex, - (o, v) => o.CaretIndex = v); + public static readonly StyledProperty CaretIndexProperty = + TextBox.CaretIndexProperty.AddOwner(new(coerce: TextBox.CoerceCaretIndex)); public static readonly StyledProperty RevealPasswordProperty = AvaloniaProperty.Register(nameof(RevealPassword)); @@ -36,33 +32,23 @@ namespace Avalonia.Controls.Presenters public static readonly StyledProperty CaretBrushProperty = AvaloniaProperty.Register(nameof(CaretBrush)); - public static readonly DirectProperty SelectionStartProperty = - TextBox.SelectionStartProperty.AddOwner( - o => o.SelectionStart, - (o, v) => o.SelectionStart = v); + public static readonly StyledProperty SelectionStartProperty = + TextBox.SelectionStartProperty.AddOwner(new(coerce: TextBox.CoerceCaretIndex)); - public static readonly DirectProperty SelectionEndProperty = - TextBox.SelectionEndProperty.AddOwner( - o => o.SelectionEnd, - (o, v) => o.SelectionEnd = v); + public static readonly StyledProperty SelectionEndProperty = + TextBox.SelectionEndProperty.AddOwner(new(coerce: TextBox.CoerceCaretIndex)); /// /// Defines the property. /// - public static readonly DirectProperty TextProperty = - AvaloniaProperty.RegisterDirect( - nameof(Text), - o => o.Text, - (o, v) => o.Text = v, defaultBindingMode: BindingMode.OneWay); + public static readonly StyledProperty TextProperty = + TextBlock.TextProperty.AddOwner(new(string.Empty)); /// /// Defines the property. /// - public static readonly DirectProperty PreeditTextProperty = - AvaloniaProperty.RegisterDirect( - nameof(PreeditText), - o => o.PreeditText, - (o, v) => o.PreeditText = v); + public static readonly StyledProperty PreeditTextProperty = + AvaloniaProperty.Register(nameof(PreeditText)); /// /// Defines the property. @@ -104,18 +90,13 @@ namespace Avalonia.Controls.Presenters Border.BackgroundProperty.AddOwner(); private readonly DispatcherTimer _caretTimer; - private int _caretIndex; - private int _selectionStart; - private int _selectionEnd; private bool _caretBlink; - internal string? _text; private TextLayout? _textLayout; private Size _constraint; private CharacterHit _lastCharacterHit; private Rect _caretBounds; private Point _navigationPosition; - private string? _preeditText; private TextRange? _compositionRegion; static TextPresenter() @@ -125,7 +106,6 @@ namespace Avalonia.Controls.Presenters public TextPresenter() { - _text = string.Empty; _caretTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) }; _caretTimer.Tick += CaretTimerTick; } @@ -147,14 +127,14 @@ namespace Avalonia.Controls.Presenters [Content] public string? Text { - get => _text; - set => SetAndRaise(TextProperty, ref _text, value); + get => GetValue(TextProperty); + set => SetValue(TextProperty, value); } public string? PreeditText { - get => _preeditText; - set => SetAndRaise(PreeditTextProperty, ref _preeditText, value); + get => GetValue(PreeditTextProperty); + set => SetValue(PreeditTextProperty, value); } public TextRange? CompositionRegion @@ -275,17 +255,8 @@ namespace Avalonia.Controls.Presenters public int CaretIndex { - get - { - return _caretIndex; - } - set - { - if (value != _caretIndex) - { - MoveCaretToTextPosition(value); - } - } + get => GetValue(CaretIndexProperty); + set => SetValue(CaretIndexProperty, value); } public char PasswordChar @@ -320,30 +291,14 @@ namespace Avalonia.Controls.Presenters public int SelectionStart { - get - { - return _selectionStart; - } - - set - { - value = CoerceCaretIndex(value); - SetAndRaise(SelectionStartProperty, ref _selectionStart, value); - } + get => GetValue(SelectionStartProperty); + set => SetValue(SelectionStartProperty, value); } public int SelectionEnd { - get - { - return _selectionEnd; - } - - set - { - value = CoerceCaretIndex(value); - SetAndRaise(SelectionEndProperty, ref _selectionEnd, value); - } + get => GetValue(SelectionEndProperty); + set => SetValue(SelectionEndProperty, value); } protected override bool BypassFlowDirectionPolicies => true; @@ -535,12 +490,12 @@ namespace Avalonia.Controls.Presenters { TextLayout result; - var text = _text; + var text = Text; var typeface = new Typeface(FontFamily, FontStyle, FontWeight); - var selectionStart = CoerceCaretIndex(SelectionStart); - var selectionEnd = CoerceCaretIndex(SelectionEnd); + var selectionStart = SelectionStart; + var selectionEnd = SelectionEnd; var start = Math.Min(selectionStart, selectionEnd); var length = Math.Max(selectionStart, selectionEnd) - start; @@ -561,9 +516,9 @@ namespace Avalonia.Controls.Presenters }; } - else if (!string.IsNullOrEmpty(_preeditText)) + else if (!string.IsNullOrEmpty(PreeditText)) { - var preeditHighlight = new ValueSpan(_caretIndex, _preeditText.Length, + var preeditHighlight = new ValueSpan(CaretIndex, PreeditText.Length, new GenericTextRunProperties(typeface, FontSize, foregroundBrush: foreground, textDecorations: TextDecorations.Underline)); @@ -643,13 +598,6 @@ namespace Avalonia.Controls.Presenters return finalSize; } - private int CoerceCaretIndex(int value) - { - var text = Text; - var length = text?.Length ?? 0; - return Math.Max(0, Math.Min(length, value)); - } - private void CaretTimerTick(object? sender, EventArgs e) { _caretBlink = !_caretBlink; @@ -865,7 +813,7 @@ namespace Avalonia.Controls.Presenters if (notify) { - SetAndRaise(CaretIndexProperty, ref _caretIndex, caretIndex); + SetCurrentValue(CaretIndexProperty, caretIndex); } } @@ -887,6 +835,11 @@ namespace Avalonia.Controls.Presenters { base.OnPropertyChanged(change); + if (change.Property == CaretIndexProperty) + { + MoveCaretToTextPosition(change.GetNewValue()); + } + switch (change.Property.Name) { case nameof(PreeditText): diff --git a/src/Avalonia.Controls/SelectableTextBlock.cs b/src/Avalonia.Controls/SelectableTextBlock.cs index 6603e20a2a..c428d4d160 100644 --- a/src/Avalonia.Controls/SelectableTextBlock.cs +++ b/src/Avalonia.Controls/SelectableTextBlock.cs @@ -17,17 +17,11 @@ namespace Avalonia.Controls /// public class SelectableTextBlock : TextBlock, IInlineHost { - public static readonly DirectProperty SelectionStartProperty = - AvaloniaProperty.RegisterDirect( - nameof(SelectionStart), - o => o.SelectionStart, - (o, v) => o.SelectionStart = v); - - public static readonly DirectProperty SelectionEndProperty = - AvaloniaProperty.RegisterDirect( - nameof(SelectionEnd), - o => o.SelectionEnd, - (o, v) => o.SelectionEnd = v); + public static readonly StyledProperty SelectionStartProperty = + TextBox.SelectionStartProperty.AddOwner(new(coerce: TextBox.CoerceCaretIndex)); + + public static readonly StyledProperty SelectionEndProperty = + TextBox.SelectionEndProperty.AddOwner(new(coerce: TextBox.CoerceCaretIndex)); public static readonly DirectProperty SelectedTextProperty = AvaloniaProperty.RegisterDirect( @@ -35,21 +29,16 @@ namespace Avalonia.Controls o => o.SelectedText); public static readonly StyledProperty SelectionBrushProperty = - AvaloniaProperty.Register(nameof(SelectionBrush), Brushes.Blue); - + TextBox.SelectionBrushProperty.AddOwner(new(new Data.Optional(Brushes.Blue))); public static readonly DirectProperty CanCopyProperty = - AvaloniaProperty.RegisterDirect( - nameof(CanCopy), - o => o.CanCopy); + TextBox.CanCopyProperty.AddOwner(o => o.CanCopy); public static readonly RoutedEvent CopyingToClipboardEvent = RoutedEvent.Register( nameof(CopyingToClipboard), RoutingStrategies.Bubble); private bool _canCopy; - private int _selectionStart; - private int _selectionEnd; private int _wordSelectionStart = -1; static SelectableTextBlock() @@ -78,16 +67,8 @@ namespace Avalonia.Controls /// public int SelectionStart { - get => _selectionStart; - set - { - if (SetAndRaise(SelectionStartProperty, ref _selectionStart, value)) - { - RaisePropertyChanged(SelectedTextProperty, "", ""); - - UpdateCommandStates(); - } - } + get => GetValue(SelectionStartProperty); + set => SetValue(SelectionStartProperty, value); } /// @@ -95,16 +76,8 @@ namespace Avalonia.Controls /// public int SelectionEnd { - get => _selectionEnd; - set - { - if (SetAndRaise(SelectionEndProperty, ref _selectionEnd, value)) - { - RaisePropertyChanged(SelectedTextProperty, "", ""); - - UpdateCommandStates(); - } - } + get => GetValue(SelectionEndProperty); + set => SetValue(SelectionEndProperty, value); } /// @@ -150,7 +123,7 @@ namespace Avalonia.Controls await ((IClipboard)AvaloniaLocator.Current.GetRequiredService(typeof(IClipboard))) .SetTextAsync(text); } - } + } /// /// Select all text in the TextBox @@ -159,8 +132,8 @@ namespace Avalonia.Controls { var text = Text; - SelectionStart = 0; - SelectionEnd = text?.Length ?? 0; + SetCurrentValue(SelectionStartProperty, 0); + SetCurrentValue(SelectionEndProperty, text?.Length ?? 0); } /// @@ -168,7 +141,7 @@ namespace Avalonia.Controls /// public void ClearSelection() { - SelectionEnd = SelectionStart; + SetCurrentValue(SelectionEndProperty, SelectionStart); } protected override void OnGotFocus(GotFocusEventArgs e) @@ -240,6 +213,17 @@ namespace Avalonia.Controls e.Handled = handled; } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == SelectionStartProperty || change.Property == SelectionEndProperty) + { + RaisePropertyChanged(SelectedTextProperty, "", ""); + UpdateCommandStates(); + } + } + protected override void OnPointerPressed(PointerPressedEventArgs e) { base.OnPointerPressed(e); @@ -271,25 +255,26 @@ namespace Avalonia.Controls if (index > _wordSelectionStart) { - SelectionEnd = StringUtils.NextWord(text, index); + SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, index)); } if (index < _wordSelectionStart || previousWord == _wordSelectionStart) { - SelectionStart = previousWord; + SetCurrentValue(SelectionStartProperty, previousWord); } } else { - SelectionStart = Math.Min(oldIndex, index); - SelectionEnd = Math.Max(oldIndex, index); + SetCurrentValue(SelectionStartProperty, Math.Min(oldIndex, index)); + SetCurrentValue(SelectionEndProperty, Math.Max(oldIndex, index)); } } else { if (_wordSelectionStart == -1 || index < SelectionStart || index > SelectionEnd) { - SelectionStart = SelectionEnd = index; + SetCurrentValue(SelectionStartProperty, index); + SetCurrentValue(SelectionEndProperty, index); _wordSelectionStart = -1; } @@ -299,16 +284,16 @@ namespace Avalonia.Controls case 2: if (!StringUtils.IsStartOfWord(text, index)) { - SelectionStart = StringUtils.PreviousWord(text, index); + SetCurrentValue(SelectionStartProperty, StringUtils.PreviousWord(text, index)); } _wordSelectionStart = SelectionStart; if (!StringUtils.IsEndOfWord(text, index)) { - SelectionEnd = StringUtils.NextWord(text, index); + SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, index)); } - + break; case 3: _wordSelectionStart = -1; @@ -347,22 +332,22 @@ namespace Avalonia.Controls if (distance <= 0) { - SelectionStart = StringUtils.PreviousWord(text, textPosition); + SetCurrentValue(SelectionStartProperty, StringUtils.PreviousWord(text, textPosition)); } if (distance >= 0) { if (SelectionStart != _wordSelectionStart) { - SelectionStart = _wordSelectionStart; + SetCurrentValue(SelectionStartProperty, _wordSelectionStart); } - SelectionEnd = StringUtils.NextWord(text, textPosition); + SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, textPosition)); } } else { - SelectionEnd = textPosition; + SetCurrentValue(SelectionEndProperty, textPosition); } } @@ -395,7 +380,8 @@ namespace Avalonia.Controls caretIndex >= firstSelection && caretIndex <= lastSelection; if (!didClickInSelection) { - SelectionStart = SelectionEnd = caretIndex; + SetCurrentValue(SelectionStartProperty, caretIndex); + SetCurrentValue(SelectionEndProperty, caretIndex); } } @@ -411,9 +397,8 @@ namespace Avalonia.Controls private string GetSelection() { - var text = GetText(); - - if (string.IsNullOrEmpty(text)) + var textLength = Text?.Length ?? 0; + if (textLength == 0) { return ""; } @@ -423,14 +408,14 @@ namespace Avalonia.Controls var start = Math.Min(selectionStart, selectionEnd); var end = Math.Max(selectionStart, selectionEnd); - if (start == end || text.Length < end) + if (start == end || textLength < end) { return ""; } var length = Math.Max(0, end - start); - var selectedText = text.Substring(start, length); + var selectedText = Text!.Substring(start, length); return selectedText; } diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index df98d1073e..de1fd045d4 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using Avalonia.Automation.Peers; using Avalonia.Controls.Documents; using Avalonia.Layout; @@ -13,6 +14,7 @@ namespace Avalonia.Controls /// /// A control that displays a block of text. /// + [DebuggerDisplay("{DebugText}")] public class TextBlock : Control, IInlineHost { /// @@ -103,11 +105,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty TextProperty = - AvaloniaProperty.RegisterDirect( - nameof(Text), - o => o.GetText(), - (o, v) => o.SetText(v)); + public static readonly StyledProperty TextProperty = + AvaloniaProperty.Register(nameof(Text)); /// /// Defines the property. @@ -142,14 +141,14 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty InlinesProperty = - AvaloniaProperty.Register( - nameof(Inlines)); + public static readonly DirectProperty InlinesProperty = + AvaloniaProperty.RegisterDirect( + nameof(Inlines), t => t.Inlines, (t, v) => t.Inlines = v); - internal string? _text; protected TextLayout? _textLayout; protected Size _constraint; private IReadOnlyList? _textRuns; + private InlineCollection? _inlines; /// /// Initializes static members of the class. @@ -173,7 +172,7 @@ namespace Avalonia.Controls /// /// Gets the used to render the text. /// - public TextLayout TextLayout => _textLayout ??= CreateTextLayout(_text); + public TextLayout TextLayout => _textLayout ??= CreateTextLayout(Text); /// /// Gets or sets the padding to place around the . @@ -198,10 +197,12 @@ namespace Avalonia.Controls /// public string? Text { - get => GetText(); - set => SetText(value); + get => GetValue(TextProperty); + set => SetValue(TextProperty, value); } + private string? DebugText => Text ?? Inlines?.Text; + /// /// Gets or sets the font family used to draw the control's text. /// @@ -325,8 +326,8 @@ namespace Avalonia.Controls [Content] public InlineCollection? Inlines { - get => GetValue(InlinesProperty); - set => SetValue(InlinesProperty, value); + get => _inlines; + set => SetAndRaise(InlinesProperty, ref _inlines, value); } protected override bool BypassFlowDirectionPolicies => true; @@ -590,19 +591,18 @@ namespace Avalonia.Controls TextLayout.Draw(context, origin); } - protected virtual string? GetText() - { - return _text ?? Inlines?.Text; - } - - protected virtual void SetText(string? text) + private bool _clearTextInternal; + internal void ClearTextInternal() { - if (HasComplexContent) + _clearTextInternal = true; + try + { + SetCurrentValue(TextProperty, null); + } + finally { - Inlines?.Clear(); + _clearTextInternal = false; } - - SetAndRaise(TextProperty, ref _text, text); } /// @@ -780,6 +780,14 @@ namespace Avalonia.Controls { base.OnPropertyChanged(change); + if (change.Property == TextProperty) + { + if (HasComplexContent && !_clearTextInternal) + { + Inlines?.Clear(); + } + } + switch (change.Property.Name) { case nameof(FontSize): @@ -794,10 +802,10 @@ namespace Avalonia.Controls case nameof(FlowDirection): - case nameof (Padding): - case nameof (LineHeight): - case nameof (LetterSpacing): - case nameof (MaxLines): + case nameof(Padding): + case nameof(LineHeight): + case nameof(LetterSpacing): + case nameof(MaxLines): case nameof(Text): case nameof(TextDecorations): @@ -899,7 +907,7 @@ namespace Avalonia.Controls continue; } - if (textRun is TextCharacters) + if (textRun is TextCharacters) { var skip = Math.Max(0, textSourceIndex - currentPosition); diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index ac1b5cd417..d244290c89 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -61,11 +61,9 @@ namespace Avalonia.Controls /// /// Defines the property /// - public static readonly DirectProperty CaretIndexProperty = - AvaloniaProperty.RegisterDirect( - nameof(CaretIndex), - o => o.CaretIndex, - (o, v) => o.CaretIndex = v); + public static readonly StyledProperty CaretIndexProperty = + AvaloniaProperty.Register(nameof(CaretIndex), + coerce: CoerceCaretIndex); /// /// Defines the property @@ -100,42 +98,37 @@ namespace Avalonia.Controls /// /// Defines the property /// - public static readonly DirectProperty SelectionStartProperty = - AvaloniaProperty.RegisterDirect( - nameof(SelectionStart), - o => o.SelectionStart, - (o, v) => o.SelectionStart = v); + public static readonly StyledProperty SelectionStartProperty = + AvaloniaProperty.Register(nameof(SelectionStart), + coerce: CoerceCaretIndex); /// /// Defines the property /// - public static readonly DirectProperty SelectionEndProperty = - AvaloniaProperty.RegisterDirect( - nameof(SelectionEnd), - o => o.SelectionEnd, - (o, v) => o.SelectionEnd = v); + public static readonly StyledProperty SelectionEndProperty = + AvaloniaProperty.Register(nameof(SelectionEnd), + coerce: CoerceCaretIndex); /// /// Defines the property /// public static readonly StyledProperty MaxLengthProperty = - AvaloniaProperty.Register(nameof(MaxLength), defaultValue: 0); + AvaloniaProperty.Register(nameof(MaxLength)); /// /// Defines the property /// public static readonly StyledProperty MaxLinesProperty = - AvaloniaProperty.Register(nameof(MaxLines), defaultValue: 0); + AvaloniaProperty.Register(nameof(MaxLines)); /// /// Defines the property /// - public static readonly DirectProperty TextProperty = - TextBlock.TextProperty.AddOwnerWithDataValidation( - o => o.Text, - (o, v) => o.Text = v, + public static readonly StyledProperty TextProperty = + TextBlock.TextProperty.AddOwner(new( + coerce: CoerceText, defaultBindingMode: BindingMode.TwoWay, - enableDataValidation: true); + enableDataValidation: true)); /// /// Defines the property @@ -185,9 +178,8 @@ namespace Avalonia.Controls /// /// Defines the property /// - public static readonly DirectProperty NewLineProperty = - AvaloniaProperty.RegisterDirect(nameof(NewLine), - textbox => textbox.NewLine, (textbox, newline) => textbox.NewLine = newline); + public static readonly StyledProperty NewLineProperty = + AvaloniaProperty.Register(nameof(NewLine), Environment.NewLine); /// /// Defines the property @@ -242,12 +234,8 @@ namespace Avalonia.Controls /// /// Defines the property /// - public static readonly DirectProperty UndoLimitProperty = - AvaloniaProperty.RegisterDirect( - nameof(UndoLimit), - o => o.UndoLimit, - (o, v) => o.UndoLimit = v, - unsetValue: -1); + public static readonly StyledProperty UndoLimitProperty = + AvaloniaProperty.Register(nameof(UndoLimit), UndoRedoHelper.DefaultUndoLimit); /// /// Defines the property @@ -318,18 +306,13 @@ namespace Avalonia.Controls public override int GetHashCode() => Text?.GetHashCode() ?? 0; } - private string? _text; - private int _caretIndex; - private int _selectionStart; - private int _selectionEnd; private TextPresenter? _presenter; - private TextBoxTextInputMethodClient _imClient = new TextBoxTextInputMethodClient(); - private UndoRedoHelper _undoRedoHelper; + private readonly TextBoxTextInputMethodClient _imClient = new(); + private readonly UndoRedoHelper _undoRedoHelper; private bool _isUndoingRedoing; private bool _canCut; private bool _canCopy; private bool _canPaste; - private string _newLine = Environment.NewLine; private static readonly string[] invalidCharacters = new String[1] { "\u007f" }; private bool _canUndo; private bool _canRedo; @@ -399,18 +382,19 @@ namespace Avalonia.Controls /// public int CaretIndex { - get => _caretIndex; - set - { - value = CoerceCaretIndex(value); - SetAndRaise(CaretIndexProperty, ref _caretIndex, value); + get => GetValue(CaretIndexProperty); + set => SetValue(CaretIndexProperty, value); + } - UndoRedoState state; - if (IsUndoEnabled && _undoRedoHelper.TryGetLastState(out state) && state.Text == Text) - _undoRedoHelper.UpdateLastState(); + private void OnCaretIndexChanged(AvaloniaPropertyChangedEventArgs e) + { + UndoRedoState state; + if (IsUndoEnabled && _undoRedoHelper.TryGetLastState(out state) && state.Text == Text) + _undoRedoHelper.UpdateLastState(); - SelectionStart = SelectionEnd = value; - } + var newValue = e.GetNewValue(); + SetCurrentValue(SelectionStartProperty, newValue); + SetCurrentValue(SelectionEndProperty, newValue); } /// @@ -463,21 +447,18 @@ namespace Avalonia.Controls /// public int SelectionStart { - get => _selectionStart; - set - { - value = CoerceCaretIndex(value); - var changed = SetAndRaise(SelectionStartProperty, ref _selectionStart, value); + get => GetValue(SelectionStartProperty); + set => SetValue(SelectionStartProperty, value); + } - if (changed) - { - UpdateCommandStates(); - } + private void OnSelectionStartChanged(AvaloniaPropertyChangedEventArgs e) + { + UpdateCommandStates(); - if (SelectionEnd == value && CaretIndex != value) - { - CaretIndex = value; - } + var value = e.GetNewValue(); + if (SelectionEnd == value && CaretIndex != value) + { + SetCurrentValue(CaretIndexProperty, value); } } @@ -490,21 +471,18 @@ namespace Avalonia.Controls /// public int SelectionEnd { - get => _selectionEnd; - set - { - value = CoerceCaretIndex(value); - var changed = SetAndRaise(SelectionEndProperty, ref _selectionEnd, value); - - if (changed) - { - UpdateCommandStates(); - } + get => GetValue(SelectionEndProperty); + set => SetValue(SelectionEndProperty, value); + } + + private void OnSelectionEndChanged(AvaloniaPropertyChangedEventArgs e) + { + UpdateCommandStates(); - if (SelectionStart == value && CaretIndex != value) - { - CaretIndex = value; - } + var value = e.GetNewValue(); + if (SelectionStart == value && CaretIndex != value) + { + SetCurrentValue(CaretIndexProperty, value); } } @@ -550,36 +528,27 @@ namespace Avalonia.Controls [Content] public string? Text { - get => _text; - set - { - var caretIndex = CaretIndex; - var selectionStart = SelectionStart; - var selectionEnd = SelectionEnd; - - CaretIndex = CoerceCaretIndex(caretIndex, value); - SelectionStart = CoerceCaretIndex(selectionStart, value); - SelectionEnd = CoerceCaretIndex(selectionEnd, value); - - // Before #9490, snapshot here was done AFTER text change - this doesn't make sense - // since intial state would never be no text and you'd always have to make a text - // change before undo would be available - // The undo/redo stacks were also cleared at this point, which also doesn't make sense - // as it is still valid to want to undo a programmatic text set - // So we snapshot text now BEFORE the change so we can always revert - // Also don't need to check IsUndoEnabled here, that's done in SnapshotUndoRedo - if (!_isUndoingRedoing) - { - SnapshotUndoRedo(); - } - - var textChanged = SetAndRaise(TextProperty, ref _text, value); + get => GetValue(TextProperty); + set => SetValue(TextProperty, value); + } - if (textChanged) - { - RaiseTextChangeEvents(); - } + private static string? CoerceText(AvaloniaObject sender, string? value) + { + var textBox = (TextBox)sender; + + // Before #9490, snapshot here was done AFTER text change - this doesn't make sense + // since intial state would never be no text and you'd always have to make a text + // change before undo would be available + // The undo/redo stacks were also cleared at this point, which also doesn't make sense + // as it is still valid to want to undo a programmatic text set + // So we snapshot text now BEFORE the change so we can always revert + // Also don't need to check IsUndoEnabled here, that's done in SnapshotUndoRedo + if (!textBox._isUndoingRedoing) + { + textBox.SnapshotUndoRedo(); } + + return value; } /// @@ -691,8 +660,8 @@ namespace Avalonia.Controls /// public string NewLine { - get => _newLine; - set => SetAndRaise(NewLineProperty, ref _newLine, value); + get => GetValue(NewLineProperty); + set => SetValue(NewLineProperty, value); } /// @@ -700,7 +669,8 @@ namespace Avalonia.Controls /// public void ClearSelection() { - CaretIndex = SelectionStart; + SetCurrentValue(CaretIndexProperty, SelectionStart); + SetCurrentValue(SelectionEndProperty, SelectionStart); } /// @@ -744,25 +714,20 @@ namespace Avalonia.Controls /// public int UndoLimit { - get => _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(); - _selectedTextChangesMadeSinceLastUndoSnapshot = 0; - _hasDoneSnapshotOnce = false; - } + get => GetValue(UndoLimitProperty); + set => SetValue(UndoLimitProperty, value); + } + + private void OnUndoLimitChanged(int newValue) + { + _undoRedoHelper.Limit = newValue; + + // 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; } /// @@ -866,9 +831,31 @@ namespace Avalonia.Controls if (change.Property == TextProperty) { + CoerceValue(CaretIndexProperty); + CoerceValue(SelectionStartProperty); + CoerceValue(SelectionEndProperty); + + RaiseTextChangeEvents(); + UpdatePseudoclasses(); UpdateCommandStates(); } + else if (change.Property == CaretIndexProperty) + { + OnCaretIndexChanged(change); + } + else if (change.Property == SelectionStartProperty) + { + OnSelectionStartChanged(change); + } + else if (change.Property == SelectionEndProperty) + { + OnSelectionEndChanged(change); + } + else if (change.Property == UndoLimitProperty) + { + OnUndoLimitChanged(change.GetNewValue()); + } else if (change.Property == IsUndoEnabledProperty && change.GetNewValue() == false) { // from docs at @@ -920,7 +907,7 @@ namespace Avalonia.Controls (ContextMenu == null || !ContextMenu.IsOpen)) { ClearSelection(); - RevealPassword = false; + SetCurrentValue(RevealPasswordProperty, false); } UpdateCommandStates(); @@ -986,35 +973,44 @@ namespace Avalonia.Controls } } - var text = Text ?? string.Empty; - var newLength = input.Length + text.Length - Math.Abs(SelectionStart - SelectionEnd); + var currentText = Text ?? string.Empty; + var selectionLength = Math.Abs(SelectionStart - SelectionEnd); + var newLength = input.Length + currentText.Length - selectionLength; if (MaxLength > 0 && newLength > MaxLength) { input = input.Remove(Math.Max(0, input.Length - (newLength - MaxLength))); + newLength = MaxLength; } if (!string.IsNullOrEmpty(input)) { - var oldText = _text; - - DeleteSelection(false); + var textBuilder = StringBuilderCache.Acquire(Math.Max(currentText.Length, newLength)); + textBuilder.Append(currentText); + var caretIndex = CaretIndex; - text = Text ?? string.Empty; - SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex)); - ClearSelection(); - if (IsUndoEnabled) + if (selectionLength != 0) { - _undoRedoHelper.DiscardRedo(); + var (start, _) = GetSelectionRange(); + + textBuilder.Remove(start, selectionLength); + + caretIndex = start; } - if (_text != oldText) + textBuilder.Insert(caretIndex, input); + + SetCurrentValue(TextProperty, StringBuilderCache.GetStringAndRelease(textBuilder)); + + ClearSelection(); + + if (IsUndoEnabled) { - RaisePropertyChanged(TextProperty, oldText, _text); + _undoRedoHelper.DiscardRedo(); } - CaretIndex = caretIndex + input.Length; + SetCurrentValue(CaretIndexProperty, caretIndex + input.Length); } } @@ -1168,7 +1164,7 @@ namespace Avalonia.Controls movement = true; selection = false; handled = true; - CaretIndex = _presenter.CaretIndex; + SetCurrentValue(CaretIndexProperty, _presenter.CaretIndex); } else if (Match(keymap.MoveCursorToTheEndOfDocument)) { @@ -1176,7 +1172,7 @@ namespace Avalonia.Controls movement = true; selection = false; handled = true; - CaretIndex = _presenter.CaretIndex; + SetCurrentValue(CaretIndexProperty, _presenter.CaretIndex); } else if (Match(keymap.MoveCursorToTheStartOfLine)) { @@ -1184,7 +1180,7 @@ namespace Avalonia.Controls movement = true; selection = false; handled = true; - CaretIndex = _presenter.CaretIndex; + SetCurrentValue(CaretIndexProperty, _presenter.CaretIndex); } else if (Match(keymap.MoveCursorToTheEndOfLine)) { @@ -1192,31 +1188,31 @@ namespace Avalonia.Controls movement = true; selection = false; handled = true; - CaretIndex = _presenter.CaretIndex; + SetCurrentValue(CaretIndexProperty, _presenter.CaretIndex); } else if (Match(keymap.MoveCursorToTheStartOfDocumentWithSelection)) { - SelectionStart = caretIndex; + SetCurrentValue(SelectionStartProperty, caretIndex); MoveHome(true); - SelectionEnd = _presenter.CaretIndex; + SetCurrentValue(SelectionEndProperty, _presenter.CaretIndex); movement = true; selection = true; handled = true; } else if (Match(keymap.MoveCursorToTheEndOfDocumentWithSelection)) { - SelectionStart = caretIndex; + SetCurrentValue(SelectionStartProperty, caretIndex); MoveEnd(true); - SelectionEnd = _presenter.CaretIndex; + SetCurrentValue(SelectionEndProperty, _presenter.CaretIndex); movement = true; selection = true; handled = true; } else if (Match(keymap.MoveCursorToTheStartOfLineWithSelection)) { - SelectionStart = caretIndex; + SetCurrentValue(SelectionStartProperty, caretIndex); MoveHome(false); - SelectionEnd = _presenter.CaretIndex; + SetCurrentValue(SelectionEndProperty, _presenter.CaretIndex); movement = true; selection = true; handled = true; @@ -1224,9 +1220,9 @@ namespace Avalonia.Controls } else if (Match(keymap.MoveCursorToTheEndOfLineWithSelection)) { - SelectionStart = caretIndex; + SetCurrentValue(SelectionStartProperty, caretIndex); MoveEnd(false); - SelectionEnd = _presenter.CaretIndex; + SetCurrentValue(SelectionEndProperty, _presenter.CaretIndex); movement = true; selection = true; handled = true; @@ -1261,11 +1257,11 @@ namespace Avalonia.Controls if (selection) { - SelectionEnd = _presenter.CaretIndex; + SetCurrentValue(SelectionEndProperty, _presenter.CaretIndex); } else { - CaretIndex = _presenter.CaretIndex; + SetCurrentValue(CaretIndexProperty, _presenter.CaretIndex); } break; @@ -1283,11 +1279,11 @@ namespace Avalonia.Controls if (selection) { - SelectionEnd = _presenter.CaretIndex; + SetCurrentValue(SelectionEndProperty, _presenter.CaretIndex); } else { - CaretIndex = _presenter.CaretIndex; + SetCurrentValue(CaretIndexProperty, _presenter.CaretIndex); } break; @@ -1314,11 +1310,13 @@ namespace Avalonia.Controls var length = end - start; - var editedText = text.Substring(0, start) + text.Substring(Math.Min(end, text.Length)); + var sb = StringBuilderCache.Acquire(text.Length); + sb.Append(text); + sb.Remove(start, end - start); - SetTextInternal(editedText); + SetCurrentValue(TextProperty, StringBuilderCache.GetStringAndRelease(sb)); - CaretIndex = start; + SetCurrentValue(CaretIndexProperty, start); } } @@ -1346,9 +1344,11 @@ namespace Avalonia.Controls var start = Math.Min(nextPosition, caretIndex); var end = Math.Max(nextPosition, caretIndex); - var editedText = text.Substring(0, start) + text.Substring(Math.Min(end, text.Length)); + var sb = StringBuilderCache.Acquire(text.Length); + sb.Append(text); + sb.Remove(start, end - start); - SetTextInternal(editedText); + SetCurrentValue(TextProperty, StringBuilderCache.GetStringAndRelease(sb)); } } @@ -1425,7 +1425,7 @@ namespace Avalonia.Controls var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift); - SetAndRaise(CaretIndexProperty, ref _caretIndex, index); + SetCurrentValue(CaretIndexProperty, index); switch (e.ClickCount) { @@ -1438,25 +1438,26 @@ namespace Avalonia.Controls if (index > _wordSelectionStart) { - SelectionEnd = StringUtils.NextWord(text, index); + SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, index)); } if (index < _wordSelectionStart || previousWord == _wordSelectionStart) { - SelectionStart = previousWord; + SetCurrentValue(SelectionStartProperty, previousWord); } } else { - SelectionStart = Math.Min(oldIndex, index); - SelectionEnd = Math.Max(oldIndex, index); + SetCurrentValue(SelectionStartProperty, Math.Min(oldIndex, index)); + SetCurrentValue(SelectionEndProperty, Math.Max(oldIndex, index)); } } else { if(_wordSelectionStart == -1 || index < SelectionStart || index > SelectionEnd) { - SelectionStart = SelectionEnd = index; + SetCurrentValue(SelectionStartProperty, index); + SetCurrentValue(SelectionEndProperty, index); _wordSelectionStart = -1; } } @@ -1466,14 +1467,14 @@ namespace Avalonia.Controls if (!StringUtils.IsStartOfWord(text, index)) { - SelectionStart = StringUtils.PreviousWord(text, index); + SetCurrentValue(SelectionStartProperty, StringUtils.PreviousWord(text, index)); } _wordSelectionStart = SelectionStart; if (!StringUtils.IsEndOfWord(text, index)) { - SelectionEnd = StringUtils.NextWord(text, index); + SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, index)); } break; @@ -1517,22 +1518,22 @@ namespace Avalonia.Controls if (distance <= 0) { - SelectionStart = StringUtils.PreviousWord(text, caretIndex); + SetCurrentValue(SelectionStartProperty, StringUtils.PreviousWord(text, caretIndex)); } if (distance >= 0) { if(SelectionStart != _wordSelectionStart) { - SelectionStart = _wordSelectionStart; + SetCurrentValue(SelectionStartProperty, _wordSelectionStart); } - SelectionEnd = StringUtils.NextWord(text, caretIndex); + SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, caretIndex)); } } else { - SelectionEnd = caretIndex; + SetCurrentValue(SelectionEndProperty, caretIndex); } } } @@ -1565,7 +1566,9 @@ namespace Avalonia.Controls caretIndex >= firstSelection && caretIndex <= lastSelection; if (!didClickInSelection) { - CaretIndex = SelectionEnd = SelectionStart = caretIndex; + SetCurrentValue(CaretIndexProperty, caretIndex); + SetCurrentValue(SelectionEndProperty, caretIndex); + SetCurrentValue(SelectionStartProperty, caretIndex); } } @@ -1588,10 +1591,10 @@ namespace Avalonia.Controls } } - private int CoerceCaretIndex(int value) => CoerceCaretIndex(value, Text); - - private static int CoerceCaretIndex(int value, string? text) + internal static int CoerceCaretIndex(AvaloniaObject sender, int value) { + var text = sender.GetValue(TextProperty); // method also used by TextPresenter and SelectableTextBlock + if (text == null) { return 0; @@ -1619,10 +1622,7 @@ namespace Avalonia.Controls /// /// Clears the text in the TextBox /// - public void Clear() - { - Text = string.Empty; - } + public void Clear() => SetCurrentValue(TextProperty, string.Empty); private void MoveHorizontal(int direction, bool wholeWord, bool isSelecting) { @@ -1645,7 +1645,7 @@ namespace Avalonia.Controls LogicalDirection.Forward : LogicalDirection.Backward); - SelectionEnd = _presenter.CaretIndex; + SetCurrentValue(SelectionEndProperty, _presenter.CaretIndex); } else { @@ -1662,7 +1662,7 @@ namespace Avalonia.Controls LogicalDirection.Backward); } - CaretIndex = _presenter.CaretIndex; + SetCurrentValue(CaretIndexProperty, _presenter.CaretIndex); } } else @@ -1678,17 +1678,17 @@ namespace Avalonia.Controls offset = StringUtils.PreviousWord(text, selectionEnd) - selectionEnd; } - SelectionEnd += offset; + SetCurrentValue(SelectionEndProperty, SelectionEnd + offset); _presenter.MoveCaretToTextPosition(SelectionEnd); if (!isSelecting) { - CaretIndex = SelectionEnd; + SetCurrentValue(CaretIndexProperty, SelectionEnd); } else { - SelectionStart = selectionStart; + SetCurrentValue(SelectionStartProperty, selectionStart); } } } @@ -1747,36 +1747,45 @@ namespace Avalonia.Controls /// public void SelectAll() { - SelectionStart = 0; - SelectionEnd = Text?.Length ?? 0; + SetCurrentValue(SelectionStartProperty, 0); + SetCurrentValue(SelectionEndProperty, Text?.Length ?? 0); } - internal bool DeleteSelection(bool raiseTextChanged = true) + private (int start, int end) GetSelectionRange() + { + var selectionStart = SelectionStart; + var selectionEnd = SelectionEnd; + + return (Math.Min(selectionStart, selectionEnd), Math.Max(selectionStart, selectionEnd)); + } + + internal bool DeleteSelection() { if (IsReadOnly) return true; - var selectionStart = SelectionStart; - var selectionEnd = SelectionEnd; + var (start, end) = GetSelectionRange(); - if (selectionStart != selectionEnd) + if (start != end) { - var start = Math.Min(selectionStart, selectionEnd); - var end = Math.Max(selectionStart, selectionEnd); var text = Text!; + var textBuilder = StringBuilderCache.Acquire(text.Length); - SetTextInternal(text.Substring(0, start) + text.Substring(end), raiseTextChanged); + textBuilder.Append(text); + textBuilder.Remove(start, end - start); + + SetCurrentValue(TextProperty, textBuilder.ToString()); _presenter?.MoveCaretToTextPosition(start); - CaretIndex = start; + SetCurrentValue(CaretIndexProperty, start); ClearSelection(); return true; } - CaretIndex = SelectionStart; + SetCurrentValue(CaretIndexProperty, SelectionStart); return false; } @@ -1826,46 +1835,30 @@ namespace Avalonia.Controls }, DispatcherPriority.Normal); } - private void SetTextInternal(string value, bool raiseTextChanged = true) - { - if (raiseTextChanged) - { - bool textChanged = SetAndRaise(TextProperty, ref _text, value); - - if (textChanged) - { - RaiseTextChangeEvents(); - } - } - else - { - _text = value; - } - } - private void SetSelectionForControlBackspace() { var selectionStart = CaretIndex; MoveHorizontal(-1, true, false); - SelectionStart = selectionStart; + SetCurrentValue(SelectionStartProperty, selectionStart); } private void SetSelectionForControlDelete() { - if (_text == null || _presenter == null) + var textLength = Text?.Length ?? 0; + if (_presenter == null || textLength == 0) { return; } - SelectionStart = CaretIndex; + SetCurrentValue(SelectionStartProperty, CaretIndex); MoveHorizontal(1, true, true); - if (SelectionEnd < _text.Length && _text[SelectionEnd] == ' ') + if (SelectionEnd < textLength && Text![SelectionEnd] == ' ') { - SelectionEnd++; + SetCurrentValue(SelectionEndProperty, SelectionEnd + 1); } } @@ -1881,8 +1874,8 @@ namespace Avalonia.Controls get => new UndoRedoState(Text, CaretIndex); set { - Text = value.Text; - CaretIndex = value.CaretPosition; + SetCurrentValue(TextProperty, value.Text); + SetCurrentValue(CaretIndexProperty, value.CaretPosition); ClearSelection(); } } diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index deace4084c..9c6f784a15 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -177,7 +177,7 @@ namespace Avalonia.Controls var text = GetText(preeditText); - _presenter._text = text; + _presenter.SetCurrentValue(TextPresenter.TextProperty, text); _presenter.PreeditText = preeditText; diff --git a/src/Avalonia.Controls/Utils/UndoRedoHelper.cs b/src/Avalonia.Controls/Utils/UndoRedoHelper.cs index 7b663dd017..3452b8bb03 100644 --- a/src/Avalonia.Controls/Utils/UndoRedoHelper.cs +++ b/src/Avalonia.Controls/Utils/UndoRedoHelper.cs @@ -4,6 +4,8 @@ namespace Avalonia.Controls.Utils { class UndoRedoHelper { + public const int DefaultUndoLimit = 10; + private readonly IUndoRedoHost _host; public interface IUndoRedoHost @@ -23,7 +25,7 @@ namespace Avalonia.Controls.Utils /// Maximum number of states this helper can store for undo/redo. /// If -1, no limit is imposed. /// - public int Limit { get; set; } = 10; + public int Limit { get; set; } = DefaultUndoLimit; public bool CanUndo => _currentNode?.Previous != null; diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.cs index 66fad557d5..1e5674cc21 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.cs @@ -7,23 +7,18 @@ namespace Avalonia.Diagnostics.Controls { internal class FilterTextBox : TextBox, IStyleable { - public static readonly DirectProperty UseRegexFilterProperty = - AvaloniaProperty.RegisterDirect(nameof(UseRegexFilter), - o => o.UseRegexFilter, (o, v) => o.UseRegexFilter = v, + public static readonly StyledProperty UseRegexFilterProperty = + AvaloniaProperty.Register(nameof(UseRegexFilter), defaultBindingMode: BindingMode.TwoWay); - public static readonly DirectProperty UseCaseSensitiveFilterProperty = - AvaloniaProperty.RegisterDirect(nameof(UseCaseSensitiveFilter), - o => o.UseCaseSensitiveFilter, (o, v) => o.UseCaseSensitiveFilter = v, + public static readonly StyledProperty UseCaseSensitiveFilterProperty = + AvaloniaProperty.Register(nameof(UseCaseSensitiveFilter), defaultBindingMode: BindingMode.TwoWay); - public static readonly DirectProperty UseWholeWordFilterProperty = - AvaloniaProperty.RegisterDirect(nameof(UseWholeWordFilter), - o => o.UseWholeWordFilter, (o, v) => o.UseWholeWordFilter = v, + public static readonly StyledProperty UseWholeWordFilterProperty = + AvaloniaProperty.Register(nameof(UseWholeWordFilter), defaultBindingMode: BindingMode.TwoWay); - private bool _useRegexFilter, _useCaseSensitiveFilter, _useWholeWordFilter; - public FilterTextBox() { Classes.Add("filter-text-box"); @@ -31,20 +26,20 @@ namespace Avalonia.Diagnostics.Controls public bool UseRegexFilter { - get => _useRegexFilter; - set => SetAndRaise(UseRegexFilterProperty, ref _useRegexFilter, value); + get => GetValue(UseRegexFilterProperty); + set => SetValue(UseRegexFilterProperty, value); } public bool UseCaseSensitiveFilter { - get => _useCaseSensitiveFilter; - set => SetAndRaise(UseCaseSensitiveFilterProperty, ref _useCaseSensitiveFilter, value); + get => GetValue(UseCaseSensitiveFilterProperty); + set => SetValue(UseCaseSensitiveFilterProperty,value); } public bool UseWholeWordFilter { - get => _useWholeWordFilter; - set => SetAndRaise(UseWholeWordFilterProperty, ref _useWholeWordFilter, value); + get => GetValue(UseWholeWordFilterProperty); + set => SetValue(UseWholeWordFilterProperty, value); } Type IStyleable.StyleKey => typeof(TextBox); diff --git a/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs b/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs index 33023c4851..009a26ba18 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs @@ -72,51 +72,51 @@ namespace Avalonia.Base.UnitTests.Styling [Fact] public void Can_Set_Direct_Property_In_Style_Without_Activator() { - var control = new TextBlock(); + var control = new DirectPropertyClass(); var target = new Setter(); - var style = new Style(x => x.Is()) + var style = new Style(x => x.Is()) { Setters = { - new Setter(TextBlock.TextProperty, "foo"), + new Setter(DirectPropertyClass.FooProperty, "foo"), } }; Apply(style, control); - Assert.Equal("foo", control.Text); + Assert.Equal("foo", control.Foo); } [Fact] public void Can_Set_Direct_Property_Binding_In_Style_Without_Activator() { - var control = new TextBlock(); + var control = new DirectPropertyClass(); var target = new Setter(); var source = new BehaviorSubject("foo"); - var style = new Style(x => x.Is()) + var style = new Style(x => x.Is()) { Setters = { - new Setter(TextBlock.TextProperty, source.ToBinding()), + new Setter(DirectPropertyClass.FooProperty, source.ToBinding()), } }; Apply(style, control); - Assert.Equal("foo", control.Text); + Assert.Equal("foo", control.Foo); } [Fact] public void Cannot_Set_Direct_Property_Binding_In_Style_With_Activator() { - var control = new TextBlock(); + var control = new DirectPropertyClass(); var target = new Setter(); var source = new BehaviorSubject("foo"); - var style = new Style(x => x.Is().Class("foo")) + var style = new Style(x => x.Is().Class("foo")) { Setters = { - new Setter(TextBlock.TextProperty, source.ToBinding()), + new Setter(DirectPropertyClass.FooProperty, source.ToBinding()), } }; @@ -126,13 +126,13 @@ namespace Avalonia.Base.UnitTests.Styling [Fact] public void Cannot_Set_Direct_Property_In_Style_With_Activator() { - var control = new TextBlock(); + var control = new DirectPropertyClass(); var target = new Setter(); - var style = new Style(x => x.Is().Class("foo")) + var style = new Style(x => x.Is().Class("foo")) { Setters = { - new Setter(TextBlock.TextProperty, "foo"), + new Setter(DirectPropertyClass.FooProperty, "foo"), } }; @@ -288,18 +288,18 @@ namespace Avalonia.Base.UnitTests.Styling { using var app = UnitTestApplication.Start(TestServices.MockThreadingInterface); var data = new Data { Foo = "foo" }; - var control = new TextBox + var control = new DirectPropertyClass { DataContext = data, }; - var style = new Style(x => x.OfType()) + var style = new Style(x => x.OfType()) { Setters = { new Setter { - Property = TextBox.TextProperty, + Property = DirectPropertyClass.FooProperty, Value = new Binding { Path = "Foo", @@ -310,9 +310,9 @@ namespace Avalonia.Base.UnitTests.Styling }; Apply(style, control); - Assert.Equal("foo", control.Text); + Assert.Equal("foo", control.Foo); - control.Text = "bar"; + control.Foo = "bar"; Assert.Equal("bar", data.Foo); } @@ -502,9 +502,9 @@ namespace Avalonia.Base.UnitTests.Styling Assert.Equal(Brushes.Blue, data.Bar); } - private void Apply(Style style, Control control) + private void Apply(Style style, StyledElement element) { - StyleHelpers.TryAttach(style, control); + StyleHelpers.TryAttach(style, element); } private void Apply(Setter setter, Control control) @@ -535,5 +535,18 @@ namespace Avalonia.Base.UnitTests.Styling throw new NotImplementedException(); } } + + private class DirectPropertyClass : StyledElement + { + public static readonly DirectProperty FooProperty = AvaloniaProperty.RegisterDirect(nameof(Foo), + x => x.Foo, (x, v) => x.Foo = v); + + private string? _foo; + public string? Foo + { + get => _foo; + set => SetAndRaise(FooProperty, ref _foo, value); + } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs index de5e5a8ea3..d8da198f5b 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs @@ -192,7 +192,7 @@ namespace Avalonia.Controls.UnitTests target.Inlines.Add(new Run("Hello World")); - Assert.Equal("Hello World", target.Text); + Assert.Equal(null, target.Text); Assert.Equal(1, target.Inlines.Count);