using Avalonia.Input.Platform; using System; using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; using Avalonia.Input; 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.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Automation.Peers; namespace Avalonia.Controls { /// /// Represents a control that can be used to display or edit unformatted text. /// [TemplatePart("PART_TextPresenter", typeof(TextPresenter))] [PseudoClasses(":empty")] public class TextBox : TemplatedControl, UndoRedoHelper.IUndoRedoHost { public static KeyGesture? CutGesture { get; } = AvaloniaLocator.Current .GetService()?.Cut.FirstOrDefault(); public static KeyGesture? CopyGesture { get; } = AvaloniaLocator.Current .GetService()?.Copy.FirstOrDefault(); public static KeyGesture? PasteGesture { get; } = AvaloniaLocator.Current .GetService()?.Paste.FirstOrDefault(); public static readonly StyledProperty AcceptsReturnProperty = AvaloniaProperty.Register(nameof(AcceptsReturn)); public static readonly StyledProperty AcceptsTabProperty = AvaloniaProperty.Register(nameof(AcceptsTab)); public static readonly DirectProperty CaretIndexProperty = AvaloniaProperty.RegisterDirect( nameof(CaretIndex), o => o.CaretIndex, (o, v) => o.CaretIndex = v); public static readonly StyledProperty IsReadOnlyProperty = AvaloniaProperty.Register(nameof(IsReadOnly)); public static readonly StyledProperty PasswordCharProperty = AvaloniaProperty.Register(nameof(PasswordChar)); public static readonly StyledProperty SelectionBrushProperty = AvaloniaProperty.Register(nameof(SelectionBrushProperty)); public static readonly StyledProperty SelectionForegroundBrushProperty = AvaloniaProperty.Register(nameof(SelectionForegroundBrushProperty)); public static readonly StyledProperty CaretBrushProperty = AvaloniaProperty.Register(nameof(CaretBrushProperty)); public static readonly DirectProperty SelectionStartProperty = AvaloniaProperty.RegisterDirect( nameof(SelectionStart), o => o.SelectionStart, (o, v) => o.SelectionStart = v); public static readonly DirectProperty SelectionEndProperty = AvaloniaProperty.RegisterDirect( nameof(SelectionEnd), o => o.SelectionEnd, (o, v) => o.SelectionEnd = v); public static readonly StyledProperty MaxLengthProperty = AvaloniaProperty.Register(nameof(MaxLength), defaultValue: 0); public static readonly StyledProperty MaxLinesProperty = AvaloniaProperty.Register(nameof(MaxLines), defaultValue: 0); public static readonly DirectProperty TextProperty = TextBlock.TextProperty.AddOwnerWithDataValidation( o => o.Text, (o, v) => o.Text = v, defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true); public static readonly StyledProperty TextAlignmentProperty = TextBlock.TextAlignmentProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty HorizontalContentAlignmentProperty = ContentControl.HorizontalContentAlignmentProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty VerticalContentAlignmentProperty = ContentControl.VerticalContentAlignmentProperty.AddOwner(); public static readonly StyledProperty TextWrappingProperty = TextBlock.TextWrappingProperty.AddOwner(); public static readonly StyledProperty WatermarkProperty = AvaloniaProperty.Register(nameof(Watermark)); public static readonly StyledProperty UseFloatingWatermarkProperty = AvaloniaProperty.Register(nameof(UseFloatingWatermark)); public static readonly DirectProperty NewLineProperty = AvaloniaProperty.RegisterDirect(nameof(NewLine), textbox => textbox.NewLine, (textbox, newline) => textbox.NewLine = newline); public static readonly StyledProperty InnerLeftContentProperty = AvaloniaProperty.Register(nameof(InnerLeftContent)); public static readonly StyledProperty InnerRightContentProperty = AvaloniaProperty.Register(nameof(InnerRightContent)); public static readonly StyledProperty RevealPasswordProperty = AvaloniaProperty.Register(nameof(RevealPassword)); public static readonly DirectProperty CanCutProperty = AvaloniaProperty.RegisterDirect( nameof(CanCut), o => o.CanCut); public static readonly DirectProperty CanCopyProperty = AvaloniaProperty.RegisterDirect( nameof(CanCopy), o => o.CanCopy); public static readonly DirectProperty CanPasteProperty = AvaloniaProperty.RegisterDirect( nameof(CanPaste), o => o.CanPaste); public static readonly StyledProperty IsUndoEnabledProperty = AvaloniaProperty.Register( nameof(IsUndoEnabled), defaultValue: true); public static readonly DirectProperty UndoLimitProperty = AvaloniaProperty.RegisterDirect( nameof(UndoLimit), o => o.UndoLimit, (o, v) => o.UndoLimit = v, unsetValue: -1); public static readonly RoutedEvent CopyingToClipboardEvent = RoutedEvent.Register( "CopyingToClipboard", RoutingStrategies.Bubble); public static readonly RoutedEvent CuttingToClipboardEvent = RoutedEvent.Register( "CuttingToClipboard", RoutingStrategies.Bubble); public static readonly RoutedEvent PastingFromClipboardEvent = RoutedEvent.Register( "PastingFromClipboard", RoutingStrategies.Bubble); readonly struct UndoRedoState : IEquatable { public string? Text { get; } public int CaretPosition { get; } public UndoRedoState(string? text, int caretPosition) { Text = text; CaretPosition = caretPosition; } public bool Equals(UndoRedoState other) => ReferenceEquals(Text, other.Text) || Equals(Text, other.Text); public override bool Equals(object? obj) => obj is UndoRedoState other && Equals(other); public override int GetHashCode() => Text?.GetHashCode() ?? 0; } private string? _text; private int _caretIndex; private int _selectionStart; private int _selectionEnd; private TextPresenter? _presenter; private TextBoxTextInputMethodClient _imClient = new TextBoxTextInputMethodClient(); private UndoRedoHelper _undoRedoHelper; private bool _isUndoingRedoing; private bool _ignoreTextChanges; private bool _canCut; private bool _canCopy; private bool _canPaste; private string _newLine = Environment.NewLine; private static readonly string[] invalidCharacters = new String[1] { "\u007f" }; private int _selectedTextChangesMadeSinceLastUndoSnapshot; private bool _hasDoneSnapshotOnce; private const int _maxCharsBeforeUndoSnapshot = 7; static TextBox() { FocusableProperty.OverrideDefaultValue(typeof(TextBox), true); TextInputMethodClientRequestedEvent.AddClassHandler((tb, e) => { if (!tb.IsReadOnly) { e.Client = tb._imClient; } }); } public TextBox() { var horizontalScrollBarVisibility = Observable.CombineLatest( this.GetObservable(AcceptsReturnProperty), this.GetObservable(TextWrappingProperty), (acceptsReturn, wrapping) => { if (wrapping != TextWrapping.NoWrap) { return ScrollBarVisibility.Disabled; } return acceptsReturn ? ScrollBarVisibility.Auto : ScrollBarVisibility.Hidden; }); this.Bind( ScrollViewer.HorizontalScrollBarVisibilityProperty, horizontalScrollBarVisibility, BindingPriority.Style); _undoRedoHelper = new UndoRedoHelper(this); _selectedTextChangesMadeSinceLastUndoSnapshot = 0; _hasDoneSnapshotOnce = false; UpdatePseudoclasses(); } public bool AcceptsReturn { get { return GetValue(AcceptsReturnProperty); } set { SetValue(AcceptsReturnProperty, value); } } public bool AcceptsTab { get { return GetValue(AcceptsTabProperty); } set { SetValue(AcceptsTabProperty, value); } } public int CaretIndex { get { return _caretIndex; } set { value = CoerceCaretIndex(value); SetAndRaise(CaretIndexProperty, ref _caretIndex, value); UndoRedoState state; if (IsUndoEnabled && _undoRedoHelper.TryGetLastState(out state) && state.Text == Text) _undoRedoHelper.UpdateLastState(); SelectionStart = SelectionEnd = value; } } public bool IsReadOnly { get { return GetValue(IsReadOnlyProperty); } set { SetValue(IsReadOnlyProperty, value); } } public char PasswordChar { get => GetValue(PasswordCharProperty); set => SetValue(PasswordCharProperty, value); } public IBrush? SelectionBrush { get => GetValue(SelectionBrushProperty); set => SetValue(SelectionBrushProperty, value); } public IBrush? SelectionForegroundBrush { get => GetValue(SelectionForegroundBrushProperty); set => SetValue(SelectionForegroundBrushProperty, value); } public IBrush? CaretBrush { get => GetValue(CaretBrushProperty); set => SetValue(CaretBrushProperty, value); } public int SelectionStart { get { return _selectionStart; } set { value = CoerceCaretIndex(value); var changed = SetAndRaise(SelectionStartProperty, ref _selectionStart, value); if (changed) { UpdateCommandStates(); } if (SelectionEnd == value && CaretIndex != value) { CaretIndex = value; } } } public int SelectionEnd { get { return _selectionEnd; } set { value = CoerceCaretIndex(value); var changed = SetAndRaise(SelectionEndProperty, ref _selectionEnd, value); if (changed) { UpdateCommandStates(); } if (SelectionStart == value && CaretIndex != value) { CaretIndex = value; } } } public int MaxLength { get { return GetValue(MaxLengthProperty); } set { SetValue(MaxLengthProperty, value); } } public int MaxLines { get { return GetValue(MaxLinesProperty); } set { SetValue(MaxLinesProperty, value); } } [Content] public string? Text { get { return _text; } set { if (!_ignoreTextChanges) { var caretIndex = CaretIndex; var selectionStart = SelectionStart; var selectionEnd = SelectionEnd; CaretIndex = CoerceCaretIndex(caretIndex, value); SelectionStart = CoerceCaretIndex(selectionStart, value); SelectionEnd = CoerceCaretIndex(selectionEnd, value); if (SetAndRaise(TextProperty, ref _text, value) && IsUndoEnabled && !_isUndoingRedoing) { _undoRedoHelper.Clear(); SnapshotUndoRedo(); // so we always have an initial state } } } } public string SelectedText { get { return GetSelection(); } set { if (string.IsNullOrEmpty(value)) { _selectedTextChangesMadeSinceLastUndoSnapshot++; SnapshotUndoRedo(ignoreChangeCount: false); DeleteSelection(); } else { HandleTextInput(value); } } } /// /// Gets or sets the horizontal alignment of the content within the control. /// public HorizontalAlignment HorizontalContentAlignment { get { return GetValue(HorizontalContentAlignmentProperty); } set { SetValue(HorizontalContentAlignmentProperty, value); } } /// /// Gets or sets the vertical alignment of the content within the control. /// public VerticalAlignment VerticalContentAlignment { get { return GetValue(VerticalContentAlignmentProperty); } set { SetValue(VerticalContentAlignmentProperty, value); } } public TextAlignment TextAlignment { get { return GetValue(TextAlignmentProperty); } set { SetValue(TextAlignmentProperty, value); } } public string? Watermark { get { return GetValue(WatermarkProperty); } set { SetValue(WatermarkProperty, value); } } public bool UseFloatingWatermark { get { return GetValue(UseFloatingWatermarkProperty); } set { SetValue(UseFloatingWatermarkProperty, value); } } public object InnerLeftContent { get { return GetValue(InnerLeftContentProperty); } set { SetValue(InnerLeftContentProperty, value); } } public object InnerRightContent { get { return GetValue(InnerRightContentProperty); } set { SetValue(InnerRightContentProperty, value); } } public bool RevealPassword { get { return GetValue(RevealPasswordProperty); } set { SetValue(RevealPasswordProperty, value); } } public TextWrapping TextWrapping { get { return GetValue(TextWrappingProperty); } set { SetValue(TextWrappingProperty, value); } } /// /// Gets or sets which characters are inserted when Enter is pressed. Default: /// public string NewLine { get { return _newLine; } set { SetAndRaise(NewLineProperty, ref _newLine, value); } } /// /// Clears the current selection, maintaining the /// public void ClearSelection() { CaretIndex = SelectionStart; } /// /// Property for determining if the Cut command can be executed. /// public bool CanCut { get { return _canCut; } private set { SetAndRaise(CanCutProperty, ref _canCut, value); } } /// /// Property for determining if the Copy command can be executed. /// public bool CanCopy { get { return _canCopy; } private set { SetAndRaise(CanCopyProperty, ref _canCopy, value); } } /// /// Property for determining if the Paste command can be executed. /// public bool CanPaste { get { return _canPaste; } private set { SetAndRaise(CanPasteProperty, ref _canPaste, value); } } /// /// Property for determining whether undo/redo is enabled /// public bool IsUndoEnabled { get { return GetValue(IsUndoEnabledProperty); } set { SetValue(IsUndoEnabledProperty, value); } } public int UndoLimit { get { return _undoRedoHelper.Limit; } set { if (_undoRedoHelper.Limit != value) { // can't use SetAndRaise due to using _undoRedoHelper.Limit // (can't send a ref of a property to SetAndRaise), // so use RaisePropertyChanged instead. var oldValue = _undoRedoHelper.Limit; _undoRedoHelper.Limit = value; RaisePropertyChanged(UndoLimitProperty, oldValue, value); } // from docs at // https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled: // "Setting UndoLimit clears the undo queue." _undoRedoHelper.Clear(); _selectedTextChangesMadeSinceLastUndoSnapshot = 0; _hasDoneSnapshotOnce = false; } } public event EventHandler? CopyingToClipboard { add => AddHandler(CopyingToClipboardEvent, value); remove => RemoveHandler(CopyingToClipboardEvent, value); } public event EventHandler? CuttingToClipboard { add => AddHandler(CuttingToClipboardEvent, value); remove => RemoveHandler(CuttingToClipboardEvent, value); } public event EventHandler? PastingFromClipboard { add => AddHandler(PastingFromClipboardEvent, value); remove => RemoveHandler(PastingFromClipboardEvent, value); } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { _presenter = e.NameScope.Get("PART_TextPresenter"); _imClient.SetPresenter(_presenter, this); if (IsFocused) { _presenter?.ShowCaret(); } } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); if (IsFocused) { _presenter?.ShowCaret(); } } protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); _imClient.SetPresenter(null, null); } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == TextProperty) { UpdatePseudoclasses(); UpdateCommandStates(); } else if (change.Property == IsUndoEnabledProperty && change.GetNewValue() == false) { // from docs at // https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled: // "Setting this property to false clears the undo stack. // Therefore, if you disable undo and then re-enable it, undo commands still do not work // because the undo stack was emptied when you disabled undo." _undoRedoHelper.Clear(); _selectedTextChangesMadeSinceLastUndoSnapshot = 0; _hasDoneSnapshotOnce = false; } } private void UpdateCommandStates() { var text = GetSelection(); var isSelectionNullOrEmpty = string.IsNullOrEmpty(text); CanCopy = !IsPasswordBox && !isSelectionNullOrEmpty; CanCut = !IsPasswordBox && !isSelectionNullOrEmpty && !IsReadOnly; CanPaste = !IsReadOnly; } protected override void OnGotFocus(GotFocusEventArgs e) { base.OnGotFocus(e); // 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 if (e.NavigationMethod == NavigationMethod.Tab && !AcceptsReturn && Text?.Length > 0) { SelectAll(); } UpdateCommandStates(); _imClient.SetPresenter(_presenter, this); _presenter?.ShowCaret(); } protected override void OnLostFocus(RoutedEventArgs e) { base.OnLostFocus(e); if ((ContextFlyout == null || !ContextFlyout.IsOpen) && (ContextMenu == null || !ContextMenu.IsOpen)) { ClearSelection(); RevealPassword = false; } UpdateCommandStates(); _presenter?.HideCaret(); _imClient.SetPresenter(null, null); } protected override void OnTextInput(TextInputEventArgs e) { if (!e.Handled) { HandleTextInput(e.Text); e.Handled = true; } } private void HandleTextInput(string? input) { if (IsReadOnly) { return; } input = RemoveInvalidCharacters(input); if (string.IsNullOrEmpty(input)) { return; } _selectedTextChangesMadeSinceLastUndoSnapshot++; SnapshotUndoRedo(ignoreChangeCount: false); if (_presenter != null && MaxLines > 0) { var lineCount = _presenter.TextLayout.TextLines.Count; var length = 0; var graphemeEnumerator = new GraphemeEnumerator(input.AsMemory()); while (graphemeEnumerator.MoveNext()) { var grapheme = graphemeEnumerator.Current; if (grapheme.FirstCodepoint.IsBreakChar) { if(lineCount + 1 > MaxLines) { break; } else { lineCount++; } } length += grapheme.Text.Length; } if (length < input.Length) { input = input.Remove(Math.Max(0, length)); } } var text = Text ?? string.Empty; var newLength = input.Length + text.Length - Math.Abs(SelectionStart - SelectionEnd); if (MaxLength > 0 && newLength > MaxLength) { input = input.Remove(Math.Max(0, input.Length - (newLength - MaxLength))); } if (!string.IsNullOrEmpty(input)) { var oldText = _text; _ignoreTextChanges = true; try { DeleteSelection(false); var caretIndex = CaretIndex; text = Text ?? string.Empty; SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex)); ClearSelection(); if (IsUndoEnabled) { _undoRedoHelper.DiscardRedo(); } if (_text != oldText) { RaisePropertyChanged(TextProperty, oldText, _text); } CaretIndex = caretIndex + input.Length; } finally { _ignoreTextChanges = false; } } } public string? RemoveInvalidCharacters(string? text) { if (text is null) return null; for (var i = 0; i < invalidCharacters.Length; i++) { text = text.Replace(invalidCharacters[i], string.Empty); } return text; } public async void Cut() { var text = GetSelection(); if (string.IsNullOrEmpty(text)) { return; } var eventArgs = new RoutedEventArgs(CuttingToClipboardEvent); RaiseEvent(eventArgs); if (!eventArgs.Handled) { SnapshotUndoRedo(); await ((IClipboard)AvaloniaLocator.Current.GetRequiredService(typeof(IClipboard))) .SetTextAsync(text); DeleteSelection(); } } public async void Copy() { var text = GetSelection(); if (string.IsNullOrEmpty(text)) { return; } var eventArgs = new RoutedEventArgs(CopyingToClipboardEvent); RaiseEvent(eventArgs); if (!eventArgs.Handled) { await ((IClipboard)AvaloniaLocator.Current.GetRequiredService(typeof(IClipboard))) .SetTextAsync(text); } } public async void Paste() { var eventArgs = new RoutedEventArgs(PastingFromClipboardEvent); RaiseEvent(eventArgs); if (eventArgs.Handled) { return; } var text = await ((IClipboard)AvaloniaLocator.Current.GetRequiredService(typeof(IClipboard))).GetTextAsync(); if (string.IsNullOrEmpty(text)) { return; } SnapshotUndoRedo(); HandleTextInput(text); } protected override void OnKeyDown(KeyEventArgs e) { if (_presenter == null) { return; } var text = Text ?? string.Empty; var caretIndex = CaretIndex; var movement = false; var selection = false; var handled = false; var modifiers = e.KeyModifiers; var keymap = AvaloniaLocator.Current.GetRequiredService(); bool Match(List gestures) => gestures.Any(g => g.Matches(e)); bool DetectSelection() => e.KeyModifiers.HasAllFlags(keymap.SelectionModifiers); if (Match(keymap.SelectAll)) { SelectAll(); handled = true; } else if (Match(keymap.Copy)) { if (!IsPasswordBox) { Copy(); } handled = true; } else if (Match(keymap.Cut)) { if (!IsPasswordBox) { Cut(); } handled = true; } else if (Match(keymap.Paste)) { Paste(); handled = true; } else if (Match(keymap.Undo) && IsUndoEnabled) { try { SnapshotUndoRedo(); _isUndoingRedoing = true; _undoRedoHelper.Undo(); } finally { _isUndoingRedoing = false; } handled = true; } else if (Match(keymap.Redo) && IsUndoEnabled) { try { _isUndoingRedoing = true; _undoRedoHelper.Redo(); } finally { _isUndoingRedoing = false; } handled = true; } else if (Match(keymap.MoveCursorToTheStartOfDocument)) { MoveHome(true); movement = true; selection = false; handled = true; CaretIndex = _presenter.CaretIndex; } else if (Match(keymap.MoveCursorToTheEndOfDocument)) { MoveEnd(true); movement = true; selection = false; handled = true; CaretIndex = _presenter.CaretIndex; } else if (Match(keymap.MoveCursorToTheStartOfLine)) { MoveHome(false); movement = true; selection = false; handled = true; CaretIndex = _presenter.CaretIndex; } else if (Match(keymap.MoveCursorToTheEndOfLine)) { MoveEnd(false); movement = true; selection = false; handled = true; CaretIndex = _presenter.CaretIndex; } else if (Match(keymap.MoveCursorToTheStartOfDocumentWithSelection)) { SelectionStart = caretIndex; MoveHome(true); SelectionEnd = _presenter.CaretIndex; movement = true; selection = true; handled = true; } else if (Match(keymap.MoveCursorToTheEndOfDocumentWithSelection)) { SelectionStart = caretIndex; MoveEnd(true); SelectionEnd = _presenter.CaretIndex; movement = true; selection = true; handled = true; } else if (Match(keymap.MoveCursorToTheStartOfLineWithSelection)) { SelectionStart = caretIndex; MoveHome(false); SelectionEnd = _presenter.CaretIndex; movement = true; selection = true; handled = true; } else if (Match(keymap.MoveCursorToTheEndOfLineWithSelection)) { SelectionStart = caretIndex; MoveEnd(false); SelectionEnd = _presenter.CaretIndex; movement = true; selection = true; handled = true; } else { bool hasWholeWordModifiers = modifiers.HasAllFlags(keymap.WholeWordTextActionModifiers); switch (e.Key) { case Key.Left: selection = DetectSelection(); MoveHorizontal(-1, hasWholeWordModifiers, selection); movement = true; break; case Key.Right: selection = DetectSelection(); MoveHorizontal(1, hasWholeWordModifiers, selection); movement = true; break; case Key.Up: { selection = DetectSelection(); _presenter.MoveCaretVertical(LogicalDirection.Backward); if (caretIndex != _presenter.CaretIndex) { movement = true; } if (selection) { SelectionEnd = _presenter.CaretIndex; } else { CaretIndex = _presenter.CaretIndex; } break; } case Key.Down: { selection = DetectSelection(); _presenter.MoveCaretVertical(); if (caretIndex != _presenter.CaretIndex) { movement = true; } if (selection) { SelectionEnd = _presenter.CaretIndex; } else { CaretIndex = _presenter.CaretIndex; } break; } case Key.Back: { SnapshotUndoRedo(); if (hasWholeWordModifiers && SelectionStart == SelectionEnd) { SetSelectionForControlBackspace(); } if (!DeleteSelection()) { var characterHit = _presenter.GetNextCharacterHit(LogicalDirection.Backward); var backspacePosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; if (caretIndex != backspacePosition) { var start = Math.Min(backspacePosition, caretIndex); var end = Math.Max(backspacePosition, caretIndex); var length = end - start; var editedText = text.Substring(0, start) + text.Substring(Math.Min(end, text.Length)); SetTextInternal(editedText); CaretIndex = start; } } SnapshotUndoRedo(); handled = true; break; } case Key.Delete: SnapshotUndoRedo(); if (hasWholeWordModifiers && SelectionStart == SelectionEnd) { SetSelectionForControlDelete(); } if (!DeleteSelection()) { var characterHit = _presenter.GetNextCharacterHit(); var nextPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; if(nextPosition != caretIndex) { var start = Math.Min(nextPosition, caretIndex); var end = Math.Max(nextPosition, caretIndex); var editedText = text.Substring(0, start) + text.Substring(Math.Min(end, text.Length)); SetTextInternal(editedText); } } SnapshotUndoRedo(); handled = true; break; case Key.Enter: if (AcceptsReturn) { SnapshotUndoRedo(); HandleTextInput(NewLine); handled = true; } break; case Key.Tab: if (AcceptsTab) { SnapshotUndoRedo(); HandleTextInput("\t"); handled = true; } else { base.OnKeyDown(e); } break; case Key.Space: SnapshotUndoRedo(); // always snapshot in between words break; default: handled = false; break; } } if (movement && !selection) { ClearSelection(); } if (handled || movement) { e.Handled = true; } } protected override void OnPointerPressed(PointerPressedEventArgs e) { if (_presenter == null) { return; } var text = Text; var clickInfo = e.GetCurrentPoint(this); if (text != null && clickInfo.Properties.IsLeftButtonPressed && !(clickInfo.Pointer?.Captured is Border)) { var point = e.GetPosition(_presenter); var oldIndex = CaretIndex; _presenter.MoveCaretToPoint(point); var index = _presenter.CaretIndex; var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift); SetAndRaise(CaretIndexProperty, ref _caretIndex, index); #pragma warning disable CS0618 // Type or member is obsolete switch (e.ClickCount) #pragma warning restore CS0618 // Type or member is obsolete { case 1: if (clickToSelect) { SelectionStart = Math.Min(oldIndex, index); SelectionEnd = Math.Max(oldIndex, index); } else { SelectionStart = SelectionEnd = index; } break; case 2: if (!StringUtils.IsStartOfWord(text, index)) { SelectionStart = StringUtils.PreviousWord(text, index); } SelectionEnd = StringUtils.NextWord(text, index); break; case 3: SelectAll(); break; } } e.Pointer.Capture(_presenter); e.Handled = true; } protected override void OnPointerMoved(PointerEventArgs e) { if (_presenter == null) { return; } // selection should not change during pointer move if the user right clicks if (e.Pointer.Captured == _presenter && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { var point = e.GetPosition(_presenter); 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))); _presenter.MoveCaretToPoint(point); SelectionEnd = _presenter.CaretIndex; } } protected override void OnPointerReleased(PointerReleasedEventArgs e) { if (_presenter == null) { return; } if (e.Pointer.Captured != _presenter) { return; } if (e.InitialPressMouseButton == MouseButton.Right) { var point = e.GetPosition(_presenter); _presenter.MoveCaretToPoint(point); var caretIndex = _presenter.CaretIndex; // see if mouse clicked inside current selection // if it did not, we change the selection to where the user clicked var firstSelection = Math.Min(SelectionStart, SelectionEnd); var lastSelection = Math.Max(SelectionStart, SelectionEnd); var didClickInSelection = SelectionStart != SelectionEnd && caretIndex >= firstSelection && caretIndex <= lastSelection; if (!didClickInSelection) { CaretIndex = SelectionEnd = SelectionStart = caretIndex; } } e.Pointer.Capture(null); } protected override AutomationPeer OnCreateAutomationPeer() { return new TextBoxAutomationPeer(this); } protected override void UpdateDataValidation( AvaloniaProperty property, BindingValueType state, Exception? error) { if (property == TextProperty) { DataValidationErrors.SetError(this, error); } } private int CoerceCaretIndex(int value) => CoerceCaretIndex(value, Text); private int CoerceCaretIndex(int value, string? text) { if (text == null) { return 0; } var length = text.Length; if (value < 0) { return 0; } else if (value > length) { return length; } else if (value > 0 && text[value - 1] == '\r' && value < length && text[value] == '\n') { return value + 1; } else { return value; } } public void Clear() { Text = string.Empty; } private void MoveHorizontal(int direction, bool wholeWord, bool isSelecting) { if (_presenter == null) { return; } var text = Text ?? string.Empty; var selectionStart = SelectionStart; var selectionEnd = SelectionEnd; if (!wholeWord) { if (isSelecting) { _presenter.MoveCaretToTextPosition(selectionEnd); _presenter.MoveCaretHorizontal(direction > 0 ? LogicalDirection.Forward : LogicalDirection.Backward); SelectionEnd = _presenter.CaretIndex; } else { if (selectionStart != selectionEnd) { _presenter.MoveCaretToTextPosition(direction > 0 ? Math.Max(selectionStart, selectionEnd) : Math.Min(selectionStart, selectionEnd)); } else { _presenter.MoveCaretHorizontal(direction > 0 ? LogicalDirection.Forward : LogicalDirection.Backward); } CaretIndex = _presenter.CaretIndex; } } else { int offset; if (direction > 0) { offset = StringUtils.NextWord(text, selectionEnd) - selectionEnd; } else { offset = StringUtils.PreviousWord(text, selectionEnd) - selectionEnd; } SelectionEnd += offset; _presenter.MoveCaretToTextPosition(SelectionEnd); if (!isSelecting) { CaretIndex = SelectionEnd; } else { SelectionStart = selectionStart; } } } private void MoveHome(bool document) { if (_presenter is null) { return; } var caretIndex = CaretIndex; if (document) { _presenter.MoveCaretToTextPosition(0); } else { var textLines = _presenter.TextLayout.TextLines; var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(caretIndex, false); var textLine = textLines[lineIndex]; _presenter.MoveCaretToTextPosition(textLine.FirstTextSourceIndex); } } private void MoveEnd(bool document) { if (_presenter is null) { return; } var text = Text ?? string.Empty; var caretIndex = CaretIndex; if (document) { _presenter.MoveCaretToTextPosition(text.Length, true); } else { var textLines = _presenter.TextLayout.TextLines; var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(caretIndex, false); var textLine = textLines[lineIndex]; var textPosition = textLine.FirstTextSourceIndex + textLine.Length; _presenter.MoveCaretToTextPosition(textPosition, true); } } /// /// Select all text in the TextBox /// public void SelectAll() { SelectionStart = 0; SelectionEnd = Text?.Length ?? 0; } private bool DeleteSelection(bool raiseTextChanged = true) { if (IsReadOnly) return true; var selectionStart = SelectionStart; var selectionEnd = SelectionEnd; if (selectionStart != selectionEnd) { var start = Math.Min(selectionStart, selectionEnd); var end = Math.Max(selectionStart, selectionEnd); var text = Text!; SetTextInternal(text.Substring(0, start) + text.Substring(end), raiseTextChanged); _presenter?.MoveCaretToTextPosition(start); CaretIndex= start; ClearSelection(); return true; } CaretIndex = SelectionStart; return false; } private string GetSelection() { var text = Text; if (string.IsNullOrEmpty(text)) { return ""; } var selectionStart = SelectionStart; var selectionEnd = SelectionEnd; var start = Math.Min(selectionStart, selectionEnd); var end = Math.Max(selectionStart, selectionEnd); if (start == end || (Text?.Length ?? 0) < end) { return ""; } return text.Substring(start, end - start); } private void SetTextInternal(string value, bool raiseTextChanged = true) { if (raiseTextChanged) { try { _ignoreTextChanges = true; SetAndRaise(TextProperty, ref _text, value); } finally { _ignoreTextChanges = false; } } else { _text = value; } } private void SetSelectionForControlBackspace() { var selectionStart = CaretIndex; MoveHorizontal(-1, true, false); SelectionStart = selectionStart; } private void SetSelectionForControlDelete() { if (_text == null || _presenter == null) { return; } SelectionStart = CaretIndex; MoveHorizontal(1, true, true); if (SelectionEnd < _text.Length && _text[SelectionEnd] == ' ') { SelectionEnd++; } } private void UpdatePseudoclasses() { PseudoClasses.Set(":empty", string.IsNullOrEmpty(Text)); } private bool IsPasswordBox => PasswordChar != default(char); UndoRedoState UndoRedoHelper.IUndoRedoHost.UndoRedoState { get { return new UndoRedoState(Text, CaretIndex); } set { Text = value.Text; CaretIndex = value.CaretPosition; ClearSelection(); } } private void SnapshotUndoRedo(bool ignoreChangeCount = true) { if (IsUndoEnabled) { if (ignoreChangeCount || !_hasDoneSnapshotOnce || (!ignoreChangeCount && _selectedTextChangesMadeSinceLastUndoSnapshot >= _maxCharsBeforeUndoSnapshot)) { _undoRedoHelper.Snapshot(); _selectedTextChangesMadeSinceLastUndoSnapshot = 0; _hasDoneSnapshotOnce = true; } } } } }