Skip to content

Commit

Permalink
X11 IME preedit, preedit cursor, input context improvements (#13282)
Browse files Browse the repository at this point in the history
Co-authored-by: Dan Walmsley <[email protected]>
  • Loading branch information
kekekeks and danwalmsley authored Oct 17, 2023
1 parent 43f1b9d commit 5b02b03
Show file tree
Hide file tree
Showing 13 changed files with 340 additions and 49 deletions.
26 changes: 24 additions & 2 deletions src/Avalonia.Base/Input/InputMethod.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
namespace Avalonia.Input
using System;
using Avalonia.Input.TextInput;
using Avalonia.Interactivity;

namespace Avalonia.Input
{
public class InputMethod
{
Expand All @@ -23,7 +27,25 @@ public static bool GetIsInputMethodEnabled(InputElement target)
{
return target.GetValue<bool>(IsInputMethodEnabledProperty);
}


/// <summary>
/// Defines the <see cref="TextInputMethodClientRequeryRequested"/> event.
/// </summary>
public static readonly RoutedEvent<TextInputMethodClientRequeryRequestedEventArgs> TextInputMethodClientRequeryRequestedEvent =
RoutedEvent.Register<InputElement, TextInputMethodClientRequeryRequestedEventArgs>(
"TextInputMethodClientRequeryRequested",
RoutingStrategies.Bubble);

public static void AddTextInputMethodClientRequeryRequestedHandler(Interactive element, EventHandler<RoutedEventArgs> handler)
{
element.AddHandler(TextInputMethodClientRequeryRequestedEvent, handler);
}

public static void RemoveTextInputMethodClientRequeryRequestedHandler(Interactive element, EventHandler<RoutedEventArgs> handler)
{
element.AddHandler(TextInputMethodClientRequeryRequestedEvent, handler);
}

private InputMethod()
{

Expand Down
63 changes: 49 additions & 14 deletions src/Avalonia.Base/Input/TextInput/InputMethodManager.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using Avalonia.Interactivity;
using Avalonia.Reactive;

namespace Avalonia.Input.TextInput
Expand All @@ -7,6 +8,7 @@ internal class TextInputMethodManager
{
private ITextInputMethodImpl? _im;
private IInputElement? _focusedElement;
private Interactive? _visualRoot;
private TextInputMethodClient? _client;
private readonly TransformTrackingHelper _transformTracker = new TransformTrackingHelper();

Expand All @@ -30,6 +32,7 @@ private TextInputMethodClient? Client
{
_client.CursorRectangleChanged -= OnCursorRectangleChanged;
_client.TextViewVisualChanged -= OnTextViewVisualChanged;
_client.ResetRequested -= OnResetRequested;

_client = null;

Expand All @@ -42,21 +45,9 @@ private TextInputMethodClient? Client
{
_client.CursorRectangleChanged += OnCursorRectangleChanged;
_client.TextViewVisualChanged += OnTextViewVisualChanged;
_client.ResetRequested += OnResetRequested;

if (_focusedElement is StyledElement target)
{
_im?.SetOptions(TextInputOptions.FromStyledElement(target));
}
else
{
_im?.SetOptions(TextInputOptions.Default);
}

_transformTracker.SetVisual(_client?.TextViewVisual);

_im?.SetClient(_client);

UpdateCursorRect();
PopulateImWithInitialValues();
}
else
{
Expand All @@ -66,6 +57,33 @@ private TextInputMethodClient? Client
}
}

void PopulateImWithInitialValues()
{
if (_focusedElement is StyledElement target)
{
_im?.SetOptions(TextInputOptions.FromStyledElement(target));
}
else
{
_im?.SetOptions(TextInputOptions.Default);
}

_transformTracker.SetVisual(_client?.TextViewVisual);

_im?.SetClient(_client);

UpdateCursorRect();
}

private void OnResetRequested(object? sender, EventArgs args)
{
if (_im != null && sender == _client)
{
_im.Reset();
PopulateImWithInitialValues();
}
}

private void OnIsInputMethodEnabledChanged(AvaloniaPropertyChangedEventArgs<bool> obj)
{
if (ReferenceEquals(obj.Sender, _focusedElement))
Expand Down Expand Up @@ -102,8 +120,18 @@ public void SetFocusedElement(IInputElement? element)
{
if(_focusedElement == element)
return;

if (_visualRoot != null)
InputMethod.RemoveTextInputMethodClientRequeryRequestedHandler(_visualRoot,
TextInputMethodClientRequeryRequested);

_focusedElement = element;

_visualRoot = (element as Visual)?.VisualRoot as Interactive;
if (_visualRoot != null)
InputMethod.AddTextInputMethodClientRequeryRequestedHandler(_visualRoot,
TextInputMethodClientRequeryRequested);

var inputMethod = ((element as Visual)?.VisualRoot as ITextInputMethodRoot)?.InputMethod;

if (_im != inputMethod)
Expand All @@ -112,10 +140,17 @@ public void SetFocusedElement(IInputElement? element)
}

_im = inputMethod;


TryFindAndApplyClient();
}

private void TextInputMethodClientRequeryRequested(object? sender, RoutedEventArgs e)
{
if (_im != null)
TryFindAndApplyClient();
}

private void TryFindAndApplyClient()
{
if (_focusedElement is not InputElement focused ||
Expand Down
18 changes: 18 additions & 0 deletions src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ public abstract class TextInputMethodClient
/// Fires when the selection has changed
/// </summary>
public event EventHandler? SelectionChanged;

/// <summary>
/// Fires when client wants to reset IME state
/// </summary>
public event EventHandler? ResetRequested;

/// <summary>
/// The visual that's showing the text
Expand Down Expand Up @@ -59,6 +64,14 @@ public abstract class TextInputMethodClient
/// </summary>
public virtual void SetPreeditText(string? preeditText) { }

/// <summary>
/// Sets the non-committed input string and cursor offset in that string
/// </summary>
public virtual void SetPreeditText(string? preeditText, int? cursorPos)
{
SetPreeditText(preeditText);
}

protected virtual void RaiseTextViewVisualChanged()
{
TextViewVisualChanged?.Invoke(this, EventArgs.Empty);
Expand All @@ -78,6 +91,11 @@ protected virtual void RaiseSelectionChanged()
{
SelectionChanged?.Invoke(this, EventArgs.Empty);
}

protected virtual void RequestReset()
{
ResetRequested?.Invoke(this, EventArgs.Empty);
}
}

public record struct TextSelection(int Start, int End);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Avalonia.Interactivity;

namespace Avalonia.Input.TextInput;

public class TextInputMethodClientRequeryRequestedEventArgs : RoutedEventArgs
{

}
25 changes: 25 additions & 0 deletions src/Avalonia.Base/Media/TextFormatting/Unicode/Utf16Utils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;

namespace Avalonia.Media.TextFormatting.Unicode;

internal class Utf16Utils
{
public static int CharacterOffsetToStringOffset(string s, int off, bool throwOnOutOfRange)
{
if (off == 0)
return 0;
var symbolOffset = 0;
for (var c = 0; c < s.Length; c++)
{
if (symbolOffset == off)
return c;

if (!char.IsSurrogatePair(s, c))
symbolOffset++;
}

if (throwOnOutOfRange)
throw new IndexOutOfRangeException();
return s.Length;
}
}
28 changes: 24 additions & 4 deletions src/Avalonia.Controls/Presenters/TextPresenter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ public class TextPresenter : Control
/// </summary>
public static readonly StyledProperty<string?> PreeditTextProperty =
AvaloniaProperty.Register<TextPresenter, string?>(nameof(PreeditText));

/// <summary>
/// Defines the <see cref="PreeditText"/> property.
/// </summary>
public static readonly StyledProperty<int?> PreeditTextCursorPositionProperty =
AvaloniaProperty.Register<TextPresenter, int?>(nameof(PreeditTextCursorPosition));

/// <summary>
/// Defines the <see cref="TextAlignment"/> property.
Expand Down Expand Up @@ -125,6 +131,12 @@ public string? PreeditText
get => GetValue(PreeditTextProperty);
set => SetValue(PreeditTextProperty, value);
}

public int? PreeditTextCursorPosition
{
get => GetValue(PreeditTextCursorPositionProperty);
set => SetValue(PreeditTextCursorPositionProperty, value);
}

/// <summary>
/// Gets or sets the font family.
Expand Down Expand Up @@ -828,16 +840,19 @@ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e

_caretTimer.Tick -= CaretTimerTick;
}

private void OnPreeditTextChanged(string? preeditText)
private void OnPreeditChanged(string? preeditText, int? cursorPosition)
{
if (string.IsNullOrEmpty(preeditText))
{
UpdateCaret(new CharacterHit(CaretIndex), false);
}
else
{
UpdateCaret(new CharacterHit(CaretIndex + preeditText.Length), false);
var cursorPos = cursorPosition is >= 0 && cursorPosition <= preeditText.Length
? cursorPosition.Value
: preeditText.Length;
UpdateCaret(new CharacterHit(CaretIndex + cursorPos), false);
InvalidateMeasure();
CaretChanged();
}
Expand All @@ -854,7 +869,12 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang

if(change.Property == PreeditTextProperty)
{
OnPreeditTextChanged(change.NewValue as string);
OnPreeditChanged(change.NewValue as string, PreeditTextCursorPosition);
}

if(change.Property == PreeditTextCursorPositionProperty)
{
OnPreeditChanged(PreeditText, PreeditTextCursorPosition);
}

if(change.Property == TextProperty)
Expand Down
5 changes: 4 additions & 1 deletion src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,14 +145,17 @@ public void SetPresenter(TextPresenter? presenter, TextBox? parent)
RaiseCursorRectangleChanged();
}

public override void SetPreeditText(string? preeditText)
public override void SetPreeditText(string? preeditText) => SetPreeditText(preeditText, null);

public override void SetPreeditText(string? preeditText, int? cursorPos)
{
if (_presenter == null || _parent == null)
{
return;
}

_presenter.SetCurrentValue(TextPresenter.PreeditTextProperty, preeditText);
_presenter.SetCurrentValue(TextPresenter.PreeditTextCursorPositionProperty, cursorPos);
}

private static string GetTextLineText(TextLine textLine)
Expand Down
16 changes: 15 additions & 1 deletion src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public DBusTextInputMethodBase(Connection connection, params string[] knownNames
_ = WatchAsync();
}

public TextInputMethodClient Client => _client;
public TextInputMethodClient? Client => _client;

public bool IsActive => _client is not null;

Expand Down Expand Up @@ -190,6 +190,8 @@ public void Dispose()

protected abstract Task SetCursorRectCore(PixelRect rect);
protected abstract Task SetActiveCore(bool active);

protected virtual Task SetCapabilitiesCore(bool supportsPreedit, bool supportsSurroundingText) => Task.CompletedTask;
protected abstract Task ResetContextCore();
protected abstract Task<bool> HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode);

Expand All @@ -208,6 +210,17 @@ private void UpdateActive()
}
});
}

