Skip to content

Commit

Permalink
Do not reset text selection when the TextBox loses focus (#17195)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Gillibald authored and maxkatz6 committed Oct 27, 2024
1 parent 3ebd774 commit 0b2d7e7
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 6 deletions.
2 changes: 1 addition & 1 deletion samples/ControlCatalog/Pages/TextBoxPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -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>

Expand Down
21 changes: 17 additions & 4 deletions src/Avalonia.Controls/Presenters/TextPresenter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down Expand Up @@ -105,7 +108,7 @@ public class TextPresenter : Control

static TextPresenter()
{
AffectsRender<TextPresenter>(CaretBrushProperty, SelectionBrushProperty, SelectionForegroundBrushProperty, TextElement.ForegroundProperty);
AffectsRender<TextPresenter>(CaretBrushProperty, SelectionBrushProperty, SelectionForegroundBrushProperty, TextElement.ForegroundProperty, ShowSelectionHighlightProperty);
}

public TextPresenter() { }
Expand All @@ -121,6 +124,15 @@ public IBrush? Background
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>
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -473,7 +485,7 @@ public void HideCaret()
_caretBlink = false;
RemoveTextSelectionCanvas();
_caretTimer?.Stop();
InvalidateVisual();
InvalidateTextLayout();
}

internal void CaretChanged()
Expand Down Expand Up @@ -552,7 +564,7 @@ protected virtual TextLayout CreateTextLayout()
}
else
{
if (length > 0 && SelectionForegroundBrush != null)
if (ShowSelectionHighlight && length > 0 && SelectionForegroundBrush != null)
{
textStyleOverrides = new[]
{
Expand Down Expand Up @@ -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):
Expand Down
53 changes: 52 additions & 1 deletion src/Avalonia.Controls/TextBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ public class TextBox : TemplatedControl, UndoRedoHelper<TextBox.UndoRedoState>.I
/// </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>
Expand Down Expand Up @@ -373,6 +385,24 @@ public TextBox()
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>
Expand Down Expand Up @@ -880,6 +910,13 @@ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
_presenter.ShowCaret();
}
else
{
if (IsInactiveSelectionHighlightEnabled)
{
_presenter.ShowSelectionHighlight = true;
}
}

_presenter.PropertyChanged += PresenterPropertyChanged;
}
Expand Down Expand Up @@ -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
Expand All @@ -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);
}

Expand All @@ -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)
Expand Down
42 changes: 42 additions & 0 deletions tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down

0 comments on commit 0b2d7e7

Please sign in to comment.