Browse Source

Merge pull request #11848 from Gillibald/textInputMethodClientRework

Rework ITextInputMethodClient
release/11.0.0-rc2.1
Dan Walmsley 3 years ago
committed by GitHub
parent
commit
fddb07e430
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      samples/MobileSandbox.Android/MainActivity.cs
  2. 70
      src/Android/Avalonia.Android/AndroidInputMethod.cs
  3. 127
      src/Android/Avalonia.Android/InputEditable.cs
  4. 204
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  5. 23
      src/Avalonia.Base/Input/TextInput/ITextEditable.cs
  6. 66
      src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs
  7. 2
      src/Avalonia.Base/Input/TextInput/ITextInputMethodImpl.cs
  8. 4
      src/Avalonia.Base/Input/TextInput/InputMethodManager.cs
  9. 84
      src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs
  10. 2
      src/Avalonia.Base/Input/TextInput/TextInputMethodClientRequestedEventArgs.cs
  11. 94
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  12. 4
      src/Avalonia.Controls/TextBox.cs
  13. 292
      src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
  14. 6
      src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs
  15. 18
      src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs
  16. 6
      src/Avalonia.X11/X11Window.Xim.cs
  17. 18
      src/Browser/Avalonia.Browser/AvaloniaView.cs
  18. 6
      src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs
  19. 2
      src/iOS/Avalonia.iOS/AvaloniaView.Text.cs
  20. 2
      src/iOS/Avalonia.iOS/AvaloniaView.cs
  21. 36
      src/iOS/Avalonia.iOS/TextInputResponder.cs

5
samples/MobileSandbox.Android/MainActivity.cs

@ -1,5 +1,8 @@
using Android.App; using System;
using Android.App;
using Android.Content.PM; using Android.Content.PM;
using Android.OS;
using Avalonia;
using Avalonia.Android; using Avalonia.Android;
namespace MobileSandbox.Android namespace MobileSandbox.Android

70
src/Android/Avalonia.Android/AndroidInputMethod.cs

@ -1,10 +1,10 @@
using System; using System;
using Android.Content; using Android.Content;
using Android.Runtime; using Android.Runtime;
using Android.Text;
using Android.Views; using Android.Views;
using Android.Views.InputMethods; using Android.Views.InputMethods;
using Avalonia.Android.Platform.SkiaPlatform; using Avalonia.Android.Platform.SkiaPlatform;
using Avalonia.Controls.Presenters;
using Avalonia.Input.TextInput; using Avalonia.Input.TextInput;
namespace Avalonia.Android namespace Avalonia.Android
@ -13,7 +13,7 @@ namespace Avalonia.Android
{ {
public View View { get; } public View View { get; }
public ITextInputMethodClient Client { get; } public TextInputMethodClient Client { get; }
public bool IsActive { get; } public bool IsActive { get; }
@ -36,7 +36,7 @@ namespace Avalonia.Android
{ {
private readonly TView _host; private readonly TView _host;
private readonly InputMethodManager _imm; private readonly InputMethodManager _imm;
private ITextInputMethodClient _client; private TextInputMethodClient _client;
private AvaloniaInputConnection _inputConnection; private AvaloniaInputConnection _inputConnection;
public AndroidInputMethod(TView host) public AndroidInputMethod(TView host)
@ -56,7 +56,7 @@ namespace Avalonia.Android
public bool IsActive => Client != null; public bool IsActive => Client != null;
public ITextInputMethodClient Client => _client; public TextInputMethodClient Client => _client;
public InputMethodManager IMM => _imm; public InputMethodManager IMM => _imm;
@ -65,7 +65,7 @@ namespace Avalonia.Android
} }
public void SetClient(ITextInputMethodClient client) public void SetClient(TextInputMethodClient client)
{ {
_client = client; _client = client;
@ -77,9 +77,24 @@ namespace Avalonia.Android
_imm.ShowSoftInput(_host, ShowFlags.Implicit); _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 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);
_inputConnection.SetSelection(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);
}
public void SetCursorRect(Rect rect) public void SetCursorRect(Rect rect)
{ {
@ -134,25 +173,8 @@ namespace Avalonia.Android
outAttrs.ImeOptions |= ImeFlags.NoFullscreen | ImeFlags.NoExtractUi; outAttrs.ImeOptions |= ImeFlags.NoFullscreen | ImeFlags.NoExtractUi;
_client.TextEditable = _inputConnection.InputEditable;
return _inputConnection; 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;
}
} }

127
src/Android/Avalonia.Android/InputEditable.cs

@ -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);
}
}
}

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

