diff --git a/src/Android/Avalonia.Android/AndroidInputMethod.cs b/src/Android/Avalonia.Android/AndroidInputMethod.cs index b68293949d..d3b9700253 100644 --- a/src/Android/Avalonia.Android/AndroidInputMethod.cs +++ b/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(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(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; }); } diff --git a/src/Android/Avalonia.Android/InputEditable.cs b/src/Android/Avalonia.Android/InputEditable.cs index b702b0205c..2463280769 100644 --- a/src/Android/Avalonia.Android/InputEditable.cs +++ b/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; } } } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index b6bcc4ac9f..df35b069dc 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/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); } diff --git a/src/Avalonia.Base/Input/TextInput/ITextEditable.cs b/src/Avalonia.Base/Input/TextInput/ITextEditable.cs new file mode 100644 index 0000000000..4d86ef8d5a --- /dev/null +++ b/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; } + } +} diff --git a/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs b/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs index 8239bc6a21..2cdcd33626 100644 --- a/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs +++ b/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs @@ -49,6 +49,11 @@ namespace Avalonia.Input.TextInput /// event EventHandler? SurroundingTextChanged; + /// + /// Gets or sets a platform editable. Text and selection changes made in the editable are forwarded to the IM client. + /// + ITextEditable? TextEditable { get; set; } + void SelectInSurroundingText(int start, int end); } diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index 239501aace..6713fed244 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/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; + } } }