From be9a26cbb1d62e5e4621c96db698c20a872d6a6f Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 23 Jun 2023 10:22:07 +0200 Subject: [PATCH] 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); } }