committed by
GitHub
5 changed files with 632 additions and 332 deletions
@ -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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue