Browse Source

add composing region to text input client, improves android composition

pull/10328/head
Emmanuel Hansen 3 years ago
parent
commit
3e0179e1e0
  1. 40
      src/Android/Avalonia.Android/AndroidInputMethod.cs
  2. 93
      src/Android/Avalonia.Android/InputEditable.cs
  3. 132
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  4. 6
      src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs
  5. 33
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  6. 11
      src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

40
src/Android/Avalonia.Android/AndroidInputMethod.cs

@ -5,8 +5,10 @@ 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.TextInput;
using Avalonia.Reactive;
namespace Avalonia.Android
{
@ -39,6 +41,7 @@ namespace Avalonia.Android
private readonly InputMethodManager _imm;
private ITextInputMethodClient _client;
private AvaloniaInputConnection _inputConnection;
private IDisposable _textChangeObservable;
public AndroidInputMethod(TView host)
{
@ -70,13 +73,9 @@ namespace Avalonia.Android
{
if (_client != null)
{
_textChangeObservable?.Dispose();
_client.SurroundingTextChanged -= SurroundingTextChanged;
}
if(_inputConnection != null)
{
_inputConnection.ComposingText = null;
_inputConnection.ComposingRegion = default;
_client.TextViewVisualChanged -= TextViewVisualChanged;
}
_client = client;
@ -84,6 +83,12 @@ namespace Avalonia.Android
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();
@ -101,6 +106,23 @@ 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)
@ -109,12 +131,10 @@ namespace Avalonia.Android
_inputConnection.SurroundingText = surroundingText;
_imm.UpdateSelection(_host, surroundingText.AnchorOffset, surroundingText.CursorOffset, surroundingText.AnchorOffset, surroundingText.CursorOffset);
if (_inputConnection.ComposingText != null && !_inputConnection.IsCommiting && surroundingText.AnchorOffset == surroundingText.CursorOffset)
if ((_inputConnection?.Editable as InputEditable)?.IsInBatchEdit != true)
{
_inputConnection.CommitText(_inputConnection.ComposingText, 0);
_inputConnection.SetSelection(surroundingText.AnchorOffset, surroundingText.CursorOffset);
_imm.UpdateSelection(_host, surroundingText.AnchorOffset, surroundingText.CursorOffset, surroundingText.AnchorOffset, surroundingText.CursorOffset);
}
}
}

93
src/Android/Avalonia.Android/InputEditable.cs

@ -0,0 +1,93 @@
using System;
using Android.Runtime;
using Android.Text;
using Android.Views;
using Android.Views.InputMethods;
using Avalonia.Android.Platform.SkiaPlatform;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Java.Lang;
using static System.Net.Mime.MediaTypeNames;
namespace Avalonia.Android
{
internal class InputEditable : SpannableStringBuilder
{
private readonly TopLevelImpl _topLevel;
private readonly IAndroidInputMethod _inputMethod;
private int _currentBatchLevel;
private string _previousText;
public InputEditable(TopLevelImpl topLevel, IAndroidInputMethod inputMethod)
{
_topLevel = topLevel;
_inputMethod = inputMethod;
}
public InputEditable(ICharSequence text) : base(text)
{
}
public InputEditable(string text) : base(text)
{
}
public InputEditable(ICharSequence text, int start, int end) : base(text, start, end)
{
}
public InputEditable(string text, int start, int end) : base(text, start, end)
{
}
protected InputEditable(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer)
{
}
public bool IsInBatchEdit => _currentBatchLevel > 0;
public void BeginBatchEdit()
{
_currentBatchLevel++;
if(_currentBatchLevel == 1)
{
_previousText = ToString();
}
}
public void EndBatchEdit()
{
if (_currentBatchLevel == 1)
{
_inputMethod.Client.SelectInSurroundingText(-1, _previousText.Length);
var time = DateTime.Now.TimeOfDay;
var currentText = ToString();
if (string.IsNullOrEmpty(currentText))
{
_inputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel));
}
else
{
var rawTextEvent = new RawTextInputEventArgs(KeyboardDevice.Instance, (ulong)time.Ticks, _topLevel.InputRoot, currentText);
_topLevel.Input(rawTextEvent);
}
_inputMethod.Client.SelectInSurroundingText(Selection.GetSelectionStart(this), Selection.GetSelectionEnd(this));
_previousText = "";
}
_currentBatchLevel--;
}
public void UpdateString(string? text)
{
if(text != ToString())
{
Clear();
Insert(0, text);
}
}
}
}

