From 368de7e79f5bb8d5870b4aea530633a8534a70d5 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Mon, 17 Jul 2023 13:18:24 +0000 Subject: [PATCH 1/7] clean up; apply only changed text to editable --- .../Avalonia.Android/AndroidInputMethod.cs | 32 +++++++++++++++++-- .../Platform/SkiaPlatform/TopLevelImpl.cs | 16 ++++------ 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/Android/Avalonia.Android/AndroidInputMethod.cs b/src/Android/Avalonia.Android/AndroidInputMethod.cs index ea1958efa6..dea029aaf2 100644 --- a/src/Android/Avalonia.Android/AndroidInputMethod.cs +++ b/src/Android/Avalonia.Android/AndroidInputMethod.cs @@ -114,16 +114,42 @@ namespace Avalonia.Android private void _client_SurroundingTextChanged(object sender, EventArgs e) { var surroundingText = _client.SurroundingText ?? ""; + var editableText = _inputConnection.EditableWrapper.ToString(); - _inputConnection.EditableWrapper.IgnoreChange = true; + if (editableText != surroundingText) + { + _inputConnection.EditableWrapper.IgnoreChange = true; + + var diff = GetDiff(); - _inputConnection.Editable.Replace(0, _inputConnection.Editable.Length(), surroundingText); + _inputConnection.Editable.Replace(diff.index, editableText.Length, diff.diff); - _inputConnection.EditableWrapper.IgnoreChange = false; + _inputConnection.EditableWrapper.IgnoreChange = false; + } var selection = Client.Selection; _imm.UpdateSelection(_host, selection.Start, selection.End, selection.Start, selection.End); + + (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..5492742a4c 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -447,6 +447,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform { private readonly AvaloniaInputConnection _inputConnection; + public event EventHandler SelectionChanged; + public EditableWrapper(AvaloniaInputConnection inputConnection) { _inputConnection = inputConnection; @@ -458,8 +460,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform { if (!IgnoreChange && start != end) { - var text = tb.SubSequence(0, tb.Length()); - SelectSurroundingTextForDeletion(start, end); } @@ -470,8 +470,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform { if (!IgnoreChange && start != end) { - var text = tb.SubSequence(tbstart, tbend); - SelectSurroundingTextForDeletion(start, end); } @@ -482,6 +480,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform { _inputConnection.InputMethod.Client.Selection = new TextSelection(start, end); } + + public override void SetSpan(Java.Lang.Object what, int start, int end, [GeneratedEnum] SpanTypes flags) + { + base.SetSpan(what, start, end, flags); + } } internal class AvaloniaInputConnection : BaseInputConnection @@ -548,11 +551,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform if (_inputMethod.IsActive && !string.IsNullOrEmpty(committedText)) { - if(_composingRegion != null) - { - _inputMethod.Client.Selection = new TextSelection(_composingRegion.Value.Start, _composingRegion.Value.End); - } - _toplevel.TextInput(committedText); _composingRegion = null; From 117ea16f0ba8d0099cd96875336f82a9e1082030 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Mon, 17 Jul 2023 17:24:19 +0000 Subject: [PATCH 2/7] remove redundant selection update --- src/Android/Avalonia.Android/AndroidInputMethod.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Android/Avalonia.Android/AndroidInputMethod.cs b/src/Android/Avalonia.Android/AndroidInputMethod.cs index dea029aaf2..1404adbae0 100644 --- a/src/Android/Avalonia.Android/AndroidInputMethod.cs +++ b/src/Android/Avalonia.Android/AndroidInputMethod.cs @@ -127,10 +127,6 @@ namespace Avalonia.Android _inputConnection.EditableWrapper.IgnoreChange = false; } - var selection = Client.Selection; - - _imm.UpdateSelection(_host, selection.Start, selection.End, selection.Start, selection.End); - (int index, string diff) GetDiff() { int index = 0; From 24e2321bf1c15dec876fbc67f176d00c11953bf0 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Wed, 19 Jul 2023 08:04:22 +0000 Subject: [PATCH 3/7] android - use batch edits to defer updates to input manager --- .../Avalonia.Android/AndroidInputMethod.cs | 29 ++++++- .../Platform/SkiaPlatform/TopLevelImpl.cs | 85 +++++++++++-------- 2 files changed, 76 insertions(+), 38 deletions(-) diff --git a/src/Android/Avalonia.Android/AndroidInputMethod.cs b/src/Android/Avalonia.Android/AndroidInputMethod.cs index 1404adbae0..60f56805d1 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; @@ -112,6 +121,22 @@ 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() { var surroundingText = _client.SurroundingText ?? ""; var editableText = _inputConnection.EditableWrapper.ToString(); @@ -133,7 +158,7 @@ namespace Avalonia.Android var longerLength = Math.Max(surroundingText.Length, editableText.Length); - for(int i = 0; i < longerLength; i++) + for (int i = 0; i < longerLength; i++) { if (surroundingText.Length == i || editableText.Length == i || surroundingText[i] != editableText[i]) { @@ -142,7 +167,7 @@ namespace Avalonia.Android } } - var diffString = surroundingText.Substring(index, surroundingText.Length - index); + var diffString = surroundingText.Substring(index, surroundingText.Length - index); return (index, diffString); } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 5492742a4c..26b8837f76 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; @@ -454,6 +455,9 @@ namespace Avalonia.Android.Platform.SkiaPlatform _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) @@ -480,11 +484,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform { _inputConnection.InputMethod.Client.Selection = new TextSelection(start, end); } - - public override void SetSpan(Java.Lang.Object what, int start, int end, [GeneratedEnum] SpanTypes flags) - { - base.SetSpan(what, start, end, flags); - } } internal class AvaloniaInputConnection : BaseInputConnection @@ -493,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) { @@ -513,36 +511,64 @@ 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 (string.IsNullOrEmpty(compositionText)) + _inputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel)); - if (_inputMethod.IsActive && !_commitInProgress) + 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); @@ -551,22 +577,15 @@ namespace Avalonia.Android.Platform.SkiaPlatform if (_inputMethod.IsActive && !string.IsNullOrEmpty(committedText)) { - _toplevel.TextInput(committedText); - - _composingRegion = null; + _toplevel.TextInput(committedText); } _commitInProgress = false; + EndBatchEdit(); return ret; } - public override bool FinishComposingText() - { - _composingRegion = null; - return base.FinishComposingText(); - } - public override bool DeleteSurroundingText(int beforeLength, int afterLength) { if (InputMethod.IsActive) @@ -577,7 +596,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform if (InputMethod.IsActive) { - var selection = _selection; + var selection = _editable.CurrentSelection; InputMethod.Client.Selection = new TextSelection(selection.Start - beforeLength, selection.End + afterLength); @@ -589,12 +608,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform return result; } - public override bool SetSelection(int start, int end) - { - _selection = new TextSelection(start, end); - return base.SetSelection(start, end); - } - public override bool PerformEditorAction([GeneratedEnum] ImeAction actionCode) { switch (actionCode) @@ -628,7 +641,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform return null; } - var selection = _selection; + var selection = _editable.CurrentSelection; ExtractedText extract = new ExtractedText { From f762be4ff6deaf4456d80a7bf44c69d1cdc871a8 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Wed, 19 Jul 2023 12:39:01 +0000 Subject: [PATCH 4/7] only handle deletion in surrounding text on the client side --- .../Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 26b8837f76..fa01cb83e3 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -583,7 +583,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform _commitInProgress = false; EndBatchEdit(); - return ret; + return true; } public override bool DeleteSurroundingText(int beforeLength, int afterLength) @@ -592,7 +592,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform { EditableWrapper.IgnoreChange = true; } - var result = base.DeleteSurroundingText(beforeLength, afterLength); if (InputMethod.IsActive) { @@ -605,7 +604,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform EditableWrapper.IgnoreChange = true; } - return result; + return true; } public override bool PerformEditorAction([GeneratedEnum] ImeAction actionCode) From fde8744175ee25ba5e231fc58a3064e94f03face Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Wed, 19 Jul 2023 15:09:53 +0000 Subject: [PATCH 5/7] delete selection when committext has null text --- .../Platform/SkiaPlatform/TopLevelImpl.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index fa01cb83e3..3b2a3657b3 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -575,10 +575,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform var committedText = text.SubSequence(0, text.Length()); - if (_inputMethod.IsActive && !string.IsNullOrEmpty(committedText)) - { - _toplevel.TextInput(committedText); - } + if (_inputMethod.IsActive) + if (string.IsNullOrEmpty(committedText)) + _inputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel)); + else + _toplevel.TextInput(committedText); _commitInProgress = false; EndBatchEdit(); From a54163f1c10fcb5dbaabd8a9b3eae379f5f53a81 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Thu, 20 Jul 2023 13:10:50 +0000 Subject: [PATCH 6/7] select current composing region if active when commiting and ensure selection is set when surrounding text changes --- src/Android/Avalonia.Android/AndroidInputMethod.cs | 7 +++++++ .../Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/src/Android/Avalonia.Android/AndroidInputMethod.cs b/src/Android/Avalonia.Android/AndroidInputMethod.cs index 60f56805d1..b6c1154455 100644 --- a/src/Android/Avalonia.Android/AndroidInputMethod.cs +++ b/src/Android/Avalonia.Android/AndroidInputMethod.cs @@ -150,6 +150,13 @@ namespace Avalonia.Android _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() diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 3b2a3657b3..a6e8d10777 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -571,8 +571,15 @@ namespace Avalonia.Android.Platform.SkiaPlatform 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) From 99f1225f57372094353870ceb60837da7cabf73b Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 20 Jul 2023 18:28:41 +0200 Subject: [PATCH 7/7] Prevent NRE --- src/Android/Avalonia.Android/AndroidInputMethod.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Android/Avalonia.Android/AndroidInputMethod.cs b/src/Android/Avalonia.Android/AndroidInputMethod.cs index b6c1154455..7d5130cf5d 100644 --- a/src/Android/Avalonia.Android/AndroidInputMethod.cs +++ b/src/Android/Avalonia.Android/AndroidInputMethod.cs @@ -138,6 +138,11 @@ namespace Avalonia.Android private void OnSurroundingTextChanged() { + if(_client is null) + { + return; + } + var surroundingText = _client.SurroundingText ?? ""; var editableText = _inputConnection.EditableWrapper.ToString();