Browse Source

Merge branch 'master' of https://github.com/AvaloniaUI/Avalonia

release/0.10.0-rc2
Dan Walmsley 5 years ago
parent
commit
08d38bd110
  1. 3
      Avalonia.sln.DotSettings
  2. 2
      build/AndroidWorkarounds.props
  3. 4
      build/HarfBuzzSharp.props
  4. 23
      build/SourceLink.props
  5. 12
      packages/Avalonia/Avalonia.csproj
  6. 4
      samples/ControlCatalog.Android/ControlCatalog.Android.csproj
  7. 2
      samples/ControlCatalog.Android/Properties/AndroidManifest.xml
  8. 23
      samples/ControlCatalog.Android/Resources/Resource.Designer.cs
  9. 3
      samples/ControlCatalog.NetCore/Program.cs
  10. BIN
      samples/ControlCatalog/Assets/Fonts/WenQuanYiMicroHei-01.ttf
  11. 4
      samples/ControlCatalog/Pages/DataGridPage.xaml
  12. 14
      samples/ControlCatalog/Pages/SliderPage.xaml
  13. 3
      samples/ControlCatalog/Pages/TextBoxPage.xaml
  14. 27
      samples/ControlCatalog/Pages/ToolTipPage.xaml
  15. 4
      src/Android/Avalonia.Android/Platform/ClipboardImpl.cs
  16. 13
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  17. 11
      src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs
  18. 60
      src/Android/Avalonia.Android/Resources/Resource.Designer.cs
  19. 5
      src/Android/Avalonia.Android/SystemDialogImpl.cs
  20. 4
      src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj
  21. 4
      src/Android/Avalonia.AndroidTestApplication/MainActivity.cs
  22. 2
      src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml
  23. 17
      src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs
  24. 3
      src/Avalonia.Base/Avalonia.Base.csproj
  25. 8
      src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
  26. 5
      src/Avalonia.Base/Data/Core/SettableNode.cs
  27. 18
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  28. 35
      src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs
  29. 32
      src/Avalonia.Controls/Calendar/CalendarDatePicker.cs
  30. 33
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
  31. 8
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  32. 11
      src/Avalonia.Controls/Platform/ITopLevelImplWithTextInputMethod.cs
  33. 25
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  34. 7
      src/Avalonia.Controls/TextBox.cs
  35. 41
      src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
  36. 40
      src/Avalonia.Controls/ToolTip.cs
  37. 10
      src/Avalonia.Controls/ToolTipService.cs
  38. 6
      src/Avalonia.Controls/TopLevel.cs
  39. 110
      src/Avalonia.FreeDesktop/DBusCallQueue.cs
  40. 11
      src/Avalonia.FreeDesktop/DBusHelper.cs
  41. 288
      src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs
  42. 69
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs
  43. 67
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs
  44. 51
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs
  45. 149
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs
  46. 52
      src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs
  47. 45
      src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs
  48. 105
      src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs
  49. 53
      src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs
  50. 31
      src/Avalonia.FreeDesktop/IX11InputMethod.cs
  51. 35
      src/Avalonia.Input/InputElement.cs
  52. 6
      src/Avalonia.Input/KeyboardDevice.cs
  53. 60
      src/Avalonia.Input/TextInput/ITextInputMethodClient.cs
  54. 15
      src/Avalonia.Input/TextInput/ITextInputMethodImpl.cs
  55. 101
      src/Avalonia.Input/TextInput/InputMethodManager.cs
  56. 12
      src/Avalonia.Input/TextInput/TextInputContentType.cs
  57. 12
      src/Avalonia.Input/TextInput/TextInputMethodClientRequestedEventArgs.cs
  58. 32
      src/Avalonia.Input/TextInput/TextInputOptionsQueryEventArgs.cs
  59. 109
      src/Avalonia.Input/TextInput/TransformTrackingHelper.cs
  60. 2
      src/Avalonia.Themes.Default/CalendarDatePicker.xaml
  61. 4
      src/Avalonia.Themes.Default/NumericUpDown.xaml
  62. 3
      src/Avalonia.Themes.Fluent/Controls/Button.xaml
  63. 10
      src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml
  64. 4
      src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml
  65. 6
      src/Avalonia.Themes.Fluent/Controls/FocusAdorner.xaml
  66. 2
      src/Avalonia.Themes.Fluent/Controls/ListBoxItem.xaml
  67. 2
      src/Avalonia.Themes.Fluent/Controls/Menu.xaml
  68. 6
      src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
  69. 7
      src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml
  70. 2
      src/Avalonia.Themes.Fluent/Controls/TabControl.xaml
  71. 10
      src/Avalonia.Themes.Fluent/Controls/TabItem.xaml
  72. 2
      src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml
  73. 4
      src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml
  74. 2
      src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml
  75. 4
      src/Avalonia.X11/X11Clipboard.cs
  76. 4
      src/Avalonia.X11/X11Globals.cs
  77. 25
      src/Avalonia.X11/X11Info.cs
  78. 72
      src/Avalonia.X11/X11Platform.cs
  79. 11
      src/Avalonia.X11/X11PlatformThreading.cs
  80. 2
      src/Avalonia.X11/X11Screens.cs
  81. 130
      src/Avalonia.X11/X11Structs.cs
  82. 208
      src/Avalonia.X11/X11Window.Ime.cs
  83. 121
      src/Avalonia.X11/X11Window.Xim.cs
  84. 80
      src/Avalonia.X11/X11Window.cs
  85. 66
      src/Avalonia.X11/XLib.cs
  86. 5
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs
  87. 11
      src/Skia/Avalonia.Skia/FontManagerImpl.cs
  88. 8
      src/Windows/Avalonia.Win32/Win32GlManager.cs
  89. 20
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs
  90. 2
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs
  91. 2
      tests/Avalonia.Markup.Xaml.UnitTests/StyleIncludeTests.cs

3
Avalonia.sln.DotSettings

@ -38,4 +38,5 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=TypeParameters/@EntryIndexedValue">&lt;Policy Inspect="False" Prefix="T" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=TypesAndNamespaces/@EntryIndexedValue">&lt;Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EFeature_002EServices_002EDaemon_002ESettings_002EMigration_002ESwaWarningsModeSettingsMigrate/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Avalonia/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Avalonia/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Fcitx/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

2
build/AndroidWorkarounds.props

@ -2,7 +2,7 @@
<ItemGroup Condition="'$(AndroidApplication)' == 'true'">
<!-- WORKAROUND: The packages below are transitively referenced by System.Memory,
but Xamarin.Android applications need the newest versions directly referenced for the linker. -->
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.5.1" />
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.6.0" />
<PackageReference Include="System.Buffers" Version="4.5.0" />
</ItemGroup>
<Target Name="_RemoveNonExistingResgenFile" BeforeTargets="CoreCompile" Condition="'$(_SdkSetAndroidResgenFile)' == 'true' And '$(AndroidResgenFile)' != '' And !Exists('$(AndroidResgenFile)')">

4
build/HarfBuzzSharp.props

@ -1,6 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="HarfBuzzSharp" Version="2.6.1.6" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.6.1.6" />
<PackageReference Include="HarfBuzzSharp" Version="2.6.1.7" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.6.1.7" />
</ItemGroup>
</Project>

23
build/SourceLink.props

@ -1,5 +1,26 @@
<Project>
<PropertyGroup>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>false</IncludeSymbols>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<DebugType>embedded</DebugType>
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
</PropertyGroup>
<PropertyGroup Condition="'$(TF_BUILD)' == 'true'">
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SourceLink.Create.CommandLine" Version="2.8.3" PrivateAssets="All" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
</ItemGroup>
<!-- Workaround for https://github.com/dotnet/sdk/issues/11105 -->
<ItemGroup>
<SourceRoot Include="$(NuGetPackageRoot)" Condition="'$(NuGetPackageRoot)' != ''" />
</ItemGroup>
</Project>

12
packages/Avalonia/Avalonia.csproj

@ -6,7 +6,9 @@
<ItemGroup>
<ProjectReference Include="../../src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj" />
<ProjectReference Include="../../src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj" />
<ProjectReference Include="../../src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj" >
<PrivateAssets>all</PrivateAssets>
</ProjectReference>
</ItemGroup>
<PropertyGroup>
@ -29,21 +31,23 @@
</_PackageFiles>
</ItemGroup>
</Target>
<ItemGroup>
<Content Include="*.props">
<Pack>true</Pack>
<PackagePath>build\</PackagePath>
<PackagePath>build\;buildTransitive\</PackagePath>
</Content>
<Content Include="*.targets">
<Pack>true</Pack>
<PackagePath>build\</PackagePath>
<PackagePath>build\;buildTransitive\</PackagePath>
</Content>
<Content Include="AvaloniaItemSchema.xaml">
<Pack>true</Pack>
<PackagePath>build\</PackagePath>
<PackagePath>build\;buildTransitive\</PackagePath>
</Content>
</ItemGroup>
<Import Project="..\..\build\SharedVersion.props" />
<Import Project="..\..\build\NetFX.props" />
<Import Project="..\..\build\CoreLibraries.props" />
<Import Project="..\..\build\SourceLink.props" Condition="'$(DisableSourceLink)' == ''" />
</Project>

4
samples/ControlCatalog.Android/ControlCatalog.Android.csproj

@ -16,7 +16,7 @@
<AndroidResgenFile>Resources\Resource.Designer.cs</AndroidResgenFile>
<GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
<AndroidUseLatestPlatformSdk>False</AndroidUseLatestPlatformSdk>
<TargetFrameworkVersion>v9.0</TargetFrameworkVersion>
<TargetFrameworkVersion>v10.0</TargetFrameworkVersion>
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
@ -156,4 +156,4 @@
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
<Import Project="..\..\build\AndroidWorkarounds.props" />
<Import Project="..\..\build\LegacyProject.targets" />
</Project>
</Project>

2
samples/ControlCatalog.Android/Properties/AndroidManifest.xml

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="ControlCatalog.Android" android:versionCode="1" android:versionName="1.0" android:installLocation="auto">
<uses-sdk />
<uses-sdk android:targetSdkVersion="29" />
<application android:label="ControlCatalog.Android"></application>
</manifest>

23
samples/ControlCatalog.Android/Resources/Resource.Designer.cs

