Browse Source

Convert Text properties to StyledProperty

pull/10617/head
Tom Edwards 3 years ago
parent
commit
86324dca77
  1. 12
      src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.Properties.cs
  2. 3
      src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs
  3. 8
      src/Avalonia.Controls/Documents/InlineCollection.cs
  4. 135
      src/Avalonia.Controls/MaskedTextBox.cs
  5. 109
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  6. 105
      src/Avalonia.Controls/SelectableTextBlock.cs
  7. 66
      src/Avalonia.Controls/TextBlock.cs
  8. 433
      src/Avalonia.Controls/TextBox.cs
  9. 2
      src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
  10. 4
      src/Avalonia.Controls/Utils/UndoRedoHelper.cs
  11. 29
      src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.cs
  12. 55
      tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs
  13. 2
      tests/Avalonia.Controls.UnitTests/TextBlockTests.cs

12
src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.Properties.cs

@ -87,12 +87,10 @@ namespace Avalonia.Controls
/// Identifies the <see cref="Text" /> property.
/// </summary>
/// <value>The identifier for the <see cref="Text" /> property.</value>
public static readonly DirectProperty<AutoCompleteBox, string?> TextProperty =
TextBlock.TextProperty.AddOwnerWithDataValidation<AutoCompleteBox>(
o => o.Text,
(o, v) => o.Text = v,
public static readonly StyledProperty<string?> TextProperty =
TextBlock.TextProperty.AddOwner<AutoCompleteBox>(new(string.Empty,
defaultBindingMode: BindingMode.TwoWay,
enableDataValidation: true);
enableDataValidation: true));
/// <summary>
/// Identifies the <see cref="SearchText" /> property.
@ -317,8 +315,8 @@ namespace Avalonia.Controls
/// <see cref="AutoCompleteBox" /> control.</value>
public string? Text
{
get => _text;
set => SetAndRaise(TextProperty, ref _text, value);
get => GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
/// <summary>

3
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<object?>? _itemFilter;
@ -1275,7 +1274,7 @@ namespace Avalonia.Controls
if ((userInitiated ?? true) && Text != value)
{
_ignoreTextPropertyChange++;
Text = value;
SetCurrentValue(TextProperty, value);
callTextChanged = true;
}

8
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
{

135
src/Avalonia.Controls/MaskedTextBox.cs

@ -15,9 +15,8 @@ namespace Avalonia.Controls
public static readonly StyledProperty<bool> AsciiOnlyProperty =
AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(AsciiOnly));
public static readonly DirectProperty<MaskedTextBox, CultureInfo?> CultureProperty =
AvaloniaProperty.RegisterDirect<MaskedTextBox, CultureInfo?>(nameof(Culture), o => o.Culture,
(o, v) => o.Culture = v, CultureInfo.CurrentCulture);
public static readonly StyledProperty<CultureInfo?> CultureProperty =
AvaloniaProperty.Register<MaskedTextBox, CultureInfo?>(nameof(Culture), CultureInfo.CurrentCulture);
public static readonly StyledProperty<bool> HidePromptOnLeaveProperty =
AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(HidePromptOnLeave));
@ -32,26 +31,49 @@ namespace Avalonia.Controls
AvaloniaProperty.Register<MaskedTextBox, string?>(nameof(Mask), string.Empty);
public static readonly StyledProperty<char> PromptCharProperty =
AvaloniaProperty.Register<MaskedTextBox, char>(nameof(PromptChar), '_');
AvaloniaProperty.Register<MaskedTextBox, char>(nameof(PromptChar), '_', coerce: CoercePromptChar);
public static readonly DirectProperty<MaskedTextBox, bool> ResetOnPromptProperty =
AvaloniaProperty.RegisterDirect<MaskedTextBox, bool>(nameof(ResetOnPrompt), o => o.ResetOnPrompt, (o, v) => o.ResetOnPrompt = v);
public static readonly StyledProperty<bool> ResetOnPromptProperty =
AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(ResetOnPrompt), true);
public static readonly DirectProperty<MaskedTextBox, bool> ResetOnSpaceProperty =
AvaloniaProperty.RegisterDirect<MaskedTextBox, bool>(nameof(ResetOnSpace), o => o.ResetOnSpace, (o, v) => o.ResetOnSpace = v);
public static readonly StyledProperty<bool> ResetOnSpaceProperty =
AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(ResetOnSpace), true);
private CultureInfo? _culture;
private bool _ignoreTextChanges;
private bool _resetOnPrompt = true;
static MaskedTextBox()
{
PasswordCharProperty.OverrideMetadata<MaskedTextBox>(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<MaskedTextBox>('\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
/// <summary>
/// Constructs the MaskedTextBox with the specified MaskedTextProvider object.
/// </summary>
[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
/// </summary>
public CultureInfo? Culture
{
get => _culture;
set => SetAndRaise(CultureProperty, ref _culture, value);
get => GetValue(CultureProperty);
set => SetValue(CultureProperty, value);
}
/// <summary>
@ -131,15 +156,6 @@ namespace Avalonia.Controls
/// </summary>
public MaskedTextProvider? MaskProvider { get; private set; }
/// <summary>
/// Gets or sets the character to be displayed in substitute for user input.
/// </summary>
public new char PasswordChar
{
get => GetValue(PasswordCharProperty);
set => SetValue(PasswordCharProperty, value);
}
/// <summary>
/// Gets or sets the character used to represent the absence of user input in MaskedTextBox.
/// </summary>
@ -154,16 +170,8 @@ namespace Avalonia.Controls
/// </summary>
public bool ResetOnPrompt
{
get => _resetOnPrompt;
set
{
SetAndRaise(ResetOnPromptProperty, ref _resetOnPrompt, value);
if (MaskProvider != null)
{
MaskProvider.ResetOnPrompt = value;
}
}
get => GetValue(ResetOnPromptProperty);
set => SetValue(ResetOnPromptProperty, value);
}
/// <summary>
@ -171,16 +179,8 @@ namespace Avalonia.Controls
/// </summary>
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<bool>() 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<bool>() 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);
}
}

109
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<TextPresenter, int> CaretIndexProperty =
TextBox.CaretIndexProperty.AddOwner<TextPresenter>(
o => o.CaretIndex,
(o, v) => o.CaretIndex = v);
public static readonly StyledProperty<int> CaretIndexProperty =
TextBox.CaretIndexProperty.AddOwner<TextPresenter>(new(coerce: TextBox.CoerceCaretIndex));
public static readonly StyledProperty<bool> RevealPasswordProperty =
AvaloniaProperty.Register<TextPresenter, bool>(nameof(RevealPassword));
@ -36,33 +32,23 @@ namespace Avalonia.Controls.Presenters
public static readonly StyledProperty<IBrush?> CaretBrushProperty =
AvaloniaProperty.Register<TextPresenter, IBrush?>(nameof(CaretBrush));
public static readonly DirectProperty<TextPresenter, int> SelectionStartProperty =
TextBox.SelectionStartProperty.AddOwner<TextPresenter>(
o => o.SelectionStart,
(o, v) => o.SelectionStart = v);
public static readonly StyledProperty<int> SelectionStartProperty =
TextBox.SelectionStartProperty.AddOwner<TextPresenter>(new(coerce: TextBox.CoerceCaretIndex));
public static readonly DirectProperty<TextPresenter, int> SelectionEndProperty =
TextBox.SelectionEndProperty.AddOwner<TextPresenter>(
o => o.SelectionEnd,
(o, v) => o.SelectionEnd = v);
public static readonly StyledProperty<int> SelectionEndProperty =
TextBox.SelectionEndProperty.AddOwner<TextPresenter>(new(coerce: TextBox.CoerceCaretIndex));
/// <summary>
/// Defines the <see cref="Text"/> property.
/// </summary>
public static readonly DirectProperty<TextPresenter, string?> TextProperty =
AvaloniaProperty.RegisterDirect<TextPresenter, string?>(
nameof(Text),
o => o.Text,
(o, v) => o.Text = v, defaultBindingMode: BindingMode.OneWay);
public static readonly StyledProperty<string?> TextProperty =
TextBlock.TextProperty.AddOwner<TextPresenter>(new(string.Empty));
/// <summary>
/// Defines the <see cref="PreeditText"/> property.
/// </summary>
public static readonly DirectProperty<TextPresenter, string?> PreeditTextProperty =
AvaloniaProperty.RegisterDirect<TextPresenter, string?>(
nameof(PreeditText),
o => o.PreeditText,
(o, v) => o.PreeditText = v);
public static readonly StyledProperty<string?> PreeditTextProperty =
AvaloniaProperty.Register<TextPresenter, string?>(nameof(PreeditText));
/// <summary>
/// Defines the <see cref="CompositionRegion"/> property.
@ -104,18 +90,13 @@ namespace Avalonia.Controls.Presenters
Border.BackgroundProperty.AddOwner<TextPresenter>();
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<TextRunProperties>(_caretIndex, _preeditText.Length,
var preeditHighlight = new ValueSpan<TextRunProperties>(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<int>());
}
switch (change.Property.Name)
{
case nameof(PreeditText):

105
src/Avalonia.Controls/SelectableTextBlock.cs

@ -17,17 +17,11 @@ namespace Avalonia.Controls
/// </summary>
public class SelectableTextBlock : TextBlock, IInlineHost
{
public static readonly DirectProperty<SelectableTextBlock, int> SelectionStartProperty =
AvaloniaProperty.RegisterDirect<SelectableTextBlock, int>(
nameof(SelectionStart),
o => o.SelectionStart,
(o, v) => o.SelectionStart = v);
public static readonly DirectProperty<SelectableTextBlock, int> SelectionEndProperty =
AvaloniaProperty.RegisterDirect<SelectableTextBlock, int>(
nameof(SelectionEnd),
o => o.SelectionEnd,
(o, v) => o.SelectionEnd = v);
public static readonly StyledProperty<int> SelectionStartProperty =
TextBox.SelectionStartProperty.AddOwner<SelectableTextBlock>(new(coerce: TextBox.CoerceCaretIndex));
public static readonly StyledProperty<int> SelectionEndProperty =
TextBox.SelectionEndProperty.AddOwner<SelectableTextBlock>(new(coerce: TextBox.CoerceCaretIndex));
public static readonly DirectProperty<SelectableTextBlock, string> SelectedTextProperty =
AvaloniaProperty.RegisterDirect<SelectableTextBlock, string>(
@ -35,21 +29,16 @@ namespace Avalonia.Controls
o => o.SelectedText);
public static readonly StyledProperty<IBrush?> SelectionBrushProperty =
AvaloniaProperty.Register<SelectableTextBlock, IBrush?>(nameof(SelectionBrush), Brushes.Blue);
TextBox.SelectionBrushProperty.AddOwner<SelectableTextBlock>(new(new Data.Optional<IBrush?>(Brushes.Blue)));
public static readonly DirectProperty<SelectableTextBlock, bool> CanCopyProperty =
AvaloniaProperty.RegisterDirect<SelectableTextBlock, bool>(
nameof(CanCopy),
o => o.CanCopy);
TextBox.CanCopyProperty.AddOwner<SelectableTextBlock>(o => o.CanCopy);
public static readonly RoutedEvent<RoutedEventArgs> CopyingToClipboardEvent =
RoutedEvent.Register<SelectableTextBlock, RoutedEventArgs>(
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
/// </summary>
public int SelectionStart
{
get => _selectionStart;
set
{
if (SetAndRaise(SelectionStartProperty, ref _selectionStart, value))
{
RaisePropertyChanged(SelectedTextProperty, "", "");
UpdateCommandStates();
}
}
get => GetValue(SelectionStartProperty);
set => SetValue(SelectionStartProperty, value);
}
/// <summary>
@ -95,16 +76,8 @@ namespace Avalonia.Controls
/// </summary>
public int SelectionEnd
{
get => _selectionEnd;
set
{
if (SetAndRaise(SelectionEndProperty, ref _selectionEnd, value))
{
RaisePropertyChanged(SelectedTextProperty, "", "");
UpdateCommandStates();
}
}
get => GetValue(SelectionEndProperty);
set => SetValue(SelectionEndProperty, value);
}
/// <summary>
@ -150,7 +123,7 @@ namespace Avalonia.Controls
await ((IClipboard)AvaloniaLocator.Current.GetRequiredService(typeof(IClipboard)))
.SetTextAsync(text);
}
}
}
/// <summary>
/// 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);
}
/// <summary>
@ -168,7 +141,7 @@ namespace Avalonia.Controls
/// </summary>
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;
}

