Browse Source

Attempts to fix Android IME

Benedikt Stebner 3 years ago
parent
commit
040cccf619

+ 1 - 0
Avalonia.sln

@@ -522,6 +522,7 @@ Global
 		{3B8519C1-2F51-4F12-A348-120AB91D4532}.Release|Any CPU.Build.0 = Release|Any CPU
 		{C90FE60B-B01E-4F35-91D6-379D6966030F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{C90FE60B-B01E-4F35-91D6-379D6966030F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{C90FE60B-B01E-4F35-91D6-379D6966030F}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
 		{C90FE60B-B01E-4F35-91D6-379D6966030F}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{C90FE60B-B01E-4F35-91D6-379D6966030F}.Release|Any CPU.Build.0 = Release|Any CPU
 		{FED9A71D-00D7-4F40-A9E4-1229EEA28EEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU

+ 1 - 0
samples/MobileSandbox/MainView.xaml

@@ -3,6 +3,7 @@
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
   <StackPanel Margin="100 50" Spacing="50">
     <TextBlock Text="Login" Foreground="White" />
+    <TextBox Watermark="Text" />
     <TextBox Watermark="Username" TextInputOptions.ContentType="Email" AcceptsReturn="True" TextInputOptions.ReturnKeyType="Search" />
     <TextBox Watermark="Password" PasswordChar="*" TextInputOptions.ContentType="Password" />
     <TextBox Watermark="Pin" PasswordChar="*" TextInputOptions.ContentType="Digits" />

+ 20 - 17
src/Android/Avalonia.Android/AndroidInputMethod.cs

@@ -1,6 +1,7 @@
 using System;
 using Android.Content;
 using Android.Runtime;
+using Android.Text;
 using Android.Views;
 using Android.Views.InputMethods;
 using Avalonia.Android.Platform.SkiaPlatform;
@@ -62,22 +63,21 @@ namespace Avalonia.Android
 
         public void Reset()
         {
-            _imm.RestartInput(_host);
+
         }
 
         public void SetClient(ITextInputMethodClient client)
         {
-            if(client is null)
-            {
-                _inputConnection?.SetComposingText("", 0);
-            }
-
             if (_client != null)
             {
                 _client.SurroundingTextChanged -= SurroundingTextChanged;
             }
 
-            Reset();
+            if(_inputConnection != null)
+            {
+                _inputConnection.ComposingText = null;
+                _inputConnection.ComposingRegion = default;
+            }
 
             _client = client;
 
@@ -87,23 +87,31 @@ namespace Avalonia.Android
 
                 _host.RequestFocus();
 
+                _imm.RestartInput(View);              
+
                 _imm.ShowSoftInput(_host, ShowFlags.Implicit);
-            }
-            else
-            {
-                _imm.HideSoftInputFromWindow(_host.WindowToken, HideSoftInputFlags.None);
+
+                var surroundingText = Client.SurroundingText;
+
+                _imm.UpdateSelection(_host, surroundingText.AnchorOffset, surroundingText.CursorOffset, surroundingText.AnchorOffset, surroundingText.CursorOffset);
             }
         }
 
         private void SurroundingTextChanged(object sender, EventArgs e)
         {
-            if (IsActive)
+            if (IsActive && _inputConnection != null)
             {
                 var surroundingText = Client.SurroundingText;
 
                 _inputConnection.SurroundingText = surroundingText;
 
                 _imm.UpdateSelection(_host, surroundingText.AnchorOffset, surroundingText.CursorOffset, surroundingText.AnchorOffset, surroundingText.CursorOffset);
+
+                if (_inputConnection.ComposingText != null && !_inputConnection.IsCommiting && surroundingText.AnchorOffset == surroundingText.CursorOffset)
+                {
+                    _inputConnection.CommitText(_inputConnection.ComposingText, 0);
+                    _inputConnection.SetSelection(surroundingText.AnchorOffset, surroundingText.CursorOffset);
+                }
             }
         }
 
@@ -153,10 +161,5 @@ namespace Avalonia.Android
                 return _inputConnection;
             });
         }
-
-        private void RestoreSoftKeyboard(object sender, PointerReleasedEventArgs e)
-        {
-            _imm.ShowSoftInput(_host, ShowFlags.Implicit);
-        }
     }
 }

+ 70 - 26
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@@ -3,7 +3,9 @@ using System.Collections.Generic;
 using Android.App;
 using Android.Content;
 using Android.Graphics;
+using Android.OS;
 using Android.Runtime;