@ -2,7 +2,6 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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()
{

3
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
{

BIN
samples/ControlCatalog/Assets/Fonts/WenQuanYiMicroHei-01.ttf

Binary file not shown.

4
samples/ControlCatalog/Pages/DataGridPage.xaml

@ -38,8 +38,8 @@
<DataGrid.Columns>
<DataGridTextColumn Header="Country" Binding="{Binding Name}" Width="6*" />
<DataGridTextColumn Header="Region" Binding="{Binding Region}" Width="4*" />
<DataGridTextColumn Header="Population" Binding="{Binding Population}" Width="3*" />
<DataGridTextColumn Header="Area" Binding="{Binding Area}" Width="3*" />
<DataGridTextColumn DisplayIndex="3" Header="Population" Binding="{Binding Population}" Width="3*" />
<DataGridTextColumn DisplayIndex="2" Header="Area" Binding="{Binding Area}" Width="3*" />
<DataGridTextColumn Header="GDP" Binding="{Binding GDP}" Width="3*" />
</DataGrid.Columns>
</DataGrid>

14
samples/ControlCatalog/Pages/SliderPage.xaml

@ -22,6 +22,20 @@
IsSnapToTickEnabled="True"
Ticks="0,20,25,40,75,100"
Width="300" />
<Slider Name="SliderWithTooltip"
Value="0"
Minimum="0"
Maximum="100"
Width="300">
<Slider.Styles>
<Style Selector="Slider /template/ Thumb">
<Setter Property="ToolTip.Tip" Value="{Binding $parent[Slider].Value, Mode=OneWay, StringFormat='Value \{0:f\}'}" />
<Setter Property="ToolTip.Placement" Value="Top" />
<Setter Property="ToolTip.VerticalOffset" Value="-10" />
<Setter Property="ToolTip.HorizontalOffset" Value="-30" />
</Style>
</Slider.Styles>
</Slider>
<Slider Value="0"
Minimum="0"
Maximum="100"

3
samples/ControlCatalog/Pages/TextBoxPage.xaml

@ -64,5 +64,8 @@
<TextBox Width="200" Text="Custom font italic bold" FontWeight="Bold" FontStyle="Italic" FontFamily="/Assets/Fonts/SourceSansPro-*.ttf#Source Sans Pro"/>
</StackPanel>
</StackPanel>
<TextBox AcceptsReturn="True" TextWrapping="Wrap" Height="200" MaxWidth="400"
FontFamily="avares://ControlCatalog/Assets/Fonts#WenQuanYi Micro Hei"
Text="计算机科学(是系统性研究信息与计算的理论基础以及它们在计算机系统中如何实现与应用的实用技术的学科。它通常被形容为对那些创造、描述以及转换信息的算法处理的系统研究。计算机科学包含很多分支领域;有些强调特定结果的计算,比如计算机图形学;而有些是探討计算问题的性质,比如计算复杂性理论;还有一些领域專注于怎样实现计算,比如程式語言理論是研究描述计算的方法,而程式设计是应用特定的程式語言解决特定的计算问题,人机交互则是專注于怎样使计算机和计算变得有用、好用,以及随时随地为人所用。&#xD;&#xD;有时公众会误以为计算机科学就是解决计算机问题的事业(比如信息技术),或者只是与使用计算机的经验有关,如玩游戏、上网或者文字处理。其实计算机科学所关注的,不仅仅是去理解实现类似游戏、浏览器这些软件的程序的性质,更要通过现有的知识创造新的程序或者改进已有的程序。" />
</StackPanel>
</UserControl>

27
samples/ControlCatalog/Pages/ToolTipPage.xaml

@ -6,7 +6,7 @@
<TextBlock Classes="h1">ToolTip</TextBlock>
<TextBlock Classes="h2">A control which pops up a hint when a control is hovered</TextBlock>
<Grid RowDefinitions="Auto,Auto"
<Grid RowDefinitions="Auto,Auto,Auto"
ColumnDefinitions="Auto,Auto"
Margin="0,16,0,0"
HorizontalAlignment="Center">
@ -38,6 +38,31 @@
</ToolTip.Tip>
<TextBlock>ToolTip bottom placement</TextBlock>
</Border>
<Border Grid.Row="2"
Grid.ColumnSpan="2"
Background="{DynamicResource SystemAccentColor}"
Margin="5"
Padding="50"
ToolTip.Tip="Hello"
ToolTip.Placement="Top">
<Border.Styles>
<Style Selector="Border">
<Style.Animations>
<Animation Duration="0:0:2" IterationCount="Infinite">
<KeyFrame KeyTime="0:0:0">
<Setter Property="ToolTip.HorizontalOffset" Value="0" />
<Setter Property="ToolTip.VerticalOffset" Value="-50" />
</KeyFrame>
<KeyFrame KeyTime="0:0:2" >
<Setter Property="ToolTip.HorizontalOffset" Value="100" />
<Setter Property="ToolTip.VerticalOffset" Value="50" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</Border.Styles>
<TextBlock>Moving offset</TextBlock>
</Border>
</Grid>
</StackPanel>
</UserControl>

4
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;

13
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<WindowTransparencyLevel> 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();
}
}
}

11
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<TView> : IDisposable where TView :ITopLevelImpl, IAndroidView
internal class AndroidKeyboardEventsHelper<TView> : 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);

60
src/Android/Avalonia.Android/Resources/Resource.Designer.cs

@ -1,60 +0,0 @@
#pragma warning disable 1591
//------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
//------------------------------------------------------------------------------
[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

5
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<string[]> ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent)
public Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent)
{
throw new NotImplementedException();
}
public Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent)
public Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent)
{
throw new NotImplementedException();
}

4
src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj

@ -16,7 +16,7 @@
<AndroidResgenFile>Resources\Resource.Designer.cs</AndroidResgenFile>
<GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
<AndroidUseLatestPlatformSdk>False</AndroidUseLatestPlatformSdk>
<TargetFrameworkVersion>v9.0</TargetFrameworkVersion>
<TargetFrameworkVersion>v10.0</TargetFrameworkVersion>
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
@ -150,4 +150,4 @@
<Import Project="..\..\..\build\System.Memory.props" />
<Import Project="..\..\..\build\AndroidWorkarounds.props" />
<Import Project="..\..\..\build\LegacyProject.targets" />
</Project>
</Project>

4
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);

2
src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="Avalonia.AndroidTestApplication" android:versionCode="1" android:versionName="1.0" android:installLocation="auto">
<uses-sdk />
<uses-sdk android:targetSdkVersion="29" />
<application android:label="Avalonia.AndroidTestApplication" android:icon="@drawable/Icon" android:hardwareAccelerated="true"></application>
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

17
src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs

@ -2,7 +2,6 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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()
{

3
src/Avalonia.Base/Avalonia.Base.csproj

@ -5,9 +5,6 @@
<RootNamespace>Avalonia</RootNamespace>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Build.Tasks\Avalonia.Build.Tasks.csproj"/>
</ItemGroup>
<Import Project="..\..\build\Base.props" />
<Import Project="..\..\build\Binding.props" />
<Import Project="..\..\build\Rx.props" />

8
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>();
/// <inheritdoc/>
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<object> reference, PropertyInfo property)
public Accessor(WeakReference<object> reference, PropertyInfo property)
{
Contract.Requires<ArgumentNullException>(reference != null);
Contract.Requires<ArgumentNullException>(property != null);

5
src/Avalonia.Base/Data/Core/SettableNode.cs

@ -15,7 +15,8 @@ namespace Avalonia.Data.Core
private bool ShouldNotSet(object value)
{
if (PropertyType == null)
var propertyType = PropertyType;
if (propertyType == null)
{
return false;
}
@ -37,7 +38,7 @@ namespace Avalonia.Data.Core
return false;
}
if (PropertyType.IsValueType)
if (propertyType.IsValueType)
{
return lastValue.Equals(value);
}

18
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -2285,6 +2285,17 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Comparator class so we can sort list by the display index
/// </summary>
public class DisplayIndexComparer : IComparer<DataGridColumn>
{
int IComparer<DataGridColumn>.Compare(DataGridColumn x, DataGridColumn y)
{
return (x.DisplayIndexWithFiller < y.DisplayIndexWithFiller) ? -1 : 1;
}
}
/// <summary>
/// Builds the visual tree for the column header when a new template is applied.
/// </summary>
@ -2309,8 +2320,11 @@ namespace Avalonia.Controls
ColumnsInternal.FillerColumn.IsRepresented = false;
}
_columnHeadersPresenter.OwningGrid = this;
// Columns were added before before our Template was applied, add the ColumnHeaders now
foreach (DataGridColumn column in ColumnsItemsInternal)
// Columns were added before our Template was applied, add the ColumnHeaders now
List<DataGridColumn> sortedInternal = new List<DataGridColumn>(ColumnsItemsInternal);
sortedInternal.Sort(new DisplayIndexComparer());
foreach (DataGridColumn column in sortedInternal)
{
InsertDisplayedColumnHeader(column);
}

35
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
{
/// <summary>
/// Used within the template of a <see cref="T:Avalonia.Controls.DataGrid" /> to specify the
/// Used within the template of a <see cref="T:Avalonia.Controls.DataGrid" /> to specify the
/// location in the control's visual tree where the rows are to be added.
/// </summary>
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)

32
src/Avalonia.Controls/Calendar/CalendarDatePicker.cs

@ -11,6 +11,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
namespace Avalonia.Controls
{
@ -209,6 +210,18 @@ namespace Avalonia.Controls
TextBox.UseFloatingWatermarkProperty.AddOwner<CalendarDatePicker>();
/// <summary>
/// Defines the <see cref="HorizontalContentAlignment"/> property.
/// </summary>
public static readonly StyledProperty<HorizontalAlignment> HorizontalContentAlignmentProperty =
ContentControl.HorizontalContentAlignmentProperty.AddOwner<CalendarDatePicker>();
/// <summary>
/// Defines the <see cref="VerticalContentAlignment"/> property.
/// </summary>
public static readonly StyledProperty<VerticalAlignment> VerticalContentAlignmentProperty =
ContentControl.VerticalContentAlignmentProperty.AddOwner<CalendarDatePicker>();
/// <summary>
/// Gets or sets the date to display.
/// </summary>
@ -364,6 +377,25 @@ namespace Avalonia.Controls
set { SetValue(UseFloatingWatermarkProperty, value); }
}
/// <summary>
/// Gets or sets the horizontal alignment of the content within the control.
/// </summary>
public HorizontalAlignment HorizontalContentAlignment
{
get => GetValue(HorizontalContentAlignmentProperty);
set => SetValue(HorizontalContentAlignmentProperty, value);
}
/// <summary>
/// Gets or sets the vertical alignment of the content within the control.
/// </summary>
public VerticalAlignment VerticalContentAlignment
{
get => GetValue(VerticalContentAlignmentProperty);
set => SetValue(VerticalContentAlignmentProperty, value);
}
/// <summary>
/// Occurs when the drop-down
/// <see cref="T:Avalonia.Controls.Calendar" /> is closed.

33
src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs

@ -6,6 +6,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Threading;
using Avalonia.Utilities;
@ -105,6 +106,19 @@ namespace Avalonia.Controls
public static readonly StyledProperty<string> WatermarkProperty =
AvaloniaProperty.Register<NumericUpDown, string>(nameof(Watermark));
/// <summary>
/// Defines the <see cref="HorizontalContentAlignment"/> property.
/// </summary>
public static readonly StyledProperty<HorizontalAlignment> HorizontalContentAlignmentProperty =
ContentControl.HorizontalContentAlignmentProperty.AddOwner<NumericUpDown>();
/// <summary>
/// Defines the <see cref="VerticalContentAlignment"/> property.
/// </summary>
public static readonly StyledProperty<VerticalAlignment> VerticalContentAlignmentProperty =
ContentControl.VerticalContentAlignmentProperty.AddOwner<NumericUpDown>();
private IDisposable _textBoxTextChangedSubscription;
private double _value;
@ -256,6 +270,25 @@ namespace Avalonia.Controls
set { SetValue(WatermarkProperty, value); }
}
/// <summary>
/// Gets or sets the horizontal alignment of the content within the control.
/// </summary>
public HorizontalAlignment HorizontalContentAlignment
{
get => GetValue(HorizontalContentAlignmentProperty);
set => SetValue(HorizontalContentAlignmentProperty, value);
}
/// <summary>
/// Gets or sets the vertical alignment of the content within the control.
/// </summary>
public VerticalAlignment VerticalContentAlignment
{
get => GetValue(VerticalContentAlignmentProperty);
set => SetValue(VerticalContentAlignmentProperty, value);
}
/// <summary>
/// Initializes new instance of <see cref="NumericUpDown"/> class.
/// </summary>

8
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);
}
}
}

11
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; }
}
}

25
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);
}
}
}

7
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<UndoRedoState> _undoRedoHelper;
private bool _isUndoingRedoing;
private bool _ignoreTextChanges;
@ -161,6 +162,10 @@ namespace Avalonia.Controls
static TextBox()
{
FocusableProperty.OverrideDefaultValue(typeof(TextBox), true);
TextInputMethodClientRequestedEvent.AddClassHandler<TextBox>((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<TextPresenter>("PART_TextPresenter");
_imClient.SetPresenter(_presenter);
if (IsFocused)
{
_presenter?.ShowCaret();

41
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);
}
}
}

40
src/Avalonia.Controls/ToolTip.cs

