Browse Source

Android - Initial attempt to fix focus management and implement SurroundingText

pull/8963/head
Benedikt Stebner 4 years ago
parent
commit
391b5c2c11
  1. 44
      src/Android/Avalonia.Android/AndroidInputMethod.cs
  2. 105
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  3. 8
      src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs
  4. 53
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  5. 2
      src/Avalonia.Controls/TextBox.cs
  6. 31
      src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

44
src/Android/Avalonia.Android/AndroidInputMethod.cs

@ -15,12 +15,12 @@ namespace Avalonia.Android
internal interface IAndroidInputMethod internal interface IAndroidInputMethod
{ {
public ITextInputMethodClient Client { get; } public ITextInputMethodClient Client { get; }
public bool IsActive { get; } public bool IsActive { get; }
} }
class AndroidInputMethod<TView> : ITextInputMethodImpl, IAndroidInputMethod class AndroidInputMethod<TView> : ITextInputMethodImpl, IAndroidInputMethod
where TView: View, IInitEditorInfo where TView : View, IInitEditorInfo
{ {
private readonly TView _host; private readonly TView _host;
private readonly InputMethodManager _imm; private readonly InputMethodManager _imm;
@ -51,16 +51,42 @@ namespace Avalonia.Android
public void SetClient(ITextInputMethodClient client) public void SetClient(ITextInputMethodClient client)
{ {
if(client is null)
{
_inputConnection?.SetComposingText("", 0);
}
if (_client != null)
{
_client.SurroundingTextChanged -= SurroundingTextChanged;
}
Reset();
_client = client; _client = client;
if (IsActive) if (IsActive)
{ {
_client.SurroundingTextChanged += SurroundingTextChanged;
_host.RequestFocus(); _host.RequestFocus();
Reset();
_imm.ShowSoftInput(_host, ShowFlags.Implicit); _imm.ShowSoftInput(_host, ShowFlags.Implicit);
} }
else else
_imm.HideSoftInputFromWindow(_host.WindowToken, HideSoftInputFlags.None); {
_imm.HideSoftInputFromWindow(_host.WindowToken, HideSoftInputFlags.None);
}
}
private void SurroundingTextChanged(object sender, EventArgs e)
{
if (IsActive)
{
var surroundingText = Client.SurroundingText;
_imm.UpdateSelection(_host, surroundingText.AnchorOffset, surroundingText.CursorOffset, surroundingText.AnchorOffset, surroundingText.CursorOffset);
}
} }
public void SetCursorRect(Rect rect) public void SetCursorRect(Rect rect)
@ -69,10 +95,12 @@ namespace Avalonia.Android
public void SetOptions(TextInputOptions options) public void SetOptions(TextInputOptions options)
{ {
_inputConnection = new InputConnectionImpl(_host, this);
_host.InitEditorInfo((_host, outAttrs) => _host.InitEditorInfo((_host, outAttrs) =>
{ {
_inputConnection?.CommitText("", 0);
_inputConnection = new InputConnectionImpl(_host, this);
outAttrs.InputType = options.ContentType switch outAttrs.InputType = options.ContentType switch
{ {
TextInputContentType.Email => global::Android.Text.InputTypes.TextVariationEmailAddress, TextInputContentType.Email => global::Android.Text.InputTypes.TextVariationEmailAddress,

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

@ -13,6 +13,7 @@ using Avalonia.Android.Platform.Specific;
using Avalonia.Android.Platform.Specific.Helpers; using Avalonia.Android.Platform.Specific.Helpers;
using Avalonia.Android.Platform.Storage; using Avalonia.Android.Platform.Storage;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Controls.Platform.Surfaces; using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Input; using Avalonia.Input;
@ -25,6 +26,7 @@ using Avalonia.Platform.Storage;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition;
using Java.Lang; using Java.Lang;
using static System.Net.Mime.MediaTypeNames;
namespace Avalonia.Android.Platform.SkiaPlatform namespace Avalonia.Android.Platform.SkiaPlatform
{ {
@ -36,7 +38,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
private readonly AndroidKeyboardEventsHelper<TopLevelImpl> _keyboardHelper; private readonly AndroidKeyboardEventsHelper<TopLevelImpl> _keyboardHelper;
private readonly AndroidMotionEventsHelper _pointerHelper; private readonly AndroidMotionEventsHelper _pointerHelper;
private readonly ITextInputMethodImpl _textInputMethod; private readonly AndroidInputMethod<ViewImpl> _textInputMethod;
private ViewImpl _view; private ViewImpl _view;
public TopLevelImpl(AvaloniaView avaloniaView, bool placeOnTop = false) public TopLevelImpl(AvaloniaView avaloniaView, bool placeOnTop = false)
@ -211,7 +213,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform
} }
private Func<View, EditorInfo, InputConnectionImpl> _initEditorInfo; private Func<View, EditorInfo, InputConnectionImpl> _initEditorInfo;
protected InputConnectionImpl _inputConnection;
public void InitEditorInfo(Func<View, EditorInfo, InputConnectionImpl> init) public void InitEditorInfo(Func<View, EditorInfo, InputConnectionImpl> init)
{ {
@ -257,7 +258,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
} }
} }
internal class InputConnectionImpl: BaseInputConnection internal class InputConnectionImpl: BaseInputConnection, IInputConnection
{ {
private readonly IAndroidInputMethod _inputMethod; private readonly IAndroidInputMethod _inputMethod;
@ -271,11 +272,93 @@ namespace Avalonia.Android.Platform.SkiaPlatform
{ {
} }
public string CompositionText { get; private set; }
public override bool SetComposingRegion(int start, int end)
{
if (_inputMethod.IsActive)
{
var surroundingText = _inputMethod.Client.SurroundingText;
System.Diagnostics.Debug.WriteLine($"Composing Region: [{start}|{end}] {surroundingText.Text?.Substring(start, end - start)}");
}
return base.SetComposingRegion(start, end);
}
public override bool CommitCorrection(CorrectionInfo correctionInfo)
{
return base.CommitCorrection(correctionInfo);
}
public override bool DeleteSurroundingTextInCodePoints(int beforeLength, int afterLength)
{
return base.DeleteSurroundingTextInCodePoints(beforeLength, afterLength);
}
public override bool DeleteSurroundingText(int beforeLength, int afterLength)
{
if (_inputMethod.IsActive && _inputMethod.Client.SupportsSurroundingText)
{
_inputMethod.Client.DeleteSurroundingText(beforeLength, afterLength);
}
return base.DeleteSurroundingText(beforeLength, afterLength);
}
public override ICharSequence GetTextBeforeCursorFormatted(int length, [GeneratedEnum] GetTextFlags flags)
{
if (_inputMethod.IsActive)
{
var surroundingText = _inputMethod.Client.SurroundingText;
if (_inputMethod.IsActive && !string.IsNullOrEmpty(surroundingText.Text))
{
var start = System.Math.Max(surroundingText.CursorOffset - length, 0);
var end = System.Math.Min(start + length, surroundingText.CursorOffset);
var text = surroundingText.Text.Substring(start, end - start);
System.Diagnostics.Debug.WriteLine($"Text Before: {text}");
return new Java.Lang.String(text);
}
}
return base.GetTextBeforeCursorFormatted(length, flags);
}
public override ICharSequence GetTextAfterCursorFormatted(int length, [GeneratedEnum] GetTextFlags flags)
{
if (_inputMethod.IsActive)
{
var surroundingText = _inputMethod.Client.SurroundingText;
if (_inputMethod.IsActive && !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}");
return new Java.Lang.String(text);
}
}
return base.GetTextAfterCursorFormatted(length, flags);
}
public override bool SetComposingText(ICharSequence text, int newCursorPosition) public override bool SetComposingText(ICharSequence text, int newCursorPosition)
{ {
CompositionText = text.ToString();
if (_inputMethod.IsActive) if (_inputMethod.IsActive)
{ {
_inputMethod.Client.SetPreeditText(text.ToString()); _inputMethod.Client.SetPreeditText(CompositionText);
} }
return base.SetComposingText(text, newCursorPosition); return base.SetComposingText(text, newCursorPosition);
@ -283,6 +366,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public override bool CommitText(ICharSequence text, int newCursorPosition) public override bool CommitText(ICharSequence text, int newCursorPosition)
{ {
CompositionText = null;
if (_inputMethod.IsActive) if (_inputMethod.IsActive)
{ {
_inputMethod.Client.SetPreeditText(null); _inputMethod.Client.SetPreeditText(null);
@ -290,5 +375,17 @@ namespace Avalonia.Android.Platform.SkiaPlatform
return base.CommitText(text, newCursorPosition); return base.CommitText(text, newCursorPosition);
} }
public override bool FinishComposingText()
{
CompositionText = null;
if (_inputMethod.IsActive)
{
_inputMethod.Client.SetPreeditText(CompositionText);
}
return base.FinishComposingText();
}
} }
} }

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

@ -41,6 +41,14 @@ namespace Avalonia.Input.TextInput
/// Should be fired when surrounding text changed /// Should be fired when surrounding text changed
/// </summary> /// </summary>
event EventHandler? SurroundingTextChanged; event EventHandler? SurroundingTextChanged;
/// <summary>
/// Deletes specified length before and after current selection.
/// </summary>
/// <param name="beforeLength">The length before current selection.</param>
/// <param name="afterLength">The length after current selection.</param>
void DeleteSurroundingText(int beforeLength, int afterLength);
/// <summary> /// <summary>
/// Returns the text before the cursor. Must return a non-empty string if cursor is not at the beginning of the text entry /// Returns the text before the cursor. Must return a non-empty string if cursor is not at the beginning of the text entry
/// </summary> /// </summary>

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

@ -9,7 +9,6 @@ using Avalonia.VisualTree;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
using Avalonia.Controls.Documents; using Avalonia.Controls.Documents;
using Avalonia.Media.TextFormatting.Unicode;
namespace Avalonia.Controls.Presenters namespace Avalonia.Controls.Presenters
{ {
@ -100,6 +99,7 @@ namespace Avalonia.Controls.Presenters
private Rect _caretBounds; private Rect _caretBounds;
private Point _navigationPosition; private Point _navigationPosition;
private string? _preeditText; private string? _preeditText;
private CharacterHit _compositionStartHit = new CharacterHit(-1);
static TextPresenter() static TextPresenter()
{ {
@ -235,7 +235,7 @@ namespace Avalonia.Controls.Presenters
_textLayout = CreateTextLayout(); _textLayout = CreateTextLayout();
UpdateCaret(_lastCharacterHit); UpdateCaret(_lastCharacterHit, false);
return _textLayout; return _textLayout;
} }
@ -532,8 +532,8 @@ namespace Avalonia.Controls.Presenters
{ {
var preeditHighlight = new ValueSpan<TextRunProperties>(_caretIndex, _preeditText.Length, var preeditHighlight = new ValueSpan<TextRunProperties>(_caretIndex, _preeditText.Length,
new GenericTextRunProperties(typeface, FontSize, new GenericTextRunProperties(typeface, FontSize,
foregroundBrush: foreground/*, foregroundBrush: foreground,
textDecorations: TextDecorations.Underline*/)); textDecorations: TextDecorations.Underline));
textStyleOverrides = new[] textStyleOverrides = new[]
{ {
@ -573,7 +573,6 @@ namespace Avalonia.Controls.Presenters
InvalidateMeasure(); InvalidateMeasure();
} }
protected override Size MeasureOverride(Size availableSize) protected override Size MeasureOverride(Size availableSize)
{ {
_constraint = availableSize; _constraint = availableSize;
@ -800,7 +799,7 @@ namespace Avalonia.Controls.Presenters
CaretChanged(); CaretChanged();
} }
private void UpdateCaret(CharacterHit characterHit) private void UpdateCaret(CharacterHit characterHit, bool updateCaretIndex = true)
{ {
_lastCharacterHit = characterHit; _lastCharacterHit = characterHit;
@ -828,7 +827,10 @@ namespace Avalonia.Controls.Presenters
CaretBoundsChanged?.Invoke(this, EventArgs.Empty); CaretBoundsChanged?.Invoke(this, EventArgs.Empty);
} }
SetAndRaise(CaretIndexProperty, ref _caretIndex, caretIndex); if (updateCaretIndex)
{
SetAndRaise(CaretIndexProperty, ref _caretIndex, caretIndex);
}
} }
internal Rect GetCursorRectangle() internal Rect GetCursorRectangle()
@ -845,12 +847,48 @@ namespace Avalonia.Controls.Presenters
_caretTimer.Tick -= CaretTimerTick; _caretTimer.Tick -= CaretTimerTick;
} }
protected void OnPreeditTextChanged(string? preeditText)
{
InvalidateTextLayout();
if (preeditText is null)
{
_compositionStartHit = new CharacterHit(-1);
}
else
{
if (_compositionStartHit.FirstCharacterIndex == -1)
{
_compositionStartHit = _lastCharacterHit;
}
}
if (_compositionStartHit.FirstCharacterIndex != -1)
{
var textPosition = _compositionStartHit.FirstCharacterIndex + _compositionStartHit.TrailingLength + preeditText?.Length ?? 0;
var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(textPosition, true);
var textLine = TextLayout.TextLines[lineIndex];
var characterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(textPosition - 1));
UpdateCaret(characterHit, false);
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
switch (change.Property.Name) switch (change.Property.Name)
{ {
case nameof(PreeditText):
{
OnPreeditTextChanged(change.NewValue as string);
break;
}
case nameof(Foreground): case nameof(Foreground):
case nameof(FontSize): case nameof(FontSize):
case nameof(FontStyle): case nameof(FontStyle):
@ -859,7 +897,6 @@ namespace Avalonia.Controls.Presenters
case nameof(FontStretch): case nameof(FontStretch):
case nameof(Text): case nameof(Text):
case nameof(PreeditText):
case nameof(TextAlignment): case nameof(TextAlignment):
case nameof(TextWrapping): case nameof(TextWrapping):

2
src/Avalonia.Controls/TextBox.cs

@ -1482,7 +1482,7 @@ namespace Avalonia.Controls
SelectionEnd = Text?.Length ?? 0; SelectionEnd = Text?.Length ?? 0;
} }
private bool DeleteSelection(bool raiseTextChanged = true) internal bool DeleteSelection(bool raiseTextChanged = true)
{ {
if (IsReadOnly) if (IsReadOnly)
return true; return true;

31
src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

@ -3,6 +3,7 @@ using System.Diagnostics;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.TextInput; using Avalonia.Input.TextInput;
using Avalonia.Media;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Controls namespace Avalonia.Controls
@ -56,7 +57,7 @@ namespace Avalonia.Controls
public TextInputMethodSurroundingText SurroundingText => new() public TextInputMethodSurroundingText SurroundingText => new()
{ {
Text = _presenter?.Text ?? "", Text = _presenter?.Text ?? "",
CursorOffset = _presenter?.CaretIndex ?? 0, CursorOffset = _presenter?.SelectionEnd ?? 0,
AnchorOffset = _presenter?.SelectionStart ?? 0 AnchorOffset = _presenter?.SelectionStart ?? 0
}; };
@ -78,10 +79,14 @@ namespace Avalonia.Controls
{ {
if (e.Property == TextBox.TextProperty || e.Property == TextBox.SelectionStartProperty || if (e.Property == TextBox.TextProperty || e.Property == TextBox.SelectionStartProperty ||
e.Property == TextBox.SelectionEndProperty) e.Property == TextBox.SelectionEndProperty)
SurroundingTextChanged?.Invoke(this, EventArgs.Empty); {
if (string.IsNullOrEmpty(_presenter?.PreeditText))
{
SurroundingTextChanged?.Invoke(this, EventArgs.Empty);
}
}
} }
public void SetPresenter(TextPresenter? presenter, TextBox? parent) public void SetPresenter(TextPresenter? presenter, TextBox? parent)
{ {
if (_parent != null) if (_parent != null)
@ -98,6 +103,8 @@ namespace Avalonia.Controls
if (_presenter != null) if (_presenter != null)
{ {
_presenter.PreeditText = null;
_presenter.CaretBoundsChanged -= OnCaretBoundsChanged; _presenter.CaretBoundsChanged -= OnCaretBoundsChanged;
} }
@ -107,14 +114,22 @@ namespace Avalonia.Controls
{ {
_presenter.CaretBoundsChanged += OnCaretBoundsChanged; _presenter.CaretBoundsChanged += OnCaretBoundsChanged;
} }
TextViewVisualChanged?.Invoke(this, EventArgs.Empty);
CursorRectangleChanged?.Invoke(this, EventArgs.Empty);
}
if(presenter == null) public void DeleteSurroundingText(int beforeLength, int afterLength)
{
if (_parent != null && _presenter != null && string.IsNullOrEmpty(_presenter.PreeditText))
{ {
SetPreeditText(null); var start = _presenter.SelectionStart;
_parent.SelectionStart = start - beforeLength;
_parent.SelectionEnd = start + afterLength;
_parent.DeleteSelection(true);
} }
TextViewVisualChanged?.Invoke(this, EventArgs.Empty);
CursorRectangleChanged?.Invoke(this, EventArgs.Empty);
} }
} }
} }

Loading…
Cancel
Save