diff --git a/src/Android/Avalonia.Android/AndroidInputMethod.cs b/src/Android/Avalonia.Android/AndroidInputMethod.cs index ea1958efa6..7d5130cf5d 100644 --- a/src/Android/Avalonia.Android/AndroidInputMethod.cs +++ b/src/Android/Avalonia.Android/AndroidInputMethod.cs @@ -18,6 +18,8 @@ namespace Avalonia.Android public bool IsActive { get; } public InputMethodManager IMM { get; } + + void OnBatchEditedEnded(); } enum CustomImeFlags @@ -103,6 +105,13 @@ namespace Avalonia.Android } private void _client_SelectionChanged(object sender, EventArgs e) + { + if (_inputConnection.IsInBatchEdit) + return; + OnSelectionChanged(); + } + + private void OnSelectionChanged() { var selection = Client.Selection; @@ -113,17 +122,67 @@ namespace Avalonia.Android private void _client_SurroundingTextChanged(object sender, EventArgs e) { + if (_inputConnection.IsInBatchEdit) + return; + OnSurroundingTextChanged(); + } + + public void OnBatchEditedEnded() + { + if (_inputConnection.IsInBatchEdit) + return; + + OnSurroundingTextChanged(); + OnSelectionChanged(); + } + + private void OnSurroundingTextChanged() + { + if(_client is null) + { + return; + } + var surroundingText = _client.SurroundingText ?? ""; + var editableText = _inputConnection.EditableWrapper.ToString(); - _inputConnection.EditableWrapper.IgnoreChange = true; + if (editableText != surroundingText) + { + _inputConnection.EditableWrapper.IgnoreChange = true; - _inputConnection.Editable.Replace(0, _inputConnection.Editable.Length(), surroundingText); + var diff = GetDiff(); - _inputConnection.EditableWrapper.IgnoreChange = false; + _inputConnection.Editable.Replace(diff.index, editableText.Length, diff.diff); - var selection = Client.Selection; + _inputConnection.EditableWrapper.IgnoreChange = false; - _imm.UpdateSelection(_host, selection.Start, selection.End, selection.Start, selection.End); + 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 longerLength = Math.Max(surroundingText.Length, editableText.Length); + + for (int i = 0; i < longerLength; i++) + { + if (surroundingText.Length == i || editableText.Length == i || surroundingText[i] != editableText[i]) + { + index = i; + break; + } + } + + var diffString = surroundingText.Substring(index, surroundingText.Length - index); + + return (index, diffString); + } } public void SetCursorRect(Rect rect) diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 5d82d099c9..a6e8d10777 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Runtime.Versioning; +using System.Threading; using Android.App; using Android.Content; using Android.Graphics; @@ -447,19 +448,22 @@ namespace Avalonia.Android.Platform.SkiaPlatform { private readonly AvaloniaInputConnection _inputConnection; + public event EventHandler SelectionChanged; + 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) { - var text = tb.SubSequence(0, tb.Length()); - SelectSurroundingTextForDeletion(start, end); } @@ -470,8 +474,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform { if (!IgnoreChange && start != end) { - var text = tb.SubSequence(tbstart, tbend); - SelectSurroundingTextForDeletion(start, end); } @@ -490,8 +492,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform private readonly IAndroidInputMethod _inputMethod; private readonly EditableWrapper _editable; private bool _commitInProgress; - private (int Start, int End)? _composingRegion; - private TextSelection _selection; + private int _batchLevel = 0; public AvaloniaInputConnection(TopLevelImpl toplevel, IAndroidInputMethod inputMethod) : base(inputMethod.View, true) { @@ -510,63 +511,87 @@ namespace Avalonia.Android.Platform.SkiaPlatform public TopLevelImpl Toplevel => _toplevel; + public bool IsInBatchEdit => _batchLevel > 0; + public override bool SetComposingRegion(int start, int end) { - _composingRegion = new(start, end); - return base.SetComposingRegion(start, end); } public override bool SetComposingText(ICharSequence text, int newCursorPosition) { - if(_composingRegion != null) + BeginBatchEdit(); + _editable.IgnoreChange = true; + + try { - // Select the composing region. - InputMethod.Client.Selection = new TextSelection(_composingRegion.Value.Start, _composingRegion.Value.End); - } - var compositionText = text.SubSequence(0, text.Length()); + 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 (_inputMethod.IsActive && !_commitInProgress) + { + if (string.IsNullOrEmpty(compositionText)) + _inputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel)); + + else + _toplevel.TextInput(compositionText); + } + base.SetComposingText(text, newCursorPosition); + } + finally { - if (string.IsNullOrEmpty(compositionText)) - _inputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel)); + _editable.IgnoreChange = false; - else - _toplevel.TextInput(compositionText); + 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) { + BeginBatchEdit(); _commitInProgress = true; - var ret = base.CommitText(text, newCursorPosition); + var composingRegion = _editable.CurrentComposition; - var committedText = text.SubSequence(0, text.Length()); + var ret = base.CommitText(text, newCursorPosition); - if (_inputMethod.IsActive && !string.IsNullOrEmpty(committedText)) + if(composingRegion.Start != -1) { - if(_composingRegion != null) - { - _inputMethod.Client.Selection = new TextSelection(_composingRegion.Value.Start, _composingRegion.Value.End); - } + InputMethod.Client.Selection = composingRegion; + } - _toplevel.TextInput(committedText); + var committedText = text.SubSequence(0, text.Length()); - _composingRegion = null; - } + if (_inputMethod.IsActive) + if (string.IsNullOrEmpty(committedText)) + _inputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel)); + else + _toplevel.TextInput(committedText); _commitInProgress = false; + EndBatchEdit(); - return ret; - } - - public override bool FinishComposingText() - { - _composingRegion = null; - return base.FinishComposingText(); + return true; } public override bool DeleteSurroundingText(int beforeLength, int afterLength) @@ -575,11 +600,10 @@ namespace Avalonia.Android.Platform.SkiaPlatform { EditableWrapper.IgnoreChange = true; } - var result = base.DeleteSurroundingText(beforeLength, afterLength); if (InputMethod.IsActive) { - var selection = _selection; + var selection = _editable.CurrentSelection; InputMethod.Client.Selection = new TextSelection(selection.Start - beforeLength, selection.End + afterLength); @@ -588,13 +612,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform EditableWrapper.IgnoreChange = true; } - return result; - } - - public override bool SetSelection(int start, int end) - { - _selection = new TextSelection(start, end); - return base.SetSelection(start, end); + return true; } public override bool PerformEditorAction([GeneratedEnum] ImeAction actionCode) @@ -630,7 +648,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform return null; } - var selection = _selection; + var selection = _editable.CurrentSelection; ExtractedText extract = new ExtractedText {