diff --git a/Avalonia.sln.DotSettings b/Avalonia.sln.DotSettings index 25d62b0494..2c0a6b9dc8 100644 --- a/Avalonia.sln.DotSettings +++ b/Avalonia.sln.DotSettings @@ -38,4 +38,5 @@ <Policy Inspect="False" Prefix="T" Suffix="" Style="AaBb" /> <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /> True - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/build/AndroidWorkarounds.props b/build/AndroidWorkarounds.props index 67947296b3..de86acc6de 100644 --- a/build/AndroidWorkarounds.props +++ b/build/AndroidWorkarounds.props @@ -2,7 +2,7 @@ - + diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props index e636461ad9..13419eb173 100644 --- a/build/HarfBuzzSharp.props +++ b/build/HarfBuzzSharp.props @@ -1,6 +1,6 @@  - - + + diff --git a/build/SourceLink.props b/build/SourceLink.props index e27727c9e8..1e007e01eb 100644 --- a/build/SourceLink.props +++ b/build/SourceLink.props @@ -1,5 +1,26 @@ + + true + false + true + embedded + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + + + + true + + + + true + + - + + + + + + diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index 75ee4a05cb..44e2290a0d 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -6,7 +6,9 @@ - + + all + @@ -29,21 +31,23 @@ + true - build\ + build\;buildTransitive\ true - build\ + build\;buildTransitive\ true - build\ + build\;buildTransitive\ + diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj index 5b82e2caee..97bd0eac86 100644 --- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj +++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj @@ -16,7 +16,7 @@ Resources\Resource.Designer.cs Off False - v9.0 + v10.0 Properties\AndroidManifest.xml @@ -156,4 +156,4 @@ - + \ No newline at end of file diff --git a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml index e39ec39f1c..02e97f3065 100644 --- a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml +++ b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/samples/ControlCatalog.Android/Resources/Resource.Designer.cs b/samples/ControlCatalog.Android/Resources/Resource.Designer.cs index 96f0e76fd8..4d0a6eff58 100644 --- a/samples/ControlCatalog.Android/Resources/Resource.Designer.cs +++ b/samples/ControlCatalog.Android/Resources/Resource.Designer.cs @@ -2,7 +2,6 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -15,7 +14,7 @@ namespace ControlCatalog.Android { - [System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")] public partial class Resource { @@ -26,8 +25,6 @@ namespace ControlCatalog.Android public static void UpdateIdValues() { - global::Avalonia.Android.Resource.String.ApplicationName = global::ControlCatalog.Android.Resource.String.ApplicationName; - global::Avalonia.Android.Resource.String.Hello = global::ControlCatalog.Android.Resource.String.Hello; } public partial class Attribute @@ -46,8 +43,8 @@ namespace ControlCatalog.Android public partial class Drawable { - // aapt resource value: 0x7f020000 - public const int Icon = 2130837504; + // aapt resource value: 0x7F010000 + public const int Icon = 2130771968; static Drawable() { @@ -62,8 +59,8 @@ namespace ControlCatalog.Android public partial class Id { - // aapt resource value: 0x7f050000 - public const int MyButton = 2131034112; + // aapt resource value: 0x7F020000 + public const int MyButton = 2130837504; static Id() { @@ -78,7 +75,7 @@ namespace ControlCatalog.Android public partial class Layout { - // aapt resource value: 0x7f030000 + // aapt resource value: 0x7F030000 public const int Main = 2130903040; static Layout() @@ -94,11 +91,11 @@ namespace ControlCatalog.Android public partial class String { - // aapt resource value: 0x7f040001 - public const int ApplicationName = 2130968577; + // aapt resource value: 0x7F040000 + public const int ApplicationName = 2130968576; - // aapt resource value: 0x7f040000 - public const int Hello = 2130968576; + // aapt resource value: 0x7F040001 + public const int Hello = 2130968577; static String() { diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index d7c5bd4415..0c8fd9465c 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -109,7 +109,8 @@ namespace ControlCatalog.NetCore .With(new X11PlatformOptions { EnableMultiTouch = true, - UseDBusMenu = true + UseDBusMenu = true, + EnableIme = true, }) .With(new Win32PlatformOptions { diff --git a/samples/ControlCatalog/Assets/Fonts/WenQuanYiMicroHei-01.ttf b/samples/ControlCatalog/Assets/Fonts/WenQuanYiMicroHei-01.ttf new file mode 100644 index 0000000000..61e2583a6c Binary files /dev/null and b/samples/ControlCatalog/Assets/Fonts/WenQuanYiMicroHei-01.ttf differ diff --git a/samples/ControlCatalog/Pages/DataGridPage.xaml b/samples/ControlCatalog/Pages/DataGridPage.xaml index cacc2204bd..6817d0698e 100644 --- a/samples/ControlCatalog/Pages/DataGridPage.xaml +++ b/samples/ControlCatalog/Pages/DataGridPage.xaml @@ -38,8 +38,8 @@ - - + + diff --git a/samples/ControlCatalog/Pages/SliderPage.xaml b/samples/ControlCatalog/Pages/SliderPage.xaml index b3f32ed421..eeb198976b 100644 --- a/samples/ControlCatalog/Pages/SliderPage.xaml +++ b/samples/ControlCatalog/Pages/SliderPage.xaml @@ -22,6 +22,20 @@ IsSnapToTickEnabled="True" Ticks="0,20,25,40,75,100" Width="300" /> + + + + + + diff --git a/samples/ControlCatalog/Pages/ToolTipPage.xaml b/samples/ControlCatalog/Pages/ToolTipPage.xaml index ec073d48a9..de23c7a169 100644 --- a/samples/ControlCatalog/Pages/ToolTipPage.xaml +++ b/samples/ControlCatalog/Pages/ToolTipPage.xaml @@ -6,7 +6,7 @@ ToolTip A control which pops up a hint when a control is hovered - @@ -38,6 +38,31 @@ ToolTip bottom placement + + + + + Moving offset + diff --git a/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs b/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs index 7802f336fb..d1a116345b 100644 --- a/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs +++ b/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs @@ -1,7 +1,11 @@ +using System; using System.Threading.Tasks; + using Android.Content; using Android.Runtime; using Android.Views; + +using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Platform; diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 71dce93ce7..360e76b2dc 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -6,6 +6,7 @@ using Android.Views; using Avalonia.Android.Platform.Input; using Avalonia.Android.Platform.Specific; using Avalonia.Android.Platform.Specific.Helpers; +using Avalonia.Controls; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Input; using Avalonia.Input.Raw; @@ -196,7 +197,17 @@ namespace Avalonia.Android.Platform.SkiaPlatform public IPopupImpl CreatePopup() => null; public Action LostFocus { get; set; } + public Action TransparencyLevelChanged { get; set; } - ILockedFramebuffer IFramebufferPlatformSurface.Lock()=>new AndroidFramebuffer(_view.Holder.Surface); + public WindowTransparencyLevel TransparencyLevel => WindowTransparencyLevel.None; + + public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1); + + ILockedFramebuffer IFramebufferPlatformSurface.Lock() => new AndroidFramebuffer(_view.Holder.Surface); + + public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) + { + throw new NotImplementedException(); + } } } diff --git a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs index 1179ce9235..426b221738 100644 --- a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs +++ b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs @@ -5,14 +5,14 @@ using Android.Runtime; using Android.Views; using Android.Views.InputMethods; using Avalonia.Android.Platform.Input; +using Avalonia.Android.Platform.SkiaPlatform; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Input.Raw; -using Avalonia.Platform; namespace Avalonia.Android.Platform.Specific.Helpers { - public class AndroidKeyboardEventsHelper : IDisposable where TView :ITopLevelImpl, IAndroidView + internal class AndroidKeyboardEventsHelper : IDisposable where TView : TopLevelImpl, IAndroidView { private TView _view; private IInputElement _lastFocusedElement; @@ -46,9 +46,11 @@ namespace Avalonia.Android.Platform.Specific.Helpers var rawKeyEvent = new RawKeyEventArgs( AndroidKeyboardDevice.Instance, - Convert.ToUInt32(e.EventTime), + Convert.ToUInt64(e.EventTime), + _view.InputRoot, e.Action == KeyEventActions.Down ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp, - AndroidKeyboardDevice.ConvertKey(e.KeyCode), GetModifierKeys(e)); + AndroidKeyboardDevice.ConvertKey(e.KeyCode), GetModifierKeys(e)); + _view.Input(rawKeyEvent); if (e.Action == KeyEventActions.Down && e.UnicodeChar >= 32) @@ -56,6 +58,7 @@ namespace Avalonia.Android.Platform.Specific.Helpers var rawTextEvent = new RawTextInputEventArgs( AndroidKeyboardDevice.Instance, Convert.ToUInt32(e.EventTime), + _view.InputRoot, Convert.ToChar(e.UnicodeChar).ToString() ); _view.Input(rawTextEvent); diff --git a/src/Android/Avalonia.Android/Resources/Resource.Designer.cs b/src/Android/Avalonia.Android/Resources/Resource.Designer.cs deleted file mode 100644 index 80cbbc51ec..0000000000 --- a/src/Android/Avalonia.Android/Resources/Resource.Designer.cs +++ /dev/null @@ -1,60 +0,0 @@ -#pragma warning disable 1591 -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -[assembly: global::Android.Runtime.ResourceDesignerAttribute("Avalonia.Android.Resource", IsApplication=false)] - -namespace Avalonia.Android -{ - - - [System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")] - public partial class Resource - { - - static Resource() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - public partial class Attribute - { - - static Attribute() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - private Attribute() - { - } - } - - public partial class String - { - - // aapt resource value: 0x7f020001 - public static int ApplicationName = 2130837505; - - // aapt resource value: 0x7f020000 - public static int Hello = 2130837504; - - static String() - { - global::Android.Runtime.ResourceIdManager.UpdateIdValues(); - } - - private String() - { - } - } - } -} -#pragma warning restore 1591 diff --git a/src/Android/Avalonia.Android/SystemDialogImpl.cs b/src/Android/Avalonia.Android/SystemDialogImpl.cs index a8d201d66e..1ed1f688b1 100644 --- a/src/Android/Avalonia.Android/SystemDialogImpl.cs +++ b/src/Android/Avalonia.Android/SystemDialogImpl.cs @@ -2,18 +2,17 @@ using System; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Platform; -using Avalonia.Platform; namespace Avalonia.Android { internal class SystemDialogImpl : ISystemDialogImpl { - public Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) + public Task ShowFileDialogAsync(FileDialog dialog, Window parent) { throw new NotImplementedException(); } - public Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + public Task ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) { throw new NotImplementedException(); } diff --git a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj index f880e48282..4f49f3a863 100644 --- a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj +++ b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj @@ -16,7 +16,7 @@ Resources\Resource.Designer.cs Off False - v9.0 + v10.0 Properties\AndroidManifest.xml @@ -150,4 +150,4 @@ - + \ No newline at end of file diff --git a/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs b/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs index ad2cec2ae3..121acb6351 100644 --- a/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs +++ b/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs @@ -4,7 +4,6 @@ using Android.Content.PM; using Android.OS; using Avalonia.Android; using Avalonia.Controls; -using Avalonia.Controls.Templates; using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.Styling; @@ -38,8 +37,7 @@ namespace Avalonia.AndroidTestApplication { Styles.Add(new DefaultTheme()); - var loader = new AvaloniaXamlLoader(); - var baseLight = (IStyle)loader.Load( + var baseLight = (IStyle)AvaloniaXamlLoader.Load( new Uri("resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default")); Styles.Add(baseLight); diff --git a/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml b/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml index 4792c8a1ec..e8e81da9de 100644 --- a/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml +++ b/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml @@ -1,6 +1,6 @@  - + \ No newline at end of file diff --git a/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs b/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs index e171dd6162..83db67fcee 100644 --- a/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs +++ b/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs @@ -2,7 +2,6 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -15,7 +14,7 @@ namespace Avalonia.AndroidTestApplication { - [System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")] public partial class Resource { @@ -26,8 +25,6 @@ namespace Avalonia.AndroidTestApplication public static void UpdateIdValues() { - global::Avalonia.Android.Resource.String.ApplicationName = global::Avalonia.AndroidTestApplication.Resource.String.ApplicationName; - global::Avalonia.Android.Resource.String.Hello = global::Avalonia.AndroidTestApplication.Resource.String.Hello; } public partial class Attribute @@ -46,8 +43,8 @@ namespace Avalonia.AndroidTestApplication public partial class Drawable { - // aapt resource value: 0x7f020000 - public const int Icon = 2130837504; + // aapt resource value: 0x7F010000 + public const int Icon = 2130771968; static Drawable() { @@ -62,11 +59,11 @@ namespace Avalonia.AndroidTestApplication public partial class String { - // aapt resource value: 0x7f030001 - public const int ApplicationName = 2130903041; + // aapt resource value: 0x7F020000 + public const int ApplicationName = 2130837504; - // aapt resource value: 0x7f030000 - public const int Hello = 2130903040; + // aapt resource value: 0x7F020001 + public const int Hello = 2130837505; static String() { diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index 9bfd387ca9..0470dbfe89 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -5,9 +5,6 @@ Avalonia True - - - diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index 3bf6842cd6..d600603d5c 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -14,7 +14,7 @@ namespace Avalonia.Data.Core.Plugins { private readonly Dictionary<(Type, string), PropertyInfo> _propertyLookup = new Dictionary<(Type, string), PropertyInfo>(); - + /// public bool Match(object obj, string propertyName) => GetFirstPropertyWithName(obj.GetType(), propertyName) != null; @@ -51,7 +51,7 @@ namespace Avalonia.Data.Core.Plugins private PropertyInfo GetFirstPropertyWithName(Type type, string propertyName) { var key = (type, propertyName); - + if (!_propertyLookup.TryGetValue(key, out PropertyInfo propertyInfo)) { propertyInfo = TryFindAndCacheProperty(type, propertyName); @@ -59,7 +59,7 @@ namespace Avalonia.Data.Core.Plugins return propertyInfo; } - + private PropertyInfo TryFindAndCacheProperty(Type type, string propertyName) { PropertyInfo found = null; @@ -90,7 +90,7 @@ namespace Avalonia.Data.Core.Plugins private readonly PropertyInfo _property; private bool _eventRaised; - public Accessor(WeakReference reference, PropertyInfo property) + public Accessor(WeakReference reference, PropertyInfo property) { Contract.Requires(reference != null); Contract.Requires(property != null); diff --git a/src/Avalonia.Base/Data/Core/SettableNode.cs b/src/Avalonia.Base/Data/Core/SettableNode.cs index d0a918dc88..363d3da0ef 100644 --- a/src/Avalonia.Base/Data/Core/SettableNode.cs +++ b/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); } diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 6687c59aa4..8f9b9583cf 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -2285,6 +2285,17 @@ namespace Avalonia.Controls } } + /// + /// Comparator class so we can sort list by the display index + /// + public class DisplayIndexComparer : IComparer + { + int IComparer.Compare(DataGridColumn x, DataGridColumn y) + { + return (x.DisplayIndexWithFiller < y.DisplayIndexWithFiller) ? -1 : 1; + } + } + /// /// Builds the visual tree for the column header when a new template is applied. /// @@ -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 sortedInternal = new List(ColumnsItemsInternal); + sortedInternal.Sort(new DisplayIndexComparer()); + foreach (DataGridColumn column in sortedInternal) { InsertDisplayedColumnHeader(column); } diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs index 76e5427fa4..0d19f4c479 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs @@ -3,14 +3,15 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. -using Avalonia.Media; using System; using System.Diagnostics; +using Avalonia.Layout; +using Avalonia.Media; namespace Avalonia.Controls.Primitives { /// - /// Used within the template of a to specify the + /// Used within the template of a to specify the /// location in the control's visual tree where the rows are to be added. /// public sealed class DataGridRowsPresenter : Panel @@ -22,25 +23,10 @@ namespace Avalonia.Controls.Primitives } private double _measureHeightOffset = 0; - private double _effectiveViewPortHeight = 0; - - public DataGridRowsPresenter() - { - EffectiveViewportChanged += OnEffectiveViewportChanged; - } - - private void OnEffectiveViewportChanged(object sender, Layout.EffectiveViewportChangedEventArgs e) - { - if (_effectiveViewPortHeight != e.EffectiveViewport.Height) - { - _effectiveViewPortHeight = e.EffectiveViewport.Height; - InvalidateMeasure(); - } - } private double CalculateEstimatedAvailableHeight(Size availableSize) { - if(!Double.IsPositiveInfinity(availableSize.Height)) + if (!Double.IsPositiveInfinity(availableSize.Height)) { return availableSize.Height + _measureHeightOffset; } @@ -66,10 +52,10 @@ namespace Avalonia.Controls.Primitives return base.ArrangeOverride(finalSize); } - if(OwningGrid.RowsPresenterAvailableSize.HasValue) + if (OwningGrid.RowsPresenterAvailableSize.HasValue) { var availableHeight = OwningGrid.RowsPresenterAvailableSize.Value.Height; - if(!Double.IsPositiveInfinity(availableHeight)) + if (!Double.IsPositiveInfinity(availableHeight)) { _measureHeightOffset = finalSize.Height - availableHeight; OwningGrid.RowsPresenterEstimatedAvailableHeight = finalSize.Height; @@ -126,7 +112,14 @@ namespace Avalonia.Controls.Primitives { if (double.IsInfinity(availableSize.Height)) { - availableSize = availableSize.WithHeight(_effectiveViewPortHeight); + if (VisualRoot is TopLevel topLevel) + { + double maxHeight = topLevel.IsArrangeValid ? + topLevel.Bounds.Height : + LayoutHelper.ApplyLayoutConstraints(topLevel, availableSize).Height; + + availableSize = availableSize.WithHeight(maxHeight); + } } if (availableSize.Height == 0 || OwningGrid == null) diff --git a/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs b/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs index 7c259f0a09..af3fdbd662 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs +++ b/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(); + /// + /// Defines the property. + /// + public static readonly StyledProperty HorizontalContentAlignmentProperty = + ContentControl.HorizontalContentAlignmentProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty VerticalContentAlignmentProperty = + ContentControl.VerticalContentAlignmentProperty.AddOwner(); + /// /// Gets or sets the date to display. /// @@ -364,6 +377,25 @@ namespace Avalonia.Controls set { SetValue(UseFloatingWatermarkProperty, value); } } + + /// + /// Gets or sets the horizontal alignment of the content within the control. + /// + public HorizontalAlignment HorizontalContentAlignment + { + get => GetValue(HorizontalContentAlignmentProperty); + set => SetValue(HorizontalContentAlignmentProperty, value); + } + + /// + /// Gets or sets the vertical alignment of the content within the control. + /// + public VerticalAlignment VerticalContentAlignment + { + get => GetValue(VerticalContentAlignmentProperty); + set => SetValue(VerticalContentAlignmentProperty, value); + } + /// /// Occurs when the drop-down /// is closed. diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index aae041071d..b6b71644a3 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/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 WatermarkProperty = AvaloniaProperty.Register(nameof(Watermark)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty HorizontalContentAlignmentProperty = + ContentControl.HorizontalContentAlignmentProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty VerticalContentAlignmentProperty = + ContentControl.VerticalContentAlignmentProperty.AddOwner(); + private IDisposable _textBoxTextChangedSubscription; private double _value; @@ -256,6 +270,25 @@ namespace Avalonia.Controls set { SetValue(WatermarkProperty, value); } } + + /// + /// Gets or sets the horizontal alignment of the content within the control. + /// + public HorizontalAlignment HorizontalContentAlignment + { + get => GetValue(HorizontalContentAlignmentProperty); + set => SetValue(HorizontalContentAlignmentProperty, value); + } + + /// + /// Gets or sets the vertical alignment of the content within the control. + /// + public VerticalAlignment VerticalContentAlignment + { + get => GetValue(VerticalContentAlignmentProperty); + set => SetValue(VerticalContentAlignmentProperty, value); + } + /// /// Initializes new instance of class. /// diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 5cbd3698b6..e3e9e84d7e 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -355,7 +355,13 @@ namespace Avalonia.Controls.Platform } else if (!item.IsPointerOverSubMenu) { - item.IsSubMenuOpen = false; + DelayRun(() => + { + if (!item.IsPointerOverSubMenu) + { + item.IsSubMenuOpen = false; + } + }, MenuShowDelay); } } } diff --git a/src/Avalonia.Controls/Platform/ITopLevelImplWithTextInputMethod.cs b/src/Avalonia.Controls/Platform/ITopLevelImplWithTextInputMethod.cs new file mode 100644 index 0000000000..9c29415a6a --- /dev/null +++ b/src/Avalonia.Controls/Platform/ITopLevelImplWithTextInputMethod.cs @@ -0,0 +1,11 @@ +using Avalonia.Input; +using Avalonia.Input.TextInput; +using Avalonia.Platform; + +namespace Avalonia.Controls.Platform +{ + public interface ITopLevelImplWithTextInputMethod : ITopLevelImpl + { + public ITextInputMethodImpl TextInputMethod { get; } + } +} diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 078d8050bf..6bbb1c13bf 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Linq; +using Avalonia.Input.TextInput; using Avalonia.Media; using Avalonia.Metadata; using Avalonia.Threading; @@ -378,19 +379,23 @@ namespace Avalonia.Controls.Presenters if (_caretBlink) { - var charPos = FormattedText.HitTestTextPosition(CaretIndex); - var x = Math.Floor(charPos.X) + 0.5; - var y = Math.Floor(charPos.Y) + 0.5; - var b = Math.Ceiling(charPos.Bottom) - 0.5; - + var (p1, p2) = GetCaretPoints(); context.DrawLine( new Pen(caretBrush, 1), - new Point(x, y), - new Point(x, b)); + p1, p2); } } } + (Point, Point) GetCaretPoints() + { + var charPos = FormattedText.HitTestTextPosition(CaretIndex); + var x = Math.Floor(charPos.X) + 0.5; + var y = Math.Floor(charPos.Y) + 0.5; + var b = Math.Ceiling(charPos.Bottom) - 0.5; + return (new Point(x, y), new Point(x, b)); + } + public void ShowCaret() { _caretBlink = true; @@ -538,5 +543,11 @@ namespace Avalonia.Controls.Presenters _caretBlink = !_caretBlink; InvalidateVisual(); } + + internal Rect GetCursorRectangle() + { + var (p1, p2) = GetCaretPoints(); + return new Rect(p1, p2); + } } } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 28426ee70f..90064fad57 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -149,6 +149,7 @@ namespace Avalonia.Controls private int _selectionStart; private int _selectionEnd; private TextPresenter _presenter; + private TextBoxTextInputMethodClient _imClient = new TextBoxTextInputMethodClient(); private UndoRedoHelper _undoRedoHelper; private bool _isUndoingRedoing; private bool _ignoreTextChanges; @@ -161,6 +162,10 @@ namespace Avalonia.Controls static TextBox() { FocusableProperty.OverrideDefaultValue(typeof(TextBox), true); + TextInputMethodClientRequestedEvent.AddClassHandler((tb, e) => + { + e.Client = tb._imClient; + }); } public TextBox() @@ -437,7 +442,7 @@ namespace Avalonia.Controls protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { _presenter = e.NameScope.Get("PART_TextPresenter"); - + _imClient.SetPresenter(_presenter); if (IsFocused) { _presenter?.ShowCaret(); diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs new file mode 100644 index 0000000000..e8122dd311 --- /dev/null +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -0,0 +1,41 @@ +using System; +using Avalonia.Controls.Presenters; +using Avalonia.Input.TextInput; +using Avalonia.VisualTree; + +namespace Avalonia.Controls +{ + internal class TextBoxTextInputMethodClient : ITextInputMethodClient + { + private TextPresenter _presenter; + private IDisposable _subscription; + public Rect CursorRectangle => _presenter?.GetCursorRectangle() ?? default; + public event EventHandler CursorRectangleChanged; + public IVisual TextViewVisual => _presenter; + public event EventHandler TextViewVisualChanged; + public bool SupportsPreedit => false; + public void SetPreeditText(string text) => throw new NotSupportedException(); + + public bool SupportsSurroundingText => false; + public TextInputMethodSurroundingText SurroundingText => throw new NotSupportedException(); + public event EventHandler SurroundingTextChanged; + public string TextBeforeCursor => null; + public string TextAfterCursor => null; + + private void OnCaretIndexChanged(int index) => CursorRectangleChanged?.Invoke(this, EventArgs.Empty); + + public void SetPresenter(TextPresenter presenter) + { + _subscription?.Dispose(); + _subscription = null; + _presenter = presenter; + if (_presenter != null) + { + _subscription = _presenter.GetObservable(TextPresenter.CaretIndexProperty) + .Subscribe(OnCaretIndexChanged); + } + TextViewVisualChanged?.Invoke(this, EventArgs.Empty); + CursorRectangleChanged?.Invoke(this, EventArgs.Empty); + } + } +} diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index 71bd0726d4..ab507d07a2 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/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 /// /// Defines the ToolTip.Tip attached property. /// - public static readonly AttachedProperty TipProperty = - AvaloniaProperty.RegisterAttached("Tip"); + public static readonly AttachedProperty TipProperty = + AvaloniaProperty.RegisterAttached("Tip"); /// /// Defines the ToolTip.IsOpen attached property. @@ -57,10 +58,10 @@ namespace Avalonia.Controls /// /// Stores the current instance in the control. /// - internal static readonly AttachedProperty ToolTipProperty = - AvaloniaProperty.RegisterAttached("ToolTip"); + internal static readonly AttachedProperty ToolTipProperty = + AvaloniaProperty.RegisterAttached("ToolTip"); - private IPopupHost _popup; + private IPopupHost? _popup; /// /// Initializes static members of the 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); } /// @@ -79,7 +84,7 @@ namespace Avalonia.Controls /// /// The content to be displayed in the control's tooltip. /// - public static object GetTip(Control element) + public static object? GetTip(Control element) { return element.GetValue(TipProperty); } @@ -89,7 +94,7 @@ namespace Avalonia.Controls /// /// The control to get the property from. /// The content to be displayed in the control's tooltip. - 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(); diff --git a/src/Avalonia.Controls/ToolTipService.cs b/src/Avalonia.Controls/ToolTipService.cs index 341ab2fe81..9e0bf60f42 100644 --- a/src/Avalonia.Controls/ToolTipService.cs +++ b/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) }; diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 3d24f60463..4e43ce13b7 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -1,8 +1,10 @@ using System; using System.Reactive.Linq; +using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; using Avalonia.Layout; using Avalonia.Logging; using Avalonia.LogicalTree; @@ -31,6 +33,7 @@ namespace Avalonia.Controls ICloseable, IStyleHost, ILogicalRoot, + ITextInputMethodRoot, IWeakSubscriber { /// @@ -489,5 +492,8 @@ namespace Avalonia.Controls if (focused == this) KeyboardDevice.Instance.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None); } + + ITextInputMethodImpl ITextInputMethodRoot.InputMethod => + (PlatformImpl as ITopLevelImplWithTextInputMethod)?.TextInputMethod; } } diff --git a/src/Avalonia.FreeDesktop/DBusCallQueue.cs b/src/Avalonia.FreeDesktop/DBusCallQueue.cs new file mode 100644 index 0000000000..5cd748be02 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusCallQueue.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Avalonia.FreeDesktop +{ + class DBusCallQueue + { + private readonly Func _errorHandler; + + class Item + { + public Func Callback; + public Action OnFinish; + } + private Queue _q = new Queue(); + private bool _processing; + + public DBusCallQueue(Func errorHandler) + { + _errorHandler = errorHandler; + } + + public void Enqueue(Func cb) + { + _q.Enqueue(new Item + { + Callback = cb + }); + Process(); + } + + public Task EnqueueAsync(Func cb) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + _q.Enqueue(new Item + { + Callback = cb, + OnFinish = e => + { + if (e == null) + tcs.TrySetResult(0); + else + tcs.TrySetException(e); + } + }); + Process(); + return tcs.Task; + } + + public Task EnqueueAsync(Func> cb) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + _q.Enqueue(new Item + { + Callback = async () => + { + var res = await cb(); + tcs.TrySetResult(res); + }, + OnFinish = e => + { + if (e != null) + tcs.TrySetException(e); + } + }); + Process(); + return tcs.Task; + } + + async void Process() + { + if(_processing) + return; + _processing = true; + try + { + while (_q.Count > 0) + { + var item = _q.Dequeue(); + try + { + await item.Callback(); + item.OnFinish?.Invoke(null); + } + catch(Exception e) + { + if (item.OnFinish != null) + item.OnFinish(e); + else + await _errorHandler(e); + } + } + } + finally + { + _processing = false; + } + } + + public void FailAll() + { + while (_q.Count>0) + { + var item = _q.Dequeue(); + item.OnFinish?.Invoke(new OperationCanceledException()); + } + } + } +} diff --git a/src/Avalonia.FreeDesktop/DBusHelper.cs b/src/Avalonia.FreeDesktop/DBusHelper.cs index b445f86613..7996a94dd0 100644 --- a/src/Avalonia.FreeDesktop/DBusHelper.cs +++ b/src/Avalonia.FreeDesktop/DBusHelper.cs @@ -1,5 +1,6 @@ using System; using System.Threading; +using Avalonia.Logging; using Avalonia.Threading; using Tmds.DBus; @@ -48,8 +49,10 @@ namespace Avalonia.FreeDesktop } public static Connection Connection { get; private set; } - public static Exception TryInitialize(string dbusAddress = null) + public static Connection TryInitialize(string dbusAddress = null) { + if (Connection != null) + return Connection; var oldContext = SynchronizationContext.Current; try { @@ -70,13 +73,15 @@ namespace Avalonia.FreeDesktop } catch (Exception e) { - return e; + Logger.TryGet(LogEventLevel.Error, "DBUS") + ?.Log(null, "Unable to connect to DBus: " + e); } finally { SynchronizationContext.SetSynchronizationContext(oldContext); } - return null; + + return Connection; } } } diff --git a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs new file mode 100644 index 0000000000..a7e83140ae --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs @@ -0,0 +1,288 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using System.Threading.Tasks; +using Avalonia.FreeDesktop.DBusIme.Fcitx; +using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; +using Avalonia.Logging; +using Tmds.DBus; + +namespace Avalonia.FreeDesktop.DBusIme +{ + internal class DBusInputMethodFactory : IX11InputMethodFactory where T : ITextInputMethodImpl, IX11InputMethodControl + { + private readonly Func _factory; + + public DBusInputMethodFactory(Func factory) + { + _factory = factory; + } + + public (ITextInputMethodImpl method, IX11InputMethodControl control) CreateClient(IntPtr xid) + { + var im = _factory(xid); + return (im, im); + } + } + + internal abstract class DBusTextInputMethodBase : IX11InputMethodControl, ITextInputMethodImpl + { + private List _disposables = new List(); + private Queue _onlineNamesQueue = new Queue(); + protected Connection Connection { get; } + private readonly string[] _knownNames; + private bool _connecting; + private string _currentName; + private DBusCallQueue _queue; + private bool _controlActive, _windowActive; + private bool? _imeActive; + private Rect _logicalRect; + private PixelRect? _lastReportedRect; + private double _scaling = 1; + private PixelPoint _windowPosition; + + protected bool IsConnected => _currentName != null; + + public DBusTextInputMethodBase(Connection connection, params string[] knownNames) + { + _queue = new DBusCallQueue(QueueOnError); + Connection = connection; + _knownNames = knownNames; + Watch(); + } + + async void Watch() + { + foreach (var name in _knownNames) + _disposables.Add(await Connection.ResolveServiceOwnerAsync(name, OnNameChange)); + } + + protected abstract Task Connect(string name); + + protected string GetAppName() => + Application.Current.Name ?? Assembly.GetEntryAssembly()?.GetName()?.Name ?? "Avalonia"; + + private async void OnNameChange(ServiceOwnerChangedEventArgs args) + { + if (args.NewOwner != null && _currentName == null) + { + _onlineNamesQueue.Enqueue(args.ServiceName); + if(!_connecting) + { + _connecting = true; + try + { + while (_onlineNamesQueue.Count > 0) + { + var name = _onlineNamesQueue.Dequeue(); + try + { + if (await Connect(name)) + { + _onlineNamesQueue.Clear(); + _currentName = name; + return; + } + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, "IME") + ?.Log(this, "Unable to create IME input context:\n" + e); + } + } + } + finally + { + _connecting = false; + } + } + + } + + // IME has crashed + if (args.NewOwner == null && args.ServiceName == _currentName) + { + _currentName = null; + foreach(var s in _disposables) + s.Dispose(); + _disposables.Clear(); + + OnDisconnected(); + Reset(); + + // Watch again + Watch(); + } + } + + protected virtual Task Disconnect() + { + return Task.CompletedTask; + } + + protected virtual void OnDisconnected() + { + + } + + protected virtual void Reset() + { + _lastReportedRect = null; + _imeActive = null; + } + + async Task QueueOnError(Exception e) + { + Logger.TryGet(LogEventLevel.Error, "IME") + ?.Log(this, "Error:\n" + e); + try + { + await Disconnect(); + } + catch (Exception ex) + { + Logger.TryGet(LogEventLevel.Error, "IME") + ?.Log(this, "Error while destroying the context:\n" + ex); + } + OnDisconnected(); + _currentName = null; + } + + protected void Enqueue(Func cb) => _queue.Enqueue(cb); + + protected void AddDisposable(IDisposable d) => _disposables.Add(d); + + public void Dispose() + { + foreach(var d in _disposables) + d.Dispose(); + _disposables.Clear(); + try + { + Disconnect().ContinueWith(_ => { }); + } + catch + { + // fire and forget + } + _currentName = null; + } + + protected abstract Task SetCursorRectCore(PixelRect rect); + protected abstract Task SetActiveCore(bool active); + protected abstract Task ResetContextCore(); + protected abstract Task HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode); + + void UpdateActive() + { + _queue.Enqueue(async () => + { + if(!IsConnected) + return; + + var active = _windowActive && _controlActive; + if (active != _imeActive) + { + _imeActive = active; + await SetActiveCore(active); + } + }); + } + + + void IX11InputMethodControl.SetWindowActive(bool active) + { + _windowActive = active; + UpdateActive(); + } + + void ITextInputMethodImpl.SetActive(bool active) + { + _controlActive = active; + UpdateActive(); + } + + bool IX11InputMethodControl.IsEnabled => IsConnected && _imeActive == true; + + async ValueTask IX11InputMethodControl.HandleEventAsync(RawKeyEventArgs args, int keyVal, int keyCode) + { + try + { + return await _queue.EnqueueAsync(async () => await HandleKeyCore(args, keyVal, keyCode)); + } + // Disconnected + catch (OperationCanceledException) + { + return false; + } + // Error, disconnect + catch (Exception e) + { + await QueueOnError(e); + return false; + } + } + + private Action _onCommit; + event Action IX11InputMethodControl.Commit + { + add => _onCommit += value; + remove => _onCommit -= value; + } + + protected void FireCommit(string s) => _onCommit?.Invoke(s); + + private Action _onForward; + event Action IX11InputMethodControl.ForwardKey + { + add => _onForward += value; + remove => _onForward -= value; + } + + protected void FireForward(X11InputMethodForwardedKey k) => _onForward?.Invoke(k); + + void UpdateCursorRect() + { + _queue.Enqueue(async () => + { + if(!IsConnected) + return; + var cursorRect = PixelRect.FromRect(_logicalRect, _scaling); + cursorRect = cursorRect.Translate(_windowPosition); + if (cursorRect != _lastReportedRect) + { + _lastReportedRect = cursorRect; + await SetCursorRectCore(cursorRect); + } + }); + } + + void IX11InputMethodControl.UpdateWindowInfo(PixelPoint position, double scaling) + { + _windowPosition = position; + _scaling = scaling; + UpdateCursorRect(); + } + + void ITextInputMethodImpl.SetCursorRect(Rect rect) + { + _logicalRect = rect; + UpdateCursorRect(); + } + + public abstract void SetOptions(TextInputOptionsQueryEventArgs options); + + void ITextInputMethodImpl.Reset() + { + Reset(); + _queue.Enqueue(async () => + { + if (!IsConnected) + return; + await ResetContextCore(); + }); + } + } +} diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs new file mode 100644 index 0000000000..7ce2339763 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Tmds.DBus; + +[assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)] +namespace Avalonia.FreeDesktop.DBusIme.Fcitx +{ + [DBusInterface("org.fcitx.Fcitx.InputMethod")] + interface IFcitxInputMethod : IDBusObject + { + Task<(int icid, bool enable, uint keyval1, uint state1, uint keyval2, uint state2)> CreateICv3Async( + string Appname, int Pid); + } + + + [DBusInterface("org.fcitx.Fcitx.InputContext")] + interface IFcitxInputContext : IDBusObject + { + Task EnableICAsync(); + Task CloseICAsync(); + Task FocusInAsync(); + Task FocusOutAsync(); + Task ResetAsync(); + Task MouseEventAsync(int X); + Task SetCursorLocationAsync(int X, int Y); + Task SetCursorRectAsync(int X, int Y, int W, int H); + Task SetCapacityAsync(uint Caps); + Task SetSurroundingTextAsync(string Text, uint Cursor, uint Anchor); + Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor); + Task DestroyICAsync(); + Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, int Type, uint Time); + Task WatchEnableIMAsync(Action handler, Action onError = null); + Task WatchCloseIMAsync(Action handler, Action onError = null); + Task WatchCommitStringAsync(Action handler, Action onError = null); + Task WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action onError = null); + Task WatchUpdatePreeditAsync(Action<(string str, int cursorpos)> handler, Action onError = null); + Task WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action onError = null); + Task WatchUpdateClientSideUIAsync(Action<(string auxup, string auxdown, string preedit, string candidateword, string imname, int cursorpos)> handler, Action onError = null); + Task WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler, Action onError = null); + Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action onError = null); + } + + [DBusInterface("org.fcitx.Fcitx.InputContext1")] + interface IFcitxInputContext1 : IDBusObject + { + Task FocusInAsync(); + Task FocusOutAsync(); + Task ResetAsync(); + Task SetCursorRectAsync(int X, int Y, int W, int H); + Task SetCapabilityAsync(ulong Caps); + Task SetSurroundingTextAsync(string Text, uint Cursor, uint Anchor); + Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor); + Task DestroyICAsync(); + Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, bool Type, uint Time); + Task WatchCommitStringAsync(Action handler, Action onError = null); + Task WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action onError = null); + Task WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action onError = null); + Task WatchForwardKeyAsync(Action<(uint keyval, uint state, bool type)> handler, Action onError = null); + Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action onError = null); + } + + [DBusInterface("org.fcitx.Fcitx.InputMethod1")] + interface IFcitxInputMethod1 : IDBusObject + { + Task<(ObjectPath path, byte[] data)> CreateInputContextAsync((string, string)[] arg0); + } +} diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs new file mode 100644 index 0000000000..6510a5877a --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs @@ -0,0 +1,67 @@ +using System; + +namespace Avalonia.FreeDesktop.DBusIme.Fcitx +{ + enum FcitxKeyEventType + { + FCITX_PRESS_KEY, + FCITX_RELEASE_KEY + }; + + [Flags] + enum FcitxCapabilityFlags + { + CAPACITY_NONE = 0, + CAPACITY_CLIENT_SIDE_UI = (1 << 0), + CAPACITY_PREEDIT = (1 << 1), + CAPACITY_CLIENT_SIDE_CONTROL_STATE = (1 << 2), + CAPACITY_PASSWORD = (1 << 3), + CAPACITY_FORMATTED_PREEDIT = (1 << 4), + CAPACITY_CLIENT_UNFOCUS_COMMIT = (1 << 5), + CAPACITY_SURROUNDING_TEXT = (1 << 6), + CAPACITY_EMAIL = (1 << 7), + CAPACITY_DIGIT = (1 << 8), + CAPACITY_UPPERCASE = (1 << 9), + CAPACITY_LOWERCASE = (1 << 10), + CAPACITY_NOAUTOUPPERCASE = (1 << 11), + CAPACITY_URL = (1 << 12), + CAPACITY_DIALABLE = (1 << 13), + CAPACITY_NUMBER = (1 << 14), + CAPACITY_NO_ON_SCREEN_KEYBOARD = (1 << 15), + CAPACITY_SPELLCHECK = (1 << 16), + CAPACITY_NO_SPELLCHECK = (1 << 17), + CAPACITY_WORD_COMPLETION = (1 << 18), + CAPACITY_UPPERCASE_WORDS = (1 << 19), + CAPACITY_UPPERCASE_SENTENCES = (1 << 20), + CAPACITY_ALPHA = (1 << 21), + CAPACITY_NAME = (1 << 22), + CAPACITY_GET_IM_INFO_ON_FOCUS = (1 << 23), + CAPACITY_RELATIVE_CURSOR_RECT = (1 << 24), + }; + + [Flags] + enum FcitxKeyState + { + FcitxKeyState_None = 0, + FcitxKeyState_Shift = 1 << 0, + FcitxKeyState_CapsLock = 1 << 1, + FcitxKeyState_Ctrl = 1 << 2, + FcitxKeyState_Alt = 1 << 3, + FcitxKeyState_Alt_Shift = FcitxKeyState_Alt | FcitxKeyState_Shift, + FcitxKeyState_Ctrl_Shift = FcitxKeyState_Ctrl | FcitxKeyState_Shift, + FcitxKeyState_Ctrl_Alt = FcitxKeyState_Ctrl | FcitxKeyState_Alt, + + FcitxKeyState_Ctrl_Alt_Shift = + FcitxKeyState_Ctrl | FcitxKeyState_Alt | FcitxKeyState_Shift, + FcitxKeyState_NumLock = 1 << 4, + FcitxKeyState_Super = 1 << 6, + FcitxKeyState_ScrollLock = 1 << 7, + FcitxKeyState_MousePressed = 1 << 8, + FcitxKeyState_HandledMask = 1 << 24, + FcitxKeyState_IgnoredMask = 1 << 25, + FcitxKeyState_Super2 = 1 << 26, + FcitxKeyState_Hyper = 1 << 27, + FcitxKeyState_Meta = 1 << 28, + FcitxKeyState_UsedMask = 0x5c001fff + }; +} diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs new file mode 100644 index 0000000000..a03ea213aa --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs @@ -0,0 +1,51 @@ +using System; +using System.Threading.Tasks; + +namespace Avalonia.FreeDesktop.DBusIme.Fcitx +{ + internal class FcitxICWrapper + { + private readonly IFcitxInputContext1 _modern; + private readonly IFcitxInputContext _old; + + public FcitxICWrapper(IFcitxInputContext old) + { + _old = old; + } + + public FcitxICWrapper(IFcitxInputContext1 modern) + { + _modern = modern; + } + + public Task FocusInAsync() => _old?.FocusInAsync() ?? _modern.FocusInAsync(); + + public Task FocusOutAsync() => _old?.FocusOutAsync() ?? _modern.FocusOutAsync(); + + public Task ResetAsync() => _old?.ResetAsync() ?? _modern.ResetAsync(); + + public Task SetCursorRectAsync(int x, int y, int w, int h) => + _old?.SetCursorRectAsync(x, y, w, h) ?? _modern.SetCursorRectAsync(x, y, w, h); + public Task DestroyICAsync() => _old?.DestroyICAsync() ?? _modern.DestroyICAsync(); + + public async Task ProcessKeyEventAsync(uint keyVal, uint keyCode, uint state, int type, uint time) + { + if(_old!=null) + return await _old.ProcessKeyEventAsync(keyVal, keyCode, state, type, time) != 0; + return await _modern.ProcessKeyEventAsync(keyVal, keyCode, state, type > 0, time); + } + + public Task WatchCommitStringAsync(Action handler) => + _old?.WatchCommitStringAsync(handler) ?? _modern.WatchCommitStringAsync(handler); + + public Task WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler) + { + return _old?.WatchForwardKeyAsync(handler) + ?? _modern.WatchForwardKeyAsync(ev => + handler((ev.keyval, ev.state, ev.type ? 1 : 0))); + } + + public Task SetCapacityAsync(uint flags) => + _old?.SetCapacityAsync(flags) ?? _modern.SetCapabilityAsync(flags); + } +} diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs new file mode 100644 index 0000000000..8239b3f35d --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs @@ -0,0 +1,149 @@ +using System; +using System.Diagnostics; +using System.Reactive.Concurrency; +using System.Reflection; +using System.Threading.Tasks; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; +using Tmds.DBus; + +namespace Avalonia.FreeDesktop.DBusIme.Fcitx +{ + internal class FcitxX11TextInputMethod : DBusTextInputMethodBase + { + private FcitxICWrapper _context; + private FcitxCapabilityFlags? _lastReportedFlags; + + public FcitxX11TextInputMethod(Connection connection) : base(connection, + "org.fcitx.Fcitx", + "org.freedesktop.portal.Fcitx" + ) + { + + } + + protected override async Task Connect(string name) + { + if (name == "org.fcitx.Fcitx") + { + var method = Connection.CreateProxy(name, "/inputmethod"); + var resp = await method.CreateICv3Async(GetAppName(), + Process.GetCurrentProcess().Id); + + var proxy = Connection.CreateProxy(name, + "/inputcontext_" + resp.icid); + + _context = new FcitxICWrapper(proxy); + } + else + { + var method = Connection.CreateProxy(name, "/inputmethod"); + var resp = await method.CreateInputContextAsync(new[] { ("appName", GetAppName()) }); + var proxy = Connection.CreateProxy(name, resp.path); + _context = new FcitxICWrapper(proxy); + } + + AddDisposable(await _context.WatchCommitStringAsync(OnCommitString)); + AddDisposable(await _context.WatchForwardKeyAsync(OnForward)); + return true; + } + + protected override Task Disconnect() => _context.DestroyICAsync(); + + protected override void OnDisconnected() => _context = null; + + protected override void Reset() + { + _lastReportedFlags = null; + base.Reset(); + } + + protected override Task SetCursorRectCore(PixelRect cursorRect) => + _context.SetCursorRectAsync(cursorRect.X, cursorRect.Y, Math.Max(1, cursorRect.Width), + Math.Max(1, cursorRect.Height)); + + protected override Task SetActiveCore(bool active) + { + if (active) + return _context.FocusInAsync(); + else + return _context.FocusOutAsync(); + } + + protected override Task ResetContextCore() => _context.ResetAsync(); + + protected override async Task HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode) + { + FcitxKeyState state = default; + if (args.Modifiers.HasFlagCustom(RawInputModifiers.Control)) + state |= FcitxKeyState.FcitxKeyState_Ctrl; + if (args.Modifiers.HasFlagCustom(RawInputModifiers.Alt)) + state |= FcitxKeyState.FcitxKeyState_Alt; + if (args.Modifiers.HasFlagCustom(RawInputModifiers.Shift)) + state |= FcitxKeyState.FcitxKeyState_Shift; + if (args.Modifiers.HasFlagCustom(RawInputModifiers.Meta)) + state |= FcitxKeyState.FcitxKeyState_Super; + + var type = args.Type == RawKeyEventType.KeyDown ? + FcitxKeyEventType.FCITX_PRESS_KEY : + FcitxKeyEventType.FCITX_RELEASE_KEY; + + return await _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state, (int)type, + (uint)args.Timestamp).ConfigureAwait(false); + } + + public override void SetOptions(TextInputOptionsQueryEventArgs options) => + Enqueue(async () => + { + if(_context == null) + return; + FcitxCapabilityFlags flags = default; + if (options.Lowercase) + flags |= FcitxCapabilityFlags.CAPACITY_LOWERCASE; + if (options.Uppercase) + flags |= FcitxCapabilityFlags.CAPACITY_UPPERCASE; + if (!options.AutoCapitalization) + flags |= FcitxCapabilityFlags.CAPACITY_NOAUTOUPPERCASE; + if (options.ContentType == TextInputContentType.Email) + flags |= FcitxCapabilityFlags.CAPACITY_EMAIL; + else if (options.ContentType == TextInputContentType.Number) + flags |= FcitxCapabilityFlags.CAPACITY_NUMBER; + else if (options.ContentType == TextInputContentType.Password) + flags |= FcitxCapabilityFlags.CAPACITY_PASSWORD; + else if (options.ContentType == TextInputContentType.Phone) + flags |= FcitxCapabilityFlags.CAPACITY_DIALABLE; + else if (options.ContentType == TextInputContentType.Url) + flags |= FcitxCapabilityFlags.CAPACITY_URL; + if (flags != _lastReportedFlags) + { + _lastReportedFlags = flags; + await _context.SetCapacityAsync((uint)flags); + } + }); + + private void OnForward((uint keyval, uint state, int type) ev) + { + var state = (FcitxKeyState)ev.state; + KeyModifiers mods = default; + if (state.HasFlagCustom(FcitxKeyState.FcitxKeyState_Ctrl)) + mods |= KeyModifiers.Control; + if (state.HasFlagCustom(FcitxKeyState.FcitxKeyState_Alt)) + mods |= KeyModifiers.Alt; + if (state.HasFlagCustom(FcitxKeyState.FcitxKeyState_Shift)) + mods |= KeyModifiers.Shift; + if (state.HasFlagCustom(FcitxKeyState.FcitxKeyState_Super)) + mods |= KeyModifiers.Meta; + FireForward(new X11InputMethodForwardedKey + { + Modifiers = mods, + KeyVal = (int)ev.keyval, + Type = ev.type == (int)FcitxKeyEventType.FCITX_PRESS_KEY ? + RawKeyEventType.KeyDown : + RawKeyEventType.KeyUp + }); + } + + private void OnCommitString(string s) => FireCommit(s); + } +} diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs new file mode 100644 index 0000000000..26c0d249f3 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Tmds.DBus; + +[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] +namespace Avalonia.FreeDesktop.DBusIme.IBus +{ + [DBusInterface("org.freedesktop.IBus.InputContext")] + interface IIBusInputContext : IDBusObject + { + Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State); + Task SetCursorLocationAsync(int X, int Y, int W, int H); + Task FocusInAsync(); + Task FocusOutAsync(); + Task ResetAsync(); + Task SetCapabilitiesAsync(uint Caps); + Task PropertyActivateAsync(string Name, int State); + Task SetEngineAsync(string Name); + Task GetEngineAsync(); + Task DestroyAsync(); + Task SetSurroundingTextAsync(object Text, uint CursorPos, uint AnchorPos); + Task WatchCommitTextAsync(Action cb, Action onError = null); + Task WatchForwardKeyEventAsync(Action<(uint keyval, uint keycode, uint state)> handler, Action onError = null); + Task WatchRequireSurroundingTextAsync(Action handler, Action onError = null); + Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchars)> handler, Action onError = null); + Task WatchUpdatePreeditTextAsync(Action<(object text, uint cursorPos, bool visible)> handler, Action onError = null); + Task WatchShowPreeditTextAsync(Action handler, Action onError = null); + Task WatchHidePreeditTextAsync(Action handler, Action onError = null); + Task WatchUpdateAuxiliaryTextAsync(Action<(object text, bool visible)> handler, Action onError = null); + Task WatchShowAuxiliaryTextAsync(Action handler, Action onError = null); + Task WatchHideAuxiliaryTextAsync(Action handler, Action onError = null); + Task WatchUpdateLookupTableAsync(Action<(object table, bool visible)> handler, Action onError = null); + Task WatchShowLookupTableAsync(Action handler, Action onError = null); + Task WatchHideLookupTableAsync(Action handler, Action onError = null); + Task WatchPageUpLookupTableAsync(Action handler, Action onError = null); + Task WatchPageDownLookupTableAsync(Action handler, Action onError = null); + Task WatchCursorUpLookupTableAsync(Action handler, Action onError = null); + Task WatchCursorDownLookupTableAsync(Action handler, Action onError = null); + Task WatchRegisterPropertiesAsync(Action handler, Action onError = null); + Task WatchUpdatePropertyAsync(Action handler, Action onError = null); + } + + + [DBusInterface("org.freedesktop.IBus.Portal")] + interface IIBusPortal : IDBusObject + { + Task CreateInputContextAsync(string Name); + } +} diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs new file mode 100644 index 0000000000..3070f51a8e --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs @@ -0,0 +1,45 @@ +using System; + +namespace Avalonia.FreeDesktop.DBusIme.IBus +{ + [Flags] + internal enum IBusModifierMask + { + ShiftMask = 1 << 0, + LockMask = 1 << 1, + ControlMask = 1 << 2, + Mod1Mask = 1 << 3, + Mod2Mask = 1 << 4, + Mod3Mask = 1 << 5, + Mod4Mask = 1 << 6, + Mod5Mask = 1 << 7, + Button1Mask = 1 << 8, + Button2Mask = 1 << 9, + Button3Mask = 1 << 10, + Button4Mask = 1 << 11, + Button5Mask = 1 << 12, + + HandledMask = 1 << 24, + ForwardMask = 1 << 25, + IgnoredMask = ForwardMask, + + SuperMask = 1 << 26, + HyperMask = 1 << 27, + MetaMask = 1 << 28, + + ReleaseMask = 1 << 30, + + ModifierMask = 0x5c001fff + } + + [Flags] + internal enum IBusCapability + { + CapPreeditText = 1 << 0, + CapAuxiliaryText = 1 << 1, + CapLookupTable = 1 << 2, + CapFocus = 1 << 3, + CapProperty = 1 << 4, + CapSurroundingText = 1 << 5, + } +} diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs new file mode 100644 index 0000000000..74f54267d0 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs @@ -0,0 +1,105 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; +using Tmds.DBus; + +namespace Avalonia.FreeDesktop.DBusIme.IBus +{ + internal class IBusX11TextInputMethod : DBusTextInputMethodBase + { + private IIBusInputContext _context; + + public IBusX11TextInputMethod(Connection connection) : base(connection, + "org.freedesktop.portal.IBus") + { + } + + protected override async Task Connect(string name) + { + var path = + await Connection.CreateProxy(name, "/org/freedesktop/IBus") + .CreateInputContextAsync(GetAppName()); + + _context = Connection.CreateProxy(name, path); + AddDisposable(await _context.WatchCommitTextAsync(OnCommitText)); + AddDisposable(await _context.WatchForwardKeyEventAsync(OnForwardKey)); + Enqueue(() => _context.SetCapabilitiesAsync((uint)IBusCapability.CapFocus)); + return true; + } + + private void OnForwardKey((uint keyval, uint keycode, uint state) k) + { + var state = (IBusModifierMask)k.state; + KeyModifiers mods = default; + if (state.HasFlagCustom(IBusModifierMask.ControlMask)) + mods |= KeyModifiers.Control; + if (state.HasFlagCustom(IBusModifierMask.Mod1Mask)) + mods |= KeyModifiers.Alt; + if (state.HasFlagCustom(IBusModifierMask.ShiftMask)) + mods |= KeyModifiers.Shift; + if (state.HasFlagCustom(IBusModifierMask.Mod4Mask)) + mods |= KeyModifiers.Meta; + FireForward(new X11InputMethodForwardedKey + { + KeyVal = (int)k.keyval, + Type = state.HasFlagCustom(IBusModifierMask.ReleaseMask) ? RawKeyEventType.KeyUp : RawKeyEventType.KeyDown, + Modifiers = mods + }); + } + + + private void OnCommitText(object wtf) + { + // Hello darkness, my old friend + var prop = wtf.GetType().GetField("Item3"); + if (prop != null) + { + var text = (string)prop.GetValue(wtf); + if (!string.IsNullOrEmpty(text)) + FireCommit(text); + } + } + + protected override Task Disconnect() => _context.DestroyAsync(); + + protected override void OnDisconnected() + { + _context = null; + base.OnDisconnected(); + } + + protected override Task SetCursorRectCore(PixelRect rect) + => _context.SetCursorLocationAsync(rect.X, rect.Y, rect.Width, rect.Height); + + protected override Task SetActiveCore(bool active) + => active ? _context.FocusInAsync() : _context.FocusOutAsync(); + + protected override Task ResetContextCore() + => _context.ResetAsync(); + + protected override Task HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode) + { + IBusModifierMask state = default; + if (args.Modifiers.HasFlagCustom(RawInputModifiers.Control)) + state |= IBusModifierMask.ControlMask; + if (args.Modifiers.HasFlagCustom(RawInputModifiers.Alt)) + state |= IBusModifierMask.Mod1Mask; + if (args.Modifiers.HasFlagCustom(RawInputModifiers.Shift)) + state |= IBusModifierMask.ShiftMask; + if (args.Modifiers.HasFlagCustom(RawInputModifiers.Meta)) + state |= IBusModifierMask.Mod4Mask; + + if (args.Type == RawKeyEventType.KeyUp) + state |= IBusModifierMask.ReleaseMask; + + return _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state); + } + + public override void SetOptions(TextInputOptionsQueryEventArgs options) + { + // No-op, because ibus + } + } +} diff --git a/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs b/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs new file mode 100644 index 0000000000..7f71ecf0ff --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using Avalonia.FreeDesktop.DBusIme.Fcitx; +using Avalonia.FreeDesktop.DBusIme.IBus; +using Tmds.DBus; + +namespace Avalonia.FreeDesktop.DBusIme +{ + public class X11DBusImeHelper + { + private static readonly Dictionary> KnownMethods = + new Dictionary> + { + ["fcitx"] = conn => + new DBusInputMethodFactory(_ => new FcitxX11TextInputMethod(conn)), + ["ibus"] = conn => + new DBusInputMethodFactory(_ => new IBusX11TextInputMethod(conn)) + }; + + static Func DetectInputMethod() + { + foreach (var name in new[] { "AVALONIA_IM_MODULE", "GTK_IM_MODULE", "QT_IM_MODULE" }) + { + var value = Environment.GetEnvironmentVariable(name); + + if (value == "none") + return null; + + if (value != null && KnownMethods.TryGetValue(value, out var factory)) + return factory; + } + + return null; + } + + public static bool DetectAndRegister() + { + var factory = DetectInputMethod(); + if (factory != null) + { + var conn = DBusHelper.TryInitialize(); + if (conn != null) + { + AvaloniaLocator.CurrentMutable.Bind().ToConstant(factory(conn)); + return true; + } + } + + return false; + + } + } +} diff --git a/src/Avalonia.FreeDesktop/IX11InputMethod.cs b/src/Avalonia.FreeDesktop/IX11InputMethod.cs new file mode 100644 index 0000000000..5d91118978 --- /dev/null +++ b/src/Avalonia.FreeDesktop/IX11InputMethod.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; + +namespace Avalonia.FreeDesktop +{ + public interface IX11InputMethodFactory + { + (ITextInputMethodImpl method, IX11InputMethodControl control) CreateClient(IntPtr xid); + } + + public struct X11InputMethodForwardedKey + { + public int KeyVal { get; set; } + public KeyModifiers Modifiers { get; set; } + public RawKeyEventType Type { get; set; } + } + + public interface IX11InputMethodControl : IDisposable + { + void SetWindowActive(bool active); + bool IsEnabled { get; } + ValueTask HandleEventAsync(RawKeyEventArgs args, int keyVal, int keyCode); + event Action Commit; + event Action ForwardKey; + + void UpdateWindowInfo(PixelPoint position, double scaling); + } +} diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 66fb9cfb1c..f3996cea76 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -5,6 +5,7 @@ using Avalonia.Controls; using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Input.GestureRecognizers; +using Avalonia.Input.TextInput; using Avalonia.Interactivity; using Avalonia.VisualTree; @@ -103,6 +104,22 @@ namespace Avalonia.Input RoutedEvent.Register( "TextInput", RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent TextInputMethodClientRequestedEvent = + RoutedEvent.Register( + "TextInputMethodClientRequested", + RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent TextInputOptionsQueryEvent = + RoutedEvent.Register( + "TextInputOptionsQuery", + RoutingStrategies.Tunnel | RoutingStrategies.Bubble); /// /// Defines the event. @@ -243,6 +260,24 @@ namespace Avalonia.Input add { AddHandler(TextInputEvent, value); } remove { RemoveHandler(TextInputEvent, value); } } + + /// + /// Occurs when an input element gains input focus and input method is looking for the corresponding client + /// + public event EventHandler TextInputMethodClientRequested + { + add { AddHandler(TextInputMethodClientRequestedEvent, value); } + remove { RemoveHandler(TextInputMethodClientRequestedEvent, value); } + } + + /// + /// Occurs when an input element gains input focus and input method is asking for required content options + /// + public event EventHandler TextInputOptionsQuery + { + add { AddHandler(TextInputOptionsQueryEvent, value); } + remove { RemoveHandler(TextInputOptionsQueryEvent, value); } + } /// /// Occurs when the pointer enters the control. diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs index 6f4cb7a35c..5899824c29 100644 --- a/src/Avalonia.Input/KeyboardDevice.cs +++ b/src/Avalonia.Input/KeyboardDevice.cs @@ -1,6 +1,7 @@ using System.ComponentModel; using System.Runtime.CompilerServices; using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; using Avalonia.Interactivity; using Avalonia.VisualTree; @@ -18,6 +19,10 @@ namespace Avalonia.Input public IInputManager InputManager => AvaloniaLocator.Current.GetService(); public IFocusManager FocusManager => AvaloniaLocator.Current.GetService(); + + // This should live in the FocusManager, but with the current outdated architecture + // the source of truth about the input focus is in KeyboardDevice + private readonly TextInputMethodManager _textInputManager = new TextInputMethodManager(); public IInputElement? FocusedElement { @@ -40,6 +45,7 @@ namespace Avalonia.Input } RaisePropertyChanged(); + _textInputManager.SetFocusedElement(value); } } diff --git a/src/Avalonia.Input/TextInput/ITextInputMethodClient.cs b/src/Avalonia.Input/TextInput/ITextInputMethodClient.cs new file mode 100644 index 0000000000..d385f5b162 --- /dev/null +++ b/src/Avalonia.Input/TextInput/ITextInputMethodClient.cs @@ -0,0 +1,60 @@ +using System; +using Avalonia.VisualTree; + +namespace Avalonia.Input.TextInput +{ + public interface ITextInputMethodClient + { + /// + /// The cursor rectangle relative to the TextViewVisual + /// + Rect CursorRectangle { get; } + /// + /// Should be fired when cursor rectangle is changed inside the TextViewVisual + /// + event EventHandler CursorRectangleChanged; + /// + /// The visual that's showing the text + /// + IVisual TextViewVisual { get; } + /// + /// Should be fired when text-hosting visual is changed + /// + event EventHandler TextViewVisualChanged; + /// + /// Indicates if TextViewVisual is capable of displaying non-commited input on the cursor position + /// + bool SupportsPreedit { get; } + /// + /// Sets the non-commited input string + /// + void SetPreeditText(string text); + /// + /// Indicates if text input client is capable of providing the text around the cursor + /// + bool SupportsSurroundingText { get; } + /// + /// Returns the text around the cursor, usually the current paragraph, the cursor position inside that text and selection start position + /// + TextInputMethodSurroundingText SurroundingText { get; } + /// + /// Should be fired when surrounding text changed + /// + event EventHandler SurroundingTextChanged; + /// + /// Returns the text before the cursor. Must return a non-empty string if cursor is not at the beginning of the text entry + /// + string TextBeforeCursor { get; } + /// + /// Returns the text before the cursor. Must return a non-empty string if cursor is not at the end of the text entry + /// + string TextAfterCursor { get; } + } + + public struct TextInputMethodSurroundingText + { + public string Text { get; set; } + public int CursorOffset { get; set; } + public int AnchorOffset { get; set; } + } +} diff --git a/src/Avalonia.Input/TextInput/ITextInputMethodImpl.cs b/src/Avalonia.Input/TextInput/ITextInputMethodImpl.cs new file mode 100644 index 0000000000..0069314d28 --- /dev/null +++ b/src/Avalonia.Input/TextInput/ITextInputMethodImpl.cs @@ -0,0 +1,15 @@ +namespace Avalonia.Input.TextInput +{ + public interface ITextInputMethodImpl + { + void SetActive(bool active); + void SetCursorRect(Rect rect); + void SetOptions(TextInputOptionsQueryEventArgs options); + void Reset(); + } + + public interface ITextInputMethodRoot : IInputRoot + { + ITextInputMethodImpl InputMethod { get; } + } +} diff --git a/src/Avalonia.Input/TextInput/InputMethodManager.cs b/src/Avalonia.Input/TextInput/InputMethodManager.cs new file mode 100644 index 0000000000..207ba6096e --- /dev/null +++ b/src/Avalonia.Input/TextInput/InputMethodManager.cs @@ -0,0 +1,101 @@ +using System; +using Avalonia.VisualTree; + +namespace Avalonia.Input.TextInput +{ + internal class TextInputMethodManager + { + private ITextInputMethodImpl? _im; + private IInputElement? _focusedElement; + private ITextInputMethodClient? _client; + private readonly TransformTrackingHelper _transformTracker = new TransformTrackingHelper(); + + public TextInputMethodManager() => _transformTracker.MatrixChanged += UpdateCursorRect; + + private ITextInputMethodClient? Client + { + get => _client; + set + { + if(_client == value) + return; + if (_client != null) + { + _client.CursorRectangleChanged -= OnCursorRectangleChanged; + _client.TextViewVisualChanged -= OnTextViewVisualChanged; + } + + _client = value; + + if (_client != null) + { + _client.CursorRectangleChanged += OnCursorRectangleChanged; + _client.TextViewVisualChanged += OnTextViewVisualChanged; + var optionsQuery = new TextInputOptionsQueryEventArgs + { + RoutedEvent = InputElement.TextInputOptionsQueryEvent + }; + _focusedElement?.RaiseEvent(optionsQuery); + _im?.Reset(); + _im?.SetOptions(optionsQuery); + _transformTracker?.SetVisual(_client?.TextViewVisual); + UpdateCursorRect(); + _im?.SetActive(true); + } + else + { + _im?.SetActive(false); + _transformTracker.SetVisual(null); + } + } + } + + private void OnTextViewVisualChanged(object sender, EventArgs e) + => _transformTracker.SetVisual(_client?.TextViewVisual); + + private void UpdateCursorRect() + { + if (_im == null || _client == null || _focusedElement?.VisualRoot == null) + return; + var transform = _focusedElement.TransformToVisual(_focusedElement.VisualRoot); + if (transform == null) + _im.SetCursorRect(default); + else + _im.SetCursorRect(_client.CursorRectangle.TransformToAABB(transform.Value)); + } + + private void OnCursorRectangleChanged(object sender, EventArgs e) + { + if (sender == _client) + UpdateCursorRect(); + } + + public void SetFocusedElement(IInputElement? element) + { + if(_focusedElement == element) + return; + _focusedElement = element; + + var inputMethod = (element?.VisualRoot as ITextInputMethodRoot)?.InputMethod; + if(_im != inputMethod) + _im?.SetActive(false); + + _im = inputMethod; + + if (_focusedElement == null || _im == null) + { + Client = null; + _im?.SetActive(false); + return; + } + + var clientQuery = new TextInputMethodClientRequestedEventArgs + { + RoutedEvent = InputElement.TextInputMethodClientRequestedEvent + }; + + _focusedElement.RaiseEvent(clientQuery); + Client = clientQuery.Client; + } + } +} diff --git a/src/Avalonia.Input/TextInput/TextInputContentType.cs b/src/Avalonia.Input/TextInput/TextInputContentType.cs new file mode 100644 index 0000000000..5d73fc1552 --- /dev/null +++ b/src/Avalonia.Input/TextInput/TextInputContentType.cs @@ -0,0 +1,12 @@ +namespace Avalonia.Input.TextInput +{ + public enum TextInputContentType + { + Normal = 0, + Email = 1, + Phone = 2, + Number = 3, + Url = 4, + Password = 5 + } +} diff --git a/src/Avalonia.Input/TextInput/TextInputMethodClientRequestedEventArgs.cs b/src/Avalonia.Input/TextInput/TextInputMethodClientRequestedEventArgs.cs new file mode 100644 index 0000000000..bec43487d2 --- /dev/null +++ b/src/Avalonia.Input/TextInput/TextInputMethodClientRequestedEventArgs.cs @@ -0,0 +1,12 @@ +using Avalonia.Interactivity; + +namespace Avalonia.Input.TextInput +{ + public class TextInputMethodClientRequestedEventArgs : RoutedEventArgs + { + /// + /// Set this property to a valid text input client to enable input method interaction + /// + public ITextInputMethodClient? Client { get; set; } + } +} diff --git a/src/Avalonia.Input/TextInput/TextInputOptionsQueryEventArgs.cs b/src/Avalonia.Input/TextInput/TextInputOptionsQueryEventArgs.cs new file mode 100644 index 0000000000..924d0eb166 --- /dev/null +++ b/src/Avalonia.Input/TextInput/TextInputOptionsQueryEventArgs.cs @@ -0,0 +1,32 @@ +using Avalonia.Interactivity; + +namespace Avalonia.Input.TextInput +{ + public class TextInputOptionsQueryEventArgs : RoutedEventArgs + { + /// + /// The content type (mostly for determining the shape of the virtual keyboard) + /// + public TextInputContentType ContentType { get; set; } + /// + /// Text is multiline + /// + public bool Multiline { get; set; } + /// + /// Text is in lower case + /// + public bool Lowercase { get; set; } + /// + /// Text is in upper case + /// + public bool Uppercase { get; set; } + /// + /// Automatically capitalize letters at the start of the sentence + /// + public bool AutoCapitalization { get; set; } + /// + /// Text contains sensitive data like card numbers and should not be stored + /// + public bool IsSensitive { get; set; } + } +} diff --git a/src/Avalonia.Input/TextInput/TransformTrackingHelper.cs b/src/Avalonia.Input/TextInput/TransformTrackingHelper.cs new file mode 100644 index 0000000000..4211360a8f --- /dev/null +++ b/src/Avalonia.Input/TextInput/TransformTrackingHelper.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using Avalonia.Threading; +using Avalonia.VisualTree; + +namespace Avalonia.Input.TextInput +{ + class TransformTrackingHelper : IDisposable + { + private IVisual? _visual; + private bool _queuedForUpdate; + private readonly EventHandler _propertyChangedHandler; + private readonly List _propertyChangedSubscriptions = new List(); + + public TransformTrackingHelper() + { + _propertyChangedHandler = PropertyChangedHandler; + } + + public void SetVisual(IVisual? visual) + { + Dispose(); + _visual = visual; + if (visual != null) + { + visual.AttachedToVisualTree += OnAttachedToVisualTree; + visual.DetachedFromVisualTree -= OnDetachedFromVisualTree; + if (visual.IsAttachedToVisualTree) + SubscribeToParents(); + UpdateMatrix(); + } + } + + public Matrix? Matrix { get; private set; } + public event Action? MatrixChanged; + + public void Dispose() + { + if(_visual == null) + return; + UnsubscribeFromParents(); + _visual.AttachedToVisualTree -= OnAttachedToVisualTree; + _visual.DetachedFromVisualTree -= OnDetachedFromVisualTree; + _visual = null; + } + + private void SubscribeToParents() + { + var visual = _visual; + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + // false positive + while (visual != null) + { + if (visual is Visual v) + { + v.PropertyChanged += _propertyChangedHandler; + _propertyChangedSubscriptions.Add(v); + } + + visual = visual.VisualParent; + } + } + + private void UnsubscribeFromParents() + { + foreach (var v in _propertyChangedSubscriptions) + v.PropertyChanged -= _propertyChangedHandler; + _propertyChangedSubscriptions.Clear(); + } + + void UpdateMatrix() + { + Matrix? matrix = null; + if (_visual != null && _visual.VisualRoot != null) + matrix = _visual.TransformToVisual(_visual.VisualRoot); + if (Matrix != matrix) + { + Matrix = matrix; + MatrixChanged?.Invoke(); + } + } + + private void OnAttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs visualTreeAttachmentEventArgs) + { + SubscribeToParents(); + UpdateMatrix(); + } + + private void EnqueueForUpdate() + { + if(_queuedForUpdate) + return; + _queuedForUpdate = true; + Dispatcher.UIThread.Post(UpdateMatrix, DispatcherPriority.Render); + } + + private void PropertyChangedHandler(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.IsEffectiveValueChange && e.Property == Visual.BoundsProperty) + EnqueueForUpdate(); + } + + private void OnDetachedFromVisualTree(object sender, VisualTreeAttachmentEventArgs visualTreeAttachmentEventArgs) + { + UnsubscribeFromParents(); + UpdateMatrix(); + } + } +} diff --git a/src/Avalonia.Themes.Default/CalendarDatePicker.xaml b/src/Avalonia.Themes.Default/CalendarDatePicker.xaml index bc1aba1a03..aab7d06c46 100644 --- a/src/Avalonia.Themes.Default/CalendarDatePicker.xaml +++ b/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"/>