@ -1,3 +1,4 @@
#nullable enable
using System;
using System.Reactive.Linq;
using Avalonia.Controls.Metadata;
@ -21,8 +22,8 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the ToolTip.Tip attached property.
/// </summary>
public static readonly AttachedProperty<object> TipProperty =
AvaloniaProperty.RegisterAttached<ToolTip, Control, object>("Tip");
public static readonly AttachedProperty<object?> TipProperty =
AvaloniaProperty.RegisterAttached<ToolTip, Control, object?>("Tip");
/// <summary>
/// Defines the ToolTip.IsOpen attached property.
@ -57,10 +58,10 @@ namespace Avalonia.Controls
/// <summary>
/// Stores the current <see cref="ToolTip"/> instance in the control.
/// </summary>
internal static readonly AttachedProperty<ToolTip> ToolTipProperty =
AvaloniaProperty.RegisterAttached<ToolTip, Control, ToolTip>("ToolTip");
internal static readonly AttachedProperty<ToolTip?> ToolTipProperty =
AvaloniaProperty.RegisterAttached<ToolTip, Control, ToolTip?>("ToolTip");
private IPopupHost _popup;
private IPopupHost? _popup;
/// <summary>
/// Initializes static members of the <see cref="ToolTip"/> class.
@ -70,6 +71,10 @@ namespace Avalonia.Controls
TipProperty.Changed.Subscribe(ToolTipService.Instance.TipChanged);
IsOpenProperty.Changed.Subscribe(ToolTipService.Instance.TipOpenChanged);
IsOpenProperty.Changed.Subscribe(IsOpenChanged);
HorizontalOffsetProperty.Changed.Subscribe(RecalculatePositionOnPropertyChanged);
VerticalOffsetProperty.Changed.Subscribe(RecalculatePositionOnPropertyChanged);
PlacementProperty.Changed.Subscribe(RecalculatePositionOnPropertyChanged);
}
/// <summary>
@ -79,7 +84,7 @@ namespace Avalonia.Controls
/// <returns>
/// The content to be displayed in the control's tooltip.
/// </returns>
public static object GetTip(Control element)
public static object? GetTip(Control element)
{
return element.GetValue(TipProperty);
}
@ -89,7 +94,7 @@ namespace Avalonia.Controls
/// </summary>
/// <param name="element">The control to get the property from.</param>
/// <param name="value">The content to be displayed in the control's tooltip.</param>
public static void SetTip(Control element, object value)
public static void SetTip(Control element, object? value)
{
element.SetValue(TipProperty, value);
}
@ -207,8 +212,8 @@ namespace Avalonia.Controls
private static void IsOpenChanged(AvaloniaPropertyChangedEventArgs e)
{
var control = (Control)e.Sender;
var newValue = (bool)e.NewValue;
ToolTip toolTip;
var newValue = (bool)e.NewValue!;
ToolTip? toolTip;
if (newValue)
{
@ -235,6 +240,23 @@ namespace Avalonia.Controls
toolTip?.UpdatePseudoClasses(newValue);
}
private static void RecalculatePositionOnPropertyChanged(AvaloniaPropertyChangedEventArgs args)
{
var control = (Control)args.Sender;
var tooltip = control.GetValue(ToolTipProperty);
if (tooltip == null)
{
return;
}
tooltip.RecalculatePosition(control);
}
internal void RecalculatePosition(Control control)
{
_popup?.ConfigurePosition(control, GetPlacement(control), new Point(GetHorizontalOffset(control), GetVerticalOffset(control)));
}
private void Open(Control control)
{
Close();

10
src/Avalonia.Controls/ToolTipService.cs

@ -51,10 +51,12 @@ namespace Avalonia.Controls
if (e.OldValue is false && e.NewValue is true)
{
control.DetachedFromVisualTree += ControlDetaching;
control.EffectiveViewportChanged += ControlEffectiveViewportChanged;
}
else if(e.OldValue is true && e.NewValue is false)
{
control.DetachedFromVisualTree -= ControlDetaching;
control.EffectiveViewportChanged -= ControlEffectiveViewportChanged;
}
}
@ -62,6 +64,7 @@ namespace Avalonia.Controls
{
var control = (Control)sender;
control.DetachedFromVisualTree -= ControlDetaching;
control.EffectiveViewportChanged -= ControlEffectiveViewportChanged;
Close(control);
}
@ -97,6 +100,13 @@ namespace Avalonia.Controls
Close(control);
}
private void ControlEffectiveViewportChanged(object sender, Layout.EffectiveViewportChangedEventArgs e)
{
var control = (Control)sender;
var toolTip = control.GetValue(ToolTip.ToolTipProperty);
toolTip?.RecalculatePosition(control);
}
private void StartShowTimer(int showDelay, Control control)
{
_timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(showDelay) };

6
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<ResourcesChangedEventArgs>
{
/// <summary>
@ -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;
}
}

110
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<Exception, Task> _errorHandler;
class Item
{
public Func<Task> Callback;
public Action<Exception> OnFinish;
}
private Queue<Item> _q = new Queue<Item>();
private bool _processing;
public DBusCallQueue(Func<Exception, Task> errorHandler)
{
_errorHandler = errorHandler;
}
public void Enqueue(Func<Task> cb)
{
_q.Enqueue(new Item
{
Callback = cb
});
Process();
}
public Task EnqueueAsync(Func<Task> cb)
{
var tcs = new TaskCompletionSource<int>(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<T> EnqueueAsync<T>(Func<Task<T>> cb)
{
var tcs = new TaskCompletionSource<T>(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());
}
}
}
}

11
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;
}
}
}

288
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<T> : IX11InputMethodFactory where T : ITextInputMethodImpl, IX11InputMethodControl
{
private readonly Func<IntPtr, T> _factory;
public DBusInputMethodFactory(Func<IntPtr, T> 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<IDisposable> _disposables = new List<IDisposable>();
private Queue<string> _onlineNamesQueue = new Queue<string>();
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<bool> 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<Task> 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<bool> 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<bool> 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<string> _onCommit;
event Action<string> IX11InputMethodControl.Commit
{
add => _onCommit += value;
remove => _onCommit -= value;
}
protected void FireCommit(string s) => _onCommit?.Invoke(s);
private Action<X11InputMethodForwardedKey> _onForward;
event Action<X11InputMethodForwardedKey> 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();
});
}
}
}

69
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<int> ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, int Type, uint Time);
Task<IDisposable> WatchEnableIMAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchCloseIMAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchCommitStringAsync(Action<string> handler, Action<Exception> onError = null);
Task<IDisposable> WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchUpdatePreeditAsync(Action<(string str, int cursorpos)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchUpdateClientSideUIAsync(Action<(string auxup, string auxdown, string preedit, string candidateword, string imname, int cursorpos)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action<Exception> 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<bool> ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, bool Type, uint Time);
Task<IDisposable> WatchCommitStringAsync(Action<string> handler, Action<Exception> onError = null);
Task<IDisposable> WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchForwardKeyAsync(Action<(uint keyval, uint state, bool type)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action<Exception> onError = null);
}
[DBusInterface("org.fcitx.Fcitx.InputMethod1")]
interface IFcitxInputMethod1 : IDBusObject
{
Task<(ObjectPath path, byte[] data)> CreateInputContextAsync((string, string)[] arg0);
}
}

67
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
};
}

51
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<bool> 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<IDisposable> WatchCommitStringAsync(Action<string> handler) =>
_old?.WatchCommitStringAsync(handler) ?? _modern.WatchCommitStringAsync(handler);
public Task<IDisposable> 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);
}
}

149
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<bool> Connect(string name)
{
if (name == "org.fcitx.Fcitx")
{
var method = Connection.CreateProxy<IFcitxInputMethod>(name, "/inputmethod");
var resp = await method.CreateICv3Async(GetAppName(),
Process.GetCurrentProcess().Id);
var proxy = Connection.CreateProxy<IFcitxInputContext>(name,
"/inputcontext_" + resp.icid);
_context = new FcitxICWrapper(proxy);
}
else
{
var method = Connection.CreateProxy<IFcitxInputMethod1>(name, "/inputmethod");
var resp = await method.CreateInputContextAsync(new[] { ("appName", GetAppName()) });
var proxy = Connection.CreateProxy<IFcitxInputContext1>(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<bool> 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);
}
}

52
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<bool> 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<object> GetEngineAsync();
Task DestroyAsync();
Task SetSurroundingTextAsync(object Text, uint CursorPos, uint AnchorPos);
Task<IDisposable> WatchCommitTextAsync(Action<object> cb, Action<Exception> onError = null);
Task<IDisposable> WatchForwardKeyEventAsync(Action<(uint keyval, uint keycode, uint state)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchRequireSurroundingTextAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchars)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchUpdatePreeditTextAsync(Action<(object text, uint cursorPos, bool visible)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchShowPreeditTextAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchHidePreeditTextAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchUpdateAuxiliaryTextAsync(Action<(object text, bool visible)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchShowAuxiliaryTextAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchHideAuxiliaryTextAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchUpdateLookupTableAsync(Action<(object table, bool visible)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchShowLookupTableAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchHideLookupTableAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchPageUpLookupTableAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchPageDownLookupTableAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchCursorUpLookupTableAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchCursorDownLookupTableAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchRegisterPropertiesAsync(Action<object> handler, Action<Exception> onError = null);
Task<IDisposable> WatchUpdatePropertyAsync(Action<object> handler, Action<Exception> onError = null);
}
[DBusInterface("org.freedesktop.IBus.Portal")]
interface IIBusPortal : IDBusObject
{
Task<ObjectPath> CreateInputContextAsync(string Name);
}
}

45
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,
}
}

105
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<bool> Connect(string name)
{
var path =
await Connection.CreateProxy<IIBusPortal>(name, "/org/freedesktop/IBus")
.CreateInputContextAsync(GetAppName());
_context = Connection.CreateProxy<IIBusInputContext>(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<bool> 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
}
}
}

53
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<string, Func<Connection, IX11InputMethodFactory>> KnownMethods =
new Dictionary<string, Func<Connection, IX11InputMethodFactory>>
{
["fcitx"] = conn =>
new DBusInputMethodFactory<FcitxX11TextInputMethod>(_ => new FcitxX11TextInputMethod(conn)),
["ibus"] = conn =>
new DBusInputMethodFactory<IBusX11TextInputMethod>(_ => new IBusX11TextInputMethod(conn))
};
static Func<Connection, IX11InputMethodFactory> 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<IX11InputMethodFactory>().ToConstant(factory(conn));
return true;
}
}
return false;
}
}
}

31
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<bool> HandleEventAsync(RawKeyEventArgs args, int keyVal, int keyCode);
event Action<string> Commit;
event Action<X11InputMethodForwardedKey> ForwardKey;
void UpdateWindowInfo(PixelPoint position, double scaling);
}
}

35
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<InputElement, TextInputEventArgs>(
"TextInput",
RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="TextInputMethodClientRequested"/> event.
/// </summary>
public static readonly RoutedEvent<TextInputMethodClientRequestedEventArgs> TextInputMethodClientRequestedEvent =
RoutedEvent.Register<InputElement, TextInputMethodClientRequestedEventArgs>(
"TextInputMethodClientRequested",
RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="TextInputOptionsQuery"/> event.
/// </summary>
public static readonly RoutedEvent<TextInputOptionsQueryEventArgs> TextInputOptionsQueryEvent =
RoutedEvent.Register<InputElement, TextInputOptionsQueryEventArgs>(
"TextInputOptionsQuery",
RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="PointerEnter"/> event.
@ -243,6 +260,24 @@ namespace Avalonia.Input
add { AddHandler(TextInputEvent, value); }
remove { RemoveHandler(TextInputEvent, value); }
}
/// <summary>
/// Occurs when an input element gains input focus and input method is looking for the corresponding client
/// </summary>
public event EventHandler<TextInputMethodClientRequestedEventArgs> TextInputMethodClientRequested
{
add { AddHandler(TextInputMethodClientRequestedEvent, value); }
remove { RemoveHandler(TextInputMethodClientRequestedEvent, value); }
}
/// <summary>
/// Occurs when an input element gains input focus and input method is asking for required content options
/// </summary>
public event EventHandler<TextInputOptionsQueryEventArgs> TextInputOptionsQuery
{
add { AddHandler(TextInputOptionsQueryEvent, value); }
remove { RemoveHandler(TextInputOptionsQueryEvent, value); }
}
/// <summary>
/// Occurs when the pointer enters the control.

6
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<IInputManager>();
public IFocusManager FocusManager => AvaloniaLocator.Current.GetService<IFocusManager>();
// 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);
}
}

