Browse Source

wip touch improvement textbox

text_selector_zindex
Emmanuel Hansen 3 weeks ago
parent
commit
d0bdab0fa4
  1. 2
      samples/ControlCatalog/MainView.xaml
  2. 95
      samples/ControlCatalog/Pages/TextBoxPage.xaml
  3. 11
      src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
  4. 17
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  5. 348
      src/Avalonia.Controls/Primitives/TextSelectionCanvas.cs
  6. 16
      src/Avalonia.Controls/Primitives/TextSelectionHandle.cs
  7. 399
      src/Avalonia.Controls/TextBox.cs
  8. 3
      src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

2
samples/ControlCatalog/MainView.xaml

@ -173,7 +173,7 @@
<TabItem Header="TabStrip"> <TabItem Header="TabStrip">
<pages:TabStripPage /> <pages:TabStripPage />
</TabItem> </TabItem>
<TabItem Header="TextBox"> <TabItem Header="TextBox" IsSelected="True">
<pages:TextBoxPage /> <pages:TextBoxPage />
</TabItem> </TabItem>
<TabItem Header="TextBlock"> <TabItem Header="TextBlock">

95
samples/ControlCatalog/Pages/TextBoxPage.xaml

@ -8,97 +8,10 @@
<WrapPanel Margin="-8,0" <WrapPanel Margin="-8,0"
HorizontalAlignment="Center"> HorizontalAlignment="Center">
<StackPanel Orientation="Vertical" Spacing="8" Margin="8"> <StackPanel Orientation="Vertical" Spacing="8" Margin="8">
<TextBox Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit." Width="200" <TextBox AcceptsReturn="True" TextWrapping="Wrap" Width="500" Height="300"
FontFamily="Comic Sans MS" Text="Multiline TextBox with TextWrapping.&#xD;&#xD;Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est. Multiline TextBox with TextWrapping.&#xD;&#xD;Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est. Multiline TextBox with TextWrapping.&#xD;&#xD;Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est. Multiline TextBox with TextWrapping.&#xD;&#xD;Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est." />
Foreground="Blue">
<TextBox.ContextFlyout>
<Flyout>
<TextBlock>Custom context flyout</TextBlock>
</Flyout>
</TextBox.ContextFlyout>
</TextBox>
<TextBox Width="200" PlaceholderText="ReadOnly" IsReadOnly="True" Text="This is read only"/>
<TextBox Width="200" PlaceholderText="Numeric with placeholder" TextInputOptions.ContentType="Number" />
<TextBox Width="200"
PlaceholderText="Floating Placeholder"
UseFloatingPlaceholder="True"
Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit."/>
<TextBox Width="200"
PlaceholderText="Custom Placeholder Color"
PlaceholderForeground="Red"/>
<TextBox Width="200"
PlaceholderText="Floating Placeholder Color"
UseFloatingPlaceholder="True"
PlaceholderForeground="Purple"/>
<MaskedTextBox Width="200" ResetOnSpace="False" Mask="(LLL) 999-0000"/>
<TextBox Width="200" Text="Validation Error">
<DataValidationErrors.Error>
<sys:Exception />
</DataValidationErrors.Error>
</TextBox>
<TextBox Width="200"
PlaceholderText="Password Box"
Classes="revealPasswordButton"
TextInputOptions.ContentType="Password"
UseFloatingPlaceholder="True"
PasswordChar="*"
Text="Password" />
<TextBox Width="200" PlaceholderText="Suggestions are hidden" TextInputOptions.ShowSuggestions="False" />
<TextBox Width="200" Text="Left aligned text" TextAlignment="Left" AcceptsTab="True" />
<TextBox Width="200" Text="Center aligned text" TextAlignment="Center" />
<TextBox Width="200" Text="Right aligned text" TextAlignment="Right" />
<TextBox Width="200" Text="Custom selection brush"
SelectionStart="5" SelectionEnd="22"
SelectionBrush="Green" SelectionForegroundBrush="Yellow" ClearSelectionOnLostFocus="False"/>
<TextBox Width="200" Text="Custom caret brush" CaretBrush="DarkOrange"/>
</StackPanel>
<StackPanel Orientation="Vertical" Spacing="8" Margin="8">
<TextBox AcceptsReturn="True" TextWrapping="Wrap" Width="200" Height="125"
Text="Multiline TextBox with TextWrapping.&#xD;&#xD;Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est." />
<TextBox AcceptsReturn="True" Width="200" Height="125"
Text="Multiline TextBox with no TextWrapping.&#xD;&#xD;Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est." />
<TextBox Classes="clearButton" Text="Clear Content" Width="200" FontWeight="Normal" FontStyle="Normal" PlaceholderText="Placeholder" FontFamily="avares://ControlCatalog/Assets/Fonts#Source Sans Pro"/>
<TextBox Text="IME small font" Width="200"
FontFamily="Comic Sans MS"
FontSize="10"
Foreground="Red"/>
<TextBox Text="IME large font" Width="200"
FontFamily="Comic Sans MS"
FontSize="22"
Foreground="Red"/>
<TextBox Text="IME disabled" Width="200"
FontFamily="Comic Sans MS"
InputMethod.IsInputMethodEnabled="False"
Foreground="Red"/>
<TextBox AcceptsReturn="True"
TextWrapping="Wrap"
Width="200"
Height="125"
LineHeight="32"
Text="Multiline TextBox with TextWrapping and increased LineHeight.&#xD;&#xD;Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est." />
</StackPanel>
<StackPanel Orientation="Vertical" Spacing="8" Margin="8">
<Label Classes="h2" Target="{Binding #firstResMFont}">res_m fonts</Label>
<TextBox Width="200" x:Name="firstResMFont" Text="Custom font regular" FontWeight="Normal" FontStyle="Normal" FontFamily="resm:ControlCatalog.Assets.Fonts?assembly=ControlCatalog#Source Sans Pro"/>
<TextBox Width="200" Text="Custom font bold" FontWeight="Bold" FontStyle="Normal" FontFamily="resm:ControlCatalog.Assets.Fonts?assembly=ControlCatalog#Source Sans Pro"/>
<TextBox Width="200" Text="Custom font italic" FontWeight="Normal" FontStyle="Italic" FontFamily="resm:ControlCatalog.Assets.Fonts.SourceSansPro-Italic.ttf?assembly=ControlCatalog#Source Sans Pro"/>
<TextBox Width="200" Text="Custom font italic bold" FontWeight="Bold" FontStyle="Italic" FontFamily="resm:ControlCatalog.Assets.Fonts.SourceSansPro-*.ttf?assembly=ControlCatalog#Source Sans Pro"/>
</StackPanel>
<StackPanel Orientation="Vertical" Spacing="8" Margin="8">
<Label Classes="h2" Target="{Binding #firstResFont}">_res fonts</Label>
<TextBox Width="200" x:Name="firstResFont" Text="Custom font regular" FontWeight="Normal" FontStyle="Normal" FontFamily="avares://ControlCatalog/Assets/Fonts#Source Sans Pro"/>
<TextBox Width="200" Text="Custom font bold" FontWeight="Bold" FontStyle="Normal" FontFamily="avares://ControlCatalog/Assets/Fonts#Source Sans Pro"/>
<TextBox Width="200" Text="Custom font italic" FontWeight="Normal" FontStyle="Italic" FontFamily="/Assets/Fonts/SourceSansPro-Italic.ttf#Source Sans Pro"/>
<TextBox Width="200" Text="Custom font italic bold" FontWeight="Bold" FontStyle="Italic" FontFamily="/Assets/Fonts/SourceSansPro-*.ttf#Source Sans Pro"/>
</StackPanel> </StackPanel>
</WrapPanel> </WrapPanel>
<TextBox AcceptsReturn="True" TextWrapping="Wrap" Height="200" MaxWidth="400" </StackPanel>
FontFamily="avares://ControlCatalog/Assets/Fonts#WenQuanYi Micro Hei"
Text="计算机科学(是系统性研究信息与计算的理论基础以及它们在计算机系统中如何实现与应用的实用技术的学科。它通常被形容为对那些创造、描述以及转换信息的算法处理的系统研究。计算机科学包含很多分支领域;有些强调特定结果的计算,比如计算机图形学;而有些是探討计算问题的性质,比如计算复杂性理论;还有一些领域專注于怎样实现计算,比如程式語言理論是研究描述计算的方法,而程式设计是应用特定的程式語言解决特定的计算问题,人机交互则是專注于怎样使计算机和计算变得有用、好用,以及随时随地为人所用。&#xD;&#xD;有时公众会误以为计算机科学就是解决计算机问题的事业(比如信息技术),或者只是与使用计算机的经验有关,如玩游戏、上网或者文字处理。其实计算机科学所关注的,不仅仅是去理解实现类似游戏、浏览器这些软件的程序的性质,更要通过现有的知识创造新的程序或者改进已有的程序。" />
</StackPanel>
</UserControl> </UserControl>