66
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
/// <summary>
/// A control that displays a block of text.
/// </summary>
[DebuggerDisplay("{DebugText}")]
public class TextBlock : Control, IInlineHost
{
/// <summary>
@ -103,11 +105,8 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="Text"/> property.
/// </summary>
public static readonly DirectProperty<TextBlock, string?> TextProperty =
AvaloniaProperty.RegisterDirect<TextBlock, string?>(
nameof(Text),
o => o.GetText(),
(o, v) => o.SetText(v));
public static readonly StyledProperty<string?> TextProperty =
AvaloniaProperty.Register<TextBlock, string?>(nameof(Text));
/// <summary>
/// Defines the <see cref="TextAlignment"/> property.
@ -142,14 +141,14 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="Inlines"/> property.
/// </summary>
public static readonly StyledProperty<InlineCollection?> InlinesProperty =
AvaloniaProperty.Register<TextBlock, InlineCollection?>(
nameof(Inlines));
public static readonly DirectProperty<TextBlock, InlineCollection?> InlinesProperty =
AvaloniaProperty.RegisterDirect<TextBlock, InlineCollection?>(
nameof(Inlines), t => t.Inlines, (t, v) => t.Inlines = v);
internal string? _text;
protected TextLayout? _textLayout;
protected Size _constraint;
private IReadOnlyList<TextRun>? _textRuns;
private InlineCollection? _inlines;
/// <summary>
/// Initializes static members of the <see cref="TextBlock"/> class.
@ -173,7 +172,7 @@ namespace Avalonia.Controls
/// <summary>
/// Gets the <see cref="TextLayout"/> used to render the text.
/// </summary>
public TextLayout TextLayout => _textLayout ??= CreateTextLayout(_text);
public TextLayout TextLayout => _textLayout ??= CreateTextLayout(Text);
/// <summary>
/// Gets or sets the padding to place around the <see cref="Text"/>.
@ -198,10 +197,12 @@ namespace Avalonia.Controls
/// </summary>
public string? Text
{
get => GetText();
set => SetText(value);
get => GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
private string? DebugText => Text ?? Inlines?.Text;
/// <summary>
/// Gets or sets the font family used to draw the control's text.
/// </summary>
@ -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);
}
/// <summary>
@ -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);