60
src/Avalonia.Input/TextInput/ITextInputMethodClient.cs

@ -0,0 +1,60 @@
using System;
using Avalonia.VisualTree;
namespace Avalonia.Input.TextInput
{
public interface ITextInputMethodClient
{
/// <summary>
/// The cursor rectangle relative to the TextViewVisual
/// </summary>
Rect CursorRectangle { get; }
/// <summary>
/// Should be fired when cursor rectangle is changed inside the TextViewVisual
/// </summary>
event EventHandler CursorRectangleChanged;
/// <summary>
/// The visual that's showing the text
/// </summary>
IVisual TextViewVisual { get; }
/// <summary>
/// Should be fired when text-hosting visual is changed
/// </summary>
event EventHandler TextViewVisualChanged;
/// <summary>
/// Indicates if TextViewVisual is capable of displaying non-commited input on the cursor position
/// </summary>
bool SupportsPreedit { get; }
/// <summary>
/// Sets the non-commited input string
/// </summary>
void SetPreeditText(string text);
/// <summary>
/// Indicates if text input client is capable of providing the text around the cursor
/// </summary>
bool SupportsSurroundingText { get; }
/// <summary>
/// Returns the text around the cursor, usually the current paragraph, the cursor position inside that text and selection start position
/// </summary>
TextInputMethodSurroundingText SurroundingText { get; }
/// <summary>
/// Should be fired when surrounding text changed
/// </summary>
event EventHandler SurroundingTextChanged;
/// <summary>
/// Returns the text before the cursor. Must return a non-empty string if cursor is not at the beginning of the text entry
/// </summary>
string TextBeforeCursor { get; }
/// <summary>
/// Returns the text before the cursor. Must return a non-empty string if cursor is not at the end of the text entry
/// </summary>
string TextAfterCursor { get; }
}
public struct TextInputMethodSurroundingText
{
public string Text { get; set; }
public int CursorOffset { get; set; }
public int AnchorOffset { get; set; }
}
}

15
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; }
}
}

101
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;
}
}
}

12
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
}
}

12
src/Avalonia.Input/TextInput/TextInputMethodClientRequestedEventArgs.cs

@ -0,0 +1,12 @@
using Avalonia.Interactivity;
namespace Avalonia.Input.TextInput
{
public class TextInputMethodClientRequestedEventArgs : RoutedEventArgs
{
/// <summary>
/// Set this property to a valid text input client to enable input method interaction
/// </summary>
public ITextInputMethodClient? Client { get; set; }
}
}

32
src/Avalonia.Input/TextInput/TextInputOptionsQueryEventArgs.cs

@ -0,0 +1,32 @@
using Avalonia.Interactivity;
namespace Avalonia.Input.TextInput
{
public class TextInputOptionsQueryEventArgs : RoutedEventArgs
{
/// <summary>
/// The content type (mostly for determining the shape of the virtual keyboard)
/// </summary>
public TextInputContentType ContentType { get; set; }
/// <summary>
/// Text is multiline
/// </summary>
public bool Multiline { get; set; }
/// <summary>
/// Text is in lower case
/// </summary>
public bool Lowercase { get; set; }
/// <summary>
/// Text is in upper case
/// </summary>
public bool Uppercase { get; set; }
/// <summary>
/// Automatically capitalize letters at the start of the sentence
/// </summary>
public bool AutoCapitalization { get; set; }
/// <summary>
/// Text contains sensitive data like card numbers and should not be stored
/// </summary>
public bool IsSensitive { get; set; }
}
}

109
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<AvaloniaPropertyChangedEventArgs> _propertyChangedHandler;
private readonly List<Visual> _propertyChangedSubscriptions = new List<Visual>();
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();
}
}
}

2
src/Avalonia.Themes.Default/CalendarDatePicker.xaml

@ -93,6 +93,8 @@
Watermark="{TemplateBinding Watermark}"
UseFloatingWatermark="{TemplateBinding UseFloatingWatermark}"
DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
Grid.Column="0"/>
<Button Name="PART_Button"

4
src/Avalonia.Themes.Default/NumericUpDown.xaml

@ -23,6 +23,8 @@
Watermark="{TemplateBinding Watermark}"
DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}"
IsReadOnly="{TemplateBinding IsReadOnly}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
Text="{TemplateBinding Text}"
AcceptsReturn="False"
TextWrapping="NoWrap">
@ -35,4 +37,4 @@
<Setter Property="Margin" Value="4"/>
<Setter Property="MinWidth" Value="20"/>
</Style>
</Styles>
</Styles>

3
src/Avalonia.Themes.Fluent/Controls/Button.xaml

@ -16,8 +16,7 @@
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrush}" />
<Setter Property="BorderThickness" Value="{DynamicResource ButtonBorderThemeThickness}" />
<Setter Property="Padding" Value="8,5,8,5" />
<Setter Property="Padding" Value="{StaticResource ButtonPadding}" />
<Setter Property="Padding" Value="{DynamicResource ButtonPadding}" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="FontWeight" Value="Normal" />

10
src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml

@ -9,8 +9,10 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=netstandard">
<Design.PreviewWith>
<Border Margin="20">
<CalendarDatePicker/>
<Border Margin="20, 20, 20, 200">
<CalendarDatePicker Width="200"
VerticalContentAlignment="Center"
HorizontalContentAlignment="Center" />
</Border>
</Design.PreviewWith>
@ -74,7 +76,7 @@
Grid.Row="1"
Grid.ColumnSpan="4"
Grid.RowSpan="3"
FontSize="{StaticResource CalendarDatePickerCurrentDayFontSize}"
FontSize="{DynamicResource CalendarDatePickerCurrentDayFontSize}"
Text="{Binding Source={x:Static sys:DateTime.Today}, Path=Day}"/>
<Ellipse HorizontalAlignment="Center" VerticalAlignment="Center" Fill="{DynamicResource SystemControlBackgroundChromeBlackHighBrush}" StrokeThickness="0" Grid.ColumnSpan="4" Width="3" Height="3"/>
@ -104,6 +106,8 @@
Watermark="{TemplateBinding Watermark}"
UseFloatingWatermark="{TemplateBinding UseFloatingWatermark}"
DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
Grid.Column="0"/>
<Button Name="PART_Button"

4
src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml

@ -141,8 +141,8 @@
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
IsEnabled="{TemplateBinding IsEnabled}"
MinWidth="{StaticResource DatePickerThemeMinWidth}"
MaxWidth="{StaticResource DatePickerThemeMaxWidth}"
MinWidth="{DynamicResource DatePickerThemeMinWidth}"
MaxWidth="{DynamicResource DatePickerThemeMaxWidth}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalContentAlignment="Stretch"

6
src/Avalonia.Themes.Fluent/Controls/FocusAdorner.xaml

@ -10,10 +10,10 @@
<Style Selector=":is(Control)">
<Setter Property="FocusAdorner">
<FocusAdornerTemplate>
<Border BorderThickness="{StaticResource SystemControlFocusVisualPrimaryThickness}"
<Border BorderThickness="{DynamicResource SystemControlFocusVisualPrimaryThickness}"
BorderBrush="{DynamicResource SystemControlFocusVisualPrimaryBrush}"
Margin="{StaticResource SystemControlFocusVisualMargin}">
<Border BorderThickness="{StaticResource SystemControlFocusVisualSecondaryThickness}"
Margin="{DynamicResource SystemControlFocusVisualMargin}">
<Border BorderThickness="{DynamicResource SystemControlFocusVisualSecondaryThickness}"
BorderBrush="{DynamicResource SystemControlFocusVisualSecondaryBrush}" />
</Border>
</FocusAdornerTemplate>

2
src/Avalonia.Themes.Fluent/Controls/ListBoxItem.xaml

@ -17,7 +17,7 @@
</Styles.Resources>
<Style Selector="ListBoxItem">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Padding" Value="{StaticResource ListBoxItemPadding}" />
<Setter Property="Padding" Value="{DynamicResource ListBoxItemPadding}" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="Template">
<ControlTemplate>

2
src/Avalonia.Themes.Fluent/Controls/Menu.xaml

@ -16,7 +16,7 @@
</Style.Resources>
<Setter Property="Background" Value="Transparent" />
<Setter Property="Height" Value="{StaticResource MenuBarHeight}" />
<Setter Property="Height" Value="{DynamicResource MenuBarHeight}" />
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}"

6
src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml

@ -112,7 +112,7 @@
<Popup Name="PART_Popup"
WindowManagerAddShadowHint="False"
PlacementMode="Right"
HorizontalOffset="{StaticResource MenuFlyoutSubItemPopupHorizontalOffset}"
HorizontalOffset="{DynamicResource MenuFlyoutSubItemPopupHorizontalOffset}"
IsLightDismissEnabled="True"
IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}">
<Border Background="{DynamicResource MenuFlyoutPresenterBackground}"
@ -189,12 +189,12 @@
<Style Selector="MenuItem">
<!-- Narrow padding should be used for mouse input, when non-narrow one should be used for touch input in future. -->
<Setter Property="Padding" Value="{StaticResource MenuFlyoutItemThemePaddingNarrow}" />
<Setter Property="Padding" Value="{DynamicResource MenuFlyoutItemThemePaddingNarrow}" />
</Style>
<Style Selector="Menu > MenuItem">
<!-- Custom padding for Menu > MenuItem -->
<Setter Property="Padding" Value="{StaticResource MenuBarItemPadding}" />
<Setter Property="Padding" Value="{DynamicResource MenuBarItemPadding}" />
</Style>
<Style Selector="MenuItem /template/ ContentPresenter#PART_IconPresenter">

7
src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml

@ -1,8 +1,7 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<Border Padding="20"
Background="Black">
<Border Padding="20">
<StackPanel Spacing="20">
<NumericUpDown Minimum="0"
Maximum="10"
@ -13,6 +12,8 @@
Maximum="10"
Increment="0.5"
Width="150"
VerticalContentAlignment="Center"
HorizontalContentAlignment="Center"
ButtonSpinnerLocation="Left"
Watermark="Enter text" />
</StackPanel>
@ -48,6 +49,8 @@
Padding="{TemplateBinding Padding}"
Watermark="{TemplateBinding Watermark}"
IsReadOnly="{TemplateBinding IsReadOnly}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
Text="{TemplateBinding Text}"
AcceptsReturn="False"
TextWrapping="NoWrap" />

2
src/Avalonia.Themes.Fluent/Controls/TabControl.xaml

@ -60,6 +60,6 @@
<Setter Property="Orientation" Value="Vertical" />
</Style>
<Style Selector="TabControl[TabStripPlacement=Top] /template/ ItemsPresenter#PART_ItemsPresenter">
<Setter Property="Margin" Value="{StaticResource TabControlTopPlacementItemMargin}" />
<Setter Property="Margin" Value="{DynamicResource TabControlTopPlacementItemMargin}" />
</Style>
</Styles>

10
src/Avalonia.Themes.Fluent/Controls/TabItem.xaml

