Browse Source

Do not reset text selection when the TextBox loses focus (#17195)

* Do not reset the selected range when the TextBox loses focus
Do not render selection highlight when the TextBox doesn't has focus

* Invalidate TextLayout when the focus is lost

* Make ClearSelectionAfterFocusLost optional
Make inactive selection highlight optional

* Make sure changes to ShowSelectionHighlight invalidate the visual and text layout
release/11.2.0-rc2
Benedikt Stebner 1 year ago
committed by Max Katz
parent
commit
0b2d7e760a
  1. 2
      samples/ControlCatalog/Pages/TextBoxPage.xaml
  2. 21
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  3. 53
      src/Avalonia.Controls/TextBox.cs
  4. 42
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

2
samples/ControlCatalog/Pages/TextBoxPage.xaml

@ -43,7 +43,7 @@
<TextBox Width="200" Text="Right aligned text" TextAlignment="Right" />
<TextBox Width="200" Text="Custom selection brush"
SelectionStart="5" SelectionEnd="22"
SelectionBrush="Green" SelectionForegroundBrush="Yellow"/>
SelectionBrush="Green" SelectionForegroundBrush="Yellow" ClearSelectionOnLostFocus="False"/>
<TextBox Width="200" Text="Custom caret brush" CaretBrush="DarkOrange"/>
</StackPanel>

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

@ -17,6 +17,9 @@ namespace Avalonia.Controls.Presenters
{
public class TextPresenter : Control
{
public static readonly StyledProperty<bool> ShowSelectionHighlightProperty =
AvaloniaProperty.Register<TextPresenter, bool>(nameof(ShowSelectionHighlight), defaultValue: true);
public static readonly StyledProperty<int> CaretIndexProperty =
TextBox.CaretIndexProperty.AddOwner<TextPresenter>(new(coerce: TextBox.CoerceCaretIndex));
@ -105,7 +108,7 @@ namespace Avalonia.Controls.Presenters
static TextPresenter()
{
AffectsRender<TextPresenter>(CaretBrushProperty, SelectionBrushProperty, SelectionForegroundBrushProperty, TextElement.ForegroundProperty);
AffectsRender<TextPresenter>(CaretBrushProperty, SelectionBrushProperty, SelectionForegroundBrushProperty, TextElement.ForegroundProperty, ShowSelectionHighlightProperty);
}
public TextPresenter() { }
@ -121,6 +124,15 @@ namespace Avalonia.Controls.Presenters
set => SetValue(BackgroundProperty, value);
}
/// <summary>
/// Gets or sets a value that determines whether the TextPresenter shows a selection highlight.
/// </summary>
public bool ShowSelectionHighlight
{
get => GetValue(ShowSelectionHighlightProperty);
set => SetValue(ShowSelectionHighlightProperty, value);
}
/// <summary>
/// Gets or sets the text.
/// </summary>
@ -386,7 +398,7 @@ namespace Avalonia.Controls.Presenters
var selectionEnd = SelectionEnd;
var selectionBrush = SelectionBrush;
if (selectionStart != selectionEnd && selectionBrush != null)
if (ShowSelectionHighlight && selectionStart != selectionEnd && selectionBrush != null)
{
var start = Math.Min(selectionStart, selectionEnd);
var length = Math.Max(selectionStart, selectionEnd) - start;
@ -473,7 +485,7 @@ namespace Avalonia.Controls.Presenters
_caretBlink = false;
RemoveTextSelectionCanvas();
_caretTimer?.Stop();
InvalidateVisual();
InvalidateTextLayout();
}
internal void CaretChanged()
@ -552,7 +564,7 @@ namespace Avalonia.Controls.Presenters
}
else
{
if (length > 0 && SelectionForegroundBrush != null)
if (ShowSelectionHighlight && length > 0 && SelectionForegroundBrush != null)
{
textStyleOverrides = new[]
{
@ -1031,6 +1043,7 @@ namespace Avalonia.Controls.Presenters
case nameof(SelectionStart):
case nameof(SelectionEnd):
case nameof(SelectionForegroundBrush):
case nameof(ShowSelectionHighlightProperty):
case nameof(PasswordChar):
case nameof(RevealPassword):

53
src/Avalonia.Controls/TextBox.cs

@ -44,6 +44,18 @@ namespace Avalonia.Controls
/// </summary>
public static KeyGesture? PasteGesture => Application.Current?.PlatformSettings?.HotkeyConfiguration.Paste.FirstOrDefault();
/// <summary>
/// Defines the <see cref="IsInactiveSelectionHighlightEnabled"/> property
/// </summary>
public static readonly StyledProperty<bool> IsInactiveSelectionHighlightEnabledProperty =
AvaloniaProperty.Register<TextBox, bool>(nameof(IsInactiveSelectionHighlightEnabled), defaultValue: true);
/// <summary>
/// Defines the <see cref="ClearSelectionOnLostFocus"/> property
/// </summary>
public static readonly StyledProperty<bool> ClearSelectionOnLostFocusProperty =
AvaloniaProperty.Register<TextBox, bool>(nameof(ClearSelectionOnLostFocus), defaultValue: true);
/// <summary>
/// Defines the <see cref="AcceptsReturn"/> property
/// </summary>
@ -373,6 +385,24 @@ namespace Avalonia.Controls
UpdatePseudoclasses();
}
/// <summary>
/// Gets or sets a value that determines whether the TextBox shows a selection highlight when it is not focused.
/// </summary>
public bool IsInactiveSelectionHighlightEnabled
{
get => GetValue(IsInactiveSelectionHighlightEnabledProperty);
set => SetValue(IsInactiveSelectionHighlightEnabledProperty, value);
}
/// <summary>
/// Gets or sets a value that determines whether the TextBox clears its selection after it loses focus.
/// </summary>
public bool ClearSelectionOnLostFocus
{
get=> GetValue(ClearSelectionOnLostFocusProperty);
set=> SetValue(ClearSelectionOnLostFocusProperty, value);
}
/// <summary>
/// Gets or sets a value that determines whether the TextBox allows and displays newline or return characters
/// </summary>
@ -880,6 +910,13 @@ namespace Avalonia.Controls
{
_presenter.ShowCaret();
}
else
{
if (IsInactiveSelectionHighlightEnabled)
{
_presenter.ShowSelectionHighlight = true;
}
}
_presenter.PropertyChanged += PresenterPropertyChanged;
}
@ -977,6 +1014,11 @@ namespace Avalonia.Controls
{
base.OnGotFocus(e);
if(_presenter != null)
{
_presenter.ShowSelectionHighlight = true;
}
// when navigating to a textbox via the tab key, select all text if
// 1) this textbox is *not* a multiline textbox
// 2) this textbox has any text to select
@ -1001,7 +1043,11 @@ namespace Avalonia.Controls
if ((ContextFlyout == null || !ContextFlyout.IsOpen) &&
(ContextMenu == null || !ContextMenu.IsOpen))
{
ClearSelection();
if (ClearSelectionOnLostFocus)
{
ClearSelection();
}
SetCurrentValue(RevealPasswordProperty, false);
}
@ -1010,6 +1056,11 @@ namespace Avalonia.Controls
_presenter?.HideCaret();
_imClient.SetPresenter(null, null);
if (_presenter != null && !IsInactiveSelectionHighlightEnabled)
{
_presenter.ShowSelectionHighlight = false;
}
}
protected override void OnTextInput(TextInputEventArgs e)

42
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@ -1552,6 +1552,48 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(oldCaretY, caretY);
}
[Fact]
public void Losing_Focus_Should_Not_Reset_Selection()
{
using (UnitTestApplication.Start(FocusServices))
{
var target1 = new TextBox
{
Template = CreateTemplate(),
Text = "1234",
ClearSelectionOnLostFocus = false
};
target1.ApplyTemplate();
var target2 = new TextBox
{
Template = CreateTemplate(),
};
target2.ApplyTemplate();
var sp = new StackPanel();
sp.Children.Add(target1);
sp.Children.Add(target2);
var root = new TestRoot() { Child = sp };
target1.SelectionStart = 0;
target1.SelectionEnd = 4;
target1.Focus();
Assert.True(target1.IsFocused);
Assert.Equal("1234", target1.SelectedText);
target2.Focus();
Assert.Equal("1234", target1.SelectedText);
}
}
private static TestServices FocusServices => TestServices.MockThreadingInterface.With(
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice(),

Loading…
Cancel
Save