فهرست منبع

remove surrounding text from android ime, add ITextEditable to handle sync between platform editable and IM client

Emmanuel Hansen 2 سال پیش
والد
کامیت
da1c2034f5

+ 9 - 46
src/Android/Avalonia.Android/AndroidInputMethod.cs

@@ -41,7 +41,6 @@ namespace Avalonia.Android
         private readonly InputMethodManager _imm;
         private ITextInputMethodClient _client;
         private AvaloniaInputConnection _inputConnection;
-        private IDisposable _textChangeObservable;
 
         public AndroidInputMethod(TView host)
         {
@@ -71,25 +70,15 @@ namespace Avalonia.Android
 
         public void SetClient(ITextInputMethodClient client)
         {
-            if (_client != null)
+            if(_inputConnection!= null)
             {
-                _textChangeObservable?.Dispose();
-                _client.SurroundingTextChanged -= SurroundingTextChanged;
-                _client.TextViewVisualChanged -= TextViewVisualChanged;
+                (_inputConnection.InputEditable as IDisposable)?.Dispose();
             }
 
             _client = client;
 
             if (IsActive)
             {
-                _client.SurroundingTextChanged += SurroundingTextChanged;
-                _client.TextViewVisualChanged += TextViewVisualChanged;
-
-                if(_client.TextViewVisual is TextPresenter textVisual)
-                {
-                    _textChangeObservable = textVisual.GetObservable(TextPresenter.TextProperty).Subscribe(new AnonymousObserver<string?>(UpdateText));
-                }
-
                 _host.RequestFocus();
 
                 _imm.RestartInput(View);              
@@ -106,39 +95,6 @@ namespace Avalonia.Android
             }
         }
 
-        private void TextViewVisualChanged(object sender, EventArgs e)
-        {
-            var textVisual = _client.TextViewVisual as TextPresenter;
-            _textChangeObservable?.Dispose();
-            _textChangeObservable = null;
-
-            if(textVisual != null)
-            {
-                _textChangeObservable = textVisual.GetObservable(TextPresenter.TextProperty).Subscribe(new AnonymousObserver<string?>(UpdateText));
-            }
-        }
-
-        private void UpdateText(string? obj)
-        {
-            (_inputConnection?.Editable as InputEditable)?.UpdateString(obj);
-        }
-
-        private void SurroundingTextChanged(object sender, EventArgs e)
-        {
-            if (IsActive && _inputConnection != null)
-            {
-                var surroundingText = Client.SurroundingText;
-
-                _inputConnection.SurroundingText = surroundingText;
-
-                if ((_inputConnection?.Editable as InputEditable)?.IsInBatchEdit != true)
-                {
-                    _inputConnection.SetSelection(surroundingText.AnchorOffset, surroundingText.CursorOffset);
-                    _imm.UpdateSelection(_host, surroundingText.AnchorOffset, surroundingText.CursorOffset, surroundingText.AnchorOffset, surroundingText.CursorOffset);
-                }
-            }
-        }
-
         public void SetCursorRect(Rect rect)
         {
             
@@ -183,6 +139,13 @@ namespace Avalonia.Android
 
                 outAttrs.ImeOptions |= ImeFlags.NoFullscreen | ImeFlags.NoExtractUi;
 
+                if(_client.TextViewVisual is TextPresenter presenter)
+                {
+                    _inputConnection?.InputEditable.SetPresenter(presenter);
+                }
+
+                _client.TextEditable = _inputConnection.InputEditable;
+
                 return _inputConnection;
             });
         }

+ 63 - 20
src/Android/Avalonia.Android/InputEditable.cs

@@ -4,24 +4,34 @@ using Android.Text;
 using Android.Views;
 using Android.Views.InputMethods;
 using Avalonia.Android.Platform.SkiaPlatform;
+using Avalonia.Controls.Presenters;
 using Avalonia.Input;
 using Avalonia.Input.Raw;