@ -11,6 +11,7 @@ using Android.Text;
using Android.Views; using Android.Views;
using Android.Views.InputMethods; using Android.Views.InputMethods;
using AndroidX.AppCompat.App; using AndroidX.AppCompat.App;
using Avalonia.Android.Platform.Input;
using Avalonia.Android.Platform.Specific; using Avalonia.Android.Platform.Specific;
using Avalonia.Android.Platform.Specific.Helpers; using Avalonia.Android.Platform.Specific.Helpers;
using Avalonia.Android.Platform.Storage; using Avalonia.Android.Platform.Storage;
@ -28,6 +29,7 @@ using Avalonia.Platform.Storage;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition;
using Java.Lang; using Java.Lang;
using static System.Net.Mime.MediaTypeNames;
using ClipboardManager = Android.Content.ClipboardManager; using ClipboardManager = Android.Content.ClipboardManager;
namespace Avalonia.Android.Platform.SkiaPlatform namespace Avalonia.Android.Platform.SkiaPlatform
@ -429,79 +431,170 @@ namespace Avalonia.Android.Platform.SkiaPlatform
activity.Window.Attributes = attr; 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 EditableWrapper : SpannableStringBuilder
{
private readonly AvaloniaInputConnection _inputConnection;
public EditableWrapper(AvaloniaInputConnection inputConnection)
{
_inputConnection = inputConnection;
}
public bool IgnoreChange { get; set; }
public override IEditable Replace(int start, int end, ICharSequence tb)
{
if (!IgnoreChange && start != end)
{
var text = tb.SubSequence(0, tb.Length());
SelectSurroundingTextForDeletion(start, end);
}
return base.Replace(start, end, tb);
}
public override IEditable Replace(int start, int end, ICharSequence tb, int tbstart, int tbend)
{
if (!IgnoreChange && start != end)
{
var text = tb.SubSequence(tbstart, tbend);
SelectSurroundingTextForDeletion(start, end);
}
return base.Replace(start, end, tb, tbstart, tbend);
}
private void SelectSurroundingTextForDeletion(int start, int end)
{
_inputConnection.InputMethod.Client.Selection = new TextSelection(start, end);
}
} }
internal class AvaloniaInputConnection : BaseInputConnection internal class AvaloniaInputConnection : BaseInputConnection
{ {
private readonly TopLevelImpl _topLevel; private readonly TopLevelImpl _toplevel;
private readonly IAndroidInputMethod _inputMethod; private readonly IAndroidInputMethod _inputMethod;
private readonly InputEditable _editable; 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) public AvaloniaInputConnection(TopLevelImpl toplevel, IAndroidInputMethod inputMethod) : base(inputMethod.View, true)
{ {
_topLevel = topLevel; _toplevel = toplevel;
_inputMethod = inputMethod; _inputMethod = inputMethod;
_editable = new InputEditable(_topLevel, _inputMethod, this); _editable = new EditableWrapper(this);
} }
public int ExtractedTextToken { get; private set; }
public override IEditable Editable => _editable; public override IEditable Editable => _editable;
internal InputEditable InputEditable => _editable; public EditableWrapper EditableWrapper => _editable;
public IAndroidInputMethod InputMethod => _inputMethod;
public TopLevelImpl Toplevel => _toplevel;
public override bool SetComposingRegion(int start, int end) public override bool SetComposingRegion(int start, int end)
{ {
var ret = base.SetComposingRegion(start, end); _composingRegion = new(start, end);
InputEditable.RaiseCompositionChanged();
return ret; return base.SetComposingRegion(start, end);
} }
public override bool SetComposingText(ICharSequence text, int newCursorPosition) public override bool SetComposingText(ICharSequence text, int newCursorPosition)
{ {
var composingText = text.ToString(); if(_composingRegion != null)
if (string.IsNullOrEmpty(composingText))
{ {
return CommitText(text, newCursorPosition); // Select the composing region.
InputMethod.Client.Selection = new TextSelection(_composingRegion.Value.Start, _composingRegion.Value.End);
} }
else var compositionText = text.SubSequence(0, text.Length());
{
var ret = base.SetComposingText(text, newCursorPosition);
InputEditable.RaiseCompositionChanged(); if (_inputMethod.IsActive && !_commitInProgress)
{
if (string.IsNullOrEmpty(compositionText))
_inputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel));
return ret; else
_toplevel.TextInput(compositionText);
} }
return true;
} }
public override bool BeginBatchEdit() public override bool CommitText(ICharSequence text, int newCursorPosition)
{ {
_editable.BeginBatchEdit(); _commitInProgress = true;
return base.BeginBatchEdit(); var ret = base.CommitText(text, newCursorPosition);
}
public override bool EndBatchEdit() var committedText = text.SubSequence(0, text.Length());
{
var ret = base.EndBatchEdit(); if (_inputMethod.IsActive && !string.IsNullOrEmpty(committedText))
_editable.EndBatchEdit(); {
if(_composingRegion != null)
{
_inputMethod.Client.Selection = new TextSelection(_composingRegion.Value.Start, _composingRegion.Value.End);
}
_toplevel.TextInput(committedText);
_composingRegion = null;
}
_commitInProgress = false;
return ret; return ret;
} }
public override bool FinishComposingText() public override bool FinishComposingText()
{ {
var ret = base.FinishComposingText(); _composingRegion = null;
InputEditable.RaiseCompositionChanged(); return base.FinishComposingText();
return ret;
} }
public override bool CommitText(ICharSequence text, int newCursorPosition) public override bool DeleteSurroundingText(int beforeLength, int afterLength)
{ {
var ret = base.CommitText(text, newCursorPosition); if (InputMethod.IsActive)
InputEditable.RaiseCompositionChanged(); {
return ret; EditableWrapper.IgnoreChange = true;
}
var result = base.DeleteSurroundingText(beforeLength, afterLength);
if (InputMethod.IsActive)
{
var selection = _selection;
InputMethod.Client.Selection = new TextSelection(selection.Start - beforeLength, selection.End + afterLength);
InputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel));
EditableWrapper.IgnoreChange = true;
}
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) public override bool PerformEditorAction([GeneratedEnum] ImeAction actionCode)
@ -517,5 +610,48 @@ namespace Avalonia.Android.Platform.SkiaPlatform
return base.PerformEditorAction(actionCode); 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 = _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;
}
} }
} }

23
src/Avalonia.Base/Input/TextInput/ITextEditable.cs

@ -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; }
}
}

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

