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