433
src/Avalonia.Controls/TextBox.cs

@ -61,11 +61,9 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="CaretIndex"/> property
/// </summary>
public static readonly DirectProperty<TextBox, int> CaretIndexProperty =
AvaloniaProperty.RegisterDirect<TextBox, int>(
nameof(CaretIndex),
o => o.CaretIndex,
(o, v) => o.CaretIndex = v);
public static readonly StyledProperty<int> CaretIndexProperty =
AvaloniaProperty.Register<TextBox, int>(nameof(CaretIndex),
coerce: CoerceCaretIndex);
/// <summary>
/// Defines the <see cref="IsReadOnly"/> property
@ -100,42 +98,37 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="SelectionStart"/> property
/// </summary>
public static readonly DirectProperty<TextBox, int> SelectionStartProperty =
AvaloniaProperty.RegisterDirect<TextBox, int>(
nameof(SelectionStart),
o => o.SelectionStart,
(o, v) => o.SelectionStart = v);
public static readonly StyledProperty<int> SelectionStartProperty =
AvaloniaProperty.Register<TextBox, int>(nameof(SelectionStart),
coerce: CoerceCaretIndex);
/// <summary>
/// Defines the <see cref="SelectionEnd"/> property
/// </summary>
public static readonly DirectProperty<TextBox, int> SelectionEndProperty =
AvaloniaProperty.RegisterDirect<TextBox, int>(
nameof(SelectionEnd),
o => o.SelectionEnd,
(o, v) => o.SelectionEnd = v);
public static readonly StyledProperty<int> SelectionEndProperty =
AvaloniaProperty.Register<TextBox, int>(nameof(SelectionEnd),
coerce: CoerceCaretIndex);
/// <summary>
/// Defines the <see cref="MaxLength"/> property
/// </summary>
public static readonly StyledProperty<int> MaxLengthProperty =
AvaloniaProperty.Register<TextBox, int>(nameof(MaxLength), defaultValue: 0);
AvaloniaProperty.Register<TextBox, int>(nameof(MaxLength));
/// <summary>
/// Defines the <see cref="MaxLines"/> property
/// </summary>
public static readonly StyledProperty<int> MaxLinesProperty =
AvaloniaProperty.Register<TextBox, int>(nameof(MaxLines), defaultValue: 0);
AvaloniaProperty.Register<TextBox, int>(nameof(MaxLines));
/// <summary>
/// Defines the <see cref="Text"/> property
/// </summary>
public static readonly DirectProperty<TextBox, string?> TextProperty =
TextBlock.TextProperty.AddOwnerWithDataValidation<TextBox>(
o => o.Text,
(o, v) => o.Text = v,
public static readonly StyledProperty<string?> TextProperty =
TextBlock.TextProperty.AddOwner<TextBox>(new(
coerce: CoerceText,
defaultBindingMode: BindingMode.TwoWay,
enableDataValidation: true);
enableDataValidation: true));
/// <summary>
/// Defines the <see cref="TextAlignment"/> property
@ -185,9 +178,8 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="NewLine"/> property
/// </summary>
public static readonly DirectProperty<TextBox, string> NewLineProperty =
AvaloniaProperty.RegisterDirect<TextBox, string>(nameof(NewLine),
textbox => textbox.NewLine, (textbox, newline) => textbox.NewLine = newline);
public static readonly StyledProperty<string> NewLineProperty =
AvaloniaProperty.Register<TextBox, string>(nameof(NewLine), Environment.NewLine);
/// <summary>
/// Defines the <see cref="InnerLeftContent"/> property
@ -242,12 +234,8 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="UndoLimit"/> property
/// </summary>
public static readonly DirectProperty<TextBox, int> UndoLimitProperty =
AvaloniaProperty.RegisterDirect<TextBox, int>(
nameof(UndoLimit),
o => o.UndoLimit,
(o, v) => o.UndoLimit = v,
unsetValue: -1);
public static readonly StyledProperty<int> UndoLimitProperty =
AvaloniaProperty.Register<TextBox, int>(nameof(UndoLimit), UndoRedoHelper<UndoRedoState>.DefaultUndoLimit);
/// <summary>
/// Defines the <see cref="CanUndo"/> 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<UndoRedoState> _undoRedoHelper;
private readonly TextBoxTextInputMethodClient _imClient = new();
private readonly UndoRedoHelper<UndoRedoState> _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
/// </summary>
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<int>();
SetCurrentValue(SelectionStartProperty, newValue);
SetCurrentValue(SelectionEndProperty, newValue);
}
/// <summary>
@ -463,21 +447,18 @@ namespace Avalonia.Controls
/// </summary>
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<int>();
if (SelectionEnd == value && CaretIndex != value)
{
SetCurrentValue(CaretIndexProperty, value);
}
}
@ -490,21 +471,18 @@ namespace Avalonia.Controls
/// </remarks>
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<int>();
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;
}
/// <summary>
@ -691,8 +660,8 @@ namespace Avalonia.Controls
/// </summary>
public string NewLine
{
get => _newLine;
set => SetAndRaise(NewLineProperty, ref _newLine, value);
get => GetValue(NewLineProperty);
set => SetValue(NewLineProperty, value);
}
/// <summary>
@ -700,7 +669,8 @@ namespace Avalonia.Controls
/// </summary>
public void ClearSelection()
{
CaretIndex = SelectionStart;
SetCurrentValue(CaretIndexProperty, SelectionStart);
SetCurrentValue(SelectionEndProperty, SelectionStart);
}
/// <summary>
@ -744,25 +714,20 @@ namespace Avalonia.Controls
/// </summary>
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;
}
/// <summary>
@ -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<int>());
}
else if (change.Property == IsUndoEnabledProperty && change.GetNewValue<bool>() == 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
/// <summary>
/// Clears the text in the TextBox
/// </summary>
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
/// </summary>
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();
}
}