@ -1,66 +0,0 @@
using System;
using Avalonia.Media.TextFormatting;
using Avalonia.VisualTree;
namespace Avalonia.Input.TextInput
{
public interface ITextInputMethodClient
{
/// <summary>
/// The cursor rectangle relative to the TextViewVisual
/// </summary>
Rect CursorRectangle { get; }
/// <summary>
/// Should be fired when cursor rectangle is changed inside the TextViewVisual
/// </summary>
event EventHandler? CursorRectangleChanged;
/// <summary>
/// The visual that's showing the text
/// </summary>
Visual TextViewVisual { get; }
/// <summary>
/// Should be fired when text-hosting visual is changed
/// </summary>
event EventHandler? TextViewVisualChanged;
/// <summary>
/// Indicates if TextViewVisual is capable of displaying non-committed input on the cursor position
/// </summary>
bool SupportsPreedit { get; }
/// <summary>
/// Sets the non-committed input string
/// </summary>
void SetPreeditText(string? text);
/// <summary>
/// Sets the current composing region. This doesn't remove the composing text from the commited text.
/// </summary>
void SetComposingRegion(TextRange? region);
/// <summary>
/// Indicates if text input client is capable of providing the text around the cursor
/// </summary>
bool SupportsSurroundingText { get; }
/// <summary>
/// Returns the text around the cursor, usually the current paragraph, the cursor position inside that text and selection start position
/// </summary>
TextInputMethodSurroundingText SurroundingText { get; }
/// <summary>
/// Should be fired when surrounding text changed
/// </summary>
event EventHandler? SurroundingTextChanged;
/// <summary>
/// Gets or sets a platform editable. Text and selection changes made in the editable are forwarded to the IM client.
/// </summary>
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; }
}
}

2
src/Avalonia.Base/Input/TextInput/ITextInputMethodImpl.cs

@ -5,7 +5,7 @@ namespace Avalonia.Input.TextInput
[Unstable] [Unstable]
public interface ITextInputMethodImpl public interface ITextInputMethodImpl
{ {
void SetClient(ITextInputMethodClient? client); void SetClient(TextInputMethodClient? client);
void SetCursorRect(Rect rect); void SetCursorRect(Rect rect);
void SetOptions(TextInputOptions options); void SetOptions(TextInputOptions options);
void Reset(); void Reset();

4
src/Avalonia.Base/Input/TextInput/InputMethodManager.cs

@ -7,7 +7,7 @@ namespace Avalonia.Input.TextInput
{ {
private ITextInputMethodImpl? _im; private ITextInputMethodImpl? _im;
private IInputElement? _focusedElement; private IInputElement? _focusedElement;
private ITextInputMethodClient? _client; private TextInputMethodClient? _client;
private readonly TransformTrackingHelper _transformTracker = new TransformTrackingHelper(); private readonly TransformTrackingHelper _transformTracker = new TransformTrackingHelper();
public TextInputMethodManager() public TextInputMethodManager()
@ -16,7 +16,7 @@ namespace Avalonia.Input.TextInput
InputMethod.IsInputMethodEnabledProperty.Changed.Subscribe(OnIsInputMethodEnabledChanged); InputMethod.IsInputMethodEnabledProperty.Changed.Subscribe(OnIsInputMethodEnabledChanged);
} }
private ITextInputMethodClient? Client private TextInputMethodClient? Client
{ {
get => _client; get => _client;
set set

84
src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs

@ -0,0 +1,84 @@
using System;
namespace Avalonia.Input.TextInput
{
public abstract class TextInputMethodClient
{
/// <summary>
/// Fires when the text view visual has changed
/// </summary>
public event EventHandler? TextViewVisualChanged;
/// <summary>
/// Fires when the cursor rectangle has changed
/// </summary>
public event EventHandler? CursorRectangleChanged;
/// <summary>
/// Fires when the surrounding text has changed
/// </summary>
public event EventHandler? SurroundingTextChanged;
/// <summary>
/// Fires when the selection has changed
/// </summary>
public event EventHandler? SelectionChanged;
/// <summary>
/// The visual that's showing the text
/// </summary>
public abstract Visual TextViewVisual { get; }
/// <summary>
/// Indicates if TextViewVisual is capable of displaying non-committed input on the cursor position
/// </summary>
public abstract bool SupportsPreedit { get; }
/// <summary>
/// Indicates if text input client is capable of providing the text around the cursor
/// </summary>
public abstract bool SupportsSurroundingText { get; }
/// <summary>
/// Returns the text around the cursor, usually the current paragraph
/// </summary>
public abstract string SurroundingText { get; }
/// <summary>
/// Gets the cursor rectangle relative to the TextViewVisual
/// </summary>
public abstract Rect CursorRectangle { get; }
/// <summary>
/// Gets or sets the curent selection range within current surrounding text.
/// </summary>
public abstract TextSelection Selection { get; set; }
/// <summary>
/// Sets the non-committed input string
/// </summary>
public virtual void SetPreeditText(string? preeditText) { }
protected virtual void RaiseTextViewVisualChanged()
{
TextViewVisualChanged?.Invoke(this, EventArgs.Empty);
}
protected virtual void RaiseCursorRectangleChanged()
{
CursorRectangleChanged?.Invoke(this, EventArgs.Empty);
}
protected virtual void RaiseSurroundingTextChanged()
{
SurroundingTextChanged?.Invoke(this, EventArgs.Empty);
}
protected virtual void RaiseSelectionChanged()
{
SelectionChanged?.Invoke(this, EventArgs.Empty);
}
}
public record struct TextSelection(int Start, int End);
}

2
src/Avalonia.Base/Input/TextInput/TextInputMethodClientRequestedEventArgs.cs

@ -7,6 +7,6 @@ namespace Avalonia.Input.TextInput
/// <summary> /// <summary>
/// Set this property to a valid text input client to enable input method interaction /// Set this property to a valid text input client to enable input method interaction
/// </summary> /// </summary>
public ITextInputMethodClient? Client { get; set; } public TextInputMethodClient? Client { get; set; }
} }
} }

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