11
src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs

@ -133,8 +133,6 @@ namespace Avalonia.Input.GestureRecognizers
_trackedRootPoint = new Point( _trackedRootPoint = new Point(
_trackedRootPoint.X - (_trackedRootPoint.X >= rootPoint.X ? ScrollStartDistance : -ScrollStartDistance), _trackedRootPoint.X - (_trackedRootPoint.X >= rootPoint.X ? ScrollStartDistance : -ScrollStartDistance),
_trackedRootPoint.Y - (_trackedRootPoint.Y >= rootPoint.Y ? ScrollStartDistance : -ScrollStartDistance)); _trackedRootPoint.Y - (_trackedRootPoint.Y >= rootPoint.Y ? ScrollStartDistance : -ScrollStartDistance));
Capture(e.Pointer);
} }
} }
@ -145,9 +143,14 @@ namespace Avalonia.Input.GestureRecognizers
_velocityTracker?.AddPosition(TimeSpan.FromMilliseconds(e.Timestamp), _pointerPressedPoint - rootPoint); _velocityTracker?.AddPosition(TimeSpan.FromMilliseconds(e.Timestamp), _pointerPressedPoint - rootPoint);
_lastMoveTimestamp = e.Timestamp; _lastMoveTimestamp = e.Timestamp;
Target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector)); var scrollEventArgs = new ScrollGestureEventArgs(_gestureId, vector);
Target!.RaiseEvent(scrollEventArgs);
_trackedRootPoint = rootPoint; _trackedRootPoint = rootPoint;
e.Handled = true; e.Handled = scrollEventArgs.Handled;
if(e.Handled)
{
Capture(e.Pointer);
}
} }
} }
} }

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