+using Android.Text;
 using Android.Views;
 using Android.Views.InputMethods;
 using Avalonia.Android.OpenGL;
@@ -23,6 +25,7 @@ using Avalonia.Platform.Storage;
 using Avalonia.Rendering;
 using Avalonia.Rendering.Composition;
 using Java.Lang;
+using Math = System.Math;
 
 namespace Avalonia.Android.Platform.SkiaPlatform
 {
@@ -254,8 +257,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform
         {
             throw new NotImplementedException();
         }
-
-       
     }
 
     internal class AvaloniaInputConnection : BaseInputConnection
@@ -267,15 +268,18 @@ namespace Avalonia.Android.Platform.SkiaPlatform
         {
             _topLevel = topLevel;
             _inputMethod = inputMethod;
+
+            _inputMethod.Client?.SetComposingRegion(null);
         }
 
         public TextInputMethodSurroundingText SurroundingText { get; set; }
 
-        public string ComposingText { get; private set; }
+        public string ComposingText { get; internal set; }
 
-        public ComposingRegion ComposingRegion { get; private set; }
+        public ComposingRegion? ComposingRegion { get; internal set; }
 
-        public bool IsComposing { get; private set; }
+        public bool IsComposing => !string.IsNullOrEmpty(ComposingText);
+        public bool IsCommiting { get; private set; }
 
         public override bool SetComposingRegion(int start, int end)
         {
@@ -283,6 +287,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
 
             ComposingRegion = new ComposingRegion(start, end);
 
+            _inputMethod.Client?.SetComposingRegion(ComposingRegion);
+
             return base.SetComposingRegion(start, end);
         }
 
@@ -292,8 +298,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform
 
             ComposingText = composingText;
 
-            IsComposing = true;
-
             _inputMethod.Client?.SetPreeditText(ComposingText);
 
             return base.SetComposingText(text, newCursorPosition);
@@ -301,20 +305,25 @@ namespace Avalonia.Android.Platform.SkiaPlatform
 
         public override bool FinishComposingText()
         {
-            IsComposing = false;
-
-            ComposingRegion = new ComposingRegion(SurroundingText.CursorOffset, SurroundingText.CursorOffset);
+            if (!string.IsNullOrEmpty(ComposingText))
+            {
+                CommitText(ComposingText, ComposingText.Length);
+            }
+            else
+            {
+                ComposingRegion = new ComposingRegion(SurroundingText.CursorOffset, SurroundingText.CursorOffset);
+            }
 
             return base.FinishComposingText();
         }
 
         public override ICharSequence GetTextBeforeCursorFormatted(int length, [GeneratedEnum] GetTextFlags flags)
         {
-            if (!string.IsNullOrEmpty(SurroundingText.Text))
+            if (!string.IsNullOrEmpty(SurroundingText.Text) && length > 0)
             {
                 var start = System.Math.Max(SurroundingText.CursorOffset - length, 0);
 
-                var end = System.Math.Min(start + length, SurroundingText.CursorOffset);
+                var end = System.Math.Min(start + length - 1, SurroundingText.CursorOffset);
 
                 var text = SurroundingText.Text.Substring(start, end - start);
 
@@ -346,11 +355,31 @@ namespace Avalonia.Android.Platform.SkiaPlatform
 
         public override bool CommitText(ICharSequence text, int newCursorPosition)
         {
+            IsCommiting = true;
             var committedText = text.ToString();
 
             _inputMethod.Client.SetPreeditText(null);
 
-            _inputMethod.Client.SelectInSurroundingText(ComposingRegion.Start, ComposingRegion.End);
+            int? start, end;
+
+            if(SurroundingText.CursorOffset != SurroundingText.AnchorOffset)
+            {
+                start = Math.Min(SurroundingText.CursorOffset, SurroundingText.AnchorOffset);
+                end = Math.Max(SurroundingText.CursorOffset, SurroundingText.AnchorOffset);
+            }
+            else if (ComposingRegion != null)
+            {
+                start = ComposingRegion?.Start;
+                end = ComposingRegion?.End;
+
+                ComposingRegion = null;
+            }
+            else
+            {
+                start = end = _inputMethod.Client.SurroundingText.CursorOffset;
+            }
+
+            _inputMethod.Client.SelectInSurroundingText((int)start, (int)end);
 
             var time = DateTime.Now.TimeOfDay;
 
@@ -358,15 +387,29 @@ namespace Avalonia.Android.Platform.SkiaPlatform
 
             _topLevel.Input(rawTextEvent);
 
+            ComposingText = null;
+
+            ComposingRegion = new ComposingRegion(newCursorPosition, newCursorPosition);
+
             return base.CommitText(text, newCursorPosition);
         }
 
         public override bool DeleteSurroundingText(int beforeLength, int afterLength)
         {
-            _inputMethod.Client.SelectInSurroundingText(beforeLength, afterLength);
+            var surroundingText = _inputMethod.Client.SurroundingText;
+
+            var selectionStart = surroundingText.CursorOffset;
+
+            _inputMethod.Client.SelectInSurroundingText(selectionStart - beforeLength, selectionStart + afterLength);
 
             _inputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel));
 
+            surroundingText = _inputMethod.Client.SurroundingText;
+
+            selectionStart = surroundingText.CursorOffset;
+
+            ComposingRegion = new ComposingRegion(selectionStart, selectionStart);
+
             return base.DeleteSurroundingText(beforeLength, afterLength);
         }
 
