Преглед на файлове

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
Benedikt Stebner преди 1 година
родител
ревизия
0944e043ca

+ 1 - 1
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>
 

+ 17 - 4
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):

+ 52 - 1
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 - 0
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(),