Browse Source

Convert Text properties to StyledProperty

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

3
src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs

@ -198,7 +198,6 @@ namespace Avalonia.Controls
private bool _isDropDownOpen; private bool _isDropDownOpen;
private bool _isFocused = false; private bool _isFocused = false;
private string? _text = string.Empty;
private string? _searchText = string.Empty; private string? _searchText = string.Empty;
private AutoCompleteFilterPredicate<object?>? _itemFilter; private AutoCompleteFilterPredicate<object?>? _itemFilter;
@ -1275,7 +1274,7 @@ namespace Avalonia.Controls
if ((userInitiated ?? true) && Text != value) if ((userInitiated ?? true) && Text != value)
{ {
_ignoreTextPropertyChange++; _ignoreTextPropertyChange++;
Text = value; SetCurrentValue(TextProperty, value);
callTextChanged = true; callTextChanged = true;
} }

8
src/Avalonia.Controls/Documents/InlineCollection.cs

@ -91,11 +91,11 @@ namespace Avalonia.Controls.Documents
public override void Add(Inline inline) 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); base.Add(inline);
@ -113,7 +113,7 @@ namespace Avalonia.Controls.Documents
{ {
if (InlineHost is TextBlock textBlock && !textBlock.HasComplexContent) if (InlineHost is TextBlock textBlock && !textBlock.HasComplexContent)
{ {
textBlock._text += text; textBlock.Text += text;
} }
else else
{ {

135
src/Avalonia.Controls/MaskedTextBox.cs

@ -15,9 +15,8 @@ namespace Avalonia.Controls
public static readonly StyledProperty<bool> AsciiOnlyProperty = public static readonly StyledProperty<bool> AsciiOnlyProperty =
AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(AsciiOnly)); AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(AsciiOnly));
public static readonly DirectProperty<MaskedTextBox, CultureInfo?> CultureProperty = public static readonly StyledProperty<CultureInfo?> CultureProperty =
AvaloniaProperty.RegisterDirect<MaskedTextBox, CultureInfo?>(nameof(Culture), o => o.Culture, AvaloniaProperty.Register<MaskedTextBox, CultureInfo?>(nameof(Culture), CultureInfo.CurrentCulture);
(o, v) => o.Culture = v, CultureInfo.CurrentCulture);
public static readonly StyledProperty<bool> HidePromptOnLeaveProperty = public static readonly StyledProperty<bool> HidePromptOnLeaveProperty =
AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(HidePromptOnLeave)); AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(HidePromptOnLeave));
@ -32,26 +31,49 @@ namespace Avalonia.Controls
AvaloniaProperty.Register<MaskedTextBox, string?>(nameof(Mask), string.Empty); AvaloniaProperty.Register<MaskedTextBox, string?>(nameof(Mask), string.Empty);
public static readonly StyledProperty<char> PromptCharProperty = 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 = public static readonly StyledProperty<bool> ResetOnPromptProperty =
AvaloniaProperty.RegisterDirect<MaskedTextBox, bool>(nameof(ResetOnPrompt), o => o.ResetOnPrompt, (o, v) => o.ResetOnPrompt = v); AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(ResetOnPrompt), true);
public static readonly DirectProperty<MaskedTextBox, bool> ResetOnSpaceProperty = public static readonly StyledProperty<bool> ResetOnSpaceProperty =
AvaloniaProperty.RegisterDirect<MaskedTextBox, bool>(nameof(ResetOnSpace), o => o.ResetOnSpace, (o, v) => o.ResetOnSpace = v); 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 if (!MaskedTextProvider.IsValidInputChar(baseValue))
.OverrideDefaultValue<MaskedTextBox>('\0'); {
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() { } public MaskedTextBox() { }
@ -59,6 +81,9 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Constructs the MaskedTextBox with the specified MaskedTextProvider object. /// Constructs the MaskedTextBox with the specified MaskedTextProvider object.
/// </summary> /// </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) public MaskedTextBox(MaskedTextProvider maskedTextProvider)
{ {
if (maskedTextProvider == null) if (maskedTextProvider == null)
@ -87,8 +112,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public CultureInfo? Culture public CultureInfo? Culture
{ {
get => _culture; get => GetValue(CultureProperty);
set => SetAndRaise(CultureProperty, ref _culture, value); set => SetValue(CultureProperty, value);
} }
/// <summary> /// <summary>
@ -131,15 +156,6 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public MaskedTextProvider? MaskProvider { get; private set; } 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> /// <summary>
/// Gets or sets the character used to represent the absence of user input in MaskedTextBox. /// Gets or sets the character used to represent the absence of user input in MaskedTextBox.
/// </summary> /// </summary>
@ -154,16 +170,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public bool ResetOnPrompt public bool ResetOnPrompt
{ {
get => _resetOnPrompt; get => GetValue(ResetOnPromptProperty);
set set => SetValue(ResetOnPromptProperty, value);
{
SetAndRaise(ResetOnPromptProperty, ref _resetOnPrompt, value);
if (MaskProvider != null)
{
MaskProvider.ResetOnPrompt = value;
}
}
} }
/// <summary> /// <summary>
@ -171,16 +179,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public bool ResetOnSpace public bool ResetOnSpace
{ {
get => _resetOnSpace; get => GetValue(ResetOnSpaceProperty);
set set => SetValue(ResetOnSpaceProperty, value);
{
SetAndRaise(ResetOnSpaceProperty, ref _resetOnSpace, value);
if (MaskProvider != null)
{
MaskProvider.ResetOnSpace = value;
}
}
} }
Type IStyleable.StyleKey => typeof(TextBox); Type IStyleable.StyleKey => typeof(TextBox);
@ -190,7 +190,7 @@ namespace Avalonia.Controls
{ {
if (HidePromptOnLeave == true && MaskProvider != null) if (HidePromptOnLeave == true && MaskProvider != null)
{ {
Text = MaskProvider.ToDisplayString(); SetCurrentValue(TextProperty, MaskProvider.ToDisplayString());
} }
base.OnGotFocus(e); base.OnGotFocus(e);
} }
@ -225,11 +225,11 @@ namespace Avalonia.Controls
var index = GetNextCharacterPosition(CaretIndex); var index = GetNextCharacterPosition(CaretIndex);
if (MaskProvider.InsertAt(item, index)) if (MaskProvider.InsertAt(item, index))
{ {
CaretIndex = ++index; SetCurrentValue(CaretIndexProperty, ++index);
} }
} }
Text = MaskProvider.ToDisplayString(); SetCurrentValue(TextProperty, MaskProvider.ToDisplayString());
e.Handled = true; e.Handled = true;
return; return;
} }
@ -279,7 +279,7 @@ namespace Avalonia.Controls
{ {
if (HidePromptOnLeave && MaskProvider != null) if (HidePromptOnLeave && MaskProvider != null)
{ {
Text = MaskProvider.ToString(!HidePromptOnLeave, true); SetCurrentValue(TextProperty, MaskProvider.ToString(!HidePromptOnLeave, true));
} }
base.OnLostFocus(e); base.OnLostFocus(e);
} }
@ -326,15 +326,6 @@ namespace Avalonia.Controls
} }
else if (change.Property == PasswordCharProperty) 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) if (MaskProvider != null && MaskProvider.PasswordChar != PasswordChar)
{ {
UpdateMaskProvider(); UpdateMaskProvider();
@ -342,17 +333,23 @@ namespace Avalonia.Controls
} }
else if (change.Property == PromptCharProperty) 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 else if (change.Property == AsciiOnlyProperty && MaskProvider != null && MaskProvider.AsciiOnly != AsciiOnly
@ -390,7 +387,7 @@ namespace Avalonia.Controls
if (CaretIndex < Text?.Length) if (CaretIndex < Text?.Length)
{ {
CaretIndex = GetNextCharacterPosition(CaretIndex); SetCurrentValue(CaretIndexProperty, GetNextCharacterPosition(CaretIndex));
if (MaskProvider.InsertAt(e.Text!, CaretIndex)) if (MaskProvider.InsertAt(e.Text!, CaretIndex))
{ {
@ -399,7 +396,7 @@ namespace Avalonia.Controls
var nextPos = GetNextCharacterPosition(CaretIndex); var nextPos = GetNextCharacterPosition(CaretIndex);
if (nextPos != 0 && CaretIndex != Text.Length) if (nextPos != 0 && CaretIndex != Text.Length)
{ {
CaretIndex = nextPos; SetCurrentValue(CaretIndexProperty, nextPos);
} }
} }
@ -434,8 +431,8 @@ namespace Avalonia.Controls
{ {
if (provider != null) if (provider != null)
{ {
Text = provider.ToDisplayString(); SetCurrentValue(TextProperty, provider.ToDisplayString());
CaretIndex = position; SetCurrentValue(CaretIndexProperty, position);
} }
} }

109
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -9,17 +9,13 @@ using Avalonia.VisualTree;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
using Avalonia.Controls.Documents; using Avalonia.Controls.Documents;
using Avalonia.Input.TextInput;
using Avalonia.Data;
namespace Avalonia.Controls.Presenters namespace Avalonia.Controls.Presenters
{ {
public class TextPresenter : Control public class TextPresenter : Control
{ {
public static readonly DirectProperty<TextPresenter, int> CaretIndexProperty = public static readonly StyledProperty<int> CaretIndexProperty =
TextBox.CaretIndexProperty.AddOwner<TextPresenter>( TextBox.CaretIndexProperty.AddOwner<TextPresenter>(new(coerce: TextBox.CoerceCaretIndex));
o => o.CaretIndex,
(o, v) => o.CaretIndex = v);
public static readonly StyledProperty<bool> RevealPasswordProperty = public static readonly StyledProperty<bool> RevealPasswordProperty =
AvaloniaProperty.Register<TextPresenter, bool>(nameof(RevealPassword)); AvaloniaProperty.Register<TextPresenter, bool>(nameof(RevealPassword));
@ -36,33 +32,23 @@ namespace Avalonia.Controls.Presenters
public static readonly StyledProperty<IBrush?> CaretBrushProperty = public static readonly StyledProperty<IBrush?> CaretBrushProperty =
AvaloniaProperty.Register<TextPresenter, IBrush?>(nameof(CaretBrush)); AvaloniaProperty.Register<TextPresenter, IBrush?>(nameof(CaretBrush));
public static readonly DirectProperty<TextPresenter, int> SelectionStartProperty = public static readonly StyledProperty<int> SelectionStartProperty =
TextBox.SelectionStartProperty.AddOwner<TextPresenter>( TextBox.SelectionStartProperty.AddOwner<TextPresenter>(new(coerce: TextBox.CoerceCaretIndex));
o => o.SelectionStart,
(o, v) => o.SelectionStart = v);
public static readonly DirectProperty<TextPresenter, int> SelectionEndProperty = public static readonly StyledProperty<int> SelectionEndProperty =
TextBox.SelectionEndProperty.AddOwner<TextPresenter>( TextBox.SelectionEndProperty.AddOwner<TextPresenter>(new(coerce: TextBox.CoerceCaretIndex));
o => o.SelectionEnd,
(o, v) => o.SelectionEnd = v);
/// <summary> /// <summary>
/// Defines the <see cref="Text"/> property. /// Defines the <see cref="Text"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<TextPresenter, string?> TextProperty = public static readonly StyledProperty<string?> TextProperty =
AvaloniaProperty.RegisterDirect<TextPresenter, string?>( TextBlock.TextProperty.AddOwner<TextPresenter>(new(string.Empty));
nameof(Text),
o => o.Text,
(o, v) => o.Text = v, defaultBindingMode: BindingMode.OneWay);
/// <summary> /// <summary>
/// Defines the <see cref="PreeditText"/> property. /// Defines the <see cref="PreeditText"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<TextPresenter, string?> PreeditTextProperty = public static readonly StyledProperty<string?> PreeditTextProperty =
AvaloniaProperty.RegisterDirect<TextPresenter, string?>( AvaloniaProperty.Register<TextPresenter, string?>(nameof(PreeditText));
nameof(PreeditText),
o => o.PreeditText,
(o, v) => o.PreeditText = v);
/// <summary> /// <summary>
/// Defines the <see cref="CompositionRegion"/> property. /// Defines the <see cref="CompositionRegion"/> property.
@ -104,18 +90,13 @@ namespace Avalonia.Controls.Presenters
Border.BackgroundProperty.AddOwner<TextPresenter>(); Border.BackgroundProperty.AddOwner<TextPresenter>();
private readonly DispatcherTimer _caretTimer; private readonly DispatcherTimer _caretTimer;
private int _caretIndex;
private int _selectionStart;
private int _selectionEnd;
private bool _caretBlink; private bool _caretBlink;
internal string? _text;
private TextLayout? _textLayout; private TextLayout? _textLayout;
private Size _constraint; private Size _constraint;
private CharacterHit _lastCharacterHit; private CharacterHit _lastCharacterHit;
private Rect _caretBounds; private Rect _caretBounds;
private Point _navigationPosition; private Point _navigationPosition;
private string? _preeditText;
private TextRange? _compositionRegion; private TextRange? _compositionRegion;
static TextPresenter() static TextPresenter()
@ -125,7 +106,6 @@ namespace Avalonia.Controls.Presenters
public TextPresenter() public TextPresenter()
{ {
_text = string.Empty;
_caretTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) }; _caretTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) };
_caretTimer.Tick += CaretTimerTick; _caretTimer.Tick += CaretTimerTick;
} }
@ -147,14 +127,14 @@ namespace Avalonia.Controls.Presenters
[Content] [Content]
public string? Text public string? Text
{ {
get => _text; get => GetValue(TextProperty);
set => SetAndRaise(TextProperty, ref _text, value); set => SetValue(TextProperty, value);
} }
public string? PreeditText public string? PreeditText
{ {
get => _preeditText; get => GetValue(PreeditTextProperty);
set => SetAndRaise(PreeditTextProperty, ref _preeditText, value); set => SetValue(PreeditTextProperty, value);
} }
public TextRange? CompositionRegion public TextRange? CompositionRegion
@ -275,17 +255,8 @@ namespace Avalonia.Controls.Presenters
public int CaretIndex public int CaretIndex
{ {
get get => GetValue(CaretIndexProperty);
{ set => SetValue(CaretIndexProperty, value);
return _caretIndex;
}
set
{
if (value != _caretIndex)
{
MoveCaretToTextPosition(value);
}
}
} }
public char PasswordChar public char PasswordChar
@ -320,30 +291,14 @@ namespace Avalonia.Controls.Presenters
public int SelectionStart public int SelectionStart
{ {
get get => GetValue(SelectionStartProperty);
{ set => SetValue(SelectionStartProperty, value);
return _selectionStart;
}
set
{
value = CoerceCaretIndex(value);
SetAndRaise(SelectionStartProperty, ref _selectionStart, value);
}
} }
public int SelectionEnd public int SelectionEnd
{ {
get get => GetValue(SelectionEndProperty);
{ set => SetValue(SelectionEndProperty, value);
return _selectionEnd;
}
set
{
value = CoerceCaretIndex(value);
SetAndRaise(SelectionEndProperty, ref _selectionEnd, value);
}
} }
protected override bool BypassFlowDirectionPolicies => true; protected override bool BypassFlowDirectionPolicies => true;
@ -535,12 +490,12 @@ namespace Avalonia.Controls.Presenters
{ {
TextLayout result; TextLayout result;
var text = _text; var text = Text;
var typeface = new Typeface(FontFamily, FontStyle, FontWeight); var typeface = new Typeface(FontFamily, FontStyle, FontWeight);
var selectionStart = CoerceCaretIndex(SelectionStart); var selectionStart = SelectionStart;
var selectionEnd = CoerceCaretIndex(SelectionEnd); var selectionEnd = SelectionEnd;
var start = Math.Min(selectionStart, selectionEnd); var start = Math.Min(selectionStart, selectionEnd);
var length = Math.Max(selectionStart, selectionEnd) - start; 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, new GenericTextRunProperties(typeface, FontSize,
foregroundBrush: foreground, foregroundBrush: foreground,
textDecorations: TextDecorations.Underline)); textDecorations: TextDecorations.Underline));
@ -643,13 +598,6 @@ namespace Avalonia.Controls.Presenters
return finalSize; 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) private void CaretTimerTick(object? sender, EventArgs e)
{ {
_caretBlink = !_caretBlink; _caretBlink = !_caretBlink;
@ -865,7 +813,7 @@ namespace Avalonia.Controls.Presenters
if (notify) if (notify)
{ {
SetAndRaise(CaretIndexProperty, ref _caretIndex, caretIndex); SetCurrentValue(CaretIndexProperty, caretIndex);
} }
} }
@ -887,6 +835,11 @@ namespace Avalonia.Controls.Presenters
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
if (change.Property == CaretIndexProperty)
{
MoveCaretToTextPosition(change.GetNewValue<int>());
}
switch (change.Property.Name) switch (change.Property.Name)
{ {
case nameof(PreeditText): case nameof(PreeditText):

105
src/Avalonia.Controls/SelectableTextBlock.cs

@ -17,17 +17,11 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public class SelectableTextBlock : TextBlock, IInlineHost public class SelectableTextBlock : TextBlock, IInlineHost
{ {
public static readonly DirectProperty<SelectableTextBlock, int> SelectionStartProperty = public static readonly StyledProperty<int> SelectionStartProperty =
AvaloniaProperty.RegisterDirect<SelectableTextBlock, int>( TextBox.SelectionStartProperty.AddOwner<SelectableTextBlock>(new(coerce: TextBox.CoerceCaretIndex));
nameof(SelectionStart),
o => o.SelectionStart, public static readonly StyledProperty<int> SelectionEndProperty =
(o, v) => o.SelectionStart = v); TextBox.SelectionEndProperty.AddOwner<SelectableTextBlock>(new(coerce: TextBox.CoerceCaretIndex));
public static readonly DirectProperty<SelectableTextBlock, int> SelectionEndProperty =
AvaloniaProperty.RegisterDirect<SelectableTextBlock, int>(
nameof(SelectionEnd),
o => o.SelectionEnd,
(o, v) => o.SelectionEnd = v);
public static readonly DirectProperty<SelectableTextBlock, string> SelectedTextProperty = public static readonly DirectProperty<SelectableTextBlock, string> SelectedTextProperty =
AvaloniaProperty.RegisterDirect<SelectableTextBlock, string>( AvaloniaProperty.RegisterDirect<SelectableTextBlock, string>(
@ -35,21 +29,16 @@ namespace Avalonia.Controls
o => o.SelectedText); o => o.SelectedText);
public static readonly StyledProperty<IBrush?> SelectionBrushProperty = 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 = public static readonly DirectProperty<SelectableTextBlock, bool> CanCopyProperty =
AvaloniaProperty.RegisterDirect<SelectableTextBlock, bool>( TextBox.CanCopyProperty.AddOwner<SelectableTextBlock>(o => o.CanCopy);
nameof(CanCopy),
o => o.CanCopy);
public static readonly RoutedEvent<RoutedEventArgs> CopyingToClipboardEvent = public static readonly RoutedEvent<RoutedEventArgs> CopyingToClipboardEvent =
RoutedEvent.Register<SelectableTextBlock, RoutedEventArgs>( RoutedEvent.Register<SelectableTextBlock, RoutedEventArgs>(
nameof(CopyingToClipboard), RoutingStrategies.Bubble); nameof(CopyingToClipboard), RoutingStrategies.Bubble);
private bool _canCopy; private bool _canCopy;
private int _selectionStart;
private int _selectionEnd;
private int _wordSelectionStart = -1; private int _wordSelectionStart = -1;
static SelectableTextBlock() static SelectableTextBlock()
@ -78,16 +67,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public int SelectionStart public int SelectionStart
{ {
get => _selectionStart; get => GetValue(SelectionStartProperty);
set set => SetValue(SelectionStartProperty, value);
{
if (SetAndRaise(SelectionStartProperty, ref _selectionStart, value))
{
RaisePropertyChanged(SelectedTextProperty, "", "");
UpdateCommandStates();
}
}
} }
/// <summary> /// <summary>
@ -95,16 +76,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public int SelectionEnd public int SelectionEnd
{ {
get => _selectionEnd; get => GetValue(SelectionEndProperty);
set set => SetValue(SelectionEndProperty, value);
{
if (SetAndRaise(SelectionEndProperty, ref _selectionEnd, value))
{
RaisePropertyChanged(SelectedTextProperty, "", "");
UpdateCommandStates();
}
}
} }
/// <summary> /// <summary>
@ -150,7 +123,7 @@ namespace Avalonia.Controls
await ((IClipboard)AvaloniaLocator.Current.GetRequiredService(typeof(IClipboard))) await ((IClipboard)AvaloniaLocator.Current.GetRequiredService(typeof(IClipboard)))
.SetTextAsync(text); .SetTextAsync(text);
} }
} }
/// <summary> /// <summary>
/// Select all text in the TextBox /// Select all text in the TextBox
@ -159,8 +132,8 @@ namespace Avalonia.Controls
{ {
var text = Text; var text = Text;
SelectionStart = 0; SetCurrentValue(SelectionStartProperty, 0);
SelectionEnd = text?.Length ?? 0; SetCurrentValue(SelectionEndProperty, text?.Length ?? 0);
} }
/// <summary> /// <summary>
@ -168,7 +141,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public void ClearSelection() public void ClearSelection()
{ {
SelectionEnd = SelectionStart; SetCurrentValue(SelectionEndProperty, SelectionStart);
} }
protected override void OnGotFocus(GotFocusEventArgs e) protected override void OnGotFocus(GotFocusEventArgs e)
@ -240,6 +213,17 @@ namespace Avalonia.Controls
e.Handled = handled; 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) protected override void OnPointerPressed(PointerPressedEventArgs e)
{ {
base.OnPointerPressed(e); base.OnPointerPressed(e);
@ -271,25 +255,26 @@ namespace Avalonia.Controls
if (index > _wordSelectionStart) if (index > _wordSelectionStart)
{ {
SelectionEnd = StringUtils.NextWord(text, index); SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, index));
} }
if (index < _wordSelectionStart || previousWord == _wordSelectionStart) if (index < _wordSelectionStart || previousWord == _wordSelectionStart)
{ {
SelectionStart = previousWord; SetCurrentValue(SelectionStartProperty, previousWord);
} }
} }
else else
{ {
SelectionStart = Math.Min(oldIndex, index); SetCurrentValue(SelectionStartProperty, Math.Min(oldIndex, index));
SelectionEnd = Math.Max(oldIndex, index); SetCurrentValue(SelectionEndProperty, Math.Max(oldIndex, index));
} }
} }
else else
{ {
if (_wordSelectionStart == -1 || index < SelectionStart || index > SelectionEnd) if (_wordSelectionStart == -1 || index < SelectionStart || index > SelectionEnd)
{ {
SelectionStart = SelectionEnd = index; SetCurrentValue(SelectionStartProperty, index);
SetCurrentValue(SelectionEndProperty, index);
_wordSelectionStart = -1; _wordSelectionStart = -1;
} }
@ -299,16 +284,16 @@ namespace Avalonia.Controls
case 2: case 2:
if (!StringUtils.IsStartOfWord(text, index)) if (!StringUtils.IsStartOfWord(text, index))
{ {
SelectionStart = StringUtils.PreviousWord(text, index); SetCurrentValue(SelectionStartProperty, StringUtils.PreviousWord(text, index));
} }
_wordSelectionStart = SelectionStart; _wordSelectionStart = SelectionStart;
if (!StringUtils.IsEndOfWord(text, index)) if (!StringUtils.IsEndOfWord(text, index))
{ {
SelectionEnd = StringUtils.NextWord(text, index); SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, index));
} }
break; break;
case 3: case 3:
_wordSelectionStart = -1; _wordSelectionStart = -1;
@ -347,22 +332,22 @@ namespace Avalonia.Controls
if (distance <= 0) if (distance <= 0)
{ {
SelectionStart = StringUtils.PreviousWord(text, textPosition); SetCurrentValue(SelectionStartProperty, StringUtils.PreviousWord(text, textPosition));
} }
if (distance >= 0) if (distance >= 0)
{ {
if (SelectionStart != _wordSelectionStart) if (SelectionStart != _wordSelectionStart)
{ {
SelectionStart = _wordSelectionStart; SetCurrentValue(SelectionStartProperty, _wordSelectionStart);
} }
SelectionEnd = StringUtils.NextWord(text, textPosition); SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, textPosition));
} }
} }
else else
{ {
SelectionEnd = textPosition; SetCurrentValue(SelectionEndProperty, textPosition);
} }
} }
@ -395,7 +380,8 @@ namespace Avalonia.Controls
caretIndex >= firstSelection && caretIndex <= lastSelection; caretIndex >= firstSelection && caretIndex <= lastSelection;
if (!didClickInSelection) if (!didClickInSelection)
{ {
SelectionStart = SelectionEnd = caretIndex; SetCurrentValue(SelectionStartProperty, caretIndex);
SetCurrentValue(SelectionEndProperty, caretIndex);
} }
} }
@ -411,9 +397,8 @@ namespace Avalonia.Controls
private string GetSelection() private string GetSelection()
{ {
var text = GetText(); var textLength = Text?.Length ?? 0;
if (textLength == 0)
if (string.IsNullOrEmpty(text))
{ {
return ""; return "";
} }
@ -423,14 +408,14 @@ namespace Avalonia.Controls
var start = Math.Min(selectionStart, selectionEnd); var start = Math.Min(selectionStart, selectionEnd);
var end = Math.Max(selectionStart, selectionEnd); var end = Math.Max(selectionStart, selectionEnd);
if (start == end || text.Length < end) if (start == end || textLength < end)
{ {
return ""; return "";
} }
var length = Math.Max(0, end - start); var length = Math.Max(0, end - start);
var selectedText = text.Substring(start, length); var selectedText = Text!.Substring(start, length);
return selectedText; return selectedText;
} }

