diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index 20b7d33c60..9a0e4e7f8d 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -173,7 +173,7 @@
-
+
diff --git a/samples/ControlCatalog/Pages/TextBoxPage.xaml b/samples/ControlCatalog/Pages/TextBoxPage.xaml
index 346e561dfc..1b0b936965 100644
--- a/samples/ControlCatalog/Pages/TextBoxPage.xaml
+++ b/samples/ControlCatalog/Pages/TextBoxPage.xaml
@@ -8,97 +8,10 @@
-
-
-
- Custom context flyout
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
+
diff --git a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
index a00a7be6c2..af060c04b3 100644
--- a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
+++ b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
@@ -133,8 +133,6 @@ namespace Avalonia.Input.GestureRecognizers
_trackedRootPoint = new Point(
_trackedRootPoint.X - (_trackedRootPoint.X >= rootPoint.X ? 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);
_lastMoveTimestamp = e.Timestamp;
- Target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector));
+ var scrollEventArgs = new ScrollGestureEventArgs(_gestureId, vector);
+ Target!.RaiseEvent(scrollEventArgs);
_trackedRootPoint = rootPoint;
- e.Handled = true;
+ e.Handled = scrollEventArgs.Handled;
+ if(e.Handled)
+ {
+ Capture(e.Pointer);
+ }
}
}
}
diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs
index 05c74b39e5..1247476c0b 100644
--- a/src/Avalonia.Controls/Presenters/TextPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs
@@ -1,14 +1,14 @@
using System;
using System.Collections.Generic;
-using System.Data;
using Avalonia.Controls.Documents;
using Avalonia.Controls.Primitives;
-using Avalonia.Interactivity;
+using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
+using Avalonia.Platform;
using Avalonia.Threading;
using Avalonia.Utilities;
using Avalonia.VisualTree;
@@ -334,6 +334,7 @@ namespace Avalonia.Controls.Presenters
protected override bool BypassFlowDirectionPolicies => true;
internal TextSelectionHandleCanvas? TextSelectionHandleCanvas { get; set; }
+ internal TextBoxTextInputMethodClient? CurrentImClient { get; set; }
///
/// Creates the used to render the text.
@@ -1026,15 +1027,7 @@ namespace Avalonia.Controls.Presenters
OnPreeditChanged(PreeditText, PreeditTextCursorPosition);
}
- if(change.Property == TextProperty)
- {
- if (!string.IsNullOrEmpty(PreeditText))
- {
- SetCurrentValue(PreeditTextProperty, null);
- }
- }
-
- if(change.Property == CaretIndexProperty)
+ if(change.Property == TextProperty || change.Property == CaretIndexProperty)
{
if (!string.IsNullOrEmpty(PreeditText))
{
@@ -1067,7 +1060,7 @@ namespace Avalonia.Controls.Presenters
case nameof(SelectionStart):
case nameof(SelectionEnd):
case nameof(SelectionForegroundBrush):
- case nameof(ShowSelectionHighlightProperty):
+ case nameof(ShowSelectionHighlight):
case nameof(PasswordChar):
case nameof(RevealPassword):
diff --git a/src/Avalonia.Controls/Primitives/TextSelectionCanvas.cs b/src/Avalonia.Controls/Primitives/TextSelectionCanvas.cs
index 049188008a..89897e0ee4 100644
--- a/src/Avalonia.Controls/Primitives/TextSelectionCanvas.cs
+++ b/src/Avalonia.Controls/Primitives/TextSelectionCanvas.cs
@@ -1,9 +1,10 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using Avalonia.Controls.Presenters;
using Avalonia.Input;
+using Avalonia.Interactivity;
using Avalonia.Layout;
+using Avalonia.Threading;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives
@@ -11,6 +12,7 @@ namespace Avalonia.Controls.Primitives
internal class TextSelectionHandleCanvas : Canvas
{
private const int ContextMenuPadding = 16;
+ private static bool s_isInTouchMode;
private readonly TextSelectionHandle _caretHandle;
private readonly TextSelectionHandle _startHandle;
@@ -18,7 +20,8 @@ namespace Avalonia.Controls.Primitives
private TextPresenter? _presenter;
private TextBox? _textBox;
private bool _showHandle;
- private bool _canShowContextMenu = true;
+ private IDisposable? _showDisposable;
+ private PresenterVisualListener _layoutListener;
internal bool ShowHandles
{
@@ -66,30 +69,46 @@ namespace Avalonia.Controls.Primitives
_caretHandle.SetTopLeft(default);
_endHandle.SetTopLeft(default);
- _startHandle.PointerReleased += Handle_PointerReleased;
- _caretHandle.PointerReleased += Handle_PointerReleased;
- _endHandle.PointerReleased += Handle_PointerReleased;
-
- _startHandle.Holding += Caret_Holding;
- _caretHandle.Holding += Caret_Holding;
- _endHandle.Holding += Caret_Holding;
+ _startHandle.ContextCanceled += Caret_ContextCanceled;
+ _caretHandle.ContextCanceled += Caret_ContextCanceled;
+ _endHandle.ContextCanceled += Caret_ContextCanceled;
+ _startHandle.ContextRequested += Caret_ContextRequested;
+ _caretHandle.ContextRequested += Caret_ContextRequested;
+ _endHandle.ContextRequested += Caret_ContextRequested;
IsVisible = ShowHandles;
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)
{
- if (_textBox?.ContextFlyout is { } flyout)
- {
- flyout.Hide();
- }
+ CloseFlyout();
+ }
+
+ private void CloseFlyout()
+ {
+ _presenter?.RaiseEvent(new Interactivity.RoutedEventArgs(InputElement.ContextCanceledEvent));
}
private void EndHandle_DragDelta(object? sender, VectorEventArgs e)
@@ -106,41 +125,99 @@ namespace Avalonia.Controls.Primitives
private void CaretHandle_DragDelta(object? sender, VectorEventArgs e)
{
- _canShowContextMenu = false;
if (_presenter != null && _textBox != null)
{
var point = ToPresenter(_caretHandle.IndicatorPosition);
+ using var _ = BeginChange();
_presenter.MoveCaretToPoint(point);
- _textBox.SelectionStart = _textBox.SelectionEnd = _presenter.CaretIndex;
- var points = _presenter.GetCaretPoints();
+ var caretIndex = _presenter.CaretIndex;
+ _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)
{
MoveHandlesToSelection();
-
- ShowContextMenu();
}
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 topLeft = t.TranslatePoint(default, r) ?? default;
- bounds = bounds.WithX(topLeft.X).WithY(topLeft.Y);
-
- var hasSelection = _textBox.SelectionStart != _textBox.SelectionEnd;
-
- _startHandle.IsVisible = bounds.Contains(new Point(GetLeft(_startHandle), GetTop(_startHandle))) &&
- ShowHandles && hasSelection;
- _endHandle.IsVisible = bounds.Contains(new Point(GetLeft(_endHandle), GetTop(_endHandle))) &&
- ShowHandles && hasSelection;
- _caretHandle.IsVisible = bounds.Contains(new Point(GetLeft(_caretHandle), GetTop(_caretHandle))) &&
- ShowHandles && !hasSelection;
+ var bounds = presenter.GetTransformedBounds();
+
+ if (bounds == null)
+ return;
+
+ var hasSelection = _presenter.SelectionStart != _presenter.SelectionEnd;
+
+ _startHandle.IsVisible = ShowHandles && hasSelection &&
+ !IsOccluded(new Point(GetLeft(_startHandle), GetTop(_startHandle)));
+ _endHandle.IsVisible = ShowHandles && hasSelection &&
+ !IsOccluded(new Point(GetLeft(_endHandle), GetTop(_endHandle)));
+ _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 (_textBox.ContextFlyout is { } flyout)
- {
- flyout.Hide();
- }
+ CloseFlyout();
var point = ToPresenter(handle.IndicatorPosition);
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 otherHandle = handle == _startHandle ? _endHandle : _startHandle;
+ using var _ = BeginChange();
if (handle.SelectionHandleType == SelectionHandleType.Start)
{
if (position >= _textBox.SelectionEnd)
position = _textBox.SelectionEnd - 1;
- _textBox.SelectionStart = position;
+ _textBox.SetCurrentValue(TextBox.SelectionStartProperty, position);
}
else
{
if (position <= _textBox.SelectionStart)
position = _textBox.SelectionStart + 1;
- _textBox.SelectionEnd = position;
+ _textBox.SetCurrentValue(TextBox.SelectionEndProperty, position);
}
var selectionStart = _textBox.SelectionStart;
@@ -182,7 +257,7 @@ namespace Avalonia.Controls.Primitives
if (rects.Count > 0)
{
var first = rects[0];
- var last = rects[rects.Count -1];
+ var last = rects[rects.Count - 1];
if (handle.SelectionHandleType == SelectionHandleType.Start)
handle?.SetTopLeft(ToLayer(first.BottomLeft));
@@ -196,9 +271,9 @@ namespace Avalonia.Controls.Primitives
}
_presenter?.MoveCaretToTextPosition(position);
- }
- EnsureVisible();
+ EnsureVisible();
+ }
}
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;
}
- private Point ToTextBox(Point point)
- {
- return (_textBox is { } p) ? (p.VisualRoot as Visual)?.TranslatePoint(point, p) ?? point : point;
- }
-
public void MoveHandlesToSelection()
{
if (_presenter == null
- || _textBox == null
|| _startHandle.IsDragging
- || _endHandle.IsDragging
- || _textBox.ContextFlyout?.IsOpen == true
- || _textBox.ContextMenu?.IsOpen == true)
+ || _caretHandle.IsDragging
+ || _endHandle.IsDragging)
{
return;
}
- var hasSelection = _textBox.SelectionStart != _textBox.SelectionEnd;
+ var selectionStart = _presenter.SelectionStart;
+ var selectionEnd = _presenter.SelectionEnd;
+ var hasSelection = selectionStart != selectionEnd;
var points = _presenter.GetCaretPoints();
@@ -236,8 +306,6 @@ namespace Avalonia.Controls.Primitives
if (hasSelection)
{
- var selectionStart = _textBox.SelectionStart;
- var selectionEnd = _textBox.SelectionEnd;
var start = Math.Min(selectionStart, selectionEnd);
var length = Math.Max(selectionStart, selectionEnd) - start;
@@ -265,6 +333,10 @@ namespace Avalonia.Controls.Primitives
}
}
}
+
+ ShowHandles = true;
+
+ EnsureVisible();
}
internal void SetPresenter(TextPresenter? textPresenter)
@@ -272,58 +344,77 @@ namespace Avalonia.Controls.Primitives
if (_presenter == textPresenter)
return;
- if (_textBox != null)
+ if (_presenter != null)
{
- _textBox.RemoveHandler(TextBox.TextChangingEvent, TextChanged);
- _textBox.RemoveHandler(KeyDownEvent, TextBoxKeyDown);
-
- _textBox.PropertyChanged -= TextBoxPropertyChanged;
- _textBox.EffectiveViewportChanged -= TextBoxEffectiveViewportChanged;
- _textBox.SizeChanged -= TextBox_SizeChanged;
+ _layoutListener.Detach();
+ _presenter.RemoveHandler(KeyDownEvent, PresenterKeyDown);
+ _presenter.RemoveHandler(TappedEvent, PresenterTapped);
+ _presenter.RemoveHandler(PointerPressedEvent, PresenterPressed);
+ _presenter.RemoveHandler(GotFocusEvent, PresenterFocused);
+ if (_textBox != null)
+ {
+ _textBox.PropertyChanged -= TextBox_PropertyChanged;
+ }
_textBox = null;
+
+ _presenter = null;
}
_presenter = textPresenter;
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();
if (_textBox != null)
{
- _textBox.AddHandler(TextBox.TextChangingEvent, TextChanged, handledEventsToo: true);
- _textBox.AddHandler(KeyDownEvent, TextBoxKeyDown, handledEventsToo: true);
-
- _textBox.PropertyChanged += TextBoxPropertyChanged;
- _textBox.EffectiveViewportChanged += TextBoxEffectiveViewportChanged;
- _textBox.SizeChanged += TextBox_SizeChanged;
+ _textBox.PropertyChanged += TextBox_PropertyChanged;
}
}
}
- 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();
}
}
- 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())
- e.Handled = true;
+ InvalidateMeasure();
}
- internal bool ShowContextMenu()
+ internal bool ShowFlyout(ContextRequestedEventArgs e)
{
- if (_textBox != null && _canShowContextMenu)
+ if (_textBox != null)
{
if (_textBox.ContextFlyout is PopupFlyoutBase flyout)
{
@@ -349,7 +440,7 @@ namespace Avalonia.Controls.Primitives
if (handle != null)
{
- var topLeft = ToTextBox(handle.GetTopLeft());
+ var topLeft = ToPresenter(handle.GetTopLeft());
flyout.VerticalOffset = topLeft.Y - verticalOffset;
flyout.HorizontalOffset = topLeft.X;
flyout.Placement = PlacementMode.TopEdgeAlignedLeft;
@@ -360,12 +451,10 @@ namespace Avalonia.Controls.Primitives
}
else
{
- _textBox.RaiseEvent(new ContextRequestedEventArgs());
+ _textBox.RaiseEvent(new ContextRequestedEventArgs(e));
}
}
- _canShowContextMenu = true;
-
return false;
}
@@ -377,26 +466,105 @@ namespace Avalonia.Controls.Primitives
EnsureVisible();
}
- private void TextBoxPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
+ private void TextBox_PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
- if (ShowHandles && (e.Property == TextBox.SelectionStartProperty ||
- e.Property == TextBox.SelectionEndProperty))
+ if (s_isInTouchMode && (e.Property == TextPresenter.SelectionStartProperty ||
+ e.Property == TextPresenter.SelectionEndProperty))
{
MoveHandlesToSelection();
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;
+ s_isInTouchMode = false;
}
- private void TextChanged(object? sender, TextChangingEventArgs e)
+ private class PresenterVisualListener
{
- ShowHandles = false;
- if (_textBox?.ContextFlyout is { } flyout && flyout.IsOpen)
- flyout.Hide();
+ private List _attachedVisuals = new List();
+ private TextPresenter? _presenter;
+ 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);
+ }
}
}
}
diff --git a/src/Avalonia.Controls/Primitives/TextSelectionHandle.cs b/src/Avalonia.Controls/Primitives/TextSelectionHandle.cs
index 6518b96bce..a36bb1c8f8 100644
--- a/src/Avalonia.Controls/Primitives/TextSelectionHandle.cs
+++ b/src/Avalonia.Controls/Primitives/TextSelectionHandle.cs
@@ -10,6 +10,8 @@ namespace Avalonia.Controls.Primitives
///
public class TextSelectionHandle : Thumb
{
+ private const int DragDetectionRadius = 2;
+
internal SelectionHandleType SelectionHandleType { get; set; }
private Point _startPosition;
@@ -59,9 +61,13 @@ namespace Avalonia.Controls.Primitives
protected override void OnDragDelta(VectorEventArgs e)
{
base.OnDragDelta(e);
+ var newDelta = e.Vector;
- _delta = e.Vector;
- UpdateTextSelectionHandlePosition();
+ if (Math.Abs((newDelta - _delta).Length) > DragDetectionRadius)
+ {
+ _delta = e.Vector;
+ UpdateTextSelectionHandlePosition();
+ }
}
protected override void OnDragCompleted(VectorEventArgs e)
@@ -134,6 +140,9 @@ namespace Avalonia.Controls.Primitives
protected override void OnPointerMoved(PointerEventArgs e)
{
+ if (e.Pointer.Captured != this)
+ return;
+
VectorEventArgs ev;
if (!_lastPoint.HasValue)
@@ -163,8 +172,8 @@ namespace Avalonia.Controls.Primitives
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
- e.Handled = true;
PseudoClasses.Add(":pressed");
+ e.Pointer.Capture(this);
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
@@ -184,6 +193,7 @@ namespace Avalonia.Controls.Primitives
}
PseudoClasses.Remove(":pressed");
+ e.Pointer.Capture(null);
}
}
}
diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs
index 447a6a41fc..8bcd3b2c97 100644
--- a/src/Avalonia.Controls/TextBox.cs
+++ b/src/Avalonia.Controls/TextBox.cs
@@ -1,24 +1,26 @@
-using Avalonia.Input.Platform;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
+using System.Drawing;
using System.Linq;
-using Avalonia.Reactive;
+using Avalonia.Automation.Peers;
+using Avalonia.Controls.Metadata;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils;
+using Avalonia.Data;
using Avalonia.Input;
+using Avalonia.Input.Platform;
using Avalonia.Interactivity;
-using Avalonia.Media;
-using Avalonia.Metadata;
-using Avalonia.Data;
using Avalonia.Layout;
-using Avalonia.Utilities;
-using Avalonia.Controls.Metadata;
+using Avalonia.Media;
using Avalonia.Media.TextFormatting;
-using Avalonia.Automation.Peers;
using Avalonia.Media.TextFormatting.Unicode;
+using Avalonia.Metadata;
+using Avalonia.Platform;
+using Avalonia.Reactive;
using Avalonia.Threading;
+using Avalonia.Utilities;
namespace Avalonia.Controls
{
@@ -30,6 +32,11 @@ namespace Avalonia.Controls
[PseudoClasses(":empty")]
public class TextBox : TemplatedControl, UndoRedoHelper.IUndoRedoHost
{
+ ///
+ /// The radius for touch input. Used to determine if selection should change from moving a touch pointer.
+ ///
+ private readonly static int s_touchRadius = (int)((AvaloniaLocator.Current?.GetService()?.GetTapSize(PointerType.Touch).Height ?? 10) / 2) + 5;
+
///
/// Gets a platform-specific for the Cut action
///
@@ -368,9 +375,13 @@ namespace Avalonia.Controls
private int _wordSelectionStart = -1;
private int _selectedTextChangesMadeSinceLastUndoSnapshot;
private bool _hasDoneSnapshotOnce;
- private static bool _isHolding;
private int _currentClickCount;
private bool _isDoubleTapped;
+ private bool _isInTouchMode;
+ private Point _lastPoint;
+ private bool _isInTouchSelectionMode;
+ private bool _isInTouchCaretMode;
+ private bool _hasTouchSelection;
private const int _maxCharsBeforeUndoSnapshot = 7;
static TextBox()
@@ -467,7 +478,7 @@ namespace Avalonia.Controls
SetCurrentValue(SelectionStartProperty, newValue);
SetCurrentValue(SelectionEndProperty, newValue);
- _presenter?.SetCurrentValue(TextPresenter.CaretIndexProperty, newValue);
+ _presenter?.SetCurrentValue(TextPresenter.CaretIndexProperty, newValue);
}
///
@@ -953,18 +964,8 @@ namespace Avalonia.Controls
{
_presenter = e.NameScope.Get("PART_TextPresenter");
- if (_scrollViewer != null)
- {
- _scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged;
- }
-
_scrollViewer = e.NameScope.Find("PART_ScrollViewer");
- if (_scrollViewer != null)
- {
- _scrollViewer.ScrollChanged += ScrollViewer_ScrollChanged;
- }
-
_imClient.SetPresenter(_presenter, this);
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)
{
base.OnAttachedToVisualTree(e);
@@ -1092,7 +1088,7 @@ namespace Avalonia.Controls
{
base.OnGotFocus(e);
- if(_presenter != null)
+ if (_presenter != null)
{
_presenter.ShowSelectionHighlight = true;
}
@@ -1112,6 +1108,9 @@ namespace Avalonia.Controls
_imClient.SetPresenter(_presenter, this);
_presenter?.ShowCaret();
+
+ if (SelectionStart != SelectionEnd)
+ _presenter?.TextSelectionHandleCanvas?.ShowOnFocused();
}
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)
{
if (_presenter == null)
@@ -1682,112 +1735,184 @@ namespace Avalonia.Controls
using var _ = _imClient.BeginChange();
- if (text != null && (e.Pointer.Type == PointerType.Mouse || e.ClickCount >= 2) && clickInfo.Properties.IsLeftButtonPressed &&
- !(clickInfo.Pointer?.Captured is Border))
+ _isInTouchMode = false;
+ _isInTouchSelectionMode = false;
+ _isDoubleTapped = e.ClickCount == 2;
+ if (text != null && clickInfo.Pointer?.Captured is not Border)
{
- _currentClickCount = e.ClickCount;
- var point = e.GetPosition(_presenter);
+ if (e.Pointer.Type == PointerType.Mouse && clickInfo.Properties.IsLeftButtonPressed)
+ {
+ _currentClickCount = e.ClickCount;
+ var point = e.GetPosition(_presenter);
- _presenter.MoveCaretToPoint(point);
+ _presenter.MoveCaretToPoint(point);
- var caretIndex = _presenter.CaretIndex;
- var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
- var selectionStart = SelectionStart;
- var selectionEnd = SelectionEnd;
+ var caretIndex = _presenter.CaretIndex;
+ var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
+ var selectionStart = SelectionStart;
+ var selectionEnd = SelectionEnd;
- switch (e.ClickCount)
- {
- case 1:
- if (clickToSelect)
- {
- if (_wordSelectionStart >= 0)
+ switch (e.ClickCount)
+ {
+ case 1:
+ if (clickToSelect)
{
- UpdateWordSelectionRange(caretIndex, ref selectionStart, ref selectionEnd);
+ if (_wordSelectionStart >= 0)
+ {
+ UpdateWordSelectionRange(caretIndex, ref selectionStart, ref selectionEnd);
- SetCurrentValue(SelectionStartProperty, selectionStart);
- SetCurrentValue(SelectionEndProperty, selectionEnd);
+ SetCurrentValue(SelectionStartProperty, selectionStart);
+ SetCurrentValue(SelectionEndProperty, selectionEnd);
+ }
+ else
+ {
+ SetCurrentValue(SelectionEndProperty, caretIndex);
+ }
}
else
{
+ SetCurrentValue(SelectionStartProperty, caretIndex);
SetCurrentValue(SelectionEndProperty, caretIndex);
+ _wordSelectionStart = -1;
}
- }
- else
- {
- SetCurrentValue(SelectionStartProperty, caretIndex);
- SetCurrentValue(SelectionEndProperty, caretIndex);
- _wordSelectionStart = -1;
- }
- break;
- case 2:
- if (IsPasswordBox && !RevealPassword)
- {
- // double-clicking in a cloaked single-line password box selects all text
- // see https://github.com/AvaloniaUI/Avalonia/issues/14956
- goto case 3;
- }
+ break;
+ case 2:
+ SelectWord(text, caretIndex, selectionStart, selectionEnd);
- if (!StringUtils.IsStartOfWord(text, caretIndex))
- {
- selectionStart = StringUtils.PreviousWord(text, caretIndex);
- }
+ break;
+ case 3:
+ _wordSelectionStart = -1;
- if (!StringUtils.IsEndOfWord(text, caretIndex))
- {
- selectionEnd = StringUtils.NextWord(text, caretIndex);
- }
+ SelectAll();
+ break;
+ }
+ }
+ 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);
- SetCurrentValue(SelectionEndProperty, selectionEnd);
-
- break;
- case 3:
- _wordSelectionStart = -1;
-
- SelectAll();
- break;
+ SelectWord(text, caretIndex, selectionStart, selectionEnd);
+ }
}
}
- _isDoubleTapped = e.ClickCount == 2;
e.Pointer.Capture(_presenter);
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)
{
- if (_presenter == null || _isHolding)
+ if (_presenter == null)
{
return;
}
using var _ = _imClient.BeginChange();
+ 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)
+ if (e.Pointer.Type == PointerType.Mouse)
{
- 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(
- 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;
- 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)
- e.PreventGestureRecognition();
+ if (_wordSelectionStart >= 0)
+ {
+ 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 selectionEnd = SelectionEnd;
@@ -1805,8 +1930,28 @@ namespace Avalonia.Controls
}
else
{
- SetCurrentValue(SelectionStartProperty, caretIndex);
- SetCurrentValue(SelectionEndProperty, caretIndex);
+ if (!_isInTouchCaretMode)
+ {
+
+ 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();
- if (e.Pointer.Type != PointerType.Mouse && !_isDoubleTapped)
+ if (e.Pointer.Type != PointerType.Mouse && !_isInTouchSelectionMode)
{
- var text = Text;
- var clickInfo = e.GetCurrentPoint(this);
- if (text != null && !(clickInfo.Pointer?.Captured is Border))
+ if (!_isDoubleTapped && !_hasTouchSelection)
{
var point = e.GetPosition(_presenter);
_presenter.MoveCaretToPoint(point);
var caretIndex = _presenter.CaretIndex;
- var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
- var selectionStart = SelectionStart;
- var selectionEnd = SelectionEnd;
-
- 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();
+ SetCurrentValue(CaretIndexProperty, caretIndex);
+ SetCurrentValue(SelectionEndProperty, caretIndex);
+ SetCurrentValue(SelectionStartProperty, caretIndex);
}
}
- // Don't update selection if the pointer was held
- if (_isHolding)
- {
- _isHolding = false;
- }
- else if (e.InitialPressMouseButton == MouseButton.Right)
+ if (e.InitialPressMouseButton == MouseButton.Right)
{
var point = e.GetPosition(_presenter);
@@ -1917,26 +2032,10 @@ namespace Avalonia.Controls
SetCurrentValue(SelectionStartProperty, caretIndex);
}
}
- else if (e.Pointer.Type == PointerType.Touch)
- {
- if (_currentClickCount == 1)
- {
- 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();
- }
- }
+ _isInTouchMode = false;
+ _isInTouchSelectionMode = false;
+ _isInTouchCaretMode = false;
+ _hasTouchSelection = false;
e.Pointer.Capture(null);
}
diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
index 12e8e97640..84efc9c174 100644
--- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
+++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
@@ -133,6 +133,7 @@ namespace Avalonia.Controls
if (oldPresenter != null)
{
+ oldPresenter.CurrentImClient = null;
oldPresenter.ClearValue(TextPresenter.PreeditTextProperty);
oldPresenter.CaretBoundsChanged -= (s, e) => RaiseCursorRectangleChanged();
@@ -142,6 +143,8 @@ namespace Avalonia.Controls
if (_presenter != null)
{
+
+ _presenter.CurrentImClient = this;
_presenter.CaretBoundsChanged += (s, e) => RaiseCursorRectangleChanged();
}