@ -1,14 +1,14 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data;
using Avalonia.Controls.Documents; using Avalonia.Controls.Documents;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Interactivity; using Avalonia.Input;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Platform;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.Utilities; using Avalonia.Utilities;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -334,6 +334,7 @@ namespace Avalonia.Controls.Presenters
protected override bool BypassFlowDirectionPolicies => true; protected override bool BypassFlowDirectionPolicies => true;
internal TextSelectionHandleCanvas? TextSelectionHandleCanvas { get; set; } internal TextSelectionHandleCanvas? TextSelectionHandleCanvas { get; set; }
internal TextBoxTextInputMethodClient? CurrentImClient { get; set; }
/// <summary> /// <summary>
/// Creates the <see cref="TextLayout"/> used to render the text. /// Creates the <see cref="TextLayout"/> used to render the text.
@ -1026,15 +1027,7 @@ namespace Avalonia.Controls.Presenters
OnPreeditChanged(PreeditText, PreeditTextCursorPosition); OnPreeditChanged(PreeditText, PreeditTextCursorPosition);
} }
if(change.Property == TextProperty) if(change.Property == TextProperty || change.Property == CaretIndexProperty)
{
if (!string.IsNullOrEmpty(PreeditText))
{
SetCurrentValue(PreeditTextProperty, null);
}
}
if(change.Property == CaretIndexProperty)
{ {
if (!string.IsNullOrEmpty(PreeditText)) if (!string.IsNullOrEmpty(PreeditText))
{ {
@ -1067,7 +1060,7 @@ namespace Avalonia.Controls.Presenters
case nameof(SelectionStart): case nameof(SelectionStart):
case nameof(SelectionEnd): case nameof(SelectionEnd):
case nameof(SelectionForegroundBrush): case nameof(SelectionForegroundBrush):
case nameof(ShowSelectionHighlightProperty): case nameof(ShowSelectionHighlight):
case nameof(PasswordChar): case nameof(PasswordChar):
case nameof(RevealPassword): case nameof(RevealPassword):

348
src/Avalonia.Controls/Primitives/TextSelectionCanvas.cs

@ -1,9 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Threading;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives namespace Avalonia.Controls.Primitives
@ -11,6 +12,7 @@ namespace Avalonia.Controls.Primitives
internal class TextSelectionHandleCanvas : Canvas internal class TextSelectionHandleCanvas : Canvas
{ {
private const int ContextMenuPadding = 16; private const int ContextMenuPadding = 16;
private static bool s_isInTouchMode;
private readonly TextSelectionHandle _caretHandle; private readonly TextSelectionHandle _caretHandle;
private readonly TextSelectionHandle _startHandle; private readonly TextSelectionHandle _startHandle;
@ -18,7 +20,8 @@ namespace Avalonia.Controls.Primitives
private TextPresenter? _presenter; private TextPresenter? _presenter;
private TextBox? _textBox; private TextBox? _textBox;
private bool _showHandle; private bool _showHandle;
private bool _canShowContextMenu = true; private IDisposable? _showDisposable;
private PresenterVisualListener _layoutListener;
internal bool ShowHandles internal bool ShowHandles
{ {
@ -66,30 +69,46 @@ namespace Avalonia.Controls.Primitives
_caretHandle.SetTopLeft(default); _caretHandle.SetTopLeft(default);
_endHandle.SetTopLeft(default); _endHandle.SetTopLeft(default);
_startHandle.PointerReleased += Handle_PointerReleased; _startHandle.ContextCanceled += Caret_ContextCanceled;
_caretHandle.PointerReleased += Handle_PointerReleased; _caretHandle.ContextCanceled += Caret_ContextCanceled;
_endHandle.PointerReleased += Handle_PointerReleased; _endHandle.ContextCanceled += Caret_ContextCanceled;
_startHandle.ContextRequested += Caret_ContextRequested;
_startHandle.Holding += Caret_Holding; _caretHandle.ContextRequested += Caret_ContextRequested;
_caretHandle.Holding += Caret_Holding; _endHandle.ContextRequested += Caret_ContextRequested;
_endHandle.Holding += Caret_Holding;
IsVisible = ShowHandles; IsVisible = ShowHandles;
ClipToBounds = false; ClipToBounds = false;
_layoutListener = new PresenterVisualListener();
_layoutListener.Invalidated += LayoutListener_Invalidated;
} }
private void Handle_PointerReleased(object? sender, PointerReleasedEventArgs e) private void LayoutListener_Invalidated(object? sender, EventArgs e)
{ {
ShowContextMenu(); if (ShowHandles)
MoveHandlesToSelection();
}
private void Caret_ContextCanceled(object? sender, RoutedEventArgs e)
{
CloseFlyout();
}
private void Caret_ContextRequested(object? sender, ContextRequestedEventArgs e)
{
ShowFlyout(e);
e.Handled = true;
} }
private void Handle_DragStarted(object? sender, VectorEventArgs e) private void Handle_DragStarted(object? sender, VectorEventArgs e)
{ {
if (_textBox?.ContextFlyout is { } flyout) CloseFlyout();
{ }
flyout.Hide();
} private void CloseFlyout()
{
_presenter?.RaiseEvent(new Interactivity.RoutedEventArgs(InputElement.ContextCanceledEvent));
} }
private void EndHandle_DragDelta(object? sender, VectorEventArgs e) private void EndHandle_DragDelta(object? sender, VectorEventArgs e)
@ -106,41 +125,99 @@ namespace Avalonia.Controls.Primitives
private void CaretHandle_DragDelta(object? sender, VectorEventArgs e) private void CaretHandle_DragDelta(object? sender, VectorEventArgs e)
{ {
_canShowContextMenu = false;
if (_presenter != null && _textBox != null) if (_presenter != null && _textBox != null)
{ {
var point = ToPresenter(_caretHandle.IndicatorPosition); var point = ToPresenter(_caretHandle.IndicatorPosition);
using var _ = BeginChange();
_presenter.MoveCaretToPoint(point); _presenter.MoveCaretToPoint(point);
_textBox.SelectionStart = _textBox.SelectionEnd = _presenter.CaretIndex; var caretIndex = _presenter.CaretIndex;
var points = _presenter.GetCaretPoints(); _textBox.SetCurrentValue(TextBox.CaretIndexProperty, caretIndex);
_textBox.SetCurrentValue(TextBox.SelectionStartProperty, caretIndex);
_textBox.SetCurrentValue(TextBox.SelectionEndProperty, caretIndex);
ClampHandle(_caretHandle);
}
}
_caretHandle?.SetTopLeft(ToLayer(points.Item2)); public void Hide()
{
ShowHandles = false;
}
internal void ShowOnFocused()
{
if (s_isInTouchMode)
{
MoveHandlesToSelection();
}
}
private IDisposable? BeginChange()
{
return _presenter?.CurrentImClient?.BeginChange();
}
private void ClampHandle(TextSelectionHandle handle)
{
var bounds = _presenter?.GetTransformedBounds();
if (bounds.HasValue)
{
var point = _caretHandle.IndicatorPosition;
var rect = bounds.Value.Clip;
if (point.X < rect.X)
point = point.WithX(rect.X);
if (point.X > rect.Right)
point = point.WithX(rect.Right);
if (point.Y < rect.Y)
point = point.WithY(rect.Y);
if (point.Y > rect.Bottom)
point = point.WithY(rect.Bottom);
handle?.SetTopLeft(point);
} }
} }
private void Handle_DragCompleted(object? sender, VectorEventArgs e) private void Handle_DragCompleted(object? sender, VectorEventArgs e)
{ {
MoveHandlesToSelection(); MoveHandlesToSelection();
ShowContextMenu();
} }
private void EnsureVisible() private void EnsureVisible()
{ {
if (_textBox is { } t && t.VisualRoot is Visual r) _showDisposable?.Dispose();
_showDisposable = null;
if (_presenter is { } presenter && presenter.VisualRoot is InputElement root)
{ {
var bounds = t.Bounds; var bounds = presenter.GetTransformedBounds();
var topLeft = t.TranslatePoint(default, r) ?? default;
bounds = bounds.WithX(topLeft.X).WithY(topLeft.Y); if (bounds == null)
return;
var hasSelection = _textBox.SelectionStart != _textBox.SelectionEnd;
var hasSelection = _presenter.SelectionStart != _presenter.SelectionEnd;
_startHandle.IsVisible = bounds.Contains(new Point(GetLeft(_startHandle), GetTop(_startHandle))) &&
ShowHandles && hasSelection; _startHandle.IsVisible = ShowHandles && hasSelection &&
_endHandle.IsVisible = bounds.Contains(new Point(GetLeft(_endHandle), GetTop(_endHandle))) && !IsOccluded(new Point(GetLeft(_startHandle), GetTop(_startHandle)));
ShowHandles && hasSelection; _endHandle.IsVisible = ShowHandles && hasSelection &&
_caretHandle.IsVisible = bounds.Contains(new Point(GetLeft(_caretHandle), GetTop(_caretHandle))) && !IsOccluded(new Point(GetLeft(_endHandle), GetTop(_endHandle)));
ShowHandles && !hasSelection; _caretHandle.IsVisible = ShowHandles && !hasSelection &&
!IsOccluded(new Point(GetLeft(_caretHandle), GetTop(_caretHandle)));
bool IsOccluded(Point point)
{
return !bounds.Value.Clip.Contains(point);
}
if (ShowHandles && !hasSelection)
{
_showDisposable = DispatcherTimer.RunOnce(() =>
{
ShowHandles = false;
_showDisposable?.Dispose();
}, TimeSpan.FromSeconds(5), DispatcherPriority.Background);
}
} }
} }
@ -148,10 +225,7 @@ namespace Avalonia.Controls.Primitives
{ {
if (_presenter != null && _textBox != null) if (_presenter != null && _textBox != null)
{ {
if (_textBox.ContextFlyout is { } flyout) CloseFlyout();
{
flyout.Hide();
}
var point = ToPresenter(handle.IndicatorPosition); var point = ToPresenter(handle.IndicatorPosition);
point = point.WithY(point.Y - _presenter.FontSize / 2); point = point.WithY(point.Y - _presenter.FontSize / 2);
@ -159,18 +233,19 @@ namespace Avalonia.Controls.Primitives
var position = hit.CharacterHit.FirstCharacterIndex + hit.CharacterHit.TrailingLength; var position = hit.CharacterHit.FirstCharacterIndex + hit.CharacterHit.TrailingLength;
var otherHandle = handle == _startHandle ? _endHandle : _startHandle; var otherHandle = handle == _startHandle ? _endHandle : _startHandle;
using var _ = BeginChange();
if (handle.SelectionHandleType == SelectionHandleType.Start) if (handle.SelectionHandleType == SelectionHandleType.Start)
{ {
if (position >= _textBox.SelectionEnd) if (position >= _textBox.SelectionEnd)
position = _textBox.SelectionEnd - 1; position = _textBox.SelectionEnd - 1;
_textBox.SelectionStart = position; _textBox.SetCurrentValue(TextBox.SelectionStartProperty, position);
} }
else else
{ {
if (position <= _textBox.SelectionStart) if (position <= _textBox.SelectionStart)
position = _textBox.SelectionStart + 1; position = _textBox.SelectionStart + 1;
_textBox.SelectionEnd = position; _textBox.SetCurrentValue(TextBox.SelectionEndProperty, position);
} }
var selectionStart = _textBox.SelectionStart; var selectionStart = _textBox.SelectionStart;
@ -182,7 +257,7 @@ namespace Avalonia.Controls.Primitives
if (rects.Count > 0) if (rects.Count > 0)
{ {
var first = rects[0]; var first = rects[0];
var last = rects[rects.Count -1]; var last = rects[rects.Count - 1];
if (handle.SelectionHandleType == SelectionHandleType.Start) if (handle.SelectionHandleType == SelectionHandleType.Start)
handle?.SetTopLeft(ToLayer(first.BottomLeft)); handle?.SetTopLeft(ToLayer(first.BottomLeft));
@ -196,9 +271,9 @@ namespace Avalonia.Controls.Primitives
} }
_presenter?.MoveCaretToTextPosition(position); _presenter?.MoveCaretToTextPosition(position);
}
EnsureVisible(); EnsureVisible();
}
} }
private Point ToLayer(Point point) private Point ToLayer(Point point)
@ -211,24 +286,19 @@ namespace Avalonia.Controls.Primitives
return (_presenter is { } p) ? (p.VisualRoot as Visual)?.TranslatePoint(point, p) ?? point : point; return (_presenter is { } p) ? (p.VisualRoot as Visual)?.TranslatePoint(point, p) ?? point : point;
} }
private Point ToTextBox(Point point)
{
return (_textBox is { } p) ? (p.VisualRoot as Visual)?.TranslatePoint(point, p) ?? point : point;
}
public void MoveHandlesToSelection() public void MoveHandlesToSelection()
{ {
if (_presenter == null if (_presenter == null
|| _textBox == null
|| _startHandle.IsDragging || _startHandle.IsDragging
|| _endHandle.IsDragging || _caretHandle.IsDragging
|| _textBox.ContextFlyout?.IsOpen == true || _endHandle.IsDragging)
|| _textBox.ContextMenu?.IsOpen == true)
{ {
return; return;
} }
var hasSelection = _textBox.SelectionStart != _textBox.SelectionEnd; var selectionStart = _presenter.SelectionStart;
var selectionEnd = _presenter.SelectionEnd;
var hasSelection = selectionStart != selectionEnd;
var points = _presenter.GetCaretPoints(); var points = _presenter.GetCaretPoints();
@ -236,8 +306,6 @@ namespace Avalonia.Controls.Primitives
if (hasSelection) if (hasSelection)
{ {
var selectionStart = _textBox.SelectionStart;
var selectionEnd = _textBox.SelectionEnd;
var start = Math.Min(selectionStart, selectionEnd); var start = Math.Min(selectionStart, selectionEnd);
var length = Math.Max(selectionStart, selectionEnd) - start; var length = Math.Max(selectionStart, selectionEnd) - start;
@ -265,6 +333,10 @@ namespace Avalonia.Controls.Primitives
} }
} }
} }
ShowHandles = true;
EnsureVisible();
} }
internal void SetPresenter(TextPresenter? textPresenter) internal void SetPresenter(TextPresenter? textPresenter)
@ -272,58 +344,77 @@ namespace Avalonia.Controls.Primitives
if (_presenter == textPresenter) if (_presenter == textPresenter)
return; return;
if (_textBox != null) if (_presenter != null)
{ {
_textBox.RemoveHandler(TextBox.TextChangingEvent, TextChanged); _layoutListener.Detach();
_textBox.RemoveHandler(KeyDownEvent, TextBoxKeyDown); _presenter.RemoveHandler(KeyDownEvent, PresenterKeyDown);
_presenter.RemoveHandler(TappedEvent, PresenterTapped);
_textBox.PropertyChanged -= TextBoxPropertyChanged; _presenter.RemoveHandler(PointerPressedEvent, PresenterPressed);
_textBox.EffectiveViewportChanged -= TextBoxEffectiveViewportChanged; _presenter.RemoveHandler(GotFocusEvent, PresenterFocused);
_textBox.SizeChanged -= TextBox_SizeChanged;
if (_textBox != null)
{
_textBox.PropertyChanged -= TextBox_PropertyChanged;
}
_textBox = null; _textBox = null;
_presenter = null;
} }
_presenter = textPresenter; _presenter = textPresenter;
if (_presenter != null) if (_presenter != null)
{ {
_layoutListener.Attach(_presenter);
_presenter.AddHandler(KeyDownEvent, PresenterKeyDown, handledEventsToo: true);
_presenter.AddHandler(TappedEvent, PresenterTapped);
_presenter.AddHandler(PointerPressedEvent, PresenterPressed);
_presenter.AddHandler(GotFocusEvent, PresenterFocused, handledEventsToo: true);
_textBox = _presenter.FindAncestorOfType<TextBox>(); _textBox = _presenter.FindAncestorOfType<TextBox>();
if (_textBox != null) if (_textBox != null)
{ {
_textBox.AddHandler(TextBox.TextChangingEvent, TextChanged, handledEventsToo: true); _textBox.PropertyChanged += TextBox_PropertyChanged;
_textBox.AddHandler(KeyDownEvent, TextBoxKeyDown, handledEventsToo: true);
_textBox.PropertyChanged += TextBoxPropertyChanged;
_textBox.EffectiveViewportChanged += TextBoxEffectiveViewportChanged;
_textBox.SizeChanged += TextBox_SizeChanged;
} }
} }
} }
private void TextBox_SizeChanged(object? sender, SizeChangedEventArgs e) private void PresenterPressed(object? sender, PointerPressedEventArgs e)
{ {
InvalidateMeasure(); s_isInTouchMode = e.Pointer.Type != PointerType.Mouse;
} }
private void TextBoxEffectiveViewportChanged(object? sender, EffectiveViewportChangedEventArgs e) private void PresenterFocused(object? sender, GotFocusEventArgs e)
{ {
if (ShowHandles) if (_presenter != null && _presenter.SelectionStart != _presenter.SelectionEnd)
{ {
MoveHandlesToSelection(); ShowHandles = true;
EnsureVisible(); EnsureVisible();
} }
} }
private void Caret_Holding(object? sender, HoldingRoutedEventArgs e) private void PresenterTapped(object? sender, TappedEventArgs e)
{
s_isInTouchMode = e.Pointer.Type != PointerType.Mouse;
if (s_isInTouchMode)
MoveHandlesToSelection();
else
{
ShowHandles = false;
_showDisposable?.Dispose();
_showDisposable = null;
}
}
private void Presenter_SizeChanged(object? sender, SizeChangedEventArgs e)
{ {
if (ShowContextMenu()) InvalidateMeasure();
e.Handled = true;
} }
internal bool ShowContextMenu() internal bool ShowFlyout(ContextRequestedEventArgs e)
{ {
if (_textBox != null && _canShowContextMenu) if (_textBox != null)
{ {
if (_textBox.ContextFlyout is PopupFlyoutBase flyout) if (_textBox.ContextFlyout is PopupFlyoutBase flyout)
{ {
@ -349,7 +440,7 @@ namespace Avalonia.Controls.Primitives
if (handle != null) if (handle != null)
{ {
var topLeft = ToTextBox(handle.GetTopLeft()); var topLeft = ToPresenter(handle.GetTopLeft());
flyout.VerticalOffset = topLeft.Y - verticalOffset; flyout.VerticalOffset = topLeft.Y - verticalOffset;
flyout.HorizontalOffset = topLeft.X; flyout.HorizontalOffset = topLeft.X;
flyout.Placement = PlacementMode.TopEdgeAlignedLeft; flyout.Placement = PlacementMode.TopEdgeAlignedLeft;
@ -360,12 +451,10 @@ namespace Avalonia.Controls.Primitives
} }
else else
{ {
_textBox.RaiseEvent(new ContextRequestedEventArgs()); _textBox.RaiseEvent(new ContextRequestedEventArgs(e));
} }
} }
_canShowContextMenu = true;
return false; return false;
} }
@ -377,26 +466,105 @@ namespace Avalonia.Controls.Primitives
EnsureVisible(); EnsureVisible();
} }
private void TextBoxPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) private void TextBox_PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{ {
if (ShowHandles && (e.Property == TextBox.SelectionStartProperty || if (s_isInTouchMode && (e.Property == TextPresenter.SelectionStartProperty ||
e.Property == TextBox.SelectionEndProperty)) e.Property == TextPresenter.SelectionEndProperty))
{ {
MoveHandlesToSelection(); MoveHandlesToSelection();
EnsureVisible(); EnsureVisible();
} }
else if (e.Property == TextPresenter.TextProperty)
{
ShowHandles = false;
if (_presenter?.ContextFlyout is { } flyout && flyout.IsOpen)
flyout.Hide();
}
} }
private void TextBoxKeyDown(object? sender, KeyEventArgs e) private void PresenterKeyDown(object? sender, KeyEventArgs e)
{ {
ShowHandles = false; ShowHandles = false;
s_isInTouchMode = false;
} }
private void TextChanged(object? sender, TextChangingEventArgs e) private class PresenterVisualListener
{ {
ShowHandles = false; private List<Visual> _attachedVisuals = new List<Visual>();
if (_textBox?.ContextFlyout is { } flyout && flyout.IsOpen) private TextPresenter? _presenter;
flyout.Hide(); private object _lock = new object();
public event EventHandler? Invalidated;
public void Attach(TextPresenter presenter)
{
lock (_lock)
{
if (_presenter != null)
throw new InvalidOperationException("Listener is already attached to a TextPresenter");
_presenter = presenter;
presenter.SizeChanged += Presenter_SizeChanged;
presenter.EffectiveViewportChanged += Visual_EffectiveViewportChanged;
void AttachViewportHandler(Visual visual)
{
if (visual is Layoutable layoutable)
{
layoutable.EffectiveViewportChanged += Visual_EffectiveViewportChanged;
}
_attachedVisuals.Add(visual);
}
var visualParent = presenter.VisualParent;
while (visualParent != null)
{
AttachViewportHandler(visualParent);
visualParent = visualParent.VisualParent;
}
}
}
private void Visual_EffectiveViewportChanged(object? sender, Layout.EffectiveViewportChangedEventArgs e)
{
OnInvalidated();
}
private void Presenter_SizeChanged(object? sender, SizeChangedEventArgs e)
{
OnInvalidated();
}
public void Detach()
{
lock (_lock)
{
if (_presenter is { } presenter)
{
presenter.SizeChanged -= Presenter_SizeChanged;
presenter.EffectiveViewportChanged -= Visual_EffectiveViewportChanged;
}
foreach (var visual in _attachedVisuals)
{
if (visual is Layoutable layoutable)
{
layoutable.EffectiveViewportChanged -= Visual_EffectiveViewportChanged;
}
}
_presenter = null;
_attachedVisuals.Clear();
}
}
private void OnInvalidated()
{
Invalidated?.Invoke(this, EventArgs.Empty);
}
} }
} }
} }