+using Avalonia.Input.TextInput;
 using Java.Lang;
 using static System.Net.Mime.MediaTypeNames;
 
 namespace Avalonia.Android
 {
-    internal class InputEditable : SpannableStringBuilder
+    internal class InputEditable : SpannableStringBuilder, IDisposable, ITextEditable
     {
         private readonly TopLevelImpl _topLevel;
         private readonly IAndroidInputMethod _inputMethod;
+        private readonly AvaloniaInputConnection _avaloniaInputConnection;
         private int _currentBatchLevel;
         private string _previousText;
+        private int _previousSelectionStart;
+        private int _previousSelectionEnd;
+        private TextPresenter _presenter;
 
-        public InputEditable(TopLevelImpl topLevel, IAndroidInputMethod inputMethod)
+        public event EventHandler TextChanged;
+        public event EventHandler SelectionChanged;
+
+        public InputEditable(TopLevelImpl topLevel, IAndroidInputMethod inputMethod, AvaloniaInputConnection avaloniaInputConnection)
         {
             _topLevel = topLevel;
             _inputMethod = inputMethod;
+            _avaloniaInputConnection = avaloniaInputConnection;
         }
 
         public InputEditable(ICharSequence text) : base(text)
@@ -46,47 +56,80 @@ namespace Avalonia.Android
 
         public bool IsInBatchEdit => _currentBatchLevel > 0;
 
+        public TextPresenter Presenter { get => _presenter; }
+
+
+        public int SelectionStart
+        {
+            get => Selection.GetSelectionStart(this); set
+            {
+                var end = SelectionEnd < 0 ? 0 : SelectionEnd;
+                _avaloniaInputConnection.SetSelection(value, end);
+                _inputMethod.IMM.UpdateSelection(_topLevel.View, value, end, value, end);
+            }
+        }
+        public int SelectionEnd
+        {
+            get => Selection.GetSelectionEnd(this); set
+            {
+                var start = SelectionStart < 0 ? 0 : SelectionStart;
+                _avaloniaInputConnection.SetSelection(start, value);
+                _inputMethod.IMM.UpdateSelection(_topLevel.View, start, value, start, value);
+            }
+        }
+
+        public string? Text
+        {
+            get => ToString(); set
+            {
+                if (Text != value)
+                {
+                    Clear();
+                    Insert(0, value ?? "");
+                }
+            }
+        }
+
         public void BeginBatchEdit()
         {
             _currentBatchLevel++;
 
-            if(_currentBatchLevel == 1)
+            if (_currentBatchLevel == 1)
             {
                 _previousText = ToString();
+                _previousSelectionStart =  SelectionStart;
+                _previousSelectionEnd = SelectionEnd;
             }
         }
 
         public void EndBatchEdit()
         {
-            if (_currentBatchLevel == 1)
+            if (_currentBatchLevel == 1 && _presenter != null)
             {
-                _inputMethod.Client.SelectInSurroundingText(-1, _previousText.Length);
-                var time = DateTime.Now.TimeOfDay;
-                var currentText = ToString();
-
-                if (string.IsNullOrEmpty(currentText))
+                if(_previousText != Text)
                 {
-                    _inputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel));
+                    TextChanged?.Invoke(this, EventArgs.Empty);
                 }
-                else
+
+                if (_previousSelectionStart != SelectionStart || _previousSelectionEnd != SelectionEnd)
                 {
-                    var rawTextEvent = new RawTextInputEventArgs(KeyboardDevice.Instance, (ulong)time.Ticks, _topLevel.InputRoot, currentText);
-                    _topLevel.Input(rawTextEvent);
+                    SelectionChanged?.Invoke(this, EventArgs.Empty);
                 }
-                _inputMethod.Client.SelectInSurroundingText(Selection.GetSelectionStart(this), Selection.GetSelectionEnd(this));
-
-                _previousText = "";
             }
 
             _currentBatchLevel--;
         }
 
-        public void UpdateString(string? text)
+        void IDisposable.Dispose()
+        {
+            _presenter = null;
+        }
+
+        public void SetPresenter(TextPresenter presenter)
         {
-            if(text != ToString())
+            if (_presenter == null)
             {
-                Clear();
-                Insert(0, text);
+                _presenter = presenter;
             }
         }
     }

+ 23 - 7
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@@ -411,23 +411,27 @@ namespace Avalonia.Android.Platform.SkiaPlatform
         private readonly TopLevelImpl _topLevel;
         private readonly IAndroidInputMethod _inputMethod;
         private readonly InputEditable _editable;
