From 20fe710681f5546f3a7baccb3d781cd9a7442c12 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Thu, 1 Aug 2024 22:46:41 +0000 Subject: [PATCH] refactor android input connection (#16490) --- .../Input}/AndroidInputMethod.cs | 101 +++--- .../Platform/Input/AvaloniaInputConnection.cs | 310 ++++++++++++++++++ .../Platform/Input/EditCommand.cs | 181 ++++++++++ .../Platform/Input/TextEditBuffer.cs | 102 ++++++ .../Platform/SkiaPlatform/TopLevelImpl.cs | 270 --------------- 5 files changed, 632 insertions(+), 332 deletions(-) rename src/Android/Avalonia.Android/{ => Platform/Input}/AndroidInputMethod.cs (65%) create mode 100644 src/Android/Avalonia.Android/Platform/Input/AvaloniaInputConnection.cs create mode 100644 src/Android/Avalonia.Android/Platform/Input/EditCommand.cs create mode 100644 src/Android/Avalonia.Android/Platform/Input/TextEditBuffer.cs diff --git a/src/Android/Avalonia.Android/AndroidInputMethod.cs b/src/Android/Avalonia.Android/Platform/Input/AndroidInputMethod.cs similarity index 65% rename from src/Android/Avalonia.Android/AndroidInputMethod.cs rename to src/Android/Avalonia.Android/Platform/Input/AndroidInputMethod.cs index 0c487046a9..bbcf8276f1 100644 --- a/src/Android/Avalonia.Android/AndroidInputMethod.cs +++ b/src/Android/Avalonia.Android/Platform/Input/AndroidInputMethod.cs @@ -5,10 +5,9 @@ using Android.Runtime; using Android.Text; using Android.Views; using Android.Views.InputMethods; -using Avalonia.Android.Platform.SkiaPlatform; using Avalonia.Input.TextInput; -namespace Avalonia.Android +namespace Avalonia.Android.Platform.Input { internal interface IAndroidInputMethod { @@ -21,18 +20,18 @@ namespace Avalonia.Android public InputMethodManager IMM { get; } - void OnBatchEditedEnded(); + void OnBatchEditEnded(); } enum CustomImeFlags - { + { ActionNone = 0x00000001, - ActionGo = 0x00000002, - ActionSearch = 0x00000003, - ActionSend = 0x00000004, - ActionNext = 0x00000005, - ActionDone = 0x00000006, - ActionPrevious = 0x00000007, + ActionGo = 0x00000002, + ActionSearch = 0x00000003, + ActionSend = 0x00000004, + ActionNext = 0x00000005, + ActionDone = 0x00000006, + ActionPrevious = 0x00000007, } internal class AndroidInputMethod : ITextInputMethodImpl, IAndroidInputMethod @@ -79,7 +78,7 @@ namespace Avalonia.Android { _host.RequestFocus(); - _imm.RestartInput(View); + _imm.RestartInput(View); _imm.ShowSoftInput(_host, ShowFlags.Implicit); @@ -110,27 +109,29 @@ namespace Avalonia.Android private void _client_SelectionChanged(object? sender, EventArgs e) { - if (_inputConnection is null || _inputConnection.IsInBatchEdit) + if (_inputConnection is null || _inputConnection.IsInBatchEdit || _inputConnection.IsInUpdate) return; OnSelectionChanged(); } private void OnSelectionChanged() { - if (Client is null || _inputConnection is null) + if (Client is null || _inputConnection is null || _inputConnection.IsInUpdate) { return; } OnSurroundingTextChanged(); - var selection = Client.Selection; + _inputConnection.IsInUpdate = true; - _inputConnection.SetSelection(selection.Start, selection.End); + var selection = Client.Selection; - var composition = _inputConnection.EditableWrapper.CurrentComposition; + var composition = _inputConnection.EditBuffer.HasComposition ? _inputConnection.EditBuffer.Composition!.Value : new TextSelection(-1,-1); _imm.UpdateSelection(_host, selection.Start, selection.End, composition.Start, composition.End); + + _inputConnection.IsInUpdate = false; } private void _client_SurroundingTextChanged(object? sender, EventArgs e) @@ -140,7 +141,7 @@ namespace Avalonia.Android OnSurroundingTextChanged(); } - public void OnBatchEditedEnded() + public void OnBatchEditEnded() { if (_inputConnection is null || _inputConnection.IsInBatchEdit) return; @@ -149,56 +150,32 @@ namespace Avalonia.Android private void OnSurroundingTextChanged() { - if(_client is null || _inputConnection is null) + if (_client is null || _inputConnection is null || _inputConnection.IsInUpdate) { return; } - var surroundingText = _client.SurroundingText ?? ""; - var editableText = _inputConnection.EditableWrapper.ToString(); - - if (editableText != surroundingText) + if (_inputConnection.IsInMonitorMode) { - _inputConnection.EditableWrapper.IgnoreChange = true; - - var diff = GetDiff(); - - _inputConnection.Editable.Replace(diff.index, editableText.Length, diff.diff); - - _inputConnection.EditableWrapper.IgnoreChange = false; - - if(diff.index == 0) - { - var selection = _client.Selection; - _client.Selection = new TextSelection(selection.Start, 0); - _client.Selection = selection; - } - } - - (int index, string diff) GetDiff() - { - int index = 0; + var surroundingText = _client.SurroundingText ?? ""; - var longerLength = Math.Max(surroundingText.Length, editableText.Length); + var selection = _client.Selection; - for (int i = 0; i < longerLength; i++) + var extractedText = new ExtractedText { - if (surroundingText.Length == i || editableText.Length == i || surroundingText[i] != editableText[i]) - { - index = i; - break; - } - } - - var diffString = surroundingText.Substring(index, surroundingText.Length - index); + Text = new Java.Lang.String(surroundingText), + SelectionStart = selection.Start, + SelectionEnd = selection.End, + PartialEndOffset = surroundingText.Length + }; - return (index, diffString); + _imm.UpdateExtractedText(_host, _inputConnection.ExtractedTextToken, extractedText); } } public void SetCursorRect(Rect rect) { - + } public void SetOptions(TextInputOptions options) @@ -212,22 +189,22 @@ namespace Avalonia.Android outAttrs.InputType = options.ContentType switch { - TextInputContentType.Email => global::Android.Text.InputTypes.TextVariationEmailAddress, - TextInputContentType.Number => global::Android.Text.InputTypes.ClassNumber, - TextInputContentType.Password => global::Android.Text.InputTypes.TextVariationPassword, - TextInputContentType.Digits => global::Android.Text.InputTypes.ClassPhone, - TextInputContentType.Url => global::Android.Text.InputTypes.TextVariationUri, - _ => global::Android.Text.InputTypes.ClassText + TextInputContentType.Email => InputTypes.TextVariationEmailAddress, + TextInputContentType.Number => InputTypes.ClassNumber, + TextInputContentType.Password => InputTypes.TextVariationPassword, + TextInputContentType.Digits => InputTypes.ClassPhone, + TextInputContentType.Url => InputTypes.TextVariationUri, + _ => InputTypes.ClassText }; if (options.AutoCapitalization) { - outAttrs.InitialCapsMode = global::Android.Text.CapitalizationMode.Sentences; - outAttrs.InputType |= global::Android.Text.InputTypes.TextFlagCapSentences; + outAttrs.InitialCapsMode = CapitalizationMode.Sentences; + outAttrs.InputType |= InputTypes.TextFlagCapSentences; } if (options.Multiline) - outAttrs.InputType |= global::Android.Text.InputTypes.TextFlagMultiLine; + outAttrs.InputType |= InputTypes.TextFlagMultiLine; outAttrs.ImeOptions = options.ReturnKeyType switch { diff --git a/src/Android/Avalonia.Android/Platform/Input/AvaloniaInputConnection.cs b/src/Android/Avalonia.Android/Platform/Input/AvaloniaInputConnection.cs new file mode 100644 index 0000000000..7519ab15d1 --- /dev/null +++ b/src/Android/Avalonia.Android/Platform/Input/AvaloniaInputConnection.cs @@ -0,0 +1,310 @@ +using System.Collections.Concurrent; +using System.Threading; +using Android.OS; +using Android.Runtime; +using Android.Text; +using Android.Views; +using Android.Views.InputMethods; +using Avalonia.Android.Platform.SkiaPlatform; +using Avalonia.Input; +using Avalonia.Input.TextInput; +using Java.Lang; + +namespace Avalonia.Android.Platform.Input +{ + internal class AvaloniaInputConnection : Object, IInputConnection + { + private readonly TopLevelImpl _toplevel; + private readonly IAndroidInputMethod _inputMethod; + private readonly TextEditBuffer _editBuffer; + private readonly ConcurrentQueue _commandQueue; + + private int _batchLevel = 0; + + public AvaloniaInputConnection(TopLevelImpl toplevel, IAndroidInputMethod inputMethod) + { + _toplevel = toplevel; + _inputMethod = inputMethod; + _editBuffer = new TextEditBuffer(_inputMethod, toplevel); + _commandQueue = new ConcurrentQueue(); + } + + public int ExtractedTextToken { get; private set; } + + public IAndroidInputMethod InputMethod => _inputMethod; + + public TopLevelImpl Toplevel => _toplevel; + + public bool IsInBatchEdit => _batchLevel > 0; + public bool IsInMonitorMode { get; private set; } + + public Handler? Handler => null; + + public TextEditBuffer EditBuffer => _editBuffer; + + public bool IsInUpdate { get; set; } + + public bool SetComposingRegion(int start, int end) + { + if (InputMethod.IsActive) + { + QueueCommand(new CompositionRegionCommand(start, end)); + } + return InputMethod.IsActive; + } + + public bool SetComposingText(ICharSequence? text, int newCursorPosition) + { + if (text is null) + { + return false; + } + + if (InputMethod.IsActive) + { + var compositionText = text.SubSequence(0, text.Length()); + QueueCommand(new CompositionTextCommand(compositionText, newCursorPosition)); + } + + return InputMethod.IsActive; + } + + public bool SetSelection(int start, int end) + { + if (InputMethod.IsActive) + { + if (IsInUpdate) + new SelectionCommand(start, end).Apply(EditBuffer); + else + QueueCommand(new SelectionCommand(start, end)); + } + + return InputMethod.IsActive; + } + + public bool BeginBatchEdit() + { + _batchLevel = Interlocked.Increment(ref _batchLevel); + return InputMethod.IsActive; + } + + public bool EndBatchEdit() + { + _batchLevel = Interlocked.Decrement(ref _batchLevel); + + if (!IsInBatchEdit) + { + IsInUpdate = true; + while (_commandQueue.TryDequeue(out var command)) + { + command.Apply(_editBuffer); + } + IsInUpdate = false; + } + return IsInBatchEdit; + } + + public bool CommitText(ICharSequence? text, int newCursorPosition) + { + if (InputMethod.Client is null || text is null) + { + return false; + } + + if (InputMethod.IsActive) + { + var committedText = text.SubSequence(0, text.Length()); + QueueCommand(new CommitTextCommand(committedText, newCursorPosition)); + } + + return InputMethod.IsActive; + } + + public bool DeleteSurroundingText(int beforeLength, int afterLength) + { + if (InputMethod.IsActive) + { + QueueCommand(new DeleteRegionCommand(beforeLength, afterLength)); + } + + return InputMethod.IsActive; + } + + public bool PerformEditorAction([GeneratedEnum] ImeAction actionCode) + { + switch (actionCode) + { + case ImeAction.Done: + { + _inputMethod.IMM.HideSoftInputFromWindow(_inputMethod.View.WindowToken, HideSoftInputFlags.ImplicitOnly); + break; + } + case ImeAction.Next: + { + FocusManager.GetFocusManager(_toplevel.InputRoot)? + .TryMoveFocus(NavigationDirection.Next); + break; + } + } + + return InputMethod.IsActive; + } + + public ExtractedText? GetExtractedText(ExtractedTextRequest? request, [GeneratedEnum] GetTextFlags flags) + { + IsInMonitorMode = ((int)flags & (int)TextExtractFlags.Monitor) != 0; + + ExtractedTextToken = IsInMonitorMode ? request?.Token ?? 0 : ExtractedTextToken; + + if (!_inputMethod.IsActive) + { + return null; + } + + var extract = new ExtractedText + { + Flags = 0, + PartialStartOffset = -1, + PartialEndOffset = -1, + SelectionStart = _editBuffer.Selection.Start, + SelectionEnd = _editBuffer.Selection.End, + StartOffset = 0 + }; + + extract.Text = new SpannableString(_editBuffer.Text); + + return extract; + } + + public bool PerformContextMenuAction(int id) + { + if (InputMethod.Client is not { } client) + return false; + + switch (id) + { + case global::Android.Resource.Id.SelectAll: + client.ExecuteContextMenuAction(ContextMenuAction.SelectAll); + return true; + case global::Android.Resource.Id.Cut: + client.ExecuteContextMenuAction(ContextMenuAction.Cut); + return true; + case global::Android.Resource.Id.Copy: + client.ExecuteContextMenuAction(ContextMenuAction.Copy); + return true; + case global::Android.Resource.Id.Paste: + client.ExecuteContextMenuAction(ContextMenuAction.Paste); + return true; + default: + break; + } + return InputMethod.IsActive; + } + + public bool ClearMetaKeyStates([GeneratedEnum] MetaKeyStates states) + { + return false; + } + + public void CloseConnection() + { + _commandQueue.Clear(); + _batchLevel = 0; + } + + public bool CommitCompletion(CompletionInfo? text) + { + return false; + } + + public bool CommitContent(InputContentInfo inputContentInfo, [GeneratedEnum] InputContentFlags flags, Bundle? opts) + { + return false; + } + + public bool CommitCorrection(CorrectionInfo? correctionInfo) + { + return false; + } + + public bool DeleteSurroundingTextInCodePoints(int beforeLength, int afterLength) + { + if (InputMethod.IsActive) + { + QueueCommand(new DeleteRegionInCodePointsCommand(beforeLength, afterLength)); + } + + return InputMethod.IsActive; + } + + public bool FinishComposingText() + { + if (InputMethod.IsActive) + { + QueueCommand(new FinishComposingCommand()); + } + + return InputMethod.IsActive; + } + + [return: GeneratedEnum] + public CapitalizationMode GetCursorCapsMode([GeneratedEnum] CapitalizationMode reqModes) + { + return TextUtils.GetCapsMode(_editBuffer.Text, _editBuffer.Selection.Start, reqModes); + } + + public ICharSequence? GetSelectedTextFormatted([GeneratedEnum] GetTextFlags flags) + { + return new SpannableString(_editBuffer.SelectedText); + } + + public ICharSequence? GetTextAfterCursorFormatted(int n, [GeneratedEnum] GetTextFlags flags) + { + var end = Math.Min(_editBuffer.Selection.End, _editBuffer.Text.Length); + return new SpannableString(_editBuffer.Text.Substring(end, Math.Min(n, _editBuffer.Text.Length - end))); + } + + public ICharSequence? GetTextBeforeCursorFormatted(int n, [GeneratedEnum] GetTextFlags flags) + { + var start = Math.Max(0, _editBuffer.Selection.Start - n); + var length = _editBuffer.Selection.Start - start; + return _editBuffer.Text == null ? null : new SpannableString(_editBuffer.Text.Substring(start, length)); + } + + public bool PerformPrivateCommand(string? action, Bundle? data) + { + return false; + } + + public bool ReportFullscreenMode(bool enabled) + { + return false; + } + + public bool RequestCursorUpdates(int cursorUpdateMode) + { + return false; + } + + public bool SendKeyEvent(KeyEvent? e) + { + _inputMethod.View.DispatchKeyEvent(e); + + return true; + } + + private void QueueCommand(EditCommand command) + { + BeginBatchEdit(); + + try + { + _commandQueue.Enqueue(command); + } + finally + { + EndBatchEdit(); + } + } + } +} diff --git a/src/Android/Avalonia.Android/Platform/Input/EditCommand.cs b/src/Android/Avalonia.Android/Platform/Input/EditCommand.cs new file mode 100644 index 0000000000..1e9ce5f19f --- /dev/null +++ b/src/Android/Avalonia.Android/Platform/Input/EditCommand.cs @@ -0,0 +1,181 @@ +using System; +using Avalonia.Input.TextInput; + +namespace Avalonia.Android.Platform.Input +{ + internal abstract class EditCommand + { + public abstract void Apply(TextEditBuffer buffer); + } + + internal class SelectionCommand : EditCommand + { + private readonly int _start; + private readonly int _end; + + public SelectionCommand(int start, int end) + { + _start = Math.Min(start, end); + _end = Math.Max(start, end); + } + + public override void Apply(TextEditBuffer buffer) + { + buffer.Selection = new TextSelection(Math.Max(_start, 0), Math.Min(_end, buffer.Text.Length)); + } + } + + internal class CompositionRegionCommand : EditCommand + { + private readonly int _start; + private readonly int _end; + + public CompositionRegionCommand(int start, int end) + { + _start = Math.Min(start, end); + _end = Math.Max(start, end); + } + + public override void Apply(TextEditBuffer buffer) + { + buffer.Composition = new TextSelection(_start, _end); + } + } + + internal class DeleteRegionCommand : EditCommand + { + private readonly int _before; + private readonly int _after; + + public DeleteRegionCommand(int before, int after) + { + _before = before; + _after = after; + } + + public override void Apply(TextEditBuffer buffer) + { + var end = Math.Min(buffer.Text.Length, buffer.Selection.End + _after); + var endCount = end - buffer.Selection.End; + var start = Math.Max(0, buffer.Selection.Start - _before); + buffer.Remove(buffer.Selection.End, endCount); + buffer.Remove(start, buffer.Selection.Start - start); + buffer.Selection = new TextSelection(start, start); + } + } + + internal class DeleteRegionInCodePointsCommand : EditCommand + { + private readonly int _before; + private readonly int _after; + + public DeleteRegionInCodePointsCommand(int before, int after) + { + _before = before; + _after = after; + } + + public override void Apply(TextEditBuffer buffer) + { + var beforeLengthInChar = 0; + + for (int i = 0; i < _before; i++) + { + beforeLengthInChar++; + if (buffer.Selection.Start > beforeLengthInChar) + { + var lead = buffer.Text[buffer.Selection.Start - beforeLengthInChar - 1]; + var trail = buffer.Text[buffer.Selection.Start - beforeLengthInChar]; + + if (char.IsSurrogatePair(lead, trail)) + { + beforeLengthInChar++; + } + } + + if (beforeLengthInChar == buffer.Selection.Start) + break; + } + + var afterLengthInChar = 0; + for (int i = 0; i < _after; i++) + { + afterLengthInChar++; + if (buffer.Selection.End > afterLengthInChar) + { + var lead = buffer.Text[buffer.Selection.End + afterLengthInChar - 1]; + var trail = buffer.Text[buffer.Selection.End + afterLengthInChar]; + + if (char.IsSurrogatePair(lead, trail)) + { + afterLengthInChar++; + } + } + + if (buffer.Selection.End + afterLengthInChar == buffer.Text.Length) + break; + } + + var start = buffer.Selection.Start - beforeLengthInChar; + buffer.Remove(buffer.Selection.End, afterLengthInChar); + buffer.Remove(start, beforeLengthInChar); + buffer.Selection = new TextSelection(start, start); + } + } + + internal class CompositionTextCommand : EditCommand + { + private readonly string _text; + private readonly int _newCursorPosition; + + public CompositionTextCommand(string text, int newCursorPosition) + { + _text = text; + _newCursorPosition = newCursorPosition; + } + + public override void Apply(TextEditBuffer buffer) + { + buffer.ComposingText = _text; + var newCursor = _newCursorPosition > 0 ? buffer.Selection.Start + _newCursorPosition - 1 : buffer.Selection.Start + _newCursorPosition; + buffer.Selection = new TextSelection(newCursor, newCursor); + } + } + + internal class CommitTextCommand : EditCommand + { + private readonly string _text; + private readonly int _newCursorPosition; + + public CommitTextCommand(string text, int newCursorPosition) + { + _text = text; + _newCursorPosition = newCursorPosition; + } + + public override void Apply(TextEditBuffer buffer) + { + if (buffer.HasComposition) + { + buffer.Remove(buffer.Composition!.Value.Start, buffer.Composition!.Value.End - buffer.Composition!.Value.Start); + buffer.Insert(buffer.Composition!.Value.Start, _text); + } + else + { + buffer.Remove(buffer.Selection.Start, buffer.Selection.End - buffer.Selection.Start); + buffer.Insert(buffer.Selection.Start, _text); + + } + var newCursor = _newCursorPosition > 0 ? buffer.Selection.Start + _newCursorPosition - 1 : buffer.Selection.Start + _newCursorPosition - _text.Length; + buffer.Selection = new TextSelection(newCursor, newCursor); + } + } + + internal class FinishComposingCommand : EditCommand + { + public override void Apply(TextEditBuffer buffer) + { + buffer.Composition = default; + } + } +} diff --git a/src/Android/Avalonia.Android/Platform/Input/TextEditBuffer.cs b/src/Android/Avalonia.Android/Platform/Input/TextEditBuffer.cs new file mode 100644 index 0000000000..21dea56fb4 --- /dev/null +++ b/src/Android/Avalonia.Android/Platform/Input/TextEditBuffer.cs @@ -0,0 +1,102 @@ +using System; +using Android.Views; +using Avalonia.Android.Platform.SkiaPlatform; +using Avalonia.Input.TextInput; + +namespace Avalonia.Android.Platform.Input +{ + internal class TextEditBuffer + { + private readonly IAndroidInputMethod _textInputMethod; + private readonly TopLevelImpl _topLevel; + private TextSelection? _composition; + + public TextEditBuffer(IAndroidInputMethod textInputMethod, TopLevelImpl topLevel) + { + _textInputMethod = textInputMethod; + _topLevel = topLevel; + } + + public bool HasComposition => Composition is { } composition && composition.Start != composition.End; + + public TextSelection Selection + { + get => _textInputMethod.Client?.Selection ?? default; set + { + if (_textInputMethod.Client is { } client) + client.Selection = value; + } + } + + public TextSelection? Composition + { + get => _composition; set + { + if (value is { } v) + { + var text = Text; + var start = Math.Clamp(v.Start, 0, text.Length); + var end = Math.Clamp(v.End, 0, text.Length); + _composition = new TextSelection(start, end); + } + else + _composition = null; + } + } + + public string? SelectedText + { + get + { + if(_textInputMethod.Client is not { } client || Selection.Start < 0 || Selection.End >= client.SurroundingText.Length) + { + return ""; + } + + return client.SurroundingText.Substring(Selection.Start, Selection.End - Selection.Start); + } + } + + public string? ComposingText + { + get => !HasComposition ? null : Text?.Substring(Composition!.Value.Start, Composition!.Value.End - Composition!.Value.Start); set + { + if (HasComposition) + { + var start = Composition!.Value.Start; + Remove(start, Composition!.Value.End - start); + Insert(start, value ?? ""); + Composition = new TextSelection(start, start + (value?.Length ?? 0)); + } + else + { + var start = Selection.Start; + Remove(start, Selection.End - start); + Insert(start, value ?? ""); + Composition = new TextSelection(start, start + (value?.Length ?? 0)); + } + } + } + + public string Text => _textInputMethod.Client?.SurroundingText ?? ""; + + internal void Insert(int index, string text) + { + if (_textInputMethod.Client is { } client) + { + client.Selection = new TextSelection(index, index); + _topLevel.TextInput(text); + } + } + + internal void Remove(int index, int length) + { + if (_textInputMethod.Client is { } client) + { + client.Selection = new TextSelection(index, index + length); + if (length > 0) + _textInputMethod?.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel)); + } + } + } +} diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 79052bf9bd..8b0d80a416 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Threading; using Android.App; using Android.Content; using Android.Graphics; @@ -438,273 +437,4 @@ namespace Avalonia.Android.Platform.SkiaPlatform } } } - - internal class EditableWrapper : SpannableStringBuilder - { - private readonly AvaloniaInputConnection _inputConnection; - - public EditableWrapper(AvaloniaInputConnection inputConnection) - { - _inputConnection = inputConnection; - } - - public TextSelection CurrentSelection => new TextSelection(Selection.GetSelectionStart(this), Selection.GetSelectionEnd(this)); - public TextSelection CurrentComposition => new TextSelection(BaseInputConnection.GetComposingSpanStart(this), BaseInputConnection.GetComposingSpanEnd(this)); - - public bool IgnoreChange { get; set; } - - public override IEditable? Replace(int start, int end, ICharSequence? tb) - { - if (!IgnoreChange && start != end) - { - SelectSurroundingTextForDeletion(start, end); - } - - return base.Replace(start, end, tb); - } - - public override IEditable? Replace(int start, int end, ICharSequence? tb, int tbstart, int tbend) - { - if (!IgnoreChange && start != end) - { - SelectSurroundingTextForDeletion(start, end); - } - - return base.Replace(start, end, tb, tbstart, tbend); - } - - private void SelectSurroundingTextForDeletion(int start, int end) - { - _inputConnection.InputMethod.Client!.Selection = new TextSelection(start, end); - } - } - - internal class AvaloniaInputConnection : BaseInputConnection - { - private readonly TopLevelImpl _toplevel; - private readonly IAndroidInputMethod _inputMethod; - private readonly EditableWrapper _editable; - private bool _commitInProgress; - private int _batchLevel = 0; - - public AvaloniaInputConnection(TopLevelImpl toplevel, IAndroidInputMethod inputMethod) : base(inputMethod.View, true) - { - _toplevel = toplevel; - _inputMethod = inputMethod; - _editable = new EditableWrapper(this); - } - - public int ExtractedTextToken { get; private set; } - - public override IEditable Editable => _editable; - - public EditableWrapper EditableWrapper => _editable; - - public IAndroidInputMethod InputMethod => _inputMethod; - - public TopLevelImpl Toplevel => _toplevel; - - public bool IsInBatchEdit => _batchLevel > 0; - - public override bool SetComposingRegion(int start, int end) - { - return base.SetComposingRegion(start, end); - } - - public override bool SetComposingText(ICharSequence? text, int newCursorPosition) - { - if (InputMethod.Client is null || text is null) - { - return false; - } - - BeginBatchEdit(); - _editable.IgnoreChange = true; - - try - { - if (_editable.CurrentComposition.Start > -1) - { - // Select the composing region. - InputMethod.Client.Selection = new TextSelection(_editable.CurrentComposition.Start, _editable.CurrentComposition.End); - } - var compositionText = text.SubSequence(0, text.Length()); - - if (_inputMethod.IsActive && !_commitInProgress) - { - if (string.IsNullOrEmpty(compositionText)) - { - if (_editable.CurrentComposition.Start > -1) - _inputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel)); - } - else - _toplevel.TextInput(compositionText); - } - base.SetComposingText(text, newCursorPosition); - } - finally - { - _editable.IgnoreChange = false; - - EndBatchEdit(); - } - - return true; - } - - public override bool BeginBatchEdit() - { - _batchLevel = Interlocked.Increment(ref _batchLevel); - return base.BeginBatchEdit(); - } - - public override bool EndBatchEdit() - { - _batchLevel = Interlocked.Decrement(ref _batchLevel); - - _inputMethod.OnBatchEditedEnded(); - return base.EndBatchEdit(); - } - - public override bool CommitText(ICharSequence? text, int newCursorPosition) - { - if (InputMethod.Client is null || text is null) - { - return false; - } - - BeginBatchEdit(); - _commitInProgress = true; - - var composingRegion = _editable.CurrentComposition; - - var ret = base.CommitText(text, newCursorPosition); - - if(composingRegion.Start != -1) - { - InputMethod.Client.Selection = composingRegion; - } - - var committedText = text.SubSequence(0, text.Length()); - - if (_inputMethod.IsActive) - if (string.IsNullOrEmpty(committedText)) - _inputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel)); - else - _toplevel.TextInput(committedText); - - _commitInProgress = false; - EndBatchEdit(); - - return true; - } - - public override bool DeleteSurroundingText(int beforeLength, int afterLength) - { - if (InputMethod.IsActive) - { - EditableWrapper.IgnoreChange = true; - } - - if (InputMethod.IsActive) - { - var selection = _editable.CurrentSelection; - - InputMethod.Client.Selection = new TextSelection(selection.Start - beforeLength, selection.End + afterLength); - - InputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel)); - - EditableWrapper.IgnoreChange = true; - } - - return true; - } - - public override bool PerformEditorAction([GeneratedEnum] ImeAction actionCode) - { - switch (actionCode) - { - case ImeAction.Done: - { - _inputMethod.IMM.HideSoftInputFromWindow(_inputMethod.View.WindowToken, HideSoftInputFlags.ImplicitOnly); - break; - } - case ImeAction.Next: - { - FocusManager.GetFocusManager(_toplevel.InputRoot)? - .TryMoveFocus(NavigationDirection.Next); - break; - } - } - - return base.PerformEditorAction(actionCode); - } - - public override ExtractedText? GetExtractedText(ExtractedTextRequest? request, [GeneratedEnum] GetTextFlags flags) - { - if (request == null) - return null; - - ExtractedTextToken = request.Token; - - var editable = Editable; - - if (editable == null) - { - return null; - } - - if (!_inputMethod.IsActive) - { - return null; - } - - var selection = _editable.CurrentSelection; - - ExtractedText extract = new ExtractedText - { - Flags = 0, - PartialStartOffset = -1, - PartialEndOffset = -1, - SelectionStart = selection.Start, - SelectionEnd = selection.End, - StartOffset = 0 - }; - - if ((request.Flags & GetTextFlags.WithStyles) != 0) - { - extract.Text = new SpannableString(editable); - } - else - { - extract.Text = editable; - } - - return extract; - } - - public override bool PerformContextMenuAction(int id) - { - if (InputMethod.Client is not { } client) return false; - - switch (id) - { - case global::Android.Resource.Id.SelectAll: - client.ExecuteContextMenuAction(ContextMenuAction.SelectAll); - return true; - case global::Android.Resource.Id.Cut: - client.ExecuteContextMenuAction(ContextMenuAction.Cut); - return true; - case global::Android.Resource.Id.Copy: - client.ExecuteContextMenuAction(ContextMenuAction.Copy); - return true; - case global::Android.Resource.Id.Paste: - client.ExecuteContextMenuAction(ContextMenuAction.Paste); - return true; - default: - break; - } - return base.PerformContextMenuAction(id); - } - } }