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
{
public ITextInputMethodClient Client { get; }
public bool IsActive { get; }
}
class AndroidInputMethod<TView> : ITextInputMethodImpl, IAndroidInputMethod
where TView: View, IInitEditorInfo
where TView : View, IInitEditorInfo
{
private readonly TView _host;
private readonly InputMethodManager _imm;
@ -51,16 +51,42 @@ namespace Avalonia.Android
public void SetClient(ITextInputMethodClient client)
{
if(client is null)
{
_inputConnection?.SetComposingText("", 0);
}
if (_client != null)
{
_client.SurroundingTextChanged -= SurroundingTextChanged;
}
Reset();
_client = client;
if (IsActive)
{
_client.SurroundingTextChanged += SurroundingTextChanged;
_host.RequestFocus();
Reset();
_imm.ShowSoftInput(_host, ShowFlags.Implicit);
}
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)
@ -69,10 +95,12 @@ namespace Avalonia.Android
public void SetOptions(TextInputOptions options)
{
_inputConnection = new InputConnectionImpl(_host, this);
_host.InitEditorInfo((_host, outAttrs) =>
{
_inputConnection?.CommitText("", 0);
_inputConnection = new InputConnectionImpl(_host, this);
outAttrs.InputType = options.ContentType switch
{
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.Storage;
using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Input;
@ -25,6 +26,7 @@ using Avalonia.Platform.Storage;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Java.Lang;
using static System.Net.Mime.MediaTypeNames;
namespace Avalonia.Android.Platform.SkiaPlatform
{
@ -36,7 +38,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
private readonly AndroidKeyboardEventsHelper<TopLevelImpl> _keyboardHelper;
private readonly AndroidMotionEventsHelper _pointerHelper;
private readonly ITextInputMethodImpl _textInputMethod;
private readonly AndroidInputMethod<ViewImpl> _textInputMethod;
private ViewImpl _view;
public TopLevelImpl(AvaloniaView avaloniaView, bool placeOnTop = false)
@ -211,7 +213,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform
}
private Func<View, EditorInfo, InputConnectionImpl> _initEditorInfo;
protected InputConnectionImpl _inputConnection;
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;
@ -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)
{
CompositionText = text.ToString();
if (_inputMethod.IsActive)
{
_inputMethod.Client.SetPreeditText(text.ToString());
_inputMethod.Client.SetPreeditText(CompositionText);
}
return base.SetComposingText(text, newCursorPosition);
@ -283,6 +366,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public override bool CommitText(ICharSequence text, int newCursorPosition)
{
CompositionText = null;
if (_inputMethod.IsActive)
{
_inputMethod.Client.SetPreeditText(null);
@ -290,5 +375,17 @@ namespace Avalonia.Android.Platform.SkiaPlatform
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
/// </summary>
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>
/// Returns the text before the cursor. Must return a non-empty string if cursor is not at the beginning of the text entry
/// </summary>

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

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

2
src/Avalonia.Controls/TextBox.cs

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

31
src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

@ -3,6 +3,7 @@ using System.Diagnostics;
using Avalonia.Controls.Presenters;
using Avalonia.Input;
using Avalonia.Input.TextInput;
using Avalonia.Media;
using Avalonia.VisualTree;
namespace Avalonia.Controls
@ -56,7 +57,7 @@ namespace Avalonia.Controls
public TextInputMethodSurroundingText SurroundingText => new()
{
Text = _presenter?.Text ?? "",
CursorOffset = _presenter?.CaretIndex ?? 0,
CursorOffset = _presenter?.SelectionEnd ?? 0,
AnchorOffset = _presenter?.SelectionStart ?? 0
};
@ -78,10 +79,14 @@ namespace Avalonia.Controls
{
if (e.Property == TextBox.TextProperty || e.Property == TextBox.SelectionStartProperty ||
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)
{
if (_parent != null)
@ -98,6 +103,8 @@ namespace Avalonia.Controls
if (_presenter != null)
{
_presenter.PreeditText = null;
_presenter.CaretBoundsChanged -= OnCaretBoundsChanged;
}
@ -107,14 +114,22 @@ namespace Avalonia.Controls
{
_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