private void UpdateCapabilities(bool supportsPreedit, bool supportsSurroundingText)
{
_queue.Enqueue(async () =>
{
if(!IsConnected)
return;
await SetCapabilitiesCore(supportsPreedit, supportsSurroundingText);
});
}


void IX11InputMethodControl.SetWindowActive(bool active)
Expand All @@ -220,6 +233,7 @@ void ITextInputMethodImpl.SetClient(TextInputMethodClient? client)
{
_client = client;
UpdateActive();
UpdateCapabilities(client?.SupportsPreedit ?? false, client?.SupportsSurroundingText ?? false);
}

bool IX11InputMethodControl.IsEnabled => IsConnected && _imeActive == true;
Expand Down
6 changes: 6 additions & 0 deletions src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ public async Task<bool> ProcessKeyEventAsync(uint keyVal, uint keyCode, uint sta
?? _modern?.WatchForwardKeyAsync((e, ev) => handler.Invoke(e, (ev.keyval, ev.state, ev.type ? 1 : 0)))
?? new ValueTask<IDisposable?>(default(IDisposable?));

public ValueTask<IDisposable?> WatchUpdateFormattedPreeditAsync(
Action<Exception?, ((string, int)[] @str, int @cursorpos)> handler) =>
_old?.WatchUpdateFormattedPreeditAsync(handler)
?? _modern?.WatchUpdateFormattedPreeditAsync(handler)
?? new(default);

public Task SetCapacityAsync(uint flags) =>
_old?.SetCapacityAsync(flags) ?? _modern?.SetCapabilityAsync(flags) ?? Task.CompletedTask;
}
Expand Down
Loading

0 comments on commit 5b02b03

Please sign in to comment.