@ -97,8 +97,8 @@
<!-- TabStripPlacement States Group -->
<Style Selector="TabItem[TabStripPlacement=Left] /template/ Border#PART_SelectedPipe">
<Setter Property="Width" Value="{StaticResource TabItemPipeThickness}" />
<Setter Property="Height" Value="{StaticResource TabItemVerticalPipeHeight}" />
<Setter Property="Width" Value="{DynamicResource TabItemPipeThickness}" />
<Setter Property="Height" Value="{DynamicResource TabItemVerticalPipeHeight}" />
<Setter Property="Margin" Value="0,0,2,0" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
@ -108,15 +108,15 @@
</Style>
<Style Selector="TabItem[TabStripPlacement=Top] /template/ Border#PART_SelectedPipe, TabItem[TabStripPlacement=Bottom] /template/ Border#PART_SelectedPipe">
<Setter Property="Height" Value="{StaticResource TabItemPipeThickness}" />
<Setter Property="Height" Value="{DynamicResource TabItemPipeThickness}" />
<Setter Property="Margin" Value="0,0,0,2" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Bottom" />
</Style>
<Style Selector="TabItem[TabStripPlacement=Right] /template/ Border#PART_SelectedPipe">
<Setter Property="Width" Value="{StaticResource TabItemPipeThickness}" />
<Setter Property="Height" Value="{StaticResource TabItemVerticalPipeHeight}" />
<Setter Property="Width" Value="{DynamicResource TabItemPipeThickness}" />
<Setter Property="Height" Value="{DynamicResource TabItemVerticalPipeHeight}" />
<Setter Property="Margin" Value="2,0,0,0" />
<Setter Property="HorizontalAlignment" Value="Right" />
<Setter Property="VerticalAlignment" Value="Center" />

2
src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml

@ -57,7 +57,7 @@
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="TabStripItem /template/ Border#PART_SelectedPipe, TabItem[TabStripPlacement=Bottom] /template/ Border#PART_SelectedPipe">
<Setter Property="Height" Value="{StaticResource TabStripItemPipeThickness}" />
<Setter Property="Height" Value="{DynamicResource TabStripItemPipeThickness}" />
<Setter Property="Margin" Value="0,0,0,2" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Bottom" />

4
src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml

@ -60,8 +60,8 @@
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
IsEnabled="{TemplateBinding IsEnabled}"
MinWidth="{StaticResource TimePickerThemeMinWidth}"
MaxWidth="{StaticResource TimePickerThemeMaxWidth}"
MinWidth="{DynamicResource TimePickerThemeMinWidth}"
MaxWidth="{DynamicResource TimePickerThemeMaxWidth}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
VerticalAlignment="Top"

2
src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml

@ -56,7 +56,7 @@
VerticalAlignment="Top"/>
<Grid Grid.Row="1"
MinWidth="{StaticResource ToggleSwitchThemeMinWidth}"
MinWidth="{DynamicResource ToggleSwitchThemeMinWidth}"
HorizontalAlignment="Left"
VerticalAlignment="Top">

4
src/Avalonia.X11/X11Clipboard.cs