@ -50,15 +50,6 @@ namespace Avalonia.Controls.Presenters
public static readonly StyledProperty<string?> PreeditTextProperty = public static readonly StyledProperty<string?> PreeditTextProperty =
AvaloniaProperty.Register<TextPresenter, string?>(nameof(PreeditText)); AvaloniaProperty.Register<TextPresenter, string?>(nameof(PreeditText));
/// <summary>
/// Defines the <see cref="CompositionRegion"/> property.
/// </summary>
public static readonly DirectProperty<TextPresenter, TextRange?> CompositionRegionProperty =
AvaloniaProperty.RegisterDirect<TextPresenter, TextRange?>(
nameof(CompositionRegion),
o => o.CompositionRegion,
(o, v) => o.CompositionRegion = v);
/// <summary> /// <summary>
/// Defines the <see cref="TextAlignment"/> property. /// Defines the <see cref="TextAlignment"/> property.
/// </summary> /// </summary>
@ -97,7 +88,6 @@ namespace Avalonia.Controls.Presenters
private CharacterHit _lastCharacterHit; private CharacterHit _lastCharacterHit;
private Rect _caretBounds; private Rect _caretBounds;
private Point _navigationPosition; private Point _navigationPosition;
private TextRange? _compositionRegion;
static TextPresenter() static TextPresenter()
{ {
@ -137,12 +127,6 @@ namespace Avalonia.Controls.Presenters
set => SetValue(PreeditTextProperty, value); set => SetValue(PreeditTextProperty, value);
} }
public TextRange? CompositionRegion
{
get => _compositionRegion;
set => SetAndRaise(CompositionRegionProperty, ref _compositionRegion, value);
}
/// <summary> /// <summary>
/// Gets or sets the font family. /// Gets or sets the font family.
/// </summary> /// </summary>
@ -490,10 +474,10 @@ namespace Avalonia.Controls.Presenters
{ {
TextLayout result; 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 typeface = new Typeface(FontFamily, FontStyle, FontWeight);
var selectionStart = SelectionStart; var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd; var selectionEnd = SelectionEnd;
var start = Math.Min(selectionStart, selectionEnd); var start = Math.Min(selectionStart, selectionEnd);
@ -503,22 +487,9 @@ namespace Avalonia.Controls.Presenters
var foreground = Foreground; var foreground = Foreground;
if (_compositionRegion != null) if (!string.IsNullOrEmpty(preeditText))
{
var preeditHighlight = new ValueSpan<TextRunProperties>(_compositionRegion?.Start ?? 0, _compositionRegion?.Length ?? 0,
new GenericTextRunProperties(typeface, FontSize,
foregroundBrush: foreground,
textDecorations: TextDecorations.Underline));
textStyleOverrides = new[]
{
preeditHighlight
};
}
else if (!string.IsNullOrEmpty(PreeditText))
{ {
var preeditHighlight = new ValueSpan<TextRunProperties>(CaretIndex, PreeditText.Length, var preeditHighlight = new ValueSpan<TextRunProperties>(caretIndex, preeditText.Length,
new GenericTextRunProperties(typeface, FontSize, new GenericTextRunProperties(typeface, FontSize,
foregroundBrush: foreground, foregroundBrush: foreground,
textDecorations: TextDecorations.Underline)); textDecorations: TextDecorations.Underline));
@ -554,6 +525,27 @@ namespace Avalonia.Controls.Presenters
return result; 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() protected virtual void InvalidateTextLayout()
{ {
_textLayout?.Dispose(); _textLayout?.Dispose();
@ -831,6 +823,18 @@ namespace Avalonia.Controls.Presenters
_caretTimer.Tick -= CaretTimerTick; _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) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
@ -840,10 +844,30 @@ namespace Avalonia.Controls.Presenters
MoveCaretToTextPosition(change.GetNewValue<int>()); MoveCaretToTextPosition(change.GetNewValue<int>());
} }
if(change.Property == PreeditTextProperty)
{
OnPreeditTextChanged(change.NewValue as string);
}
if(change.Property == TextProperty)
{
if (!string.IsNullOrEmpty(PreeditText))
{
SetCurrentValue(PreeditTextProperty, null);
}
}
if(change.Property == CaretIndexProperty)
{
if (!string.IsNullOrEmpty(PreeditText))
{
SetCurrentValue(PreeditTextProperty, null);
}
}
switch (change.Property.Name) switch (change.Property.Name)
{ {
case nameof(PreeditText): case nameof(PreeditText):
case nameof(CompositionRegion):
case nameof(Foreground): case nameof(Foreground):
case nameof(FontSize): case nameof(FontSize):
case nameof(FontStyle): case nameof(FontStyle):

4
src/Avalonia.Controls/TextBox.cs

@ -1000,7 +1000,9 @@ namespace Avalonia.Controls
textBuilder.Insert(caretIndex, input); textBuilder.Insert(caretIndex, input);
SetCurrentValue(TextProperty, StringBuilderCache.GetStringAndRelease(textBuilder)); var text = StringBuilderCache.GetStringAndRelease(textBuilder);
SetCurrentValue(TextProperty, text);
ClearSelection(); ClearSelection();

292
src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

@ -1,243 +1,114 @@
using System; using System;
using System.Diagnostics;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Input.TextInput; using Avalonia.Input.TextInput;
using Avalonia.Media;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using Avalonia.Threading;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
internal class TextBoxTextInputMethodClient : ITextInputMethodClient internal class TextBoxTextInputMethodClient : TextInputMethodClient
{ {
private TextBox? _parent; private TextBox? _parent;
private TextPresenter? _presenter; private TextPresenter? _presenter;
private ITextEditable? _textEditable;
public Visual TextViewVisual => _presenter!; public override Visual TextViewVisual => _presenter!;
public bool SupportsPreedit => true; public override string SurroundingText
public bool SupportsSurroundingText => true;
public Rect CursorRectangle
{ {
get get
{ {
if (_parent == null || _presenter == null) if (_presenter is null || _parent is null)
{ {
return default; return "";
} }
var transform = _presenter.TransformToVisual(_parent); if (_parent.CaretIndex != _presenter.CaretIndex)
if (transform == null)
{ {
return default; _presenter.SetCurrentValue(TextPresenter.CaretIndexProperty, _parent.CaretIndex);
} }
var rect = _presenter.GetCursorRectangle().TransformToAABB(transform.Value); if (_parent.Text != _presenter.Text)
return rect;
}
}
public TextInputMethodSurroundingText SurroundingText
{
get
{
if (_presenter is null || _parent is null)
{ {
return default; _presenter.SetCurrentValue(TextPresenter.TextProperty, _parent.Text);
} }
var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(_presenter.CaretIndex, false); var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(_presenter.CaretIndex, false);
var textLine = _presenter.TextLayout.TextLines[lineIndex]; var textLine = _presenter.TextLayout.TextLines[lineIndex];
var lineStart = textLine.FirstTextSourceIndex;
var lineText = GetTextLineText(textLine); var lineText = GetTextLineText(textLine);
var anchorOffset = Math.Max(0, _parent.SelectionStart - lineStart); return lineText;
var cursorOffset = Math.Max(0, _presenter.SelectionEnd - lineStart);
return new TextInputMethodSurroundingText
{
Text = lineText ?? "",
AnchorOffset = anchorOffset,
CursorOffset = cursorOffset
};
} }
} }
public ITextEditable? TextEditable public override Rect CursorRectangle
{ {
get => _textEditable; set get
{ {
if (_textEditable != null) if (_parent == null || _presenter == null)
{ {
_textEditable.TextChanged -= TextEditable_TextChanged; return default;
_textEditable.SelectionChanged -= TextEditable_SelectionChanged;
_textEditable.CompositionChanged -= TextEditable_CompositionChanged;
} }
_textEditable = value; var transform = _presenter.TransformToVisual(_parent);
if (_textEditable != null) if (transform == null)
{ {
_textEditable.TextChanged += TextEditable_TextChanged; return default;
_textEditable.SelectionChanged += TextEditable_SelectionChanged;
_textEditable.CompositionChanged += TextEditable_CompositionChanged;
if (_presenter != null)
{
_textEditable.Text = _presenter.Text;
_textEditable.SelectionStart = _presenter.SelectionStart;
_textEditable.SelectionEnd = _presenter.SelectionEnd;
}
} }
}
}
private void TextEditable_CompositionChanged(object? sender, EventArgs e) return _presenter.GetCursorRectangle().TransformToAABB(transform.Value);
{
if (_presenter != null && _textEditable != null)
{
_presenter.SetCurrentValue(TextPresenter.CompositionRegionProperty, new TextRange(_textEditable.CompositionStart, _textEditable.CompositionEnd));
} }
} }
private void TextEditable_SelectionChanged(object? sender, EventArgs e) public override TextSelection Selection
{ {
if (_parent != null && _textEditable != null) get
{
_parent.SelectionStart = _textEditable.SelectionStart;
_parent.SelectionEnd = _textEditable.SelectionEnd;
}
}
private void TextEditable_TextChanged(object? sender, EventArgs e)
{
if (_parent != null)
{
if (_parent.Text != _textEditable?.Text)
{
_parent.Text = _textEditable?.Text;
}
}
}
private static string GetTextLineText(TextLine textLine)
{
var builder = StringBuilderCache.Acquire(textLine.Length);
foreach (var run in textLine.TextRuns)
{ {
if (run.Length > 0) if (_presenter is null || _parent is null)
{ {
#if NET6_0_OR_GREATER return default;
builder.Append(run.Text.Span);
#else
builder.Append(run.Text.Span.ToArray());
#endif
} }
}
var lineText = builder.ToString();
StringBuilderCache.Release(builder); var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(_parent.CaretIndex, false);
return lineText;
}
public event EventHandler? TextViewVisualChanged;
public event EventHandler? CursorRectangleChanged; var textLine = _presenter.TextLayout.TextLines[lineIndex];
public event EventHandler? SurroundingTextChanged; var lineStart = textLine.FirstTextSourceIndex;
private string? _presenterText; var selectionStart = Math.Max(0, _parent.SelectionStart - lineStart);
private int _compositionStart;
public void SetPreeditText(string? preeditText) var selectionEnd = Math.Max(0, _parent.SelectionEnd - lineStart);
{
if (_presenter == null || _parent == null)
{
return;
}
if (_presenterText is null) return new TextSelection(selectionStart, selectionEnd);
{
_presenterText = _parent.Text ?? "";
_compositionStart = _parent.CaretIndex;
} }
set
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))
{ {
_presenterText = null; if (_parent is null || _presenter is null)
} {
} return;
}
private string? GetText(string? preeditText) var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(_parent.CaretIndex, false);
{
if (string.IsNullOrEmpty(preeditText))
{
return _presenterText;
}
if (string.IsNullOrEmpty(_presenterText)) var textLine = _presenter.TextLayout.TextLines[lineIndex];
{
return preeditText;
}
var sb = StringBuilderCache.Acquire(_presenterText.Length + preeditText.Length); var lineStart = textLine.FirstTextSourceIndex;
sb.Append(_presenterText); var selectionStart = lineStart + value.Start;
sb.Insert(_compositionStart, preeditText); var selectionEnd = lineStart + value.End;
return StringBuilderCache.GetStringAndRelease(sb); _parent.SelectionStart = selectionStart;
} _parent.SelectionEnd = selectionEnd;
public void SetComposingRegion(TextRange? region) RaiseSelectionChanged();
{
if (_presenter == null)
{
return;
} }
_presenter.SetCurrentValue(TextPresenter.CompositionRegionProperty, region);
} }
public void SelectInSurroundingText(int start, int end) public override bool SupportsPreedit => true;
{
if (_parent is null || _presenter is null)
{
return;
}
var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(_presenter.CaretIndex, false);
var textLine = _presenter.TextLayout.TextLines[lineIndex];
var lineStart = textLine.FirstTextSourceIndex;
var selectionStart = lineStart + start; public override bool SupportsSurroundingText => true;
var selectionEnd = lineStart + end;
_parent.SelectionStart = selectionStart;
_parent.SelectionEnd = selectionEnd;
}
public void SetPresenter(TextPresenter? presenter, TextBox? parent) public void SetPresenter(TextPresenter? presenter, TextBox? parent)
{ {
@ -253,66 +124,71 @@ namespace Avalonia.Controls
_parent.PropertyChanged += OnParentPropertyChanged; _parent.PropertyChanged += OnParentPropertyChanged;
} }
if (_presenter != null) var oldPresenter = _presenter;
{
_presenter.ClearValue(TextPresenter.PreeditTextProperty);
_presenter.ClearValue(TextPresenter.CompositionRegionProperty); if (oldPresenter != null)
{
oldPresenter.ClearValue(TextPresenter.PreeditTextProperty);
_presenter.CaretBoundsChanged -= OnCaretBoundsChanged; oldPresenter.CaretBoundsChanged -= (s,e) => RaiseCursorRectangleChanged();
} }
_presenter = presenter; _presenter = presenter;
if (_presenter != null) if (_presenter != null)
{ {
_presenter.CaretBoundsChanged += OnCaretBoundsChanged; _presenter.CaretBoundsChanged += (s, e) => RaiseCursorRectangleChanged();
} }
TextViewVisualChanged?.Invoke(this, EventArgs.Empty); RaiseTextViewVisualChanged();
OnCaretBoundsChanged(this, EventArgs.Empty); RaiseCursorRectangleChanged();
} }
private void OnParentPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) public override void SetPreeditText(string? preeditText)
{ {
if (e.Property == TextBox.SelectionStartProperty || e.Property == TextBox.SelectionEndProperty) if (_presenter == null || _parent == null)
{ {
if (SupportsSurroundingText) return;
{
SurroundingTextChanged?.Invoke(this, e);
}
if (_textEditable != null)
{
var value = (int)(e.NewValue ?? 0);
if (e.Property == TextBox.SelectionStartProperty)
{
_textEditable.SelectionStart = value;
}
if (e.Property == TextBox.SelectionEndProperty)
{
_textEditable.SelectionEnd = value;
}
}
} }
if (e.Property == TextBox.TextProperty) _presenter.SetCurrentValue(TextPresenter.PreeditTextProperty, preeditText);
}
private static string GetTextLineText(TextLine textLine)
{
var builder = StringBuilderCache.Acquire(textLine.Length);
foreach (var run in textLine.TextRuns)
{ {
if (_textEditable != null) if (run.Length > 0)
{ {
_textEditable.Text = (string?)e.NewValue; #if NET6_0_OR_GREATER
builder.Append(run.Text.Span);
#else
builder.Append(run.Text.Span.ToArray());
#endif
} }
} }
var lineText = builder.ToString();
StringBuilderCache.Release(builder);
return lineText;
} }
private void OnCaretBoundsChanged(object? sender, EventArgs e) private void OnParentPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{ {
Dispatcher.UIThread.Post(() => if (e.Property == TextBox.TextProperty)
{ {
CursorRectangleChanged?.Invoke(this, e); RaiseSurroundingTextChanged();
}
}, DispatcherPriority.Input); if (e.Property == TextBox.SelectionStartProperty || e.Property == TextBox.SelectionEndProperty)
{
RaiseSelectionChanged();
}
} }
} }
} }

