diff --git a/src/Avalonia.Controls/SelectableTextBlock.cs b/src/Avalonia.Controls/SelectableTextBlock.cs index 493c1631d4..1b8935cf1f 100644 --- a/src/Avalonia.Controls/SelectableTextBlock.cs +++ b/src/Avalonia.Controls/SelectableTextBlock.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -using System.Data; using System.Linq; +using Avalonia.Collections; using Avalonia.Controls.Documents; using Avalonia.Controls.Utils; using Avalonia.Input; @@ -9,7 +9,6 @@ using Avalonia.Input.Platform; using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Media.TextFormatting; -using Avalonia.Platform; using Avalonia.Utilities; namespace Avalonia.Controls @@ -20,10 +19,10 @@ namespace Avalonia.Controls public class SelectableTextBlock : TextBlock, IInlineHost { public static readonly StyledProperty SelectionStartProperty = - TextBox.SelectionStartProperty.AddOwner(); + TextBox.SelectionStartProperty.AddOwner(new(coerce: TextBox.CoerceCaretIndex)); public static readonly StyledProperty SelectionEndProperty = - TextBox.SelectionEndProperty.AddOwner(); + TextBox.SelectionEndProperty.AddOwner(new(coerce: TextBox.CoerceCaretIndex)); public static readonly DirectProperty SelectedTextProperty = AvaloniaProperty.RegisterDirect( @@ -48,7 +47,7 @@ namespace Avalonia.Controls static SelectableTextBlock() { - FocusableProperty.OverrideDefaultValue(typeof(SelectableTextBlock), true); + FocusableProperty.OverrideDefaultValue(true); AffectsRender(SelectionStartProperty, SelectionEndProperty, SelectionBrushProperty); } @@ -331,15 +330,31 @@ namespace Avalonia.Controls { base.OnPropertyChanged(change); - if (change.Property == SelectionStartProperty || - change.Property == SelectionEndProperty) + if (change.Property == InlinesProperty) + { + if (change.OldValue is InlineCollection oldInlines) + { + oldInlines.Invalidated -= OnInlinesInvalidated; + } + + if (change.NewValue is InlineCollection newInlines) + { + newInlines.Invalidated += OnInlinesInvalidated; + } + + OnTextOrInlinesChanged(); + } + else if (change.Property == TextProperty) + { + OnTextOrInlinesChanged(); + } + else if (change.Property == SelectionStartProperty || change.Property == SelectionEndProperty) { RaisePropertyChanged(SelectedTextProperty, "", ""); UpdateCommandStates(); InvalidateTextLayout(); } - - if(change.Property == SelectionForegroundBrushProperty) + else if (change.Property == SelectionForegroundBrushProperty) { InvalidateTextLayout(); } @@ -505,6 +520,16 @@ namespace Avalonia.Controls e.Pointer.Capture(null); } + private void OnInlinesInvalidated(object? sender, EventArgs e) => OnTextOrInlinesChanged(); + + private void OnTextOrInlinesChanged() + { + CoerceValue(SelectionStartProperty); + CoerceValue(SelectionEndProperty); + RaisePropertyChanged(SelectedTextProperty, "", ""); + UpdateCommandStates(); + } + private void UpdateCommandStates() { var text = GetSelection(); diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index c8e37c132a..0126d62f27 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -2136,7 +2136,10 @@ namespace Avalonia.Controls internal static int CoerceCaretIndex(AvaloniaObject sender, int value) { - var text = sender.GetValue(TextProperty); // method also used by TextPresenter and SelectableTextBlock + // method also used by TextPresenter and SelectableTextBlock + var text = sender is SelectableTextBlock { HasComplexContent: true } textBlock + ? textBlock.Inlines?.Text + : sender.GetValue(TextProperty); if (text == null) { diff --git a/tests/Avalonia.Controls.UnitTests/SelectableTextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/SelectableTextBlockTests.cs index 58b9654e61..e360694b69 100644 --- a/tests/Avalonia.Controls.UnitTests/SelectableTextBlockTests.cs +++ b/tests/Avalonia.Controls.UnitTests/SelectableTextBlockTests.cs @@ -95,5 +95,91 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Inlines_Changes_Should_Update_Selection() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + var target = new SelectableTextBlock(); + target.Inlines!.Add(new Run("foo")); + target.SelectionEnd = 3; + + var selectedTextChanged = false; + target.PropertyChanged += (_, e) => + { + if (e.Property == SelectableTextBlock.SelectedTextProperty) + { + selectedTextChanged = true; + } + }; + + target.Inlines.Add(new Run("bar")); + + Assert.True(selectedTextChanged); + + target.SelectionStart = 6; + target.SelectionEnd = 0; + target.Inlines.RemoveAt(1); + + Assert.Equal(3, target.SelectionStart); + Assert.Equal(0, target.SelectionEnd); + + target.SelectionStart = 0; + target.SelectionEnd = 3; + + target.Inlines[0] = new Run("a"); + + Assert.Equal(0, target.SelectionStart); + Assert.Equal(1, target.SelectionEnd); + Assert.Equal("a", target.SelectedText); + } + } + + [Fact] + public void Text_Changes_Should_Update_Selection() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + var target = new SelectableTextBlock + { + Text = "foo", + SelectionEnd = 3 + }; + + var selectedTextChanged = false; + target.PropertyChanged += (_, e) => + { + if (e.Property == SelectableTextBlock.SelectedTextProperty) + { + selectedTextChanged = true; + } + }; + + target.Text = "a"; + + Assert.Equal(0, target.SelectionStart); + Assert.Equal(1, target.SelectionEnd); + Assert.True(selectedTextChanged); + } + } + + [Fact] + public void CoerceCaretIndex_OnTextChanged() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + var target = new SelectableTextBlock + { + Text = "foo", + SelectionStart = 3, + SelectionEnd = 3 + }; + + target.Text = "a"; + + Assert.Equal(1, target.SelectionStart); + Assert.Equal(1, target.SelectionEnd); + } + } } }