Browse Source

Fix SelectableTextBlock selection when text content changes (#21894)

* Fix SelectableTextBlock selection when text content changes

* Clean up
pull/21921/head
Yoyo 2 months ago
committed by GitHub
parent
commit
e36b803256
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 43
      src/Avalonia.Controls/SelectableTextBlock.cs
  2. 5
      src/Avalonia.Controls/TextBox.cs
  3. 86
      tests/Avalonia.Controls.UnitTests/SelectableTextBlockTests.cs

43
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<int> SelectionStartProperty =
TextBox.SelectionStartProperty.AddOwner<SelectableTextBlock>();
TextBox.SelectionStartProperty.AddOwner<SelectableTextBlock>(new(coerce: TextBox.CoerceCaretIndex));
public static readonly StyledProperty<int> SelectionEndProperty =
TextBox.SelectionEndProperty.AddOwner<SelectableTextBlock>();
TextBox.SelectionEndProperty.AddOwner<SelectableTextBlock>(new(coerce: TextBox.CoerceCaretIndex));
public static readonly DirectProperty<SelectableTextBlock, string> SelectedTextProperty =
AvaloniaProperty.RegisterDirect<SelectableTextBlock, string>(
@ -48,7 +47,7 @@ namespace Avalonia.Controls
static SelectableTextBlock()
{
FocusableProperty.OverrideDefaultValue(typeof(SelectableTextBlock), true);
FocusableProperty.OverrideDefaultValue<SelectableTextBlock>(true);
AffectsRender<SelectableTextBlock>(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();

5
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)
{

86
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);
}
}
}
}

Loading…
Cancel
Save