6
src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs

@ -41,7 +41,7 @@ namespace Avalonia.FreeDesktop.DBusIme
private PixelRect? _lastReportedRect; private PixelRect? _lastReportedRect;
private double _scaling = 1; private double _scaling = 1;
private PixelPoint _windowPosition; private PixelPoint _windowPosition;
private ITextInputMethodClient? _client; private TextInputMethodClient? _client;
protected bool IsConnected => _currentName != null; protected bool IsConnected => _currentName != null;
@ -53,7 +53,7 @@ namespace Avalonia.FreeDesktop.DBusIme
_ = WatchAsync(); _ = WatchAsync();
} }
public ITextInputMethodClient Client => _client; public TextInputMethodClient Client => _client;
public bool IsActive => _client is not null; public bool IsActive => _client is not null;
@ -210,7 +210,7 @@ namespace Avalonia.FreeDesktop.DBusIme
UpdateActive(); UpdateActive();
} }
void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client) void ITextInputMethodImpl.SetClient(TextInputMethodClient? client)
{ {
_client = client; _client = client;
UpdateActive(); UpdateActive();

18
src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs

@ -1,5 +1,6 @@
using System; using System;
using Avalonia.Input.TextInput; using Avalonia.Input.TextInput;
using Avalonia.Media.TextFormatting;
using Avalonia.Native.Interop; using Avalonia.Native.Interop;
#nullable enable #nullable enable
@ -8,7 +9,7 @@ namespace Avalonia.Native
{ {
internal class AvaloniaNativeTextInputMethod : ITextInputMethodImpl, IDisposable internal class AvaloniaNativeTextInputMethod : ITextInputMethodImpl, IDisposable
{ {
private ITextInputMethodClient? _client; private TextInputMethodClient? _client;
private IAvnTextInputMethodClient? _nativeClient; private IAvnTextInputMethodClient? _nativeClient;
private readonly IAvnTextInputMethod _inputMethod; private readonly IAvnTextInputMethod _inputMethod;
@ -28,7 +29,7 @@ namespace Avalonia.Native
_inputMethod.Reset(); _inputMethod.Reset();
} }
public void SetClient(ITextInputMethodClient? client) public void SetClient(TextInputMethodClient? client)
{ {
if (_client is { SupportsSurroundingText: true }) if (_client is { SupportsSurroundingText: true })
{ {
@ -96,11 +97,12 @@ namespace Avalonia.Native
} }
var surroundingText = _client.SurroundingText; var surroundingText = _client.SurroundingText;
var selection = _client.Selection;
_inputMethod.SetSurroundingText( _inputMethod.SetSurroundingText(
surroundingText.Text ?? "", surroundingText ?? "",
surroundingText.AnchorOffset, selection.Start,
surroundingText.CursorOffset selection.End
); );
} }
@ -116,9 +118,9 @@ namespace Avalonia.Native
private class AvnTextInputMethodClient : NativeCallbackBase, IAvnTextInputMethodClient private class AvnTextInputMethodClient : NativeCallbackBase, IAvnTextInputMethodClient
{ {
private readonly ITextInputMethodClient _client; private readonly TextInputMethodClient _client;
public AvnTextInputMethodClient(ITextInputMethodClient client) public AvnTextInputMethodClient(TextInputMethodClient client)
{ {
_client = client; _client = client;
} }
@ -135,7 +137,7 @@ namespace Avalonia.Native
{ {
if (_client.SupportsSurroundingText) if (_client.SupportsSurroundingText)
{ {
_client.SelectInSurroundingText(start, end); _client.Selection = new TextSelection(start, end);
} }
} }
} }

6
src/Avalonia.X11/X11Window.Xim.cs

@ -15,14 +15,14 @@ namespace Avalonia.X11
private readonly X11Window _parent; private readonly X11Window _parent;
private bool _windowActive, _imeActive; private bool _windowActive, _imeActive;
private Rect? _queuedCursorRect; private Rect? _queuedCursorRect;
private ITextInputMethodClient? _client; private TextInputMethodClient? _client;
public XimInputMethod(X11Window parent) public XimInputMethod(X11Window parent)
{ {
_parent = parent; _parent = parent;
} }
public ITextInputMethodClient? Client => _client; public TextInputMethodClient? Client => _client;
public bool IsActive => _client != null; public bool IsActive => _client != null;
@ -62,7 +62,7 @@ namespace Avalonia.X11
UpdateActive(); UpdateActive();
} }
public void SetClient(ITextInputMethodClient client) public void SetClient(TextInputMethodClient client)
{ {
_client = client; _client = client;
UpdateActive(); UpdateActive();

18
src/Browser/Avalonia.Browser/AvaloniaView.cs

@ -42,7 +42,7 @@ namespace Avalonia.Browser
private const SKColorType ColorType = SKColorType.Rgba8888; private const SKColorType ColorType = SKColorType.Rgba8888;
private bool _useGL; private bool _useGL;
private ITextInputMethodClient? _client; private TextInputMethodClient? _client;
/// <param name="divId">ID of the html element where avalonia content should be rendered.</param> /// <param name="divId">ID of the html element where avalonia content should be rendered.</param>
public AvaloniaView(string divId) public AvaloniaView(string divId)
@ -397,7 +397,7 @@ namespace Avalonia.Browser
if(start != -1 && end != -1 && _client != null) if(start != -1 && end != -1 && _client != null)
{ {
_client.SelectInSurroundingText(start, end); _client.Selection = new TextSelection(start, end);
} }
return false; return false;
} }
@ -513,7 +513,7 @@ namespace Avalonia.Browser
InputHelper.FocusElement(_containerElement); InputHelper.FocusElement(_containerElement);
} }
void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client) void ITextInputMethodImpl.SetClient(TextInputMethodClient? client)
{ {
if (_client != null) if (_client != null)
{ {
@ -534,9 +534,10 @@ namespace Avalonia.Browser
InputHelper.ShowElement(_inputElement); InputHelper.ShowElement(_inputElement);
InputHelper.FocusElement(_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 else
{ {
@ -548,16 +549,17 @@ namespace Avalonia.Browser
{ {
if (_client != null) 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) void ITextInputMethodImpl.SetCursorRect(Rect rect)
{ {
InputHelper.FocusElement(_inputElement); 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); InputHelper.FocusElement(_inputElement);
} }

6
src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs

@ -26,7 +26,7 @@ namespace Avalonia.Win32.Input
private bool _ignoreComposition; private bool _ignoreComposition;
public ITextInputMethodClient? Client { get; private set; } public TextInputMethodClient? Client { get; private set; }
[MemberNotNullWhen(true, nameof(Client))] [MemberNotNullWhen(true, nameof(Client))]
public bool IsActive => Client != null; 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) if(Client != null)
{ {
@ -325,7 +325,7 @@ namespace Avalonia.Win32.Input
{ {
Client.SetPreeditText(null); Client.SetPreeditText(null);
if (Client.SupportsSurroundingText && Client.SurroundingText.AnchorOffset != Client.SurroundingText.CursorOffset) if (Client.SupportsSurroundingText && Client.Selection.Start != Client.Selection.End)
{ {
KeyPress(Key.Delete); KeyPress(Key.Delete);
} }

2
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); private bool IsDrivingText => CurrentAvaloniaResponder is TextInputResponder t && ReferenceEquals(t.NextResponder, this);
void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client) void ITextInputMethodImpl.SetClient(TextInputMethodClient? client)
{ {
_client = client; _client = client;
if (_client == null && IsDrivingText) if (_client == null && IsDrivingText)

2
src/iOS/Avalonia.iOS/AvaloniaView.cs

@ -27,7 +27,7 @@ namespace Avalonia.iOS
private TopLevelImpl _topLevelImpl; private TopLevelImpl _topLevelImpl;
private EmbeddableControlRoot _topLevel; private EmbeddableControlRoot _topLevel;
private TouchHandler _touches; private TouchHandler _touches;
private ITextInputMethodClient _client; private TextInputMethodClient _client;
private IAvaloniaViewController _controller; private IAvaloniaViewController _controller;
public AvaloniaView() public AvaloniaView()

36
src/iOS/Avalonia.iOS/TextInputResponder.cs

@ -78,7 +78,7 @@ partial class AvaloniaView
public NSObject Copy(NSZone? zone) => this; public NSObject Copy(NSZone? zone) => this;
} }
public TextInputResponder(AvaloniaView view, ITextInputMethodClient client) public TextInputResponder(AvaloniaView view, TextInputMethodClient client)
{ {
_view = view; _view = view;
NextResponder = view; NextResponder = view;
@ -88,12 +88,12 @@ partial class AvaloniaView
public override UIResponder NextResponder { get; } public override UIResponder NextResponder { get; }
private readonly ITextInputMethodClient _client; private readonly TextInputMethodClient _client;
private int _inSurroundingTextUpdateEvent; private int _inSurroundingTextUpdateEvent;
private readonly UITextPosition _beginningOfDocument = new AvaloniaTextPosition(0); private readonly UITextPosition _beginningOfDocument = new AvaloniaTextPosition(0);
private readonly UITextInputStringTokenizer _tokenizer; private readonly UITextInputStringTokenizer _tokenizer;
public ITextInputMethodClient? Client => _client; public TextInputMethodClient? Client => _client;
public override bool CanResignFirstResponder => true; public override bool CanResignFirstResponder => true;
@ -201,17 +201,23 @@ partial class AvaloniaView
string IUITextInput.TextInRange(UITextRange range) string IUITextInput.TextInRange(UITextRange range)
{ {
var r = (AvaloniaTextRange)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); Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "IUIKeyInput.TextInRange {start} {end}", r.StartIndex, r.EndIndex);
string result = ""; string result = "";
if (string.IsNullOrEmpty(_markedText)) 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 else
{ {
var span = new CombinedSpan3<char>(s.Text.AsSpan().Slice(0, s.CursorOffset), var span = new CombinedSpan3<char>(surroundingText.AsSpan().Slice(0, currentSelection.Start),
_markedText, _markedText,
s.Text.AsSpan().Slice(s.CursorOffset)); surroundingText.AsSpan().Slice(currentSelection.Start));
var buf = new char[r.EndIndex - r.StartIndex]; var buf = new char[r.EndIndex - r.StartIndex];
span.CopyTo(buf, r.StartIndex); span.CopyTo(buf, r.StartIndex);
result = new string(buf); result = new string(buf);
@ -226,7 +232,7 @@ partial class AvaloniaView
var r = (AvaloniaTextRange)range; var r = (AvaloniaTextRange)range;
Logger.TryGet(LogEventLevel.Debug, ImeLog)? Logger.TryGet(LogEventLevel.Debug, ImeLog)?
.Log(null, "IUIKeyInput.ReplaceText {start} {end} {text}", r.StartIndex, r.EndIndex, text); .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); TextInput(text);
} }
@ -447,21 +453,19 @@ partial class AvaloniaView
{ {
get get
{ {
return new AvaloniaTextRange( return new AvaloniaTextRange(_client.Selection.Start, _client.Selection.End);
Math.Min(_client.SurroundingText.CursorOffset, _client.SurroundingText.AnchorOffset),
Math.Max(_client.SurroundingText.CursorOffset, _client.SurroundingText.AnchorOffset));
} }
set set
{ {
if (_inSurroundingTextUpdateEvent > 0) if (_inSurroundingTextUpdateEvent > 0)
return; return;
if (value == null) if (value == null)
_client.SelectInSurroundingText(_client.SurroundingText.CursorOffset, _client.Selection = default;
_client.SurroundingText.CursorOffset);
else else
{ {
var r = (AvaloniaTextRange)value; 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; 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); UITextPosition IUITextInput.EndOfDocument => new AvaloniaTextPosition(DocumentLength);
UITextRange IUITextInput.MarkedTextRange UITextRange IUITextInput.MarkedTextRange
@ -483,7 +487,7 @@ partial class AvaloniaView
{ {
if (string.IsNullOrWhiteSpace(_markedText)) if (string.IsNullOrWhiteSpace(_markedText))
return null!; return null!;
return new AvaloniaTextRange(_client.SurroundingText.CursorOffset, _client.SurroundingText.CursorOffset + _markedText.Length); return new AvaloniaTextRange(_client.Selection.Start, _client.Selection.Start + _markedText.Length);
} }
} }

Loading…
Cancel
Save