16
src/Avalonia.Controls/Primitives/TextSelectionHandle.cs

@ -10,6 +10,8 @@ namespace Avalonia.Controls.Primitives
/// </summary> /// </summary>
public class TextSelectionHandle : Thumb public class TextSelectionHandle : Thumb
{ {
private const int DragDetectionRadius = 2;
internal SelectionHandleType SelectionHandleType { get; set; } internal SelectionHandleType SelectionHandleType { get; set; }
private Point _startPosition; private Point _startPosition;
@ -59,9 +61,13 @@ namespace Avalonia.Controls.Primitives
protected override void OnDragDelta(VectorEventArgs e) protected override void OnDragDelta(VectorEventArgs e)
{ {
base.OnDragDelta(e); base.OnDragDelta(e);
var newDelta = e.Vector;
_delta = e.Vector; if (Math.Abs((newDelta - _delta).Length) > DragDetectionRadius)
UpdateTextSelectionHandlePosition(); {
_delta = e.Vector;
UpdateTextSelectionHandlePosition();
}
} }
protected override void OnDragCompleted(VectorEventArgs e) protected override void OnDragCompleted(VectorEventArgs e)
@ -134,6 +140,9 @@ namespace Avalonia.Controls.Primitives
protected override void OnPointerMoved(PointerEventArgs e) protected override void OnPointerMoved(PointerEventArgs e)
{ {
if (e.Pointer.Captured != this)
return;
VectorEventArgs ev; VectorEventArgs ev;
if (!_lastPoint.HasValue) if (!_lastPoint.HasValue)
@ -163,8 +172,8 @@ namespace Avalonia.Controls.Primitives
protected override void OnPointerPressed(PointerPressedEventArgs e) protected override void OnPointerPressed(PointerPressedEventArgs e)
{ {
e.Handled = true;
PseudoClasses.Add(":pressed"); PseudoClasses.Add(":pressed");
e.Pointer.Capture(this);
} }
protected override void OnPointerReleased(PointerReleasedEventArgs e) protected override void OnPointerReleased(PointerReleasedEventArgs e)
@ -184,6 +193,7 @@ namespace Avalonia.Controls.Primitives
} }
PseudoClasses.Remove(":pressed"); PseudoClasses.Remove(":pressed");
e.Pointer.Capture(null);
} }
} }
} }

