From be9a26cbb1d62e5e4621c96db698c20a872d6a6f Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 23 Jun 2023 10:22:07 +0200 Subject: [PATCH 01/16] Rework ITextInputMethodClient --- samples/MobileSandbox.Android/MainActivity.cs | 15 +- .../MobileSandbox.Android.csproj | 4 + .../Avalonia.Android/AndroidInputMethod.cs | 70 +++-- .../Avalonia.Android/Avalonia.Android.csproj | 3 + src/Android/Avalonia.Android/InputEditable.cs | 127 -------- .../Platform/SkiaPlatform/TopLevelImpl.cs | 172 ++++++++--- .../Input/TextInput/ITextEditable.cs | 23 -- .../Input/TextInput/ITextInputMethodClient.cs | 66 ---- .../Input/TextInput/ITextInputMethodImpl.cs | 2 +- .../Input/TextInput/InputMethodManager.cs | 4 +- .../Input/TextInput/TextInputMethodClient.cs | 136 ++++++++ ...TextInputMethodClientRequestedEventArgs.cs | 2 +- .../Presenters/TextPresenter.cs | 94 +++--- src/Avalonia.Controls/TextBox.cs | 4 +- .../TextBoxTextInputMethodClient.cs | 292 ++++++------------ .../DBusIme/DBusTextInputMethodBase.cs | 6 +- .../AvaloniaNativeTextInputMethod.cs | 18 +- src/Avalonia.X11/X11Window.Xim.cs | 6 +- src/Browser/Avalonia.Browser/AvaloniaView.cs | 18 +- .../Avalonia.Win32/Input/Imm32InputMethod.cs | 4 +- src/iOS/Avalonia.iOS/AvaloniaView.Text.cs | 2 +- src/iOS/Avalonia.iOS/AvaloniaView.cs | 2 +- src/iOS/Avalonia.iOS/TextInputResponder.cs | 36 ++- 23 files changed, 543 insertions(+), 563 deletions(-) delete mode 100644 src/Android/Avalonia.Android/InputEditable.cs delete mode 100644 src/Avalonia.Base/Input/TextInput/ITextEditable.cs delete mode 100644 src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs create mode 100644 src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs diff --git a/samples/MobileSandbox.Android/MainActivity.cs b/samples/MobileSandbox.Android/MainActivity.cs index 807afb0bd0..bc1be42f12 100644 --- a/samples/MobileSandbox.Android/MainActivity.cs +++ b/samples/MobileSandbox.Android/MainActivity.cs @@ -1,5 +1,7 @@ -using Android.App; +using System; +using Android.App; using Android.Content.PM; +using Avalonia; using Avalonia.Android; namespace MobileSandbox.Android @@ -7,5 +9,16 @@ namespace MobileSandbox.Android [Activity(Label = "MobileSandbox.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", MainLauncher = true, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)] public class MainActivity : AvaloniaMainActivity { + protected override void OnCreate(Bundle savedInstanceState) + { + AppDomain.CurrentDomain.UnhandledException += CurrentDomainUnhandledException; + + base.OnCreate(savedInstanceState); + } + + private void CurrentDomainUnhandledException(object sender, UnhandledExceptionEventArgs e) + { + System.Diagnostics.Debug.WriteLine(e.ToString()); + } } } diff --git a/samples/MobileSandbox.Android/MobileSandbox.Android.csproj b/samples/MobileSandbox.Android/MobileSandbox.Android.csproj index 3b67d79f7b..54cfea83fa 100644 --- a/samples/MobileSandbox.Android/MobileSandbox.Android.csproj +++ b/samples/MobileSandbox.Android/MobileSandbox.Android.csproj @@ -38,6 +38,10 @@ True + + True + + diff --git a/src/Android/Avalonia.Android/AndroidInputMethod.cs b/src/Android/Avalonia.Android/AndroidInputMethod.cs index 2bf3486ca5..fc00143a9a 100644 --- a/src/Android/Avalonia.Android/AndroidInputMethod.cs +++ b/src/Android/Avalonia.Android/AndroidInputMethod.cs @@ -1,10 +1,10 @@ using System; using Android.Content; using Android.Runtime; +using Android.Text; using Android.Views; using Android.Views.InputMethods; using Avalonia.Android.Platform.SkiaPlatform; -using Avalonia.Controls.Presenters; using Avalonia.Input.TextInput; namespace Avalonia.Android @@ -13,7 +13,7 @@ namespace Avalonia.Android { public View View { get; } - public ITextInputMethodClient Client { get; } + public TextInputMethodClient Client { get; } public bool IsActive { get; } @@ -36,7 +36,7 @@ namespace Avalonia.Android { private readonly TView _host; private readonly InputMethodManager _imm; - private ITextInputMethodClient _client; + private TextInputMethodClient _client; private AvaloniaInputConnection _inputConnection; public AndroidInputMethod(TView host) @@ -56,7 +56,7 @@ namespace Avalonia.Android public bool IsActive => Client != null; - public ITextInputMethodClient Client => _client; + public TextInputMethodClient Client => _client; public InputMethodManager IMM => _imm; @@ -65,7 +65,7 @@ namespace Avalonia.Android } - public void SetClient(ITextInputMethodClient client) + public void SetClient(TextInputMethodClient client) { _client = client; @@ -77,9 +77,24 @@ namespace Avalonia.Android _imm.ShowSoftInput(_host, ShowFlags.Implicit); - var surroundingText = Client.SurroundingText; + var selection = Client.Selection; - _imm.UpdateSelection(_host, surroundingText.AnchorOffset, surroundingText.CursorOffset, surroundingText.AnchorOffset, surroundingText.CursorOffset); + _imm.UpdateSelection(_host, selection.Start, selection.End, selection.Start, selection.End); + + var surroundingText = _client.SurroundingText ?? ""; + + var extractedText = new ExtractedText + { + Text = new Java.Lang.String(surroundingText), + SelectionStart = selection.Start, + SelectionEnd = selection.End, + PartialEndOffset = surroundingText.Length + }; + + _imm.UpdateExtractedText(_host, _inputConnection?.ExtractedTextToken ?? 0, extractedText); + + _client.SurroundingTextChanged += _client_SurroundingTextChanged; + _client.SelectionChanged += _client_SelectionChanged; } else { @@ -87,6 +102,30 @@ namespace Avalonia.Android } } + private void _client_SelectionChanged(object sender, EventArgs e) + { + var selection = Client.Selection; + + _imm.UpdateSelection(_host, selection.Start, selection.End, selection.Start, selection.End); + } + + private void _client_SurroundingTextChanged(object sender, EventArgs e) + { + var surroundingText = _client.SurroundingText ?? ""; + + _inputConnection.EditableWrapper.IgnoreChange = true; + + _inputConnection.Editable.Replace(0, _inputConnection.Editable.Length(), surroundingText); + + _inputConnection.EditableWrapper.IgnoreChange = false; + + var selection = Client.Selection; + + _imm.UpdateSelection(_host, selection.Start, selection.End, selection.Start, selection.End); + + //Debug.WriteLine($"SurroundingText: {surroundingText}, CaretIndex: {selection.Start}"); + } + public void SetCursorRect(Rect rect) { @@ -134,25 +173,8 @@ namespace Avalonia.Android outAttrs.ImeOptions |= ImeFlags.NoFullscreen | ImeFlags.NoExtractUi; - _client.TextEditable = _inputConnection.InputEditable; - return _inputConnection; }); } } - - internal readonly record struct ComposingRegion - { - private readonly int _start = -1; - private readonly int _end = -1; - - public ComposingRegion(int start, int end) - { - _start = start; - _end = end; - } - - public int Start => _start; - public int End => _end; - } } diff --git a/src/Android/Avalonia.Android/Avalonia.Android.csproj b/src/Android/Avalonia.Android/Avalonia.Android.csproj index d8b0c3d534..e04a1acde1 100644 --- a/src/Android/Avalonia.Android/Avalonia.Android.csproj +++ b/src/Android/Avalonia.Android/Avalonia.Android.csproj @@ -7,6 +7,9 @@ portable Avalonia.Android.Internal + + True + diff --git a/src/Android/Avalonia.Android/InputEditable.cs b/src/Android/Avalonia.Android/InputEditable.cs deleted file mode 100644 index c5b68d2652..0000000000 --- a/src/Android/Avalonia.Android/InputEditable.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System; -using Android.Runtime; -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, 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; - - public event EventHandler TextChanged; - public event EventHandler SelectionChanged; - public event EventHandler CompositionChanged; - - public InputEditable(TopLevelImpl topLevel, IAndroidInputMethod inputMethod, AvaloniaInputConnection avaloniaInputConnection) - { - _topLevel = topLevel; - _inputMethod = inputMethod; - _avaloniaInputConnection = avaloniaInputConnection; - } - - 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 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 int CompositionStart => BaseInputConnection.GetComposingSpanStart(this); - - public int CompositionEnd => BaseInputConnection.GetComposingSpanEnd(this); - - public void BeginBatchEdit() - { - _currentBatchLevel++; - - if (_currentBatchLevel == 1) - { - _previousText = ToString(); - _previousSelectionStart = SelectionStart; - _previousSelectionEnd = SelectionEnd; - } - } - - public void EndBatchEdit() - { - if (_currentBatchLevel == 1) - { - if(_previousText != Text) - { - TextChanged?.Invoke(this, EventArgs.Empty); - } - - if (_previousSelectionStart != SelectionStart || _previousSelectionEnd != SelectionEnd) - { - SelectionChanged?.Invoke(this, EventArgs.Empty); - } - } - - _currentBatchLevel--; - } - - public void RaiseCompositionChanged() - { - CompositionChanged?.Invoke(this, EventArgs.Empty); - } - } -} diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index a29f06a106..f725240c22 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -11,6 +11,7 @@ using Android.Text; using Android.Views; using Android.Views.InputMethods; using AndroidX.AppCompat.App; +using Avalonia.Android.Platform.Input; using Avalonia.Android.Platform.Specific; using Avalonia.Android.Platform.Specific.Helpers; using Avalonia.Android.Platform.Storage; @@ -429,78 +430,124 @@ namespace Avalonia.Android.Platform.SkiaPlatform activity.Window.Attributes = attr; } } + + internal void TextInput(string text) + { + if(Input != null) + { + var args = new RawTextInputEventArgs(AndroidKeyboardDevice.Instance, (ulong)DateTime.Now.Ticks, InputRoot, text); + + Input(args); + } + } } - internal class AvaloniaInputConnection : BaseInputConnection + internal class EditableWrapper : SpannableStringBuilder { - private readonly TopLevelImpl _topLevel; - private readonly IAndroidInputMethod _inputMethod; - private readonly InputEditable _editable; + private readonly AvaloniaInputConnection _inputConnection; - public AvaloniaInputConnection(TopLevelImpl topLevel, IAndroidInputMethod inputMethod) : base(inputMethod.View, true) + public EditableWrapper(AvaloniaInputConnection inputConnection) { - _topLevel = topLevel; - _inputMethod = inputMethod; - _editable = new InputEditable(_topLevel, _inputMethod, this); + _inputConnection = inputConnection; } - public override IEditable Editable => _editable; - - internal InputEditable InputEditable => _editable; + public bool IgnoreChange { get; set; } - public override bool SetComposingRegion(int start, int end) + public override IEditable Replace(int start, int end, ICharSequence tb) { - var ret = base.SetComposingRegion(start, end); + if (!IgnoreChange && !_inputConnection.IsComposing && start != end) + { + var text = tb.SubSequence(0, tb.Length()); - InputEditable.RaiseCompositionChanged(); + //System.Diagnostics.Debug.WriteLine($"Replace: start: {start}, end: {end}, text: {text}"); - return ret; + _inputConnection.InputMethod.Client.Selection = new TextSelection(start, end); + } + + return base.Replace(start, end, tb); } - public override bool SetComposingText(ICharSequence text, int newCursorPosition) + public override IEditable Replace(int start, int end, ICharSequence tb, int tbstart, int tbend) { - var composingText = text.ToString(); - - if (string.IsNullOrEmpty(composingText)) - { - return CommitText(text, newCursorPosition); - } - else + if (!IgnoreChange && !_inputConnection.IsComposing && start != end) { - var ret = base.SetComposingText(text, newCursorPosition); + var text = tb.SubSequence(tbstart, tbend); - InputEditable.RaiseCompositionChanged(); + //System.Diagnostics.Debug.WriteLine($"Replace: start: {start}, end: {end}, text: {text}"); - return ret; + _inputConnection.InputMethod.Client.Selection = new TextSelection(start, end); } + + return base.Replace(start, end, tb, tbstart, tbend); } + } - public override bool BeginBatchEdit() - { - _editable.BeginBatchEdit(); + internal class AvaloniaInputConnection : BaseInputConnection + { + private readonly TopLevelImpl _toplevel; + private readonly IAndroidInputMethod _inputMethod; + private readonly EditableWrapper _editable; + private string _compositionText; + private bool _commitInProgress; - return base.BeginBatchEdit(); + public AvaloniaInputConnection(TopLevelImpl toplevel, IAndroidInputMethod inputMethod) : base(inputMethod.View, true) + { + _toplevel = toplevel; + _inputMethod = inputMethod; + _editable = new EditableWrapper(this); } - public override bool EndBatchEdit() - { - var ret = base.EndBatchEdit(); - _editable.EndBatchEdit(); + public bool IsComposing => !string.IsNullOrEmpty(_compositionText); - return ret; - } + public int ExtractedTextToken { get; private set; } + + public override IEditable Editable => _editable; + + public EditableWrapper EditableWrapper => _editable; + + public IAndroidInputMethod InputMethod => _inputMethod; - public override bool FinishComposingText() + public override bool SetComposingText(ICharSequence text, int newCursorPosition) { - var ret = base.FinishComposingText(); - InputEditable.RaiseCompositionChanged(); - return ret; + _compositionText = text.SubSequence(0, text.Length()); + + System.Diagnostics.Debug.WriteLine($"Composition Changed: {_compositionText}"); + + if(_inputMethod.IsActive && !_commitInProgress) + { + _inputMethod.Client.SetPreeditText(_compositionText); + } + + return base.SetComposingText(text, newCursorPosition); } public override bool CommitText(ICharSequence text, int newCursorPosition) { + _commitInProgress = true; + var ret = base.CommitText(text, newCursorPosition); - InputEditable.RaiseCompositionChanged(); + + var committedText = text.SubSequence(0, text.Length()); + + if (string.IsNullOrEmpty(committedText)) + { + committedText = _compositionText; + } + + if (_inputMethod.IsActive && !string.IsNullOrEmpty(committedText)) + { + if (!string.IsNullOrEmpty(_compositionText)) + { + _inputMethod.Client.SetPreeditText(null); + } + + _toplevel.TextInput(committedText); + + _compositionText = null; + } + + _commitInProgress = false; + return ret; } @@ -517,5 +564,48 @@ namespace Avalonia.Android.Platform.SkiaPlatform return base.PerformEditorAction(actionCode); } + + public override ExtractedText GetExtractedText(ExtractedTextRequest request, [GeneratedEnum] GetTextFlags flags) + { + if (request == null) + return null; + + ExtractedTextToken = request.Token; + + var editable = Editable; + + if (editable == null) + { + return null; + } + + if (!_inputMethod.IsActive) + { + return null; + } + + var selection = _inputMethod.Client.Selection; + + ExtractedText extract = new ExtractedText + { + Flags = 0, + PartialStartOffset = -1, + PartialEndOffset = -1, + SelectionStart = selection.Start, + SelectionEnd = selection.End, + StartOffset = 0 + }; + + if ((request.Flags & GetTextFlags.WithStyles) != 0) + { + extract.Text = new SpannableString(editable); + } + else + { + extract.Text = editable; + } + + return extract; + } } } diff --git a/src/Avalonia.Base/Input/TextInput/ITextEditable.cs b/src/Avalonia.Base/Input/TextInput/ITextEditable.cs deleted file mode 100644 index f9f5ec8d40..0000000000 --- a/src/Avalonia.Base/Input/TextInput/ITextEditable.cs +++ /dev/null @@ -1,23 +0,0 @@ -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; - event EventHandler CompositionChanged; - int SelectionStart { get; set; } - int SelectionEnd { get; set; } - int CompositionStart { get; } - int CompositionEnd { get; } - - string? Text { get; set; } - } -} diff --git a/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs b/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs deleted file mode 100644 index 2cdcd33626..0000000000 --- a/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using Avalonia.Media.TextFormatting; -using Avalonia.VisualTree; - -namespace Avalonia.Input.TextInput -{ - public interface ITextInputMethodClient - { - /// - /// The cursor rectangle relative to the TextViewVisual - /// - Rect CursorRectangle { get; } - /// - /// Should be fired when cursor rectangle is changed inside the TextViewVisual - /// - event EventHandler? CursorRectangleChanged; - /// - /// The visual that's showing the text - /// - Visual TextViewVisual { get; } - /// - /// Should be fired when text-hosting visual is changed - /// - event EventHandler? TextViewVisualChanged; - /// - /// Indicates if TextViewVisual is capable of displaying non-committed input on the cursor position - /// - bool SupportsPreedit { get; } - /// - /// Sets the non-committed input string - /// - void SetPreeditText(string? text); - - /// - /// Sets the current composing region. This doesn't remove the composing text from the commited text. - /// - void SetComposingRegion(TextRange? region); - - /// - /// Indicates if text input client is capable of providing the text around the cursor - /// - bool SupportsSurroundingText { get; } - /// - /// Returns the text around the cursor, usually the current paragraph, the cursor position inside that text and selection start position - /// - TextInputMethodSurroundingText SurroundingText { get; } - /// - /// Should be fired when surrounding text changed - /// - 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); - } - - public record struct TextInputMethodSurroundingText - { - public string Text { get; set; } - public int CursorOffset { get; set; } - public int AnchorOffset { get; set; } - } -} diff --git a/src/Avalonia.Base/Input/TextInput/ITextInputMethodImpl.cs b/src/Avalonia.Base/Input/TextInput/ITextInputMethodImpl.cs index be7ad81f81..2969b2e60d 100644 --- a/src/Avalonia.Base/Input/TextInput/ITextInputMethodImpl.cs +++ b/src/Avalonia.Base/Input/TextInput/ITextInputMethodImpl.cs @@ -5,7 +5,7 @@ namespace Avalonia.Input.TextInput [Unstable] public interface ITextInputMethodImpl { - void SetClient(ITextInputMethodClient? client); + void SetClient(TextInputMethodClient? client); void SetCursorRect(Rect rect); void SetOptions(TextInputOptions options); void Reset(); diff --git a/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs b/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs index c7fca04ea9..387179b970 100644 --- a/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs +++ b/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs @@ -7,7 +7,7 @@ namespace Avalonia.Input.TextInput { private ITextInputMethodImpl? _im; private IInputElement? _focusedElement; - private ITextInputMethodClient? _client; + private TextInputMethodClient? _client; private readonly TransformTrackingHelper _transformTracker = new TransformTrackingHelper(); public TextInputMethodManager() @@ -16,7 +16,7 @@ namespace Avalonia.Input.TextInput InputMethod.IsInputMethodEnabledProperty.Changed.Subscribe(OnIsInputMethodEnabledChanged); } - private ITextInputMethodClient? Client + private TextInputMethodClient? Client { get => _client; set diff --git a/src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs b/src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs new file mode 100644 index 0000000000..87f8d7ba97 --- /dev/null +++ b/src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs @@ -0,0 +1,136 @@ +using System; + +namespace Avalonia.Input.TextInput +{ + public abstract class TextInputMethodClient + { + private Rect _cursorRectangle; + private string _surroundingText = ""; + private TextSelection _selection; + + /// + /// Fires when the text view visual has changed + /// + public event EventHandler? TextViewVisualChanged; + + /// + /// Fires when the cursor rectangle has changed + /// + public event EventHandler? CursorRectangleChanged; + + /// + /// Fires when the surrounding text has changed + /// + public event EventHandler? SurroundingTextChanged; + + /// + /// Fires when the selection has changed + /// + public event EventHandler? SelectionChanged; + + /// + /// The visual that's showing the text + /// + public abstract Visual TextViewVisual { get; } + + /// + /// Indicates if TextViewVisual is capable of displaying non-committed input on the cursor position + /// + public abstract bool SupportsPreedit { get; } + + /// + /// Indicates if text input client is capable of providing the text around the cursor + /// + public abstract bool SupportsSurroundingText { get; } + + /// + /// Returns the text around the cursor, usually the current paragraph + /// + public string SurroundingText + { + get => _surroundingText; + set + { + var oldValue = _surroundingText; + + if (oldValue == value) + { + return; + } + + _surroundingText = value; + + OnSurroundingTextChanged(oldValue, value); + } + } + + /// + /// Gets the cursor rectangle relative to the TextViewVisual + /// + public Rect CursorRectangle + { + get => _cursorRectangle; + protected set + { + var oldvalue = _cursorRectangle; + + if (oldvalue == value) + { + return; + } + + _cursorRectangle = value; + + OnCursorRectangleChanged(oldvalue, value); + } + } + + /// + /// Gets or sets the curent selection range within current surrounding text. + /// + public TextSelection Selection + { + get => _selection; + set + { + var oldValue = _selection; + + if (oldValue == value) + { + return; + } + + _selection = value; + + OnSelectionChanged(oldValue, value); + } + } + + /// + /// Sets the non-committed input string + /// + public virtual void SetPreeditText(string? preeditText) { } + + protected virtual void OnCursorRectangleChanged(Rect oldValue, Rect newValue) + { + CursorRectangleChanged?.Invoke(this, EventArgs.Empty); + } + + protected virtual void OnTextViewVisualChanged(Visual? oldValue, Visual? newValue) + { + TextViewVisualChanged?.Invoke(this, EventArgs.Empty); + } + + protected virtual void OnSurroundingTextChanged(string? oldValue, string? newValue) + { + SurroundingTextChanged?.Invoke(this, EventArgs.Empty); + } + + protected virtual void OnSelectionChanged(TextSelection oldValue, TextSelection newValue) + { + SelectionChanged?.Invoke(this, EventArgs.Empty); + } + } + + public record struct TextSelection(int Start, int End); +} diff --git a/src/Avalonia.Base/Input/TextInput/TextInputMethodClientRequestedEventArgs.cs b/src/Avalonia.Base/Input/TextInput/TextInputMethodClientRequestedEventArgs.cs index bec43487d2..d90d4257fa 100644 --- a/src/Avalonia.Base/Input/TextInput/TextInputMethodClientRequestedEventArgs.cs +++ b/src/Avalonia.Base/Input/TextInput/TextInputMethodClientRequestedEventArgs.cs @@ -7,6 +7,6 @@ namespace Avalonia.Input.TextInput /// /// Set this property to a valid text input client to enable input method interaction /// - public ITextInputMethodClient? Client { get; set; } + public TextInputMethodClient? Client { get; set; } } } diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index c5b4834ded..e3949a3255 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -50,15 +50,6 @@ namespace Avalonia.Controls.Presenters public static readonly StyledProperty PreeditTextProperty = AvaloniaProperty.Register(nameof(PreeditText)); - /// - /// Defines the property. - /// - public static readonly DirectProperty CompositionRegionProperty = - AvaloniaProperty.RegisterDirect( - nameof(CompositionRegion), - o => o.CompositionRegion, - (o, v) => o.CompositionRegion = v); - /// /// Defines the property. /// @@ -97,7 +88,6 @@ namespace Avalonia.Controls.Presenters private CharacterHit _lastCharacterHit; private Rect _caretBounds; private Point _navigationPosition; - private TextRange? _compositionRegion; static TextPresenter() { @@ -137,12 +127,6 @@ namespace Avalonia.Controls.Presenters set => SetValue(PreeditTextProperty, value); } - public TextRange? CompositionRegion - { - get => _compositionRegion; - set => SetAndRaise(CompositionRegionProperty, ref _compositionRegion, value); - } - /// /// Gets or sets the font family. /// @@ -490,10 +474,10 @@ namespace Avalonia.Controls.Presenters { TextLayout result; - var text = Text; - + var caretIndex = CaretIndex; + var preeditText = PreeditText; + var text = GetCombinedText(Text, caretIndex, preeditText); var typeface = new Typeface(FontFamily, FontStyle, FontWeight); - var selectionStart = SelectionStart; var selectionEnd = SelectionEnd; var start = Math.Min(selectionStart, selectionEnd); @@ -503,22 +487,9 @@ namespace Avalonia.Controls.Presenters var foreground = Foreground; - if (_compositionRegion != null) - { - var preeditHighlight = new ValueSpan(_compositionRegion?.Start ?? 0, _compositionRegion?.Length ?? 0, - new GenericTextRunProperties(typeface, FontSize, - foregroundBrush: foreground, - textDecorations: TextDecorations.Underline)); - - textStyleOverrides = new[] - { - preeditHighlight - }; - - } - else if (!string.IsNullOrEmpty(PreeditText)) + if (!string.IsNullOrEmpty(preeditText)) { - var preeditHighlight = new ValueSpan(CaretIndex, PreeditText.Length, + var preeditHighlight = new ValueSpan(caretIndex, preeditText.Length, new GenericTextRunProperties(typeface, FontSize, foregroundBrush: foreground, textDecorations: TextDecorations.Underline)); @@ -554,6 +525,27 @@ namespace Avalonia.Controls.Presenters return result; } + private static string? GetCombinedText(string? text, int caretIndex, string? preeditText) + { + if (string.IsNullOrEmpty(preeditText)) + { + return text; + } + + if (string.IsNullOrEmpty(text)) + { + return preeditText; + } + + var sb = StringBuilderCache.Acquire(text.Length + preeditText.Length); + + sb.Append(text.Substring(0, caretIndex)); + sb.Insert(caretIndex, preeditText); + sb.Append(text.Substring(caretIndex)); + + return StringBuilderCache.GetStringAndRelease(sb); + } + protected virtual void InvalidateTextLayout() { _textLayout?.Dispose(); @@ -831,6 +823,18 @@ namespace Avalonia.Controls.Presenters _caretTimer.Tick -= CaretTimerTick; } + private void OnPreeditTextChanged(string? preeditText) + { + if (string.IsNullOrEmpty(preeditText)) + { + UpdateCaret(new CharacterHit(CaretIndex), false); + } + else + { + UpdateCaret(new CharacterHit(CaretIndex + preeditText.Length), false); + } + } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -840,10 +844,30 @@ namespace Avalonia.Controls.Presenters MoveCaretToTextPosition(change.GetNewValue()); } + if(change.Property == PreeditTextProperty) + { + OnPreeditTextChanged(change.NewValue as string); + } + + if(change.Property == TextProperty) + { + if (!string.IsNullOrEmpty(PreeditText)) + { + PreeditText = null; + } + } + + if(change.Property == CaretIndexProperty) + { + if (!string.IsNullOrEmpty(PreeditText)) + { + PreeditText = null; + } + } + switch (change.Property.Name) { case nameof(PreeditText): - case nameof(CompositionRegion): case nameof(Foreground): case nameof(FontSize): case nameof(FontStyle): diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 3016dc8239..f8eabf670c 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -975,7 +975,9 @@ namespace Avalonia.Controls textBuilder.Insert(caretIndex, input); - SetCurrentValue(TextProperty, StringBuilderCache.GetStringAndRelease(textBuilder)); + var text = StringBuilderCache.GetStringAndRelease(textBuilder); + + SetCurrentValue(TextProperty, text); ClearSelection(); diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index e999d0bb74..33a0482c68 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -1,132 +1,80 @@ using System; -using System.Diagnostics; using Avalonia.Controls.Presenters; using Avalonia.Input.TextInput; -using Avalonia.Media; using Avalonia.Media.TextFormatting; -using Avalonia.Threading; using Avalonia.Utilities; namespace Avalonia.Controls { - internal class TextBoxTextInputMethodClient : ITextInputMethodClient + internal class TextBoxTextInputMethodClient : TextInputMethodClient { private TextBox? _parent; private TextPresenter? _presenter; - private ITextEditable? _textEditable; + private bool _isPropertyChange; - public Visual TextViewVisual => _presenter!; + public override Visual TextViewVisual => _presenter!; - public bool SupportsPreedit => true; + public override bool SupportsPreedit => true; - public bool SupportsSurroundingText => true; + public override bool SupportsSurroundingText => true; - public Rect CursorRectangle + public void SetPresenter(TextPresenter? presenter, TextBox? parent) { - get + if (_parent != null) { - if (_parent == null || _presenter == null) - { - return default; - } - - var transform = _presenter.TransformToVisual(_parent); - - if (transform == null) - { - return default; - } - - var rect = _presenter.GetCursorRectangle().TransformToAABB(transform.Value); - - return rect; + _parent.PropertyChanged -= OnParentPropertyChanged; } - } - - public TextInputMethodSurroundingText SurroundingText - { - get - { - if (_presenter is null || _parent is null) - { - return default; - } - - var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(_presenter.CaretIndex, false); - var textLine = _presenter.TextLayout.TextLines[lineIndex]; - - var lineStart = textLine.FirstTextSourceIndex; + _parent = parent; - var lineText = GetTextLineText(textLine); + if (_parent != null) + { + _parent.PropertyChanged += OnParentPropertyChanged; + } - var anchorOffset = Math.Max(0, _parent.SelectionStart - lineStart); + var oldPresenter = _presenter; - var cursorOffset = Math.Max(0, _presenter.SelectionEnd - lineStart); + if (oldPresenter != null) + { + oldPresenter.ClearValue(TextPresenter.PreeditTextProperty); - return new TextInputMethodSurroundingText - { - Text = lineText ?? "", - AnchorOffset = anchorOffset, - CursorOffset = cursorOffset - }; + oldPresenter.CaretBoundsChanged -= OnPresenterCursorRectangleChanged; } - } - public ITextEditable? TextEditable - { - get => _textEditable; set + _presenter = presenter; + + if (_presenter != null) { - if (_textEditable != null) - { - _textEditable.TextChanged -= TextEditable_TextChanged; - _textEditable.SelectionChanged -= TextEditable_SelectionChanged; - _textEditable.CompositionChanged -= TextEditable_CompositionChanged; - } + _presenter.CaretBoundsChanged += OnPresenterCursorRectangleChanged; + } - _textEditable = value; + OnTextViewVisualChanged(oldPresenter, presenter); - if (_textEditable != null) - { - _textEditable.TextChanged += TextEditable_TextChanged; - _textEditable.SelectionChanged += TextEditable_SelectionChanged; - _textEditable.CompositionChanged += TextEditable_CompositionChanged; - - if (_presenter != null) - { - _textEditable.Text = _presenter.Text; - _textEditable.SelectionStart = _presenter.SelectionStart; - _textEditable.SelectionEnd = _presenter.SelectionEnd; - } - } - } + OnPresenterCursorRectangleChanged(this, EventArgs.Empty); } - private void TextEditable_CompositionChanged(object? sender, EventArgs e) + public override void SetPreeditText(string? preeditText) { - if (_presenter != null && _textEditable != null) + if (_presenter == null || _parent == null) { - _presenter.SetCurrentValue(TextPresenter.CompositionRegionProperty, new TextRange(_textEditable.CompositionStart, _textEditable.CompositionEnd)); + return; } + + _presenter.SetCurrentValue(TextPresenter.PreeditTextProperty, preeditText); } - private void TextEditable_SelectionChanged(object? sender, EventArgs e) + protected override void OnSelectionChanged(TextSelection oldValue, TextSelection newValue) { - if (_parent != null && _textEditable != null) + base.OnSelectionChanged(oldValue, newValue); + + if (_isPropertyChange) { - _parent.SelectionStart = _textEditable.SelectionStart; - _parent.SelectionEnd = _textEditable.SelectionEnd; + return; } - } - private void TextEditable_TextChanged(object? sender, EventArgs e) - { - if (_parent != null) + if (oldValue != newValue) { - if (_parent.Text != _textEditable?.Text) - { - _parent.Text = _textEditable?.Text; - } + SetParentSelection(newValue); } } @@ -153,166 +101,112 @@ namespace Avalonia.Controls return lineText; } - public event EventHandler? TextViewVisualChanged; - - public event EventHandler? CursorRectangleChanged; - - public event EventHandler? SurroundingTextChanged; - - private string? _presenterText; - private int _compositionStart; - - public void SetPreeditText(string? preeditText) + private void OnParentTextChanged() { - if (_presenter == null || _parent == null) + if (_presenter is null || _parent is null) { - return; - } + SurroundingText = ""; - if (_presenterText is null) - { - _presenterText = _parent.Text ?? ""; - _compositionStart = _parent.CaretIndex; + return; } - var text = GetText(preeditText); - - _presenter.SetCurrentValue(TextPresenter.TextProperty, text); - - _presenter.SetCurrentValue(TextPresenter.PreeditTextProperty, preeditText); - - _presenter.UpdateCaret(new CharacterHit(_compositionStart + (preeditText != null ? preeditText.Length : 0)), false); - - if (string.IsNullOrEmpty(preeditText)) +#if DEBUG + if (_parent.CaretIndex != _presenter.CaretIndex) { - _presenterText = null; + throw new InvalidOperationException("TextBox and TextPresenter are out of sync"); } - } - private string? GetText(string? preeditText) - { - if (string.IsNullOrEmpty(preeditText)) + if (_parent.Text != _presenter.Text) { - return _presenterText; + throw new InvalidOperationException("TextBox and TextPresenter are out of sync"); } +#endif - if (string.IsNullOrEmpty(_presenterText)) - { - return preeditText; - } + var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(_presenter.CaretIndex, false); - var sb = StringBuilderCache.Acquire(_presenterText.Length + preeditText.Length); + var textLine = _presenter.TextLayout.TextLines[lineIndex]; - sb.Append(_presenterText); - sb.Insert(_compositionStart, preeditText); + var lineText = GetTextLineText(textLine); - return StringBuilderCache.GetStringAndRelease(sb); + SurroundingText = lineText; } - public void SetComposingRegion(TextRange? region) + private void OnPresenterCursorRectangleChanged(object? sender, EventArgs e) { - if (_presenter == null) + if (_parent == null || _presenter == null) { + CursorRectangle = default; + return; } - _presenter.SetCurrentValue(TextPresenter.CompositionRegionProperty, region); - } + var transform = _presenter.TransformToVisual(_parent); - public void SelectInSurroundingText(int start, int end) - { - if (_parent is null || _presenter is null) + if (transform == null) { + CursorRectangle = default; + return; } - var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(_presenter.CaretIndex, false); - - var textLine = _presenter.TextLayout.TextLines[lineIndex]; - - var lineStart = textLine.FirstTextSourceIndex; - - var selectionStart = lineStart + start; - var selectionEnd = lineStart + end; - - _parent.SelectionStart = selectionStart; - _parent.SelectionEnd = selectionEnd; + CursorRectangle = _presenter.GetCursorRectangle().TransformToAABB(transform.Value); } - public void SetPresenter(TextPresenter? presenter, TextBox? parent) + private void OnParentPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { - if (_parent != null) + _isPropertyChange = true; + + if (e.Property == TextBox.TextProperty) { - _parent.PropertyChanged -= OnParentPropertyChanged; + OnParentTextChanged(); } - _parent = parent; - - if (_parent != null) + if (e.Property == TextBox.SelectionStartProperty || e.Property == TextBox.SelectionEndProperty) { - _parent.PropertyChanged += OnParentPropertyChanged; + Selection = GetParentSelection(); } - if (_presenter != null) + _isPropertyChange = false; + } + + private TextSelection GetParentSelection() + { + if (_presenter is null || _parent is null) { - _presenter.ClearValue(TextPresenter.PreeditTextProperty); + return default; + } - _presenter.ClearValue(TextPresenter.CompositionRegionProperty); + var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(_parent.CaretIndex, false); - _presenter.CaretBoundsChanged -= OnCaretBoundsChanged; - } + var textLine = _presenter.TextLayout.TextLines[lineIndex]; - _presenter = presenter; + var lineStart = textLine.FirstTextSourceIndex; - if (_presenter != null) - { - _presenter.CaretBoundsChanged += OnCaretBoundsChanged; - } + var selectionStart = Math.Max(0, _parent.SelectionStart - lineStart); - TextViewVisualChanged?.Invoke(this, EventArgs.Empty); + var selectionEnd = Math.Max(0, _parent.SelectionEnd - lineStart); - OnCaretBoundsChanged(this, EventArgs.Empty); + return new TextSelection(selectionStart, selectionEnd); } - private void OnParentPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + private void SetParentSelection(TextSelection selection) { - if (e.Property == TextBox.SelectionStartProperty || e.Property == TextBox.SelectionEndProperty) + if (_parent is null || _presenter is null) { - 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; - } - } + return; } - if (e.Property == TextBox.TextProperty) - { - if (_textEditable != null) - { - _textEditable.Text = (string?)e.NewValue; - } - } - } + var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(_parent.CaretIndex, false); - private void OnCaretBoundsChanged(object? sender, EventArgs e) - { - Dispatcher.UIThread.Post(() => - { - CursorRectangleChanged?.Invoke(this, e); + var textLine = _presenter.TextLayout.TextLines[lineIndex]; + + var lineStart = textLine.FirstTextSourceIndex; + + var selectionStart = lineStart + selection.Start; + var selectionEnd = lineStart + selection.End; - }, DispatcherPriority.Input); + _parent.SelectionStart = selectionStart; + _parent.SelectionEnd = selectionEnd; } } } diff --git a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs index 936e856cf0..9ce6604594 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs @@ -41,7 +41,7 @@ namespace Avalonia.FreeDesktop.DBusIme private PixelRect? _lastReportedRect; private double _scaling = 1; private PixelPoint _windowPosition; - private ITextInputMethodClient? _client; + private TextInputMethodClient? _client; protected bool IsConnected => _currentName != null; @@ -53,7 +53,7 @@ namespace Avalonia.FreeDesktop.DBusIme _ = WatchAsync(); } - public ITextInputMethodClient Client => _client; + public TextInputMethodClient Client => _client; public bool IsActive => _client is not null; @@ -210,7 +210,7 @@ namespace Avalonia.FreeDesktop.DBusIme UpdateActive(); } - void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client) + void ITextInputMethodImpl.SetClient(TextInputMethodClient? client) { _client = client; UpdateActive(); diff --git a/src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs b/src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs index 4c93c06ef3..0171ada7b9 100644 --- a/src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs +++ b/src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Input.TextInput; +using Avalonia.Media.TextFormatting; using Avalonia.Native.Interop; #nullable enable @@ -8,7 +9,7 @@ namespace Avalonia.Native { internal class AvaloniaNativeTextInputMethod : ITextInputMethodImpl, IDisposable { - private ITextInputMethodClient? _client; + private TextInputMethodClient? _client; private IAvnTextInputMethodClient? _nativeClient; private readonly IAvnTextInputMethod _inputMethod; @@ -28,7 +29,7 @@ namespace Avalonia.Native _inputMethod.Reset(); } - public void SetClient(ITextInputMethodClient? client) + public void SetClient(TextInputMethodClient? client) { if (_client is { SupportsSurroundingText: true }) { @@ -96,11 +97,12 @@ namespace Avalonia.Native } var surroundingText = _client.SurroundingText; + var selection = _client.Selection; _inputMethod.SetSurroundingText( - surroundingText.Text ?? "", - surroundingText.AnchorOffset, - surroundingText.CursorOffset + surroundingText ?? "", + selection.Start, + selection.End ); } @@ -116,9 +118,9 @@ namespace Avalonia.Native private class AvnTextInputMethodClient : NativeCallbackBase, IAvnTextInputMethodClient { - private readonly ITextInputMethodClient _client; + private readonly TextInputMethodClient _client; - public AvnTextInputMethodClient(ITextInputMethodClient client) + public AvnTextInputMethodClient(TextInputMethodClient client) { _client = client; } @@ -135,7 +137,7 @@ namespace Avalonia.Native { if (_client.SupportsSurroundingText) { - _client.SelectInSurroundingText(start, end); + _client.Selection = new TextSelection(start, end); } } } diff --git a/src/Avalonia.X11/X11Window.Xim.cs b/src/Avalonia.X11/X11Window.Xim.cs index cc5ecad09f..3dc860d612 100644 --- a/src/Avalonia.X11/X11Window.Xim.cs +++ b/src/Avalonia.X11/X11Window.Xim.cs @@ -15,14 +15,14 @@ namespace Avalonia.X11 private readonly X11Window _parent; private bool _windowActive, _imeActive; private Rect? _queuedCursorRect; - private ITextInputMethodClient? _client; + private TextInputMethodClient? _client; public XimInputMethod(X11Window parent) { _parent = parent; } - public ITextInputMethodClient? Client => _client; + public TextInputMethodClient? Client => _client; public bool IsActive => _client != null; @@ -62,7 +62,7 @@ namespace Avalonia.X11 UpdateActive(); } - public void SetClient(ITextInputMethodClient client) + public void SetClient(TextInputMethodClient client) { _client = client; UpdateActive(); diff --git a/src/Browser/Avalonia.Browser/AvaloniaView.cs b/src/Browser/Avalonia.Browser/AvaloniaView.cs index b5dd08d854..0b88d9c3e9 100644 --- a/src/Browser/Avalonia.Browser/AvaloniaView.cs +++ b/src/Browser/Avalonia.Browser/AvaloniaView.cs @@ -42,7 +42,7 @@ namespace Avalonia.Browser private const SKColorType ColorType = SKColorType.Rgba8888; private bool _useGL; - private ITextInputMethodClient? _client; + private TextInputMethodClient? _client; /// ID of the html element where avalonia content should be rendered. public AvaloniaView(string divId) @@ -397,7 +397,7 @@ namespace Avalonia.Browser if(start != -1 && end != -1 && _client != null) { - _client.SelectInSurroundingText(start, end); + _client.Selection = new TextSelection(start, end); } return false; } @@ -513,7 +513,7 @@ namespace Avalonia.Browser InputHelper.FocusElement(_containerElement); } - void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client) + void ITextInputMethodImpl.SetClient(TextInputMethodClient? client) { if (_client != null) { @@ -534,9 +534,10 @@ namespace Avalonia.Browser InputHelper.ShowElement(_inputElement); InputHelper.FocusElement(_inputElement); - var surroundingText = _client.SurroundingText; + var surroundingText = _client.SurroundingText ?? ""; + var selection = _client.Selection; - InputHelper.SetSurroundingText(_inputElement, surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset); + InputHelper.SetSurroundingText(_inputElement, surroundingText, selection.Start, selection.End); } else { @@ -548,16 +549,17 @@ namespace Avalonia.Browser { if (_client != null) { - var surroundingText = _client.SurroundingText; + var surroundingText = _client.SurroundingText ?? ""; + var selection = _client.Selection; - InputHelper.SetSurroundingText(_inputElement, surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset); + InputHelper.SetSurroundingText(_inputElement, surroundingText, selection.Start, selection.End); } } void ITextInputMethodImpl.SetCursorRect(Rect rect) { InputHelper.FocusElement(_inputElement); - InputHelper.SetBounds(_inputElement, (int)rect.X, (int)rect.Y, (int)rect.Width, (int)rect.Height, _client?.SurroundingText.CursorOffset ?? 0); + InputHelper.SetBounds(_inputElement, (int)rect.X, (int)rect.Y, (int)rect.Width, (int)rect.Height, _client?.Selection.End ?? 0); InputHelper.FocusElement(_inputElement); } diff --git a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs index 05074cc82e..f36fb29107 100644 --- a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs +++ b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs @@ -26,7 +26,7 @@ namespace Avalonia.Win32.Input private bool _ignoreComposition; - public ITextInputMethodClient? Client { get; private set; } + public TextInputMethodClient? Client { get; private set; } [MemberNotNullWhen(true, nameof(Client))] public bool IsActive => Client != null; @@ -145,7 +145,7 @@ namespace Avalonia.Win32.Input }); } - public void SetClient(ITextInputMethodClient? client) + public void SetClient(TextInputMethodClient? client) { if(Client != null) { diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs b/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs index a1836f3ce4..0b0e169e8a 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs @@ -29,7 +29,7 @@ public partial class AvaloniaView private bool IsDrivingText => CurrentAvaloniaResponder is TextInputResponder t && ReferenceEquals(t.NextResponder, this); - void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client) + void ITextInputMethodImpl.SetClient(TextInputMethodClient? client) { _client = client; if (_client == null && IsDrivingText) diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index 74cabb294d..939df1a3a0 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -27,7 +27,7 @@ namespace Avalonia.iOS private TopLevelImpl _topLevelImpl; private EmbeddableControlRoot _topLevel; private TouchHandler _touches; - private ITextInputMethodClient _client; + private TextInputMethodClient _client; private IAvaloniaViewController _controller; public AvaloniaView() diff --git a/src/iOS/Avalonia.iOS/TextInputResponder.cs b/src/iOS/Avalonia.iOS/TextInputResponder.cs index e444a90506..b54969e5ff 100644 --- a/src/iOS/Avalonia.iOS/TextInputResponder.cs +++ b/src/iOS/Avalonia.iOS/TextInputResponder.cs @@ -78,7 +78,7 @@ partial class AvaloniaView public NSObject Copy(NSZone? zone) => this; } - public TextInputResponder(AvaloniaView view, ITextInputMethodClient client) + public TextInputResponder(AvaloniaView view, TextInputMethodClient client) { _view = view; NextResponder = view; @@ -88,12 +88,12 @@ partial class AvaloniaView public override UIResponder NextResponder { get; } - private readonly ITextInputMethodClient _client; + private readonly TextInputMethodClient _client; private int _inSurroundingTextUpdateEvent; private readonly UITextPosition _beginningOfDocument = new AvaloniaTextPosition(0); private readonly UITextInputStringTokenizer _tokenizer; - public ITextInputMethodClient? Client => _client; + public TextInputMethodClient? Client => _client; public override bool CanResignFirstResponder => true; @@ -201,17 +201,23 @@ partial class AvaloniaView string IUITextInput.TextInRange(UITextRange range) { var r = (AvaloniaTextRange)range; - var s = _client.SurroundingText; + var surroundingText = _client.SurroundingText; + + var currentSelection = _client.Selection; + Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "IUIKeyInput.TextInRange {start} {end}", r.StartIndex, r.EndIndex); string result = ""; if (string.IsNullOrEmpty(_markedText)) - result = s.Text[r.StartIndex..r.EndIndex]; + if(surroundingText != null && r.EndIndex < surroundingText.Length) + { + result = surroundingText[r.StartIndex..r.EndIndex]; + } else { - var span = new CombinedSpan3(s.Text.AsSpan().Slice(0, s.CursorOffset), + var span = new CombinedSpan3(surroundingText.AsSpan().Slice(0, currentSelection.Start), _markedText, - s.Text.AsSpan().Slice(s.CursorOffset)); + surroundingText.AsSpan().Slice(currentSelection.Start)); var buf = new char[r.EndIndex - r.StartIndex]; span.CopyTo(buf, r.StartIndex); result = new string(buf); @@ -226,7 +232,7 @@ partial class AvaloniaView var r = (AvaloniaTextRange)range; Logger.TryGet(LogEventLevel.Debug, ImeLog)? .Log(null, "IUIKeyInput.ReplaceText {start} {end} {text}", r.StartIndex, r.EndIndex, text); - _client.SelectInSurroundingText(r.StartIndex, r.EndIndex); + _client.Selection = new TextSelection(r.StartIndex, r.EndIndex); TextInput(text); } @@ -447,21 +453,19 @@ partial class AvaloniaView { get { - return new AvaloniaTextRange( - Math.Min(_client.SurroundingText.CursorOffset, _client.SurroundingText.AnchorOffset), - Math.Max(_client.SurroundingText.CursorOffset, _client.SurroundingText.AnchorOffset)); + return new AvaloniaTextRange(_client.Selection.Start, _client.Selection.End); } set { if (_inSurroundingTextUpdateEvent > 0) return; if (value == null) - _client.SelectInSurroundingText(_client.SurroundingText.CursorOffset, - _client.SurroundingText.CursorOffset); + _client.Selection = default; else { var r = (AvaloniaTextRange)value; - _client.SelectInSurroundingText(r.StartIndex, r.EndIndex); + + _client.Selection = new TextSelection(r.StartIndex, r.EndIndex); } } } @@ -474,7 +478,7 @@ partial class AvaloniaView UITextPosition IUITextInput.BeginningOfDocument => _beginningOfDocument; - private int DocumentLength => (_client.SurroundingText.Text?.Length ?? 0) + (_markedText?.Length ?? 0); + private int DocumentLength => (_client.SurroundingText?.Length ?? 0) + (_markedText?.Length ?? 0); UITextPosition IUITextInput.EndOfDocument => new AvaloniaTextPosition(DocumentLength); UITextRange IUITextInput.MarkedTextRange @@ -483,7 +487,7 @@ partial class AvaloniaView { if (string.IsNullOrWhiteSpace(_markedText)) return null!; - return new AvaloniaTextRange(_client.SurroundingText.CursorOffset, _client.SurroundingText.CursorOffset + _markedText.Length); + return new AvaloniaTextRange(_client.Selection.Start, _client.Selection.Start + _markedText.Length); } } From 037bbd3ca012b3289f00be03b37df1cb58f8ff1c Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 23 Jun 2023 10:55:23 +0200 Subject: [PATCH 02/16] Adjust TextInputMethodClient --- samples/MobileSandbox.Android/MainActivity.cs | 1 + .../Input/TextInput/TextInputMethodClient.cs | 70 +----- .../TextBoxTextInputMethodClient.cs | 216 ++++++++---------- 3 files changed, 110 insertions(+), 177 deletions(-) diff --git a/samples/MobileSandbox.Android/MainActivity.cs b/samples/MobileSandbox.Android/MainActivity.cs index bc1be42f12..7a0de74757 100644 --- a/samples/MobileSandbox.Android/MainActivity.cs +++ b/samples/MobileSandbox.Android/MainActivity.cs @@ -1,6 +1,7 @@ using System; using Android.App; using Android.Content.PM; +using Android.OS; using Avalonia; using Avalonia.Android; diff --git a/src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs b/src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs index 87f8d7ba97..4abd20f911 100644 --- a/src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs +++ b/src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs @@ -4,10 +4,6 @@ namespace Avalonia.Input.TextInput { public abstract class TextInputMethodClient { - private Rect _cursorRectangle; - private string _surroundingText = ""; - private TextSelection _selection; - /// /// Fires when the text view visual has changed /// @@ -46,87 +42,39 @@ namespace Avalonia.Input.TextInput /// /// Returns the text around the cursor, usually the current paragraph /// - public string SurroundingText - { - get => _surroundingText; - set - { - var oldValue = _surroundingText; - - if (oldValue == value) - { - return; - } - - _surroundingText = value; - - OnSurroundingTextChanged(oldValue, value); - } - } + public abstract string SurroundingText { get; } /// /// Gets the cursor rectangle relative to the TextViewVisual /// - public Rect CursorRectangle - { - get => _cursorRectangle; - protected set - { - var oldvalue = _cursorRectangle; - - if (oldvalue == value) - { - return; - } - - _cursorRectangle = value; - - OnCursorRectangleChanged(oldvalue, value); - } - } + public abstract Rect CursorRectangle { get; } /// /// Gets or sets the curent selection range within current surrounding text. /// - public TextSelection Selection - { - get => _selection; - set - { - var oldValue = _selection; - - if (oldValue == value) - { - return; - } - - _selection = value; - - OnSelectionChanged(oldValue, value); - } - } + public abstract TextSelection Selection { get; set; } /// /// Sets the non-committed input string /// public virtual void SetPreeditText(string? preeditText) { } - protected virtual void OnCursorRectangleChanged(Rect oldValue, Rect newValue) + protected virtual void OnTextViewVisualChanged(Visual? oldValue, Visual? newValue) { - CursorRectangleChanged?.Invoke(this, EventArgs.Empty); + TextViewVisualChanged?.Invoke(this, EventArgs.Empty); } - protected virtual void OnTextViewVisualChanged(Visual? oldValue, Visual? newValue) + protected virtual void OnCursorRectangleChanged() { - TextViewVisualChanged?.Invoke(this, EventArgs.Empty); + CursorRectangleChanged?.Invoke(this, EventArgs.Empty); } - protected virtual void OnSurroundingTextChanged(string? oldValue, string? newValue) + protected virtual void OnSurroundingTextChanged() { SurroundingTextChanged?.Invoke(this, EventArgs.Empty); } - protected virtual void OnSelectionChanged(TextSelection oldValue, TextSelection newValue) + protected virtual void OnSelectionChanged() { SelectionChanged?.Invoke(this, EventArgs.Empty); } diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index 33a0482c68..de4a3c5cf9 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -10,10 +10,104 @@ namespace Avalonia.Controls { private TextBox? _parent; private TextPresenter? _presenter; - private bool _isPropertyChange; public override Visual TextViewVisual => _presenter!; + public override string SurroundingText + { + get + { + if (_presenter is null || _parent is null) + { + return ""; + } + +#if DEBUG + if (_parent.CaretIndex != _presenter.CaretIndex) + { + throw new InvalidOperationException("TextBox and TextPresenter are out of sync"); + } + + if (_parent.Text != _presenter.Text) + { + throw new InvalidOperationException("TextBox and TextPresenter are out of sync"); + } +#endif + + var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(_presenter.CaretIndex, false); + + var textLine = _presenter.TextLayout.TextLines[lineIndex]; + + var lineText = GetTextLineText(textLine); + + return lineText; + } + } + + public override Rect CursorRectangle + { + get + { + if (_parent == null || _presenter == null) + { + return default; + } + + var transform = _presenter.TransformToVisual(_parent); + + if (transform == null) + { + return default; + } + + return _presenter.GetCursorRectangle().TransformToAABB(transform.Value); + } + } + + public override TextSelection Selection + { + get + { + if (_presenter is null || _parent is null) + { + return default; + } + + var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(_parent.CaretIndex, false); + + var textLine = _presenter.TextLayout.TextLines[lineIndex]; + + var lineStart = textLine.FirstTextSourceIndex; + + var selectionStart = Math.Max(0, _parent.SelectionStart - lineStart); + + var selectionEnd = Math.Max(0, _parent.SelectionEnd - lineStart); + + return new TextSelection(selectionStart, selectionEnd); + } + set + { + if (_parent is null || _presenter is null) + { + return; + } + + var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(_parent.CaretIndex, false); + + var textLine = _presenter.TextLayout.TextLines[lineIndex]; + + var lineStart = textLine.FirstTextSourceIndex; + + var selectionStart = lineStart + value.Start; + var selectionEnd = lineStart + value.End; + + _parent.SelectionStart = selectionStart; + _parent.SelectionEnd = selectionEnd; + + OnSelectionChanged(); + } + } + public override bool SupportsPreedit => true; public override bool SupportsSurroundingText => true; @@ -38,19 +132,19 @@ namespace Avalonia.Controls { oldPresenter.ClearValue(TextPresenter.PreeditTextProperty); - oldPresenter.CaretBoundsChanged -= OnPresenterCursorRectangleChanged; + oldPresenter.CaretBoundsChanged -= (s,e) => OnCursorRectangleChanged(); } _presenter = presenter; if (_presenter != null) { - _presenter.CaretBoundsChanged += OnPresenterCursorRectangleChanged; + _presenter.CaretBoundsChanged += (s, e) => OnCursorRectangleChanged(); } OnTextViewVisualChanged(oldPresenter, presenter); - OnPresenterCursorRectangleChanged(this, EventArgs.Empty); + OnCursorRectangleChanged(); } public override void SetPreeditText(string? preeditText) @@ -63,21 +157,6 @@ namespace Avalonia.Controls _presenter.SetCurrentValue(TextPresenter.PreeditTextProperty, preeditText); } - protected override void OnSelectionChanged(TextSelection oldValue, TextSelection newValue) - { - base.OnSelectionChanged(oldValue, newValue); - - if (_isPropertyChange) - { - return; - } - - if (oldValue != newValue) - { - SetParentSelection(newValue); - } - } - private static string GetTextLineText(TextLine textLine) { var builder = StringBuilderCache.Acquire(textLine.Length); @@ -101,112 +180,17 @@ namespace Avalonia.Controls return lineText; } - private void OnParentTextChanged() - { - if (_presenter is null || _parent is null) - { - SurroundingText = ""; - - return; - } - -#if DEBUG - if (_parent.CaretIndex != _presenter.CaretIndex) - { - throw new InvalidOperationException("TextBox and TextPresenter are out of sync"); - } - - if (_parent.Text != _presenter.Text) - { - throw new InvalidOperationException("TextBox and TextPresenter are out of sync"); - } -#endif - - var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(_presenter.CaretIndex, false); - - var textLine = _presenter.TextLayout.TextLines[lineIndex]; - - var lineText = GetTextLineText(textLine); - - SurroundingText = lineText; - } - - private void OnPresenterCursorRectangleChanged(object? sender, EventArgs e) - { - if (_parent == null || _presenter == null) - { - CursorRectangle = default; - - return; - } - - var transform = _presenter.TransformToVisual(_parent); - - if (transform == null) - { - CursorRectangle = default; - - return; - } - - CursorRectangle = _presenter.GetCursorRectangle().TransformToAABB(transform.Value); - } - private void OnParentPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { - _isPropertyChange = true; - if (e.Property == TextBox.TextProperty) { - OnParentTextChanged(); + OnSurroundingTextChanged(); } if (e.Property == TextBox.SelectionStartProperty || e.Property == TextBox.SelectionEndProperty) { - Selection = GetParentSelection(); - } - - _isPropertyChange = false; - } - - private TextSelection GetParentSelection() - { - if (_presenter is null || _parent is null) - { - return default; - } - - var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(_parent.CaretIndex, false); - - var textLine = _presenter.TextLayout.TextLines[lineIndex]; - - var lineStart = textLine.FirstTextSourceIndex; - - var selectionStart = Math.Max(0, _parent.SelectionStart - lineStart); - - var selectionEnd = Math.Max(0, _parent.SelectionEnd - lineStart); - - return new TextSelection(selectionStart, selectionEnd); - } - - private void SetParentSelection(TextSelection selection) - { - if (_parent is null || _presenter is null) - { - return; + OnSelectionChanged(); } - - var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(_parent.CaretIndex, false); - - var textLine = _presenter.TextLayout.TextLines[lineIndex]; - - var lineStart = textLine.FirstTextSourceIndex; - - var selectionStart = lineStart + selection.Start; - var selectionEnd = lineStart + selection.End; - - _parent.SelectionStart = selectionStart; - _parent.SelectionEnd = selectionEnd; } } } From 86b3b70e5f76bdc84165e0a7fb4cfd67d35b95ef Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 23 Jun 2023 11:09:15 +0200 Subject: [PATCH 03/16] More adjustments --- src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs | 2 +- src/Avalonia.Controls/TextBoxTextInputMethodClient.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs b/src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs index 4abd20f911..d633fb2e46 100644 --- a/src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs +++ b/src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs @@ -59,7 +59,7 @@ namespace Avalonia.Input.TextInput /// public virtual void SetPreeditText(string? preeditText) { } - protected virtual void OnTextViewVisualChanged(Visual? oldValue, Visual? newValue) + protected virtual void OnTextViewVisualChanged() { TextViewVisualChanged?.Invoke(this, EventArgs.Empty); } diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index de4a3c5cf9..9d0fbd1a8c 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -142,7 +142,7 @@ namespace Avalonia.Controls _presenter.CaretBoundsChanged += (s, e) => OnCursorRectangleChanged(); } - OnTextViewVisualChanged(oldPresenter, presenter); + OnTextViewVisualChanged(); OnCursorRectangleChanged(); } From a445f884f6cc59e57d92c6c864c9984388324463 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 23 Jun 2023 11:15:56 +0200 Subject: [PATCH 04/16] Adjust naming --- .../Input/TextInput/TextInputMethodClient.cs | 8 ++++---- .../TextBoxTextInputMethodClient.cs | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs b/src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs index d633fb2e46..7cf3752057 100644 --- a/src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs +++ b/src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs @@ -59,22 +59,22 @@ namespace Avalonia.Input.TextInput /// public virtual void SetPreeditText(string? preeditText) { } - protected virtual void OnTextViewVisualChanged() + protected virtual void RaiseTextViewVisualChanged() { TextViewVisualChanged?.Invoke(this, EventArgs.Empty); } - protected virtual void OnCursorRectangleChanged() + protected virtual void RaiseCursorRectangleChanged() { CursorRectangleChanged?.Invoke(this, EventArgs.Empty); } - protected virtual void OnSurroundingTextChanged() + protected virtual void RaiseSurroundingTextChanged() { SurroundingTextChanged?.Invoke(this, EventArgs.Empty); } - protected virtual void OnSelectionChanged() + protected virtual void RaiseSelectionChanged() { SelectionChanged?.Invoke(this, EventArgs.Empty); } diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index 9d0fbd1a8c..3c86471fbb 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -104,7 +104,7 @@ namespace Avalonia.Controls _parent.SelectionStart = selectionStart; _parent.SelectionEnd = selectionEnd; - OnSelectionChanged(); + RaiseSelectionChanged(); } } @@ -132,19 +132,19 @@ namespace Avalonia.Controls { oldPresenter.ClearValue(TextPresenter.PreeditTextProperty); - oldPresenter.CaretBoundsChanged -= (s,e) => OnCursorRectangleChanged(); + oldPresenter.CaretBoundsChanged -= (s,e) => RaiseCursorRectangleChanged(); } _presenter = presenter; if (_presenter != null) { - _presenter.CaretBoundsChanged += (s, e) => OnCursorRectangleChanged(); + _presenter.CaretBoundsChanged += (s, e) => RaiseCursorRectangleChanged(); } - OnTextViewVisualChanged(); + RaiseTextViewVisualChanged(); - OnCursorRectangleChanged(); + RaiseCursorRectangleChanged(); } public override void SetPreeditText(string? preeditText) @@ -184,12 +184,12 @@ namespace Avalonia.Controls { if (e.Property == TextBox.TextProperty) { - OnSurroundingTextChanged(); + RaiseSurroundingTextChanged(); } if (e.Property == TextBox.SelectionStartProperty || e.Property == TextBox.SelectionEndProperty) { - OnSelectionChanged(); + RaiseSelectionChanged(); } } } From cee239b14252a1805525748799c3352836a0652e Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 23 Jun 2023 12:16:40 +0200 Subject: [PATCH 05/16] Android adjustments --- .../Avalonia.Android/AndroidInputMethod.cs | 2 ++ .../Platform/SkiaPlatform/TopLevelImpl.cs | 36 +++++++++++++++---- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/Android/Avalonia.Android/AndroidInputMethod.cs b/src/Android/Avalonia.Android/AndroidInputMethod.cs index fc00143a9a..af82c7a791 100644 --- a/src/Android/Avalonia.Android/AndroidInputMethod.cs +++ b/src/Android/Avalonia.Android/AndroidInputMethod.cs @@ -107,6 +107,8 @@ namespace Avalonia.Android var selection = Client.Selection; _imm.UpdateSelection(_host, selection.Start, selection.End, selection.Start, selection.End); + + _inputConnection.SetSelection(selection.Start, selection.End); } private void _client_SurroundingTextChanged(object sender, EventArgs e) diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index f725240c22..e597c26753 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -29,6 +29,7 @@ using Avalonia.Platform.Storage; using Avalonia.Rendering; using Avalonia.Rendering.Composition; using Java.Lang; +using static System.Net.Mime.MediaTypeNames; using ClipboardManager = Android.Content.ClipboardManager; namespace Avalonia.Android.Platform.SkiaPlatform @@ -459,9 +460,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform { var text = tb.SubSequence(0, tb.Length()); - //System.Diagnostics.Debug.WriteLine($"Replace: start: {start}, end: {end}, text: {text}"); - - _inputConnection.InputMethod.Client.Selection = new TextSelection(start, end); + SelectSurroundingTextForDeletion(start, end, text); } return base.Replace(start, end, tb); @@ -473,13 +472,18 @@ namespace Avalonia.Android.Platform.SkiaPlatform { var text = tb.SubSequence(tbstart, tbend); - //System.Diagnostics.Debug.WriteLine($"Replace: start: {start}, end: {end}, text: {text}"); - - _inputConnection.InputMethod.Client.Selection = new TextSelection(start, end); + SelectSurroundingTextForDeletion(start, end, text); } return base.Replace(start, end, tb, tbstart, tbend); } + + private void SelectSurroundingTextForDeletion(int start, int end, string text) + { + _inputConnection.InputMethod.Client.Selection = new TextSelection(start, end); + + //_inputConnection.Toplevel.TextInput(text); + } } internal class AvaloniaInputConnection : BaseInputConnection @@ -507,6 +511,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform public IAndroidInputMethod InputMethod => _inputMethod; + public TopLevelImpl Toplevel => _toplevel; + public override bool SetComposingText(ICharSequence text, int newCursorPosition) { _compositionText = text.SubSequence(0, text.Length()); @@ -551,6 +557,24 @@ namespace Avalonia.Android.Platform.SkiaPlatform return ret; } + public override bool DeleteSurroundingText(int beforeLength, int afterLength) + { + if (InputMethod.IsActive) + { + EditableWrapper.IgnoreChange = true; + + var selection = InputMethod.Client.Selection; + + InputMethod.Client.Selection = new TextSelection(selection.Start - beforeLength, selection.Start + afterLength); + + Toplevel.TextInput(""); + + EditableWrapper.IgnoreChange = true; + } + + return base.DeleteSurroundingText(beforeLength, afterLength); + } + public override bool PerformEditorAction([GeneratedEnum] ImeAction actionCode) { switch (actionCode) From a2eb8942882e857ddd81b753e175b3ba3b4fbcce Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Fri, 23 Jun 2023 11:43:53 +0000 Subject: [PATCH 06/16] android - dispatch key event for delete --- .../Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index e597c26753..eeb709b22b 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -567,7 +567,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform InputMethod.Client.Selection = new TextSelection(selection.Start - beforeLength, selection.Start + afterLength); - Toplevel.TextInput(""); + InputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel)); EditableWrapper.IgnoreChange = true; } From 73ec5a4fc55897fd1bac04e5d5a3362064586664 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Fri, 23 Jun 2023 15:48:00 +0000 Subject: [PATCH 07/16] android - set composing text as normal text input --- .../Avalonia.Android/AndroidInputMethod.cs | 2 - .../Platform/SkiaPlatform/TopLevelImpl.cs | 66 ++++++++++++------- 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/src/Android/Avalonia.Android/AndroidInputMethod.cs b/src/Android/Avalonia.Android/AndroidInputMethod.cs index af82c7a791..ea1958efa6 100644 --- a/src/Android/Avalonia.Android/AndroidInputMethod.cs +++ b/src/Android/Avalonia.Android/AndroidInputMethod.cs @@ -124,8 +124,6 @@ namespace Avalonia.Android var selection = Client.Selection; _imm.UpdateSelection(_host, selection.Start, selection.End, selection.Start, selection.End); - - //Debug.WriteLine($"SurroundingText: {surroundingText}, CaretIndex: {selection.Start}"); } public void SetCursorRect(Rect rect) diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index eeb709b22b..558470c026 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -456,11 +456,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform public override IEditable Replace(int start, int end, ICharSequence tb) { - if (!IgnoreChange && !_inputConnection.IsComposing && start != end) + if (!IgnoreChange && start != end) { var text = tb.SubSequence(0, tb.Length()); - SelectSurroundingTextForDeletion(start, end, text); + SelectSurroundingTextForDeletion(start, end); } return base.Replace(start, end, tb); @@ -468,21 +468,19 @@ namespace Avalonia.Android.Platform.SkiaPlatform public override IEditable Replace(int start, int end, ICharSequence tb, int tbstart, int tbend) { - if (!IgnoreChange && !_inputConnection.IsComposing && start != end) + if (!IgnoreChange && start != end) { var text = tb.SubSequence(tbstart, tbend); - SelectSurroundingTextForDeletion(start, end, text); + SelectSurroundingTextForDeletion(start, end); } return base.Replace(start, end, tb, tbstart, tbend); } - private void SelectSurroundingTextForDeletion(int start, int end, string text) + private void SelectSurroundingTextForDeletion(int start, int end) { _inputConnection.InputMethod.Client.Selection = new TextSelection(start, end); - - //_inputConnection.Toplevel.TextInput(text); } } @@ -491,8 +489,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform private readonly TopLevelImpl _toplevel; private readonly IAndroidInputMethod _inputMethod; private readonly EditableWrapper _editable; - private string _compositionText; private bool _commitInProgress; + private (int Start, int End)? _composingRegion; public AvaloniaInputConnection(TopLevelImpl toplevel, IAndroidInputMethod inputMethod) : base(inputMethod.View, true) { @@ -501,8 +499,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform _editable = new EditableWrapper(this); } - public bool IsComposing => !string.IsNullOrEmpty(_compositionText); - public int ExtractedTextToken { get; private set; } public override IEditable Editable => _editable; @@ -513,18 +509,34 @@ namespace Avalonia.Android.Platform.SkiaPlatform public TopLevelImpl Toplevel => _toplevel; + public override bool SetComposingRegion(int start, int end) + { + _composingRegion = new(start, end); + + return base.SetComposingRegion(start, end); + } + public override bool SetComposingText(ICharSequence text, int newCursorPosition) { - _compositionText = text.SubSequence(0, text.Length()); + if(_composingRegion != null) + { + // Select the composing region. + InputMethod.Client.Selection = new TextSelection(_composingRegion.Value.Start, _composingRegion.Value.End); + } + var compositionText = text.SubSequence(0, text.Length()); - System.Diagnostics.Debug.WriteLine($"Composition Changed: {_compositionText}"); + System.Diagnostics.Debug.WriteLine($"Composition Changed: {compositionText}"); - if(_inputMethod.IsActive && !_commitInProgress) + if (_inputMethod.IsActive && !_commitInProgress) { - _inputMethod.Client.SetPreeditText(_compositionText); + if (string.IsNullOrEmpty(compositionText)) + _inputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel)); + + else + _toplevel.TextInput(compositionText); } - return base.SetComposingText(text, newCursorPosition); + return true; } public override bool CommitText(ICharSequence text, int newCursorPosition) @@ -535,21 +547,16 @@ namespace Avalonia.Android.Platform.SkiaPlatform var committedText = text.SubSequence(0, text.Length()); - if (string.IsNullOrEmpty(committedText)) - { - committedText = _compositionText; - } - if (_inputMethod.IsActive && !string.IsNullOrEmpty(committedText)) { - if (!string.IsNullOrEmpty(_compositionText)) + if(_composingRegion != null) { - _inputMethod.Client.SetPreeditText(null); + _inputMethod.Client.Selection = new TextSelection(_composingRegion.Value.Start, _composingRegion.Value.End); } _toplevel.TextInput(committedText); - _compositionText = null; + _composingRegion = null; } _commitInProgress = false; @@ -557,11 +564,22 @@ namespace Avalonia.Android.Platform.SkiaPlatform return ret; } + public override bool FinishComposingText() + { + _composingRegion = null; + return base.FinishComposingText(); + } + public override bool DeleteSurroundingText(int beforeLength, int afterLength) { if (InputMethod.IsActive) { EditableWrapper.IgnoreChange = true; + } + var result = base.DeleteSurroundingText(beforeLength, afterLength); + + if (InputMethod.IsActive) + { var selection = InputMethod.Client.Selection; @@ -572,7 +590,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform EditableWrapper.IgnoreChange = true; } - return base.DeleteSurroundingText(beforeLength, afterLength); + return result; } public override bool PerformEditorAction([GeneratedEnum] ImeAction actionCode) From 692fd5de60d963f98173b4b97501b745e4589b24 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Mon, 26 Jun 2023 08:35:41 +0000 Subject: [PATCH 08/16] use current ime selection for deletion --- .../Platform/SkiaPlatform/TopLevelImpl.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 558470c026..ecf0fd1225 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -491,6 +491,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform private readonly EditableWrapper _editable; private bool _commitInProgress; private (int Start, int End)? _composingRegion; + private TextSelection _selection; public AvaloniaInputConnection(TopLevelImpl toplevel, IAndroidInputMethod inputMethod) : base(inputMethod.View, true) { @@ -580,10 +581,9 @@ namespace Avalonia.Android.Platform.SkiaPlatform if (InputMethod.IsActive) { + var selection = _selection; - var selection = InputMethod.Client.Selection; - - InputMethod.Client.Selection = new TextSelection(selection.Start - beforeLength, selection.Start + afterLength); + InputMethod.Client.Selection = new TextSelection(selection.Start - beforeLength, selection.End + afterLength); InputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel)); @@ -593,6 +593,12 @@ namespace Avalonia.Android.Platform.SkiaPlatform return result; } + public override bool SetSelection(int start, int end) + { + _selection = new TextSelection(start, end); + return base.SetSelection(start, end); + } + public override bool PerformEditorAction([GeneratedEnum] ImeAction actionCode) { switch (actionCode) From c7d77b15452ac6566b26613a945578da7dd76b8b Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 26 Jun 2023 11:03:59 +0200 Subject: [PATCH 09/16] Use SetCurrentValue --- src/Avalonia.Controls/Presenters/TextPresenter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index e3949a3255..43c4e76aba 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -853,7 +853,7 @@ namespace Avalonia.Controls.Presenters { if (!string.IsNullOrEmpty(PreeditText)) { - PreeditText = null; + SetCurrentValue(PreeditTextProperty, null); } } @@ -861,7 +861,7 @@ namespace Avalonia.Controls.Presenters { if (!string.IsNullOrEmpty(PreeditText)) { - PreeditText = null; + SetCurrentValue(PreeditTextProperty, null); } } From 1acbc8c36e8dddd07ec42af87b4323c9f5292f8a Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Mon, 26 Jun 2023 11:31:55 +0000 Subject: [PATCH 10/16] use ime selection in get extracted text --- .../Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index ecf0fd1225..d6c2ab7314 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -632,7 +632,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform return null; } - var selection = _inputMethod.Client.Selection; + var selection = _selection; ExtractedText extract = new ExtractedText { From 3f56045885f6c17b03ba8e48071b8e01855617be Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Thu, 29 Jun 2023 20:40:03 +0000 Subject: [PATCH 11/16] remove debug code --- samples/MobileSandbox.Android/MainActivity.cs | 11 ----------- .../MobileSandbox.Android.csproj | 4 ---- src/Android/Avalonia.Android/Avalonia.Android.csproj | 3 --- .../Platform/SkiaPlatform/TopLevelImpl.cs | 2 -- 4 files changed, 20 deletions(-) diff --git a/samples/MobileSandbox.Android/MainActivity.cs b/samples/MobileSandbox.Android/MainActivity.cs index 7a0de74757..702e9be9e6 100644 --- a/samples/MobileSandbox.Android/MainActivity.cs +++ b/samples/MobileSandbox.Android/MainActivity.cs @@ -10,16 +10,5 @@ namespace MobileSandbox.Android [Activity(Label = "MobileSandbox.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", MainLauncher = true, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)] public class MainActivity : AvaloniaMainActivity { - protected override void OnCreate(Bundle savedInstanceState) - { - AppDomain.CurrentDomain.UnhandledException += CurrentDomainUnhandledException; - - base.OnCreate(savedInstanceState); - } - - private void CurrentDomainUnhandledException(object sender, UnhandledExceptionEventArgs e) - { - System.Diagnostics.Debug.WriteLine(e.ToString()); - } } } diff --git a/samples/MobileSandbox.Android/MobileSandbox.Android.csproj b/samples/MobileSandbox.Android/MobileSandbox.Android.csproj index 54cfea83fa..3b67d79f7b 100644 --- a/samples/MobileSandbox.Android/MobileSandbox.Android.csproj +++ b/samples/MobileSandbox.Android/MobileSandbox.Android.csproj @@ -38,10 +38,6 @@ True - - True - - diff --git a/src/Android/Avalonia.Android/Avalonia.Android.csproj b/src/Android/Avalonia.Android/Avalonia.Android.csproj index e04a1acde1..d8b0c3d534 100644 --- a/src/Android/Avalonia.Android/Avalonia.Android.csproj +++ b/src/Android/Avalonia.Android/Avalonia.Android.csproj @@ -7,9 +7,6 @@ portable Avalonia.Android.Internal - - True - diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index d6c2ab7314..5d82d099c9 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -526,8 +526,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform } var compositionText = text.SubSequence(0, text.Length()); - System.Diagnostics.Debug.WriteLine($"Composition Changed: {compositionText}"); - if (_inputMethod.IsActive && !_commitInProgress) { if (string.IsNullOrEmpty(compositionText)) From fe7a118aa80ed60dc602cfdbdaef1cbbb3a491f8 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 30 Jun 2023 12:15:18 +0200 Subject: [PATCH 12/16] Fix Imm32InputMethod --- src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs index 6b283282bf..495e808a04 100644 --- a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs +++ b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs @@ -325,7 +325,7 @@ namespace Avalonia.Win32.Input { Client.SetPreeditText(null); - if (Client.SupportsSurroundingText && Client.SurroundingText.AnchorOffset != Client.SurroundingText.CursorOffset) + if (Client.SupportsSurroundingText && Client.Selection.Start != Client.Selection.End) { KeyPress(Key.Delete); } From a457dff79c1d1a197ae3076b15ae9e4bd966cc67 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 30 Jun 2023 11:44:16 +0100 Subject: [PATCH 13/16] whitespace --- src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs index 495e808a04..1beccb5239 100644 --- a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs +++ b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs @@ -325,7 +325,7 @@ namespace Avalonia.Win32.Input { Client.SetPreeditText(null); - if (Client.SupportsSurroundingText && Client.Selection.Start != Client.Selection.End) + if (Client.SupportsSurroundingText && Client.Selection.Start != Client.Selection.End) { KeyPress(Key.Delete); } From 1b10cc2ef89aaca8582e49bb4314800160396a93 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 30 Jun 2023 11:44:39 +0100 Subject: [PATCH 14/16] revert whitespace --- src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs index 1beccb5239..495e808a04 100644 --- a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs +++ b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs @@ -325,7 +325,7 @@ namespace Avalonia.Win32.Input { Client.SetPreeditText(null); - if (Client.SupportsSurroundingText && Client.Selection.Start != Client.Selection.End) + if (Client.SupportsSurroundingText && Client.Selection.Start != Client.Selection.End) { KeyPress(Key.Delete); } From 3b23aa2f079b0d2db7a5e5e4fe248c9abc1bf9b6 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 30 Jun 2023 21:53:59 +0200 Subject: [PATCH 15/16] Enforce that TextBox and TextPresenter are in sync when SurroundingText is requested --- src/Avalonia.Controls/TextBoxTextInputMethodClient.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index 3c86471fbb..66581df3be 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -25,15 +25,14 @@ namespace Avalonia.Controls #if DEBUG if (_parent.CaretIndex != _presenter.CaretIndex) { - throw new InvalidOperationException("TextBox and TextPresenter are out of sync"); + _presenter.SetCurrentValue(TextPresenter.CaretIndexProperty, _parent.CaretIndex); } if (_parent.Text != _presenter.Text) { - throw new InvalidOperationException("TextBox and TextPresenter are out of sync"); + _presenter.SetCurrentValue(TextPresenter.TextProperty, _parent.Text); } #endif - var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(_presenter.CaretIndex, false); var textLine = _presenter.TextLayout.TextLines[lineIndex]; From 792143d09d55a666eed15ce77625f70c266dfba6 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 30 Jun 2023 21:56:54 +0200 Subject: [PATCH 16/16] Remove compiler conditionals --- src/Avalonia.Controls/TextBoxTextInputMethodClient.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index 66581df3be..28a230640a 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -21,8 +21,7 @@ namespace Avalonia.Controls { return ""; } - -#if DEBUG + if (_parent.CaretIndex != _presenter.CaretIndex) { _presenter.SetCurrentValue(TextPresenter.CaretIndexProperty, _parent.CaretIndex); @@ -32,7 +31,7 @@ namespace Avalonia.Controls { _presenter.SetCurrentValue(TextPresenter.TextProperty, _parent.Text); } -#endif + var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(_presenter.CaretIndex, false); var textLine = _presenter.TextLayout.TextLines[lineIndex];