diff --git a/Avalonia.sln.DotSettings b/Avalonia.sln.DotSettings index 25d62b0494..2c0a6b9dc8 100644 --- a/Avalonia.sln.DotSettings +++ b/Avalonia.sln.DotSettings @@ -38,4 +38,5 @@ <Policy Inspect="False" Prefix="T" Suffix="" Style="AaBb" /> <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /> True - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/build/AndroidWorkarounds.props b/build/AndroidWorkarounds.props index 67947296b3..de86acc6de 100644 --- a/build/AndroidWorkarounds.props +++ b/build/AndroidWorkarounds.props @@ -2,7 +2,7 @@ - + diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props index e636461ad9..13419eb173 100644 --- a/build/HarfBuzzSharp.props +++ b/build/HarfBuzzSharp.props @@ -1,6 +1,6 @@  - - + + diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj index 5b82e2caee..97bd0eac86 100644 --- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj +++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj @@ -16,7 +16,7 @@ Resources\Resource.Designer.cs Off False - v9.0 + v10.0 Properties\AndroidManifest.xml @@ -156,4 +156,4 @@ - + \ No newline at end of file diff --git a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml index e39ec39f1c..02e97f3065 100644 --- a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml +++ b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/samples/ControlCatalog.Android/Resources/Resource.Designer.cs b/samples/ControlCatalog.Android/Resources/Resource.Designer.cs index 96f0e76fd8..4d0a6eff58 100644 --- a/samples/ControlCatalog.Android/Resources/Resource.Designer.cs +++ b/samples/ControlCatalog.Android/Resources/Resource.Designer.cs @@ -2,7 +2,6 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -15,7 +14,7 @@ namespace ControlCatalog.Android { - [System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")] public partial class Resource { @@ -26,8 +25,6 @@ namespace ControlCatalog.Android public static void UpdateIdValues() { - global::Avalonia.Android.Resource.String.ApplicationName = global::ControlCatalog.Android.Resource.String.ApplicationName; - global::Avalonia.Android.Resource.String.Hello = global::ControlCatalog.Android.Resource.String.Hello; } public partial class Attribute @@ -46,8 +43,8 @@ namespace ControlCatalog.Android public partial class Drawable { - // aapt resource value: 0x7f020000 - public const int Icon = 2130837504; + // aapt resource value: 0x7F010000 + public const int Icon = 2130771968; static Drawable() { @@ -62,8 +59,8 @@ namespace ControlCatalog.Android public partial class Id { - // aapt resource value: 0x7f050000 - public const int MyButton = 2131034112; + // aapt resource value: 0x7F020000 + public const int MyButton = 2130837504; static Id() { @@ -78,7 +75,7 @@ namespace ControlCatalog.Android public partial class Layout { - // aapt resource value: 0x7f030000 + // aapt resource value: 0x7F030000 public const int Main = 2130903040; static Layout() @@ -94,11 +91,11 @@ namespace ControlCatalog.Android public partial class String { - // aapt resource value: 0x7f040001 - public const int ApplicationName = 2130968577; + // aapt resource value: 0x7F040000 + public const int ApplicationName = 2130968576; - // aapt resource value: 0x7f040000 - public const int Hello = 2130968576; + // aapt resource value: 0x7F040001 + public const int Hello = 2130968577; static String() { diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index d7c5bd4415..0c8fd9465c 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -109,7 +109,8 @@ namespace ControlCatalog.NetCore .With(new X11PlatformOptions { EnableMultiTouch = true, - UseDBusMenu = true + UseDBusMenu = true, + EnableIme = true, }) .With(new Win32PlatformOptions { diff --git a/samples/ControlCatalog/Assets/Fonts/WenQuanYiMicroHei-01.ttf b/samples/ControlCatalog/Assets/Fonts/WenQuanYiMicroHei-01.ttf new file mode 100644 index 0000000000..61e2583a6c Binary files /dev/null and b/samples/ControlCatalog/Assets/Fonts/WenQuanYiMicroHei-01.ttf differ diff --git a/samples/ControlCatalog/Pages/TextBoxPage.xaml b/samples/ControlCatalog/Pages/TextBoxPage.xaml index 4958174f40..2030c6e744 100644 --- a/samples/ControlCatalog/Pages/TextBoxPage.xaml +++ b/samples/ControlCatalog/Pages/TextBoxPage.xaml @@ -64,5 +64,8 @@ + diff --git a/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs b/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs index 7802f336fb..d1a116345b 100644 --- a/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs +++ b/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs @@ -1,7 +1,11 @@ +using System; using System.Threading.Tasks; + using Android.Content; using Android.Runtime; using Android.Views; + +using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Platform; diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 71dce93ce7..360e76b2dc 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -6,6 +6,7 @@ using Android.Views; using Avalonia.Android.Platform.Input; using Avalonia.Android.Platform.Specific; using Avalonia.Android.Platform.Specific.Helpers; +using Avalonia.Controls; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Input; using Avalonia.Input.Raw; @@ -196,7 +197,17 @@ namespace Avalonia.Android.Platform.SkiaPlatform public IPopupImpl CreatePopup() => null; public Action LostFocus { get; set; } + public Action TransparencyLevelChanged { get; set; } - ILockedFramebuffer IFramebufferPlatformSurface.Lock()=>new AndroidFramebuffer(_view.Holder.Surface); + public WindowTransparencyLevel TransparencyLevel => WindowTransparencyLevel.None; + + public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1); + + ILockedFramebuffer IFramebufferPlatformSurface.Lock() => new AndroidFramebuffer(_view.Holder.Surface); + + public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) + { + throw new NotImplementedException(); + } } } diff --git a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs index 1179ce9235..426b221738 100644 --- a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs +++ b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs @@ -5,14 +5,14 @@ using Android.Runtime; using Android.Views; using Android.Views.InputMethods; using Avalonia.Android.Platform.Input; +using Avalonia.Android.Platform.SkiaPlatform; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Input.Raw; -using Avalonia.Platform; namespace Avalonia.Android.Platform.Specific.Helpers { - public class AndroidKeyboardEventsHelper : IDisposable where TView :ITopLevelImpl, IAndroidView + internal class AndroidKeyboardEventsHelper : IDisposable where TView : TopLevelImpl, IAndroidView { private TView _view; private IInputElement _lastFocusedElement; @@ -46,9 +46,11 @@ namespace Avalonia.Android.Platform.Specific.Helpers var rawKeyEvent = new RawKeyEventArgs( AndroidKeyboardDevice.Instance, - Convert.ToUInt32(e.EventTime), + Convert.ToUInt64(e.EventTime), + _view.InputRoot, e.Action == KeyEventActions.Down ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp, - AndroidKeyboardDevice.ConvertKey(e.KeyCode), GetModifierKeys(e)); + AndroidKeyboardDevice.ConvertKey(e.KeyCode), GetModifierKeys(e)); + _view.Input(rawKeyEvent); if (e.Action == KeyEventActions.Down && e.UnicodeChar >= 32) @@ -56,6 +58,7 @@ namespace Avalonia.Android.Platform.Specific.Helpers var rawTextEvent = new RawTextInputEventArgs( AndroidKeyboardDevice.Instance, Convert.ToUInt32(e.EventTime), + _view.InputRoot, Convert.ToChar(e.UnicodeChar).ToString() ); _view.Input(rawTextEvent); diff --git a/src/Android/Avalonia.Android/Resources/Resource.Designer.cs b/src/Android/Avalonia.Android/Resources/Resource.Designer.cs deleted file mode 100644 index 80cbbc51ec..0000000000 --- a/src/Android/Avalonia.Android/Resources/Resource.Designer.cs +++ /dev/null @@ -1,60 +0,0 @@ -#pragma warning disable 1591 -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -[assembly: global::Android.Runtime.ResourceDesignerAttribute("Avalonia.Android.Resource", IsApplication=false)] - -namespace Avalonia.Android -{ - - - [System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")] - public partial class Resource - { - - static Resource() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - public partial class Attribute - { - - static Attribute() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - private Attribute() - { - } - } - - public partial class String - { - - // aapt resource value: 0x7f020001 - public static int ApplicationName = 2130837505; - - // aapt resource value: 0x7f020000 - public static int Hello = 2130837504; - - static String() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - private String() - { - } - } - } -} -#pragma warning restore 1591 diff --git a/src/Android/Avalonia.Android/SystemDialogImpl.cs b/src/Android/Avalonia.Android/SystemDialogImpl.cs index a8d201d66e..1ed1f688b1 100644 --- a/src/Android/Avalonia.Android/SystemDialogImpl.cs +++ b/src/Android/Avalonia.Android/SystemDialogImpl.cs @@ -2,18 +2,17 @@ using System; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Platform; -using Avalonia.Platform; namespace Avalonia.Android { internal class SystemDialogImpl : ISystemDialogImpl { - public Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) + public Task ShowFileDialogAsync(FileDialog dialog, Window parent) { throw new NotImplementedException(); } - public Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + public Task ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) { throw new NotImplementedException(); } diff --git a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj index f880e48282..4f49f3a863 100644 --- a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj +++ b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj @@ -16,7 +16,7 @@ Resources\Resource.Designer.cs Off False - v9.0 + v10.0 Properties\AndroidManifest.xml @@ -150,4 +150,4 @@ - + \ No newline at end of file diff --git a/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs b/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs index ad2cec2ae3..121acb6351 100644 --- a/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs +++ b/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs @@ -4,7 +4,6 @@ using Android.Content.PM; using Android.OS; using Avalonia.Android; using Avalonia.Controls; -using Avalonia.Controls.Templates; using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.Styling; @@ -38,8 +37,7 @@ namespace Avalonia.AndroidTestApplication { Styles.Add(new DefaultTheme()); - var loader = new AvaloniaXamlLoader(); - var baseLight = (IStyle)loader.Load( + var baseLight = (IStyle)AvaloniaXamlLoader.Load( new Uri("resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default")); Styles.Add(baseLight); diff --git a/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml b/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml index 4792c8a1ec..e8e81da9de 100644 --- a/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml +++ b/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml @@ -1,6 +1,6 @@  - + \ No newline at end of file diff --git a/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs b/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs index e171dd6162..83db67fcee 100644 --- a/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs +++ b/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs @@ -2,7 +2,6 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -15,7 +14,7 @@ namespace Avalonia.AndroidTestApplication { - [System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")] public partial class Resource { @@ -26,8 +25,6 @@ namespace Avalonia.AndroidTestApplication public static void UpdateIdValues() { - global::Avalonia.Android.Resource.String.ApplicationName = global::Avalonia.AndroidTestApplication.Resource.String.ApplicationName; - global::Avalonia.Android.Resource.String.Hello = global::Avalonia.AndroidTestApplication.Resource.String.Hello; } public partial class Attribute @@ -46,8 +43,8 @@ namespace Avalonia.AndroidTestApplication public partial class Drawable { - // aapt resource value: 0x7f020000 - public const int Icon = 2130837504; + // aapt resource value: 0x7F010000 + public const int Icon = 2130771968; static Drawable() { @@ -62,11 +59,11 @@ namespace Avalonia.AndroidTestApplication public partial class String { - // aapt resource value: 0x7f030001 - public const int ApplicationName = 2130903041; + // aapt resource value: 0x7F020000 + public const int ApplicationName = 2130837504; - // aapt resource value: 0x7f030000 - public const int Hello = 2130903040; + // aapt resource value: 0x7F020001 + public const int Hello = 2130837505; static String() { diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs index 76e5427fa4..0d19f4c479 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs @@ -3,14 +3,15 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. -using Avalonia.Media; using System; using System.Diagnostics; +using Avalonia.Layout; +using Avalonia.Media; namespace Avalonia.Controls.Primitives { /// - /// Used within the template of a to specify the + /// Used within the template of a to specify the /// location in the control's visual tree where the rows are to be added. /// public sealed class DataGridRowsPresenter : Panel @@ -22,25 +23,10 @@ namespace Avalonia.Controls.Primitives } private double _measureHeightOffset = 0; - private double _effectiveViewPortHeight = 0; - - public DataGridRowsPresenter() - { - EffectiveViewportChanged += OnEffectiveViewportChanged; - } - - private void OnEffectiveViewportChanged(object sender, Layout.EffectiveViewportChangedEventArgs e) - { - if (_effectiveViewPortHeight != e.EffectiveViewport.Height) - { - _effectiveViewPortHeight = e.EffectiveViewport.Height; - InvalidateMeasure(); - } - } private double CalculateEstimatedAvailableHeight(Size availableSize) { - if(!Double.IsPositiveInfinity(availableSize.Height)) + if (!Double.IsPositiveInfinity(availableSize.Height)) { return availableSize.Height + _measureHeightOffset; } @@ -66,10 +52,10 @@ namespace Avalonia.Controls.Primitives return base.ArrangeOverride(finalSize); } - if(OwningGrid.RowsPresenterAvailableSize.HasValue) + if (OwningGrid.RowsPresenterAvailableSize.HasValue) { var availableHeight = OwningGrid.RowsPresenterAvailableSize.Value.Height; - if(!Double.IsPositiveInfinity(availableHeight)) + if (!Double.IsPositiveInfinity(availableHeight)) { _measureHeightOffset = finalSize.Height - availableHeight; OwningGrid.RowsPresenterEstimatedAvailableHeight = finalSize.Height; @@ -126,7 +112,14 @@ namespace Avalonia.Controls.Primitives { if (double.IsInfinity(availableSize.Height)) { - availableSize = availableSize.WithHeight(_effectiveViewPortHeight); + if (VisualRoot is TopLevel topLevel) + { + double maxHeight = topLevel.IsArrangeValid ? + topLevel.Bounds.Height : + LayoutHelper.ApplyLayoutConstraints(topLevel, availableSize).Height; + + availableSize = availableSize.WithHeight(maxHeight); + } } if (availableSize.Height == 0 || OwningGrid == null) diff --git a/src/Avalonia.Controls/Platform/ITopLevelImplWithTextInputMethod.cs b/src/Avalonia.Controls/Platform/ITopLevelImplWithTextInputMethod.cs new file mode 100644 index 0000000000..9c29415a6a --- /dev/null +++ b/src/Avalonia.Controls/Platform/ITopLevelImplWithTextInputMethod.cs @@ -0,0 +1,11 @@ +using Avalonia.Input; +using Avalonia.Input.TextInput; +using Avalonia.Platform; + +namespace Avalonia.Controls.Platform +{ + public interface ITopLevelImplWithTextInputMethod : ITopLevelImpl + { + public ITextInputMethodImpl TextInputMethod { get; } + } +} diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 078d8050bf..6bbb1c13bf 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Linq; +using Avalonia.Input.TextInput; using Avalonia.Media; using Avalonia.Metadata; using Avalonia.Threading; @@ -378,19 +379,23 @@ namespace Avalonia.Controls.Presenters if (_caretBlink) { - var charPos = FormattedText.HitTestTextPosition(CaretIndex); - var x = Math.Floor(charPos.X) + 0.5; - var y = Math.Floor(charPos.Y) + 0.5; - var b = Math.Ceiling(charPos.Bottom) - 0.5; - + var (p1, p2) = GetCaretPoints(); context.DrawLine( new Pen(caretBrush, 1), - new Point(x, y), - new Point(x, b)); + p1, p2); } } } + (Point, Point) GetCaretPoints() + { + var charPos = FormattedText.HitTestTextPosition(CaretIndex); + var x = Math.Floor(charPos.X) + 0.5; + var y = Math.Floor(charPos.Y) + 0.5; + var b = Math.Ceiling(charPos.Bottom) - 0.5; + return (new Point(x, y), new Point(x, b)); + } + public void ShowCaret() { _caretBlink = true; @@ -538,5 +543,11 @@ namespace Avalonia.Controls.Presenters _caretBlink = !_caretBlink; InvalidateVisual(); } + + internal Rect GetCursorRectangle() + { + var (p1, p2) = GetCaretPoints(); + return new Rect(p1, p2); + } } } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 28426ee70f..90064fad57 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -149,6 +149,7 @@ namespace Avalonia.Controls private int _selectionStart; private int _selectionEnd; private TextPresenter _presenter; + private TextBoxTextInputMethodClient _imClient = new TextBoxTextInputMethodClient(); private UndoRedoHelper _undoRedoHelper; private bool _isUndoingRedoing; private bool _ignoreTextChanges; @@ -161,6 +162,10 @@ namespace Avalonia.Controls static TextBox() { FocusableProperty.OverrideDefaultValue(typeof(TextBox), true); + TextInputMethodClientRequestedEvent.AddClassHandler((tb, e) => + { + e.Client = tb._imClient; + }); } public TextBox() @@ -437,7 +442,7 @@ namespace Avalonia.Controls protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { _presenter = e.NameScope.Get("PART_TextPresenter"); - + _imClient.SetPresenter(_presenter); if (IsFocused) { _presenter?.ShowCaret(); diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs new file mode 100644 index 0000000000..e8122dd311 --- /dev/null +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -0,0 +1,41 @@ +using System; +using Avalonia.Controls.Presenters; +using Avalonia.Input.TextInput; +using Avalonia.VisualTree; + +namespace Avalonia.Controls +{ + internal class TextBoxTextInputMethodClient : ITextInputMethodClient + { + private TextPresenter _presenter; + private IDisposable _subscription; + public Rect CursorRectangle => _presenter?.GetCursorRectangle() ?? default; + public event EventHandler CursorRectangleChanged; + public IVisual TextViewVisual => _presenter; + public event EventHandler TextViewVisualChanged; + public bool SupportsPreedit => false; + public void SetPreeditText(string text) => throw new NotSupportedException(); + + public bool SupportsSurroundingText => false; + public TextInputMethodSurroundingText SurroundingText => throw new NotSupportedException(); + public event EventHandler SurroundingTextChanged; + public string TextBeforeCursor => null; + public string TextAfterCursor => null; + + private void OnCaretIndexChanged(int index) => CursorRectangleChanged?.Invoke(this, EventArgs.Empty); + + public void SetPresenter(TextPresenter presenter) + { + _subscription?.Dispose(); + _subscription = null; + _presenter = presenter; + if (_presenter != null) + { + _subscription = _presenter.GetObservable(TextPresenter.CaretIndexProperty) + .Subscribe(OnCaretIndexChanged); + } + TextViewVisualChanged?.Invoke(this, EventArgs.Empty); + CursorRectangleChanged?.Invoke(this, EventArgs.Empty); + } + } +} diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 3d24f60463..4e43ce13b7 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -1,8 +1,10 @@ using System; using System.Reactive.Linq; +using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; using Avalonia.Layout; using Avalonia.Logging; using Avalonia.LogicalTree; @@ -31,6 +33,7 @@ namespace Avalonia.Controls ICloseable, IStyleHost, ILogicalRoot, + ITextInputMethodRoot, IWeakSubscriber { /// @@ -489,5 +492,8 @@ namespace Avalonia.Controls if (focused == this) KeyboardDevice.Instance.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None); } + + ITextInputMethodImpl ITextInputMethodRoot.InputMethod => + (PlatformImpl as ITopLevelImplWithTextInputMethod)?.TextInputMethod; } } diff --git a/src/Avalonia.FreeDesktop/DBusCallQueue.cs b/src/Avalonia.FreeDesktop/DBusCallQueue.cs new file mode 100644 index 0000000000..5cd748be02 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusCallQueue.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Avalonia.FreeDesktop +{ + class DBusCallQueue + { + private readonly Func _errorHandler; + + class Item + { + public Func Callback; + public Action OnFinish; + } + private Queue _q = new Queue(); + private bool _processing; + + public DBusCallQueue(Func errorHandler) + { + _errorHandler = errorHandler; + } + + public void Enqueue(Func cb) + { + _q.Enqueue(new Item + { + Callback = cb + }); + Process(); + } + + public Task EnqueueAsync(Func cb) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + _q.Enqueue(new Item + { + Callback = cb, + OnFinish = e => + { + if (e == null) + tcs.TrySetResult(0); + else + tcs.TrySetException(e); + } + }); + Process(); + return tcs.Task; + } + + public Task EnqueueAsync(Func> cb) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + _q.Enqueue(new Item + { + Callback = async () => + { + var res = await cb(); + tcs.TrySetResult(res); + }, + OnFinish = e => + { + if (e != null) + tcs.TrySetException(e); + } + }); + Process(); + return tcs.Task; + } + + async void Process() + { + if(_processing) + return; + _processing = true; + try + { + while (_q.Count > 0) + { + var item = _q.Dequeue(); + try + { + await item.Callback(); + item.OnFinish?.Invoke(null); + } + catch(Exception e) + { + if (item.OnFinish != null) + item.OnFinish(e); + else + await _errorHandler(e); + } + } + } + finally + { + _processing = false; + } + } + + public void FailAll() + { + while (_q.Count>0) + { + var item = _q.Dequeue(); + item.OnFinish?.Invoke(new OperationCanceledException()); + } + } + } +} diff --git a/src/Avalonia.FreeDesktop/DBusHelper.cs b/src/Avalonia.FreeDesktop/DBusHelper.cs index b445f86613..7996a94dd0 100644 --- a/src/Avalonia.FreeDesktop/DBusHelper.cs +++ b/src/Avalonia.FreeDesktop/DBusHelper.cs @@ -1,5 +1,6 @@ using System; using System.Threading; +using Avalonia.Logging; using Avalonia.Threading; using Tmds.DBus; @@ -48,8 +49,10 @@ namespace Avalonia.FreeDesktop } public static Connection Connection { get; private set; } - public static Exception TryInitialize(string dbusAddress = null) + public static Connection TryInitialize(string dbusAddress = null) { + if (Connection != null) + return Connection; var oldContext = SynchronizationContext.Current; try { @@ -70,13 +73,15 @@ namespace Avalonia.FreeDesktop } catch (Exception e) { - return e; + Logger.TryGet(LogEventLevel.Error, "DBUS") + ?.Log(null, "Unable to connect to DBus: " + e); } finally { SynchronizationContext.SetSynchronizationContext(oldContext); } - return null; + + return Connection; } } } diff --git a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs new file mode 100644 index 0000000000..a7e83140ae --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs @@ -0,0 +1,288 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using System.Threading.Tasks; +using Avalonia.FreeDesktop.DBusIme.Fcitx; +using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; +using Avalonia.Logging; +using Tmds.DBus; + +namespace Avalonia.FreeDesktop.DBusIme +{ + internal class DBusInputMethodFactory : IX11InputMethodFactory where T : ITextInputMethodImpl, IX11InputMethodControl + { + private readonly Func _factory; + + public DBusInputMethodFactory(Func factory) + { + _factory = factory; + } + + public (ITextInputMethodImpl method, IX11InputMethodControl control) CreateClient(IntPtr xid) + { + var im = _factory(xid); + return (im, im); + } + } + + internal abstract class DBusTextInputMethodBase : IX11InputMethodControl, ITextInputMethodImpl + { + private List _disposables = new List(); + private Queue _onlineNamesQueue = new Queue(); + protected Connection Connection { get; } + private readonly string[] _knownNames; + private bool _connecting; + private string _currentName; + private DBusCallQueue _queue; + private bool _controlActive, _windowActive; + private bool? _imeActive; + private Rect _logicalRect; + private PixelRect? _lastReportedRect; + private double _scaling = 1; + private PixelPoint _windowPosition; + + protected bool IsConnected => _currentName != null; + + public DBusTextInputMethodBase(Connection connection, params string[] knownNames) + { + _queue = new DBusCallQueue(QueueOnError); + Connection = connection; + _knownNames = knownNames; + Watch(); + } + + async void Watch() + { + foreach (var name in _knownNames) + _disposables.Add(await Connection.ResolveServiceOwnerAsync(name, OnNameChange)); + } + + protected abstract Task Connect(string name); + + protected string GetAppName() => + Application.Current.Name ?? Assembly.GetEntryAssembly()?.GetName()?.Name ?? "Avalonia"; + + private async void OnNameChange(ServiceOwnerChangedEventArgs args) + { + if (args.NewOwner != null && _currentName == null) + { + _onlineNamesQueue.Enqueue(args.ServiceName); + if(!_connecting) + { + _connecting = true; + try + { + while (_onlineNamesQueue.Count > 0) + { + var name = _onlineNamesQueue.Dequeue(); + try + { + if (await Connect(name)) + { + _onlineNamesQueue.Clear(); + _currentName = name; + return; + } + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, "IME") + ?.Log(this, "Unable to create IME input context:\n" + e); + } + } + } + finally + { + _connecting = false; + } + } + + } + + // IME has crashed + if (args.NewOwner == null && args.ServiceName == _currentName) + { + _currentName = null; + foreach(var s in _disposables) + s.Dispose(); + _disposables.Clear(); + + OnDisconnected(); + Reset(); + + // Watch again + Watch(); + } + } + + protected virtual Task Disconnect() + { + return Task.CompletedTask; + } + + protected virtual void OnDisconnected() + { + + } + + protected virtual void Reset() + { + _lastReportedRect = null; + _imeActive = null; + } + + async Task QueueOnError(Exception e) + { + Logger.TryGet(LogEventLevel.Error, "IME") + ?.Log(this, "Error:\n" + e); + try + { + await Disconnect(); + } + catch (Exception ex) + { + Logger.TryGet(LogEventLevel.Error, "IME") + ?.Log(this, "Error while destroying the context:\n" + ex); + } + OnDisconnected(); + _currentName = null; + } + + protected void Enqueue(Func cb) => _queue.Enqueue(cb); + + protected void AddDisposable(IDisposable d) => _disposables.Add(d); + + public void Dispose() + { + foreach(var d in _disposables) + d.Dispose(); + _disposables.Clear(); + try + { + Disconnect().ContinueWith(_ => { }); + } + catch + { + // fire and forget + } + _currentName = null; + } + + protected abstract Task SetCursorRectCore(PixelRect rect); + protected abstract Task SetActiveCore(bool active); + protected abstract Task ResetContextCore(); + protected abstract Task HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode); + + void UpdateActive() + { + _queue.Enqueue(async () => + { + if(!IsConnected) + return; + + var active = _windowActive && _controlActive; + if (active != _imeActive) + { + _imeActive = active; + await SetActiveCore(active); + } + }); + } + + + void IX11InputMethodControl.SetWindowActive(bool active) + { + _windowActive = active; + UpdateActive(); + } + + void ITextInputMethodImpl.SetActive(bool active) + { + _controlActive = active; + UpdateActive(); + } + + bool IX11InputMethodControl.IsEnabled => IsConnected && _imeActive == true; + + async ValueTask IX11InputMethodControl.HandleEventAsync(RawKeyEventArgs args, int keyVal, int keyCode) + { + try + { + return await _queue.EnqueueAsync(async () => await HandleKeyCore(args, keyVal, keyCode)); + } + // Disconnected + catch (OperationCanceledException) + { + return false; + } + // Error, disconnect + catch (Exception e) + { + await QueueOnError(e); + return false; + } + } + + private Action _onCommit; + event Action IX11InputMethodControl.Commit + { + add => _onCommit += value; + remove => _onCommit -= value; + } + + protected void FireCommit(string s) => _onCommit?.Invoke(s); + + private Action _onForward; + event Action IX11InputMethodControl.ForwardKey + { + add => _onForward += value; + remove => _onForward -= value; + } + + protected void FireForward(X11InputMethodForwardedKey k) => _onForward?.Invoke(k); + + void UpdateCursorRect() + { + _queue.Enqueue(async () => + { + if(!IsConnected) + return; + var cursorRect = PixelRect.FromRect(_logicalRect, _scaling); + cursorRect = cursorRect.Translate(_windowPosition); + if (cursorRect != _lastReportedRect) + { + _lastReportedRect = cursorRect; + await SetCursorRectCore(cursorRect); + } + }); + } + + void IX11InputMethodControl.UpdateWindowInfo(PixelPoint position, double scaling) + { + _windowPosition = position; + _scaling = scaling; + UpdateCursorRect(); + } + + void ITextInputMethodImpl.SetCursorRect(Rect rect) + { + _logicalRect = rect; + UpdateCursorRect(); + } + + public abstract void SetOptions(TextInputOptionsQueryEventArgs options); + + void ITextInputMethodImpl.Reset() + { + Reset(); + _queue.Enqueue(async () => + { + if (!IsConnected) + return; + await ResetContextCore(); + }); + } + } +} diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs new file mode 100644 index 0000000000..7ce2339763 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Tmds.DBus; + +[assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)] +namespace Avalonia.FreeDesktop.DBusIme.Fcitx +{ + [DBusInterface("org.fcitx.Fcitx.InputMethod")] + interface IFcitxInputMethod : IDBusObject + { + Task<(int icid, bool enable, uint keyval1, uint state1, uint keyval2, uint state2)> CreateICv3Async( + string Appname, int Pid); + } + + + [DBusInterface("org.fcitx.Fcitx.InputContext")] + interface IFcitxInputContext : IDBusObject + { + Task EnableICAsync(); + Task CloseICAsync(); + Task FocusInAsync(); + Task FocusOutAsync(); + Task ResetAsync(); + Task MouseEventAsync(int X); + Task SetCursorLocationAsync(int X, int Y); + Task SetCursorRectAsync(int X, int Y, int W, int H); + Task SetCapacityAsync(uint Caps); + Task SetSurroundingTextAsync(string Text, uint Cursor, uint Anchor); + Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor); + Task DestroyICAsync(); + Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, int Type, uint Time); + Task WatchEnableIMAsync(Action handler, Action onError = null); + Task WatchCloseIMAsync(Action handler, Action onError = null); + Task WatchCommitStringAsync(Action handler, Action onError = null); + Task WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action onError = null); + Task WatchUpdatePreeditAsync(Action<(string str, int cursorpos)> handler, Action onError = null); + Task WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action onError = null); + Task WatchUpdateClientSideUIAsync(Action<(string auxup, string auxdown, string preedit, string candidateword, string imname, int cursorpos)> handler, Action onError = null); + Task WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler, Action onError = null); + Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action onError = null); + } + + [DBusInterface("org.fcitx.Fcitx.InputContext1")] + interface IFcitxInputContext1 : IDBusObject + { + Task FocusInAsync(); + Task FocusOutAsync(); + Task ResetAsync(); + Task SetCursorRectAsync(int X, int Y, int W, int H); + Task SetCapabilityAsync(ulong Caps); + Task SetSurroundingTextAsync(string Text, uint Cursor, uint Anchor); + Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor); + Task DestroyICAsync(); + Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, bool Type, uint Time); + Task WatchCommitStringAsync(Action handler, Action onError = null); + Task WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action onError = null); + Task WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action onError = null); + Task WatchForwardKeyAsync(Action<(uint keyval, uint state, bool type)> handler, Action onError = null); + Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action onError = null); + } + + [DBusInterface("org.fcitx.Fcitx.InputMethod1")] + interface IFcitxInputMethod1 : IDBusObject + { + Task<(ObjectPath path, byte[] data)> CreateInputContextAsync((string, string)[] arg0); + } +} diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs new file mode 100644 index 0000000000..6510a5877a --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs @@ -0,0 +1,67 @@ +using System; + +namespace Avalonia.FreeDesktop.DBusIme.Fcitx +{ + enum FcitxKeyEventType + { + FCITX_PRESS_KEY, + FCITX_RELEASE_KEY + }; + + [Flags] + enum FcitxCapabilityFlags + { + CAPACITY_NONE = 0, + CAPACITY_CLIENT_SIDE_UI = (1 << 0), + CAPACITY_PREEDIT = (1 << 1), + CAPACITY_CLIENT_SIDE_CONTROL_STATE = (1 << 2), + CAPACITY_PASSWORD = (1 << 3), + CAPACITY_FORMATTED_PREEDIT = (1 << 4), + CAPACITY_CLIENT_UNFOCUS_COMMIT = (1 << 5), + CAPACITY_SURROUNDING_TEXT = (1 << 6), + CAPACITY_EMAIL = (1 << 7), + CAPACITY_DIGIT = (1 << 8), + CAPACITY_UPPERCASE = (1 << 9), + CAPACITY_LOWERCASE = (1 << 10), + CAPACITY_NOAUTOUPPERCASE = (1 << 11), + CAPACITY_URL = (1 << 12), + CAPACITY_DIALABLE = (1 << 13), + CAPACITY_NUMBER = (1 << 14), + CAPACITY_NO_ON_SCREEN_KEYBOARD = (1 << 15), + CAPACITY_SPELLCHECK = (1 << 16), + CAPACITY_NO_SPELLCHECK = (1 << 17), + CAPACITY_WORD_COMPLETION = (1 << 18), + CAPACITY_UPPERCASE_WORDS = (1 << 19), + CAPACITY_UPPERCASE_SENTENCES = (1 << 20), + CAPACITY_ALPHA = (1 << 21), + CAPACITY_NAME = (1 << 22), + CAPACITY_GET_IM_INFO_ON_FOCUS = (1 << 23), + CAPACITY_RELATIVE_CURSOR_RECT = (1 << 24), + }; + + [Flags] + enum FcitxKeyState + { + FcitxKeyState_None = 0, + FcitxKeyState_Shift = 1 << 0, + FcitxKeyState_CapsLock = 1 << 1, + FcitxKeyState_Ctrl = 1 << 2, + FcitxKeyState_Alt = 1 << 3, + FcitxKeyState_Alt_Shift = FcitxKeyState_Alt | FcitxKeyState_Shift, + FcitxKeyState_Ctrl_Shift = FcitxKeyState_Ctrl | FcitxKeyState_Shift, + FcitxKeyState_Ctrl_Alt = FcitxKeyState_Ctrl | FcitxKeyState_Alt, + + FcitxKeyState_Ctrl_Alt_Shift = + FcitxKeyState_Ctrl | FcitxKeyState_Alt | FcitxKeyState_Shift, + FcitxKeyState_NumLock = 1 << 4, + FcitxKeyState_Super = 1 << 6, + FcitxKeyState_ScrollLock = 1 << 7, + FcitxKeyState_MousePressed = 1 << 8, + FcitxKeyState_HandledMask = 1 << 24, + FcitxKeyState_IgnoredMask = 1 << 25, + FcitxKeyState_Super2 = 1 << 26, + FcitxKeyState_Hyper = 1 << 27, + FcitxKeyState_Meta = 1 << 28, + FcitxKeyState_UsedMask = 0x5c001fff + }; +} diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs new file mode 100644 index 0000000000..a03ea213aa --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs @@ -0,0 +1,51 @@ +using System; +using System.Threading.Tasks; + +namespace Avalonia.FreeDesktop.DBusIme.Fcitx +{ + internal class FcitxICWrapper + { + private readonly IFcitxInputContext1 _modern; + private readonly IFcitxInputContext _old; + + public FcitxICWrapper(IFcitxInputContext old) + { + _old = old; + } + + public FcitxICWrapper(IFcitxInputContext1 modern) + { + _modern = modern; + } + + public Task FocusInAsync() => _old?.FocusInAsync() ?? _modern.FocusInAsync(); + + public Task FocusOutAsync() => _old?.FocusOutAsync() ?? _modern.FocusOutAsync(); + + public Task ResetAsync() => _old?.ResetAsync() ?? _modern.ResetAsync(); + + public Task SetCursorRectAsync(int x, int y, int w, int h) => + _old?.SetCursorRectAsync(x, y, w, h) ?? _modern.SetCursorRectAsync(x, y, w, h); + public Task DestroyICAsync() => _old?.DestroyICAsync() ?? _modern.DestroyICAsync(); + + public async Task ProcessKeyEventAsync(uint keyVal, uint keyCode, uint state, int type, uint time) + { + if(_old!=null) + return await _old.ProcessKeyEventAsync(keyVal, keyCode, state, type, time) != 0; + return await _modern.ProcessKeyEventAsync(keyVal, keyCode, state, type > 0, time); + } + + public Task WatchCommitStringAsync(Action handler) => + _old?.WatchCommitStringAsync(handler) ?? _modern.WatchCommitStringAsync(handler); + + public Task WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler) + { + return _old?.WatchForwardKeyAsync(handler) + ?? _modern.WatchForwardKeyAsync(ev => + handler((ev.keyval, ev.state, ev.type ? 1 : 0))); + } + + public Task SetCapacityAsync(uint flags) => + _old?.SetCapacityAsync(flags) ?? _modern.SetCapabilityAsync(flags); + } +} diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs new file mode 100644 index 0000000000..8239b3f35d --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs @@ -0,0 +1,149 @@ +using System; +using System.Diagnostics; +using System.Reactive.Concurrency; +using System.Reflection; +using System.Threading.Tasks; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; +using Tmds.DBus; + +namespace Avalonia.FreeDesktop.DBusIme.Fcitx +{ + internal class FcitxX11TextInputMethod : DBusTextInputMethodBase + { + private FcitxICWrapper _context; + private FcitxCapabilityFlags? _lastReportedFlags; + + public FcitxX11TextInputMethod(Connection connection) : base(connection, + "org.fcitx.Fcitx", + "org.freedesktop.portal.Fcitx" + ) + { + + } + + protected override async Task Connect(string name) + { + if (name == "org.fcitx.Fcitx") + { + var method = Connection.CreateProxy(name, "/inputmethod"); + var resp = await method.CreateICv3Async(GetAppName(), + Process.GetCurrentProcess().Id); + + var proxy = Connection.CreateProxy(name, + "/inputcontext_" + resp.icid); + + _context = new FcitxICWrapper(proxy); + } + else + { + var method = Connection.CreateProxy(name, "/inputmethod"); + var resp = await method.CreateInputContextAsync(new[] { ("appName", GetAppName()) }); + var proxy = Connection.CreateProxy(name, resp.path); + _context = new FcitxICWrapper(proxy); + } + + AddDisposable(await _context.WatchCommitStringAsync(OnCommitString)); + AddDisposable(await _context.WatchForwardKeyAsync(OnForward)); + return true; + } + + protected override Task Disconnect() => _context.DestroyICAsync(); + + protected override void OnDisconnected() => _context = null; + + protected override void Reset() + { + _lastReportedFlags = null; + base.Reset(); + } + + protected override Task SetCursorRectCore(PixelRect cursorRect) => + _context.SetCursorRectAsync(cursorRect.X, cursorRect.Y, Math.Max(1, cursorRect.Width), + Math.Max(1, cursorRect.Height)); + + protected override Task SetActiveCore(bool active) + { + if (active) + return _context.FocusInAsync(); + else + return _context.FocusOutAsync(); + } + + protected override Task ResetContextCore() => _context.ResetAsync(); + + protected override async Task HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode) + { + FcitxKeyState state = default; + if (args.Modifiers.HasFlagCustom(RawInputModifiers.Control)) + state |= FcitxKeyState.FcitxKeyState_Ctrl; + if (args.Modifiers.HasFlagCustom(RawInputModifiers.Alt)) + state |= FcitxKeyState.FcitxKeyState_Alt; + if (args.Modifiers.HasFlagCustom(RawInputModifiers.Shift)) + state |= FcitxKeyState.FcitxKeyState_Shift; + if (args.Modifiers.HasFlagCustom(RawInputModifiers.Meta)) + state |= FcitxKeyState.FcitxKeyState_Super; + + var type = args.Type == RawKeyEventType.KeyDown ? + FcitxKeyEventType.FCITX_PRESS_KEY : + FcitxKeyEventType.FCITX_RELEASE_KEY; + + return await _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state, (int)type, + (uint)args.Timestamp).ConfigureAwait(false); + } + + public override void SetOptions(TextInputOptionsQueryEventArgs options) => + Enqueue(async () => + { + 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.Phone) + flags |= FcitxCapabilityFlags.CAPACITY_DIALABLE; + else if (options.ContentType == TextInputContentType.Url) + flags |= FcitxCapabilityFlags.CAPACITY_URL; + if (flags != _lastReportedFlags) + { + _lastReportedFlags = flags; + await _context.SetCapacityAsync((uint)flags); + } + }); + + private void OnForward((uint keyval, uint state, int type) ev) + { + var state = (FcitxKeyState)ev.state; + KeyModifiers mods = default; + if (state.HasFlagCustom(FcitxKeyState.FcitxKeyState_Ctrl)) + mods |= KeyModifiers.Control; + if (state.HasFlagCustom(FcitxKeyState.FcitxKeyState_Alt)) + mods |= KeyModifiers.Alt; + if (state.HasFlagCustom(FcitxKeyState.FcitxKeyState_Shift)) + mods |= KeyModifiers.Shift; + if (state.HasFlagCustom(FcitxKeyState.FcitxKeyState_Super)) + mods |= KeyModifiers.Meta; + FireForward(new X11InputMethodForwardedKey + { + Modifiers = mods, + KeyVal = (int)ev.keyval, + Type = ev.type == (int)FcitxKeyEventType.FCITX_PRESS_KEY ? + RawKeyEventType.KeyDown : + RawKeyEventType.KeyUp + }); + } + + private void OnCommitString(string s) => FireCommit(s); + } +} diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs new file mode 100644 index 0000000000..26c0d249f3 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Tmds.DBus; + +[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] +namespace Avalonia.FreeDesktop.DBusIme.IBus +{ + [DBusInterface("org.freedesktop.IBus.InputContext")] + interface IIBusInputContext : IDBusObject + { + Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State); + Task SetCursorLocationAsync(int X, int Y, int W, int H); + Task FocusInAsync(); + Task FocusOutAsync(); + Task ResetAsync(); + Task SetCapabilitiesAsync(uint Caps); + Task PropertyActivateAsync(string Name, int State); + Task SetEngineAsync(string Name); + Task GetEngineAsync(); + Task DestroyAsync(); + Task SetSurroundingTextAsync(object Text, uint CursorPos, uint AnchorPos); + Task WatchCommitTextAsync(Action cb, Action onError = null); + Task WatchForwardKeyEventAsync(Action<(uint keyval, uint keycode, uint state)> handler, Action onError = null); + Task WatchRequireSurroundingTextAsync(Action handler, Action onError = null); + Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchars)> handler, Action onError = null); + Task WatchUpdatePreeditTextAsync(Action<(object text, uint cursorPos, bool visible)> handler, Action onError = null); + Task WatchShowPreeditTextAsync(Action handler, Action onError = null); + Task WatchHidePreeditTextAsync(Action handler, Action onError = null); + Task WatchUpdateAuxiliaryTextAsync(Action<(object text, bool visible)> handler, Action onError = null); + Task WatchShowAuxiliaryTextAsync(Action handler, Action onError = null); + Task WatchHideAuxiliaryTextAsync(Action handler, Action onError = null); + Task WatchUpdateLookupTableAsync(Action<(object table, bool visible)> handler, Action onError = null); + Task WatchShowLookupTableAsync(Action handler, Action onError = null); + Task WatchHideLookupTableAsync(Action handler, Action onError = null); + Task WatchPageUpLookupTableAsync(Action handler, Action onError = null); + Task WatchPageDownLookupTableAsync(Action handler, Action onError = null); + Task WatchCursorUpLookupTableAsync(Action handler, Action onError = null); + Task WatchCursorDownLookupTableAsync(Action handler, Action onError = null); + Task WatchRegisterPropertiesAsync(Action handler, Action onError = null); + Task WatchUpdatePropertyAsync(Action handler, Action onError = null); + } + + + [DBusInterface("org.freedesktop.IBus.Portal")] + interface IIBusPortal : IDBusObject + { + Task CreateInputContextAsync(string Name); + } +} diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs new file mode 100644 index 0000000000..3070f51a8e --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs @@ -0,0 +1,45 @@ +using System; + +namespace Avalonia.FreeDesktop.DBusIme.IBus +{ + [Flags] + internal enum IBusModifierMask + { + ShiftMask = 1 << 0, + LockMask = 1 << 1, + ControlMask = 1 << 2, + Mod1Mask = 1 << 3, + Mod2Mask = 1 << 4, + Mod3Mask = 1 << 5, + Mod4Mask = 1 << 6, + Mod5Mask = 1 << 7, + Button1Mask = 1 << 8, + Button2Mask = 1 << 9, + Button3Mask = 1 << 10, + Button4Mask = 1 << 11, + Button5Mask = 1 << 12, + + HandledMask = 1 << 24, + ForwardMask = 1 << 25, + IgnoredMask = ForwardMask, + + SuperMask = 1 << 26, + HyperMask = 1 << 27, + MetaMask = 1 << 28, + + ReleaseMask = 1 << 30, + + ModifierMask = 0x5c001fff + } + + [Flags] + internal enum IBusCapability + { + CapPreeditText = 1 << 0, + CapAuxiliaryText = 1 << 1, + CapLookupTable = 1 << 2, + CapFocus = 1 << 3, + CapProperty = 1 << 4, + CapSurroundingText = 1 << 5, + } +} diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs new file mode 100644 index 0000000000..74f54267d0 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs @@ -0,0 +1,105 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; +using Tmds.DBus; + +namespace Avalonia.FreeDesktop.DBusIme.IBus +{ + internal class IBusX11TextInputMethod : DBusTextInputMethodBase + { + private IIBusInputContext _context; + + public IBusX11TextInputMethod(Connection connection) : base(connection, + "org.freedesktop.portal.IBus") + { + } + + protected override async Task Connect(string name) + { + var path = + await Connection.CreateProxy(name, "/org/freedesktop/IBus") + .CreateInputContextAsync(GetAppName()); + + _context = Connection.CreateProxy(name, path); + AddDisposable(await _context.WatchCommitTextAsync(OnCommitText)); + AddDisposable(await _context.WatchForwardKeyEventAsync(OnForwardKey)); + Enqueue(() => _context.SetCapabilitiesAsync((uint)IBusCapability.CapFocus)); + return true; + } + + private void OnForwardKey((uint keyval, uint keycode, uint state) k) + { + var state = (IBusModifierMask)k.state; + KeyModifiers mods = default; + if (state.HasFlagCustom(IBusModifierMask.ControlMask)) + mods |= KeyModifiers.Control; + if (state.HasFlagCustom(IBusModifierMask.Mod1Mask)) + mods |= KeyModifiers.Alt; + if (state.HasFlagCustom(IBusModifierMask.ShiftMask)) + mods |= KeyModifiers.Shift; + if (state.HasFlagCustom(IBusModifierMask.Mod4Mask)) + mods |= KeyModifiers.Meta; + FireForward(new X11InputMethodForwardedKey + { + KeyVal = (int)k.keyval, + Type = state.HasFlagCustom(IBusModifierMask.ReleaseMask) ? RawKeyEventType.KeyUp : RawKeyEventType.KeyDown, + Modifiers = mods + }); + } + + + private void OnCommitText(object wtf) + { + // Hello darkness, my old friend + var prop = wtf.GetType().GetField("Item3"); + if (prop != null) + { + var text = (string)prop.GetValue(wtf); + if (!string.IsNullOrEmpty(text)) + FireCommit(text); + } + } + + protected override Task Disconnect() => _context.DestroyAsync(); + + protected override void OnDisconnected() + { + _context = null; + base.OnDisconnected(); + } + + protected override Task SetCursorRectCore(PixelRect rect) + => _context.SetCursorLocationAsync(rect.X, rect.Y, rect.Width, rect.Height); + + protected override Task SetActiveCore(bool active) + => active ? _context.FocusInAsync() : _context.FocusOutAsync(); + + protected override Task ResetContextCore() + => _context.ResetAsync(); + + protected override Task HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode) + { + IBusModifierMask state = default; + if (args.Modifiers.HasFlagCustom(RawInputModifiers.Control)) + state |= IBusModifierMask.ControlMask; + if (args.Modifiers.HasFlagCustom(RawInputModifiers.Alt)) + state |= IBusModifierMask.Mod1Mask; + if (args.Modifiers.HasFlagCustom(RawInputModifiers.Shift)) + state |= IBusModifierMask.ShiftMask; + if (args.Modifiers.HasFlagCustom(RawInputModifiers.Meta)) + state |= IBusModifierMask.Mod4Mask; + + if (args.Type == RawKeyEventType.KeyUp) + state |= IBusModifierMask.ReleaseMask; + + return _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state); + } + + public override void SetOptions(TextInputOptionsQueryEventArgs options) + { + // No-op, because ibus + } + } +} diff --git a/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs b/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs new file mode 100644 index 0000000000..7f71ecf0ff --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using Avalonia.FreeDesktop.DBusIme.Fcitx; +using Avalonia.FreeDesktop.DBusIme.IBus; +using Tmds.DBus; + +namespace Avalonia.FreeDesktop.DBusIme +{ + public class X11DBusImeHelper + { + private static readonly Dictionary> KnownMethods = + new Dictionary> + { + ["fcitx"] = conn => + new DBusInputMethodFactory(_ => new FcitxX11TextInputMethod(conn)), + ["ibus"] = conn => + new DBusInputMethodFactory(_ => new IBusX11TextInputMethod(conn)) + }; + + static Func DetectInputMethod() + { + foreach (var name in new[] { "AVALONIA_IM_MODULE", "GTK_IM_MODULE", "QT_IM_MODULE" }) + { + var value = Environment.GetEnvironmentVariable(name); + + if (value == "none") + return null; + + if (value != null && KnownMethods.TryGetValue(value, out var factory)) + return factory; + } + + return null; + } + + public static bool DetectAndRegister() + { + var factory = DetectInputMethod(); + if (factory != null) + { + var conn = DBusHelper.TryInitialize(); + if (conn != null) + { + AvaloniaLocator.CurrentMutable.Bind().ToConstant(factory(conn)); + return true; + } + } + + return false; + + } + } +} diff --git a/src/Avalonia.FreeDesktop/IX11InputMethod.cs b/src/Avalonia.FreeDesktop/IX11InputMethod.cs new file mode 100644 index 0000000000..5d91118978 --- /dev/null +++ b/src/Avalonia.FreeDesktop/IX11InputMethod.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; + +namespace Avalonia.FreeDesktop +{ + public interface IX11InputMethodFactory + { + (ITextInputMethodImpl method, IX11InputMethodControl control) CreateClient(IntPtr xid); + } + + public struct X11InputMethodForwardedKey + { + public int KeyVal { get; set; } + public KeyModifiers Modifiers { get; set; } + public RawKeyEventType Type { get; set; } + } + + public interface IX11InputMethodControl : IDisposable + { + void SetWindowActive(bool active); + bool IsEnabled { get; } + ValueTask HandleEventAsync(RawKeyEventArgs args, int keyVal, int keyCode); + event Action Commit; + event Action ForwardKey; + + void UpdateWindowInfo(PixelPoint position, double scaling); + } +} diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 66fb9cfb1c..f3996cea76 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -5,6 +5,7 @@ using Avalonia.Controls; using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Input.GestureRecognizers; +using Avalonia.Input.TextInput; using Avalonia.Interactivity; using Avalonia.VisualTree; @@ -103,6 +104,22 @@ namespace Avalonia.Input RoutedEvent.Register( "TextInput", RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent TextInputMethodClientRequestedEvent = + RoutedEvent.Register( + "TextInputMethodClientRequested", + RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent TextInputOptionsQueryEvent = + RoutedEvent.Register( + "TextInputOptionsQuery", + RoutingStrategies.Tunnel | RoutingStrategies.Bubble); /// /// Defines the event. @@ -243,6 +260,24 @@ namespace Avalonia.Input add { AddHandler(TextInputEvent, value); } remove { RemoveHandler(TextInputEvent, value); } } + + /// + /// Occurs when an input element gains input focus and input method is looking for the corresponding client + /// + public event EventHandler TextInputMethodClientRequested + { + add { AddHandler(TextInputMethodClientRequestedEvent, value); } + remove { RemoveHandler(TextInputMethodClientRequestedEvent, value); } + } + + /// + /// Occurs when an input element gains input focus and input method is asking for required content options + /// + public event EventHandler TextInputOptionsQuery + { + add { AddHandler(TextInputOptionsQueryEvent, value); } + remove { RemoveHandler(TextInputOptionsQueryEvent, value); } + } /// /// Occurs when the pointer enters the control. diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs index 6f4cb7a35c..5899824c29 100644 --- a/src/Avalonia.Input/KeyboardDevice.cs +++ b/src/Avalonia.Input/KeyboardDevice.cs @@ -1,6 +1,7 @@ using System.ComponentModel; using System.Runtime.CompilerServices; using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; using Avalonia.Interactivity; using Avalonia.VisualTree; @@ -18,6 +19,10 @@ namespace Avalonia.Input public IInputManager InputManager => AvaloniaLocator.Current.GetService(); public IFocusManager FocusManager => AvaloniaLocator.Current.GetService(); + + // This should live in the FocusManager, but with the current outdated architecture + // the source of truth about the input focus is in KeyboardDevice + private readonly TextInputMethodManager _textInputManager = new TextInputMethodManager(); public IInputElement? FocusedElement { @@ -40,6 +45,7 @@ namespace Avalonia.Input } RaisePropertyChanged(); + _textInputManager.SetFocusedElement(value); } } diff --git a/src/Avalonia.Input/TextInput/ITextInputMethodClient.cs b/src/Avalonia.Input/TextInput/ITextInputMethodClient.cs new file mode 100644 index 0000000000..d385f5b162 --- /dev/null +++ b/src/Avalonia.Input/TextInput/ITextInputMethodClient.cs @@ -0,0 +1,60 @@ +using System; +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 + /// + IVisual TextViewVisual { get; } + /// + /// Should be fired when text-hosting visual is changed + /// + event EventHandler TextViewVisualChanged; + /// + /// Indicates if TextViewVisual is capable of displaying non-commited input on the cursor position + /// + bool SupportsPreedit { get; } + /// + /// Sets the non-commited input string + /// + void SetPreeditText(string text); + /// + /// 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; + /// + /// Returns the text before the cursor. Must return a non-empty string if cursor is not at the beginning of the text entry + /// + string TextBeforeCursor { get; } + /// + /// Returns the text before the cursor. Must return a non-empty string if cursor is not at the end of the text entry + /// + string TextAfterCursor { get; } + } + + public struct TextInputMethodSurroundingText + { + public string Text { get; set; } + public int CursorOffset { get; set; } + public int AnchorOffset { get; set; } + } +} diff --git a/src/Avalonia.Input/TextInput/ITextInputMethodImpl.cs b/src/Avalonia.Input/TextInput/ITextInputMethodImpl.cs new file mode 100644 index 0000000000..0069314d28 --- /dev/null +++ b/src/Avalonia.Input/TextInput/ITextInputMethodImpl.cs @@ -0,0 +1,15 @@ +namespace Avalonia.Input.TextInput +{ + public interface ITextInputMethodImpl + { + void SetActive(bool active); + void SetCursorRect(Rect rect); + void SetOptions(TextInputOptionsQueryEventArgs options); + void Reset(); + } + + public interface ITextInputMethodRoot : IInputRoot + { + ITextInputMethodImpl InputMethod { get; } + } +} diff --git a/src/Avalonia.Input/TextInput/InputMethodManager.cs b/src/Avalonia.Input/TextInput/InputMethodManager.cs new file mode 100644 index 0000000000..207ba6096e --- /dev/null +++ b/src/Avalonia.Input/TextInput/InputMethodManager.cs @@ -0,0 +1,101 @@ +using System; +using Avalonia.VisualTree; + +namespace Avalonia.Input.TextInput +{ + internal class TextInputMethodManager + { + private ITextInputMethodImpl? _im; + private IInputElement? _focusedElement; + private ITextInputMethodClient? _client; + private readonly TransformTrackingHelper _transformTracker = new TransformTrackingHelper(); + + public TextInputMethodManager() => _transformTracker.MatrixChanged += UpdateCursorRect; + + private ITextInputMethodClient? Client + { + get => _client; + set + { + if(_client == value) + return; + if (_client != null) + { + _client.CursorRectangleChanged -= OnCursorRectangleChanged; + _client.TextViewVisualChanged -= OnTextViewVisualChanged; + } + + _client = value; + + if (_client != null) + { + _client.CursorRectangleChanged += OnCursorRectangleChanged; + _client.TextViewVisualChanged += OnTextViewVisualChanged; + var optionsQuery = new TextInputOptionsQueryEventArgs + { + RoutedEvent = InputElement.TextInputOptionsQueryEvent + }; + _focusedElement?.RaiseEvent(optionsQuery); + _im?.Reset(); + _im?.SetOptions(optionsQuery); + _transformTracker?.SetVisual(_client?.TextViewVisual); + UpdateCursorRect(); + _im?.SetActive(true); + } + else + { + _im?.SetActive(false); + _transformTracker.SetVisual(null); + } + } + } + + private void OnTextViewVisualChanged(object sender, EventArgs e) + => _transformTracker.SetVisual(_client?.TextViewVisual); + + private void UpdateCursorRect() + { + if (_im == null || _client == null || _focusedElement?.VisualRoot == null) + return; + var transform = _focusedElement.TransformToVisual(_focusedElement.VisualRoot); + if (transform == null) + _im.SetCursorRect(default); + else + _im.SetCursorRect(_client.CursorRectangle.TransformToAABB(transform.Value)); + } + + private void OnCursorRectangleChanged(object sender, EventArgs e) + { + if (sender == _client) + UpdateCursorRect(); + } + + public void SetFocusedElement(IInputElement? element) + { + if(_focusedElement == element) + return; + _focusedElement = element; + + var inputMethod = (element?.VisualRoot as ITextInputMethodRoot)?.InputMethod; + if(_im != inputMethod) + _im?.SetActive(false); + + _im = inputMethod; + + if (_focusedElement == null || _im == null) + { + Client = null; + _im?.SetActive(false); + return; + } + + var clientQuery = new TextInputMethodClientRequestedEventArgs + { + RoutedEvent = InputElement.TextInputMethodClientRequestedEvent + }; + + _focusedElement.RaiseEvent(clientQuery); + Client = clientQuery.Client; + } + } +} diff --git a/src/Avalonia.Input/TextInput/TextInputContentType.cs b/src/Avalonia.Input/TextInput/TextInputContentType.cs new file mode 100644 index 0000000000..5d73fc1552 --- /dev/null +++ b/src/Avalonia.Input/TextInput/TextInputContentType.cs @@ -0,0 +1,12 @@ +namespace Avalonia.Input.TextInput +{ + public enum TextInputContentType + { + Normal = 0, + Email = 1, + Phone = 2, + Number = 3, + Url = 4, + Password = 5 + } +} diff --git a/src/Avalonia.Input/TextInput/TextInputMethodClientRequestedEventArgs.cs b/src/Avalonia.Input/TextInput/TextInputMethodClientRequestedEventArgs.cs new file mode 100644 index 0000000000..bec43487d2 --- /dev/null +++ b/src/Avalonia.Input/TextInput/TextInputMethodClientRequestedEventArgs.cs @@ -0,0 +1,12 @@ +using Avalonia.Interactivity; + +namespace Avalonia.Input.TextInput +{ + public class TextInputMethodClientRequestedEventArgs : RoutedEventArgs + { + /// + /// Set this property to a valid text input client to enable input method interaction + /// + public ITextInputMethodClient? Client { get; set; } + } +} diff --git a/src/Avalonia.Input/TextInput/TextInputOptionsQueryEventArgs.cs b/src/Avalonia.Input/TextInput/TextInputOptionsQueryEventArgs.cs new file mode 100644 index 0000000000..924d0eb166 --- /dev/null +++ b/src/Avalonia.Input/TextInput/TextInputOptionsQueryEventArgs.cs @@ -0,0 +1,32 @@ +using Avalonia.Interactivity; + +namespace Avalonia.Input.TextInput +{ + public class TextInputOptionsQueryEventArgs : RoutedEventArgs + { + /// + /// The content type (mostly for determining the shape of the virtual keyboard) + /// + public TextInputContentType ContentType { get; set; } + /// + /// Text is multiline + /// + public bool Multiline { get; set; } + /// + /// Text is in lower case + /// + public bool Lowercase { get; set; } + /// + /// Text is in upper case + /// + public bool Uppercase { get; set; } + /// + /// Automatically capitalize letters at the start of the sentence + /// + public bool AutoCapitalization { get; set; } + /// + /// Text contains sensitive data like card numbers and should not be stored + /// + public bool IsSensitive { get; set; } + } +} diff --git a/src/Avalonia.Input/TextInput/TransformTrackingHelper.cs b/src/Avalonia.Input/TextInput/TransformTrackingHelper.cs new file mode 100644 index 0000000000..4211360a8f --- /dev/null +++ b/src/Avalonia.Input/TextInput/TransformTrackingHelper.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using Avalonia.Threading; +using Avalonia.VisualTree; + +namespace Avalonia.Input.TextInput +{ + class TransformTrackingHelper : IDisposable + { + private IVisual? _visual; + private bool _queuedForUpdate; + private readonly EventHandler _propertyChangedHandler; + private readonly List _propertyChangedSubscriptions = new List(); + + public TransformTrackingHelper() + { + _propertyChangedHandler = PropertyChangedHandler; + } + + public void SetVisual(IVisual? visual) + { + Dispose(); + _visual = visual; + if (visual != null) + { + visual.AttachedToVisualTree += OnAttachedToVisualTree; + visual.DetachedFromVisualTree -= OnDetachedFromVisualTree; + if (visual.IsAttachedToVisualTree) + SubscribeToParents(); + UpdateMatrix(); + } + } + + public Matrix? Matrix { get; private set; } + public event Action? MatrixChanged; + + public void Dispose() + { + if(_visual == null) + return; + UnsubscribeFromParents(); + _visual.AttachedToVisualTree -= OnAttachedToVisualTree; + _visual.DetachedFromVisualTree -= OnDetachedFromVisualTree; + _visual = null; + } + + private void SubscribeToParents() + { + var visual = _visual; + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + // false positive + while (visual != null) + { + if (visual is Visual v) + { + v.PropertyChanged += _propertyChangedHandler; + _propertyChangedSubscriptions.Add(v); + } + + visual = visual.VisualParent; + } + } + + private void UnsubscribeFromParents() + { + foreach (var v in _propertyChangedSubscriptions) + v.PropertyChanged -= _propertyChangedHandler; + _propertyChangedSubscriptions.Clear(); + } + + void UpdateMatrix() + { + Matrix? matrix = null; + if (_visual != null && _visual.VisualRoot != null) + matrix = _visual.TransformToVisual(_visual.VisualRoot); + if (Matrix != matrix) + { + Matrix = matrix; + MatrixChanged?.Invoke(); + } + } + + private void OnAttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs visualTreeAttachmentEventArgs) + { + SubscribeToParents(); + UpdateMatrix(); + } + + private void EnqueueForUpdate() + { + if(_queuedForUpdate) + return; + _queuedForUpdate = true; + Dispatcher.UIThread.Post(UpdateMatrix, DispatcherPriority.Render); + } + + private void PropertyChangedHandler(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.IsEffectiveValueChange && e.Property == Visual.BoundsProperty) + EnqueueForUpdate(); + } + + private void OnDetachedFromVisualTree(object sender, VisualTreeAttachmentEventArgs visualTreeAttachmentEventArgs) + { + UnsubscribeFromParents(); + UpdateMatrix(); + } + } +} diff --git a/src/Avalonia.X11/X11Clipboard.cs b/src/Avalonia.X11/X11Clipboard.cs index 7023bb3ef3..d428d82744 100644 --- a/src/Avalonia.X11/X11Clipboard.cs +++ b/src/Avalonia.X11/X11Clipboard.cs @@ -52,7 +52,7 @@ namespace Avalonia.X11 : null; } - private unsafe void OnEvent(XEvent ev) + private unsafe void OnEvent(ref XEvent ev) { if (ev.type == XEventName.SelectionRequest) { @@ -62,7 +62,7 @@ namespace Avalonia.X11 SelectionEvent = { type = XEventName.SelectionNotify, - send_event = true, + send_event = 1, display = _x11.Display, selection = sel.selection, target = sel.target, diff --git a/src/Avalonia.X11/X11Globals.cs b/src/Avalonia.X11/X11Globals.cs index afde15b808..057693f810 100644 --- a/src/Avalonia.X11/X11Globals.cs +++ b/src/Avalonia.X11/X11Globals.cs @@ -123,7 +123,7 @@ namespace Avalonia.X11 } } - private void HandleCompositionAtomOwnerEvents(XEvent ev) + private void HandleCompositionAtomOwnerEvents(ref XEvent ev) { if(ev.type == XEventName.DestroyNotify) UpdateCompositingAtomOwner(); @@ -154,7 +154,7 @@ namespace Avalonia.X11 } } - private void OnRootWindowEvent(XEvent ev) + private void OnRootWindowEvent(ref XEvent ev) { if (ev.type == XEventName.PropertyNotify) { diff --git a/src/Avalonia.X11/X11Info.cs b/src/Avalonia.X11/X11Info.cs index 8cfcbb9836..3bded4cce1 100644 --- a/src/Avalonia.X11/X11Info.cs +++ b/src/Avalonia.X11/X11Info.cs @@ -32,8 +32,10 @@ namespace Avalonia.X11 public IntPtr LastActivityTimestamp { get; set; } public XVisualInfo? TransparentVisualInfo { get; set; } + public bool HasXim { get; set; } + public IntPtr DefaultFontSet { get; set; } - public unsafe X11Info(IntPtr display, IntPtr deferredDisplay) + public unsafe X11Info(IntPtr display, IntPtr deferredDisplay, bool useXim) { Display = display; DeferredDisplay = deferredDisplay; @@ -43,9 +45,24 @@ namespace Avalonia.X11 DefaultCursor = XCreateFontCursor(display, CursorFontShape.XC_top_left_arrow); DefaultRootWindow = XDefaultRootWindow(display); Atoms = new X11Atoms(display); - //TODO: Open an actual XIM once we get support for preedit in our textbox - XSetLocaleModifiers("@im=none"); - Xim = XOpenIM(display, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); + + DefaultFontSet = XCreateFontSet(Display, "-*-*-*-*-*-*-*-*-*-*-*-*-*-*", + out var _, out var _, IntPtr.Zero); + + if (useXim) + { + XSetLocaleModifiers(""); + Xim = XOpenIM(display, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); + if (Xim != IntPtr.Zero) + HasXim = true; + } + + if (Xim == IntPtr.Zero) + { + XSetLocaleModifiers("@im=none"); + Xim = XOpenIM(display, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); + } + XMatchVisualInfo(Display, DefaultScreen, 32, 4, out var visual); if (visual.depth == 32) TransparentVisualInfo = visual; diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index c6db146f7b..b871aa6fcf 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; using System.Reflection; +using System.Runtime.InteropServices; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.FreeDesktop; +using Avalonia.FreeDesktop.DBusIme; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.OpenGL; @@ -21,7 +23,8 @@ namespace Avalonia.X11 { private Lazy _keyboardDevice = new Lazy(() => new KeyboardDevice()); public KeyboardDevice KeyboardDevice => _keyboardDevice.Value; - public Dictionary> Windows = new Dictionary>(); + public Dictionary Windows = + new Dictionary(); public XI2Manager XI2; public X11Info Info { get; private set; } public IX11Screens X11Screens { get; private set; } @@ -29,9 +32,24 @@ namespace Avalonia.X11 public X11PlatformOptions Options { get; private set; } public IntPtr OrphanedWindow { get; private set; } public X11Globals Globals { get; private set; } + [DllImport("libc")] + static extern void setlocale(int type, string s); public void Initialize(X11PlatformOptions options) { Options = options; + + bool useXim = false; + if (EnableIme(options)) + { + // Attempt to configure DBus-based input method and check if we can fall back to XIM + if (!X11DBusImeHelper.DetectAndRegister() && ShouldUseXim()) + useXim = true; + } + + // XIM doesn't work at all otherwise + if (useXim) + setlocale(0, ""); + XInitThreads(); Display = XOpenDisplay(IntPtr.Zero); DeferredDisplay = XOpenDisplay(IntPtr.Zero); @@ -40,7 +58,8 @@ namespace Avalonia.X11 if (Display == IntPtr.Zero) throw new Exception("XOpenDisplay failed"); XError.Init(); - Info = new X11Info(Display, DeferredDisplay); + + Info = new X11Info(Display, DeferredDisplay, useXim); Globals = new X11Globals(this); //TODO: log if (options.UseDBusMenu) @@ -90,6 +109,54 @@ namespace Avalonia.X11 { throw new NotSupportedException(); } + + bool EnableIme(X11PlatformOptions options) + { + // Disable if explicitly asked by user + var avaloniaImModule = Environment.GetEnvironmentVariable("AVALONIA_IM_MODULE"); + if (avaloniaImModule == "none") + return false; + + // Use value from options when specified + if (options.EnableIme.HasValue) + return options.EnableIme.Value; + + // Automatically enable for CJK locales + var lang = Environment.GetEnvironmentVariable("LANG"); + var isCjkLocale = lang != null && + (lang.Contains("zh") + || lang.Contains("ja") + || lang.Contains("vi") + || lang.Contains("ko")); + + return isCjkLocale; + } + + bool ShouldUseXim() + { + // Check if we are forbidden from using IME + if (Environment.GetEnvironmentVariable("AVALONIA_IM_MODULE") == "none" + || Environment.GetEnvironmentVariable("GTK_IM_MODULE") == "none" + || Environment.GetEnvironmentVariable("QT_IM_MODULE") == "none") + return true; + + // Check if XIM is configured + var modifiers = Environment.GetEnvironmentVariable("XMODIFIERS"); + if (modifiers == null) + return false; + if (modifiers.Contains("@im=none") || modifiers.Contains("@im=null")) + return false; + if (!modifiers.Contains("@im=")) + return false; + + // Check if we are configured to use it + if (Environment.GetEnvironmentVariable("GTK_IM_MODULE") == "xim" + || Environment.GetEnvironmentVariable("QT_IM_MODULE") == "xim" + || Environment.GetEnvironmentVariable("AVALONIA_IM_MODULE") == "xim") + return true; + + return false; + } } } @@ -103,6 +170,7 @@ namespace Avalonia public bool OverlayPopups { get; set; } public bool UseDBusMenu { get; set; } public bool UseDeferredRendering { get; set; } = true; + public bool? EnableIme { get; set; } public IList GlProfiles { get; set; } = new List { diff --git a/src/Avalonia.X11/X11PlatformThreading.cs b/src/Avalonia.X11/X11PlatformThreading.cs index 0215e43d98..d724cd2aed 100644 --- a/src/Avalonia.X11/X11PlatformThreading.cs +++ b/src/Avalonia.X11/X11PlatformThreading.cs @@ -13,7 +13,9 @@ namespace Avalonia.X11 { private readonly AvaloniaX11Platform _platform; private readonly IntPtr _display; - private readonly Dictionary> _eventHandlers; + + public delegate void EventHandler(ref XEvent xev); + private readonly Dictionary _eventHandlers; private Thread _mainThread; [StructLayout(LayoutKind.Explicit)] @@ -162,13 +164,16 @@ namespace Avalonia.X11 Signaled?.Invoke(prio); } - void HandleX11(CancellationToken cancellationToken) + unsafe void HandleX11(CancellationToken cancellationToken) { while (XPending(_display) != 0) { if (cancellationToken.IsCancellationRequested) return; XNextEvent(_display, out var xev); + if(XFilterEvent(ref xev, IntPtr.Zero)) + continue; + if (xev.type == XEventName.GenericEvent) XGetEventData(_display, &xev.GenericEventCookie); try @@ -182,7 +187,7 @@ namespace Avalonia.X11 } } else if (_eventHandlers.TryGetValue(xev.AnyEvent.window, out var handler)) - handler(xev); + handler(ref xev); } finally { diff --git a/src/Avalonia.X11/X11Screens.cs b/src/Avalonia.X11/X11Screens.cs index b51497fb13..2900c9edb6 100644 --- a/src/Avalonia.X11/X11Screens.cs +++ b/src/Avalonia.X11/X11Screens.cs @@ -76,7 +76,7 @@ namespace Avalonia.X11 XRRSelectInput(_x11.Display, _window, RandrEventMask.RRScreenChangeNotify); } - private void OnEvent(XEvent ev) + private void OnEvent(ref XEvent ev) { // Invalidate cache on RRScreenChangeNotify if ((int)ev.type == _x11.RandrEventBase + (int)RandrEvent.RRScreenChangeNotify) diff --git a/src/Avalonia.X11/X11Structs.cs b/src/Avalonia.X11/X11Structs.cs index ae4954a2d5..e62aeefc5d 100644 --- a/src/Avalonia.X11/X11Structs.cs +++ b/src/Avalonia.X11/X11Structs.cs @@ -53,7 +53,7 @@ namespace Avalonia.X11 { internal struct XAnyEvent { internal XEventName type; internal IntPtr serial; - internal bool send_event; + internal int send_event; internal IntPtr display; internal IntPtr window; } @@ -62,7 +62,7 @@ namespace Avalonia.X11 { internal struct XKeyEvent { internal XEventName type; internal IntPtr serial; - internal bool send_event; + internal int send_event; internal IntPtr display; internal IntPtr window; internal IntPtr root; @@ -74,14 +74,14 @@ namespace Avalonia.X11 { internal int y_root; internal XModifierMask state; internal int keycode; - internal bool same_screen; + internal int same_screen; } [StructLayout(LayoutKind.Sequential)] internal struct XButtonEvent { internal XEventName type; internal IntPtr serial; - internal bool send_event; + internal int send_event; internal IntPtr display; internal IntPtr window; internal IntPtr root; @@ -93,14 +93,14 @@ namespace Avalonia.X11 { internal int y_root; internal XModifierMask state; internal int button; - internal bool same_screen; + internal int same_screen; } [StructLayout(LayoutKind.Sequential)] internal struct XMotionEvent { internal XEventName type; internal IntPtr serial; - internal bool send_event; + internal int send_event; internal IntPtr display; internal IntPtr window; internal IntPtr root; @@ -112,14 +112,14 @@ namespace Avalonia.X11 { internal int y_root; internal XModifierMask state; internal byte is_hint; - internal bool same_screen; + internal int same_screen; } [StructLayout(LayoutKind.Sequential)] internal struct XCrossingEvent { internal XEventName type; internal IntPtr serial; - internal bool send_event; + internal int send_event; internal IntPtr display; internal IntPtr window; internal IntPtr root; @@ -131,8 +131,8 @@ namespace Avalonia.X11 { internal int y_root; internal NotifyMode mode; internal NotifyDetail detail; - internal bool same_screen; - internal bool focus; + internal int same_screen; + internal int focus; internal XModifierMask state; } @@ -140,7 +140,7 @@ namespace Avalonia.X11 { internal struct XFocusChangeEvent { internal XEventName type; internal IntPtr serial; - internal bool send_event; + internal int send_event; internal IntPtr display; internal IntPtr window; internal int mode; @@ -151,7 +151,7 @@ namespace Avalonia.X11 { internal struct XKeymapEvent { internal XEventName type; internal IntPtr serial; - internal bool send_event; + internal int send_event; internal IntPtr display; internal IntPtr window; internal byte key_vector0; @@ -192,7 +192,7 @@ namespace Avalonia.X11 { internal struct XExposeEvent { internal XEventName type; internal IntPtr serial; - internal bool send_event; + internal int send_event; internal IntPtr display; internal IntPtr window; internal int x; @@ -206,7 +206,7 @@ namespace Avalonia.X11 { internal struct XGraphicsExposeEvent { internal XEventName type; internal IntPtr serial; - internal bool send_event; + internal int send_event; internal IntPtr display; internal IntPtr drawable; internal int x; @@ -222,7 +222,7 @@ namespace Avalonia.X11 { internal struct XNoExposeEvent { internal XEventName type; internal IntPtr serial; - internal bool send_event; + internal int send_event; internal IntPtr display; internal IntPtr drawable; internal int major_code; @@ -233,7 +233,7 @@ namespace Avalonia.X11 { internal struct XVisibilityEvent { internal XEventName type; internal IntPtr serial; - internal bool send_event; + internal int send_event; internal IntPtr display; internal IntPtr window; internal int state; @@ -243,7 +243,7 @@ namespace Avalonia.X11 { internal struct XCreateWindowEvent { internal XEventName type; internal IntPtr serial; - internal bool send_event; + internal int send_event; internal IntPtr display; internal IntPtr parent; internal IntPtr window; @@ -252,14 +252,14 @@ namespace Avalonia.X11 { internal int width; internal int height; internal int border_width; - internal bool override_redirect; + internal int override_redirect; } [StructLayout(LayoutKind.Sequential)] internal struct XDestroyWindowEvent { internal XEventName type; internal IntPtr serial; - internal bool send_event; + internal int send_event; internal IntPtr display; internal IntPtr xevent; internal IntPtr window; @@ -269,29 +269,29 @@ namespace Avalonia.X11 { internal struct XUnmapEvent { internal XEventName type; internal IntPtr serial; - internal bool send_event; + internal int send_event; internal IntPtr display; internal IntPtr xevent; internal IntPtr window; - internal bool from_configure; + internal int from_configure; } [StructLayout(LayoutKind.Sequential)] internal struct XMapEvent { internal XEventName type; internal IntPtr serial; - internal bool send_event; + internal int send_event; internal IntPtr display; internal IntPtr xevent; internal IntPtr window; - internal bool override_redirect; + internal int override_redirect; } [StructLayout(LayoutKind.Sequential)] internal struct XMapRequestEvent { internal XEventName type; internal IntPtr serial; - internal bool send_event; + internal int send_event; internal IntPtr display; internal IntPtr parent; internal IntPtr window; @@ -301,21 +301,21 @@ namespace Avalonia.X11 { internal struct XReparentEvent { internal XEventName type; internal IntPtr serial; - internal bool send_event; + internal int send_event; internal IntPtr display; internal IntPtr xevent; internal IntPtr window; internal IntPtr parent; internal int x; internal int y; - internal bool override_redirect; + internal int override_redirect; } [StructLayout(LayoutKind.Sequential)] internal struct XConfigureEvent { internal XEventName type; internal IntPtr serial; - internal bool send_event; + internal int send_event; internal IntPtr display; internal IntPtr xevent; internal IntPtr window; @@ -325,14 +325,14 @@ namespace Avalonia.X11 { internal int height; internal int border_width; internal IntPtr above; - internal bool override_redirect; + internal int override_redirect; } [StructLayout(LayoutKind.Sequential)] internal struct XGravityEvent { internal XEventName type; internal IntPtr serial; - internal bool send_event; + internal int send_event; internal IntPtr display; internal IntPtr xevent; internal IntPtr window; @@ -344,7 +344,7 @@ namespace Avalonia.X11 { internal struct XResizeRequestEvent { internal XEventName type; internal IntPtr serial; - internal bool send_event; + internal int send_event; internal IntPtr display; internal IntPtr window; internal int width; @@ -355,7 +355,7 @@ namespace Avalonia.X11 { internal struct XConfigureRequestEvent { internal XEventName type; internal IntPtr serial; - internal bool send_event; + internal int send_event; internal IntPtr display; internal IntPtr parent; internal IntPtr window; @@ -373,7 +373,7 @@ namespace Avalonia.X11 { internal struct XCirculateEvent { internal XEventName type; internal IntPtr serial; - internal bool send_event; + internal int send_event; internal IntPtr display; internal IntPtr xevent; internal IntPtr window; @@ -384,7 +384,7 @@ namespace Avalonia.X11 { internal struct XCirculateRequestEvent { internal XEventName type; internal IntPtr serial; - internal bool send_event; + internal int send_event; internal IntPtr display; internal IntPtr parent; internal IntPtr window; @@ -395,7 +395,7 @@ namespace Avalonia.X11 { internal struct XPropertyEvent { internal XEventName type; internal IntPtr serial; - internal bool send_event; + internal int send_event; internal IntPtr display; internal IntPtr window; internal IntPtr atom; @@ -407,7 +407,7 @@ namespace Avalonia.X11 { internal struct XSelectionClearEvent { internal XEventName type; internal IntPtr serial; - internal bool send_event; + internal int send_event; internal IntPtr display; internal IntPtr window; internal IntPtr selection; @@ -418,7 +418,7 @@ namespace Avalonia.X11 { internal struct XSelectionRequestEvent { internal XEventName type; internal IntPtr serial; - internal bool send_event; + internal int send_event; internal IntPtr display; internal IntPtr owner; internal IntPtr requestor; @@ -432,7 +432,7 @@ namespace Avalonia.X11 { internal struct XSelectionEvent { internal XEventName type; internal IntPtr serial; - internal bool send_event; + internal int send_event; internal IntPtr display; internal IntPtr requestor; internal IntPtr selection; @@ -445,11 +445,11 @@ namespace Avalonia.X11 { internal struct XColormapEvent { internal XEventName type; internal IntPtr serial; - internal bool send_event; + internal int send_event; internal IntPtr display; internal IntPtr window; internal IntPtr colormap; - internal bool c_new; + internal int c_new; internal int state; } @@ -457,7 +457,7 @@ namespace Avalonia.X11 { internal struct XClientMessageEvent { internal XEventName type; internal IntPtr serial; - internal bool send_event; + internal int send_event; internal IntPtr display; internal IntPtr window; internal IntPtr message_type; @@ -473,7 +473,7 @@ namespace Avalonia.X11 { internal struct XMappingEvent { internal XEventName type; internal IntPtr serial; - internal bool send_event; + internal int send_event; internal IntPtr display; internal IntPtr window; internal int request; @@ -518,6 +518,15 @@ namespace Avalonia.X11 { internal IntPtr pad21; internal IntPtr pad22; internal IntPtr pad23; + internal IntPtr pad24; + internal IntPtr pad25; + internal IntPtr pad26; + internal IntPtr pad27; + internal IntPtr pad28; + internal IntPtr pad29; + internal IntPtr pad30; + internal IntPtr pad31; + internal IntPtr pad32; } [StructLayout(LayoutKind.Sequential)] @@ -525,7 +534,7 @@ namespace Avalonia.X11 { { internal int type; /* of event. Always GenericEvent */ internal IntPtr serial; /* # of last request processed */ - internal bool send_event; /* true if from SendEvent request */ + internal int send_event; /* true if from SendEvent request */ internal IntPtr display; /* Display the event was read from */ internal int extension; /* major opcode of extension that caused the event */ internal int evtype; /* actual event type. */ @@ -672,10 +681,10 @@ namespace Avalonia.X11 { internal int backing_store; internal IntPtr backing_planes; internal IntPtr backing_pixel; - internal bool save_under; + internal int save_under; internal IntPtr event_mask; internal IntPtr do_not_propagate_mask; - internal bool override_redirect; + internal int override_redirect; internal IntPtr colormap; internal IntPtr cursor; } @@ -696,14 +705,14 @@ namespace Avalonia.X11 { internal int backing_store; internal IntPtr backing_planes; internal IntPtr backing_pixel; - internal bool save_under; + internal int save_under; internal IntPtr colormap; - internal bool map_installed; + internal int map_installed; internal MapState map_state; internal IntPtr all_event_masks; internal IntPtr your_event_mask; internal IntPtr do_not_propagate_mask; - internal bool override_direct; + internal int override_direct; internal IntPtr screen; public override string ToString () @@ -1029,7 +1038,7 @@ namespace Avalonia.X11 { internal int max_maps; internal int min_maps; internal int backing_store; - internal bool save_unders; + internal int save_unders; internal IntPtr root_input_mask; } @@ -1280,7 +1289,7 @@ namespace Avalonia.X11 { internal int ts_y_origin; internal IntPtr font; internal GCSubwindowMode subwindow_mode; - internal bool graphics_exposures; + internal int graphics_exposures; internal int clip_x_origin; internal int clib_y_origin; internal IntPtr clip_mask; @@ -1499,7 +1508,7 @@ namespace Avalonia.X11 { [StructLayout(LayoutKind.Sequential)] internal struct XWMHints { internal IntPtr flags; - internal bool input; + internal int input; internal XInitialState initial_state; internal IntPtr icon_pixmap; internal IntPtr icon_window; @@ -1708,19 +1717,30 @@ namespace Avalonia.X11 { } [StructLayout (LayoutKind.Sequential)] - internal struct XIMStyles + internal unsafe struct XIMStyles { public ushort count_styles; - public IntPtr supported_styles; + public IntPtr* supported_styles; } [StructLayout (LayoutKind.Sequential)] [Serializable] - internal class XPoint + internal struct XPoint { public short X; public short Y; } + + [StructLayout (LayoutKind.Sequential)] + [Serializable] + internal struct XRectangle + { + public short X; + public short Y; + public short W; + public short H; + } + [StructLayout (LayoutKind.Sequential)] [Serializable] @@ -1798,7 +1818,7 @@ namespace Avalonia.X11 { { public ushort Length; public IntPtr Feedback; // to XIMFeedbackStruct - public bool EncodingIsWChar; + public int EncodingIsWChar; public IntPtr String; // it could be either char* or wchar_t* } @@ -1850,6 +1870,8 @@ namespace Avalonia.X11 { public const string XNClientWindow = "clientWindow"; public const string XNInputStyle = "inputStyle"; public const string XNFocusWindow = "focusWindow"; + public const string XNResourceName = "resourceName"; + public const string XNResourceClass = "resourceClass"; // XIMPreeditCallbacks delegate names. public const string XNPreeditStartCallback = "preeditStartCallback"; diff --git a/src/Avalonia.X11/X11Window.Ime.cs b/src/Avalonia.X11/X11Window.Ime.cs new file mode 100644 index 0000000000..f469ff7455 --- /dev/null +++ b/src/Avalonia.X11/X11Window.Ime.cs @@ -0,0 +1,208 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using Avalonia.FreeDesktop; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; +using Avalonia.Platform.Interop; +using static Avalonia.X11.XLib; + +namespace Avalonia.X11 +{ + partial class X11Window + { + private ITextInputMethodImpl _ime; + private IX11InputMethodControl _imeControl; + private bool _processingIme; + + private Queue<(RawKeyEventArgs args, XEvent xev, int keyval, int keycode)> _imeQueue = + new Queue<(RawKeyEventArgs args, XEvent xev, int keyVal, int keyCode)>(); + + unsafe void CreateIC() + { + if (_x11.HasXim) + { + XGetIMValues(_x11.Xim, XNames.XNQueryInputStyle, out var supported_styles, IntPtr.Zero); + for (var c = 0; c < supported_styles->count_styles; c++) + { + var style = (XIMProperties)supported_styles->supported_styles[c]; + if ((int)(style & XIMProperties.XIMPreeditPosition) != 0 + && ((int)(style & XIMProperties.XIMStatusNothing) != 0)) + { + XPoint spot = default; + XRectangle area = default; + + + //using var areaS = new Utf8Buffer("area"); + using var spotS = new Utf8Buffer("spotLocation"); + using var fontS = new Utf8Buffer("fontSet"); + + var list = XVaCreateNestedList(0, + //areaS, &area, + spotS, &spot, + fontS, _x11.DefaultFontSet, + IntPtr.Zero); + _xic = XCreateIC(_x11.Xim, + XNames.XNClientWindow, _handle, + XNames.XNFocusWindow, _handle, + XNames.XNInputStyle, new IntPtr((int)style), + XNames.XNResourceName, _platform.Options.WmClass, + XNames.XNResourceClass, _platform.Options.WmClass, + XNames.XNPreeditAttributes, list, + IntPtr.Zero); + + XFree(list); + + break; + } + } + + XFree(new IntPtr(supported_styles)); + } + + if (_xic == IntPtr.Zero) + _xic = XCreateIC(_x11.Xim, XNames.XNInputStyle, + new IntPtr((int)(XIMProperties.XIMPreeditNothing | XIMProperties.XIMStatusNothing)), + XNames.XNClientWindow, _handle, XNames.XNFocusWindow, _handle, IntPtr.Zero); + } + + void InitializeIme() + { + var ime = AvaloniaLocator.Current.GetService()?.CreateClient(_handle); + if (ime == null && _x11.HasXim) + { + var xim = new XimInputMethod(this); + ime = (xim, xim); + } + if (ime != null) + { + (_ime, _imeControl) = ime.Value; + _imeControl.Commit += s => + ScheduleInput(new RawTextInputEventArgs(_keyboard, (ulong)_x11.LastActivityTimestamp.ToInt64(), + _inputRoot, s)); + _imeControl.ForwardKey += ev => + { + ScheduleInput(new RawKeyEventArgs(_keyboard, (ulong)_x11.LastActivityTimestamp.ToInt64(), + _inputRoot, ev.Type, X11KeyTransform.ConvertKey((X11Key)ev.KeyVal), + (RawInputModifiers)ev.Modifiers)); + }; + } + } + + void UpdateImePosition() => _imeControl?.UpdateWindowInfo(Position, RenderScaling); + + void HandleKeyEvent(ref XEvent ev) + { + var index = ev.KeyEvent.state.HasFlag(XModifierMask.ShiftMask); + + // We need the latin key, since it's mainly used for hotkeys, we use a different API for text anyway + var key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 1 : 0).ToInt32(); + + // Manually switch the Shift index for the keypad, + // there should be a proper way to do this + if (ev.KeyEvent.state.HasFlag(XModifierMask.Mod2Mask) + && key > X11Key.Num_Lock && key <= X11Key.KP_9) + key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 0 : 1).ToInt32(); + + var filtered = ScheduleKeyInput(new RawKeyEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), _inputRoot, + ev.type == XEventName.KeyPress ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp, + X11KeyTransform.ConvertKey(key), TranslateModifiers(ev.KeyEvent.state)), ref ev, (int)key, ev.KeyEvent.keycode); + + if (ev.type == XEventName.KeyPress && !filtered) + TriggerClassicTextInputEvent(ref ev); + } + + void TriggerClassicTextInputEvent(ref XEvent ev) + { + var text = TranslateEventToString(ref ev); + if (text != null) + ScheduleInput( + new RawTextInputEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), _inputRoot, text), + ref ev); + } + + private const int ImeBufferSize = 64 * 1024; + [ThreadStatic] private static IntPtr ImeBuffer; + + unsafe string TranslateEventToString(ref XEvent ev) + { + if (ImeBuffer == IntPtr.Zero) + ImeBuffer = Marshal.AllocHGlobal(ImeBufferSize); + + var len = Xutf8LookupString(_xic, ref ev, ImeBuffer.ToPointer(), ImeBufferSize, + out _, out var istatus); + var status = (XLookupStatus)istatus; + + if (len == 0) + return null; + + string text; + if (status == XLookupStatus.XBufferOverflow) + return null; + else + text = Encoding.UTF8.GetString((byte*)ImeBuffer.ToPointer(), len); + + if (text == null) + return null; + + if (text.Length == 1) + { + if (text[0] < ' ' || text[0] == 0x7f) //Control codes or DEL + return null; + } + + return text; + } + + + bool ScheduleKeyInput(RawKeyEventArgs args, ref XEvent xev, int keyval, int keycode) + { + _x11.LastActivityTimestamp = xev.ButtonEvent.time; + if (_imeControl != null && _imeControl.IsEnabled) + { + if (FilterIme(args, xev, keyval, keycode)) + return true; + } + ScheduleInput(args); + return false; + } + + bool FilterIme(RawKeyEventArgs args, XEvent xev, int keyval, int keycode) + { + if (_ime == null) + return false; + _imeQueue.Enqueue((args, xev, keyval, keycode)); + if (!_processingIme) + ProcessNextImeEvent(); + + return true; + } + + async void ProcessNextImeEvent() + { + if(_processingIme) + return; + _processingIme = true; + try + { + while (_imeQueue.Count != 0) + { + var ev = _imeQueue.Dequeue(); + if (_imeControl == null || !await _imeControl.HandleEventAsync(ev.args, ev.keyval, ev.keycode)) + { + ScheduleInput(ev.args); + if (ev.args.Type == RawKeyEventType.KeyDown) + TriggerClassicTextInputEvent(ref ev.xev); + } + } + } + finally + { + _processingIme = false; + } + } + } +} diff --git a/src/Avalonia.X11/X11Window.Xim.cs b/src/Avalonia.X11/X11Window.Xim.cs new file mode 100644 index 0000000000..444c82fd22 --- /dev/null +++ b/src/Avalonia.X11/X11Window.Xim.cs @@ -0,0 +1,121 @@ +using System; +using System.Threading.Tasks; +using Avalonia.FreeDesktop; +using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; +using Avalonia.Platform.Interop; +using Avalonia.Threading; +using static Avalonia.X11.XLib; +namespace Avalonia.X11 +{ + partial class X11Window + { + + class XimInputMethod : ITextInputMethodImpl, IX11InputMethodControl + { + private readonly X11Window _parent; + private bool _controlActive, _windowActive, _imeActive; + private Rect? _queuedCursorRect; + + public XimInputMethod(X11Window parent) + { + _parent = parent; + } + + public void SetCursorRect(Rect rect) + { + var needEnqueue = _queuedCursorRect == null; + _queuedCursorRect = rect; + if(needEnqueue) + Dispatcher.UIThread.Post(() => + { + if(_queuedCursorRect == null) + return; + var rc = _queuedCursorRect.Value; + _queuedCursorRect = null; + + if (_parent._xic == IntPtr.Zero) + return; + + rect *= _parent._scaling; + + var pt = new XPoint + { + X = (short)Math.Min(Math.Max(rect.X, short.MinValue), short.MaxValue), + Y = (short)Math.Min(Math.Max(rect.Y + rect.Height, short.MinValue), short.MaxValue) + }; + + using var spotLoc = new Utf8Buffer(XNames.XNSpotLocation); + var list = XVaCreateNestedList(0, spotLoc, ref pt, IntPtr.Zero); + XSetICValues(_parent._xic, XNames.XNPreeditAttributes, list, IntPtr.Zero); + XFree(list); + }, DispatcherPriority.Background); + } + + public void SetWindowActive(bool active) + { + _windowActive = active; + UpdateActive(); + } + + public void SetActive(bool active) + { + _controlActive = active; + UpdateActive(); + } + + private void UpdateActive() + { + var active = _windowActive && _controlActive; + if(_parent._xic == IntPtr.Zero) + return; + if (active != _imeActive) + { + _imeActive = active; + if (active) + { + Reset(); + XSetICFocus(_parent._xic); + } + else + XUnsetICFocus(_parent._xic); + } + } + + public void UpdateWindowInfo(PixelPoint position, double scaling) + { + // No-op + } + + public void SetOptions(TextInputOptionsQueryEventArgs options) + { + // No-op + } + + public void Reset() + { + if(_parent._xic == IntPtr.Zero) + return; + + var data = XmbResetIC(_parent._xic); + if (data != IntPtr.Zero) + XFree(data); + } + + public void Dispose() + { + // No-op + } + + public bool IsEnabled => false; + + public ValueTask HandleEventAsync(RawKeyEventArgs args, int keyVal, int keyCode) => + new ValueTask(false); + + public event Action Commit; + public event Action ForwardKey; + + } + + } +} diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index b061920d6a..aa83b9f114 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -5,12 +5,14 @@ using System.Diagnostics; using System.Linq; using System.Reactive.Disposables; using System.Text; +using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.FreeDesktop; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; using Avalonia.OpenGL; using Avalonia.OpenGL.Egl; using Avalonia.Platform; @@ -22,9 +24,10 @@ using static Avalonia.X11.XLib; // ReSharper disable StringLiteralTypo namespace Avalonia.X11 { - unsafe class X11Window : IWindowImpl, IPopupImpl, IXI2Client, + unsafe partial class X11Window : IWindowImpl, IPopupImpl, IXI2Client, ITopLevelImplWithNativeMenuExporter, - ITopLevelImplWithNativeControlHost + ITopLevelImplWithNativeControlHost, + ITopLevelImplWithTextInputMethod { private readonly AvaloniaX11Platform _platform; private readonly IWindowImpl _popupParent; @@ -79,7 +82,7 @@ namespace Avalonia.X11 if (_popup) { - attr.override_redirect = true; + attr.override_redirect = 1; valueMask |= SetWindowValuemask.OverrideRedirect; } @@ -178,11 +181,12 @@ namespace Avalonia.X11 Surfaces = surfaces.ToArray(); UpdateMotifHints(); UpdateSizeHints(null); - _xic = XCreateIC(_x11.Xim, XNames.XNInputStyle, XIMProperties.XIMPreeditNothing | XIMProperties.XIMStatusNothing, - XNames.XNClientWindow, _handle, IntPtr.Zero); + _transparencyHelper = new TransparencyHelper(_x11, _handle, platform.Globals); _transparencyHelper.SetTransparencyRequest(WindowTransparencyLevel.None); + CreateIC(); + XFlush(_x11.Display); if(_popup) PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(popupParent, MoveResize)); @@ -194,6 +198,7 @@ namespace Avalonia.X11 Paint?.Invoke(default); return _handle != IntPtr.Zero; }, TimeSpan.FromMilliseconds(100)); + InitializeIme(); } class SurfaceInfo : EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo @@ -350,15 +355,13 @@ namespace Avalonia.X11 (IRenderer)new X11ImmediateRendererProxy(root, loop); } - void OnEvent(XEvent ev) + void OnEvent(ref XEvent ev) { lock (SyncRoot) - OnEventSync(ev); + OnEventSync(ref ev); } - void OnEventSync(XEvent ev) + void OnEventSync(ref XEvent ev) { - if(XFilterEvent(ref ev, _handle)) - return; if (ev.type == XEventName.MapNotify) { _mapped = true; @@ -386,9 +389,13 @@ namespace Avalonia.X11 if (ActivateTransientChildIfNeeded()) return; Activated?.Invoke(); + _imeControl?.SetWindowActive(true); } else if (ev.type == XEventName.FocusOut) + { + _imeControl?.SetWindowActive(false); Deactivated?.Invoke(); + } else if (ev.type == XEventName.MotionNotify) MouseEvent(RawPointerEventType.Move, ref ev, ev.MotionEvent.state); else if (ev.type == XEventName.LeaveNotify) @@ -447,7 +454,7 @@ namespace Avalonia.X11 return; var needEnqueue = (_configure == null); _configure = ev.ConfigureEvent; - if (ev.ConfigureEvent.override_redirect || ev.ConfigureEvent.send_event) + if (ev.ConfigureEvent.override_redirect != 0 || ev.ConfigureEvent.send_event != 0) _configurePoint = new PixelPoint(ev.ConfigureEvent.x, ev.ConfigureEvent.y); else { @@ -477,6 +484,7 @@ namespace Avalonia.X11 PositionChanged?.Invoke(npos); updatedSizeViaScaling = UpdateScaling(); } + UpdateImePosition(); if (changedSize && !updatedSizeViaScaling && !_popup) Resized?.Invoke(ClientSize); @@ -487,7 +495,8 @@ namespace Avalonia.X11 XConfigureResizeWindow(_x11.Display, _renderHandle, ev.ConfigureEvent.width, ev.ConfigureEvent.height); } - else if (ev.type == XEventName.DestroyNotify && ev.AnyEvent.window == _handle) + else if (ev.type == XEventName.DestroyNotify + && ev.DestroyWindowEvent.window == _handle) { Cleanup(); } @@ -507,39 +516,7 @@ namespace Avalonia.X11 { if (ActivateTransientChildIfNeeded()) return; - var buffer = stackalloc byte[40]; - - var index = ev.KeyEvent.state.HasFlag(XModifierMask.ShiftMask); - - // We need the latin key, since it's mainly used for hotkeys, we use a different API for text anyway - var key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 1 : 0).ToInt32(); - - // Manually switch the Shift index for the keypad, - // there should be a proper way to do this - if (ev.KeyEvent.state.HasFlag(XModifierMask.Mod2Mask) - && key > X11Key.Num_Lock && key <= X11Key.KP_9) - key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 0 : 1).ToInt32(); - - - ScheduleInput(new RawKeyEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), _inputRoot, - ev.type == XEventName.KeyPress ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp, - X11KeyTransform.ConvertKey(key), TranslateModifiers(ev.KeyEvent.state)), ref ev); - - if (ev.type == XEventName.KeyPress) - { - var len = Xutf8LookupString(_xic, ref ev, buffer, 40, out _, out _); - if (len != 0) - { - var text = Encoding.UTF8.GetString(buffer, len); - if (text.Length == 1) - { - if (text[0] < ' ' || text[0] == 0x7f) //Control codes or DEL - return; - } - ScheduleInput(new RawTextInputEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), _inputRoot, text), - ref ev); - } - } + HandleKeyEvent(ref ev); } } @@ -562,6 +539,7 @@ namespace Avalonia.X11 var oldScaledSize = ClientSize; RenderScaling = newScaling; ScalingChanged?.Invoke(RenderScaling); + UpdateImePosition(); SetMinMaxSize(_scaledMinMaxSize.minSize, _scaledMinMaxSize.maxSize); if(!skipResize) Resize(oldScaledSize, true); @@ -699,6 +677,7 @@ namespace Avalonia.X11 _x11.LastActivityTimestamp = xev.ButtonEvent.time; ScheduleInput(args); } + public void ScheduleXI2Input(RawInputEventArgs args) { @@ -781,6 +760,13 @@ namespace Avalonia.X11 void Cleanup() { + if (_imeControl != null) + { + _imeControl.Dispose(); + _imeControl = null; + _ime = null; + } + if (_xic != IntPtr.Zero) { XDestroyIC(_xic); @@ -957,7 +943,7 @@ namespace Avalonia.X11 ClientMessageEvent = { type = XEventName.ClientMessage, - send_event = true, + send_event = 1, window = _handle, message_type = message_type, format = 32, @@ -1130,6 +1116,8 @@ namespace Avalonia.X11 public IPopupPositioner PopupPositioner { get; } public ITopLevelNativeMenuExporter NativeMenuExporter { get; } public INativeControlHostImpl NativeControlHost { get; } + public ITextInputMethodImpl TextInputMethod => _ime; + public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) => _transparencyHelper.SetTransparencyRequest(transparencyLevel); diff --git a/src/Avalonia.X11/XLib.cs b/src/Avalonia.X11/XLib.cs index bec94e63c9..8a1ee8d188 100644 --- a/src/Avalonia.X11/XLib.cs +++ b/src/Avalonia.X11/XLib.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; +using Avalonia.Platform.Interop; // ReSharper disable MemberCanBePrivate.Global // ReSharper disable FieldCanBeMadeReadOnly.Global @@ -57,6 +58,9 @@ namespace Avalonia.X11 [DllImport(libX11)] public static extern IntPtr XNextEvent(IntPtr display, out XEvent xevent); + + [DllImport(libX11)] + public static extern IntPtr XNextEvent(IntPtr display, XEvent* xevent); [DllImport(libX11)] public static extern int XConnectionNumber(IntPtr diplay); @@ -407,6 +411,9 @@ namespace Avalonia.X11 [DllImport(libX11)] public static extern bool XFilterEvent(ref XEvent xevent, IntPtr window); + + [DllImport(libX11)] + public static extern bool XFilterEvent(XEvent* xevent, IntPtr window); [DllImport(libX11)] public static extern void XkbSetDetectableAutoRepeat(IntPtr display, bool detectable, IntPtr supported); @@ -441,9 +448,9 @@ namespace Avalonia.X11 [DllImport(libX11)] public static extern IntPtr XCreateColormap(IntPtr display, IntPtr window, IntPtr visual, int create); - public enum XLookupStatus + public enum XLookupStatus : uint { - XBufferOverflow = -1, + XBufferOverflow = 0xffffffffu, XLookupNone = 1, XLookupChars = 2, XLookupKeySym = 3, @@ -454,7 +461,10 @@ namespace Avalonia.X11 public static extern unsafe int XLookupString(ref XEvent xevent, void* buffer, int num_bytes, out IntPtr keysym, out IntPtr status); [DllImport (libX11)] - public static extern unsafe int Xutf8LookupString(IntPtr xic, ref XEvent xevent, void* buffer, int num_bytes, out IntPtr keysym, out IntPtr status); + public static extern unsafe int Xutf8LookupString(IntPtr xic, ref XEvent xevent, void* buffer, int num_bytes, out IntPtr keysym, out UIntPtr status); + + [DllImport (libX11)] + public static extern unsafe int Xutf8LookupString(IntPtr xic, XEvent* xevent, void* buffer, int num_bytes, out IntPtr keysym, out IntPtr status); [DllImport (libX11)] public static extern unsafe IntPtr XKeycodeToKeysym(IntPtr display, int keycode, int index); @@ -464,12 +474,52 @@ namespace Avalonia.X11 [DllImport (libX11)] public static extern IntPtr XOpenIM (IntPtr display, IntPtr rdb, IntPtr res_name, IntPtr res_class); - + [DllImport (libX11)] - public static extern IntPtr XCreateIC (IntPtr xim, string name, XIMProperties im_style, string name2, IntPtr value2, IntPtr terminator); + public static extern IntPtr XGetIMValues (IntPtr xim, string name, out XIMStyles* value, IntPtr terminator); + + [DllImport (libX11)] + public static extern IntPtr XCreateIC (IntPtr xim, string name, IntPtr value, string name2, IntPtr value2, string name3, IntPtr value3, IntPtr terminator); + + [DllImport(libX11)] + public static extern IntPtr XCreateIC(IntPtr xim, string name, IntPtr value, string name2, IntPtr value2, + string name3, IntPtr value3, string name4, IntPtr value4, IntPtr terminator); + + [DllImport(libX11)] + public static extern IntPtr XCreateIC(IntPtr xim, string xnClientWindow, IntPtr handle, + string xnInputStyle, IntPtr value3, string xnResourceName, string optionsWmClass, + string xnResourceClass, string wmClass, string xnPreeditAttributes, IntPtr list, IntPtr zero); + + [DllImport(libX11)] + public static extern IntPtr XCreateIC(IntPtr xim, string xnClientWindow, IntPtr handle, string xnFocusWindow, + IntPtr value2, string xnInputStyle, IntPtr value3, string xnResourceName, string optionsWmClass, + string xnResourceClass, string wmClass, string xnPreeditAttributes, IntPtr list, IntPtr zero); + + [DllImport(libX11)] + public static extern void XSetICFocus(IntPtr xic); + + [DllImport(libX11)] + public static extern void XUnsetICFocus(IntPtr xic); + + [DllImport(libX11)] + public static extern IntPtr XmbResetIC(IntPtr xic); + + [DllImport(libX11)] + public static extern IntPtr XVaCreateNestedList(int unused, Utf8Buffer name, ref XPoint point, IntPtr terminator); + + [DllImport(libX11)] + public static extern IntPtr XVaCreateNestedList(int unused, Utf8Buffer xnArea, XRectangle* point, + Utf8Buffer xnSpotLocation, XPoint* value2, Utf8Buffer xnFontSet, IntPtr fs, IntPtr zero); + + [DllImport(libX11)] + public static extern IntPtr XVaCreateNestedList(int unused, + Utf8Buffer xnSpotLocation, XPoint* value2, Utf8Buffer xnFontSet, IntPtr fs, IntPtr zero); [DllImport (libX11)] - public static extern IntPtr XCreateIC (IntPtr xim, string name, XIMProperties im_style, string name2, IntPtr value2, string name3, IntPtr value3, IntPtr terminator); + public static extern IntPtr XCreateFontSet (IntPtr display, string name, out IntPtr list, out int count, IntPtr unused); + + [DllImport(libX11)] + public static extern IntPtr XSetICValues(IntPtr ic, string name, IntPtr data, IntPtr terminator); [DllImport (libX11)] public static extern void XCloseIM (IntPtr xim); @@ -633,14 +683,12 @@ namespace Avalonia.X11 } } - public static IntPtr CreateEventWindow(AvaloniaX11Platform plat, Action handler) + public static IntPtr CreateEventWindow(AvaloniaX11Platform plat, X11PlatformThreading.EventHandler handler) { var win = XCreateSimpleWindow(plat.Display, plat.Info.DefaultRootWindow, 0, 0, 1, 1, 0, IntPtr.Zero, IntPtr.Zero); plat.Windows[win] = handler; return win; } - - } } diff --git a/src/Windows/Avalonia.Win32/Win32GlManager.cs b/src/Windows/Avalonia.Win32/Win32GlManager.cs index f6b07ee33e..e159ba3b78 100644 --- a/src/Windows/Avalonia.Win32/Win32GlManager.cs +++ b/src/Windows/Avalonia.Win32/Win32GlManager.cs @@ -9,6 +9,8 @@ namespace Avalonia.Win32 { static class Win32GlManager { + private static readonly Version Windows7 = new Version(6, 1); + public static void Initialize() { AvaloniaLocator.CurrentMutable.Bind().ToLazy(() => @@ -20,13 +22,11 @@ namespace Avalonia.Win32 return wgl; } - if (opts?.AllowEglInitialization == true || - ((!opts?.AllowEglInitialization.HasValue ?? false) && - Win32Platform.WindowsVersion > new Version(6, 1))) + if (opts?.AllowEglInitialization ?? Win32Platform.WindowsVersion > Windows7) { var egl = EglPlatformOpenGlInterface.TryCreate(() => new AngleWin32EglDisplay()); - if (egl is { } && + if (egl != null && opts?.UseWindowsUIComposition == true) { WinUICompositorConnection.TryCreateAndRegister(egl);