Browse Source

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

pull/10328/head
Emmanuel Hansen 3 years ago
parent
commit
da1c2034f5
  1. 55
      src/Android/Avalonia.Android/AndroidInputMethod.cs
  2. 83
      src/Android/Avalonia.Android/InputEditable.cs
  3. 30
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  4. 20
      src/Avalonia.Base/Input/TextInput/ITextEditable.cs
  5. 5
      src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs
  6. 96
      src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

55
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;
});
}

83
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;
}
}
}

30
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
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
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);
}

96
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;
}
}
}

Loading…
Cancel
Save