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/ApiDiff.props b/build/ApiDiff.props
index da59ad4bf2..666417addf 100644
--- a/build/ApiDiff.props
+++ b/build/ApiDiff.props
@@ -1,6 +1,6 @@
- 0.10.0-rc1
+ 0.10.0
$(PackageId)
Avalonia
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/build/SharedVersion.props b/build/SharedVersion.props
index 9a268a21e7..75bada4bfc 100644
--- a/build/SharedVersion.props
+++ b/build/SharedVersion.props
@@ -3,7 +3,7 @@
Avalonia
0.10.999
- Copyright 2020 © The AvaloniaUI Project
+ Copyright 2021 © The AvaloniaUI Project
https://avaloniaui.net
https://github.com/AvaloniaUI/Avalonia/
true
diff --git a/build/SourceLink.props b/build/SourceLink.props
index e27727c9e8..1e007e01eb 100644
--- a/build/SourceLink.props
+++ b/build/SourceLink.props
@@ -1,5 +1,26 @@
+
+ true
+ false
+ true
+ embedded
+ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
+
+
+
+ true
+
+
+
+ true
+
+
-
+
+
+
+
+
+
diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj
index 75ee4a05cb..44e2290a0d 100644
--- a/packages/Avalonia/Avalonia.csproj
+++ b/packages/Avalonia/Avalonia.csproj
@@ -6,7 +6,9 @@
-
+
+ all
+
@@ -29,21 +31,23 @@
+
true
- build\
+ build\;buildTransitive\
true
- build\
+ build\;buildTransitive\
true
- build\
+ build\;buildTransitive\
+
diff --git a/readme.md b/readme.md
index cfa08149cb..f73bdffaeb 100644
--- a/readme.md
+++ b/readme.md
@@ -12,8 +12,6 @@ Avalonia is a cross-platform XAML-based UI framework providing a flexible stylin
([Xaml Control Gallery](https://github.com/AvaloniaUI/xamlcontrolsgallery))
-> **Note:** The UI theme you see in the picture above is still work-in-progress and will be available in the upcoming Avalonia 0.10.0 release. However, you can connect to our nightly build feed and install latest pre-release versions of Avalonia NuGet packages, if you are willing to help out with the development and testing. See [Using nightly build feed](https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed) for more info.
-
To see the status of some of our features, please see our [Roadmap](https://github.com/AvaloniaUI/Avalonia/issues/2239). You can also see what [breaking changes](https://github.com/AvaloniaUI/Avalonia/issues/3538) we have planned and what our [past breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) have been. [Awesome Avalonia](https://github.com/AvaloniaCommunity/awesome-avalonia) is community-curated list of awesome Avalonia UI tools, libraries, projects and resources. Go and see what people are building with Avalonia!
## 🚀 Getting Started
diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj
index 5b82e2caee..20ca0576d4 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
@@ -71,21 +71,23 @@
+
-
- Designer
-
+
-
+
+
-
+
+ Resources\drawable\Icon.png
+
diff --git a/samples/ControlCatalog.Android/MainActivity.cs b/samples/ControlCatalog.Android/MainActivity.cs
index 40d001a195..2ab03551b6 100644
--- a/samples/ControlCatalog.Android/MainActivity.cs
+++ b/samples/ControlCatalog.Android/MainActivity.cs
@@ -1,31 +1,18 @@
-using System;
-using Android.App;
+using Android.App;
using Android.OS;
using Android.Content.PM;
using Avalonia.Android;
-using Avalonia.Controls;
-using Avalonia.Controls.Templates;
-using Avalonia.Markup.Xaml;
-using Avalonia.Media;
-using Avalonia.Styling;
-using Avalonia.Themes.Default;
-using Avalonia;
namespace ControlCatalog.Android
{
- [Activity(Label = "ControlCatalog.Android", MainLauncher = true, Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleInstance)]
+ [Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleInstance)]
public class MainActivity : AvaloniaActivity
{
protected override void OnCreate(Bundle savedInstanceState)
{
- if (Avalonia.Application.Current == null)
- {
- AppBuilder.Configure()
- .UseAndroid()
- .SetupWithoutStarting();
- Content = new MainView();
- }
base.OnCreate(savedInstanceState);
+
+ Content = new MainView();
}
}
}
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..b1ca548e2c 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
@@ -43,69 +40,59 @@ namespace ControlCatalog.Android
}
}
- public partial class Drawable
+ public partial class Color
{
- // aapt resource value: 0x7f020000
- public const int Icon = 2130837504;
+ // aapt resource value: 0x7F010000
+ public const int splash_background = 2130771968;
- static Drawable()
+ static Color()
{
global::Android.Runtime.ResourceIdManager.UpdateIdValues();
}
- private Drawable()
+ private Color()
{
}
}
- public partial class Id
+ public partial class Drawable
{
- // aapt resource value: 0x7f050000
- public const int MyButton = 2131034112;
-
- static Id()
- {
- global::Android.Runtime.ResourceIdManager.UpdateIdValues();
- }
-
- private Id()
- {
- }
- }
-
- public partial class Layout
- {
+ // aapt resource value: 0x7F020000
+ public const int Icon = 2130837504;
- // aapt resource value: 0x7f030000
- public const int Main = 2130903040;
+ // aapt resource value: 0x7F020001
+ public const int splash_screen = 2130837505;
- static Layout()
+ static Drawable()
{
global::Android.Runtime.ResourceIdManager.UpdateIdValues();
}
- private Layout()
+ private Drawable()
{
}
}
- public partial class String
+ public partial class Style
{
- // aapt resource value: 0x7f040001
- public const int ApplicationName = 2130968577;
+ // aapt resource value: 0x7F030000
+ public const int MyTheme = 2130903040;
+
+ // aapt resource value: 0x7F030001
+ public const int MyTheme_NoActionBar = 2130903041;
- // aapt resource value: 0x7f040000
- public const int Hello = 2130968576;
+ // aapt resource value: 0x7F030002
+ public const int MyTheme_Splash = 2130903042;
- static String()
+ static Style()
{
global::Android.Runtime.ResourceIdManager.UpdateIdValues();
}
- private String()
+ private Style()
{
}
}
diff --git a/samples/ControlCatalog.Android/Resources/drawable/Icon.png b/samples/ControlCatalog.Android/Resources/drawable/Icon.png
deleted file mode 100644
index 8074c4c571..0000000000
Binary files a/samples/ControlCatalog.Android/Resources/drawable/Icon.png and /dev/null differ
diff --git a/samples/ControlCatalog.Android/Resources/drawable/splash_screen.xml b/samples/ControlCatalog.Android/Resources/drawable/splash_screen.xml
new file mode 100644
index 0000000000..2e920b4b3b
--- /dev/null
+++ b/samples/ControlCatalog.Android/Resources/drawable/splash_screen.xml
@@ -0,0 +1,13 @@
+
+
+
+ -
+
+
+
+
+
+
diff --git a/samples/ControlCatalog.Android/Resources/layout/Main.axml b/samples/ControlCatalog.Android/Resources/layout/Main.axml
deleted file mode 100644
index 570c96ad72..0000000000
--- a/samples/ControlCatalog.Android/Resources/layout/Main.axml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
diff --git a/samples/ControlCatalog.Android/Resources/values/Strings.xml b/samples/ControlCatalog.Android/Resources/values/Strings.xml
deleted file mode 100644
index 95221a08a9..0000000000
--- a/samples/ControlCatalog.Android/Resources/values/Strings.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
- Hello World, Click Me!
- ControlCatalog.Android
-
diff --git a/samples/ControlCatalog.Android/Resources/values/colors.xml b/samples/ControlCatalog.Android/Resources/values/colors.xml
new file mode 100644
index 0000000000..59279d5d32
--- /dev/null
+++ b/samples/ControlCatalog.Android/Resources/values/colors.xml
@@ -0,0 +1,4 @@
+
+
+ #FFFFFF
+
diff --git a/samples/ControlCatalog.Android/Resources/values/styles.xml b/samples/ControlCatalog.Android/Resources/values/styles.xml
new file mode 100644
index 0000000000..e017b6facf
--- /dev/null
+++ b/samples/ControlCatalog.Android/Resources/values/styles.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog.Android/SplashActivity.cs b/samples/ControlCatalog.Android/SplashActivity.cs
new file mode 100644
index 0000000000..6d7c6bc116
--- /dev/null
+++ b/samples/ControlCatalog.Android/SplashActivity.cs
@@ -0,0 +1,32 @@
+using Android.App;
+using Android.Content;
+using Android.OS;
+using Application = Android.App.Application;
+
+using Avalonia;
+
+namespace ControlCatalog.Android
+{
+ [Activity(Theme = "@style/MyTheme.Splash", MainLauncher = true, NoHistory = true)]
+ public class SplashActivity : Activity
+ {
+ protected override void OnCreate(Bundle savedInstanceState)
+ {
+ base.OnCreate(savedInstanceState);
+ }
+
+ protected override void OnResume()
+ {
+ base.OnResume();
+
+ if (Avalonia.Application.Current == null)
+ {
+ AppBuilder.Configure()
+ .UseAndroid()
+ .SetupWithoutStarting();
+ }
+
+ StartActivity(new Intent(Application.Context, typeof(MainActivity)));
+ }
+ }
+}
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/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs
index 2e6f4a67c3..e0ceb0c8b7 100644
--- a/src/Android/Avalonia.Android/AndroidPlatform.cs
+++ b/src/Android/Avalonia.Android/AndroidPlatform.cs
@@ -1,11 +1,13 @@
using System;
+
+using Avalonia.Android;
using Avalonia.Android.Platform;
using Avalonia.Android.Platform.Input;
-using Avalonia.Android.Platform.SkiaPlatform;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Input.Platform;
+using Avalonia.OpenGL.Egl;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Shared.PlatformSupport;
@@ -17,7 +19,8 @@ namespace Avalonia
{
public static T UseAndroid(this T builder) where T : AppBuilderBase, new()
{
- builder.UseWindowingSubsystem(() => Android.AndroidPlatform.Initialize(builder.ApplicationType), "Android");
+ var options = AvaloniaLocator.Current.GetService() ?? new AndroidPlatformOptions();
+ builder.UseWindowingSubsystem(() => AndroidPlatform.Initialize(builder.ApplicationType, options), "Android");
builder.UseSkia();
return builder;
}
@@ -41,7 +44,7 @@ namespace Avalonia.Android
_scalingFactor = global::Android.App.Application.Context.Resources.DisplayMetrics.ScaledDensity;
}
- public static void Initialize(Type appType)
+ public static void Initialize(Type appType, AndroidPlatformOptions options)
{
AvaloniaLocator.CurrentMutable
.Bind().ToTransient()
@@ -60,6 +63,11 @@ namespace Avalonia.Android
SkiaPlatform.Initialize();
((global::Android.App.Application) global::Android.App.Application.Context.ApplicationContext)
.RegisterActivityLifecycleCallbacks(new ActivityTracker());
+
+ if (options.UseGpu)
+ {
+ EglPlatformOpenGlInterface.TryInitialize();
+ }
}
public IWindowImpl CreateWindow()
@@ -72,4 +80,9 @@ namespace Avalonia.Android
throw new NotSupportedException();
}
}
+
+ public sealed class AndroidPlatformOptions
+ {
+ public bool UseGpu { get; set; } = true;
+ }
}
diff --git a/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs b/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs
new file mode 100644
index 0000000000..4f4c03fe77
--- /dev/null
+++ b/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs
@@ -0,0 +1,32 @@
+using System.Linq;
+
+using Avalonia.OpenGL.Egl;
+using Avalonia.OpenGL.Surfaces;
+
+namespace Avalonia.Android.OpenGL
+{
+ internal sealed class GlPlatformSurface : EglGlPlatformSurfaceBase
+ {
+ private readonly EglPlatformOpenGlInterface _egl;
+ private readonly IEglWindowGlPlatformSurfaceInfo _info;
+
+ private GlPlatformSurface(EglPlatformOpenGlInterface egl, IEglWindowGlPlatformSurfaceInfo info)
+ {
+ _egl = egl;
+ _info = info;
+ }
+
+ public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() =>
+ new GlRenderTarget(_egl, _info, _egl.CreateWindowSurface(_info.Handle));
+
+ public static GlPlatformSurface TryCreate(IEglWindowGlPlatformSurfaceInfo info)
+ {
+ if (EglPlatformOpenGlInterface.TryCreate() is EglPlatformOpenGlInterface egl)
+ {
+ return new GlPlatformSurface(egl, info);
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs b/src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs
new file mode 100644
index 0000000000..75bbd15e3e
--- /dev/null
+++ b/src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs
@@ -0,0 +1,23 @@
+using Avalonia.OpenGL.Egl;
+using Avalonia.OpenGL.Surfaces;
+
+namespace Avalonia.Android.OpenGL
+{
+ internal sealed class GlRenderTarget : EglPlatformSurfaceRenderTargetBase
+ {
+ private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info;
+ private readonly EglSurface _surface;
+
+ public GlRenderTarget(
+ EglPlatformOpenGlInterface egl,
+ EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info,
+ EglSurface surface)
+ : base(egl)
+ {
+ _info = info;
+ _surface = surface;
+ }
+
+ public override IGlPlatformSurfaceRenderingSession BeginDraw() => BeginDraw(_surface, _info);
+ }
+}
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/FramebufferManager.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs
new file mode 100644
index 0000000000..18c4796fae
--- /dev/null
+++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs
@@ -0,0 +1,17 @@
+using Avalonia.Controls.Platform.Surfaces;
+using Avalonia.Platform;
+
+namespace Avalonia.Android.Platform.SkiaPlatform
+{
+ internal sealed class FramebufferManager : IFramebufferPlatformSurface
+ {
+ private readonly TopLevelImpl _topLevel;
+
+ public FramebufferManager(TopLevelImpl topLevel)
+ {
+ _topLevel = topLevel;
+ }
+
+ public ILockedFramebuffer Lock() => new AndroidFramebuffer(_topLevel.InternalView.Holder.Surface);
+ }
+}
diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
index 71dce93ce7..a8c7f7af9b 100644
--- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
+++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
@@ -2,20 +2,29 @@ using System;
using System.Collections.Generic;
using Android.Content;
using Android.Graphics;
+using Android.Runtime;
using Android.Views;
+
+using Avalonia.Android.OpenGL;
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;
+using Avalonia.OpenGL.Egl;
+using Avalonia.OpenGL.Surfaces;
using Avalonia.Platform;
using Avalonia.Rendering;
namespace Avalonia.Android.Platform.SkiaPlatform
{
- class TopLevelImpl : IAndroidView, ITopLevelImpl, IFramebufferPlatformSurface
+ class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo
{
+ private readonly IGlPlatformSurface _gl;
+ private readonly IFramebufferPlatformSurface _framebuffer;
+
private readonly AndroidKeyboardEventsHelper _keyboardHelper;
private readonly AndroidTouchEventsHelper _touchHelper;
@@ -28,7 +37,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_touchHelper = new AndroidTouchEventsHelper(this, () => InputRoot,
p => GetAvaloniaPointFromEvent(p));
- Surfaces = new object[] { this };
+ _gl = GlPlatformSurface.TryCreate(this);
+ _framebuffer = new FramebufferManager(this);
MaxClientSize = new Size(_view.Resources.DisplayMetrics.WidthPixels,
_view.Resources.DisplayMetrics.HeightPixels);
@@ -47,7 +57,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_keyboardHelper.HandleEvents = _handleEvents;
}
}
-
+
public virtual Point GetAvaloniaPointFromEvent(MotionEvent e) => new Point(e.GetX(), e.GetY());
public IInputRoot InputRoot { get; private set; }
@@ -62,7 +72,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
}
set
{
-
+
}
}
@@ -82,9 +92,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public View View => _view;
+ internal InvalidationAwareSurfaceView InternalView => _view;
+
public IPlatformHandle Handle => _view;
- public IEnumerable
@@ -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.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj
index 9bfd387ca9..0470dbfe89 100644
--- a/src/Avalonia.Base/Avalonia.Base.csproj
+++ b/src/Avalonia.Base/Avalonia.Base.csproj
@@ -5,9 +5,6 @@
Avalonia
True
-
-
-
diff --git a/src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs b/src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs
index 7e7417d2f5..7ff0a8ceca 100644
--- a/src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs
+++ b/src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs
@@ -63,7 +63,7 @@ namespace Avalonia.Data.Converters
}
}
- void OnPropertyChanged(object sender,PropertyChangedEventArgs args)
+ void OnPropertyChanged(object sender, PropertyChangedEventArgs args)
{
if (string.IsNullOrWhiteSpace(args.PropertyName)
|| dependencyProperties?.Contains(args.PropertyName) == true)
@@ -88,12 +88,7 @@ namespace Avalonia.Data.Converters
var parameter = Expression.Parameter(typeof(object), "parameter");
- var instance = Expression.Convert
- (
- Expression.Constant(target),
- method.DeclaringType
- );
-
+ var instance = ConvertTarget(target, method);
var call = Expression.Call
(
@@ -114,11 +109,7 @@ namespace Avalonia.Data.Converters
var parameter = Expression.Parameter(typeof(object), "parameter");
- var instance = Expression.Convert
- (
- Expression.Constant(target),
- method.DeclaringType
- );
+ var instance = ConvertTarget(target, method);
Expression body;
@@ -167,11 +158,7 @@ namespace Avalonia.Data.Converters
, System.Reflection.MethodInfo method)
{
var parameter = Expression.Parameter(typeof(object), "parameter");
- var instance = Expression.Convert
- (
- Expression.Constant(target),
- method.DeclaringType
- );
+ var instance = ConvertTarget(target, method);
var call = Expression.Call
(
instance,
@@ -183,6 +170,8 @@ namespace Avalonia.Data.Converters
.Compile();
}
+ private static Expression? ConvertTarget(object? target, MethodInfo method) =>
+ target is null ? null : Expression.Convert(Expression.Constant(target), method.DeclaringType);
internal class WeakPropertyChangedProxy
{
@@ -224,7 +213,7 @@ namespace Avalonia.Data.Converters
else
Unsubscribe();
}
-
+
}
}
}
diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
index 3bf6842cd6..d600603d5c 100644
--- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
+++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
@@ -14,7 +14,7 @@ namespace Avalonia.Data.Core.Plugins
{
private readonly Dictionary<(Type, string), PropertyInfo> _propertyLookup =
new Dictionary<(Type, string), PropertyInfo>();
-
+
///
public bool Match(object obj, string propertyName) => GetFirstPropertyWithName(obj.GetType(), propertyName) != null;
@@ -51,7 +51,7 @@ namespace Avalonia.Data.Core.Plugins
private PropertyInfo GetFirstPropertyWithName(Type type, string propertyName)
{
var key = (type, propertyName);
-
+
if (!_propertyLookup.TryGetValue(key, out PropertyInfo propertyInfo))
{
propertyInfo = TryFindAndCacheProperty(type, propertyName);
@@ -59,7 +59,7 @@ namespace Avalonia.Data.Core.Plugins
return propertyInfo;
}
-
+
private PropertyInfo TryFindAndCacheProperty(Type type, string propertyName)
{
PropertyInfo found = null;
@@ -90,7 +90,7 @@ namespace Avalonia.Data.Core.Plugins
private readonly PropertyInfo _property;
private bool _eventRaised;
- public Accessor(WeakReference reference, PropertyInfo property)
+ public Accessor(WeakReference reference, PropertyInfo property)
{
Contract.Requires(reference != null);
Contract.Requires(property != null);
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/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
index 5cbd3698b6..e3e9e84d7e 100644
--- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
+++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
@@ -355,7 +355,13 @@ namespace Avalonia.Controls.Platform
}
else if (!item.IsPointerOverSubMenu)
{
- item.IsSubMenuOpen = false;
+ DelayRun(() =>
+ {
+ if (!item.IsPointerOverSubMenu)
+ {
+ item.IsSubMenuOpen = false;
+ }
+ }, MenuShowDelay);
}
}
}
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/Primitives/LightDismissOverlayLayer.cs b/src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs
index 752eedb68a..1d655bb691 100644
--- a/src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs
+++ b/src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs
@@ -2,6 +2,7 @@ using System;
using System.Linq;
using Avalonia.Controls.Templates;
using Avalonia.Input;
+using Avalonia.Media;
using Avalonia.Rendering;
using Avalonia.Styling;
using Avalonia.VisualTree;
@@ -17,6 +18,11 @@ namespace Avalonia.Controls.Primitives
{
public IInputElement? InputPassThroughElement { get; set; }
+ static LightDismissOverlayLayer()
+ {
+ BackgroundProperty.OverrideDefaultValue(Brushes.Transparent);
+ }
+
///
/// Returns the light dismiss overlay for a specified visual.
///
diff --git a/src/Avalonia.Controls/Primitives/VisualLayerManager.cs b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs
index d8d3450c6f..e5e27c169a 100644
--- a/src/Avalonia.Controls/Primitives/VisualLayerManager.cs
+++ b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs
@@ -74,7 +74,6 @@ namespace Avalonia.Controls.Primitives
{
rv = new LightDismissOverlayLayer
{
- Background = Brushes.Transparent,
IsVisible = false
};
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.Dialogs/AboutAvaloniaDialog.xaml b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml
index e51cc2f3ce..08e8798cd5 100644
--- a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml
+++ b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml
@@ -79,8 +79,8 @@
-
-
+
+
@@ -99,7 +99,7 @@
-
+
-
\ No newline at end of file
+
diff --git a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs
index 09fab0ed3f..55e30396e1 100644
--- a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs
+++ b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs
@@ -1,3 +1,4 @@
+using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Avalonia.Controls;
@@ -7,12 +8,18 @@ namespace Avalonia.Dialogs
{
public class AboutAvaloniaDialog : Window
{
+ private static readonly Version s_version = typeof(AboutAvaloniaDialog).Assembly.GetName().Version;
+
+ public static string Version { get; } = s_version.ToString(2);
+
+ public static bool IsDevelopmentBuild { get; } = s_version.Revision == 999;
+
public AboutAvaloniaDialog()
{
AvaloniaXamlLoader.Load(this);
DataContext = this;
}
-
+
public static void OpenBrowser(string url)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
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.OpenGL/Egl/EglInterface.cs b/src/Avalonia.OpenGL/Egl/EglInterface.cs
index 8055226042..cadd7cc1f2 100644
--- a/src/Avalonia.OpenGL/Egl/EglInterface.cs
+++ b/src/Avalonia.OpenGL/Egl/EglInterface.cs
@@ -30,8 +30,10 @@ namespace Avalonia.OpenGL.Egl
static Func Load()
{
var os = AvaloniaLocator.Current.GetService().GetRuntimeInfo().OperatingSystem;
- if(os == OperatingSystemType.Linux || os == OperatingSystemType.Android)
+ if(os == OperatingSystemType.Linux)
return Load("libEGL.so.1");
+ if (os == OperatingSystemType.Android)
+ return Load("libEGL.so");
throw new PlatformNotSupportedException();
}
diff --git a/src/Avalonia.OpenGL/GlInterface.cs b/src/Avalonia.OpenGL/GlInterface.cs
index 28b62136da..cae245732f 100644
--- a/src/Avalonia.OpenGL/GlInterface.cs
+++ b/src/Avalonia.OpenGL/GlInterface.cs
@@ -128,7 +128,7 @@ namespace Avalonia.OpenGL
int dstY1,
int mask,
int filter);
- [GlMinVersionEntryPoint("glBlitFramebuffer", 3, 0)]
+ [GlMinVersionEntryPoint("glBlitFramebuffer", 3, 0), GlOptionalEntryPoint]
public GlBlitFramebuffer BlitFramebuffer { get; }
public delegate void GlGenRenderbuffers(int count, int[] res);
diff --git a/src/Avalonia.Themes.Default/CaptionButtons.xaml b/src/Avalonia.Themes.Default/CaptionButtons.xaml
index 95967547c6..cf469eeac5 100644
--- a/src/Avalonia.Themes.Default/CaptionButtons.xaml
+++ b/src/Avalonia.Themes.Default/CaptionButtons.xaml
@@ -57,7 +57,7 @@
-
-