using System; using Android.Content; 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 { internal interface IAndroidInputMethod { public View View { get; } public TextInputMethodClient Client { get; } public bool IsActive { get; } public InputMethodManager IMM { get; } } enum CustomImeFlags { ActionNone = 0x00000001, ActionGo = 0x00000002, ActionSearch = 0x00000003, ActionSend = 0x00000004, ActionNext = 0x00000005, ActionDone = 0x00000006, ActionPrevious = 0x00000007, } internal class AndroidInputMethod : ITextInputMethodImpl, IAndroidInputMethod where TView : View, IInitEditorInfo { private readonly TView _host; private readonly InputMethodManager _imm; private TextInputMethodClient _client; private AvaloniaInputConnection _inputConnection; public AndroidInputMethod(TView host) { if (host.OnCheckIsTextEditor() == false) throw new InvalidOperationException("Host should return true from OnCheckIsTextEditor()"); _host = host; _imm = host.Context.GetSystemService(Context.InputMethodService).JavaCast(); _host.Focusable = true; _host.FocusableInTouchMode = true; _host.ViewTreeObserver.AddOnGlobalLayoutListener(new SoftKeyboardListener(_host)); } public View View => _host; public bool IsActive => Client != null; public TextInputMethodClient Client => _client; public InputMethodManager IMM => _imm; public void Reset() { } public void SetClient(TextInputMethodClient client) { _client = client; if (IsActive) { _host.RequestFocus(); _imm.RestartInput(View); _imm.ShowSoftInput(_host, ShowFlags.Implicit); var selection = Client.Selection; _imm.UpdateSelection(_host, selection.Start, selection.End, selection.Start, selection.End); var surroundingText = _client.SurroundingText ?? ""; var extractedText = new ExtractedText { Text = new Java.Lang.String(surroundingText), SelectionStart = selection.Start, SelectionEnd = selection.End, PartialEndOffset = surroundingText.Length }; _imm.UpdateExtractedText(_host, _inputConnection?.ExtractedTextToken ?? 0, extractedText); _client.SurroundingTextChanged += _client_SurroundingTextChanged; _client.SelectionChanged += _client_SelectionChanged; } else { _imm.HideSoftInputFromWindow(_host.WindowToken, HideSoftInputFlags.ImplicitOnly); } } private void _client_SelectionChanged(object sender, EventArgs e) { var selection = Client.Selection; _imm.UpdateSelection(_host, selection.Start, selection.End, selection.Start, selection.End); } private void _client_SurroundingTextChanged(object sender, EventArgs e) { var surroundingText = _client.SurroundingText ?? ""; _inputConnection.EditableWrapper.IgnoreChange = true; _inputConnection.Editable.Replace(0, _inputConnection.Editable.Length(), surroundingText); _inputConnection.EditableWrapper.IgnoreChange = false; var selection = Client.Selection; _imm.UpdateSelection(_host, selection.Start, selection.End, selection.Start, selection.End); //Debug.WriteLine($"SurroundingText: {surroundingText}, CaretIndex: {selection.Start}"); } public void SetCursorRect(Rect rect) { } public void SetOptions(TextInputOptions options) { _host.InitEditorInfo((topLevel, outAttrs) => { if (_client == null) return null; _inputConnection = new AvaloniaInputConnection(topLevel, this); 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 }; if (options.AutoCapitalization) { outAttrs.InitialCapsMode = global::Android.Text.CapitalizationMode.Sentences; outAttrs.InputType |= global::Android.Text.InputTypes.TextFlagCapSentences; } if (options.Multiline) outAttrs.InputType |= global::Android.Text.InputTypes.TextFlagMultiLine; outAttrs.ImeOptions = options.ReturnKeyType switch { TextInputReturnKeyType.Return => ImeFlags.NoEnterAction, TextInputReturnKeyType.Go => (ImeFlags)CustomImeFlags.ActionGo, TextInputReturnKeyType.Send => (ImeFlags)CustomImeFlags.ActionSend, TextInputReturnKeyType.Search => (ImeFlags)CustomImeFlags.ActionSearch, TextInputReturnKeyType.Next => (ImeFlags)CustomImeFlags.ActionNext, TextInputReturnKeyType.Previous => (ImeFlags)CustomImeFlags.ActionPrevious, TextInputReturnKeyType.Done => (ImeFlags)CustomImeFlags.ActionDone, _ => options.Multiline ? ImeFlags.NoEnterAction : (ImeFlags)CustomImeFlags.ActionDone }; outAttrs.ImeOptions |= ImeFlags.NoFullscreen | ImeFlags.NoExtractUi; return _inputConnection; }); } } }