66
src/Avalonia.Controls/TextBlock.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using Avalonia.Automation.Peers; using Avalonia.Automation.Peers;
using Avalonia.Controls.Documents; using Avalonia.Controls.Documents;
using Avalonia.Layout; using Avalonia.Layout;
@ -13,6 +14,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// A control that displays a block of text. /// A control that displays a block of text.
/// </summary> /// </summary>
[DebuggerDisplay("{DebugText}")]
public class TextBlock : Control, IInlineHost public class TextBlock : Control, IInlineHost
{ {
/// <summary> /// <summary>
@ -103,11 +105,8 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Defines the <see cref="Text"/> property. /// Defines the <see cref="Text"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<TextBlock, string?> TextProperty = public static readonly StyledProperty<string?> TextProperty =
AvaloniaProperty.RegisterDirect<TextBlock, string?>( AvaloniaProperty.Register<TextBlock, string?>(nameof(Text));
nameof(Text),
o => o.GetText(),
(o, v) => o.SetText(v));
/// <summary> /// <summary>
/// Defines the <see cref="TextAlignment"/> property. /// Defines the <see cref="TextAlignment"/> property.
@ -142,14 +141,14 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Defines the <see cref="Inlines"/> property. /// Defines the <see cref="Inlines"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<InlineCollection?> InlinesProperty = public static readonly DirectProperty<TextBlock, InlineCollection?> InlinesProperty =
AvaloniaProperty.Register<TextBlock, InlineCollection?>( AvaloniaProperty.RegisterDirect<TextBlock, InlineCollection?>(
nameof(Inlines)); nameof(Inlines), t => t.Inlines, (t, v) => t.Inlines = v);
internal string? _text;
protected TextLayout? _textLayout; protected TextLayout? _textLayout;
protected Size _constraint; protected Size _constraint;
private IReadOnlyList<TextRun>? _textRuns; private IReadOnlyList<TextRun>? _textRuns;
private InlineCollection? _inlines;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="TextBlock"/> class. /// Initializes static members of the <see cref="TextBlock"/> class.
@ -173,7 +172,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Gets the <see cref="TextLayout"/> used to render the text. /// Gets the <see cref="TextLayout"/> used to render the text.
/// </summary> /// </summary>
public TextLayout TextLayout => _textLayout ??= CreateTextLayout(_text); public TextLayout TextLayout => _textLayout ??= CreateTextLayout(Text);
/// <summary> /// <summary>
/// Gets or sets the padding to place around the <see cref="Text"/>. /// Gets or sets the padding to place around the <see cref="Text"/>.
@ -198,10 +197,12 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public string? Text public string? Text
{ {
get => GetText(); get => GetValue(TextProperty);
set => SetText(value); set => SetValue(TextProperty, value);
} }
private string? DebugText => Text ?? Inlines?.Text;
/// <summary> /// <summary>
/// Gets or sets the font family used to draw the control's text. /// Gets or sets the font family used to draw the control's text.
/// </summary> /// </summary>
@ -325,8 +326,8 @@ namespace Avalonia.Controls
[Content] [Content]
public InlineCollection? Inlines public InlineCollection? Inlines
{ {
get => GetValue(InlinesProperty); get => _inlines;
set => SetValue(InlinesProperty, value); set => SetAndRaise(InlinesProperty, ref _inlines, value);
} }
protected override bool BypassFlowDirectionPolicies => true; protected override bool BypassFlowDirectionPolicies => true;
@ -590,19 +591,18 @@ namespace Avalonia.Controls
TextLayout.Draw(context, origin); TextLayout.Draw(context, origin);
} }
protected virtual string? GetText() private bool _clearTextInternal;
{ internal void ClearTextInternal()
return _text ?? Inlines?.Text;
}
protected virtual void SetText(string? text)
{ {
if (HasComplexContent) _clearTextInternal = true;
try
{
SetCurrentValue(TextProperty, null);
}
finally
{ {
Inlines?.Clear(); _clearTextInternal = false;
} }
SetAndRaise(TextProperty, ref _text, text);
} }
/// <summary> /// <summary>
@ -780,6 +780,14 @@ namespace Avalonia.Controls
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
if (change.Property == TextProperty)
{
if (HasComplexContent && !_clearTextInternal)
{
Inlines?.Clear();
}
}
switch (change.Property.Name) switch (change.Property.Name)
{ {
case nameof(FontSize): case nameof(FontSize):
@ -794,10 +802,10 @@ namespace Avalonia.Controls
case nameof(FlowDirection): case nameof(FlowDirection):
case nameof (Padding): case nameof(Padding):
case nameof (LineHeight): case nameof(LineHeight):
case nameof (LetterSpacing): case nameof(LetterSpacing):
case nameof (MaxLines): case nameof(MaxLines):
case nameof(Text): case nameof(Text):
case nameof(TextDecorations): case nameof(TextDecorations):
@ -899,7 +907,7 @@ namespace Avalonia.Controls
continue; continue;
} }
if (textRun is TextCharacters) if (textRun is TextCharacters)
{ {
var skip = Math.Max(0, textSourceIndex - currentPosition); var skip = Math.Max(0, textSourceIndex - currentPosition);

433
src/Avalonia.Controls/TextBox.cs

@ -61,11 +61,9 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Defines the <see cref="CaretIndex"/> property /// Defines the <see cref="CaretIndex"/> property
/// </summary> /// </summary>
public static readonly DirectProperty<TextBox, int> CaretIndexProperty = public static readonly StyledProperty<int> CaretIndexProperty =
AvaloniaProperty.RegisterDirect<TextBox, int>( AvaloniaProperty.Register<TextBox, int>(nameof(CaretIndex),
nameof(CaretIndex), coerce: CoerceCaretIndex);
o => o.CaretIndex,
(o, v) => o.CaretIndex = v);
/// <summary> /// <summary>
/// Defines the <see cref="IsReadOnly"/> property /// Defines the <see cref="IsReadOnly"/> property
@ -100,42 +98,37 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Defines the <see cref="SelectionStart"/> property /// Defines the <see cref="SelectionStart"/> property
/// </summary> /// </summary>
public static readonly DirectProperty<TextBox, int> SelectionStartProperty = public static readonly StyledProperty<int> SelectionStartProperty =
AvaloniaProperty.RegisterDirect<TextBox, int>( AvaloniaProperty.Register<TextBox, int>(nameof(SelectionStart),
nameof(SelectionStart), coerce: CoerceCaretIndex);
o => o.SelectionStart,
(o, v) => o.SelectionStart = v);
/// <summary> /// <summary>
/// Defines the <see cref="SelectionEnd"/> property /// Defines the <see cref="SelectionEnd"/> property
/// </summary> /// </summary>
public static readonly DirectProperty<TextBox, int> SelectionEndProperty = public static readonly StyledProperty<int> SelectionEndProperty =
AvaloniaProperty.RegisterDirect<TextBox, int>( AvaloniaProperty.Register<TextBox, int>(nameof(SelectionEnd),
nameof(SelectionEnd), coerce: CoerceCaretIndex);
o => o.SelectionEnd,
(o, v) => o.SelectionEnd = v);
/// <summary> /// <summary>
/// Defines the <see cref="MaxLength"/> property /// Defines the <see cref="MaxLength"/> property
/// </summary> /// </summary>
public static readonly StyledProperty<int> MaxLengthProperty = public static readonly StyledProperty<int> MaxLengthProperty =
AvaloniaProperty.Register<TextBox, int>(nameof(MaxLength), defaultValue: 0); AvaloniaProperty.Register<TextBox, int>(nameof(MaxLength));
/// <summary> /// <summary>
/// Defines the <see cref="MaxLines"/> property /// Defines the <see cref="MaxLines"/> property
/// </summary> /// </summary>
public static readonly StyledProperty<int> MaxLinesProperty = public static readonly StyledProperty<int> MaxLinesProperty =
AvaloniaProperty.Register<TextBox, int>(nameof(MaxLines), defaultValue: 0); AvaloniaProperty.Register<TextBox, int>(nameof(MaxLines));
/// <summary> /// <summary>
/// Defines the <see cref="Text"/> property /// Defines the <see cref="Text"/> property
/// </summary> /// </summary>
public static readonly DirectProperty<TextBox, string?> TextProperty = public static readonly StyledProperty<string?> TextProperty =
TextBlock.TextProperty.AddOwnerWithDataValidation<TextBox>( TextBlock.TextProperty.AddOwner<TextBox>(new(
o => o.Text, coerce: CoerceText,
(o, v) => o.Text = v,
defaultBindingMode: BindingMode.TwoWay, defaultBindingMode: BindingMode.TwoWay,
enableDataValidation: true); enableDataValidation: true));
/// <summary> /// <summary>
/// Defines the <see cref="TextAlignment"/> property /// Defines the <see cref="TextAlignment"/> property
@ -185,9 +178,8 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Defines the <see cref="NewLine"/> property /// Defines the <see cref="NewLine"/> property
/// </summary> /// </summary>
public static readonly DirectProperty<TextBox, string> NewLineProperty = public static readonly StyledProperty<string> NewLineProperty =
AvaloniaProperty.RegisterDirect<TextBox, string>(nameof(NewLine), AvaloniaProperty.Register<TextBox, string>(nameof(NewLine), Environment.NewLine);
textbox => textbox.NewLine, (textbox, newline) => textbox.NewLine = newline);
/// <summary> /// <summary>
/// Defines the <see cref="InnerLeftContent"/> property /// Defines the <see cref="InnerLeftContent"/> property
@ -242,12 +234,8 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Defines the <see cref="UndoLimit"/> property /// Defines the <see cref="UndoLimit"/> property
/// </summary> /// </summary>
public static readonly DirectProperty<TextBox, int> UndoLimitProperty = public static readonly StyledProperty<int> UndoLimitProperty =
AvaloniaProperty.RegisterDirect<TextBox, int>( AvaloniaProperty.Register<TextBox, int>(nameof(UndoLimit), UndoRedoHelper<UndoRedoState>.DefaultUndoLimit);
nameof(UndoLimit),
o => o.UndoLimit,
(o, v) => o.UndoLimit = v,
unsetValue: -1);
/// <summary> /// <summary>
/// Defines the <see cref="CanUndo"/> property /// Defines the <see cref="CanUndo"/> property
@ -318,18 +306,13 @@ namespace Avalonia.Controls
public override int GetHashCode() => Text?.GetHashCode() ?? 0; public override int GetHashCode() => Text?.GetHashCode() ?? 0;
} }
private string? _text;
private int _caretIndex;
private int _selectionStart;
private int _selectionEnd;
private TextPresenter? _presenter; private TextPresenter? _presenter;
private TextBoxTextInputMethodClient _imClient = new TextBoxTextInputMethodClient(); private readonly TextBoxTextInputMethodClient _imClient = new();
private UndoRedoHelper<UndoRedoState> _undoRedoHelper; private readonly UndoRedoHelper<UndoRedoState> _undoRedoHelper;
private bool _isUndoingRedoing; private bool _isUndoingRedoing;
private bool _canCut; private bool _canCut;
private bool _canCopy; private bool _canCopy;
private bool _canPaste; private bool _canPaste;
private string _newLine = Environment.NewLine;
private static readonly string[] invalidCharacters = new String[1] { "\u007f" }; private static readonly string[] invalidCharacters = new String[1] { "\u007f" };
private bool _canUndo; private bool _canUndo;
private bool _canRedo; private bool _canRedo;
@ -399,18 +382,19 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public int CaretIndex public int CaretIndex
{ {
get => _caretIndex; get => GetValue(CaretIndexProperty);
set set => SetValue(CaretIndexProperty, value);
{ }
value = CoerceCaretIndex(value);
SetAndRaise(CaretIndexProperty, ref _caretIndex, value);
UndoRedoState state; private void OnCaretIndexChanged(AvaloniaPropertyChangedEventArgs e)
if (IsUndoEnabled && _undoRedoHelper.TryGetLastState(out state) && state.Text == Text) {
_undoRedoHelper.UpdateLastState(); 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> /// <summary>
@ -463,21 +447,18 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public int SelectionStart public int SelectionStart
{ {
get => _selectionStart; get => GetValue(SelectionStartProperty);
set set => SetValue(SelectionStartProperty, value);
{ }
value = CoerceCaretIndex(value);
var changed = SetAndRaise(SelectionStartProperty, ref _selectionStart, value);
if (changed) private void OnSelectionStartChanged(AvaloniaPropertyChangedEventArgs e)
{ {
UpdateCommandStates(); UpdateCommandStates();
}
if (SelectionEnd == value && CaretIndex != value) var value = e.GetNewValue<int>();
{ if (SelectionEnd == value && CaretIndex != value)
CaretIndex = value; {
} SetCurrentValue(CaretIndexProperty, value);
} }
} }
@ -490,21 +471,18 @@ namespace Avalonia.Controls
/// </remarks> /// </remarks>
public int SelectionEnd public int SelectionEnd
{ {
get => _selectionEnd; get => GetValue(SelectionEndProperty);
set set => SetValue(SelectionEndProperty, value);
{ }
value = CoerceCaretIndex(value);
var changed = SetAndRaise(SelectionEndProperty, ref _selectionEnd, value); private void OnSelectionEndChanged(AvaloniaPropertyChangedEventArgs e)
{
if (changed) UpdateCommandStates();
{
UpdateCommandStates();
}
if (SelectionStart == value && CaretIndex != value) var value = e.GetNewValue<int>();
{ if (SelectionStart == value && CaretIndex != value)
CaretIndex = value; {
} SetCurrentValue(CaretIndexProperty, value);
} }
} }
@ -550,36 +528,27 @@ namespace Avalonia.Controls
[Content] [Content]
public string? Text public string? Text
{ {
get => _text; get => GetValue(TextProperty);
set set => SetValue(TextProperty, value);
{ }
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);
if (textChanged) private static string? CoerceText(AvaloniaObject sender, string? value)
{ {
RaiseTextChangeEvents(); 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> /// <summary>
@ -691,8 +660,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public string NewLine public string NewLine
{ {
get => _newLine; get => GetValue(NewLineProperty);
set => SetAndRaise(NewLineProperty, ref _newLine, value); set => SetValue(NewLineProperty, value);
} }
/// <summary> /// <summary>
@ -700,7 +669,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public void ClearSelection() public void ClearSelection()
{ {
CaretIndex = SelectionStart; SetCurrentValue(CaretIndexProperty, SelectionStart);
SetCurrentValue(SelectionEndProperty, SelectionStart);
} }
/// <summary> /// <summary>
@ -744,25 +714,20 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public int UndoLimit public int UndoLimit
{ {
get => _undoRedoHelper.Limit; get => GetValue(UndoLimitProperty);
set set => SetValue(UndoLimitProperty, value);
{ }
if (_undoRedoHelper.Limit != value)
{ private void OnUndoLimitChanged(int newValue)
// can't use SetAndRaise due to using _undoRedoHelper.Limit {
// (can't send a ref of a property to SetAndRaise), _undoRedoHelper.Limit = newValue;
// so use RaisePropertyChanged instead.
var oldValue = _undoRedoHelper.Limit; // from docs at
_undoRedoHelper.Limit = value; // https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled:
RaisePropertyChanged(UndoLimitProperty, oldValue, value); // "Setting UndoLimit clears the undo queue."
} _undoRedoHelper.Clear();
// from docs at _selectedTextChangesMadeSinceLastUndoSnapshot = 0;
// https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled: _hasDoneSnapshotOnce = false;
// "Setting UndoLimit clears the undo queue."
_undoRedoHelper.Clear();
_selectedTextChangesMadeSinceLastUndoSnapshot = 0;
_hasDoneSnapshotOnce = false;
}
} }
/// <summary> /// <summary>
@ -866,9 +831,31 @@ namespace Avalonia.Controls
if (change.Property == TextProperty) if (change.Property == TextProperty)
{ {
CoerceValue(CaretIndexProperty);
CoerceValue(SelectionStartProperty);
CoerceValue(SelectionEndProperty);
RaiseTextChangeEvents();
UpdatePseudoclasses(); UpdatePseudoclasses();
UpdateCommandStates(); 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) else if (change.Property == IsUndoEnabledProperty && change.GetNewValue<bool>() == false)
{ {
// from docs at // from docs at
@ -920,7 +907,7 @@ namespace Avalonia.Controls
(ContextMenu == null || !ContextMenu.IsOpen)) (ContextMenu == null || !ContextMenu.IsOpen))
{ {
ClearSelection(); ClearSelection();
RevealPassword = false; SetCurrentValue(RevealPasswordProperty, false);
} }
UpdateCommandStates(); UpdateCommandStates();
@ -986,35 +973,44 @@ namespace Avalonia.Controls
} }
} }
var text = Text ?? string.Empty; var currentText = Text ?? string.Empty;
var newLength = input.Length + text.Length - Math.Abs(SelectionStart - SelectionEnd); var selectionLength = Math.Abs(SelectionStart - SelectionEnd);
var newLength = input.Length + currentText.Length - selectionLength;
if (MaxLength > 0 && newLength > MaxLength) if (MaxLength > 0 && newLength > MaxLength)
{ {
input = input.Remove(Math.Max(0, input.Length - (newLength - MaxLength))); input = input.Remove(Math.Max(0, input.Length - (newLength - MaxLength)));
newLength = MaxLength;
} }
if (!string.IsNullOrEmpty(input)) if (!string.IsNullOrEmpty(input))
{ {
var oldText = _text; var textBuilder = StringBuilderCache.Acquire(Math.Max(currentText.Length, newLength));
textBuilder.Append(currentText);
DeleteSelection(false);
var caretIndex = CaretIndex; 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; movement = true;
selection = false; selection = false;
handled = true; handled = true;
CaretIndex = _presenter.CaretIndex; SetCurrentValue(CaretIndexProperty, _presenter.CaretIndex);
} }
else if (Match(keymap.MoveCursorToTheEndOfDocument)) else if (Match(keymap.MoveCursorToTheEndOfDocument))
{ {
@ -1176,7 +1172,7 @@ namespace Avalonia.Controls
movement = true; movement = true;
selection = false; selection = false;
handled = true; handled = true;
CaretIndex = _presenter.CaretIndex; SetCurrentValue(CaretIndexProperty, _presenter.CaretIndex);
} }
else if (Match(keymap.MoveCursorToTheStartOfLine)) else if (Match(keymap.MoveCursorToTheStartOfLine))
{ {
@ -1184,7 +1180,7 @@ namespace Avalonia.Controls
movement = true; movement = true;
selection = false; selection = false;
handled = true; handled = true;
CaretIndex = _presenter.CaretIndex; SetCurrentValue(CaretIndexProperty, _presenter.CaretIndex);
} }
else if (Match(keymap.MoveCursorToTheEndOfLine)) else if (Match(keymap.MoveCursorToTheEndOfLine))
{ {
@ -1192,31 +1188,31 @@ namespace Avalonia.Controls
movement = true; movement = true;
selection = false; selection = false;
handled = true; handled = true;
CaretIndex = _presenter.CaretIndex; SetCurrentValue(CaretIndexProperty, _presenter.CaretIndex);
} }
else if (Match(keymap.MoveCursorToTheStartOfDocumentWithSelection)) else if (Match(keymap.MoveCursorToTheStartOfDocumentWithSelection))
{ {
SelectionStart = caretIndex; SetCurrentValue(SelectionStartProperty, caretIndex);
MoveHome(true); MoveHome(true);
SelectionEnd = _presenter.CaretIndex; SetCurrentValue(SelectionEndProperty, _presenter.CaretIndex);
movement = true; movement = true;
selection = true; selection = true;
handled = true; handled = true;
} }
else if (Match(keymap.MoveCursorToTheEndOfDocumentWithSelection)) else if (Match(keymap.MoveCursorToTheEndOfDocumentWithSelection))
{ {
SelectionStart = caretIndex; SetCurrentValue(SelectionStartProperty, caretIndex);
MoveEnd(true); MoveEnd(true);
SelectionEnd = _presenter.CaretIndex; SetCurrentValue(SelectionEndProperty, _presenter.CaretIndex);
movement = true; movement = true;
selection = true; selection = true;
handled = true; handled = true;
} }
else if (Match(keymap.MoveCursorToTheStartOfLineWithSelection)) else if (Match(keymap.MoveCursorToTheStartOfLineWithSelection))
{ {
SelectionStart = caretIndex; SetCurrentValue(SelectionStartProperty, caretIndex);
MoveHome(false); MoveHome(false);
SelectionEnd = _presenter.CaretIndex; SetCurrentValue(SelectionEndProperty, _presenter.CaretIndex);
movement = true; movement = true;
selection = true; selection = true;
handled = true; handled = true;
@ -1224,9 +1220,9 @@ namespace Avalonia.Controls
} }
else if (Match(keymap.MoveCursorToTheEndOfLineWithSelection)) else if (Match(keymap.MoveCursorToTheEndOfLineWithSelection))
{ {
SelectionStart = caretIndex; SetCurrentValue(SelectionStartProperty, caretIndex);
MoveEnd(false); MoveEnd(false);
SelectionEnd = _presenter.CaretIndex; SetCurrentValue(SelectionEndProperty, _presenter.CaretIndex);
movement = true; movement = true;
selection = true; selection = true;
handled = true; handled = true;
@ -1261,11 +1257,11 @@ namespace Avalonia.Controls
if (selection) if (selection)
{ {
SelectionEnd = _presenter.CaretIndex; SetCurrentValue(SelectionEndProperty, _presenter.CaretIndex);
} }
else else
{ {
CaretIndex = _presenter.CaretIndex; SetCurrentValue(CaretIndexProperty, _presenter.CaretIndex);
} }
break; break;
@ -1283,11 +1279,11 @@ namespace Avalonia.Controls
if (selection) if (selection)
{ {
SelectionEnd = _presenter.CaretIndex; SetCurrentValue(SelectionEndProperty, _presenter.CaretIndex);
} }
else else
{ {
CaretIndex = _presenter.CaretIndex; SetCurrentValue(CaretIndexProperty, _presenter.CaretIndex);
} }
break; break;
@ -1314,11 +1310,13 @@ namespace Avalonia.Controls
var length = end - start; 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 start = Math.Min(nextPosition, caretIndex);
var end = Math.Max(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); var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
SetAndRaise(CaretIndexProperty, ref _caretIndex, index); SetCurrentValue(CaretIndexProperty, index);
switch (e.ClickCount) switch (e.ClickCount)
{ {
@ -1438,25 +1438,26 @@ namespace Avalonia.Controls
if (index > _wordSelectionStart) if (index > _wordSelectionStart)
{ {
SelectionEnd = StringUtils.NextWord(text, index); SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, index));
} }
if (index < _wordSelectionStart || previousWord == _wordSelectionStart) if (index < _wordSelectionStart || previousWord == _wordSelectionStart)
{ {
SelectionStart = previousWord; SetCurrentValue(SelectionStartProperty, previousWord);
} }
} }
else else
{ {
SelectionStart = Math.Min(oldIndex, index); SetCurrentValue(SelectionStartProperty, Math.Min(oldIndex, index));
SelectionEnd = Math.Max(oldIndex, index); SetCurrentValue(SelectionEndProperty, Math.Max(oldIndex, index));
} }
} }
else else
{ {
if(_wordSelectionStart == -1 || index < SelectionStart || index > SelectionEnd) if(_wordSelectionStart == -1 || index < SelectionStart || index > SelectionEnd)
{ {
SelectionStart = SelectionEnd = index; SetCurrentValue(SelectionStartProperty, index);
SetCurrentValue(SelectionEndProperty, index);
_wordSelectionStart = -1; _wordSelectionStart = -1;
} }
} }
@ -1466,14 +1467,14 @@ namespace Avalonia.Controls
if (!StringUtils.IsStartOfWord(text, index)) if (!StringUtils.IsStartOfWord(text, index))
{ {
SelectionStart = StringUtils.PreviousWord(text, index); SetCurrentValue(SelectionStartProperty, StringUtils.PreviousWord(text, index));
} }
_wordSelectionStart = SelectionStart; _wordSelectionStart = SelectionStart;
if (!StringUtils.IsEndOfWord(text, index)) if (!StringUtils.IsEndOfWord(text, index))
{ {
SelectionEnd = StringUtils.NextWord(text, index); SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, index));
} }
break; break;
@ -1517,22 +1518,22 @@ namespace Avalonia.Controls
if (distance <= 0) if (distance <= 0)
{ {
SelectionStart = StringUtils.PreviousWord(text, caretIndex); SetCurrentValue(SelectionStartProperty, StringUtils.PreviousWord(text, caretIndex));
} }
if (distance >= 0) if (distance >= 0)
{ {
if(SelectionStart != _wordSelectionStart) if(SelectionStart != _wordSelectionStart)
{ {
SelectionStart = _wordSelectionStart; SetCurrentValue(SelectionStartProperty, _wordSelectionStart);
} }
SelectionEnd = StringUtils.NextWord(text, caretIndex); SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, caretIndex));
} }
} }
else else
{ {
SelectionEnd = caretIndex; SetCurrentValue(SelectionEndProperty, caretIndex);
} }
} }
} }
@ -1565,7 +1566,9 @@ namespace Avalonia.Controls
caretIndex >= firstSelection && caretIndex <= lastSelection; caretIndex >= firstSelection && caretIndex <= lastSelection;
if (!didClickInSelection) 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); internal static int CoerceCaretIndex(AvaloniaObject sender, int value)
private static int CoerceCaretIndex(int value, string? text)
{ {
var text = sender.GetValue(TextProperty); // method also used by TextPresenter and SelectableTextBlock
if (text == null) if (text == null)
{ {
return 0; return 0;
@ -1619,10 +1622,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Clears the text in the TextBox /// Clears the text in the TextBox
/// </summary> /// </summary>
public void Clear() public void Clear() => SetCurrentValue(TextProperty, string.Empty);
{
Text = string.Empty;
}
private void MoveHorizontal(int direction, bool wholeWord, bool isSelecting) private void MoveHorizontal(int direction, bool wholeWord, bool isSelecting)
{ {
@ -1645,7 +1645,7 @@ namespace Avalonia.Controls
LogicalDirection.Forward : LogicalDirection.Forward :
LogicalDirection.Backward); LogicalDirection.Backward);
SelectionEnd = _presenter.CaretIndex; SetCurrentValue(SelectionEndProperty, _presenter.CaretIndex);
} }
else else
{ {
@ -1662,7 +1662,7 @@ namespace Avalonia.Controls
LogicalDirection.Backward); LogicalDirection.Backward);
} }
CaretIndex = _presenter.CaretIndex; SetCurrentValue(CaretIndexProperty, _presenter.CaretIndex);
} }
} }
else else
@ -1678,17 +1678,17 @@ namespace Avalonia.Controls
offset = StringUtils.PreviousWord(text, selectionEnd) - selectionEnd; offset = StringUtils.PreviousWord(text, selectionEnd) - selectionEnd;
} }
SelectionEnd += offset; SetCurrentValue(SelectionEndProperty, SelectionEnd + offset);
_presenter.MoveCaretToTextPosition(SelectionEnd); _presenter.MoveCaretToTextPosition(SelectionEnd);
if (!isSelecting) if (!isSelecting)
{ {
CaretIndex = SelectionEnd; SetCurrentValue(CaretIndexProperty, SelectionEnd);
} }
else else
{ {
SelectionStart = selectionStart; SetCurrentValue(SelectionStartProperty, selectionStart);
} }
} }
} }
@ -1747,36 +1747,45 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public void SelectAll() public void SelectAll()
{ {
SelectionStart = 0; SetCurrentValue(SelectionStartProperty, 0);
SelectionEnd = Text?.Length ?? 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) if (IsReadOnly)
return true; return true;
var selectionStart = SelectionStart; var (start, end) = GetSelectionRange();
var selectionEnd = SelectionEnd;
if (selectionStart != selectionEnd) if (start != end)
{ {
var start = Math.Min(selectionStart, selectionEnd);
var end = Math.Max(selectionStart, selectionEnd);
var text = Text!; 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); _presenter?.MoveCaretToTextPosition(start);
CaretIndex = start; SetCurrentValue(CaretIndexProperty, start);
ClearSelection(); ClearSelection();
return true; return true;
} }
CaretIndex = SelectionStart; SetCurrentValue(CaretIndexProperty, SelectionStart);
return false; return false;
} }
@ -1826,46 +1835,30 @@ namespace Avalonia.Controls
}, DispatcherPriority.Normal); }, 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() private void SetSelectionForControlBackspace()
{ {
var selectionStart = CaretIndex; var selectionStart = CaretIndex;
MoveHorizontal(-1, true, false); MoveHorizontal(-1, true, false);
SelectionStart = selectionStart; SetCurrentValue(SelectionStartProperty, selectionStart);
} }
private void SetSelectionForControlDelete() private void SetSelectionForControlDelete()
{ {
if (_text == null || _presenter == null) var textLength = Text?.Length ?? 0;
if (_presenter == null || textLength == 0)
{ {
return; return;
} }
SelectionStart = CaretIndex; SetCurrentValue(SelectionStartProperty, CaretIndex);
MoveHorizontal(1, true, true); 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); get => new UndoRedoState(Text, CaretIndex);
set set
{ {
Text = value.Text; SetCurrentValue(TextProperty, value.Text);
CaretIndex = value.CaretPosition; SetCurrentValue(CaretIndexProperty, value.CaretPosition);
ClearSelection(); ClearSelection();
} }
} }