+        private bool _hasComposingRegion;
+        private int _compositionStart;
 
         public AvaloniaInputConnection(TopLevelImpl topLevel, IAndroidInputMethod inputMethod) : base(inputMethod.View, true)
         {
             _topLevel = topLevel;
             _inputMethod = inputMethod;
-            _editable = new InputEditable(_topLevel, _inputMethod);
+            _editable = new InputEditable(_topLevel, _inputMethod, this);
         }
 
-        public TextInputMethodSurroundingText SurroundingText { get; set; }
-
         public override IEditable Editable => _editable;
 
+        internal InputEditable InputEditable => _editable;
+
         public override bool SetComposingRegion(int start, int end)
         {
-            _inputMethod.Client.SetPreeditText(null);
             _inputMethod.Client.SetComposingRegion(new Media.TextFormatting.TextRange(start, end));
 
+            _hasComposingRegion = true;
+            _compositionStart = start;
+
             return base.SetComposingRegion(start, end);
         }
 
@@ -441,7 +445,17 @@ namespace Avalonia.Android.Platform.SkiaPlatform
             }
             else
             {
-                return base.SetComposingText(text, newCursorPosition);
+                var ret = base.SetComposingText(text, newCursorPosition);
+
+                if (!_hasComposingRegion)
+                {
+                    _compositionStart = _editable.SelectionEnd - composingText.Length;
+                    _hasComposingRegion = true;
+                }
+
+                _inputMethod.Client.SetComposingRegion(new Media.TextFormatting.TextRange(_compositionStart, _compositionStart + composingText.Length));
+
+                return ret;
             }
         }
 
@@ -463,14 +477,16 @@ namespace Avalonia.Android.Platform.SkiaPlatform
         public override bool FinishComposingText()
         {
             _inputMethod.Client?.SetComposingRegion(null);
+            _hasComposingRegion = false;
+            _compositionStart = -1;
             return base.FinishComposingText();
         }
 
         public override bool CommitText(ICharSequence text, int newCursorPosition)
         {
-            _inputMethod.Client.SetPreeditText(null);
-
             _inputMethod.Client?.SetComposingRegion(null);
+            _hasComposingRegion = false;
+            _compositionStart = -1;
 
             return base.CommitText(text, newCursorPosition);
         }

+ 20 - 0
src/Avalonia.Base/Input/TextInput/ITextEditable.cs

@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Avalonia.Metadata;
+
+namespace Avalonia.Input.TextInput
+{
+    [NotClientImplementable]
+    public interface ITextEditable
+    {
+        event EventHandler TextChanged;
+        event EventHandler SelectionChanged;
+        int SelectionStart { get; set; }
+        int SelectionEnd { get; set; }
+        
+        string? Text { get; set; }
+    }
+}

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

@@ -49,6 +49,11 @@ namespace Avalonia.Input.TextInput
         /// </summary>
         event EventHandler? SurroundingTextChanged;
 
+        /// <summary>
+        /// Gets or sets a platform editable. Text and selection changes made in the editable are forwarded to the IM client.
+        /// </summary>
+        ITextEditable? TextEditable { get; set; }
+
         void SelectInSurroundingText(int start, int end);
     }
 

+ 82 - 14
src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

