From 0944e043cab6d7c6443826bbfe770c309d611591 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 25 Oct 2024 11:46:01 +0200 Subject: [PATCH] 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 --- samples/ControlCatalog/Pages/TextBoxPage.xaml | 2 +- .../Presenters/TextPresenter.cs | 21 ++++++-- src/Avalonia.Controls/TextBox.cs | 53 ++++++++++++++++++- .../TextBoxTests.cs | 42 +++++++++++++++ 4 files changed, 112 insertions(+), 6 deletions(-) diff --git a/samples/ControlCatalog/Pages/TextBoxPage.xaml b/samples/ControlCatalog/Pages/TextBoxPage.xaml index 74083998736..058bcff84de 100644 --- a/samples/ControlCatalog/Pages/TextBoxPage.xaml +++ b/samples/ControlCatalog/Pages/TextBoxPage.xaml @@ -43,7 +43,7 @@ + SelectionBrush="Green" SelectionForegroundBrush="Yellow" ClearSelectionOnLostFocus="False"/> diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index e35d683a1f7..622727ccbe9 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -17,6 +17,9 @@ namespace Avalonia.Controls.Presenters { public class TextPresenter : Control { + public static readonly StyledProperty ShowSelectionHighlightProperty = + AvaloniaProperty.Register(nameof(ShowSelectionHighlight), defaultValue: true); + public static readonly StyledProperty CaretIndexProperty = TextBox.CaretIndexProperty.AddOwner(new(coerce: TextBox.CoerceCaretIndex)); @@ -105,7 +108,7 @@ public class TextPresenter : Control static TextPresenter() { - AffectsRender(CaretBrushProperty, SelectionBrushProperty, SelectionForegroundBrushProperty, TextElement.ForegroundProperty); + AffectsRender(CaretBrushProperty, SelectionBrushProperty, SelectionForegroundBrushProperty, TextElement.ForegroundProperty, ShowSelectionHighlightProperty); } public TextPresenter() { } @@ -121,6 +124,15 @@ public IBrush? Background set => SetValue(BackgroundProperty, value); } + /// + /// Gets or sets a value that determines whether the TextPresenter shows a selection highlight. + /// + public bool ShowSelectionHighlight + { + get => GetValue(ShowSelectionHighlightProperty); + set => SetValue(ShowSelectionHighlightProperty, value); + } + /// /// Gets or sets the text. /// @@ -386,7 +398,7 @@ public sealed override void Render(DrawingContext context) 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 @@ public void HideCaret() _caretBlink = false; RemoveTextSelectionCanvas(); _caretTimer?.Stop(); - InvalidateVisual(); + InvalidateTextLayout(); } internal void CaretChanged() @@ -552,7 +564,7 @@ protected virtual TextLayout CreateTextLayout() } else { - if (length > 0 && SelectionForegroundBrush != null) + if (ShowSelectionHighlight && length > 0 && SelectionForegroundBrush != null) { textStyleOverrides = new[] { @@ -1031,6 +1043,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang case nameof(SelectionStart): case nameof(SelectionEnd): case nameof(SelectionForegroundBrush): + case nameof(ShowSelectionHighlightProperty): case nameof(PasswordChar): case nameof(RevealPassword): diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 458040ee7d4..62070f68474 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -44,6 +44,18 @@ public class TextBox : TemplatedControl, UndoRedoHelper.I /// public static KeyGesture? PasteGesture => Application.Current?.PlatformSettings?.HotkeyConfiguration.Paste.FirstOrDefault(); + /// + /// Defines the property + /// + public static readonly StyledProperty IsInactiveSelectionHighlightEnabledProperty = + AvaloniaProperty.Register(nameof(IsInactiveSelectionHighlightEnabled), defaultValue: true); + + /// + /// Defines the property + /// + public static readonly StyledProperty ClearSelectionOnLostFocusProperty = + AvaloniaProperty.Register(nameof(ClearSelectionOnLostFocus), defaultValue: true); + /// /// Defines the property /// @@ -373,6 +385,24 @@ public TextBox() UpdatePseudoclasses(); } + /// + /// Gets or sets a value that determines whether the TextBox shows a selection highlight when it is not focused. + /// + public bool IsInactiveSelectionHighlightEnabled + { + get => GetValue(IsInactiveSelectionHighlightEnabledProperty); + set => SetValue(IsInactiveSelectionHighlightEnabledProperty, value); + } + + /// + /// Gets or sets a value that determines whether the TextBox clears its selection after it loses focus. + /// + public bool ClearSelectionOnLostFocus + { + get=> GetValue(ClearSelectionOnLostFocusProperty); + set=> SetValue(ClearSelectionOnLostFocusProperty, value); + } + /// /// Gets or sets a value that determines whether the TextBox allows and displays newline or return characters /// @@ -880,6 +910,13 @@ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { _presenter.ShowCaret(); } + else + { + if (IsInactiveSelectionHighlightEnabled) + { + _presenter.ShowSelectionHighlight = true; + } + } _presenter.PropertyChanged += PresenterPropertyChanged; } @@ -977,6 +1014,11 @@ protected override void OnGotFocus(GotFocusEventArgs e) { 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 @@ protected override void OnLostFocus(RoutedEventArgs e) if ((ContextFlyout == null || !ContextFlyout.IsOpen) && (ContextMenu == null || !ContextMenu.IsOpen)) { - ClearSelection(); + if (ClearSelectionOnLostFocus) + { + ClearSelection(); + } + SetCurrentValue(RevealPasswordProperty, false); } @@ -1010,6 +1056,11 @@ protected override void OnLostFocus(RoutedEventArgs e) _presenter?.HideCaret(); _imClient.SetPresenter(null, null); + + if (_presenter != null && !IsInactiveSelectionHighlightEnabled) + { + _presenter.ShowSelectionHighlight = false; + } } protected override void OnTextInput(TextInputEventArgs e) diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 30f82533cea..d6b24b627f9 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -1552,6 +1552,48 @@ public void Backspace_Should_Delete_Last_Character_In_Line_And_Keep_Caret_On_Sam 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(),