Skip to content

Commit 0b2d7e7

Browse files
Gillibaldmaxkatz6
authored andcommitted
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
1 parent 3ebd774 commit 0b2d7e7

File tree

4 files changed

+112
-6
lines changed

4 files changed

+112
-6
lines changed

samples/ControlCatalog/Pages/TextBoxPage.xaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
<TextBox Width="200" Text="Right aligned text" TextAlignment="Right" />
4444
<TextBox Width="200" Text="Custom selection brush"
4545
SelectionStart="5" SelectionEnd="22"
46-
SelectionBrush="Green" SelectionForegroundBrush="Yellow"/>
46+
SelectionBrush="Green" SelectionForegroundBrush="Yellow" ClearSelectionOnLostFocus="False"/>
4747
<TextBox Width="200" Text="Custom caret brush" CaretBrush="DarkOrange"/>
4848
</StackPanel>
4949

src/Avalonia.Controls/Presenters/TextPresenter.cs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ namespace Avalonia.Controls.Presenters
1717
{
1818
public class TextPresenter : Control
1919
{
20+
public static readonly StyledProperty<bool> ShowSelectionHighlightProperty =
21+
AvaloniaProperty.Register<TextPresenter, bool>(nameof(ShowSelectionHighlight), defaultValue: true);
22+
2023
public static readonly StyledProperty<int> CaretIndexProperty =
2124
TextBox.CaretIndexProperty.AddOwner<TextPresenter>(new(coerce: TextBox.CoerceCaretIndex));
2225

@@ -105,7 +108,7 @@ public class TextPresenter : Control
105108

106109
static TextPresenter()
107110
{
108-
AffectsRender<TextPresenter>(CaretBrushProperty, SelectionBrushProperty, SelectionForegroundBrushProperty, TextElement.ForegroundProperty);
111+
AffectsRender<TextPresenter>(CaretBrushProperty, SelectionBrushProperty, SelectionForegroundBrushProperty, TextElement.ForegroundProperty, ShowSelectionHighlightProperty);
109112
}
110113

111114
public TextPresenter() { }
@@ -121,6 +124,15 @@ public IBrush? Background
121124
set => SetValue(BackgroundProperty, value);
122125
}
123126

127+
/// <summary>
128+
/// Gets or sets a value that determines whether the TextPresenter shows a selection highlight.
129+
/// </summary>
130+
public bool ShowSelectionHighlight
131+
{
132+
get => GetValue(ShowSelectionHighlightProperty);
133+
set => SetValue(ShowSelectionHighlightProperty, value);
134+
}
135+
124136
/// <summary>
125137
/// Gets or sets the text.
126138
/// </summary>
@@ -386,7 +398,7 @@ public sealed override void Render(DrawingContext context)
386398
var selectionEnd = SelectionEnd;
387399
var selectionBrush = SelectionBrush;
388400

389-
if (selectionStart != selectionEnd && selectionBrush != null)
401+
if (ShowSelectionHighlight && selectionStart != selectionEnd && selectionBrush != null)
390402
{
391403
var start = Math.Min(selectionStart, selectionEnd);
392404
var length = Math.Max(selectionStart, selectionEnd) - start;
@@ -473,7 +485,7 @@ public void HideCaret()
473485
_caretBlink = false;
474486
RemoveTextSelectionCanvas();
475487
_caretTimer?.Stop();
476-
InvalidateVisual();
488+
InvalidateTextLayout();
477489
}
478490

479491
internal void CaretChanged()
@@ -552,7 +564,7 @@ protected virtual TextLayout CreateTextLayout()
552564
}
553565
else
554566
{
555-
if (length > 0 && SelectionForegroundBrush != null)
567+
if (ShowSelectionHighlight && length > 0 && SelectionForegroundBrush != null)
556568
{
557569
textStyleOverrides = new[]
558570
{
@@ -1031,6 +1043,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
10311043
case nameof(SelectionStart):
10321044
case nameof(SelectionEnd):
10331045
case nameof(SelectionForegroundBrush):
1046+
case nameof(ShowSelectionHighlightProperty):
10341047

10351048
case nameof(PasswordChar):
10361049
case nameof(RevealPassword):

src/Avalonia.Controls/TextBox.cs

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,18 @@ public class TextBox : TemplatedControl, UndoRedoHelper<TextBox.UndoRedoState>.I
4444
/// </summary>
4545
public static KeyGesture? PasteGesture => Application.Current?.PlatformSettings?.HotkeyConfiguration.Paste.FirstOrDefault();
4646

47+
/// <summary>
48+
/// Defines the <see cref="IsInactiveSelectionHighlightEnabled"/> property
49+
/// </summary>
50+
public static readonly StyledProperty<bool> IsInactiveSelectionHighlightEnabledProperty =
51+
AvaloniaProperty.Register<TextBox, bool>(nameof(IsInactiveSelectionHighlightEnabled), defaultValue: true);
52+
53+
/// <summary>
54+
/// Defines the <see cref="ClearSelectionOnLostFocus"/> property
55+
/// </summary>
56+
public static readonly StyledProperty<bool> ClearSelectionOnLostFocusProperty =
57+
AvaloniaProperty.Register<TextBox, bool>(nameof(ClearSelectionOnLostFocus), defaultValue: true);
58+
4759
/// <summary>
4860
/// Defines the <see cref="AcceptsReturn"/> property
4961
/// </summary>
@@ -373,6 +385,24 @@ public TextBox()
373385
UpdatePseudoclasses();
374386
}
375387

388+
/// <summary>
389+
/// Gets or sets a value that determines whether the TextBox shows a selection highlight when it is not focused.
390+
/// </summary>
391+
public bool IsInactiveSelectionHighlightEnabled
392+
{
393+
get => GetValue(IsInactiveSelectionHighlightEnabledProperty);
394+
set => SetValue(IsInactiveSelectionHighlightEnabledProperty, value);
395+
}
396+
397+
/// <summary>
398+
/// Gets or sets a value that determines whether the TextBox clears its selection after it loses focus.
399+
/// </summary>
400+
public bool ClearSelectionOnLostFocus
401+
{
402+
get=> GetValue(ClearSelectionOnLostFocusProperty);
403+
set=> SetValue(ClearSelectionOnLostFocusProperty, value);
404+
}
405+
376406
/// <summary>
377407
/// Gets or sets a value that determines whether the TextBox allows and displays newline or return characters
378408
/// </summary>
@@ -880,6 +910,13 @@ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
880910
{
881911
_presenter.ShowCaret();
882912
}
913+
else
914+
{
915+
if (IsInactiveSelectionHighlightEnabled)
916+
{
917+
_presenter.ShowSelectionHighlight = true;
918+
}
919+
}
883920

884921
_presenter.PropertyChanged += PresenterPropertyChanged;
885922
}
@@ -977,6 +1014,11 @@ protected override void OnGotFocus(GotFocusEventArgs e)
9771014
{
9781015
base.OnGotFocus(e);
9791016

1017+
if(_presenter != null)
1018+
{
1019+
_presenter.ShowSelectionHighlight = true;
1020+
}
1021+
9801022
// when navigating to a textbox via the tab key, select all text if
9811023
// 1) this textbox is *not* a multiline textbox
9821024
// 2) this textbox has any text to select
@@ -1001,7 +1043,11 @@ protected override void OnLostFocus(RoutedEventArgs e)
10011043
if ((ContextFlyout == null || !ContextFlyout.IsOpen) &&
10021044
(ContextMenu == null || !ContextMenu.IsOpen))
10031045
{
1004-
ClearSelection();
1046+
if (ClearSelectionOnLostFocus)
1047+
{
1048+
ClearSelection();
1049+
}
1050+
10051051
SetCurrentValue(RevealPasswordProperty, false);
10061052
}
10071053

@@ -1010,6 +1056,11 @@ protected override void OnLostFocus(RoutedEventArgs e)
10101056
_presenter?.HideCaret();
10111057

10121058
_imClient.SetPresenter(null, null);
1059+
1060+
if (_presenter != null && !IsInactiveSelectionHighlightEnabled)
1061+
{
1062+
_presenter.ShowSelectionHighlight = false;
1063+
}
10131064
}
10141065

10151066
protected override void OnTextInput(TextInputEventArgs e)

tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1552,6 +1552,48 @@ public void Backspace_Should_Delete_Last_Character_In_Line_And_Keep_Caret_On_Sam
15521552
Assert.Equal(oldCaretY, caretY);
15531553
}
15541554