@@ -374,22 +417,23 @@ namespace Avalonia.Android.Platform.SkiaPlatform
         {
             _inputMethod.Client.SelectInSurroundingText(start, end);
 
+            ComposingRegion = new ComposingRegion(start, end);
+
             return base.SetSelection(start, end);
         }
-    }
-
-    public readonly struct ComposingRegion
-    {
-        private readonly int _start = -1;
-        private readonly int _end = -1;
 
-        public ComposingRegion(int start, int end)
+        public override bool PerformEditorAction([GeneratedEnum] ImeAction actionCode)
         {
-            _start = start;
-            _end = end;
-        }
+            switch (actionCode)
+            {
+                case ImeAction.Done:
+                    {
+                        _inputMethod.IMM.HideSoftInputFromWindow(_inputMethod.View.WindowToken, HideSoftInputFlags.ImplicitOnly);
+                        break;
+                    }
+            }
 
-        public int Start => _start;
-        public int End => _end;
+            return base.PerformEditorAction(actionCode);
+        }
     }
 }

+ 1 - 1
src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs

@@ -63,7 +63,7 @@ namespace Avalonia.Android.Platform.Specific.Helpers
             {
                 var rawTextEvent = new RawTextInputEventArgs(
                   AndroidKeyboardDevice.Instance,
-                  Convert.ToUInt32(e.EventTime),
+                  Convert.ToUInt64(e.EventTime),
                   _view.InputRoot,
                   unicodeTextInput ?? Convert.ToChar(e.UnicodeChar).ToString()
                   );

+ 24 - 0
src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs

@@ -29,6 +29,8 @@ namespace Avalonia.Input.TextInput
         /// Sets the non-committed input string
         /// </summary>
         void SetPreeditText(string? text);
+
+        void SetComposingRegion(ComposingRegion? region);
         /// <summary>
         /// Indicates if text input client is capable of providing the text around the cursor
         /// </summary>
@@ -51,4 +53,26 @@ namespace Avalonia.Input.TextInput
         public int CursorOffset { get; set; }
         public int AnchorOffset { get; set; }
     }
+
+    public readonly struct ComposingRegion
+    {
+        private readonly int _start = -1;
+        private readonly int _end = -1;
+
+        public ComposingRegion(int start, int end)
+        {
+            _start = start;
+            _end = end;
+        }
+
+        public int Start => _start;
+        public int End => _end;
+
+        public bool Intersects(ComposingRegion region)
+        {
+            return _start <= region.Start && _end >= region.Start ||
+                _end >= region.End && _start <= region.End ||
+                _start >= region.Start && _end <= region.End;
+        }
+    }
 }

+ 1 - 1
src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs

@@ -140,7 +140,7 @@ namespace Avalonia.Media.TextFormatting
                 }
             }
 
-            return length;
+            return Math.Min(length, text.Length);
         }
     }
 }

+ 1 - 10
src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs

@@ -74,15 +74,6 @@ namespace Avalonia.Media.TextFormatting
 
             if (TryGetShapeableLength(text, currentTypeface, null, out var count, out var script))
             {
-                if (script == Script.Common && previousTypeface is not null)
-                {
-                    if (TryGetShapeableLength(text, previousTypeface.Value, null, out var fallbackCount, out _))
-                    {
-                        return new ShapeableTextCharacters(text.Take(fallbackCount),
-                            defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
-                    }
-                }
-
                 return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface),
                     biDiLevel);
             }
