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