399
src/Avalonia.Controls/TextBox.cs

@ -1,24 +1,26 @@
using Avalonia.Input.Platform;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Linq; using System.Linq;
using Avalonia.Reactive; using Avalonia.Automation.Peers;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils; using Avalonia.Controls.Utils;
using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Data;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Utilities; using Avalonia.Media;
using Avalonia.Controls.Metadata;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using Avalonia.Automation.Peers;
using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Metadata;
using Avalonia.Platform;
using Avalonia.Reactive;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.Utilities;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -30,6 +32,11 @@ namespace Avalonia.Controls
[PseudoClasses(":empty")] [PseudoClasses(":empty")]
public class TextBox : TemplatedControl, UndoRedoHelper<TextBox.UndoRedoState>.IUndoRedoHost public class TextBox : TemplatedControl, UndoRedoHelper<TextBox.UndoRedoState>.IUndoRedoHost
{ {
/// <summary>
/// The radius for touch input. Used to determine if selection should change from moving a touch pointer.
/// </summary>
private readonly static int s_touchRadius = (int)((AvaloniaLocator.Current?.GetService<IPlatformSettings>()?.GetTapSize(PointerType.Touch).Height ?? 10) / 2) + 5;
/// <summary> /// <summary>
/// Gets a platform-specific <see cref="KeyGesture"/> for the Cut action /// Gets a platform-specific <see cref="KeyGesture"/> for the Cut action
/// </summary> /// </summary>
@ -368,9 +375,13 @@ namespace Avalonia.Controls
private int _wordSelectionStart = -1; private int _wordSelectionStart = -1;
private int _selectedTextChangesMadeSinceLastUndoSnapshot; private int _selectedTextChangesMadeSinceLastUndoSnapshot;
private bool _hasDoneSnapshotOnce; private bool _hasDoneSnapshotOnce;
private static bool _isHolding;
private int _currentClickCount; private int _currentClickCount;
private bool _isDoubleTapped; private bool _isDoubleTapped;
private bool _isInTouchMode;
private Point _lastPoint;
private bool _isInTouchSelectionMode;
private bool _isInTouchCaretMode;
private bool _hasTouchSelection;
private const int _maxCharsBeforeUndoSnapshot = 7; private const int _maxCharsBeforeUndoSnapshot = 7;
static TextBox() static TextBox()
@ -467,7 +478,7 @@ namespace Avalonia.Controls
SetCurrentValue(SelectionStartProperty, newValue); SetCurrentValue(SelectionStartProperty, newValue);
SetCurrentValue(SelectionEndProperty, newValue); SetCurrentValue(SelectionEndProperty, newValue);
_presenter?.SetCurrentValue(TextPresenter.CaretIndexProperty, newValue); _presenter?.SetCurrentValue(TextPresenter.CaretIndexProperty, newValue);
} }
/// <summary> /// <summary>
@ -953,18 +964,8 @@ namespace Avalonia.Controls
{ {
_presenter = e.NameScope.Get<TextPresenter>("PART_TextPresenter"); _presenter = e.NameScope.Get<TextPresenter>("PART_TextPresenter");
if (_scrollViewer != null)
{
_scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged;
}
_scrollViewer = e.NameScope.Find<ScrollViewer>("PART_ScrollViewer"); _scrollViewer = e.NameScope.Find<ScrollViewer>("PART_ScrollViewer");
if (_scrollViewer != null)
{
_scrollViewer.ScrollChanged += ScrollViewer_ScrollChanged;
}
_imClient.SetPresenter(_presenter, this); _imClient.SetPresenter(_presenter, this);
if (IsFocused) if (IsFocused)
@ -973,11 +974,6 @@ namespace Avalonia.Controls
} }
} }
private void ScrollViewer_ScrollChanged(object? sender, ScrollChangedEventArgs e)
{
_presenter?.TextSelectionHandleCanvas?.MoveHandlesToSelection();
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{ {
base.OnAttachedToVisualTree(e); base.OnAttachedToVisualTree(e);
@ -1092,7 +1088,7 @@ namespace Avalonia.Controls
{ {
base.OnGotFocus(e); base.OnGotFocus(e);
if(_presenter != null) if (_presenter != null)
{ {
_presenter.ShowSelectionHighlight = true; _presenter.ShowSelectionHighlight = true;
} }
@ -1112,6 +1108,9 @@ namespace Avalonia.Controls
_imClient.SetPresenter(_presenter, this); _imClient.SetPresenter(_presenter, this);
_presenter?.ShowCaret(); _presenter?.ShowCaret();
if (SelectionStart != SelectionEnd)
_presenter?.TextSelectionHandleCanvas?.ShowOnFocused();
} }
protected override void OnLostFocus(RoutedEventArgs e) protected override void OnLostFocus(RoutedEventArgs e)
@ -1670,6 +1669,60 @@ namespace Avalonia.Controls
} }
} }
protected override void OnHolding(HoldingRoutedEventArgs e)
{
base.OnHolding(e);
if (_presenter == null || e.HoldingState != HoldingState.Started)
{
_isInTouchSelectionMode = e.HoldingState == HoldingState.Canceled;
_hasTouchSelection = false;
return;
}
var text = Text;
using var _ = _imClient.BeginChange();
if (text != null)
{
var clickInfo = e.PointerEventArgs.GetCurrentPoint(this);
_presenter.MoveCaretToPoint(clickInfo.Position);
var caretIndex = _presenter.CaretIndex;
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
var isInSelection = selectionStart != selectionEnd &&
caretIndex >= selectionStart && caretIndex <= selectionEnd;
if (isInSelection)
{
_presenter.RaiseEvent(new ContextRequestedEventArgs(e.PointerEventArgs));
}
else
{
// We select the current held word, or the whole hidden content
if (IsPasswordBox && !RevealPassword)
{
_wordSelectionStart = -1;
SelectAll();
}
else
{
selectionStart = selectionEnd = caretIndex;
SelectWord(text, caretIndex, selectionStart, selectionEnd);
_presenter?.TextSelectionHandleCanvas?.ShowOnFocused();
}
}
_hasTouchSelection = true;
e.Handled = true;
}
}
protected override void OnPointerPressed(PointerPressedEventArgs e) protected override void OnPointerPressed(PointerPressedEventArgs e)
{ {
if (_presenter == null) if (_presenter == null)
@ -1682,112 +1735,184 @@ namespace Avalonia.Controls
using var _ = _imClient.BeginChange(); using var _ = _imClient.BeginChange();
if (text != null && (e.Pointer.Type == PointerType.Mouse || e.ClickCount >= 2) && clickInfo.Properties.IsLeftButtonPressed && _isInTouchMode = false;
!(clickInfo.Pointer?.Captured is Border)) _isInTouchSelectionMode = false;
_isDoubleTapped = e.ClickCount == 2;
if (text != null && clickInfo.Pointer?.Captured is not Border)
{ {
_currentClickCount = e.ClickCount; if (e.Pointer.Type == PointerType.Mouse && clickInfo.Properties.IsLeftButtonPressed)
var point = e.GetPosition(_presenter); {
_currentClickCount = e.ClickCount;
var point = e.GetPosition(_presenter);
_presenter.MoveCaretToPoint(point); _presenter.MoveCaretToPoint(point);
var caretIndex = _presenter.CaretIndex; var caretIndex = _presenter.CaretIndex;
var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift); var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
var selectionStart = SelectionStart; var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd; var selectionEnd = SelectionEnd;
switch (e.ClickCount) switch (e.ClickCount)
{ {
case 1: case 1:
if (clickToSelect) if (clickToSelect)
{
if (_wordSelectionStart >= 0)
{ {
UpdateWordSelectionRange(caretIndex, ref selectionStart, ref selectionEnd); if (_wordSelectionStart >= 0)
{
UpdateWordSelectionRange(caretIndex, ref selectionStart, ref selectionEnd);
SetCurrentValue(SelectionStartProperty, selectionStart); SetCurrentValue(SelectionStartProperty, selectionStart);
SetCurrentValue(SelectionEndProperty, selectionEnd); SetCurrentValue(SelectionEndProperty, selectionEnd);
}
else
{
SetCurrentValue(SelectionEndProperty, caretIndex);
}
} }
else else
{ {
SetCurrentValue(SelectionStartProperty, caretIndex);
SetCurrentValue(SelectionEndProperty, caretIndex); SetCurrentValue(SelectionEndProperty, caretIndex);
_wordSelectionStart = -1;
} }
}
else
{
SetCurrentValue(SelectionStartProperty, caretIndex);
SetCurrentValue(SelectionEndProperty, caretIndex);
_wordSelectionStart = -1;
}
break; break;
case 2: case 2:
if (IsPasswordBox && !RevealPassword) SelectWord(text, caretIndex, selectionStart, selectionEnd);
{
// double-clicking in a cloaked single-line password box selects all text
// see https://github.com/AvaloniaUI/Avalonia/issues/14956
goto case 3;
}
if (!StringUtils.IsStartOfWord(text, caretIndex)) break;
{ case 3:
selectionStart = StringUtils.PreviousWord(text, caretIndex); _wordSelectionStart = -1;
}
if (!StringUtils.IsEndOfWord(text, caretIndex)) SelectAll();
{ break;
selectionEnd = StringUtils.NextWord(text, caretIndex); }
} }
else if (e.Pointer.Type != PointerType.Mouse)
{
_isInTouchMode = true;
_lastPoint = e.GetCurrentPoint(_presenter).Position;
if (selectionStart != selectionEnd) if (_isDoubleTapped)
{
var oldCaret = _presenter.CaretIndex;
_presenter.MoveCaretToPoint(_lastPoint);
var caretIndex = _presenter.CaretIndex;
if(Math.Abs(oldCaret - caretIndex) > 3)
{ {
_wordSelectionStart = selectionStart; return;
} }
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
SetCurrentValue(SelectionStartProperty, selectionStart); SelectWord(text, caretIndex, selectionStart, selectionEnd);
SetCurrentValue(SelectionEndProperty, selectionEnd); }
break;
case 3:
_wordSelectionStart = -1;
SelectAll();
break;
} }
} }
_isDoubleTapped = e.ClickCount == 2;
e.Pointer.Capture(_presenter); e.Pointer.Capture(_presenter);
e.Handled = true; e.Handled = true;
} }
private void SelectWord(string text, int caretIndex, int selectionStart, int selectionEnd)
{
if (IsPasswordBox && !RevealPassword)
{
// double-clicking in a cloaked single-line password box selects all text
// see https://github.com/AvaloniaUI/Avalonia/issues/14956
_wordSelectionStart = -1;
SelectAll();
}
if (!StringUtils.IsStartOfWord(text, caretIndex))
{
selectionStart = StringUtils.PreviousWord(text, caretIndex);
}
if (!StringUtils.IsEndOfWord(text, caretIndex))
{
selectionEnd = StringUtils.NextWord(text, caretIndex);
}
if (selectionStart != selectionEnd)
{
_wordSelectionStart = selectionStart;
}
SetCurrentValue(SelectionStartProperty, selectionStart);
SetCurrentValue(SelectionEndProperty, selectionEnd);
}
protected override void OnPointerMoved(PointerEventArgs e) protected override void OnPointerMoved(PointerEventArgs e)
{ {
if (_presenter == null || _isHolding) if (_presenter == null)
{ {
return; return;
} }
using var _ = _imClient.BeginChange(); using var _ = _imClient.BeginChange();
var point = e.GetPosition(_presenter);
// selection should not change during pointer move if the user right clicks if (e.Pointer.Type == PointerType.Mouse)
if (e.Pointer.Captured == _presenter && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{ {
var point = e.GetPosition(_presenter); // selection should not change during pointer move if the user right clicks
if (e.Pointer.Captured == _presenter && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
point = new Point(
MathUtilities.Clamp(point.X, 0, Math.Max(_presenter.Bounds.Width - 1, 0)),
MathUtilities.Clamp(point.Y, 0, Math.Max(_presenter.Bounds.Height - 1, 0)));
point = new Point( var previousIndex = _presenter.CaretIndex;
MathUtilities.Clamp(point.X, 0, Math.Max(_presenter.Bounds.Width - 1, 0)),
MathUtilities.Clamp(point.Y, 0, Math.Max(_presenter.Bounds.Height - 1, 0)));
var previousIndex = _presenter.CaretIndex; _presenter.MoveCaretToPoint(point);
_presenter.MoveCaretToPoint(point); var caretIndex = _presenter.CaretIndex;
var caretIndex = _presenter.CaretIndex; if (Math.Abs(caretIndex - previousIndex) == 1)
e.PreventGestureRecognition();
if (e.Pointer.Type == PointerType.Mouse || _isDoubleTapped)
{
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
if (Math.Abs(caretIndex - previousIndex) == 1) if (_wordSelectionStart >= 0)
e.PreventGestureRecognition(); {
UpdateWordSelectionRange(caretIndex, ref selectionStart, ref selectionEnd);
if (e.Pointer.Type == PointerType.Mouse || _isDoubleTapped) SetCurrentValue(SelectionStartProperty, selectionStart);
SetCurrentValue(SelectionEndProperty, selectionEnd);
}
else
{
SetCurrentValue(SelectionEndProperty, caretIndex);
}
}
else
{
SetCurrentValue(SelectionStartProperty, caretIndex);
SetCurrentValue(SelectionEndProperty, caretIndex);
}
}
}
else if (_isInTouchMode)
{
if (_isInTouchSelectionMode)
{ {
point = new Point(
MathUtilities.Clamp(point.X, 0, Math.Max(_presenter.Bounds.Width - 1, 0)),
MathUtilities.Clamp(point.Y, 0, Math.Max(_presenter.Bounds.Height - 1, 0)));
var previousIndex = _presenter.CaretIndex;
_presenter.MoveCaretToPoint(point);
var caretIndex = _presenter.CaretIndex;
if (Math.Abs(caretIndex - previousIndex) == 1)
e.PreventGestureRecognition();
var selectionStart = SelectionStart; var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd; var selectionEnd = SelectionEnd;
@ -1805,8 +1930,28 @@ namespace Avalonia.Controls
} }
else else
{ {
SetCurrentValue(SelectionStartProperty, caretIndex); if (!_isInTouchCaretMode)
SetCurrentValue(SelectionEndProperty, caretIndex); {
var touchRect = new Rect(_lastPoint.X, _lastPoint.Y, 0, 0).Inflate(s_touchRadius);
var isInRect = touchRect.X < point.X &&
touchRect.Y < point.Y &&
touchRect.Right > point.X &&
touchRect.Bottom > point.Y;
if (!isInRect)
{
_isInTouchCaretMode = true;
}
}
if (_isInTouchCaretMode)
{
e.PreventGestureRecognition();
_presenter.MoveCaretToPoint(point);
var caretIndex = _presenter.CaretIndex;
SetCurrentValue(SelectionStartProperty, caretIndex);
SetCurrentValue(SelectionEndProperty, caretIndex);
}
} }
} }
} }
@ -1851,52 +1996,22 @@ namespace Avalonia.Controls
using var _ = _imClient.BeginChange(); using var _ = _imClient.BeginChange();
if (e.Pointer.Type != PointerType.Mouse && !_isDoubleTapped) if (e.Pointer.Type != PointerType.Mouse && !_isInTouchSelectionMode)
{ {
var text = Text; if (!_isDoubleTapped && !_hasTouchSelection)
var clickInfo = e.GetCurrentPoint(this);
if (text != null && !(clickInfo.Pointer?.Captured is Border))
{ {
var point = e.GetPosition(_presenter); var point = e.GetPosition(_presenter);
_presenter.MoveCaretToPoint(point); _presenter.MoveCaretToPoint(point);
var caretIndex = _presenter.CaretIndex; var caretIndex = _presenter.CaretIndex;
var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift); SetCurrentValue(CaretIndexProperty, caretIndex);
var selectionStart = SelectionStart; SetCurrentValue(SelectionEndProperty, caretIndex);
var selectionEnd = SelectionEnd; SetCurrentValue(SelectionStartProperty, caretIndex);
if (clickToSelect)
{
if (_wordSelectionStart >= 0)
{
UpdateWordSelectionRange(caretIndex, ref selectionStart, ref selectionEnd);
SetCurrentValue(SelectionStartProperty, selectionStart);
SetCurrentValue(SelectionEndProperty, selectionEnd);
}
else
{
SetCurrentValue(SelectionEndProperty, caretIndex);
}
}
else
{
SetCurrentValue(SelectionStartProperty, caretIndex);
SetCurrentValue(SelectionEndProperty, caretIndex);
_wordSelectionStart = -1;
}
_presenter.TextSelectionHandleCanvas?.MoveHandlesToSelection();
} }
} }
// Don't update selection if the pointer was held if (e.InitialPressMouseButton == MouseButton.Right)
if (_isHolding)
{
_isHolding = false;
}
else if (e.InitialPressMouseButton == MouseButton.Right)
{ {
var point = e.GetPosition(_presenter); var point = e.GetPosition(_presenter);
@ -1917,26 +2032,10 @@ namespace Avalonia.Controls
SetCurrentValue(SelectionStartProperty, caretIndex); SetCurrentValue(SelectionStartProperty, caretIndex);
} }
} }
else if (e.Pointer.Type == PointerType.Touch) _isInTouchMode = false;
{ _isInTouchSelectionMode = false;
if (_currentClickCount == 1) _isInTouchCaretMode = false;
{ _hasTouchSelection = false;
var point = e.GetPosition(_presenter);
_presenter.MoveCaretToPoint(point);
var caretIndex = _presenter.CaretIndex;
SetCurrentValue(SelectionStartProperty, caretIndex);
SetCurrentValue(SelectionEndProperty, caretIndex);
}
_presenter.TextSelectionHandleCanvas?.Show();
if (SelectionStart != SelectionEnd)
{
_presenter.TextSelectionHandleCanvas?.ShowContextMenu();
}
}
e.Pointer.Capture(null); e.Pointer.Capture(null);
} }

3
src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

@ -133,6 +133,7 @@ namespace Avalonia.Controls
if (oldPresenter != null) if (oldPresenter != null)
{ {
oldPresenter.CurrentImClient = null;
oldPresenter.ClearValue(TextPresenter.PreeditTextProperty); oldPresenter.ClearValue(TextPresenter.PreeditTextProperty);
oldPresenter.CaretBoundsChanged -= (s, e) => RaiseCursorRectangleChanged(); oldPresenter.CaretBoundsChanged -= (s, e) => RaiseCursorRectangleChanged();
@ -142,6 +143,8 @@ namespace Avalonia.Controls
if (_presenter != null) if (_presenter != null)
{ {
_presenter.CurrentImClient = this;
_presenter.CaretBoundsChanged += (s, e) => RaiseCursorRectangleChanged(); _presenter.CaretBoundsChanged += (s, e) => RaiseCursorRectangleChanged();
} }

Loading…
Cancel
Save