@@ -182,7 +173,7 @@ namespace Avalonia.Media.TextFormatting
 
                 var currentScript = currentGrapheme.FirstCodepoint.Script;
 
-                if (!currentGrapheme.FirstCodepoint.IsWhiteSpace && defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
+                if (defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
                 {
                     break;
                 }

+ 12 - 21
src/Avalonia.Controls/Presenters/TextPresenter.cs

@@ -9,6 +9,7 @@ using Avalonia.VisualTree;
 using Avalonia.Layout;
 using Avalonia.Media.Immutable;
 using Avalonia.Controls.Documents;
+using Avalonia.Input.TextInput;
 
 namespace Avalonia.Controls.Presenters
 {
@@ -331,6 +332,8 @@ namespace Avalonia.Controls.Presenters
 
         protected override bool BypassFlowDirectionPolicies => true;
 
+        public ComposingRegion? ComposingRegion { get; internal set; }
+
         /// <summary>
         /// Creates the <see cref="TextLayout"/> used to render the text.
         /// </summary>
@@ -514,7 +517,12 @@ namespace Avalonia.Controls.Presenters
         {
             if (!string.IsNullOrEmpty(_preeditText))
             {
-                var text = _text?.Substring(0, _caretIndex) + _preeditText + _text?.Substring(_caretIndex);
+                if (string.IsNullOrEmpty(_text) || _caretIndex > _text.Length)
+                {
+                    return _preeditText;
+                }
+
+                var text = _text.Substring(0, _caretIndex) + _preeditText + _text.Substring(_caretIndex);
 
                 return text;
             }
@@ -545,7 +553,7 @@ namespace Avalonia.Controls.Presenters
 
             if (!string.IsNullOrEmpty(_preeditText))
             {
-                var preeditHighlight = new ValueSpan<TextRunProperties>(_caretIndex, _preeditText.Length,
+                var preeditHighlight = new ValueSpan<TextRunProperties>(ComposingRegion.HasValue ? ComposingRegion.Value.Start : _caretIndex, _preeditText.Length,
                         new GenericTextRunProperties(typeface, FontSize,
                         foregroundBrush: foreground,
                         textDecorations: TextDecorations.Underline));
@@ -868,28 +876,11 @@ namespace Avalonia.Controls.Presenters
 
             if (string.IsNullOrEmpty(newValue))
             {
-                if (!string.IsNullOrEmpty(oldValue))
-                {
-                    var textPosition = _compositionStartHit.FirstCharacterIndex + _compositionStartHit.TrailingLength + newValue?.Length ?? 0;
-
-                    var characterHit = GetCharacterHitFromTextPosition(textPosition);
-
-                    UpdateCaret(characterHit, true);
-                }
-
-                _compositionStartHit = new CharacterHit(-1);
+                UpdateCaret(_lastCharacterHit);
             }
             else
             {
-                if (_compositionStartHit.FirstCharacterIndex == -1)
-                {
-                    _compositionStartHit = _lastCharacterHit;
-                }
-            }
-
-            if (_compositionStartHit.FirstCharacterIndex != -1)
-            {
-                var textPosition = _compositionStartHit.FirstCharacterIndex + _compositionStartHit.TrailingLength + newValue?.Length ?? 0;
+                var textPosition = (ComposingRegion.HasValue? ComposingRegion.Value.Start : _caretIndex) + newValue?.Length ?? 0;
 
                 var characterHit = GetCharacterHitFromTextPosition(textPosition);
 

+ 1 - 1
src/Avalonia.Controls/TextBox.cs

@@ -1212,7 +1212,7 @@ namespace Avalonia.Controls
 
         protected override void OnPointerPressed(PointerPressedEventArgs e)
         {
-            if (_presenter == null || !string.IsNullOrEmpty(_presenter.PreeditText))
+            if (_presenter == null )
             {
                 return;
             }

+ 15 - 0
src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

@@ -108,6 +108,11 @@ namespace Avalonia.Controls
             }
 
             _presenter.PreeditText = text;
+
+            if(text == null)
+            {
+                _presenter.ComposingRegion = null;
+            }
         }
 
         public void SelectInSurroundingText(int start, int end)
@@ -182,5 +187,15 @@ namespace Avalonia.Controls
 
             }, DispatcherPriority.Input);
         }
+
+        public void SetComposingRegion(ComposingRegion? region)
+        {
+            if(_presenter == null)
+            {
+                return;
+            }
+
+            _presenter.ComposingRegion = region;
+        }
     }
 }