2
src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

@ -177,7 +177,7 @@ namespace Avalonia.Controls
var text = GetText(preeditText); var text = GetText(preeditText);
_presenter._text = text; _presenter.SetCurrentValue(TextPresenter.TextProperty, text);
_presenter.PreeditText = preeditText; _presenter.PreeditText = preeditText;

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

@ -4,6 +4,8 @@ namespace Avalonia.Controls.Utils
{ {
class UndoRedoHelper<TState> class UndoRedoHelper<TState>
{ {
public const int DefaultUndoLimit = 10;
private readonly IUndoRedoHost _host; private readonly IUndoRedoHost _host;
public interface IUndoRedoHost public interface IUndoRedoHost
@ -23,7 +25,7 @@ namespace Avalonia.Controls.Utils
/// Maximum number of states this helper can store for undo/redo. /// Maximum number of states this helper can store for undo/redo.
/// If -1, no limit is imposed. /// If -1, no limit is imposed.
/// </summary> /// </summary>
public int Limit { get; set; } = 10; public int Limit { get; set; } = DefaultUndoLimit;
public bool CanUndo => _currentNode?.Previous != null; 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 internal class FilterTextBox : TextBox, IStyleable
{ {
public static readonly DirectProperty<FilterTextBox, bool> UseRegexFilterProperty = public static readonly StyledProperty<bool> UseRegexFilterProperty =
AvaloniaProperty.RegisterDirect<FilterTextBox, bool>(nameof(UseRegexFilter), AvaloniaProperty.Register<FilterTextBox, bool>(nameof(UseRegexFilter),
o => o.UseRegexFilter, (o, v) => o.UseRegexFilter = v,
defaultBindingMode: BindingMode.TwoWay); defaultBindingMode: BindingMode.TwoWay);
public static readonly DirectProperty<FilterTextBox, bool> UseCaseSensitiveFilterProperty = public static readonly StyledProperty<bool> UseCaseSensitiveFilterProperty =
AvaloniaProperty.RegisterDirect<FilterTextBox, bool>(nameof(UseCaseSensitiveFilter), AvaloniaProperty.Register<FilterTextBox, bool>(nameof(UseCaseSensitiveFilter),
o => o.UseCaseSensitiveFilter, (o, v) => o.UseCaseSensitiveFilter = v,
defaultBindingMode: BindingMode.TwoWay); defaultBindingMode: BindingMode.TwoWay);
public static readonly DirectProperty<FilterTextBox, bool> UseWholeWordFilterProperty = public static readonly StyledProperty<bool> UseWholeWordFilterProperty =
AvaloniaProperty.RegisterDirect<FilterTextBox, bool>(nameof(UseWholeWordFilter), AvaloniaProperty.Register<FilterTextBox, bool>(nameof(UseWholeWordFilter),
o => o.UseWholeWordFilter, (o, v) => o.UseWholeWordFilter = v,
defaultBindingMode: BindingMode.TwoWay); defaultBindingMode: BindingMode.TwoWay);
private bool _useRegexFilter, _useCaseSensitiveFilter, _useWholeWordFilter;
public FilterTextBox() public FilterTextBox()
{ {
Classes.Add("filter-text-box"); Classes.Add("filter-text-box");
@ -31,20 +26,20 @@ namespace Avalonia.Diagnostics.Controls
public bool UseRegexFilter public bool UseRegexFilter
{ {
get => _useRegexFilter; get => GetValue(UseRegexFilterProperty);
set => SetAndRaise(UseRegexFilterProperty, ref _useRegexFilter, value); set => SetValue(UseRegexFilterProperty, value);
} }
public bool UseCaseSensitiveFilter public bool UseCaseSensitiveFilter
{ {
get => _useCaseSensitiveFilter; get => GetValue(UseCaseSensitiveFilterProperty);
set => SetAndRaise(UseCaseSensitiveFilterProperty, ref _useCaseSensitiveFilter, value); set => SetValue(UseCaseSensitiveFilterProperty,value);
} }
public bool UseWholeWordFilter public bool UseWholeWordFilter
{ {
get => _useWholeWordFilter; get => GetValue(UseWholeWordFilterProperty);
set => SetAndRaise(UseWholeWordFilterProperty, ref _useWholeWordFilter, value); set => SetValue(UseWholeWordFilterProperty, value);
} }
Type IStyleable.StyleKey => typeof(TextBox); Type IStyleable.StyleKey => typeof(TextBox);

55
tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs

@ -72,51 +72,51 @@ namespace Avalonia.Base.UnitTests.Styling
[Fact] [Fact]
public void Can_Set_Direct_Property_In_Style_Without_Activator() public void Can_Set_Direct_Property_In_Style_Without_Activator()
{ {
var control = new TextBlock(); var control = new DirectPropertyClass();
var target = new Setter(); var target = new Setter();
var style = new Style(x => x.Is<TextBlock>()) var style = new Style(x => x.Is<DirectPropertyClass>())
{ {
Setters = Setters =
{ {
new Setter(TextBlock.TextProperty, "foo"), new Setter(DirectPropertyClass.FooProperty, "foo"),
} }
}; };
Apply(style, control); Apply(style, control);
Assert.Equal("foo", control.Text); Assert.Equal("foo", control.Foo);
} }
[Fact] [Fact]
public void Can_Set_Direct_Property_Binding_In_Style_Without_Activator() public void Can_Set_Direct_Property_Binding_In_Style_Without_Activator()
{ {
var control = new TextBlock(); var control = new DirectPropertyClass();
var target = new Setter(); var target = new Setter();
var source = new BehaviorSubject<object?>("foo"); var source = new BehaviorSubject<object?>("foo");
var style = new Style(x => x.Is<TextBlock>()) var style = new Style(x => x.Is<DirectPropertyClass>())
{ {
Setters = Setters =
{ {
new Setter(TextBlock.TextProperty, source.ToBinding()), new Setter(DirectPropertyClass.FooProperty, source.ToBinding()),
} }
}; };
Apply(style, control); Apply(style, control);
Assert.Equal("foo", control.Text); Assert.Equal("foo", control.Foo);
} }
[Fact] [Fact]
public void Cannot_Set_Direct_Property_Binding_In_Style_With_Activator() public void Cannot_Set_Direct_Property_Binding_In_Style_With_Activator()
{ {
var control = new TextBlock(); var control = new DirectPropertyClass();
var target = new Setter(); var target = new Setter();
var source = new BehaviorSubject<object?>("foo"); 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 = Setters =
{ {
new Setter(TextBlock.TextProperty, source.ToBinding()), new Setter(DirectPropertyClass.FooProperty, source.ToBinding()),
} }
}; };
@ -126,13 +126,13 @@ namespace Avalonia.Base.UnitTests.Styling
[Fact] [Fact]
public void Cannot_Set_Direct_Property_In_Style_With_Activator() public void Cannot_Set_Direct_Property_In_Style_With_Activator()
{ {
var control = new TextBlock(); var control = new DirectPropertyClass();
var target = new Setter(); 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 = 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); using var app = UnitTestApplication.Start(TestServices.MockThreadingInterface);
var data = new Data { Foo = "foo" }; var data = new Data { Foo = "foo" };
var control = new TextBox var control = new DirectPropertyClass
{ {
DataContext = data, DataContext = data,
}; };
var style = new Style(x => x.OfType<TextBox>()) var style = new Style(x => x.OfType<DirectPropertyClass>())
{ {
Setters = Setters =
{ {
new Setter new Setter
{ {
Property = TextBox.TextProperty, Property = DirectPropertyClass.FooProperty,
Value = new Binding Value = new Binding
{ {
Path = "Foo", Path = "Foo",
@ -310,9 +310,9 @@ namespace Avalonia.Base.UnitTests.Styling
}; };
Apply(style, control); Apply(style, control);
Assert.Equal("foo", control.Text); Assert.Equal("foo", control.Foo);
control.Text = "bar"; control.Foo = "bar";
Assert.Equal("bar", data.Foo); Assert.Equal("bar", data.Foo);
} }
@ -502,9 +502,9 @@ namespace Avalonia.Base.UnitTests.Styling
Assert.Equal(Brushes.Blue, data.Bar); 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) private void Apply(Setter setter, Control control)
@ -535,5 +535,18 @@ namespace Avalonia.Base.UnitTests.Styling
throw new NotImplementedException(); 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")); target.Inlines.Add(new Run("Hello World"));
Assert.Equal("Hello World", target.Text); Assert.Equal(null, target.Text);
Assert.Equal(1, target.Inlines.Count); Assert.Equal(1, target.Inlines.Count);

Loading…
Cancel
Save