1555+
[Fact]
1556+
public void Losing_Focus_Should_Not_Reset_Selection()
1557+
{
1558+
using (UnitTestApplication.Start(FocusServices))
1559+
{
1560+
var target1 = new TextBox
1561+
{
1562+
Template = CreateTemplate(),
1563+
Text = "1234",
1564+
ClearSelectionOnLostFocus = false
1565+
};
1566+
1567+
target1.ApplyTemplate();
1568+
1569+
var target2 = new TextBox
1570+
{
1571+
Template = CreateTemplate(),
1572+
};
1573+
1574+
target2.ApplyTemplate();
1575+
1576+
var sp = new StackPanel();
1577+
sp.Children.Add(target1);
1578+
sp.Children.Add(target2);
1579+
1580+
var root = new TestRoot() { Child = sp };
1581+
1582+
target1.SelectionStart = 0;
1583+
target1.SelectionEnd = 4;
1584+
1585+
target1.Focus();
1586+
1587+
Assert.True(target1.IsFocused);
1588+
1589+
Assert.Equal("1234", target1.SelectedText);
1590+
1591+
target2.Focus();
1592+
1593+
Assert.Equal("1234", target1.SelectedText);
1594+
}
1595+
}
1596+
15551597
private static TestServices FocusServices => TestServices.MockThreadingInterface.With(
15561598
focusManager: new FocusManager(),
15571599
keyboardDevice: () => new KeyboardDevice(),

0 commit comments

Comments
 (0)