From 2adf3d5626d4150587d6d260a6742df21cc563e2 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 17 Oct 2023 15:20:43 +0600 Subject: [PATCH] X11 IME preedit, preedit cursor, input context improvements --- src/Avalonia.Base/Input/InputMethod.cs | 26 ++++- .../Input/TextInput/InputMethodManager.cs | 63 ++++++++--- .../Input/TextInput/TextInputMethodClient.cs | 18 +++ ...utMethodClientRequeryRequestedEventArgs.cs | 8 ++ .../TextFormatting/Unicode/Utf16Utils.cs | 25 +++++ .../Presenters/TextPresenter.cs | 28 ++++- .../TextBoxTextInputMethodClient.cs | 5 +- .../DBusIme/DBusTextInputMethodBase.cs | 16 ++- .../DBusIme/Fcitx/FcitxICWrapper.cs | 6 + .../DBusIme/Fcitx/FcitxX11TextInputMethod.cs | 104 +++++++++++++----- .../DBusIme/IBus/IBusX11TextInputMethod.cs | 54 ++++++++- src/Avalonia.X11/X11Platform.cs | 2 +- .../Media/TextFormatting/Utf16UtilsTests.cs | 34 ++++++ 13 files changed, 340 insertions(+), 49 deletions(-) create mode 100644 src/Avalonia.Base/Input/TextInput/TextInputMethodClientRequeryRequestedEventArgs.cs create mode 100644 src/Avalonia.Base/Media/TextFormatting/Unicode/Utf16Utils.cs create mode 100644 tests/Avalonia.Base.UnitTests/Media/TextFormatting/Utf16UtilsTests.cs diff --git a/src/Avalonia.Base/Input/InputMethod.cs b/src/Avalonia.Base/Input/InputMethod.cs index 8098b18c47..39e7d6d31f 100644 --- a/src/Avalonia.Base/Input/InputMethod.cs +++ b/src/Avalonia.Base/Input/InputMethod.cs @@ -1,4 +1,8 @@ -namespace Avalonia.Input +using System; +using Avalonia.Input.TextInput; +using Avalonia.Interactivity; + +namespace Avalonia.Input { public class InputMethod { @@ -23,7 +27,25 @@ { return target.GetValue(IsInputMethodEnabledProperty); } - + + /// + /// Defines the event. + /// + public static readonly RoutedEvent TextInputMethodClientRequeryRequestedEvent = + RoutedEvent.Register( + "TextInputMethodClientRequeryRequested", + RoutingStrategies.Bubble); + + public static void AddTextInputMethodClientRequeryRequestedHandler(Interactive element, EventHandler handler) + { + element.AddHandler(TextInputMethodClientRequeryRequestedEvent, handler); + } + + public static void RemoveTextInputMethodClientRequeryRequestedHandler(Interactive element, EventHandler handler) + { + element.AddHandler(TextInputMethodClientRequeryRequestedEvent, handler); + } + private InputMethod() { diff --git a/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs b/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs index 387179b970..ae3a8bc6a1 100644 --- a/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs +++ b/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Interactivity; using Avalonia.Reactive; namespace Avalonia.Input.TextInput @@ -7,6 +8,7 @@ namespace Avalonia.Input.TextInput { private ITextInputMethodImpl? _im; private IInputElement? _focusedElement; + private Interactive? _visualRoot; private TextInputMethodClient? _client; private readonly TransformTrackingHelper _transformTracker = new TransformTrackingHelper(); @@ -30,6 +32,7 @@ namespace Avalonia.Input.TextInput { _client.CursorRectangleChanged -= OnCursorRectangleChanged; _client.TextViewVisualChanged -= OnTextViewVisualChanged; + _client.ResetRequested -= OnResetRequested; _client = null; @@ -42,21 +45,9 @@ namespace Avalonia.Input.TextInput { _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 { @@ -66,6 +57,33 @@ namespace Avalonia.Input.TextInput } } + 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 obj) { if (ReferenceEquals(obj.Sender, _focusedElement)) @@ -102,8 +120,18 @@ namespace Avalonia.Input.TextInput { 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) @@ -112,10 +140,17 @@ namespace Avalonia.Input.TextInput } _im = inputMethod; + TryFindAndApplyClient(); } + private void TextInputMethodClientRequeryRequested(object? sender, RoutedEventArgs e) + { + if (_im != null) + TryFindAndApplyClient(); + } + private void TryFindAndApplyClient() { if (_focusedElement is not InputElement focused || diff --git a/src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs b/src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs index 7cf3752057..61a6d3e30a 100644 --- a/src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs +++ b/src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs @@ -23,6 +23,11 @@ namespace Avalonia.Input.TextInput /// Fires when the selection has changed /// public event EventHandler? SelectionChanged; + + /// + /// Fires when client wants to reset IME state + /// + public event EventHandler? ResetRequested; /// /// The visual that's showing the text @@ -59,6 +64,14 @@ namespace Avalonia.Input.TextInput /// public virtual void SetPreeditText(string? preeditText) { } + /// + /// Sets the non-committed input string and cursor offset in that string + /// + public virtual void SetPreeditText(string? preeditText, int? cursorPos) + { + SetPreeditText(preeditText); + } + protected virtual void RaiseTextViewVisualChanged() { TextViewVisualChanged?.Invoke(this, EventArgs.Empty); @@ -78,6 +91,11 @@ namespace Avalonia.Input.TextInput { SelectionChanged?.Invoke(this, EventArgs.Empty); } + + protected virtual void RequestReset() + { + ResetRequested?.Invoke(this, EventArgs.Empty); + } } public record struct TextSelection(int Start, int End); diff --git a/src/Avalonia.Base/Input/TextInput/TextInputMethodClientRequeryRequestedEventArgs.cs b/src/Avalonia.Base/Input/TextInput/TextInputMethodClientRequeryRequestedEventArgs.cs new file mode 100644 index 0000000000..f10491973e --- /dev/null +++ b/src/Avalonia.Base/Input/TextInput/TextInputMethodClientRequeryRequestedEventArgs.cs @@ -0,0 +1,8 @@ +using Avalonia.Interactivity; + +namespace Avalonia.Input.TextInput; + +public class TextInputMethodClientRequeryRequestedEventArgs : RoutedEventArgs +{ + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Utf16Utils.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Utf16Utils.cs new file mode 100644 index 0000000000..a13c0547c8 --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Utf16Utils.cs @@ -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; + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index df1d37c259..a837f10eee 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -49,6 +49,12 @@ namespace Avalonia.Controls.Presenters /// public static readonly StyledProperty PreeditTextProperty = AvaloniaProperty.Register(nameof(PreeditText)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty PreeditTextCursorPositionProperty = + AvaloniaProperty.Register(nameof(PreeditTextCursorPosition)); /// /// Defines the property. @@ -125,6 +131,12 @@ namespace Avalonia.Controls.Presenters get => GetValue(PreeditTextProperty); set => SetValue(PreeditTextProperty, value); } + + public int? PreeditTextCursorPosition + { + get => GetValue(PreeditTextCursorPositionProperty); + set => SetValue(PreeditTextCursorPositionProperty, value); + } /// /// Gets or sets the font family. @@ -828,8 +840,8 @@ namespace Avalonia.Controls.Presenters _caretTimer.Tick -= CaretTimerTick; } - - private void OnPreeditTextChanged(string? preeditText) + + private void OnPreeditChanged(string? preeditText, int? cursorPosition) { if (string.IsNullOrEmpty(preeditText)) { @@ -837,7 +849,10 @@ namespace Avalonia.Controls.Presenters } 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(); } @@ -854,7 +869,12 @@ namespace Avalonia.Controls.Presenters 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) diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index 28a230640a..faf2450a9a 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -145,7 +145,9 @@ namespace Avalonia.Controls 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) { @@ -153,6 +155,7 @@ namespace Avalonia.Controls } _presenter.SetCurrentValue(TextPresenter.PreeditTextProperty, preeditText); + _presenter.SetCurrentValue(TextPresenter.PreeditTextCursorPositionProperty, cursorPos); } private static string GetTextLineText(TextLine textLine) diff --git a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs index b897d52204..ee118d92b5 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs @@ -53,7 +53,7 @@ namespace Avalonia.FreeDesktop.DBusIme _ = WatchAsync(); } - public TextInputMethodClient Client => _client; + public TextInputMethodClient? Client => _client; public bool IsActive => _client is not null; @@ -190,6 +190,8 @@ namespace Avalonia.FreeDesktop.DBusIme 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 HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode); @@ -208,6 +210,17 @@ namespace Avalonia.FreeDesktop.DBusIme } }); } + + private void UpdateCapabilities(bool supportsPreedit, bool supportsSurroundingText) + { + _queue.Enqueue(async () => + { + if(!IsConnected) + return; + + await SetCapabilitiesCore(supportsPreedit, supportsSurroundingText); + }); + } void IX11InputMethodControl.SetWindowActive(bool active) @@ -220,6 +233,7 @@ namespace Avalonia.FreeDesktop.DBusIme { _client = client; UpdateActive(); + UpdateCapabilities(client?.SupportsPreedit ?? false, client?.SupportsSurroundingText ?? false); } bool IX11InputMethodControl.IsEnabled => IsConnected && _imeActive == true; diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs index 00d05e59a3..a0bfad6fe4 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs @@ -47,6 +47,12 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx ?? _modern?.WatchForwardKeyAsync((e, ev) => handler.Invoke(e, (ev.keyval, ev.state, ev.type ? 1 : 0))) ?? new ValueTask(default(IDisposable?)); + public ValueTask WatchUpdateFormattedPreeditAsync( + Action handler) => + _old?.WatchUpdateFormattedPreeditAsync(handler) + ?? _modern?.WatchUpdateFormattedPreeditAsync(handler) + ?? new(default); + public Task SetCapacityAsync(uint flags) => _old?.SetCapacityAsync(flags) ?? _modern?.SetCapabilityAsync(flags) ?? Task.CompletedTask; } diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs index 2eca5a1fef..12e365e28f 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs @@ -1,5 +1,7 @@ using System; using System.Diagnostics; +using System.Linq; +using System.Text; using System.Threading.Tasks; using Avalonia.Input; using Avalonia.Input.Raw; @@ -14,6 +16,8 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx { private FcitxICWrapper? _context; private FcitxCapabilityFlags? _lastReportedFlags; + private FcitxCapabilityFlags _optionFlags; + private FcitxCapabilityFlags _capabilityFlags; public FcitxX11TextInputMethod(Connection connection) : base(connection, "org.fcitx.Fcitx", "org.freedesktop.portal.Fcitx") { } @@ -38,9 +42,36 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx AddDisposable(await _context.WatchCommitStringAsync(OnCommitString)); AddDisposable(await _context.WatchForwardKeyAsync(OnForward)); + AddDisposable(await _context.WatchUpdateFormattedPreeditAsync(OnPreedit)); return true; } + private void OnPreedit(Exception? arg1, ((string, int)[] str, int cursorpos) args) + { + int? cursor = null; + string preeditString = null; + if (args.str != null! && args.str.Length > 0) + { + preeditString = string.Join("", args.str.Select(x => x.Item1)); + + if (preeditString.Length > 0 && args.cursorpos >= 0) + { + // cursorpos is a byte offset in UTF8 sequence that got sent through dbus + // Tmds.DBus has already converted it to UTF16, so we need to convert it back + // and figure out the byte offset + var utf8String = Encoding.UTF8.GetBytes(preeditString); + if (utf8String.Length >= args.cursorpos) + { + cursor = Encoding.UTF8.GetCharCount(utf8String, 0, args.cursorpos); + } + } + } + + if (Client?.SupportsPreedit == true) + Client.SetPreeditText(preeditString, cursor); + + } + protected override Task DisconnectAsync() => _context?.DestroyICAsync() ?? Task.CompletedTask; protected override void OnDisconnected() => _context = null; @@ -85,33 +116,56 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx return false; } + private void UpdateOptionsField(TextInputOptions options) + { + FcitxCapabilityFlags flags = default; + if (options.Lowercase) + flags |= FcitxCapabilityFlags.CAPACITY_LOWERCASE; + if (options.Uppercase) + flags |= FcitxCapabilityFlags.CAPACITY_UPPERCASE; + if (!options.AutoCapitalization) + flags |= FcitxCapabilityFlags.CAPACITY_NOAUTOUPPERCASE; + if (options.ContentType == TextInputContentType.Email) + flags |= FcitxCapabilityFlags.CAPACITY_EMAIL; + else if (options.ContentType == TextInputContentType.Number) + flags |= FcitxCapabilityFlags.CAPACITY_NUMBER; + else if (options.ContentType == TextInputContentType.Password) + flags |= FcitxCapabilityFlags.CAPACITY_PASSWORD; + else if (options.ContentType == TextInputContentType.Digits) + flags |= FcitxCapabilityFlags.CAPACITY_DIALABLE; + else if (options.ContentType == TextInputContentType.Url) + flags |= FcitxCapabilityFlags.CAPACITY_URL; + _optionFlags = flags; + } + + async Task PushFlagsIfNeeded() + { + if(_context == null) + return; + + var flags = _optionFlags | _capabilityFlags; + + if (flags != _lastReportedFlags) + { + _lastReportedFlags = flags; + await _context.SetCapacityAsync((uint)flags); + } + } + + protected override Task SetCapabilitiesCore(bool supportsPreedit, bool supportsSurroundingText) + { + _capabilityFlags = default; + if (supportsPreedit) + _capabilityFlags = FcitxCapabilityFlags.CAPACITY_PREEDIT; + + return PushFlagsIfNeeded(); + } + public override void SetOptions(TextInputOptions options) => - Enqueue(async () => + Enqueue(() => { - if(_context == null) - return; - FcitxCapabilityFlags flags = default; - if (options.Lowercase) - flags |= FcitxCapabilityFlags.CAPACITY_LOWERCASE; - if (options.Uppercase) - flags |= FcitxCapabilityFlags.CAPACITY_UPPERCASE; - if (!options.AutoCapitalization) - flags |= FcitxCapabilityFlags.CAPACITY_NOAUTOUPPERCASE; - if (options.ContentType == TextInputContentType.Email) - flags |= FcitxCapabilityFlags.CAPACITY_EMAIL; - else if (options.ContentType == TextInputContentType.Number) - flags |= FcitxCapabilityFlags.CAPACITY_NUMBER; - else if (options.ContentType == TextInputContentType.Password) - flags |= FcitxCapabilityFlags.CAPACITY_PASSWORD; - else if (options.ContentType == TextInputContentType.Digits) - flags |= FcitxCapabilityFlags.CAPACITY_DIALABLE; - else if (options.ContentType == TextInputContentType.Url) - flags |= FcitxCapabilityFlags.CAPACITY_URL; - if (flags != _lastReportedFlags) - { - _lastReportedFlags = flags; - await _context.SetCapacityAsync((uint)flags); - } + UpdateOptionsField(options); + return PushFlagsIfNeeded(); }); private void OnForward(Exception? e, (uint keyval, uint state, int type) ev) diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs index 26fa971106..e96172e5a6 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs @@ -4,6 +4,7 @@ using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; using Avalonia.Logging; +using Avalonia.Media.TextFormatting.Unicode; using Tmds.DBus.Protocol; using Tmds.DBus.SourceGenerator; @@ -14,6 +15,9 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus { private OrgFreedesktopIBusService? _service; private OrgFreedesktopIBusInputContext? _context; + private string? _preeditText; + private int _preeditCursor; + private bool _preeditShown = true; public IBusX11TextInputMethod(Connection connection) : base(connection, "org.freedesktop.portal.IBus") { } @@ -25,10 +29,47 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus _context = new OrgFreedesktopIBusInputContext(Connection, name, path); AddDisposable(await _context.WatchCommitTextAsync(OnCommitText)); AddDisposable(await _context.WatchForwardKeyEventAsync(OnForwardKey)); + AddDisposable(await _context.WatchUpdatePreeditTextAsync(OnUpdatePreedit)); + AddDisposable(await _context.WatchShowPreeditTextAsync(OnShowPreedit)); + AddDisposable(await _context.WatchHidePreeditTextAsync(OnHidePreedit)); Enqueue(() => _context.SetCapabilitiesAsync((uint)IBusCapability.CapFocus)); return true; } + private void OnHidePreedit(Exception? obj) + { + _preeditShown = false; + if (Client?.SupportsPreedit == true) + Client.SetPreeditText(null, null); + } + + private void OnShowPreedit(Exception? obj) + { + _preeditShown = true; + if (Client?.SupportsPreedit == true) + Client.SetPreeditText(_preeditText, _preeditText == null ? null : _preeditCursor); + } + + private void OnUpdatePreedit(Exception? arg1, (DBusVariantItem text, uint cursor_pos, bool visible) preeditComponents) + { + + if (preeditComponents.text.Value is DBusStructItem { Count: >= 3 } structItem && + structItem[2] is DBusStringItem stringItem) + { + _preeditText = stringItem.Value; + _preeditCursor = _preeditText != null + ? Utf16Utils.CharacterOffsetToStringOffset(_preeditText, + (int)Math.Min(preeditComponents.cursor_pos, int.MaxValue), false) + : 0; + + _preeditShown = true; + + if (Client?.SupportsPreedit == true) + Client.SetPreeditText( + _preeditText, _preeditCursor); + } + } + private void OnForwardKey(Exception? e, (uint keyval, uint keycode, uint state) k) { if (e is not null) @@ -85,7 +126,10 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus ?? Task.CompletedTask; protected override Task ResetContextCore() - => _context?.ResetAsync() ?? Task.CompletedTask; + { + _preeditShown = true; + return _context?.ResetAsync() ?? Task.CompletedTask; + } protected override Task HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode) { @@ -109,5 +153,13 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus { // No-op, because ibus } + + protected override async Task SetCapabilitiesCore(bool supportsPreedit, bool supportsSurroundingText) + { + var caps = IBusCapability.CapFocus; + if (supportsPreedit) + caps |= IBusCapability.CapPreeditText; + await _context.SetCapabilitiesAsync((uint)caps); + } } } diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index cd667e6c01..e8dd7d098a 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -283,7 +283,7 @@ namespace Avalonia /// Input method editor is a component that enables users to generate characters not natively available /// on their input devices by using sequences of characters or mouse operations that are natively available on their input devices. /// - public bool? EnableIme { get; set; } + public bool? EnableIme { get; set; } = true; /// /// Determines whether to enable support for the diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/Utf16UtilsTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/Utf16UtilsTests.cs new file mode 100644 index 0000000000..0443069307 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/Utf16UtilsTests.cs @@ -0,0 +1,34 @@ +using System; +using Avalonia.Media.TextFormatting.Unicode; +using Xunit; + +namespace Avalonia.Base.UnitTests.Media.TextFormatting; + +public class Utf16UtilsTests +{ + [Theory, + InlineData("\ud87e\udc32123", 1, 2), + InlineData("\ud87e\udc32123", 2, 3), + InlineData("test", 3, 3), + InlineData("\ud87e\udc32", 0, 0), + InlineData("12\ud87e\udc3212", 2, 2), + InlineData("12\ud87e\udc3212", 3, 4), + ] + public void CharacterOffsetToStringOffset(string s, int charOffset, int stringOffset) + { + Assert.Equal(stringOffset, Utf16Utils.CharacterOffsetToStringOffset(s, charOffset, false)); + } + + [Theory, + InlineData("\ud87e\udc32", 2, true), + InlineData("12", 2, true), + ] + public void CharacterOffsetToStringOffsetThrowsOnOutOfRange(string s, int charOffset, bool throws) + { + if (throws) + Assert.Throws(() => + Utf16Utils.CharacterOffsetToStringOffset(s, charOffset, true)); + else + Utf16Utils.CharacterOffsetToStringOffset(s, charOffset, true); + } +} \ No newline at end of file