Browse Source

refactor android input connection (#16490)

pull/16575/head
Emmanuel Hansen 2 years ago
committed by GitHub
parent
commit
20fe710681
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 101
      src/Android/Avalonia.Android/Platform/Input/AndroidInputMethod.cs
  2. 310
      src/Android/Avalonia.Android/Platform/Input/AvaloniaInputConnection.cs
  3. 181
      src/Android/Avalonia.Android/Platform/Input/EditCommand.cs
  4. 102
      src/Android/Avalonia.Android/Platform/Input/TextEditBuffer.cs
  5. 270
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

101
src/Android/Avalonia.Android/AndroidInputMethod.cs → 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<TView> : 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
{

310
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<EditCommand> _commandQueue;
private int _batchLevel = 0;
public AvaloniaInputConnection(TopLevelImpl toplevel, IAndroidInputMethod inputMethod)
{
_toplevel = toplevel;
_inputMethod = inputMethod;
_editBuffer = new TextEditBuffer(_inputMethod, toplevel);
_commandQueue = new ConcurrentQueue<EditCommand>();
}
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();
}
}
}
}

181
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;
}
}
}

102
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));
}
}
}
}

270
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);
}
}
}

Loading…
Cancel
Save