132
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@ -410,27 +410,23 @@ namespace Avalonia.Android.Platform.SkiaPlatform
{
private readonly TopLevelImpl _topLevel;
private readonly IAndroidInputMethod _inputMethod;
private readonly InputEditable _editable;
public AvaloniaInputConnection(TopLevelImpl topLevel, IAndroidInputMethod inputMethod) : base(inputMethod.View, true)
{
_topLevel = topLevel;
_inputMethod = inputMethod;
_editable = new InputEditable(_topLevel, _inputMethod);
}
public TextInputMethodSurroundingText SurroundingText { get; set; }
public string ComposingText { get; internal set; }
public ComposingRegion? ComposingRegion { get; internal set; }
public bool IsComposing => !string.IsNullOrEmpty(ComposingText);
public bool IsCommiting { get; private set; }
public override IEditable Editable => _editable;
public override bool SetComposingRegion(int start, int end)
{
//System.Diagnostics.Debug.WriteLine($"Composing Region: [{start}|{end}] {SurroundingText.Text?.Substring(start, end - start)}");
ComposingRegion = new ComposingRegion(start, end);
_inputMethod.Client.SetPreeditText(null);
_inputMethod.Client.SetComposingRegion(new Media.TextFormatting.TextRange(start, end));
return base.SetComposingRegion(start, end);
}
@ -439,132 +435,46 @@ namespace Avalonia.Android.Platform.SkiaPlatform
{
var composingText = text.ToString();
ComposingText = composingText;
_inputMethod.Client?.SetPreeditText(ComposingText);
return base.SetComposingText(text, newCursorPosition);
}
public override bool FinishComposingText()
{
if (!string.IsNullOrEmpty(ComposingText))
if (string.IsNullOrEmpty(composingText))
{
CommitText(ComposingText, ComposingText.Length);
return CommitText(text, newCursorPosition);
}
else
{
ComposingRegion = new ComposingRegion(SurroundingText.CursorOffset, SurroundingText.CursorOffset);
return base.SetComposingText(text, newCursorPosition);
}
return base.FinishComposingText();
}
public override ICharSequence GetTextBeforeCursorFormatted(int length, [GeneratedEnum] GetTextFlags flags)
public override bool BeginBatchEdit()
{
if (!string.IsNullOrEmpty(SurroundingText.Text) && length > 0)
{
var start = System.Math.Max(SurroundingText.CursorOffset - length, 0);
var end = System.Math.Min(start + length - 1, SurroundingText.CursorOffset);
var text = SurroundingText.Text.Substring(start, end - start);
_editable.BeginBatchEdit();
//System.Diagnostics.Debug.WriteLine($"Text Before: {text}");
return new Java.Lang.String(text);
}
return null;
return base.BeginBatchEdit();
}
public override ICharSequence GetTextAfterCursorFormatted(int length, [GeneratedEnum] GetTextFlags flags)
public override bool EndBatchEdit()
{
if (!string.IsNullOrEmpty(SurroundingText.Text))
{
var start = SurroundingText.CursorOffset;
var end = System.Math.Min(start + length, SurroundingText.Text.Length);
var text = SurroundingText.Text.Substring(start, end - start);
//System.Diagnostics.Debug.WriteLine($"Text After: {text}");
var ret = base.EndBatchEdit();
_editable.EndBatchEdit();
return new Java.Lang.String(text);
}
return ret;
}
return null;
public override bool FinishComposingText()
{
_inputMethod.Client?.SetComposingRegion(null);
return base.FinishComposingText();
}
public override bool CommitText(ICharSequence text, int newCursorPosition)
{
IsCommiting = true;
var committedText = text.ToString();
_inputMethod.Client.SetPreeditText(null);
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;
var rawTextEvent = new RawTextInputEventArgs(KeyboardDevice.Instance, (ulong)time.Ticks, _topLevel.InputRoot, committedText);
_topLevel.Input(rawTextEvent);
ComposingText = null;
ComposingRegion = new ComposingRegion(newCursorPosition, newCursorPosition);
_inputMethod.Client?.SetComposingRegion(null);
return base.CommitText(text, newCursorPosition);
}
public override bool DeleteSurroundingText(int beforeLength, int 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);
}
public override bool SetSelection(int start, int end)
{
_inputMethod.Client.SelectInSurroundingText(start, end);
ComposingRegion = new ComposingRegion(start, end);
return base.SetSelection(start, end);
}
public override bool PerformEditorAction([GeneratedEnum] ImeAction actionCode)
{
switch (actionCode)

6
src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs

@ -1,4 +1,5 @@
using System;
using Avalonia.Media.TextFormatting;
using Avalonia.VisualTree;
namespace Avalonia.Input.TextInput
@ -30,6 +31,11 @@ namespace Avalonia.Input.TextInput
/// </summary>
void SetPreeditText(string? text);
/// <summary>
/// Sets the current composing region. This doesn't remove the composing text from the commited text.
/// </summary>
void SetComposingRegion(TextRange? region);
/// <summary>
/// Indicates if text input client is capable of providing the text around the cursor
/// </summary>

33
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -63,6 +63,15 @@ namespace Avalonia.Controls.Presenters
o => o.PreeditText,
(o, v) => o.PreeditText = v);
/// <summary>
/// Defines the <see cref="CompositionRegion"/> property.
/// </summary>
public static readonly DirectProperty<TextPresenter, TextRange?> CompositionRegionProperty =
AvaloniaProperty.RegisterDirect<TextPresenter, TextRange?>(
nameof(CompositionRegion),
o => o.CompositionRegion,
(o, v) => o.CompositionRegion = v);
/// <summary>
/// Defines the <see cref="TextAlignment"/> property.
/// </summary>
@ -106,6 +115,7 @@ namespace Avalonia.Controls.Presenters
private Rect _caretBounds;
private Point _navigationPosition;
private string? _preeditText;
private TextRange? _compositionRegion;
static TextPresenter()
{
@ -146,6 +156,12 @@ namespace Avalonia.Controls.Presenters
set => SetAndRaise(PreeditTextProperty, ref _preeditText, value);
}
public TextRange? CompositionRegion
{
get => _compositionRegion;
set => SetAndRaise(CompositionRegionProperty, ref _compositionRegion, value);
}
/// <summary>
/// Gets or sets the font family.
/// </summary>
@ -548,7 +564,20 @@ namespace Avalonia.Controls.Presenters
var foreground = Foreground;
if (!string.IsNullOrEmpty(_preeditText))
if(_compositionRegion != null)
{
var preeditHighlight = new ValueSpan<TextRunProperties>(_compositionRegion?.Start ?? 0, _compositionRegion?.Length ?? 0,
new GenericTextRunProperties(typeface, FontSize,
foregroundBrush: foreground,
textDecorations: TextDecorations.Underline));
textStyleOverrides = new[]
{
preeditHighlight
};
}
else if (!string.IsNullOrEmpty(_preeditText))
{
var preeditHighlight = new ValueSpan<TextRunProperties>(_caretIndex, _preeditText.Length,
new GenericTextRunProperties(typeface, FontSize,
@ -911,6 +940,7 @@ namespace Avalonia.Controls.Presenters
break;
}
case nameof(CompositionRegion):
case nameof(Foreground):
case nameof(FontSize):
case nameof(FontStyle):
@ -931,7 +961,6 @@ namespace Avalonia.Controls.Presenters
case nameof(PasswordChar):
case nameof(RevealPassword):
case nameof(FlowDirection):
{
InvalidateTextLayout();

11
src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

@ -5,6 +5,7 @@ using Avalonia.Media.TextFormatting;
using Avalonia.Threading;
using Avalonia.Utilities;
using Avalonia.VisualTree;
using static System.Net.Mime.MediaTypeNames;
namespace Avalonia.Controls
{
@ -110,6 +111,16 @@ namespace Avalonia.Controls
_presenter.PreeditText = text;
}
public void SetComposingRegion(TextRange? region)
{
if (_presenter == null)
{
return;
}
_presenter.CompositionRegion = region;
}
public void SelectInSurroundingText(int start, int end)
{
if(_parent is null ||_presenter is null)

Loading…
Cancel
Save