2
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;

4
src/Avalonia.Controls/Utils/UndoRedoHelper.cs

@ -4,6 +4,8 @@ namespace Avalonia.Controls.Utils
{
class UndoRedoHelper<TState>
{
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.
/// </summary>
public int Limit { get; set; } = 10;
public int Limit { get; set; } = DefaultUndoLimit;
public bool CanUndo => _currentNode?.Previous != null;

29
src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.cs

@ -7,23 +7,18 @@ namespace Avalonia.Diagnostics.Controls
{
internal class FilterTextBox : TextBox, IStyleable
{
public static readonly DirectProperty<FilterTextBox, bool> UseRegexFilterProperty =
AvaloniaProperty.RegisterDirect<FilterTextBox, bool>(nameof(UseRegexFilter),
o => o.UseRegexFilter, (o, v) => o.UseRegexFilter = v,
public static readonly StyledProperty<bool> UseRegexFilterProperty =
AvaloniaProperty.Register<FilterTextBox, bool>(nameof(UseRegexFilter),
defaultBindingMode: BindingMode.TwoWay);
public static readonly DirectProperty<FilterTextBox, bool> UseCaseSensitiveFilterProperty =
AvaloniaProperty.RegisterDirect<FilterTextBox, bool>(nameof(UseCaseSensitiveFilter),
o => o.UseCaseSensitiveFilter, (o, v) => o.UseCaseSensitiveFilter = v,
public static readonly StyledProperty<bool> UseCaseSensitiveFilterProperty =
AvaloniaProperty.Register<FilterTextBox, bool>(nameof(UseCaseSensitiveFilter),
defaultBindingMode: BindingMode.TwoWay);
public static readonly DirectProperty<FilterTextBox, bool> UseWholeWordFilterProperty =
AvaloniaProperty.RegisterDirect<FilterTextBox, bool>(nameof(UseWholeWordFilter),
o => o.UseWholeWordFilter, (o, v) => o.UseWholeWordFilter = v,
public static readonly StyledProperty<bool> UseWholeWordFilterProperty =
AvaloniaProperty.Register<FilterTextBox, bool>(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);

55
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<TextBlock>())
var style = new Style(x => x.Is<DirectPropertyClass>())
{
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<object?>("foo");
var style = new Style(x => x.Is<TextBlock>())
var style = new Style(x => x.Is<DirectPropertyClass>())
{
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<object?>("foo");
var style = new Style(x => x.Is<TextBlock>().Class("foo"))
var style = new Style(x => x.Is<DirectPropertyClass>().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<TextBlock>().Class("foo"))
var style = new Style(x => x.Is<DirectPropertyClass>().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<TextBox>())
var style = new Style(x => x.OfType<DirectPropertyClass>())
{
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<DirectPropertyClass, string?> FooProperty = AvaloniaProperty.RegisterDirect<DirectPropertyClass, string?>(nameof(Foo),
x => x.Foo, (x, v) => x.Foo = v);
private string? _foo;
public string? Foo
{
get => _foo;
set => SetAndRaise(FooProperty, ref _foo, value);
}
}
}
}

2
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);

Loading…
Cancel
Save