@@ -13,6 +13,7 @@ namespace Avalonia.Controls
     {
         private TextBox? _parent;
         private TextPresenter? _presenter;
+        private ITextEditable? _textEditable;
 
         public Visual TextViewVisual => _presenter!;
 
@@ -46,7 +47,7 @@ namespace Avalonia.Controls
         {
             get
             {
-                if(_presenter is null || _parent is null)
+                if (_presenter is null || _parent is null)
                 {
                     return default;
                 }
@@ -72,13 +73,60 @@ namespace Avalonia.Controls
             }
         }
 
+        public ITextEditable? TextEditable
+        {
+            get => _textEditable; set
+            {
+                if(_textEditable != null)
+                {
+                    _textEditable.TextChanged -= TextEditable_TextChanged;
+                    _textEditable.SelectionChanged -= TextEditable_SelectionChanged;
+                }
+
+                _textEditable = value;
+
+                if(_textEditable != null)
+                {
+                    _textEditable.TextChanged += TextEditable_TextChanged;
+                    _textEditable.SelectionChanged += TextEditable_SelectionChanged;
+
+                    if (_presenter != null)
+                    {
+                        _textEditable.Text = _presenter.Text;
+                        _textEditable.SelectionStart = _presenter.SelectionStart;
+                        _textEditable.SelectionEnd = _presenter.SelectionEnd;
+                    }
+                }
+            }
+        }
+
+        private void TextEditable_SelectionChanged(object? sender, EventArgs e)
+        {
+            if(_parent != null && _textEditable != null)
+            {
+                _parent.SelectionStart = _textEditable.SelectionStart;
+                _parent.SelectionEnd = _textEditable.SelectionEnd;
+            }
+        }
+
+        private void TextEditable_TextChanged(object? sender, EventArgs e)
+        {
+            if (_parent != null)
+            {
+                if (_parent.Text != _textEditable?.Text)
+                {
+                    _parent.Text = _textEditable?.Text;
+                }
+            }
+        }
+
         private static string GetTextLineText(TextLine textLine)
         {
             var builder = StringBuilderCache.Acquire(textLine.Length);
 
             foreach (var run in textLine.TextRuns)
             {
-                if(run.Length > 0)
+                if (run.Length > 0)
                 {
 #if NET6_0_OR_GREATER
                     builder.Append(run.Text.Span);
@@ -117,13 +165,12 @@ namespace Avalonia.Controls
             {
                 return;
             }
-
             _presenter.CompositionRegion = region;
         }
 
         public void SelectInSurroundingText(int start, int end)
         {
-            if(_parent is null ||_presenter is null)
+            if (_parent is null || _presenter is null)
             {
                 return;
             }
@@ -136,21 +183,21 @@ namespace Avalonia.Controls
 
             var selectionStart = lineStart + start;
             var selectionEnd = lineStart + end;
-             
+
             _parent.SelectionStart = selectionStart;
             _parent.SelectionEnd = selectionEnd;
-        }    
-        
+        }
+
         public void SetPresenter(TextPresenter? presenter, TextBox? parent)
         {
-            if(_parent != null)
+            if (_parent != null)
             {
                 _parent.PropertyChanged -= OnParentPropertyChanged;
             }
 
             _parent = parent;
 
-            if(_parent != null)
+            if (_parent != null)
             {
                 _parent.PropertyChanged += OnParentPropertyChanged;
             }
@@ -159,16 +206,16 @@ namespace Avalonia.Controls
             {
                 _presenter.PreeditText = null;
 
-                _presenter.CaretBoundsChanged -= OnCaretBoundsChanged;             
+                _presenter.CaretBoundsChanged -= OnCaretBoundsChanged;
             }
-           
+
             _presenter = presenter;
-            
+
             if (_presenter != null)
             {
                 _presenter.CaretBoundsChanged += OnCaretBoundsChanged;
             }
-           
+
             TextViewVisualChanged?.Invoke(this, EventArgs.Empty);
 
             OnCaretBoundsChanged(this, EventArgs.Empty);
@@ -176,12 +223,33 @@ namespace Avalonia.Controls
 
         private void OnParentPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
         {
-            if(e.Property == TextBox.SelectionStartProperty || e.Property == TextBox.SelectionEndProperty)
+            if (e.Property == TextBox.SelectionStartProperty || e.Property == TextBox.SelectionEndProperty)
             {
                 if (SupportsSurroundingText)
                 {
                     SurroundingTextChanged?.Invoke(this, e);
                 }
+                if (_textEditable != null)
+                {
+                    var value = (int)(e.NewValue ?? 0);
+                    if (e.Property == TextBox.SelectionStartProperty)
+                    {
+                        _textEditable.SelectionStart = value;
+                    }
+
+                    if (e.Property == TextBox.SelectionEndProperty)
+                    {
+                        _textEditable.SelectionEnd = value;
+                    }
+                }
+            }
+
+            if(e.Property == TextBox.TextProperty)
+            {
+                if(_textEditable != null)
+                {
+                    _textEditable.Text = (string?)e.NewValue;
+                }
             }
         }