@ -52,7 +52,7 @@ namespace Avalonia.X11
: null;
}
private unsafe void OnEvent(XEvent ev)
private unsafe void OnEvent(ref XEvent ev)
{
if (ev.type == XEventName.SelectionRequest)
{
@ -62,7 +62,7 @@ namespace Avalonia.X11
SelectionEvent =
{
type = XEventName.SelectionNotify,
send_event = true,
send_event = 1,
display = _x11.Display,
selection = sel.selection,
target = sel.target,

4
src/Avalonia.X11/X11Globals.cs

@ -123,7 +123,7 @@ namespace Avalonia.X11
}
}
private void HandleCompositionAtomOwnerEvents(XEvent ev)
private void HandleCompositionAtomOwnerEvents(ref XEvent ev)
{
if(ev.type == XEventName.DestroyNotify)
UpdateCompositingAtomOwner();
@ -154,7 +154,7 @@ namespace Avalonia.X11
}
}
private void OnRootWindowEvent(XEvent ev)
private void OnRootWindowEvent(ref XEvent ev)
{
if (ev.type == XEventName.PropertyNotify)
{

25
src/Avalonia.X11/X11Info.cs

@ -32,8 +32,10 @@ namespace Avalonia.X11
public IntPtr LastActivityTimestamp { get; set; }
public XVisualInfo? TransparentVisualInfo { get; set; }
public bool HasXim { get; set; }
public IntPtr DefaultFontSet { get; set; }
public unsafe X11Info(IntPtr display, IntPtr deferredDisplay)
public unsafe X11Info(IntPtr display, IntPtr deferredDisplay, bool useXim)
{
Display = display;
DeferredDisplay = deferredDisplay;
@ -43,9 +45,24 @@ namespace Avalonia.X11
DefaultCursor = XCreateFontCursor(display, CursorFontShape.XC_top_left_arrow);
DefaultRootWindow = XDefaultRootWindow(display);
Atoms = new X11Atoms(display);
//TODO: Open an actual XIM once we get support for preedit in our textbox
XSetLocaleModifiers("@im=none");
Xim = XOpenIM(display, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
DefaultFontSet = XCreateFontSet(Display, "-*-*-*-*-*-*-*-*-*-*-*-*-*-*",
out var _, out var _, IntPtr.Zero);
if (useXim)
{
XSetLocaleModifiers("");
Xim = XOpenIM(display, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
if (Xim != IntPtr.Zero)
HasXim = true;
}
if (Xim == IntPtr.Zero)
{
XSetLocaleModifiers("@im=none");
Xim = XOpenIM(display, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
}
XMatchVisualInfo(Display, DefaultScreen, 32, 4, out var visual);
if (visual.depth == 32)
TransparentVisualInfo = visual;

72
src/Avalonia.X11/X11Platform.cs

@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.InteropServices;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.FreeDesktop;
using Avalonia.FreeDesktop.DBusIme;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.OpenGL;
@ -21,7 +23,8 @@ namespace Avalonia.X11
{
private Lazy<KeyboardDevice> _keyboardDevice = new Lazy<KeyboardDevice>(() => new KeyboardDevice());
public KeyboardDevice KeyboardDevice => _keyboardDevice.Value;
public Dictionary<IntPtr, Action<XEvent>> Windows = new Dictionary<IntPtr, Action<XEvent>>();
public Dictionary<IntPtr, X11PlatformThreading.EventHandler> Windows =
new Dictionary<IntPtr, X11PlatformThreading.EventHandler>();
public XI2Manager XI2;
public X11Info Info { get; private set; }
public IX11Screens X11Screens { get; private set; }
@ -29,9 +32,24 @@ namespace Avalonia.X11
public X11PlatformOptions Options { get; private set; }
public IntPtr OrphanedWindow { get; private set; }
public X11Globals Globals { get; private set; }
[DllImport("libc")]
static extern void setlocale(int type, string s);
public void Initialize(X11PlatformOptions options)
{
Options = options;
bool useXim = false;
if (EnableIme(options))
{
// Attempt to configure DBus-based input method and check if we can fall back to XIM
if (!X11DBusImeHelper.DetectAndRegister() && ShouldUseXim())
useXim = true;
}
// XIM doesn't work at all otherwise
if (useXim)
setlocale(0, "");
XInitThreads();
Display = XOpenDisplay(IntPtr.Zero);
DeferredDisplay = XOpenDisplay(IntPtr.Zero);
@ -40,7 +58,8 @@ namespace Avalonia.X11
if (Display == IntPtr.Zero)
throw new Exception("XOpenDisplay failed");
XError.Init();
Info = new X11Info(Display, DeferredDisplay);
Info = new X11Info(Display, DeferredDisplay, useXim);
Globals = new X11Globals(this);
//TODO: log
if (options.UseDBusMenu)
@ -90,6 +109,54 @@ namespace Avalonia.X11
{
throw new NotSupportedException();
}
bool EnableIme(X11PlatformOptions options)
{
// Disable if explicitly asked by user
var avaloniaImModule = Environment.GetEnvironmentVariable("AVALONIA_IM_MODULE");
if (avaloniaImModule == "none")
return false;
// Use value from options when specified
if (options.EnableIme.HasValue)
return options.EnableIme.Value;
// Automatically enable for CJK locales
var lang = Environment.GetEnvironmentVariable("LANG");
var isCjkLocale = lang != null &&
(lang.Contains("zh")
|| lang.Contains("ja")
|| lang.Contains("vi")
|| lang.Contains("ko"));
return isCjkLocale;
}
bool ShouldUseXim()
{
// Check if we are forbidden from using IME
if (Environment.GetEnvironmentVariable("AVALONIA_IM_MODULE") == "none"
|| Environment.GetEnvironmentVariable("GTK_IM_MODULE") == "none"
|| Environment.GetEnvironmentVariable("QT_IM_MODULE") == "none")
return true;
// Check if XIM is configured
var modifiers = Environment.GetEnvironmentVariable("XMODIFIERS");
if (modifiers == null)
return false;
if (modifiers.Contains("@im=none") || modifiers.Contains("@im=null"))
return false;
if (!modifiers.Contains("@im="))
return false;
// Check if we are configured to use it
if (Environment.GetEnvironmentVariable("GTK_IM_MODULE") == "xim"
|| Environment.GetEnvironmentVariable("QT_IM_MODULE") == "xim"
|| Environment.GetEnvironmentVariable("AVALONIA_IM_MODULE") == "xim")
return true;
return false;
}
}
}
@ -103,6 +170,7 @@ namespace Avalonia
public bool OverlayPopups { get; set; }
public bool UseDBusMenu { get; set; }
public bool UseDeferredRendering { get; set; } = true;
public bool? EnableIme { get; set; }
public IList<GlVersion> GlProfiles { get; set; } = new List<GlVersion>
{

11
src/Avalonia.X11/X11PlatformThreading.cs

@ -13,7 +13,9 @@ namespace Avalonia.X11
{
private readonly AvaloniaX11Platform _platform;
private readonly IntPtr _display;
private readonly Dictionary<IntPtr, Action<XEvent>> _eventHandlers;
public delegate void EventHandler(ref XEvent xev);
private readonly Dictionary<IntPtr, EventHandler> _eventHandlers;
private Thread _mainThread;
[StructLayout(LayoutKind.Explicit)]
@ -162,13 +164,16 @@ namespace Avalonia.X11
Signaled?.Invoke(prio);
}
void HandleX11(CancellationToken cancellationToken)
unsafe void HandleX11(CancellationToken cancellationToken)
{
while (XPending(_display) != 0)
{
if (cancellationToken.IsCancellationRequested)
return;
XNextEvent(_display, out var xev);
if(XFilterEvent(ref xev, IntPtr.Zero))
continue;
if (xev.type == XEventName.GenericEvent)
XGetEventData(_display, &xev.GenericEventCookie);
try
@ -182,7 +187,7 @@ namespace Avalonia.X11
}
}
else if (_eventHandlers.TryGetValue(xev.AnyEvent.window, out var handler))
handler(xev);
handler(ref xev);
}
finally
{

2
src/Avalonia.X11/X11Screens.cs

@ -76,7 +76,7 @@ namespace Avalonia.X11
XRRSelectInput(_x11.Display, _window, RandrEventMask.RRScreenChangeNotify);
}
private void OnEvent(XEvent ev)
private void OnEvent(ref XEvent ev)
{
// Invalidate cache on RRScreenChangeNotify
if ((int)ev.type == _x11.RandrEventBase + (int)RandrEvent.RRScreenChangeNotify)

130
src/Avalonia.X11/X11Structs.cs

@ -53,7 +53,7 @@ namespace Avalonia.X11 {
internal struct XAnyEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr window;
}
@ -62,7 +62,7 @@ namespace Avalonia.X11 {
internal struct XKeyEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr window;
internal IntPtr root;
@ -74,14 +74,14 @@ namespace Avalonia.X11 {
internal int y_root;
internal XModifierMask state;
internal int keycode;
internal bool same_screen;
internal int same_screen;
}
[StructLayout(LayoutKind.Sequential)]
internal struct XButtonEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr window;
internal IntPtr root;
@ -93,14 +93,14 @@ namespace Avalonia.X11 {
internal int y_root;
internal XModifierMask state;
internal int button;
internal bool same_screen;
internal int same_screen;
}
[StructLayout(LayoutKind.Sequential)]
internal struct XMotionEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr window;
internal IntPtr root;
@ -112,14 +112,14 @@ namespace Avalonia.X11 {
internal int y_root;
internal XModifierMask state;
internal byte is_hint;
internal bool same_screen;
internal int same_screen;
}
[StructLayout(LayoutKind.Sequential)]
internal struct XCrossingEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr window;
internal IntPtr root;
@ -131,8 +131,8 @@ namespace Avalonia.X11 {
internal int y_root;
internal NotifyMode mode;
internal NotifyDetail detail;
internal bool same_screen;
internal bool focus;
internal int same_screen;
internal int focus;
internal XModifierMask state;
}
@ -140,7 +140,7 @@ namespace Avalonia.X11 {
internal struct XFocusChangeEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr window;
internal int mode;
@ -151,7 +151,7 @@ namespace Avalonia.X11 {
internal struct XKeymapEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr window;
internal byte key_vector0;
@ -192,7 +192,7 @@ namespace Avalonia.X11 {
internal struct XExposeEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr window;
internal int x;
@ -206,7 +206,7 @@ namespace Avalonia.X11 {
internal struct XGraphicsExposeEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr drawable;
internal int x;
@ -222,7 +222,7 @@ namespace Avalonia.X11 {
internal struct XNoExposeEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr drawable;
internal int major_code;
@ -233,7 +233,7 @@ namespace Avalonia.X11 {
internal struct XVisibilityEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr window;
internal int state;
@ -243,7 +243,7 @@ namespace Avalonia.X11 {
internal struct XCreateWindowEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr parent;
internal IntPtr window;
@ -252,14 +252,14 @@ namespace Avalonia.X11 {
internal int width;
internal int height;
internal int border_width;
internal bool override_redirect;
internal int override_redirect;
}
[StructLayout(LayoutKind.Sequential)]
internal struct XDestroyWindowEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr xevent;
internal IntPtr window;
@ -269,29 +269,29 @@ namespace Avalonia.X11 {
internal struct XUnmapEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr xevent;
internal IntPtr window;
internal bool from_configure;
internal int from_configure;
}
[StructLayout(LayoutKind.Sequential)]
internal struct XMapEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr xevent;
internal IntPtr window;
internal bool override_redirect;
internal int override_redirect;
}
[StructLayout(LayoutKind.Sequential)]
internal struct XMapRequestEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr parent;
internal IntPtr window;
@ -301,21 +301,21 @@ namespace Avalonia.X11 {
internal struct XReparentEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr xevent;
internal IntPtr window;
internal IntPtr parent;
internal int x;
internal int y;
internal bool override_redirect;
internal int override_redirect;
}
[StructLayout(LayoutKind.Sequential)]
internal struct XConfigureEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr xevent;
internal IntPtr window;
@ -325,14 +325,14 @@ namespace Avalonia.X11 {
internal int height;
internal int border_width;
internal IntPtr above;
internal bool override_redirect;
internal int override_redirect;
}
[StructLayout(LayoutKind.Sequential)]
internal struct XGravityEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr xevent;
internal IntPtr window;
@ -344,7 +344,7 @@ namespace Avalonia.X11 {
internal struct XResizeRequestEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr window;
internal int width;
@ -355,7 +355,7 @@ namespace Avalonia.X11 {
internal struct XConfigureRequestEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr parent;
internal IntPtr window;
@ -373,7 +373,7 @@ namespace Avalonia.X11 {
internal struct XCirculateEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr xevent;
internal IntPtr window;
@ -384,7 +384,7 @@ namespace Avalonia.X11 {
internal struct XCirculateRequestEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr parent;
internal IntPtr window;
@ -395,7 +395,7 @@ namespace Avalonia.X11 {
internal struct XPropertyEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr window;
internal IntPtr atom;
@ -407,7 +407,7 @@ namespace Avalonia.X11 {
internal struct XSelectionClearEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr window;
internal IntPtr selection;
@ -418,7 +418,7 @@ namespace Avalonia.X11 {
internal struct XSelectionRequestEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr owner;
internal IntPtr requestor;
@ -432,7 +432,7 @@ namespace Avalonia.X11 {
internal struct XSelectionEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr requestor;
internal IntPtr selection;
@ -445,11 +445,11 @@ namespace Avalonia.X11 {
internal struct XColormapEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr window;
internal IntPtr colormap;
internal bool c_new;
internal int c_new;
internal int state;
}
@ -457,7 +457,7 @@ namespace Avalonia.X11 {
internal struct XClientMessageEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr window;
internal IntPtr message_type;
@ -473,7 +473,7 @@ namespace Avalonia.X11 {
internal struct XMappingEvent {
internal XEventName type;
internal IntPtr serial;
internal bool send_event;
internal int send_event;
internal IntPtr display;
internal IntPtr window;
internal int request;
@ -518,6 +518,15 @@ namespace Avalonia.X11 {
internal IntPtr pad21;
internal IntPtr pad22;
internal IntPtr pad23;
internal IntPtr pad24;
internal IntPtr pad25;
internal IntPtr pad26;
internal IntPtr pad27;
internal IntPtr pad28;
internal IntPtr pad29;
internal IntPtr pad30;
internal IntPtr pad31;
internal IntPtr pad32;
}
[StructLayout(LayoutKind.Sequential)]
@ -525,7 +534,7 @@ namespace Avalonia.X11 {
{
internal int type; /* of event. Always GenericEvent */
internal IntPtr serial; /* # of last request processed */
internal bool send_event; /* true if from SendEvent request */
internal int send_event; /* true if from SendEvent request */
internal IntPtr display; /* Display the event was read from */
internal int extension; /* major opcode of extension that caused the event */
internal int evtype; /* actual event type. */
@ -672,10 +681,10 @@ namespace Avalonia.X11 {
internal int backing_store;
internal IntPtr backing_planes;
internal IntPtr backing_pixel;
internal bool save_under;
internal int save_under;
internal IntPtr event_mask;
internal IntPtr do_not_propagate_mask;
internal bool override_redirect;
internal int override_redirect;
internal IntPtr colormap;
internal IntPtr cursor;
}
@ -696,14 +705,14 @@ namespace Avalonia.X11 {
internal int backing_store;
internal IntPtr backing_planes;
internal IntPtr backing_pixel;
internal bool save_under;
internal int save_under;
internal IntPtr colormap;
internal bool map_installed;
internal int map_installed;
internal MapState map_state;
internal IntPtr all_event_masks;
internal IntPtr your_event_mask;
internal IntPtr do_not_propagate_mask;
internal bool override_direct;
internal int override_direct;
internal IntPtr screen;
public override string ToString ()
@ -1029,7 +1038,7 @@ namespace Avalonia.X11 {
internal int max_maps;
internal int min_maps;
internal int backing_store;
internal bool save_unders;
internal int save_unders;
internal IntPtr root_input_mask;
}
@ -1280,7 +1289,7 @@ namespace Avalonia.X11 {
internal int ts_y_origin;
internal IntPtr font;
internal GCSubwindowMode subwindow_mode;
internal bool graphics_exposures;
internal int graphics_exposures;
internal int clip_x_origin;
internal int clib_y_origin;
internal IntPtr clip_mask;
@ -1499,7 +1508,7 @@ namespace Avalonia.X11 {
[StructLayout(LayoutKind.Sequential)]
internal struct XWMHints {
internal IntPtr flags;
internal bool input;
internal int input;
internal XInitialState initial_state;
internal IntPtr icon_pixmap;
internal IntPtr icon_window;
@ -1708,19 +1717,30 @@ namespace Avalonia.X11 {
}
[StructLayout (LayoutKind.Sequential)]
internal struct XIMStyles
internal unsafe struct XIMStyles
{
public ushort count_styles;
public IntPtr supported_styles;
public IntPtr* supported_styles;
}
[StructLayout (LayoutKind.Sequential)]
[Serializable]
internal class XPoint
internal struct XPoint
{
public short X;
public short Y;
}
[StructLayout (LayoutKind.Sequential)]
[Serializable]
internal struct XRectangle
{
public short X;
public short Y;
public short W;
public short H;
}
[StructLayout (LayoutKind.Sequential)]
[Serializable]
@ -1798,7 +1818,7 @@ namespace Avalonia.X11 {
{
public ushort Length;
public IntPtr Feedback; // to XIMFeedbackStruct
public bool EncodingIsWChar;
public int EncodingIsWChar;
public IntPtr String; // it could be either char* or wchar_t*
}
@ -1850,6 +1870,8 @@ namespace Avalonia.X11 {
public const string XNClientWindow = "clientWindow";
public const string XNInputStyle = "inputStyle";
public const string XNFocusWindow = "focusWindow";
public const string XNResourceName = "resourceName";
public const string XNResourceClass = "resourceClass";
// XIMPreeditCallbacks delegate names.
public const string XNPreeditStartCallback = "preeditStartCallback";

208
src/Avalonia.X11/X11Window.Ime.cs

@ -0,0 +1,208 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Avalonia.FreeDesktop;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.Platform.Interop;
using static Avalonia.X11.XLib;
namespace Avalonia.X11
{
partial class X11Window
{
private ITextInputMethodImpl _ime;
private IX11InputMethodControl _imeControl;
private bool _processingIme;
private Queue<(RawKeyEventArgs args, XEvent xev, int keyval, int keycode)> _imeQueue =
new Queue<(RawKeyEventArgs args, XEvent xev, int keyVal, int keyCode)>();
unsafe void CreateIC()
{
if (_x11.HasXim)
{
XGetIMValues(_x11.Xim, XNames.XNQueryInputStyle, out var supported_styles, IntPtr.Zero);
for (var c = 0; c < supported_styles->count_styles; c++)
{
var style = (XIMProperties)supported_styles->supported_styles[c];
if ((int)(style & XIMProperties.XIMPreeditPosition) != 0
&& ((int)(style & XIMProperties.XIMStatusNothing) != 0))
{
XPoint spot = default;
XRectangle area = default;
//using var areaS = new Utf8Buffer("area");
using var spotS = new Utf8Buffer("spotLocation");
using var fontS = new Utf8Buffer("fontSet");
var list = XVaCreateNestedList(0,
//areaS, &area,
spotS, &spot,
fontS, _x11.DefaultFontSet,
IntPtr.Zero);
_xic = XCreateIC(_x11.Xim,
XNames.XNClientWindow, _handle,
XNames.XNFocusWindow, _handle,
XNames.XNInputStyle, new IntPtr((int)style),
XNames.XNResourceName, _platform.Options.WmClass,
XNames.XNResourceClass, _platform.Options.WmClass,
XNames.XNPreeditAttributes, list,
IntPtr.Zero);
XFree(list);
break;
}
}
XFree(new IntPtr(supported_styles));
}
if (_xic == IntPtr.Zero)
_xic = XCreateIC(_x11.Xim, XNames.XNInputStyle,
new IntPtr((int)(XIMProperties.XIMPreeditNothing | XIMProperties.XIMStatusNothing)),
XNames.XNClientWindow, _handle, XNames.XNFocusWindow, _handle, IntPtr.Zero);
}
void InitializeIme()
{
var ime = AvaloniaLocator.Current.GetService<IX11InputMethodFactory>()?.CreateClient(_handle);
if (ime == null && _x11.HasXim)
{
var xim = new XimInputMethod(this);
ime = (xim, xim);
}
if (ime != null)
{
(_ime, _imeControl) = ime.Value;
_imeControl.Commit += s =>
ScheduleInput(new RawTextInputEventArgs(_keyboard, (ulong)_x11.LastActivityTimestamp.ToInt64(),
_inputRoot, s));
_imeControl.ForwardKey += ev =>
{
ScheduleInput(new RawKeyEventArgs(_keyboard, (ulong)_x11.LastActivityTimestamp.ToInt64(),
_inputRoot, ev.Type, X11KeyTransform.ConvertKey((X11Key)ev.KeyVal),
(RawInputModifiers)ev.Modifiers));
};
}
}
void UpdateImePosition() => _imeControl?.UpdateWindowInfo(Position, RenderScaling);
void HandleKeyEvent(ref XEvent ev)
{
var index = ev.KeyEvent.state.HasFlag(XModifierMask.ShiftMask);
// We need the latin key, since it's mainly used for hotkeys, we use a different API for text anyway
var key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 1 : 0).ToInt32();
// Manually switch the Shift index for the keypad,
// there should be a proper way to do this
if (ev.KeyEvent.state.HasFlag(XModifierMask.Mod2Mask)
&& key > X11Key.Num_Lock && key <= X11Key.KP_9)
key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 0 : 1).ToInt32();
var filtered = ScheduleKeyInput(new RawKeyEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), _inputRoot,
ev.type == XEventName.KeyPress ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp,
X11KeyTransform.ConvertKey(key), TranslateModifiers(ev.KeyEvent.state)), ref ev, (int)key, ev.KeyEvent.keycode);
if (ev.type == XEventName.KeyPress && !filtered)
TriggerClassicTextInputEvent(ref ev);
}
void TriggerClassicTextInputEvent(ref XEvent ev)
{
var text = TranslateEventToString(ref ev);
if (text != null)
ScheduleInput(
new RawTextInputEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), _inputRoot, text),
ref ev);
}
private const int ImeBufferSize = 64 * 1024;
[ThreadStatic] private static IntPtr ImeBuffer;
unsafe string TranslateEventToString(ref XEvent ev)
{
if (ImeBuffer == IntPtr.Zero)
ImeBuffer = Marshal.AllocHGlobal(ImeBufferSize);
var len = Xutf8LookupString(_xic, ref ev, ImeBuffer.ToPointer(), ImeBufferSize,
out _, out var istatus);
var status = (XLookupStatus)istatus;
if (len == 0)
return null;
string text;
if (status == XLookupStatus.XBufferOverflow)
return null;
else
text = Encoding.UTF8.GetString((byte*)ImeBuffer.ToPointer(), len);
if (text == null)
return null;
if (text.Length == 1)
{
if (text[0] < ' ' || text[0] == 0x7f) //Control codes or DEL
return null;
}
return text;
}
bool ScheduleKeyInput(RawKeyEventArgs args, ref XEvent xev, int keyval, int keycode)
{
_x11.LastActivityTimestamp = xev.ButtonEvent.time;
if (_imeControl != null && _imeControl.IsEnabled)
{
if (FilterIme(args, xev, keyval, keycode))
return true;
}
ScheduleInput(args);
return false;
}
bool FilterIme(RawKeyEventArgs args, XEvent xev, int keyval, int keycode)
{
if (_ime == null)
return false;
_imeQueue.Enqueue((args, xev, keyval, keycode));
if (!_processingIme)
ProcessNextImeEvent();
return true;
}
async void ProcessNextImeEvent()
{
if(_processingIme)
return;
_processingIme = true;
try
{
while (_imeQueue.Count != 0)
{
var ev = _imeQueue.Dequeue();
if (_imeControl == null || !await _imeControl.HandleEventAsync(ev.args, ev.keyval, ev.keycode))
{
ScheduleInput(ev.args);
if (ev.args.Type == RawKeyEventType.KeyDown)
TriggerClassicTextInputEvent(ref ev.xev);
}
}
}
finally
{
_processingIme = false;
}
}
}
}

121
src/Avalonia.X11/X11Window.Xim.cs

@ -0,0 +1,121 @@
using System;
using System.Threading.Tasks;
using Avalonia.FreeDesktop;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.Platform.Interop;
using Avalonia.Threading;
using static Avalonia.X11.XLib;
namespace Avalonia.X11
{
partial class X11Window
{
class XimInputMethod : ITextInputMethodImpl, IX11InputMethodControl
{
private readonly X11Window _parent;
private bool _controlActive, _windowActive, _imeActive;
private Rect? _queuedCursorRect;
public XimInputMethod(X11Window parent)
{
_parent = parent;
}
public void SetCursorRect(Rect rect)
{
var needEnqueue = _queuedCursorRect == null;
_queuedCursorRect = rect;
if(needEnqueue)
Dispatcher.UIThread.Post(() =>
{
if(_queuedCursorRect == null)
return;
var rc = _queuedCursorRect.Value;
_queuedCursorRect = null;
if (_parent._xic == IntPtr.Zero)
return;
rect *= _parent._scaling;
var pt = new XPoint
{
X = (short)Math.Min(Math.Max(rect.X, short.MinValue), short.MaxValue),
Y = (short)Math.Min(Math.Max(rect.Y + rect.Height, short.MinValue), short.MaxValue)
};
using var spotLoc = new Utf8Buffer(XNames.XNSpotLocation);
var list = XVaCreateNestedList(0, spotLoc, ref pt, IntPtr.Zero);
XSetICValues(_parent._xic, XNames.XNPreeditAttributes, list, IntPtr.Zero);
XFree(list);
}, DispatcherPriority.Background);
}
public void SetWindowActive(bool active)
{
_windowActive = active;
UpdateActive();
}
public void SetActive(bool active)
{
_controlActive = active;
UpdateActive();
}
private void UpdateActive()
{
var active = _windowActive && _controlActive;
if(_parent._xic == IntPtr.Zero)
return;
if (active != _imeActive)
{
_imeActive = active;
if (active)
{
Reset();
XSetICFocus(_parent._xic);
}
else
XUnsetICFocus(_parent._xic);
}
}
public void UpdateWindowInfo(PixelPoint position, double scaling)
{
// No-op
}
public void SetOptions(TextInputOptionsQueryEventArgs options)
{
// No-op
}
public void Reset()
{
if(_parent._xic == IntPtr.Zero)
return;
var data = XmbResetIC(_parent._xic);
if (data != IntPtr.Zero)
XFree(data);
}
public void Dispose()
{
// No-op
}
public bool IsEnabled => false;
public ValueTask<bool> HandleEventAsync(RawKeyEventArgs args, int keyVal, int keyCode) =>
new ValueTask<bool>(false);
public event Action<string> Commit;
public event Action<X11InputMethodForwardedKey> ForwardKey;
}
}
}

80
src/Avalonia.X11/X11Window.cs

@ -5,12 +5,14 @@ using System.Diagnostics;
using System.Linq;
using System.Reactive.Disposables;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.FreeDesktop;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.OpenGL;
using Avalonia.OpenGL.Egl;
using Avalonia.Platform;
@ -22,9 +24,10 @@ using static Avalonia.X11.XLib;
// ReSharper disable StringLiteralTypo
namespace Avalonia.X11
{
unsafe class X11Window : IWindowImpl, IPopupImpl, IXI2Client,
unsafe partial class X11Window : IWindowImpl, IPopupImpl, IXI2Client,
ITopLevelImplWithNativeMenuExporter,
ITopLevelImplWithNativeControlHost
ITopLevelImplWithNativeControlHost,
ITopLevelImplWithTextInputMethod
{
private readonly AvaloniaX11Platform _platform;
private readonly IWindowImpl _popupParent;
@ -79,7 +82,7 @@ namespace Avalonia.X11
if (_popup)
{
attr.override_redirect = true;
attr.override_redirect = 1;
valueMask |= SetWindowValuemask.OverrideRedirect;
}
@ -178,11 +181,12 @@ namespace Avalonia.X11
Surfaces = surfaces.ToArray();
UpdateMotifHints();
UpdateSizeHints(null);
_xic = XCreateIC(_x11.Xim, XNames.XNInputStyle, XIMProperties.XIMPreeditNothing | XIMProperties.XIMStatusNothing,
XNames.XNClientWindow, _handle, IntPtr.Zero);
_transparencyHelper = new TransparencyHelper(_x11, _handle, platform.Globals);
_transparencyHelper.SetTransparencyRequest(WindowTransparencyLevel.None);
CreateIC();
XFlush(_x11.Display);
if(_popup)
PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(popupParent, MoveResize));
@ -194,6 +198,7 @@ namespace Avalonia.X11
Paint?.Invoke(default);
return _handle != IntPtr.Zero;
}, TimeSpan.FromMilliseconds(100));
InitializeIme();
}
class SurfaceInfo : EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo
@ -350,15 +355,13 @@ namespace Avalonia.X11
(IRenderer)new X11ImmediateRendererProxy(root, loop);
}
void OnEvent(XEvent ev)
void OnEvent(ref XEvent ev)
{
lock (SyncRoot)
OnEventSync(ev);
OnEventSync(ref ev);
}
void OnEventSync(XEvent ev)
void OnEventSync(ref XEvent ev)
{
if(XFilterEvent(ref ev, _handle))
return;
if (ev.type == XEventName.MapNotify)
{
_mapped = true;
@ -386,9 +389,13 @@ namespace Avalonia.X11
if (ActivateTransientChildIfNeeded())
return;
Activated?.Invoke();
_imeControl?.SetWindowActive(true);
}
else if (ev.type == XEventName.FocusOut)
{
_imeControl?.SetWindowActive(false);
Deactivated?.Invoke();
}
else if (ev.type == XEventName.MotionNotify)
MouseEvent(RawPointerEventType.Move, ref ev, ev.MotionEvent.state);
else if (ev.type == XEventName.LeaveNotify)
@ -447,7 +454,7 @@ namespace Avalonia.X11
return;
var needEnqueue = (_configure == null);
_configure = ev.ConfigureEvent;
if (ev.ConfigureEvent.override_redirect || ev.ConfigureEvent.send_event)
if (ev.ConfigureEvent.override_redirect != 0 || ev.ConfigureEvent.send_event != 0)
_configurePoint = new PixelPoint(ev.ConfigureEvent.x, ev.ConfigureEvent.y);
else
{
@ -477,6 +484,7 @@ namespace Avalonia.X11
PositionChanged?.Invoke(npos);
updatedSizeViaScaling = UpdateScaling();
}
UpdateImePosition();
if (changedSize && !updatedSizeViaScaling && !_popup)
Resized?.Invoke(ClientSize);
@ -487,7 +495,8 @@ namespace Avalonia.X11
XConfigureResizeWindow(_x11.Display, _renderHandle, ev.ConfigureEvent.width,
ev.ConfigureEvent.height);
}
else if (ev.type == XEventName.DestroyNotify && ev.AnyEvent.window == _handle)
else if (ev.type == XEventName.DestroyNotify
&& ev.DestroyWindowEvent.window == _handle)
{
Cleanup();
}
@ -507,39 +516,7 @@ namespace Avalonia.X11
{
if (ActivateTransientChildIfNeeded())
return;
var buffer = stackalloc byte[40];
var index = ev.KeyEvent.state.HasFlag(XModifierMask.ShiftMask);
// We need the latin key, since it's mainly used for hotkeys, we use a different API for text anyway
var key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 1 : 0).ToInt32();
// Manually switch the Shift index for the keypad,
// there should be a proper way to do this
if (ev.KeyEvent.state.HasFlag(XModifierMask.Mod2Mask)
&& key > X11Key.Num_Lock && key <= X11Key.KP_9)
key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 0 : 1).ToInt32();
ScheduleInput(new RawKeyEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), _inputRoot,
ev.type == XEventName.KeyPress ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp,
X11KeyTransform.ConvertKey(key), TranslateModifiers(ev.KeyEvent.state)), ref ev);
if (ev.type == XEventName.KeyPress)
{
var len = Xutf8LookupString(_xic, ref ev, buffer, 40, out _, out _);
if (len != 0)
{
var text = Encoding.UTF8.GetString(buffer, len);
if (text.Length == 1)
{
if (text[0] < ' ' || text[0] == 0x7f) //Control codes or DEL
return;
}
ScheduleInput(new RawTextInputEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), _inputRoot, text),
ref ev);
}
}
HandleKeyEvent(ref ev);
}
}
@ -562,6 +539,7 @@ namespace Avalonia.X11
var oldScaledSize = ClientSize;
RenderScaling = newScaling;
ScalingChanged?.Invoke(RenderScaling);
UpdateImePosition();
SetMinMaxSize(_scaledMinMaxSize.minSize, _scaledMinMaxSize.maxSize);
if(!skipResize)
Resize(oldScaledSize, true);
@ -699,6 +677,7 @@ namespace Avalonia.X11
_x11.LastActivityTimestamp = xev.ButtonEvent.time;
ScheduleInput(args);
}
public void ScheduleXI2Input(RawInputEventArgs args)
{
@ -781,6 +760,13 @@ namespace Avalonia.X11
void Cleanup()
{
if (_imeControl != null)
{
_imeControl.Dispose();
_imeControl = null;
_ime = null;
}
if (_xic != IntPtr.Zero)
{
XDestroyIC(_xic);
@ -957,7 +943,7 @@ namespace Avalonia.X11
ClientMessageEvent =
{
type = XEventName.ClientMessage,
send_event = true,
send_event = 1,
window = _handle,
message_type = message_type,
format = 32,
@ -1130,6 +1116,8 @@ namespace Avalonia.X11
public IPopupPositioner PopupPositioner { get; }
public ITopLevelNativeMenuExporter NativeMenuExporter { get; }
public INativeControlHostImpl NativeControlHost { get; }
public ITextInputMethodImpl TextInputMethod => _ime;
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) =>
_transparencyHelper.SetTransparencyRequest(transparencyLevel);

66
src/Avalonia.X11/XLib.cs

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using Avalonia.Platform.Interop;
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable FieldCanBeMadeReadOnly.Global
@ -57,6 +58,9 @@ namespace Avalonia.X11
[DllImport(libX11)]
public static extern IntPtr XNextEvent(IntPtr display, out XEvent xevent);
[DllImport(libX11)]
public static extern IntPtr XNextEvent(IntPtr display, XEvent* xevent);
[DllImport(libX11)]
public static extern int XConnectionNumber(IntPtr diplay);
@ -407,6 +411,9 @@ namespace Avalonia.X11
[DllImport(libX11)]
public static extern bool XFilterEvent(ref XEvent xevent, IntPtr window);
[DllImport(libX11)]
public static extern bool XFilterEvent(XEvent* xevent, IntPtr window);
[DllImport(libX11)]
public static extern void XkbSetDetectableAutoRepeat(IntPtr display, bool detectable, IntPtr supported);
@ -441,9 +448,9 @@ namespace Avalonia.X11
[DllImport(libX11)]
public static extern IntPtr XCreateColormap(IntPtr display, IntPtr window, IntPtr visual, int create);
public enum XLookupStatus
public enum XLookupStatus : uint
{
XBufferOverflow = -1,
XBufferOverflow = 0xffffffffu,
XLookupNone = 1,
XLookupChars = 2,
XLookupKeySym = 3,
@ -454,7 +461,10 @@ namespace Avalonia.X11
public static extern unsafe int XLookupString(ref XEvent xevent, void* buffer, int num_bytes, out IntPtr keysym, out IntPtr status);
[DllImport (libX11)]
public static extern unsafe int Xutf8LookupString(IntPtr xic, ref XEvent xevent, void* buffer, int num_bytes, out IntPtr keysym, out IntPtr status);
public static extern unsafe int Xutf8LookupString(IntPtr xic, ref XEvent xevent, void* buffer, int num_bytes, out IntPtr keysym, out UIntPtr status);
[DllImport (libX11)]
public static extern unsafe int Xutf8LookupString(IntPtr xic, XEvent* xevent, void* buffer, int num_bytes, out IntPtr keysym, out IntPtr status);
[DllImport (libX11)]
public static extern unsafe IntPtr XKeycodeToKeysym(IntPtr display, int keycode, int index);
@ -464,12 +474,52 @@ namespace Avalonia.X11
[DllImport (libX11)]
public static extern IntPtr XOpenIM (IntPtr display, IntPtr rdb, IntPtr res_name, IntPtr res_class);
[DllImport (libX11)]
public static extern IntPtr XCreateIC (IntPtr xim, string name, XIMProperties im_style, string name2, IntPtr value2, IntPtr terminator);
public static extern IntPtr XGetIMValues (IntPtr xim, string name, out XIMStyles* value, IntPtr terminator);
[DllImport (libX11)]
public static extern IntPtr XCreateIC (IntPtr xim, string name, IntPtr value, string name2, IntPtr value2, string name3, IntPtr value3, IntPtr terminator);
[DllImport(libX11)]
public static extern IntPtr XCreateIC(IntPtr xim, string name, IntPtr value, string name2, IntPtr value2,
string name3, IntPtr value3, string name4, IntPtr value4, IntPtr terminator);
[DllImport(libX11)]
public static extern IntPtr XCreateIC(IntPtr xim, string xnClientWindow, IntPtr handle,
string xnInputStyle, IntPtr value3, string xnResourceName, string optionsWmClass,
string xnResourceClass, string wmClass, string xnPreeditAttributes, IntPtr list, IntPtr zero);
[DllImport(libX11)]
public static extern IntPtr XCreateIC(IntPtr xim, string xnClientWindow, IntPtr handle, string xnFocusWindow,
IntPtr value2, string xnInputStyle, IntPtr value3, string xnResourceName, string optionsWmClass,
string xnResourceClass, string wmClass, string xnPreeditAttributes, IntPtr list, IntPtr zero);
[DllImport(libX11)]
public static extern void XSetICFocus(IntPtr xic);
[DllImport(libX11)]
public static extern void XUnsetICFocus(IntPtr xic);
[DllImport(libX11)]
public static extern IntPtr XmbResetIC(IntPtr xic);
[DllImport(libX11)]
public static extern IntPtr XVaCreateNestedList(int unused, Utf8Buffer name, ref XPoint point, IntPtr terminator);
[DllImport(libX11)]
public static extern IntPtr XVaCreateNestedList(int unused, Utf8Buffer xnArea, XRectangle* point,
Utf8Buffer xnSpotLocation, XPoint* value2, Utf8Buffer xnFontSet, IntPtr fs, IntPtr zero);
[DllImport(libX11)]
public static extern IntPtr XVaCreateNestedList(int unused,
Utf8Buffer xnSpotLocation, XPoint* value2, Utf8Buffer xnFontSet, IntPtr fs, IntPtr zero);
[DllImport (libX11)]
public static extern IntPtr XCreateIC (IntPtr xim, string name, XIMProperties im_style, string name2, IntPtr value2, string name3, IntPtr value3, IntPtr terminator);
public static extern IntPtr XCreateFontSet (IntPtr display, string name, out IntPtr list, out int count, IntPtr unused);
[DllImport(libX11)]
public static extern IntPtr XSetICValues(IntPtr ic, string name, IntPtr data, IntPtr terminator);
[DllImport (libX11)]
public static extern void XCloseIM (IntPtr xim);
@ -633,14 +683,12 @@ namespace Avalonia.X11
}
}
public static IntPtr CreateEventWindow(AvaloniaX11Platform plat, Action<XEvent> handler)
public static IntPtr CreateEventWindow(AvaloniaX11Platform plat, X11PlatformThreading.EventHandler handler)
{
var win = XCreateSimpleWindow(plat.Display, plat.Info.DefaultRootWindow,
0, 0, 1, 1, 0, IntPtr.Zero, IntPtr.Zero);
plat.Windows[win] = handler;
return win;
}
}
}

5
src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs

@ -129,7 +129,10 @@ namespace Avalonia.Markup.Parsers.Nodes
{
get
{
Target.TryGetTarget(out object target);
if (!Target.TryGetTarget(out object target))
{
return null;
}
return GetIndexer(target.GetType().GetTypeInfo())?.PropertyType;
}

11
src/Skia/Avalonia.Skia/FontManagerImpl.cs

@ -109,20 +109,23 @@ namespace Avalonia.Skia
if (typeface.FontFamily.Key == null)
{
var defaultName = SKTypeface.Default.FamilyName;
var fontStyle = new SKFontStyle((SKFontStyleWeight)typeface.Weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style);
foreach (var familyName in typeface.FontFamily.FamilyNames)
{
skTypeface = SKTypeface.FromFamilyName(familyName, (SKFontStyleWeight)typeface.Weight,
SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style);
skTypeface = _skFontManager.MatchFamily(familyName, fontStyle);
if (!skTypeface.FamilyName.Equals(familyName, StringComparison.Ordinal) &&
defaultName.Equals(skTypeface.FamilyName, StringComparison.Ordinal))
if (skTypeface is null
|| (!skTypeface.FamilyName.Equals(familyName, StringComparison.Ordinal)
&& defaultName.Equals(skTypeface.FamilyName, StringComparison.Ordinal)))
{
continue;
}
break;
}
skTypeface ??= _skFontManager.MatchTypeface(SKTypeface.Default, fontStyle);
}
else
{

8
src/Windows/Avalonia.Win32/Win32GlManager.cs

@ -9,6 +9,8 @@ namespace Avalonia.Win32
{
static class Win32GlManager
{
private static readonly Version Windows7 = new Version(6, 1);
public static void Initialize()
{
AvaloniaLocator.CurrentMutable.Bind<IPlatformOpenGlInterface>().ToLazy<IPlatformOpenGlInterface>(() =>
@ -20,13 +22,11 @@ namespace Avalonia.Win32
return wgl;
}
if (opts?.AllowEglInitialization == true ||
((!opts?.AllowEglInitialization.HasValue ?? false) &&
Win32Platform.WindowsVersion > new Version(6, 1)))
if (opts?.AllowEglInitialization ?? Win32Platform.WindowsVersion > Windows7)
{
var egl = EglPlatformOpenGlInterface.TryCreate(() => new AngleWin32EglDisplay());
if (egl is { } &&
if (egl != null &&
opts?.UseWindowsUIComposition == true)
{
WinUICompositorConnection.TryCreateAndRegister(egl);

20
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs

@ -4,6 +4,8 @@ using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Logging;
using Avalonia.Platform;
@ -797,6 +799,24 @@ namespace Avalonia.Base.UnitTests
Assert.False(source.SetterCalled);
}
[Fact]
public void TwoWay_Binding_Should_Not_Fail_With_Null_DataContext()
{
var target = new TextBlock();
target.DataContext = null;
target.Bind(TextBlock.TextProperty, new Binding("Missing", BindingMode.TwoWay));
}
[Fact]
public void TwoWay_Binding_Should_Not_Fail_With_Null_DataContext_Indexer()
{
var target = new TextBlock();
target.DataContext = null;
target.Bind(TextBlock.TextProperty, new Binding("[0]", BindingMode.TwoWay));
}
[Fact]
public void Disposing_Completed_Binding_Does_Not_Throw()
{

2
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs

@ -5,7 +5,7 @@ using Avalonia.Media;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.MakrupExtensions
namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
{
public class ResourceIncludeTests : XamlTestBase
{

2
tests/Avalonia.Markup.Xaml.UnitTests/StyleIncludeTests.cs

@ -12,7 +12,7 @@ namespace Avalonia.Markup.Xaml.UnitTests
{
var styleXaml = @"
<Style xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Style.Resources>
<StaticResource x:Key='brush' ResourceKey='missing' />
</Style.Resources>

Loading…
Cancel
Save