diff --git a/Avalonia.sln b/Avalonia.sln index 4e7b4cc318..0354e20d4f 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -60,20 +60,17 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DEF5-D50F-4975-8B72-124C9EB54066}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI", "src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj", "{6417B24E-49C2-4985-8DB2-3AB9D898EC91}" EndProject -Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "PlatformSupport", "src\Shared\PlatformSupport\PlatformSupport.shproj", "{E4D9629C-F168-4224-3F51-A5E482FFBC42}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup", "src\Markup\Avalonia.Markup\Avalonia.Markup.csproj", "{6417E941-21BC-467B-A771-0DE389353CE6}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.UnitTests", "tests\Avalonia.Markup.UnitTests\Avalonia.Markup.UnitTests.csproj", "{8EF392D5-1416-45AA-9956-7CBBC3229E8A}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BindingDemo", "samples\BindingDemo\BindingDemo.csproj", "{08B3E6B9-1CD5-443C-9F61-6D49D1C5F162}" EndProject -Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "RenderHelpers", "src\Shared\RenderHelpers\RenderHelpers.shproj", "{3C4C0CB4-0C0F-4450-A37B-148C84FF905F}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Skia", "Skia", "{3743B0F2-CC41-4F14-A8C8-267F579BF91E}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Android", "Android", "{7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F}" @@ -235,15 +232,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsInteropTest", "sampl EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlSamples", "samples\SampleControls\ControlSamples.csproj", "{A0D0A6A4-5C72-4ADA-9B27-621C7D94F270}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.PlatformSupport", "src\Avalonia.PlatformSupport\Avalonia.PlatformSupport.csproj", "{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}" +EndProject Global - GlobalSection(SharedMSBuildProjectFiles) = preSolution - src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 - src\Shared\RenderHelpers\RenderHelpers.projitems*{3e908f67-5543-4879-a1dc-08eace79b3cd}*SharedItemsImports = 5 - src\Shared\PlatformSupport\PlatformSupport.projitems*{7b92af71-6287-4693-9dcb-bd5b6e927e23}*SharedItemsImports = 5 - src\Shared\RenderHelpers\RenderHelpers.projitems*{7d2d3083-71dd-4cc9-8907-39a0d86fb322}*SharedItemsImports = 5 - src\Shared\PlatformSupport\PlatformSupport.projitems*{88060192-33d5-4932-b0f9-8bd2763e857d}*SharedItemsImports = 5 - src\Shared\PlatformSupport\PlatformSupport.projitems*{e4d9629c-f168-4224-3f51-a5e482ffbc42}*SharedItemsImports = 13 - EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Ad-Hoc|Any CPU = Ad-Hoc|Any CPU Ad-Hoc|iPhone = Ad-Hoc|iPhone @@ -2169,6 +2160,30 @@ Global {A0D0A6A4-5C72-4ADA-9B27-621C7D94F270}.Release|iPhone.Build.0 = Release|Any CPU {A0D0A6A4-5C72-4ADA-9B27-621C7D94F270}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {A0D0A6A4-5C72-4ADA-9B27-621C7D94F270}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.AppStore|iPhone.Build.0 = Debug|Any CPU + {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Debug|iPhone.Build.0 = Debug|Any CPU + {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Release|Any CPU.Build.0 = Release|Any CPU + {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Release|iPhone.ActiveCfg = Release|Any CPU + {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Release|iPhone.Build.0 = Release|Any CPU + {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2187,11 +2202,9 @@ Global {EFB11458-9CDF-41C0-BE4F-44AF45A4CAB8} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {99135EAB-653D-47E4-A378-C96E1278CA44} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {3E53A01A-B331-47F3-B828-4A5717E77A24} = {8B6A8209-894F-4BA1-B880-965FD453982C} - {E4D9629C-F168-4224-3F51-A5E482FFBC42} = {A689DEF5-D50F-4975-8B72-124C9EB54066} {6417E941-21BC-467B-A771-0DE389353CE6} = {8B6A8209-894F-4BA1-B880-965FD453982C} {8EF392D5-1416-45AA-9956-7CBBC3229E8A} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {08B3E6B9-1CD5-443C-9F61-6D49D1C5F162} = {9B9E3891-2366-4253-A952-D08BCEB71098} - {3C4C0CB4-0C0F-4450-A37B-148C84FF905F} = {A689DEF5-D50F-4975-8B72-124C9EB54066} {7B92AF71-6287-4693-9DCB-BD5B6E927E23} = {7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F} {FF69B927-C545-49AE-8E16-3D14D621AA12} = {7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F} {4488AD85-1495-4809-9AA4-DDFE0A48527E} = {0CB0B92E-6CFF-4240-80A5-CCAFE75D91E1} diff --git a/build/CoreLibraries.props b/build/CoreLibraries.props index fff00041c3..3fccad2641 100644 --- a/build/CoreLibraries.props +++ b/build/CoreLibraries.props @@ -17,5 +17,6 @@ + diff --git a/build/SharedVersion.props b/build/SharedVersion.props index 7d75901288..7f24ef35bc 100644 --- a/build/SharedVersion.props +++ b/build/SharedVersion.props @@ -3,7 +3,7 @@ Avalonia 0.10.999 - Copyright 2021 © The AvaloniaUI Project + Copyright 2022 © The AvaloniaUI Project https://avaloniaui.net https://github.com/AvaloniaUI/Avalonia/ true diff --git a/build/SourceLink.props b/build/SourceLink.props index 1e007e01eb..9f05848881 100644 --- a/build/SourceLink.props +++ b/build/SourceLink.props @@ -3,7 +3,6 @@ true false true - embedded $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb @@ -15,6 +14,10 @@ true + + embedded + + diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 0b88908252..40180274e1 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1627,6 +1627,19 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent return; } } + else if (type == Magnify) + { + delta.X = delta.Y = [event magnification]; + } + else if (type == Rotate) + { + delta.X = delta.Y = [event rotation]; + } + else if (type == Swipe) + { + delta.X = [event deltaX]; + delta.Y = [event deltaY]; + } auto timestamp = [event timestamp] * 1000; auto modifiers = [self getModifiers:[event modifierFlags]]; @@ -1753,6 +1766,24 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [super scrollWheel:event]; } +- (void)magnifyWithEvent:(NSEvent *)event +{ + [self mouseEvent:event withType:Magnify]; + [super magnifyWithEvent:event]; +} + +- (void)rotateWithEvent:(NSEvent *)event +{ + [self mouseEvent:event withType:Rotate]; + [super rotateWithEvent:event]; +} + +- (void)swipeWithEvent:(NSEvent *)event +{ + [self mouseEvent:event withType:Swipe]; + [super swipeWithEvent:event]; +} + - (void)mouseEntered:(NSEvent *)event { _isMouseOver = true; diff --git a/samples/ControlCatalog.Android/Assets/AboutAssets.txt b/samples/ControlCatalog.Android/Assets/AboutAssets.txt index ee39886295..a9b0638eb1 100644 --- a/samples/ControlCatalog.Android/Assets/AboutAssets.txt +++ b/samples/ControlCatalog.Android/Assets/AboutAssets.txt @@ -1,7 +1,7 @@ Any raw assets you want to be deployed with your application can be placed in this directory (and child directories) and given a Build Action of "AndroidAsset". -These files will be deployed with you package and will be accessible using Android's +These files will be deployed with your package and will be accessible using Android's AssetManager, like this: public class ReadAsset : Activity @@ -16,4 +16,4 @@ public class ReadAsset : Activity Additionally, some Android functions will automatically load asset files: -Typeface tf = Typeface.CreateFromAsset (Context.Assets, "fonts/samplefont.ttf"); \ No newline at end of file +Typeface tf = Typeface.CreateFromAsset (Context.Assets, "fonts/samplefont.ttf"); diff --git a/samples/ControlCatalog.Web/ControlCatalog.Web.csproj b/samples/ControlCatalog.Web/ControlCatalog.Web.csproj index d463dfa84a..199fa85ad2 100644 --- a/samples/ControlCatalog.Web/ControlCatalog.Web.csproj +++ b/samples/ControlCatalog.Web/ControlCatalog.Web.csproj @@ -2,6 +2,7 @@ net6.0 enable + True diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 227b31bf20..d0e1bd885e 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -3,6 +3,7 @@ xmlns:vm="using:ControlCatalog.ViewModels" x:DataType="vm:ApplicationViewModel" x:CompileBindings="True" + Name="Avalonia ControlCatalog" x:Class="ControlCatalog.App"> - + + + + diff --git a/samples/RenderDemo/Pages/ClippingPage.xaml.cs b/samples/RenderDemo/Pages/ClippingPage.xaml.cs index 5357181838..c5b669343a 100644 --- a/samples/RenderDemo/Pages/ClippingPage.xaml.cs +++ b/samples/RenderDemo/Pages/ClippingPage.xaml.cs @@ -1,35 +1,18 @@ -using System; -using System.Reactive.Linq; -using Avalonia; -using Avalonia.Animation; using Avalonia.Controls; -using Avalonia.Data; using Avalonia.Markup.Xaml; -using Avalonia.Media; namespace RenderDemo.Pages { public class ClippingPage : UserControl { - private Geometry _clip; - public ClippingPage() { InitializeComponent(); - WireUpCheckbox(); } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } - - private void WireUpCheckbox() - { - var useMask = this.FindControl("useMask"); - var clipped = this.FindControl("clipped"); - _clip = clipped.Clip; - useMask.Click += (s, e) => clipped.Clip = clipped.Clip == null ? _clip : null; - } } } diff --git a/samples/RenderDemo/Pages/CustomSkiaPage.cs b/samples/RenderDemo/Pages/CustomSkiaPage.cs index 2e59d934a1..9c524a7932 100644 --- a/samples/RenderDemo/Pages/CustomSkiaPage.cs +++ b/samples/RenderDemo/Pages/CustomSkiaPage.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.Globalization; using Avalonia; using Avalonia.Controls; using Avalonia.Media; @@ -41,7 +42,10 @@ namespace RenderDemo.Pages { var canvas = (context as ISkiaDrawingContextImpl)?.SkCanvas; if (canvas == null) - context.DrawText(Brushes.Black, new Point(), _noSkia.PlatformImpl); + using (var c = new DrawingContext(context, false)) + { + c.DrawText(_noSkia, new Point()); + } else { canvas.Save(); @@ -108,10 +112,9 @@ namespace RenderDemo.Pages public override void Render(DrawingContext context) { - var noSkia = new FormattedText() - { - Text = "Current rendering API is not Skia" - }; + var noSkia = new FormattedText("Current rendering API is not Skia", CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, Typeface.Default, 12, Brushes.Black); + context.Custom(new CustomDrawOp(new Rect(0, 0, Bounds.Width, Bounds.Height), noSkia)); Dispatcher.UIThread.InvokeAsync(InvalidateVisual, DispatcherPriority.Background); } diff --git a/samples/RenderDemo/Pages/FormattedTextPage.axaml b/samples/RenderDemo/Pages/FormattedTextPage.axaml new file mode 100644 index 0000000000..92775bec9e --- /dev/null +++ b/samples/RenderDemo/Pages/FormattedTextPage.axaml @@ -0,0 +1,7 @@ + + diff --git a/samples/RenderDemo/Pages/FormattedTextPage.axaml.cs b/samples/RenderDemo/Pages/FormattedTextPage.axaml.cs new file mode 100644 index 0000000000..25e29c67a9 --- /dev/null +++ b/samples/RenderDemo/Pages/FormattedTextPage.axaml.cs @@ -0,0 +1,60 @@ +using System.Globalization; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Media; + +namespace RenderDemo.Pages +{ + public class FormattedTextPage : UserControl + { + public FormattedTextPage() + { + this.InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + public override void Render(DrawingContext context) + { + const string testString = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor"; + + // Create the initial formatted text string. + var formattedText = new FormattedText( + testString, + CultureInfo.GetCultureInfo("en-us"), + FlowDirection.LeftToRight, + new Typeface("Verdana"), + 32, + Brushes.Black) { MaxTextWidth = 300, MaxTextHeight = 240 }; + + // Set a maximum width and height. If the text overflows these values, an ellipsis "..." appears. + + // Use a larger font size beginning at the first (zero-based) character and continuing for 5 characters. + // The font size is calculated in terms of points -- not as device-independent pixels. + formattedText.SetFontSize(36 * (96.0 / 72.0), 0, 5); + + // Use a Bold font weight beginning at the 6th character and continuing for 11 characters. + formattedText.SetFontWeight(FontWeight.Bold, 6, 11); + + var gradient = new LinearGradientBrush + { + GradientStops = + new GradientStops { new GradientStop(Colors.Orange, 0), new GradientStop(Colors.Teal, 1) }, + StartPoint = new RelativePoint(0,0, RelativeUnit.Relative), + EndPoint = new RelativePoint(0,1, RelativeUnit.Relative) + }; + + // Use a linear gradient brush beginning at the 6th character and continuing for 11 characters. + formattedText.SetForegroundBrush(gradient, 6, 11); + + // Use an Italic font style beginning at the 28th character and continuing for 28 characters. + formattedText.SetFontStyle(FontStyle.Italic, 28, 28); + + context.DrawText(formattedText, new Point(10, 0)); + } + } +} diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs index 857358f6b2..7f85606957 100644 --- a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs +++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs @@ -13,6 +13,7 @@ namespace RenderDemo.Pages private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface; private readonly Random _rand = new Random(); private ushort[] _glyphIndices = new ushort[1]; + private char[] _characters = new char[1]; private float _fontSize = 20; private int _direction = 10; @@ -38,7 +39,7 @@ namespace RenderDemo.Pages private void UpdateGlyphRun() { - var c = (uint)_rand.Next(65, 90); + var c = (char)_rand.Next(65, 90); if (_fontSize + _direction > 200) { @@ -54,6 +55,8 @@ namespace RenderDemo.Pages _glyphIndices[0] = _glyphTypeface.GetGlyph(c); + _characters[0] = c; + var scale = (double)_fontSize / _glyphTypeface.DesignEmHeight; var drawingGroup = new DrawingGroup(); @@ -61,7 +64,7 @@ namespace RenderDemo.Pages var glyphRunDrawing = new GlyphRunDrawing { Foreground = Brushes.Black, - GlyphRun = new GlyphRun(_glyphTypeface, _fontSize, _glyphIndices), + GlyphRun = new GlyphRun(_glyphTypeface, _fontSize, _characters, _glyphIndices) }; drawingGroup.Children.Add(glyphRunDrawing); diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index 6a940a54f1..2d4f6a305f 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -10,7 +10,7 @@ using Avalonia.Input.Platform; using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Rendering; -using Avalonia.Shared.PlatformSupport; +using Avalonia.PlatformSupport; using Avalonia.Skia; namespace Avalonia @@ -59,8 +59,7 @@ namespace Avalonia.Android .Bind().ToSingleton() .Bind().ToConstant(new ChoreographerTimer()) .Bind().ToConstant(new RenderLoop()) - .Bind().ToSingleton() - .Bind().ToConstant(new AssetLoader(appType.Assembly)); + .Bind().ToSingleton(); SkiaPlatform.Initialize(); diff --git a/src/Android/Avalonia.Android/AppBuilder.cs b/src/Android/Avalonia.Android/AppBuilder.cs index 805bb61655..04f1ff00d0 100644 --- a/src/Android/Avalonia.Android/AppBuilder.cs +++ b/src/Android/Avalonia.Android/AppBuilder.cs @@ -1,5 +1,5 @@ using Avalonia.Controls; -using Avalonia.Shared.PlatformSupport; +using Avalonia.PlatformSupport; namespace Avalonia { diff --git a/src/Android/Avalonia.Android/Avalonia.Android.csproj b/src/Android/Avalonia.Android/Avalonia.Android.csproj index 8c6775733f..5c33dbcea6 100644 --- a/src/Android/Avalonia.Android/Avalonia.Android.csproj +++ b/src/Android/Avalonia.Android/Avalonia.Android.csproj @@ -5,9 +5,11 @@ + + TargetFramework=netstandard2.0 + - diff --git a/src/Android/Avalonia.Android/RuntimeInfo.cs b/src/Android/Avalonia.Android/RuntimeInfo.cs deleted file mode 100644 index bb2466c357..0000000000 --- a/src/Android/Avalonia.Android/RuntimeInfo.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Avalonia.Platform; - -namespace Avalonia.Shared.PlatformSupport -{ - internal partial class StandardRuntimePlatform - { - public RuntimePlatformInfo GetRuntimeInfo() => new RuntimePlatformInfo - { - IsCoreClr = false, - IsDesktop = false, - IsMobile = true, - IsDotNetFramework = false, - IsMono = true, - IsUnix = true, - OperatingSystem = OperatingSystemType.Android - }; - } -} \ No newline at end of file diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 4811028f85..50fc5ac73b 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -157,7 +157,7 @@ namespace Avalonia.Animation state.Instance?.Dispose(); state.Instance = transition.Apply( this, - Clock ?? AvaloniaLocator.Current.GetService(), + Clock ?? AvaloniaLocator.Current.GetRequiredService(), oldValue, newValue); return; @@ -169,7 +169,7 @@ namespace Avalonia.Animation base.OnPropertyChangedCore(change); } - private void TransitionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + private void TransitionsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { if (!_transitionsEnabled) { @@ -179,14 +179,14 @@ namespace Avalonia.Animation switch (e.Action) { case NotifyCollectionChangedAction.Add: - AddTransitions(e.NewItems); + AddTransitions(e.NewItems!); break; case NotifyCollectionChangedAction.Remove: - RemoveTransitions(e.OldItems); + RemoveTransitions(e.OldItems!); break; case NotifyCollectionChangedAction.Replace: - RemoveTransitions(e.OldItems); - AddTransitions(e.NewItems); + RemoveTransitions(e.OldItems!); + AddTransitions(e.NewItems!); break; case NotifyCollectionChangedAction.Reset: throw new NotSupportedException("Transitions collection cannot be reset."); @@ -204,7 +204,7 @@ namespace Avalonia.Animation for (var i = 0; i < items.Count; ++i) { - var t = (ITransition)items[i]; + var t = (ITransition)items[i]!; _transitionState.Add(t, new TransitionState { @@ -222,7 +222,7 @@ namespace Avalonia.Animation for (var i = 0; i < items.Count; ++i) { - var t = (ITransition)items[i]; + var t = (ITransition)items[i]!; if (_transitionState.TryGetValue(t, out var state)) { diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index a4515db514..03b2d17e44 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -203,7 +203,7 @@ namespace Avalonia.Animation /// /// The animation setter. /// The property animator type. - public static Type GetAnimator(IAnimationSetter setter) + public static Type? GetAnimator(IAnimationSetter setter) { if (s_animators.TryGetValue(setter, out var type)) { @@ -254,7 +254,7 @@ namespace Avalonia.Animation Animators.Insert(0, (condition, typeof(TAnimator))); } - private static Type GetAnimatorType(AvaloniaProperty property) + private static Type? GetAnimatorType(AvaloniaProperty property) { foreach (var (condition, type) in Animators) { @@ -276,6 +276,11 @@ namespace Avalonia.Animation { foreach (var setter in keyframe.Setters) { + if (setter.Property is null) + { + throw new InvalidOperationException("No Setter property assigned."); + } + var handler = Animation.GetAnimator(setter) ?? GetAnimatorType(setter.Property); if (handler == null) @@ -305,7 +310,7 @@ namespace Avalonia.Animation foreach (var (handlerType, property) in handlerList) { - var newInstance = (IAnimator)Activator.CreateInstance(handlerType); + var newInstance = (IAnimator)Activator.CreateInstance(handlerType)!; newInstance.Property = property; newAnimatorInstances.Add(newInstance); } @@ -321,32 +326,43 @@ namespace Avalonia.Animation } /// - public IDisposable Apply(Animatable control, IClock clock, IObservable match, Action onComplete) + public IDisposable Apply(Animatable control, IClock? clock, IObservable match, Action? onComplete) { var (animators, subscriptions) = InterpretKeyframes(control); if (animators.Count == 1) { - subscriptions.Add(animators[0].Apply(this, control, clock, match, onComplete)); + var subscription = animators[0].Apply(this, control, clock, match, onComplete); + + if (subscription is not null) + { + subscriptions.Add(subscription); + } } else { var completionTasks = onComplete != null ? new List() : null; foreach (IAnimator animator in animators) { - Action animatorOnComplete = null; + Action? animatorOnComplete = null; if (onComplete != null) { - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(); animatorOnComplete = () => tcs.SetResult(null); - completionTasks.Add(tcs.Task); + completionTasks!.Add(tcs.Task); + } + + var subscription = animator.Apply(this, control, clock, match, animatorOnComplete); + + if (subscription is not null) + { + subscriptions.Add(subscription); } - subscriptions.Add(animator.Apply(this, control, clock, match, animatorOnComplete)); } if (onComplete != null) { - Task.WhenAll(completionTasks).ContinueWith( - (_, state) => ((Action)state).Invoke(), + Task.WhenAll(completionTasks!).ContinueWith( + (_, state) => ((Action)state!).Invoke(), onComplete); } } @@ -354,25 +370,25 @@ namespace Avalonia.Animation } /// - public Task RunAsync(Animatable control, IClock clock = null) + public Task RunAsync(Animatable control, IClock? clock = null) { return RunAsync(control, clock, default); } /// - public Task RunAsync(Animatable control, IClock clock = null, CancellationToken cancellationToken = default) + public Task RunAsync(Animatable control, IClock? clock = null, CancellationToken cancellationToken = default) { if (cancellationToken.IsCancellationRequested) { return Task.CompletedTask; } - var run = new TaskCompletionSource(); + var run = new TaskCompletionSource(); if (this.IterationCount == IterationCount.Infinite) run.SetException(new InvalidOperationException("Looping animations must not use the Run method.")); - IDisposable subscriptions = null, cancellation = null; + IDisposable? subscriptions = null, cancellation = null; subscriptions = this.Apply(control, clock, Observable.Return(true), () => { run.TrySetResult(null); diff --git a/src/Avalonia.Animation/AnimationInstance`1.cs b/src/Avalonia.Animation/AnimationInstance`1.cs index cf79640150..52cd4b324f 100644 --- a/src/Avalonia.Animation/AnimationInstance`1.cs +++ b/src/Avalonia.Animation/AnimationInstance`1.cs @@ -31,15 +31,15 @@ namespace Avalonia.Animation private TimeSpan _initialDelay; private TimeSpan _iterationDelay; private TimeSpan _duration; - private Easings.Easing _easeFunc; - private Action _onCompleteAction; + private Easings.Easing? _easeFunc; + private Action? _onCompleteAction; private Func _interpolator; - private IDisposable _timerSub; + private IDisposable? _timerSub; private readonly IClock _baseClock; - private IClock _clock; - private EventHandler _propertyChangedDelegate; + private IClock? _clock; + private EventHandler? _propertyChangedDelegate; - public AnimationInstance(Animation animation, Animatable control, Animator animator, IClock baseClock, Action OnComplete, Func Interpolator) + public AnimationInstance(Animation animation, Animatable control, Animator animator, IClock baseClock, Action? OnComplete, Func Interpolator) { _animator = animator; _animation = animation; @@ -47,6 +47,9 @@ namespace Avalonia.Animation _onCompleteAction = OnComplete; _interpolator = Interpolator; _baseClock = baseClock; + _lastInterpValue = default!; + _firstKFValue = default!; + _neutralValue = default!; FetchProperties(); } @@ -82,7 +85,7 @@ namespace Avalonia.Animation _targetControl.PropertyChanged -= _propertyChangedDelegate; _timerSub?.Dispose(); - _clock.PlayState = PlayState.Stop; + _clock!.PlayState = PlayState.Stop; } protected override void Subscribed() @@ -108,6 +111,8 @@ namespace Avalonia.Animation private void ApplyFinalFill() { + if (_animator.Property is null) + throw new InvalidOperationException("Animator has no property specified."); if (_fillMode == FillMode.Forward || _fillMode == FillMode.Both) _targetControl.SetValue(_animator.Property, _lastInterpValue, BindingPriority.LocalValue); } @@ -130,12 +135,12 @@ namespace Avalonia.Animation private void DoPlayStates() { - if (_clock.PlayState == PlayState.Stop || _baseClock.PlayState == PlayState.Stop) + if (_clock!.PlayState == PlayState.Stop || _baseClock.PlayState == PlayState.Stop) DoComplete(); if (!_gotFirstKFValue) { - _firstKFValue = (T)_animator.First().Value; + _firstKFValue = (T)_animator.First().Value!; _gotFirstKFValue = true; } } @@ -169,7 +174,7 @@ namespace Avalonia.Animation // and snap the last iteration value to exact values. if ((_currentIteration + 1) > _iterationCount) { - var easedTime = _easeFunc.Ease(_playbackReversed ? 0.0 : 1.0); + var easedTime = _easeFunc!.Ease(_playbackReversed ? 0.0 : 1.0); _lastInterpValue = _interpolator(easedTime, _neutralValue); DoComplete(); } @@ -203,7 +208,7 @@ namespace Avalonia.Animation normalizedTime = 1 - normalizedTime; // Ease and interpolate - var easedTime = _easeFunc.Ease(normalizedTime); + var easedTime = _easeFunc!.Ease(normalizedTime); _lastInterpValue = _interpolator(easedTime, _neutralValue); PublishNext(_lastInterpValue); @@ -223,14 +228,14 @@ namespace Avalonia.Animation private void UpdateNeutralValue() { - var property = _animator.Property; + var property = _animator.Property ?? throw new InvalidOperationException("Animator has no property specified."); var baseValue = _targetControl.GetBaseValue(property, BindingPriority.LocalValue); _neutralValue = baseValue != AvaloniaProperty.UnsetValue ? - (T)baseValue : (T)_targetControl.GetValue(property); + (T)baseValue! : (T)_targetControl.GetValue(property)!; } - private void ControlPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + private void ControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { if (e.Property == _animator.Property && e.Priority > BindingPriority.Animation) { diff --git a/src/Avalonia.Animation/AnimatorKeyFrame.cs b/src/Avalonia.Animation/AnimatorKeyFrame.cs index f6a0c12be4..8af31f2948 100644 --- a/src/Avalonia.Animation/AnimatorKeyFrame.cs +++ b/src/Avalonia.Animation/AnimatorKeyFrame.cs @@ -12,22 +12,22 @@ namespace Avalonia.Animation /// public class AnimatorKeyFrame : AvaloniaObject { - public static readonly DirectProperty ValueProperty = - AvaloniaProperty.RegisterDirect(nameof(Value), k => k.Value, (k, v) => k.Value = v); + public static readonly DirectProperty ValueProperty = + AvaloniaProperty.RegisterDirect(nameof(Value), k => k.Value, (k, v) => k.Value = v); public AnimatorKeyFrame() { } - public AnimatorKeyFrame(Type animatorType, Cue cue) + public AnimatorKeyFrame(Type? animatorType, Cue cue) { AnimatorType = animatorType; Cue = cue; KeySpline = null; } - public AnimatorKeyFrame(Type animatorType, Cue cue, KeySpline keySpline) + public AnimatorKeyFrame(Type? animatorType, Cue cue, KeySpline? keySpline) { AnimatorType = animatorType; Cue = cue; @@ -35,14 +35,14 @@ namespace Avalonia.Animation } internal bool isNeutral; - public Type AnimatorType { get; } + public Type? AnimatorType { get; } public Cue Cue { get; } - public KeySpline KeySpline { get; } - public AvaloniaProperty Property { get; private set; } + public KeySpline? KeySpline { get; } + public AvaloniaProperty? Property { get; private set; } - private object _value; + private object? _value; - public object Value + public object? Value { get => _value; set => SetAndRaise(ValueProperty, ref _value, value); @@ -80,7 +80,7 @@ namespace Avalonia.Animation throw new InvalidCastException($"KeyFrame value doesnt match property type."); } - return (T)typeConv.ConvertTo(Value, typeof(T)); + return (T)typeConv.ConvertTo(Value, typeof(T))!; } } } diff --git a/src/Avalonia.Animation/Animators/Animator`1.cs b/src/Avalonia.Animation/Animators/Animator`1.cs index 23afa76bf6..248ca61c1d 100644 --- a/src/Avalonia.Animation/Animators/Animator`1.cs +++ b/src/Avalonia.Animation/Animators/Animator`1.cs @@ -24,7 +24,7 @@ namespace Avalonia.Animation.Animators /// /// Gets or sets the target property for the keyframe. /// - public AvaloniaProperty Property { get; set; } + public AvaloniaProperty? Property { get; set; } public Animator() { @@ -33,7 +33,7 @@ namespace Avalonia.Animation.Animators } /// - public virtual IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable match, Action onComplete) + public virtual IDisposable? Apply(Animation animation, Animatable control, IClock? clock, IObservable match, Action? onComplete) { if (!_isVerifiedAndConverted) VerifyConvertKeyFrames(); @@ -106,13 +106,16 @@ namespace Avalonia.Animation.Animators public virtual IDisposable BindAnimation(Animatable control, IObservable instance) { + if (Property is null) + throw new InvalidOperationException("Animator has no property specified."); + return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation); } /// /// Runs the KeyFrames Animation. /// - internal IDisposable Run(Animation animation, Animatable control, IClock clock, Action onComplete) + internal IDisposable Run(Animation animation, Animatable control, IClock? clock, Action? onComplete) { var instance = new AnimationInstance( animation, diff --git a/src/Avalonia.Animation/Avalonia.Animation.csproj b/src/Avalonia.Animation/Avalonia.Animation.csproj index 465b922b72..d81d14bbff 100644 --- a/src/Avalonia.Animation/Avalonia.Animation.csproj +++ b/src/Avalonia.Animation/Avalonia.Animation.csproj @@ -2,9 +2,13 @@ net6.0;netstandard2.0 + + + + diff --git a/src/Avalonia.Animation/Clock.cs b/src/Avalonia.Animation/Clock.cs index 5c2b7ce0dd..5afd2ae705 100644 --- a/src/Avalonia.Animation/Clock.cs +++ b/src/Avalonia.Animation/Clock.cs @@ -4,7 +4,7 @@ namespace Avalonia.Animation { public class Clock : ClockBase { - public static IClock GlobalClock => AvaloniaLocator.Current.GetService(); + public static IClock GlobalClock => AvaloniaLocator.Current.GetRequiredService(); private readonly IDisposable _parentSubscription; diff --git a/src/Avalonia.Animation/Cue.cs b/src/Avalonia.Animation/Cue.cs index 7da7a9382b..6578148b07 100644 --- a/src/Avalonia.Animation/Cue.cs +++ b/src/Avalonia.Animation/Cue.cs @@ -30,7 +30,7 @@ namespace Avalonia.Animation /// /// Parses a string to a object. /// - public static Cue Parse(string value, CultureInfo culture) + public static Cue Parse(string value, CultureInfo? culture) { string v = value; @@ -72,14 +72,14 @@ namespace Avalonia.Animation public class CueTypeConverter : TypeConverter { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { return sourceType == typeof(string); } - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) { return Cue.Parse((string)value, culture); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs b/src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs index 696f43d006..7283eaeedf 100644 --- a/src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs +++ b/src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs @@ -8,15 +8,15 @@ namespace Avalonia.Animation /// internal class DisposeAnimationInstanceSubject : IObserver, IDisposable { - private IDisposable _lastInstance; + private IDisposable? _lastInstance; private bool _lastMatch; private Animator _animator; private Animation _animation; private Animatable _control; - private Action _onComplete; - private IClock _clock; + private Action? _onComplete; + private IClock? _clock; - public DisposeAnimationInstanceSubject(Animator animator, Animation animation, Animatable control, IClock clock, Action onComplete) + public DisposeAnimationInstanceSubject(Animator animator, Animation animation, Animatable control, IClock? clock, Action? onComplete) { this._animator = animator; this._animation = animation; diff --git a/src/Avalonia.Animation/Easing/Easing.cs b/src/Avalonia.Animation/Easing/Easing.cs index e006459652..2f4b93dab1 100644 --- a/src/Avalonia.Animation/Easing/Easing.cs +++ b/src/Avalonia.Animation/Easing/Easing.cs @@ -15,7 +15,7 @@ namespace Avalonia.Animation.Easings /// public abstract double Ease(double progress); - static Dictionary _easingTypes; + static Dictionary? _easingTypes; static readonly Type s_thisType = typeof(Easing); @@ -48,7 +48,7 @@ namespace Avalonia.Animation.Easings if (_easingTypes.ContainsKey(e)) { var type = _easingTypes[e]; - return (Easing)Activator.CreateInstance(type); + return (Easing)Activator.CreateInstance(type)!; } else { diff --git a/src/Avalonia.Animation/Easing/EasingTypeConverter.cs b/src/Avalonia.Animation/Easing/EasingTypeConverter.cs index 6613f6d393..3d67d54a6f 100644 --- a/src/Avalonia.Animation/Easing/EasingTypeConverter.cs +++ b/src/Avalonia.Animation/Easing/EasingTypeConverter.cs @@ -6,12 +6,12 @@ namespace Avalonia.Animation.Easings { public class EasingTypeConverter : TypeConverter { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { return sourceType == typeof(string); } - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) { return Easing.Parse((string)value); } diff --git a/src/Avalonia.Animation/IAnimation.cs b/src/Avalonia.Animation/IAnimation.cs index d037834630..436a765d27 100644 --- a/src/Avalonia.Animation/IAnimation.cs +++ b/src/Avalonia.Animation/IAnimation.cs @@ -12,7 +12,7 @@ namespace Avalonia.Animation /// /// Apply the animation to the specified control and run it when produces true. /// - IDisposable Apply(Animatable control, IClock clock, IObservable match, Action onComplete = null); + IDisposable Apply(Animatable control, IClock? clock, IObservable match, Action? onComplete = null); /// /// Run the animation on the specified control. diff --git a/src/Avalonia.Animation/IAnimationSetter.cs b/src/Avalonia.Animation/IAnimationSetter.cs index 2d22377286..6a1d3539e2 100644 --- a/src/Avalonia.Animation/IAnimationSetter.cs +++ b/src/Avalonia.Animation/IAnimationSetter.cs @@ -2,7 +2,7 @@ namespace Avalonia.Animation { public interface IAnimationSetter { - AvaloniaProperty Property { get; set; } - object Value { get; set; } + AvaloniaProperty? Property { get; set; } + object? Value { get; set; } } } diff --git a/src/Avalonia.Animation/IAnimator.cs b/src/Avalonia.Animation/IAnimator.cs index d0fb173c54..f64ac9f913 100644 --- a/src/Avalonia.Animation/IAnimator.cs +++ b/src/Avalonia.Animation/IAnimator.cs @@ -11,11 +11,11 @@ namespace Avalonia.Animation /// /// The target property. /// - AvaloniaProperty Property {get; set;} + AvaloniaProperty? Property {get; set;} /// /// Applies the current KeyFrame group to the specified control. /// - IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable match, Action onComplete); + IDisposable? Apply(Animation animation, Animatable control, IClock? clock, IObservable match, Action? onComplete); } } diff --git a/src/Avalonia.Animation/ITransition.cs b/src/Avalonia.Animation/ITransition.cs index ade2ec8b9e..241ca208d1 100644 --- a/src/Avalonia.Animation/ITransition.cs +++ b/src/Avalonia.Animation/ITransition.cs @@ -10,7 +10,7 @@ namespace Avalonia.Animation /// /// Applies the transition to the specified . /// - IDisposable Apply(Animatable control, IClock clock, object oldValue, object newValue); + IDisposable Apply(Animatable control, IClock clock, object? oldValue, object? newValue); /// /// Gets the property to be animated. diff --git a/src/Avalonia.Animation/IterationCount.cs b/src/Avalonia.Animation/IterationCount.cs index 9463718608..3b52cdab49 100644 --- a/src/Avalonia.Animation/IterationCount.cs +++ b/src/Avalonia.Animation/IterationCount.cs @@ -97,7 +97,7 @@ namespace Avalonia.Animation /// /// The object with which to test equality. /// True if the objects are equal, otherwise false. - public override bool Equals(object o) + public override bool Equals(object? o) { if (o == null) { diff --git a/src/Avalonia.Animation/IterationCountTypeConverter.cs b/src/Avalonia.Animation/IterationCountTypeConverter.cs index 1c63f8cdf1..f64972ff5c 100644 --- a/src/Avalonia.Animation/IterationCountTypeConverter.cs +++ b/src/Avalonia.Animation/IterationCountTypeConverter.cs @@ -6,12 +6,12 @@ namespace Avalonia.Animation { public class IterationCountTypeConverter : TypeConverter { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { return sourceType == typeof(string); } - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) { return IterationCount.Parse((string)value); } diff --git a/src/Avalonia.Animation/KeyFrame.cs b/src/Avalonia.Animation/KeyFrame.cs index c2cc1aa051..3ab7a70d90 100644 --- a/src/Avalonia.Animation/KeyFrame.cs +++ b/src/Avalonia.Animation/KeyFrame.cs @@ -19,7 +19,7 @@ namespace Avalonia.Animation { private TimeSpan _ktimeSpan; private Cue _kCue; - private KeySpline _kKeySpline; + private KeySpline? _kKeySpline; public KeyFrame() { @@ -79,7 +79,7 @@ namespace Avalonia.Animation /// Gets or sets the KeySpline of this . /// /// The key spline. - public KeySpline KeySpline + public KeySpline? KeySpline { get { diff --git a/src/Avalonia.Animation/KeySplineTypeConverter.cs b/src/Avalonia.Animation/KeySplineTypeConverter.cs index b026206e5f..eecad3c3ac 100644 --- a/src/Avalonia.Animation/KeySplineTypeConverter.cs +++ b/src/Avalonia.Animation/KeySplineTypeConverter.cs @@ -12,12 +12,12 @@ namespace Avalonia.Animation /// public class KeySplineTypeConverter : TypeConverter { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { return sourceType == typeof(string); } - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) { return KeySpline.Parse((string)value, CultureInfo.InvariantCulture); } diff --git a/src/Avalonia.Animation/Transition.cs b/src/Avalonia.Animation/Transition.cs index 4115c95c0f..d307f348c4 100644 --- a/src/Avalonia.Animation/Transition.cs +++ b/src/Avalonia.Animation/Transition.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Avalonia.Animation.Easings; namespace Avalonia.Animation @@ -8,7 +9,7 @@ namespace Avalonia.Animation /// public abstract class Transition : AvaloniaObject, ITransition { - private AvaloniaProperty _prop; + private AvaloniaProperty? _prop; /// /// Gets or sets the duration of the transition. @@ -26,7 +27,8 @@ namespace Avalonia.Animation public Easing Easing { get; set; } = new LinearEasing(); /// - public AvaloniaProperty Property + [DisallowNull] + public AvaloniaProperty? Property { get { @@ -42,16 +44,25 @@ namespace Avalonia.Animation } } + AvaloniaProperty ITransition.Property + { + get => Property ?? throw new InvalidOperationException("Transition has no property specified."); + set => Property = value; + } + /// /// Apply interpolation to the property. /// public abstract IObservable DoTransition(IObservable progress, T oldValue, T newValue); /// - public virtual IDisposable Apply(Animatable control, IClock clock, object oldValue, object newValue) + public virtual IDisposable Apply(Animatable control, IClock clock, object? oldValue, object? newValue) { - var transition = DoTransition(new TransitionInstance(clock, Delay, Duration), (T)oldValue, (T)newValue); + if (Property is null) + throw new InvalidOperationException("Transition has no property specified."); + + var transition = DoTransition(new TransitionInstance(clock, Delay, Duration), (T)oldValue!, (T)newValue!); return control.Bind((AvaloniaProperty)Property, transition, Data.BindingPriority.Animation); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Animation/TransitionInstance.cs b/src/Avalonia.Animation/TransitionInstance.cs index b522d1961e..9c9494ff87 100644 --- a/src/Avalonia.Animation/TransitionInstance.cs +++ b/src/Avalonia.Animation/TransitionInstance.cs @@ -10,11 +10,11 @@ namespace Avalonia.Animation /// internal class TransitionInstance : SingleSubscriberObservableBase, IObserver { - private IDisposable _timerSubscription; + private IDisposable? _timerSubscription; private TimeSpan _delay; private TimeSpan _duration; private readonly IClock _baseClock; - private TransitionClock _clock; + private TransitionClock? _clock; public TransitionInstance(IClock clock, TimeSpan delay, TimeSpan duration) { @@ -67,7 +67,7 @@ namespace Avalonia.Animation protected override void Unsubscribed() { _timerSubscription?.Dispose(); - _clock.PlayState = PlayState.Stop; + _clock!.PlayState = PlayState.Stop; } protected override void Subscribed() diff --git a/src/Avalonia.Base/Collections/AvaloniaDictionary.cs b/src/Avalonia.Base/Collections/AvaloniaDictionary.cs index 0e027712e0..2fe68e824d 100644 --- a/src/Avalonia.Base/Collections/AvaloniaDictionary.cs +++ b/src/Avalonia.Base/Collections/AvaloniaDictionary.cs @@ -146,6 +146,7 @@ namespace Avalonia.Collections { if (_inner.TryGetValue(key, out var value)) { + _inner.Remove(key); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"Item[{key}]")); diff --git a/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs b/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs index dcd32ddd76..689fcc89a4 100644 --- a/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs +++ b/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs @@ -59,7 +59,7 @@ namespace Avalonia.Collections } private class WeakCollectionChangedObservable : LightweightObservableBase, - IWeakSubscriber + IWeakEventSubscriber { private WeakReference _sourceReference; @@ -68,31 +68,22 @@ namespace Avalonia.Collections _sourceReference = source; } - public void OnEvent(object? sender, NotifyCollectionChangedEventArgs e) + public void OnEvent(object? sender, + WeakEvent ev, + NotifyCollectionChangedEventArgs e) { PublishNext(e); } - protected override void Initialize() { if (_sourceReference.TryGetTarget(out var instance)) - { - WeakSubscriptionManager.Subscribe( - instance, - nameof(instance.CollectionChanged), - this); - } + WeakEvents.CollectionChanged.Subscribe(instance, this); } protected override void Deinitialize() { if (_sourceReference.TryGetTarget(out var instance)) - { - WeakSubscriptionManager.Unsubscribe( - instance, - nameof(instance.CollectionChanged), - this); - } + WeakEvents.CollectionChanged.Unsubscribe(instance, this); } } } diff --git a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs index e197e29103..a808827896 100644 --- a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs +++ b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs @@ -23,18 +23,16 @@ namespace Avalonia.Data.Core if (incc != null) { - inputs.Add(WeakObservable.FromEventPattern( - incc, - nameof(incc.CollectionChanged)) + inputs.Add(WeakObservable.FromEventPattern( + incc, WeakEvents.CollectionChanged) .Where(x => ShouldUpdate(x.Sender, x.EventArgs)) .Select(_ => GetValue(target))); } if (inpc != null) { - inputs.Add(WeakObservable.FromEventPattern( - inpc, - nameof(inpc.PropertyChanged)) + inputs.Add(WeakObservable.FromEventPattern( + inpc, WeakEvents.PropertyChanged) .Where(x => ShouldUpdate(x.Sender, x.EventArgs)) .Select(_ => GetValue(target))); } diff --git a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs index 9f827daf94..1e7a0d5c8f 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs @@ -11,6 +11,12 @@ namespace Avalonia.Data.Core.Plugins /// public class IndeiValidationPlugin : IDataValidationPlugin { + private static readonly WeakEvent + ErrorsChangedWeakEvent = WeakEvent.Register( + (s, h) => s.ErrorsChanged += h, + (s, h) => s.ErrorsChanged -= h + ); + /// public bool Match(WeakReference reference, string memberName) { @@ -25,7 +31,7 @@ namespace Avalonia.Data.Core.Plugins return new Validator(reference, name, accessor); } - private class Validator : DataValidationBase, IWeakSubscriber + private class Validator : DataValidationBase, IWeakEventSubscriber { private readonly WeakReference _reference; private readonly string _name; @@ -37,7 +43,7 @@ namespace Avalonia.Data.Core.Plugins _name = name; } - void IWeakSubscriber.OnEvent(object? sender, DataErrorsChangedEventArgs e) + void IWeakEventSubscriber.OnEvent(object? notifyDataErrorInfo, WeakEvent ev, DataErrorsChangedEventArgs e) { if (e.PropertyName == _name || string.IsNullOrEmpty(e.PropertyName)) { @@ -51,10 +57,7 @@ namespace Avalonia.Data.Core.Plugins if (target != null) { - WeakSubscriptionManager.Subscribe( - target, - nameof(target.ErrorsChanged), - this); + ErrorsChangedWeakEvent.Subscribe(target, this); } base.SubscribeCore(); @@ -66,10 +69,7 @@ namespace Avalonia.Data.Core.Plugins if (target != null) { - WeakSubscriptionManager.Unsubscribe( - target, - nameof(target.ErrorsChanged), - this); + ErrorsChangedWeakEvent.Unsubscribe(target, this); } base.UnsubscribeCore(); diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index fd532f3014..33cecd10a7 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.ComponentModel; using System.Reflection; using Avalonia.Utilities; @@ -85,7 +86,7 @@ namespace Avalonia.Data.Core.Plugins return found; } - private class Accessor : PropertyAccessorBase, IWeakSubscriber + private class Accessor : PropertyAccessorBase, IWeakEventSubscriber { private readonly WeakReference _reference; private readonly PropertyInfo _property; @@ -129,7 +130,8 @@ namespace Avalonia.Data.Core.Plugins return false; } - void IWeakSubscriber.OnEvent(object? sender, PropertyChangedEventArgs e) + void IWeakEventSubscriber. + OnEvent(object? notifyPropertyChanged, WeakEvent ev, PropertyChangedEventArgs e) { if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName)) { @@ -148,13 +150,8 @@ namespace Avalonia.Data.Core.Plugins { var inpc = GetReferenceTarget() as INotifyPropertyChanged; - if (inpc != null) - { - WeakSubscriptionManager.Unsubscribe( - inpc, - nameof(inpc.PropertyChanged), - this); - } + if (inpc != null) + WeakEvents.PropertyChanged.Unsubscribe(inpc, this); } private object? GetReferenceTarget() @@ -178,13 +175,8 @@ namespace Avalonia.Data.Core.Plugins { var inpc = GetReferenceTarget() as INotifyPropertyChanged; - if (inpc != null) - { - WeakSubscriptionManager.Subscribe( - inpc, - nameof(inpc.PropertyChanged), - this); - } + if (inpc != null) + WeakEvents.PropertyChanged.Subscribe(inpc, this); } } } diff --git a/src/Avalonia.Base/Platform/IRuntimePlatform.cs b/src/Avalonia.Base/Platform/IRuntimePlatform.cs index a0d5d611b3..850757a1ee 100644 --- a/src/Avalonia.Base/Platform/IRuntimePlatform.cs +++ b/src/Avalonia.Base/Platform/IRuntimePlatform.cs @@ -1,5 +1,4 @@ using System; -using System.Reflection; namespace Avalonia.Platform { @@ -23,6 +22,7 @@ namespace Avalonia.Platform public OperatingSystemType OperatingSystem { get; set; } public bool IsDesktop { get; set; } public bool IsMobile { get; set; } + public bool IsBrowser { get; set; } public bool IsCoreClr { get; set; } public bool IsMono { get; set; } public bool IsDotNetFramework { get; set; } @@ -36,6 +36,7 @@ namespace Avalonia.Platform Linux, OSX, Android, - iOS + iOS, + Browser } } diff --git a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs index b991f9eb40..21649306cb 100644 --- a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs +++ b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs @@ -30,7 +30,7 @@ namespace Avalonia.Threading /// public override void Post(SendOrPostCallback d, object? state) { - Dispatcher.UIThread.Post(() => d(state), DispatcherPriority.Send); + Dispatcher.UIThread.Post(() => d(state), DispatcherPriority.Background); } /// diff --git a/src/Avalonia.Base/Threading/Dispatcher.cs b/src/Avalonia.Base/Threading/Dispatcher.cs index 908f431776..49cee441d0 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.cs @@ -74,6 +74,13 @@ namespace Avalonia.Threading /// /// public void RunJobs(DispatcherPriority minimumPriority) => _jobRunner.RunJobs(minimumPriority); + + /// + /// Use this method to check if there are more prioritized tasks + /// + /// + public bool HasJobsWithPriority(DispatcherPriority minimumPriority) => + _jobRunner.HasJobsWithPriority(minimumPriority); /// public Task InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal) diff --git a/src/Avalonia.Base/Threading/JobRunner.cs b/src/Avalonia.Base/Threading/JobRunner.cs index f2aef0414c..4b304d44f6 100644 --- a/src/Avalonia.Base/Threading/JobRunner.cs +++ b/src/Avalonia.Base/Threading/JobRunner.cs @@ -121,6 +121,21 @@ namespace Avalonia.Threading return null; } + public bool HasJobsWithPriority(DispatcherPriority minimumPriority) + { + for (int c = (int)minimumPriority; c < (int)DispatcherPriority.MaxValue; c++) + { + var q = _queues[c]; + lock (q) + { + if (q.Count > 0) + return true; + } + } + + return false; + } + private interface IJob { /// diff --git a/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs b/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs new file mode 100644 index 0000000000..e48c0cb111 --- /dev/null +++ b/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs @@ -0,0 +1,12 @@ +using System; + +namespace Avalonia.Utilities; + +/// +/// Defines a listener to a event subscribed vis the . +/// +/// The type of the event arguments. +public interface IWeakEventSubscriber where TEventArgs : EventArgs +{ + void OnEvent(object? sender, WeakEvent ev, TEventArgs e); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs b/src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs index 90d1c52ff5..251dfe4351 100644 --- a/src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs +++ b/src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; namespace Avalonia.Utilities { - public struct ImmutableReadOnlyListStructEnumerator : IEnumerator, IEnumerator + public struct ImmutableReadOnlyListStructEnumerator : IEnumerator { private readonly IReadOnlyList _readOnlyList; private int _pos; diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs new file mode 100644 index 0000000000..0b32015a8a --- /dev/null +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using Avalonia.Threading; + +namespace Avalonia.Utilities; + +/// +/// Manages subscriptions to events using weak listeners. +/// +public class WeakEvent : WeakEvent where TEventArgs : EventArgs where TSender : class +{ + private readonly Func, Action> _subscribe; + + readonly ConditionalWeakTable _subscriptions = new(); + + internal WeakEvent( + Action> subscribe, + Action> unsubscribe) + { + _subscribe = (t, s) => + { + subscribe(t, s); + return () => unsubscribe(t, s); + }; + } + + internal WeakEvent(Func, Action> subscribe) + { + _subscribe = subscribe; + } + + public void Subscribe(TSender target, IWeakEventSubscriber subscriber) + { + if (!_subscriptions.TryGetValue(target, out var subscription)) + _subscriptions.Add(target, subscription = new Subscription(this, target)); + subscription.Add(new WeakReference>(subscriber)); + } + + public void Unsubscribe(TSender target, IWeakEventSubscriber subscriber) + { + if (_subscriptions.TryGetValue(target, out var subscription)) + subscription.Remove(subscriber); + } + + private class Subscription + { + private readonly WeakEvent _ev; + private readonly TSender _target; + private readonly Action _compact; + + private WeakReference>?[] _data = + new WeakReference>[16]; + private int _count; + private readonly Action _unsubscribe; + private bool _compactScheduled; + + public Subscription(WeakEvent ev, TSender target) + { + _ev = ev; + _target = target; + _compact = Compact; + _unsubscribe = ev._subscribe(target, OnEvent); + } + + void Destroy() + { + _unsubscribe(); + _ev._subscriptions.Remove(_target); + } + + public void Add(WeakReference> s) + { + if (_count == _data.Length) + { + //Extend capacity + var extendedData = new WeakReference>?[_data.Length * 2]; + Array.Copy(_data, extendedData, _data.Length); + _data = extendedData; + } + + _data[_count] = s; + _count++; + } + + public void Remove(IWeakEventSubscriber s) + { + var removed = false; + + for (int c = 0; c < _count; ++c) + { + var reference = _data[c]; + + if (reference != null && reference.TryGetTarget(out var instance) && instance == s) + { + _data[c] = null; + removed = true; + } + } + + if (removed) + { + ScheduleCompact(); + } + } + + void ScheduleCompact() + { + if(_compactScheduled) + return; + _compactScheduled = true; + Dispatcher.UIThread.Post(_compact, DispatcherPriority.Background); + } + + void Compact() + { + _compactScheduled = false; + int empty = -1; + for (var c = 0; c < _count; c++) + { + var r = _data[c]; + //Mark current index as first empty + if (r == null && empty == -1) + empty = c; + //If current element isn't null and we have an empty one + if (r != null && empty != -1) + { + _data[c] = null; + _data[empty] = r; + empty++; + } + } + + if (empty != -1) + _count = empty; + if (_count == 0) + Destroy(); + } + + void OnEvent(object? sender, TEventArgs eventArgs) + { + var needCompact = false; + for (var c = 0; c < _count; c++) + { + var r = _data[c]; + if (r?.TryGetTarget(out var sub) == true) + sub!.OnEvent(_target, _ev, eventArgs); + else + needCompact = true; + } + + if (needCompact) + ScheduleCompact(); + } + } + +} + +public class WeakEvent +{ + public static WeakEvent Register( + Action> subscribe, + Action> unsubscribe) where TSender : class where TEventArgs : EventArgs + { + return new WeakEvent(subscribe, unsubscribe); + } + + public static WeakEvent Register( + Func, Action> subscribe) where TSender : class where TEventArgs : EventArgs + { + return new WeakEvent(subscribe); + } + + public static WeakEvent Register( + Action subscribe, + Action unsubscribe) where TSender : class + { + return Register((s, h) => + { + EventHandler handler = (_, e) => h(s, e); + subscribe(s, handler); + return () => unsubscribe(s, handler); + }); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Utilities/WeakEvents.cs b/src/Avalonia.Base/Utilities/WeakEvents.cs new file mode 100644 index 0000000000..d1b5e7f12d --- /dev/null +++ b/src/Avalonia.Base/Utilities/WeakEvents.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Windows.Input; + +namespace Avalonia.Utilities; + +public class WeakEvents +{ + /// + /// Represents CollectionChanged event from + /// + public static readonly WeakEvent + CollectionChanged = WeakEvent.Register( + (c, s) => + { + NotifyCollectionChangedEventHandler handler = (_, e) => s(c, e); + c.CollectionChanged += handler; + return () => c.CollectionChanged -= handler; + }); + + /// + /// Represents PropertyChanged event from + /// + public static readonly WeakEvent + PropertyChanged = WeakEvent.Register( + (s, h) => + { + PropertyChangedEventHandler handler = (_, e) => h(s, e); + s.PropertyChanged += handler; + return () => s.PropertyChanged -= handler; + }); + + /// + /// Represents CanExecuteChanged event from + /// + public static readonly WeakEvent CommandCanExecuteChanged = + WeakEvent.Register((s, h) => s.CanExecuteChanged += h, + (s, h) => s.CanExecuteChanged -= h); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Utilities/WeakObservable.cs b/src/Avalonia.Base/Utilities/WeakObservable.cs index 52edc7ad1a..6bf1d4082f 100644 --- a/src/Avalonia.Base/Utilities/WeakObservable.cs +++ b/src/Avalonia.Base/Utilities/WeakObservable.cs @@ -18,6 +18,7 @@ namespace Avalonia.Utilities /// Object instance that exposes the event to convert. /// Name of the event to convert. /// + [Obsolete("Use WeakEvent-based overload")] public static IObservable> FromEventPattern( TTarget target, string eventName) @@ -34,7 +35,9 @@ namespace Avalonia.Utilities }).Publish().RefCount(); } - private class Handler : IWeakSubscriber where TEventArgs : EventArgs + private class Handler + : IWeakSubscriber, + IWeakEventSubscriber where TEventArgs : EventArgs { private IObserver> _observer; @@ -47,6 +50,36 @@ namespace Avalonia.Utilities { _observer.OnNext(new EventPattern(sender, e)); } + + public void OnEvent(object? sender, WeakEvent ev, TEventArgs e) + { + _observer.OnNext(new EventPattern(sender, e)); + } } + + /// + /// Converts a WeakEvent conforming to the standard .NET event pattern into an observable + /// sequence, subscribing weakly. + /// + /// The type of target. + /// The type of the event args. + /// Object instance that exposes the event to convert. + /// The weak event to convert. + /// + public static IObservable> FromEventPattern( + TTarget target, WeakEvent ev) + where TEventArgs : EventArgs where TTarget : class + { + _ = target ?? throw new ArgumentNullException(nameof(target)); + _ = ev ?? throw new ArgumentNullException(nameof(ev)); + + return Observable.Create>(observer => + { + var handler = new Handler(observer); + ev.Subscribe(target, handler); + return () => ev.Unsubscribe(target, handler); + }).Publish().RefCount(); + } + } } diff --git a/src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs b/src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs index 88b1e3c807..dc9e86cc32 100644 --- a/src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs +++ b/src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs @@ -19,6 +19,7 @@ namespace Avalonia.Utilities /// The event source. /// The name of the event. /// The subscriber. + [Obsolete("Use WeakEvent")] public static void Subscribe(TTarget target, string eventName, IWeakSubscriber subscriber) where TEventArgs : EventArgs { diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 10c7c16488..5d71a499e3 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -2215,7 +2215,14 @@ namespace Avalonia.Controls /// PointerWheelEventArgs protected override void OnPointerWheelChanged(PointerWheelEventArgs e) { - e.Handled = e.Handled || UpdateScroll(e.Delta * DATAGRID_mouseWheelDelta); + if(UpdateScroll(e.Delta * DATAGRID_mouseWheelDelta)) + { + e.Handled = true; + } + else + { + e.Handled = e.Handled || !ScrollViewer.GetIsScrollChainingEnabled(this); + } } internal bool UpdateScroll(Vector delta) @@ -5751,6 +5758,7 @@ namespace Avalonia.Controls return true; } // Unselect everything except the row that was clicked on + _noSelectionChangeCount++; try { UpdateSelectionAndCurrency(columnIndex, slot, DataGridSelectionAction.SelectCurrent, scrollIntoView: false); diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index 6b515503aa..a77b482436 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -448,7 +448,7 @@ namespace Avalonia.Controls internal set; } - public bool IsReadOnly + public virtual bool IsReadOnly { get { @@ -680,7 +680,7 @@ namespace Avalonia.Controls public void ClearSort() { //InvokeProcessSort is already validating if sorting is possible - _headerCell?.InvokeProcessSort(Input.KeyModifiers.Control); + _headerCell?.InvokeProcessSort(KeyboardHelper.GetPlatformCtrlOrCmdKeyModifier()); } /// diff --git a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs index 7e95dd100c..0e754d5815 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs @@ -1,4 +1,4 @@ -// (c) Copyright Microsoft Corporation. +// (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. @@ -15,7 +15,7 @@ namespace Avalonia.Controls { public class DataGridTemplateColumn : DataGridColumn { - IDataTemplate _cellTemplate; + private IDataTemplate _cellTemplate; public static readonly DirectProperty CellTemplateProperty = AvaloniaProperty.RegisterDirect( @@ -30,17 +30,38 @@ namespace Avalonia.Controls set { SetAndRaise(CellTemplateProperty, ref _cellTemplate, value); } } + private IDataTemplate _cellEditingCellTemplate; + + /// + /// Defines the property. + /// + public static readonly DirectProperty CellEditingTemplateProperty = + AvaloniaProperty.RegisterDirect( + nameof(CellEditingTemplate), + o => o.CellEditingTemplate, + (o, v) => o.CellEditingTemplate = v); + + /// + /// Gets or sets the which is used for the editing mode of the current + /// + /// + /// An for the editing mode of the current + /// + /// + /// If this property is the column is read-only. + /// + public IDataTemplate CellEditingTemplate + { + get => _cellEditingCellTemplate; + set => SetAndRaise(CellEditingTemplateProperty, ref _cellEditingCellTemplate, value); + } + private void OnCellTemplateChanged(AvaloniaPropertyChangedEventArgs e) { var oldValue = (IDataTemplate)e.OldValue; var value = (IDataTemplate)e.NewValue; } - public DataGridTemplateColumn() - { - IsReadOnly = true; - } - protected override IControl GenerateElement(DataGridCell cell, object dataItem) { if(CellTemplate != null) @@ -60,7 +81,22 @@ namespace Avalonia.Controls protected override IControl GenerateEditingElement(DataGridCell cell, object dataItem, out ICellEditBinding binding) { binding = null; - return GenerateElement(cell, dataItem); + if(CellEditingTemplate != null) + { + return CellEditingTemplate.Build(dataItem); + } + else if (CellTemplate != null) + { + return CellTemplate.Build(dataItem); + } + if (Design.IsDesignMode) + { + return null; + } + else + { + throw DataGridError.DataGridTemplateColumn.MissingTemplateForType(typeof(DataGridTemplateColumn)); + } } protected override object PrepareCellForEdit(IControl editingElement, RoutedEventArgs editingEventArgs) @@ -70,12 +106,30 @@ namespace Avalonia.Controls protected internal override void RefreshCellContent(IControl element, string propertyName) { - if(propertyName == nameof(CellTemplate) && element.Parent is DataGridCell cell) + var cell = element.Parent as DataGridCell; + if(propertyName == nameof(CellTemplate) && cell is not null) { cell.Content = GenerateElement(cell, cell.DataContext); } base.RefreshCellContent(element, propertyName); } + + public override bool IsReadOnly + { + get + { + if (CellEditingTemplate is null) + { + return true; + } + + return base.IsReadOnly; + } + set + { + base.IsReadOnly = value; + } + } } } diff --git a/src/Avalonia.Controls.DataGrid/Utils/KeyboardHelper.cs b/src/Avalonia.Controls.DataGrid/Utils/KeyboardHelper.cs index 351deceb48..d2b1fd4b8e 100644 --- a/src/Avalonia.Controls.DataGrid/Utils/KeyboardHelper.cs +++ b/src/Avalonia.Controls.DataGrid/Utils/KeyboardHelper.cs @@ -4,22 +4,29 @@ // All other rights reserved. using Avalonia.Input; +using Avalonia.Input.Platform; namespace Avalonia.Controls.Utils { internal static class KeyboardHelper { - public static void GetMetaKeyState(KeyModifiers modifiers, out bool ctrl, out bool shift) + public static void GetMetaKeyState(KeyModifiers modifiers, out bool ctrlOrCmd, out bool shift) { - ctrl = (modifiers & KeyModifiers.Control) == KeyModifiers.Control; - shift = (modifiers & KeyModifiers.Shift) == KeyModifiers.Shift; + ctrlOrCmd = modifiers.HasFlag(GetPlatformCtrlOrCmdKeyModifier()); + shift = modifiers.HasFlag(KeyModifiers.Shift); } - public static void GetMetaKeyState(KeyModifiers modifiers, out bool ctrl, out bool shift, out bool alt) + public static void GetMetaKeyState(KeyModifiers modifiers, out bool ctrlOrCmd, out bool shift, out bool alt) { - ctrl = (modifiers & KeyModifiers.Control) == KeyModifiers.Control; - shift = (modifiers & KeyModifiers.Shift) == KeyModifiers.Shift; - alt = (modifiers & KeyModifiers.Alt) == KeyModifiers.Alt; + ctrlOrCmd = modifiers.HasFlag(GetPlatformCtrlOrCmdKeyModifier()); + shift = modifiers.HasFlag(KeyModifiers.Shift); + alt = modifiers.HasFlag(KeyModifiers.Alt); + } + + public static KeyModifiers GetPlatformCtrlOrCmdKeyModifier() + { + var keymap = AvaloniaLocator.Current.GetService(); + return keymap?.CommandModifiers ?? KeyModifiers.Control; } } } diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index dd41c30e85..2c206b53f6 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -29,15 +29,25 @@ MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDownValueChang MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.NewValue.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.OldValue.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.ScrollViewer.AllowAutoHideProperty' does not exist in the implementation but it does exist in the contract. +CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.TopLevel' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Viewbox.StretchProperty' does not exist in the implementation but it does exist in the contract. +CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Window' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. +CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.WindowBase' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.EventHandler Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.ShutdownRequested' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.add_ShutdownRequested(System.EventHandler)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.remove_ShutdownRequested(System.EventHandler)' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.TryShutdown(System.Int32)' is present in the implementation but not in the contract. +CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Embedding.EmbeddableControlRoot' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. MembersMustExist : Member 'public System.Action Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.set(System.Action)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.Platform.ITopLevelNativeMenuExporter.SetNativeMenu(Avalonia.Controls.NativeMenu)' is present in the contract but not in the implementation. +MembersMustExist : Member 'protected Avalonia.Media.FormattedText Avalonia.Controls.Presenters.TextPresenter.CreateFormattedText()' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.Media.FormattedText Avalonia.Controls.Presenters.TextPresenter.FormattedText.get()' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public System.Int32 Avalonia.Controls.Presenters.TextPresenter.GetCaretIndex(Avalonia.Point)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Controls.Presenters.TextPresenter.InvalidateFormattedText()' does not exist in the implementation but it does exist in the contract. +CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Primitives.PopupRoot' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize.get()' is present in the implementation but not in the contract. @@ -57,4 +67,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platfor MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.ITrayIconImpl Avalonia.Platform.IWindowingPlatform.CreateTrayIcon()' is present in the implementation but not in the contract. -Total Issues: 58 +Total Issues: 68 diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 3a2fd68af5..edddf31d45 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -76,36 +76,21 @@ namespace Avalonia.Controls.ApplicationLifetimes return; if (ShutdownMode == ShutdownMode.OnLastWindowClose && _windows.Count == 0) - Shutdown(); - else if (ShutdownMode == ShutdownMode.OnMainWindowClose && window == MainWindow) - Shutdown(); + TryShutdown(); + else if (ShutdownMode == ShutdownMode.OnMainWindowClose && ReferenceEquals(window, MainWindow)) + TryShutdown(); } public void Shutdown(int exitCode = 0) { - if (_isShuttingDown) - throw new InvalidOperationException("Application is already shutting down."); - - _exitCode = exitCode; - _isShuttingDown = true; + DoShutdown(new ShutdownRequestedEventArgs(), true, exitCode); + } - try - { - foreach (var w in Windows) - w.Close(); - var e = new ControlledApplicationLifetimeExitEventArgs(exitCode); - Exit?.Invoke(this, e); - _exitCode = e.ApplicationExitCode; - } - finally - { - _cts?.Cancel(); - _cts = null; - _isShuttingDown = false; - } + public bool TryShutdown(int exitCode = 0) + { + return DoShutdown(new ShutdownRequestedEventArgs(), false, exitCode); } - public int Start(string[] args) { Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args)); @@ -114,7 +99,10 @@ namespace Avalonia.Controls.ApplicationLifetimes if(options != null && options.ProcessUrlActivationCommandLine && args.Length > 0) { - ((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(args); + if (Application.Current is IApplicationPlatformEvents events) + { + events.RaiseUrlsOpened(args); + } } var lifetimeEvents = AvaloniaLocator.Current.GetService(); @@ -145,23 +133,57 @@ namespace Avalonia.Controls.ApplicationLifetimes if (_activeLifetime == this) _activeLifetime = null; } - - private void OnShutdownRequested(object sender, ShutdownRequestedEventArgs e) + + private bool DoShutdown(ShutdownRequestedEventArgs e, bool force = false, int exitCode = 0) { - ShutdownRequested?.Invoke(this, e); + if (!force) + { + ShutdownRequested?.Invoke(this, e); - if (e.Cancel) - return; + if (e.Cancel) + return false; + + if (_isShuttingDown) + throw new InvalidOperationException("Application is already shutting down."); + } + + _exitCode = exitCode; + _isShuttingDown = true; - // When an OS shutdown request is received, try to close all non-owned windows. Windows can cancel - // shutdown by setting e.Cancel = true in the Closing event. Owned windows will be shutdown by their - // owners. - foreach (var w in Windows) - if (w.Owner is null) - w.Close(); - if (Windows.Count > 0) - e.Cancel = true; + try + { + // When an OS shutdown request is received, try to close all non-owned windows. Windows can cancel + // shutdown by setting e.Cancel = true in the Closing event. Owned windows will be shutdown by their + // owners. + foreach (var w in Windows) + { + if (w.Owner is null) + { + w.Close(); + } + } + + if (!force && Windows.Count > 0) + { + e.Cancel = true; + return false; + } + + var args = new ControlledApplicationLifetimeExitEventArgs(exitCode); + Exit?.Invoke(this, args); + _exitCode = args.ApplicationExitCode; + } + finally + { + _cts?.Cancel(); + _cts = null; + _isShuttingDown = false; + } + + return true; } + + private void OnShutdownRequested(object sender, ShutdownRequestedEventArgs e) => DoShutdown(e); } public class ClassicDesktopStyleApplicationLifetimeOptions diff --git a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs index a70d5dd2f1..a83229b732 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs @@ -9,6 +9,12 @@ namespace Avalonia.Controls.ApplicationLifetimes /// public interface IClassicDesktopStyleApplicationLifetime : IControlledApplicationLifetime { + /// + /// Tries to Shutdown the application. event can be used to cancel the shutdown. + /// + /// An integer exit code for an application. The default exit code is 0. + bool TryShutdown(int exitCode = 0); + /// /// Gets the arguments passed to the /// diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 8537c9acbc..ce41b90bb0 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -59,13 +59,13 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(CommandParameter)); /// - /// Defines the property. + /// Defines the property. /// public static readonly StyledProperty IsDefaultProperty = AvaloniaProperty.Register(nameof(IsDefault)); /// - /// Defines the property. + /// Defines the property. /// public static readonly StyledProperty IsCancelProperty = AvaloniaProperty.Register(nameof(IsCancel)); @@ -76,6 +76,9 @@ namespace Avalonia.Controls public static readonly RoutedEvent ClickEvent = RoutedEvent.Register(nameof(Click), RoutingStrategies.Bubble); + /// + /// Defines the property. + /// public static readonly StyledProperty IsPressedProperty = AvaloniaProperty.Register(nameof(IsPressed)); @@ -95,13 +98,12 @@ namespace Avalonia.Controls static Button() { FocusableProperty.OverrideDefaultValue(typeof(Button), true); - CommandProperty.Changed.Subscribe(CommandChanged); - CommandParameterProperty.Changed.Subscribe(CommandParameterChanged); - IsDefaultProperty.Changed.Subscribe(IsDefaultChanged); - IsCancelProperty.Changed.Subscribe(IsCancelChanged); AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler public event EventHandler Click { - add { AddHandler(ClickEvent, value); } - remove { RemoveHandler(ClickEvent, value); } + add => AddHandler(ClickEvent, value); + remove => RemoveHandler(ClickEvent, value); } /// @@ -121,8 +123,8 @@ namespace Avalonia.Controls /// public ClickMode ClickMode { - get { return GetValue(ClickModeProperty); } - set { SetValue(ClickModeProperty, value); } + get => GetValue(ClickModeProperty); + set => SetValue(ClickModeProperty, value); } /// @@ -130,8 +132,8 @@ namespace Avalonia.Controls /// public ICommand Command { - get { return _command; } - set { SetAndRaise(CommandProperty, ref _command, value); } + get => _command; + set => SetAndRaise(CommandProperty, ref _command, value); } /// @@ -139,8 +141,8 @@ namespace Avalonia.Controls /// public KeyGesture HotKey { - get { return GetValue(HotKeyProperty); } - set { SetValue(HotKeyProperty, value); } + get => GetValue(HotKeyProperty); + set => SetValue(HotKeyProperty, value); } /// @@ -148,8 +150,8 @@ namespace Avalonia.Controls /// public object CommandParameter { - get { return GetValue(CommandParameterProperty); } - set { SetValue(CommandParameterProperty, value); } + get => GetValue(CommandParameterProperty); + set => SetValue(CommandParameterProperty, value); } /// @@ -158,8 +160,8 @@ namespace Avalonia.Controls /// public bool IsDefault { - get { return GetValue(IsDefaultProperty); } - set { SetValue(IsDefaultProperty, value); } + get => GetValue(IsDefaultProperty); + set => SetValue(IsDefaultProperty, value); } /// @@ -168,18 +170,21 @@ namespace Avalonia.Controls /// public bool IsCancel { - get { return GetValue(IsCancelProperty); } - set { SetValue(IsCancelProperty, value); } + get => GetValue(IsCancelProperty); + set => SetValue(IsCancelProperty, value); } + /// + /// Gets or sets a value indicating whether the button is currently pressed. + /// public bool IsPressed { - get { return GetValue(IsPressedProperty); } - private set { SetValue(IsPressedProperty, value); } + get => GetValue(IsPressedProperty); + private set => SetValue(IsPressedProperty, value); } /// - /// Gets or sets the Flyout that should be shown with this button + /// Gets or sets the Flyout that should be shown with this button. /// public FlyoutBase Flyout { @@ -187,7 +192,8 @@ namespace Avalonia.Controls set => SetValue(FlyoutProperty, value); } - protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; + /// + protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) @@ -224,6 +230,7 @@ namespace Avalonia.Controls } } + /// protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { if (_hotkey != null) // Control attached again, set Hotkey to create a hotkey manager for this control @@ -240,6 +247,7 @@ namespace Avalonia.Controls } } + /// protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { // This will cause the hotkey manager to dispose the observer and the reference to this control @@ -358,12 +366,14 @@ namespace Avalonia.Controls } } } - + + /// protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) { IsPressed = false; } + /// protected override void OnLostFocus(RoutedEventArgs e) { base.OnLostFocus(e); @@ -371,119 +381,93 @@ namespace Avalonia.Controls IsPressed = false; } + /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); - if (change.Property == IsPressedProperty) - { - UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); - } - else if (change.Property == FlyoutProperty) + if (change.Property == CommandProperty) { - // If flyout is changed while one is already open, make sure we - // close the old one first - if (change.OldValue.GetValueOrDefault() is FlyoutBase oldFlyout && - oldFlyout.IsOpen) + if (((ILogical)this).IsAttachedToLogicalTree) { - oldFlyout.Hide(); + if (change.OldValue.GetValueOrDefault() is ICommand oldCommand) + { + oldCommand.CanExecuteChanged -= CanExecuteChanged; + } + + if (change.NewValue.GetValueOrDefault() is ICommand newCommand) + { + newCommand.CanExecuteChanged += CanExecuteChanged; + } } - } - } - protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) - { - base.UpdateDataValidation(property, value); - if (property == CommandProperty) + CanExecuteChanged(this, EventArgs.Empty); + } + else if (change.Property == CommandParameterProperty) { - if (value.Type == BindingValueType.BindingError) + CanExecuteChanged(this, EventArgs.Empty); + } + else if (change.Property == IsCancelProperty) + { + var isCancel = change.NewValue.GetValueOrDefault(); + + if (VisualRoot is IInputElement inputRoot) { - if (_commandCanExecute) + if (isCancel) { - _commandCanExecute = false; - UpdateIsEffectivelyEnabled(); + ListenForCancel(inputRoot); + } + else + { + StopListeningForCancel(inputRoot); } } } - } - - /// - /// Called when the property changes. - /// - /// The event args. - private static void CommandChanged(AvaloniaPropertyChangedEventArgs e) - { - if (e.Sender is Button button) + else if (change.Property == IsDefaultProperty) { - if (((ILogical)button).IsAttachedToLogicalTree) + var isDefault = change.NewValue.GetValueOrDefault(); + + if (VisualRoot is IInputElement inputRoot) { - if (e.OldValue is ICommand oldCommand) + if (isDefault) { - oldCommand.CanExecuteChanged -= button.CanExecuteChanged; + ListenForDefault(inputRoot); } - - if (e.NewValue is ICommand newCommand) + else { - newCommand.CanExecuteChanged += button.CanExecuteChanged; + StopListeningForDefault(inputRoot); } } - - button.CanExecuteChanged(button, EventArgs.Empty); } - } - - /// - /// Called when the property changes. - /// - /// The event args. - private static void CommandParameterChanged(AvaloniaPropertyChangedEventArgs e) - { - if (e.Sender is Button button) + else if (change.Property == IsPressedProperty) { - button.CanExecuteChanged(button, EventArgs.Empty); + UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); } - } - - /// - /// Called when the property changes. - /// - /// The event args. - private static void IsDefaultChanged(AvaloniaPropertyChangedEventArgs e) - { - var button = e.Sender as Button; - var isDefault = (bool)e.NewValue; - - if (button?.VisualRoot is IInputElement inputRoot) + else if (change.Property == FlyoutProperty) { - if (isDefault) - { - button.ListenForDefault(inputRoot); - } - else + // If flyout is changed while one is already open, make sure we + // close the old one first + if (change.OldValue.GetValueOrDefault() is FlyoutBase oldFlyout && + oldFlyout.IsOpen) { - button.StopListeningForDefault(inputRoot); + oldFlyout.Hide(); } } } - /// - /// Called when the property changes. - /// - /// The event args. - private static void IsCancelChanged(AvaloniaPropertyChangedEventArgs e) + /// + protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) { - var button = e.Sender as Button; - var isCancel = (bool)e.NewValue; - - if (button?.VisualRoot is IInputElement inputRoot) + base.UpdateDataValidation(property, value); + if (property == CommandProperty) { - if (isCancel) - { - button.ListenForCancel(inputRoot); - } - else + if (value.Type == BindingValueType.BindingError) { - button.StopListeningForCancel(inputRoot); + if (_commandCanExecute) + { + _commandCanExecute = false; + UpdateIsEffectivelyEnabled(); + } } } } @@ -566,6 +550,9 @@ namespace Avalonia.Controls } } + /// + /// Updates the visual state of the control by applying latest PseudoClasses. + /// private void UpdatePseudoClasses(bool isPressed) { PseudoClasses.Set(":pressed", isPressed); diff --git a/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs b/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs index a856ee071c..cd9c80d3e0 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs @@ -185,7 +185,8 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterDirect( nameof(SelectedDate), o => o.SelectedDate, - (o, v) => o.SelectedDate = v); + (o, v) => o.SelectedDate = v, + enableDataValidation: true); public static readonly StyledProperty SelectedDateFormatProperty = AvaloniaProperty.Register( @@ -533,13 +534,11 @@ namespace Avalonia.Controls } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) { - base.OnPropertyChanged(change); - - if (change.Property == SelectedDateProperty) + if (property == SelectedDateProperty) { - DataValidationErrors.SetError(this, change.NewValue.Error); + DataValidationErrors.SetError(this, value.Error); } } diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 441421181c..35648dd0b6 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -1,10 +1,12 @@ using System; using System.ComponentModel; +using System.Runtime.CompilerServices; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; +using Avalonia.Media; using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.VisualTree; @@ -60,7 +62,13 @@ namespace Avalonia.Controls public static readonly RoutedEvent ContextRequestedEvent = RoutedEvent.Register(nameof(ContextRequested), RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FlowDirectionProperty = + AvaloniaProperty.RegisterAttached(nameof(FlowDirection), inherits: true); + private DataTemplates? _dataTemplates; private IControl? _focusAdorner; @@ -108,6 +116,15 @@ namespace Avalonia.Controls get => GetValue(TagProperty); set => SetValue(TagProperty, value); } + + /// + /// Gets or sets the text flow direction. + /// + public FlowDirection FlowDirection + { + get => GetValue(FlowDirectionProperty); + set => SetValue(FlowDirectionProperty, value); + } /// /// Occurs when the user has completed a context input gesture, such as a right-click. diff --git a/src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs b/src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs index 3a45c85c70..bcd859100a 100644 --- a/src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs +++ b/src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs @@ -43,5 +43,18 @@ namespace Avalonia.Controls { return new MenuItemContainerGenerator(this); } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + foreach (var i in LogicalChildren) + { + if (i is MenuItem menuItem) + { + menuItem.IsSubMenuOpen = false; + } + } + } } } diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs index 83ad2b3638..a8e15ee463 100644 --- a/src/Avalonia.Controls/LayoutTransformControl.cs +++ b/src/Avalonia.Controls/LayoutTransformControl.cs @@ -18,7 +18,7 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(LayoutTransform)); public static readonly StyledProperty UseRenderTransformProperty = - AvaloniaProperty.Register(nameof(LayoutTransform)); + AvaloniaProperty.Register(nameof(UseRenderTransform)); static LayoutTransformControl() { diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index 2ceaeb6dba..4d048f0fb0 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -33,7 +33,7 @@ namespace Avalonia.Controls } - class CanExecuteChangedSubscriber : IWeakSubscriber + class CanExecuteChangedSubscriber : IWeakEventSubscriber { private readonly NativeMenuItem _parent; @@ -42,7 +42,7 @@ namespace Avalonia.Controls _parent = parent; } - public void OnEvent(object sender, EventArgs e) + public void OnEvent(object? sender, WeakEvent ev, EventArgs e) { _parent.CanExecuteChanged(); } @@ -160,14 +160,12 @@ namespace Avalonia.Controls set { if (_command != null) - WeakSubscriptionManager.Unsubscribe(_command, - nameof(ICommand.CanExecuteChanged), _canExecuteChangedSubscriber); + WeakEvents.CommandCanExecuteChanged.Unsubscribe(_command, _canExecuteChangedSubscriber); SetAndRaise(CommandProperty, ref _command, value); if (_command != null) - WeakSubscriptionManager.Subscribe(_command, - nameof(ICommand.CanExecuteChanged), _canExecuteChangedSubscriber); + WeakEvents.CommandCanExecuteChanged.Subscribe(_command, _canExecuteChangedSubscriber); CanExecuteChanged(); } diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index a2bdcd1ea8..18348571bf 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -101,7 +101,7 @@ namespace Avalonia.Controls.Platform root.Deactivated -= WindowDeactivated; } - if (_root is TopLevel tl) + if (_root is TopLevel tl && tl.PlatformImpl != null) tl.PlatformImpl.LostFocus -= TopLevelLostPlatformFocus; _inputManagerSubscription?.Dispose(); diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index a62ba306ab..8c5e644851 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -60,6 +60,12 @@ namespace Avalonia.Controls.Presenters o => o.Viewport, (o, v) => o.Viewport = v); + /// + /// Defines the property. + /// + public static readonly StyledProperty IsScrollChainingEnabledProperty = + ScrollViewer.IsScrollChainingEnabledProperty.AddOwner(); + private bool _canHorizontallyScroll; private bool _canVerticallyScroll; private bool _arranging; @@ -138,6 +144,20 @@ namespace Avalonia.Controls.Presenters private set { SetAndRaise(ViewportProperty, ref _viewport, value); } } + /// + /// Gets or sets if scroll chaining is enabled. The default value is true. + /// + /// + /// After a user hits a scroll limit on an element that has been nested within another scrollable element, + /// you can specify whether that parent element should continue the scrolling operation begun in its child element. + /// This is called scroll chaining. + /// + public bool IsScrollChainingEnabled + { + get => GetValue(IsScrollChainingEnabledProperty); + set => SetValue(IsScrollChainingEnabledProperty, value); + } + /// IControl? IScrollAnchorProvider.CurrentAnchor { @@ -405,8 +425,11 @@ namespace Avalonia.Controls.Presenters _activeLogicalGestureScrolls[e.Id] = delta; } - Offset = new Vector(x, y); - e.Handled = true; + Vector newOffset = new Vector(x, y); + bool offsetChanged = newOffset != Offset; + Offset = newOffset; + + e.Handled = !IsScrollChainingEnabled || offsetChanged; } } @@ -440,8 +463,11 @@ namespace Avalonia.Controls.Presenters x = Math.Min(x, Extent.Width - Viewport.Width); } - Offset = new Vector(x, y); - e.Handled = true; + Vector newOffset = new Vector(x, y); + bool offsetChanged = newOffset != Offset; + Offset = newOffset; + + e.Handled = !IsScrollChainingEnabled || offsetChanged; } } diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index ff63e5644f..8bbc7a9d27 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -1,9 +1,12 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; using System.Reactive.Linq; -using Avalonia.Input.TextInput; using Avalonia.Media; +using Avalonia.Media.TextFormatting; using Avalonia.Metadata; using Avalonia.Threading; +using Avalonia.Utilities; using Avalonia.VisualTree; using Avalonia.Layout; using Avalonia.Media.Immutable; @@ -75,28 +78,18 @@ namespace Avalonia.Controls.Presenters private int _selectionEnd; private bool _caretBlink; private string _text; - private FormattedText _formattedText; - private Size _constraint; + private TextLayout _textLayout; + private Size _constraint = Size.Infinity; - static TextPresenter() - { - AffectsRender(SelectionBrushProperty, TextBlock.ForegroundProperty, - SelectionForegroundBrushProperty, CaretBrushProperty, - SelectionStartProperty, SelectionEndProperty); - - AffectsMeasure(TextProperty, PasswordCharProperty, RevealPasswordProperty, - TextAlignmentProperty, TextWrappingProperty, TextBlock.FontSizeProperty, - TextBlock.FontStyleProperty, TextBlock.FontWeightProperty, TextBlock.FontFamilyProperty); + private CharacterHit _lastCharacterHit; + private Rect _caretBounds; + private Point _navigationPosition; - Observable.Merge(TextProperty.Changed, TextBlock.ForegroundProperty.Changed, - TextAlignmentProperty.Changed, TextWrappingProperty.Changed, - TextBlock.FontSizeProperty.Changed, TextBlock.FontStyleProperty.Changed, - TextBlock.FontWeightProperty.Changed, TextBlock.FontFamilyProperty.Changed, - SelectionStartProperty.Changed, SelectionEndProperty.Changed, - SelectionForegroundBrushProperty.Changed, PasswordCharProperty.Changed, RevealPasswordProperty.Changed - ).AddClassHandler((x, _) => x.InvalidateFormattedText()); + private ScrollViewer _scrollViewer; - CaretIndexProperty.Changed.AddClassHandler((x, e) => x.CaretIndexChanged((int)e.NewValue)); + static TextPresenter() + { + AffectsRender(CaretBrushProperty, SelectionBrushProperty); } public TextPresenter() @@ -106,6 +99,8 @@ namespace Avalonia.Controls.Presenters _caretTimer.Tick += CaretTimerTick; } + public event EventHandler CaretBoundsChanged; + /// /// Gets or sets a brush used to paint the control's background. /// @@ -189,13 +184,22 @@ namespace Avalonia.Controls.Presenters } /// - /// Gets the used to render the text. + /// Gets the used to render the text. /// - public FormattedText FormattedText + public TextLayout TextLayout { get { - return _formattedText ?? (_formattedText = CreateFormattedText()); + if (_textLayout != null) + { + return _textLayout; + } + + _textLayout = CreateTextLayout(); + + UpdateCaret(_lastCharacterHit); + + return _textLayout; } } @@ -205,11 +209,12 @@ namespace Avalonia.Controls.Presenters { return _caretIndex; } - set { - value = CoerceCaretIndex(value); - SetAndRaise(CaretIndexProperty, ref _caretIndex, value); + if (value != _caretIndex) + { + MoveCaretToTextPosition(value); + } } } @@ -271,37 +276,25 @@ namespace Avalonia.Controls.Presenters } } - public int GetCaretIndex(Point point) - { - var hit = FormattedText.HitTestPoint(point); - return hit.TextPosition + (hit.IsTrailing ? 1 : 0); - } - /// - /// Creates the used to render the text. + /// Creates the used to render the text. /// /// The constraint of the text. /// The text to format. - /// A object. - private FormattedText CreateFormattedTextInternal(Size constraint, string text) - { - return new FormattedText - { - Constraint = constraint, - Typeface = new Typeface(FontFamily, FontStyle, FontWeight), - FontSize = FontSize, - Text = text ?? string.Empty, - TextAlignment = TextAlignment, - TextWrapping = TextWrapping, - }; - } + /// + /// + /// A object. + private TextLayout CreateTextLayoutInternal(Size constraint, string text, Typeface typeface, + IReadOnlyList> textStyleOverrides) + { + var maxWidth = MathUtilities.IsZero(constraint.Width) ? double.PositiveInfinity : constraint.Width; + var maxHeight = MathUtilities.IsZero(constraint.Height) ? double.PositiveInfinity : constraint.Height; + + var textLayout = new TextLayout(text ?? string.Empty, typeface, FontSize, Foreground, TextAlignment, + TextWrapping, maxWidth: maxWidth, maxHeight: maxHeight, textStyleOverrides: textStyleOverrides, + flowDirection: FlowDirection); - /// - /// Invalidates . - /// - protected void InvalidateFormattedText() - { - _formattedText = null; + return textLayout; } /// @@ -317,31 +310,36 @@ namespace Avalonia.Controls.Presenters context.FillRectangle(background, new Rect(Bounds.Size)); } - double top = 0; - var textSize = FormattedText.Bounds.Size; + var top = 0d; + var left = 0.0; + + var (_, textHeight) = TextLayout.Size; - if (Bounds.Height < textSize.Height) + if (Bounds.Height < textHeight) { switch (VerticalAlignment) { case VerticalAlignment.Center: - top += (Bounds.Height - textSize.Height) / 2; + top += (Bounds.Height - textHeight) / 2; break; case VerticalAlignment.Bottom: - top += (Bounds.Height - textSize.Height); + top += (Bounds.Height - textHeight); break; } } - context.DrawText(Foreground, new Point(0, top), FormattedText); + TextLayout.Draw(context, new Point(left, top)); } public override void Render(DrawingContext context) { - FormattedText.Constraint = Bounds.Size; - - _constraint = Bounds.Size; + if (double.IsPositiveInfinity (_constraint.Width)) + { + _constraint = _scrollViewer?.Viewport ?? Size.Infinity; + + InvalidateTextLayout(); + } var selectionStart = SelectionStart; var selectionEnd = SelectionEnd; @@ -351,7 +349,7 @@ namespace Avalonia.Controls.Presenters var start = Math.Min(selectionStart, selectionEnd); var length = Math.Max(selectionStart, selectionEnd) - start; - var rects = FormattedText.HitTestTextRange(start, length); + var rects = TextLayout.HitTestTextRange(start, length); foreach (var rect in rects) { @@ -361,40 +359,47 @@ namespace Avalonia.Controls.Presenters RenderInternal(context); - if (selectionStart == selectionEnd && _caretBlink) + if (selectionStart != selectionEnd || !_caretBlink) + { + return; + } + + var caretBrush = CaretBrush?.ToImmutable(); + + if (caretBrush is null) { - var caretBrush = CaretBrush?.ToImmutable(); + var backgroundColor = (Background as ISolidColorBrush)?.Color; - if (caretBrush is null) + if (backgroundColor.HasValue) { - var backgroundColor = (Background as ISolidColorBrush)?.Color; - if (backgroundColor.HasValue) - { - byte red = (byte)~(backgroundColor.Value.R); - byte green = (byte)~(backgroundColor.Value.G); - byte blue = (byte)~(backgroundColor.Value.B); + var red = (byte)~(backgroundColor.Value.R); + var green = (byte)~(backgroundColor.Value.G); + var blue = (byte)~(backgroundColor.Value.B); - caretBrush = new ImmutableSolidColorBrush(Color.FromRgb(red, green, blue)); - } - else - { - caretBrush = Brushes.Black; - } + caretBrush = new ImmutableSolidColorBrush(Color.FromRgb(red, green, blue)); + } + else + { + caretBrush = Brushes.Black; } - - var (p1, p2) = GetCaretPoints(); - context.DrawLine( - new ImmutablePen(caretBrush, 1), - p1, p2); } - } - (Point, Point) GetCaretPoints() + var (p1, p2) = GetCaretPoints(); + + context.DrawLine(new ImmutablePen(caretBrush), p1, p2); + } + + private (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; + var x = Math.Floor(_caretBounds.X) + 0.5; + var y = Math.Floor(_caretBounds.Y) + 0.5; + var b = Math.Ceiling(_caretBounds.Bottom) - 0.5; + + if (x >= Bounds.Width) + { + x = Math.Floor(_caretBounds.X - 1) + 0.5; + } + return (new Point(x, y), new Point(x, b)); } @@ -412,7 +417,7 @@ namespace Avalonia.Controls.Presenters InvalidateVisual(); } - internal void CaretIndexChanged(int caretIndex) + internal void CaretChanged() { if (this.GetVisualParent() != null) { @@ -432,8 +437,7 @@ namespace Avalonia.Controls.Presenters if (IsMeasureValid) { - var rect = FormattedText.HitTestTextPosition(caretIndex); - this.BringIntoView(rect); + this.BringIntoView(_caretBounds); } else { @@ -443,8 +447,7 @@ namespace Avalonia.Controls.Presenters Dispatcher.UIThread.Post( () => { - var rect = FormattedText.HitTestTextPosition(caretIndex); - this.BringIntoView(rect); + this.BringIntoView(_caretBounds); }, DispatcherPriority.Render); } @@ -452,104 +455,310 @@ namespace Avalonia.Controls.Presenters } /// - /// Creates the used to render the text. + /// Creates the used to render the text. /// - /// A object. - protected virtual FormattedText CreateFormattedText() + /// A object. + protected virtual TextLayout CreateTextLayout() { - FormattedText result = null; + TextLayout result; var text = Text; - if (PasswordChar != default(char) && !RevealPassword) - { - result = CreateFormattedTextInternal(_constraint, new string(PasswordChar, text?.Length ?? 0)); - } - else - { - result = CreateFormattedTextInternal(_constraint, text); - } + var typeface = new Typeface(FontFamily, FontStyle, FontWeight); var selectionStart = SelectionStart; var selectionEnd = SelectionEnd; var start = Math.Min(selectionStart, selectionEnd); var length = Math.Max(selectionStart, selectionEnd) - start; + IReadOnlyList> textStyleOverrides = null; + if (length > 0) { - result.Spans = new[] + textStyleOverrides = new[] { - new FormattedTextStyleSpan(start, length, SelectionForegroundBrush), + new ValueSpan(start, length, + new GenericTextRunProperties(typeface, FontSize, + foregroundBrush: SelectionForegroundBrush ?? Brushes.White)) }; } + if (PasswordChar != default(char) && !RevealPassword) + { + result = CreateTextLayoutInternal(_constraint, new string(PasswordChar, text?.Length ?? 0), typeface, + textStyleOverrides); + } + else + { + result = CreateTextLayoutInternal(_constraint, text, typeface, textStyleOverrides); + } + return result; } - /// - /// Measures the control. - /// - /// The available size for the control. - /// The desired size. - private Size MeasureInternal(Size availableSize) + protected virtual void InvalidateTextLayout() + { + _textLayout = null; + + InvalidateMeasure(); + } + + protected override Size MeasureOverride(Size availableSize) + { + if (!double.IsInfinity(availableSize.Width) && availableSize != _constraint) + { + _constraint = availableSize; + + InvalidateTextLayout(); + } + + return TextLayout.Size; + } + + private int CoerceCaretIndex(int value) + { + var text = Text; + var length = text?.Length ?? 0; + return Math.Max(0, Math.Min(length, value)); + } + + private void CaretTimerTick(object sender, EventArgs e) + { + _caretBlink = !_caretBlink; + InvalidateVisual(); + } + + public void MoveCaretToTextPosition(int textPosition, bool trailingEdge = false) + { + var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(textPosition, trailingEdge); + var textLine = TextLayout.TextLines[lineIndex]; + + var characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(textPosition)); + + var nextCaretCharacterHit = textLine.GetNextCaretCharacterHit(characterHit); + + if (nextCaretCharacterHit.FirstCharacterIndex <= textPosition) + { + characterHit = nextCaretCharacterHit; + } + + if (textPosition == characterHit.FirstCharacterIndex + characterHit.TrailingLength) + { + UpdateCaret(characterHit); + } + else + { + UpdateCaret(trailingEdge ? characterHit : new CharacterHit(characterHit.FirstCharacterIndex)); + } + + _navigationPosition = _caretBounds.Position; + } + + public void MoveCaretToPoint(Point point) + { + var hit = TextLayout.HitTestPoint(point); + + UpdateCaret(hit.CharacterHit); + + _navigationPosition = _caretBounds.Position; + } + + public void MoveCaretVertical(LogicalDirection direction = LogicalDirection.Forward) { - if (!string.IsNullOrEmpty(Text)) + var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(CaretIndex, _lastCharacterHit.TrailingLength > 0); + + if (lineIndex < 0) + { + return; + } + + var (currentX, currentY) = _navigationPosition; + + if (direction == LogicalDirection.Forward) { - if (TextWrapping == TextWrapping.Wrap) + if (lineIndex + 1 > TextLayout.TextLines.Count - 1) { - _constraint = new Size(availableSize.Width, double.PositiveInfinity); + return; } - else + + var textLine = TextLayout.TextLines[lineIndex]; + + currentY += textLine.Height; + } + else + { + if (lineIndex - 1 < 0) { - _constraint = Size.Infinity; + return; } - _formattedText = null; + var textLine = TextLayout.TextLines[--lineIndex]; - return FormattedText.Bounds.Size; + currentY -= textLine.Height; } - return new Size(); + var navigationPosition = _navigationPosition; + + MoveCaretToPoint(new Point(currentX, currentY)); + + _navigationPosition = navigationPosition.WithY(_caretBounds.Y); } - protected override Size MeasureOverride(Size availableSize) + public void MoveCaretHorizontal(LogicalDirection direction = LogicalDirection.Forward) { - var text = Text; + var characterHit = _lastCharacterHit; + var caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; + + var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(caretIndex, false); - if (!string.IsNullOrEmpty(text)) + if (lineIndex < 0) { - return MeasureInternal(availableSize); + return; + } + + if (direction == LogicalDirection.Forward) + { + while (lineIndex < TextLayout.TextLines.Count) + { + var textLine = TextLayout.TextLines[lineIndex]; + + characterHit = textLine.GetNextCaretCharacterHit(characterHit); + + caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; + + if (textLine.NewLineLength > 0 && caretIndex == textLine.TextRange.Start + textLine.TextRange.Length) + { + characterHit = new CharacterHit(caretIndex); + } + + if (caretIndex >= Text.Length) + { + characterHit = new CharacterHit(Text.Length); + + break; + } + + if (caretIndex - textLine.NewLineLength == textLine.TextRange.Start + textLine.TextRange.Length) + { + break; + } + + if (caretIndex <= CaretIndex) + { + lineIndex++; + + continue; + } + + break; + } } else { - return new FormattedText + while (lineIndex >= 0) { - Text = "X", - Typeface = new Typeface(FontFamily, FontStyle, FontWeight), - FontSize = FontSize, - TextAlignment = TextAlignment, - Constraint = availableSize, - }.Bounds.Size; + var textLine = TextLayout.TextLines[lineIndex]; + + characterHit = textLine.GetPreviousCaretCharacterHit(characterHit); + + caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; + + if (caretIndex >= CaretIndex) + { + lineIndex--; + + continue; + } + + break; + } } + + UpdateCaret(characterHit); + + _navigationPosition = _caretBounds.Position; } - private int CoerceCaretIndex(int value) + private void UpdateCaret(CharacterHit characterHit) { - var text = Text; - var length = text?.Length ?? 0; - return Math.Max(0, Math.Min(length, value)); + _lastCharacterHit = characterHit; + + var caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; + + var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(caretIndex, characterHit.TrailingLength > 0); + var textLine = TextLayout.TextLines[lineIndex]; + var distanceX = textLine.GetDistanceFromCharacterHit(characterHit); + + var distanceY = 0d; + + for (var i = 0; i < lineIndex; i++) + { + var currentLine = TextLayout.TextLines[i]; + + distanceY += currentLine.Height; + } + + var caretBounds = new Rect(distanceX, distanceY, 0, textLine.Height); + + if (caretBounds != _caretBounds) + { + _caretBounds = caretBounds; + + CaretBoundsChanged?.Invoke(this, EventArgs.Empty); + } + + CaretChanged(); + + SetAndRaise(CaretIndexProperty, ref _caretIndex, caretIndex); } - private void CaretTimerTick(object sender, EventArgs e) + internal Rect GetCursorRectangle() { - _caretBlink = !_caretBlink; - InvalidateVisual(); + return _caretBounds; } - internal Rect GetCursorRectangle() + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { - var (p1, p2) = GetCaretPoints(); - return new Rect(p1, p2); + base.OnAttachedToVisualTree(e); + + _scrollViewer = this.FindAncestorOfType(); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + _scrollViewer = null; + + _caretTimer.Stop(); + + _caretTimer.Tick -= CaretTimerTick; + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + switch (change.Property.Name) + { + case nameof (TextBlock.Foreground): + case nameof (TextBlock.FontSize): + case nameof (TextBlock.FontStyle): + case nameof (TextBlock.FontWeight): + case nameof (TextBlock.FontFamily): + case nameof (Text): + case nameof (TextAlignment): + case nameof (TextWrapping): + case nameof (SelectionStart): + case nameof (SelectionEnd): + case nameof (SelectionForegroundBrush): + case nameof (PasswordChar): + case nameof (RevealPassword): + { + InvalidateTextLayout(); + break; + } + } } } } diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index ffab7f86d1..a47149a9e0 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -429,16 +429,20 @@ namespace Avalonia.Controls.Primitives (x, handler) => x.LostFocus += handler, (x, handler) => x.LostFocus -= handler).DisposeWith(handlerCleanup); - SubscribeToEventHandler>(window.PlatformImpl, WindowPositionChanged, - (x, handler) => x.PositionChanged += handler, - (x, handler) => x.PositionChanged -= handler).DisposeWith(handlerCleanup); - - if (placementTarget is Layoutable layoutTarget) + // Recalculate popup position on parent moved/resized, but not if placement was on pointer + if (PlacementMode != PlacementMode.Pointer) { - // If the placement target is moved, update the popup position - SubscribeToEventHandler(layoutTarget, PlacementTargetLayoutUpdated, - (x, handler) => x.LayoutUpdated += handler, - (x, handler) => x.LayoutUpdated -= handler).DisposeWith(handlerCleanup); + SubscribeToEventHandler>(window.PlatformImpl, WindowPositionChanged, + (x, handler) => x.PositionChanged += handler, + (x, handler) => x.PositionChanged -= handler).DisposeWith(handlerCleanup); + + if (placementTarget is Layoutable layoutTarget) + { + // If the placement target is moved, update the popup position + SubscribeToEventHandler(layoutTarget, PlacementTargetLayoutUpdated, + (x, handler) => x.LayoutUpdated += handler, + (x, handler) => x.LayoutUpdated -= handler).DisposeWith(handlerCleanup); + } } } else if (topLevel is PopupRoot parentPopupRoot) diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index 1a11778db2..517ada79f1 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -44,7 +44,7 @@ namespace Avalonia.Controls.Primitives /// The dependency resolver to use. If null the default dependency resolver will be used. /// public PopupRoot(TopLevel parent, IPopupImpl impl, IAvaloniaDependencyResolver dependencyResolver) - : base(impl, dependencyResolver) + : base(ValidatingPopupImpl.Wrap(impl), dependencyResolver) { _parent = parent; } @@ -74,7 +74,11 @@ namespace Avalonia.Controls.Primitives IStyleHost IStyleHost.StylingParent => Parent; /// - public void Dispose() => PlatformImpl?.Dispose(); + public void Dispose() + { + PlatformImpl?.Dispose(); + HandleClosed(); + } private void UpdatePosition() { diff --git a/src/Avalonia.Controls/Properties/AssemblyInfo.cs b/src/Avalonia.Controls/Properties/AssemblyInfo.cs index d1743611cd..05561a38ef 100644 --- a/src/Avalonia.Controls/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Controls/Properties/AssemblyInfo.cs @@ -3,6 +3,7 @@ using Avalonia.Metadata; [assembly: InternalsVisibleTo("Avalonia.Controls.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.DesignerSupport, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] +[assembly: InternalsVisibleTo("Avalonia.LeakTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")] diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index ecc0fa3a48..b40cf26df5 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -20,7 +20,7 @@ namespace Avalonia.Controls /// Represents a data-driven collection control that incorporates a flexible layout system, /// custom views, and virtualization. /// - public class ItemsRepeater : Panel, IChildIndexProvider + public class ItemsRepeater : Panel, IChildIndexProvider, IWeakEventSubscriber { /// /// Defines the property. @@ -723,14 +723,8 @@ namespace Avalonia.Controls { oldValue.UninitializeForContext(LayoutContext); - WeakEventHandlerManager.Unsubscribe( - oldValue, - nameof(AttachedLayout.MeasureInvalidated), - InvalidateMeasureForLayout); - WeakEventHandlerManager.Unsubscribe( - oldValue, - nameof(AttachedLayout.ArrangeInvalidated), - InvalidateArrangeForLayout); + AttachedLayout.MeasureInvalidatedWeakEvent.Unsubscribe(oldValue, this); + AttachedLayout.ArrangeInvalidatedWeakEvent.Unsubscribe(oldValue, this); // Walk through all the elements and make sure they are cleared foreach (var element in Children) @@ -748,14 +742,8 @@ namespace Avalonia.Controls { newValue.InitializeForContext(LayoutContext); - WeakEventHandlerManager.Subscribe( - newValue, - nameof(AttachedLayout.MeasureInvalidated), - InvalidateMeasureForLayout); - WeakEventHandlerManager.Subscribe( - newValue, - nameof(AttachedLayout.ArrangeInvalidated), - InvalidateArrangeForLayout); + AttachedLayout.MeasureInvalidatedWeakEvent.Subscribe(newValue, this); + AttachedLayout.ArrangeInvalidatedWeakEvent.Subscribe(newValue, this); } bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout; @@ -806,9 +794,13 @@ namespace Avalonia.Controls _viewportManager.OnBringIntoViewRequested(e); } - private void InvalidateMeasureForLayout(object sender, EventArgs e) => InvalidateMeasure(); - - private void InvalidateArrangeForLayout(object sender, EventArgs e) => InvalidateArrange(); + void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, EventArgs e) + { + if(ev == AttachedLayout.ArrangeInvalidatedWeakEvent) + InvalidateArrange(); + else if (ev == AttachedLayout.MeasureInvalidatedWeakEvent) + InvalidateMeasure(); + } private VirtualizingLayoutContext GetLayoutContext() { diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index eee6216587..10a014e81d 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -181,6 +181,14 @@ namespace Avalonia.Controls nameof(AllowAutoHide), true); + /// + /// Defines the property. + /// + public static readonly AttachedProperty IsScrollChainingEnabledProperty = + AvaloniaProperty.RegisterAttached( + nameof(IsScrollChainingEnabled), + defaultValue: true); + /// /// Defines the event. /// @@ -418,6 +426,20 @@ namespace Avalonia.Controls set => SetValue(AllowAutoHideProperty, value); } + /// + /// Gets or sets if scroll chaining is enabled. The default value is true. + /// + /// + /// After a user hits a scroll limit on an element that has been nested within another scrollable element, + /// you can specify whether that parent element should continue the scrolling operation begun in its child element. + /// This is called scroll chaining. + /// + public bool IsScrollChainingEnabled + { + get => GetValue(IsScrollChainingEnabledProperty); + set => SetValue(IsScrollChainingEnabledProperty, value); + } + /// /// Scrolls the content up one line. /// @@ -548,6 +570,36 @@ namespace Avalonia.Controls return control.GetValue(AllowAutoHideProperty); } + /// + /// Sets the value of the IsScrollChainingEnabled attached property. + /// + /// The control to set the value on. + /// The value of the property. + /// + /// After a user hits a scroll limit on an element that has been nested within another scrollable element, + /// you can specify whether that parent element should continue the scrolling operation begun in its child element. + /// This is called scroll chaining. + /// + public static void SetIsScrollChainingEnabled(Control control, bool value) + { + control.SetValue(IsScrollChainingEnabledProperty, value); + } + + /// + /// Gets the value of the IsScrollChainingEnabled attached property. + /// + /// The control to read the value from. + /// The value of the property. + /// + /// After a user hits a scroll limit on an element that has been nested within another scrollable element, + /// you can specify whether that parent element should continue the scrolling operation begun in its child element. + /// This is called scroll chaining. + /// + public static bool GetIsScrollChainingEnabled(Control control) + { + return control.GetValue(IsScrollChainingEnabledProperty); + } + /// /// Gets the value of the VerticalScrollBarVisibility attached property. /// diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 14cde774f4..8291433d45 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -131,21 +131,8 @@ namespace Avalonia.Controls static TextBlock() { ClipToBoundsProperty.OverrideDefaultValue(true); - - AffectsRender(BackgroundProperty, ForegroundProperty, - TextAlignmentProperty, TextDecorationsProperty); - - AffectsMeasure(FontSizeProperty, FontWeightProperty, - FontStyleProperty, TextWrappingProperty, FontFamilyProperty, - TextTrimmingProperty, TextProperty, PaddingProperty, LineHeightProperty, MaxLinesProperty); - - Observable.Merge(TextProperty.Changed, ForegroundProperty.Changed, - TextAlignmentProperty.Changed, TextWrappingProperty.Changed, - TextTrimmingProperty.Changed, FontSizeProperty.Changed, - FontStyleProperty.Changed, FontWeightProperty.Changed, - FontFamilyProperty.Changed, TextDecorationsProperty.Changed, - PaddingProperty.Changed, MaxLinesProperty.Changed, LineHeightProperty.Changed - ).AddClassHandler((x, _) => x.InvalidateTextLayout()); + + AffectsRender(BackgroundProperty, ForegroundProperty); } /// @@ -460,6 +447,7 @@ namespace Avalonia.Controls TextWrapping, TextTrimming, TextDecorations, + FlowDirection, constraint.Width, constraint.Height, maxLines: MaxLines, @@ -472,6 +460,8 @@ namespace Avalonia.Controls protected void InvalidateTextLayout() { _textLayout = null; + + InvalidateMeasure(); } /// @@ -507,12 +497,40 @@ namespace Avalonia.Controls base.OnAttachedToLogicalTree(e); InvalidateTextLayout(); - - InvalidateMeasure(); } private static bool IsValidMaxLines(int maxLines) => maxLines >= 0; private static bool IsValidLineHeight(double lineHeight) => double.IsNaN(lineHeight) || lineHeight > 0; + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + switch (change.Property.Name) + { + case nameof (FontSize): + case nameof (FontWeight): + case nameof (FontStyle): + case nameof (FontFamily): + + case nameof (TextWrapping): + case nameof (TextTrimming): + case nameof (TextAlignment): + case nameof (FlowDirection): + + case nameof (Padding): + case nameof (LineHeight): + case nameof (MaxLines): + + case nameof (Text): + case nameof (TextDecorations): + case nameof (Foreground): + { + InvalidateTextLayout(); + break; + } + } + } } } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 20d8a94c1a..c8955f2e1c 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -14,6 +14,8 @@ using Avalonia.Data; using Avalonia.Layout; using Avalonia.Utilities; using Avalonia.Controls.Metadata; +using Avalonia.Media.TextFormatting; +using Avalonia.Media.TextFormatting.Unicode; namespace Avalonia.Controls { @@ -250,6 +252,7 @@ namespace Avalonia.Controls { value = CoerceCaretIndex(value); SetAndRaise(CaretIndexProperty, ref _caretIndex, value); + UndoRedoState state; if (IsUndoEnabled && _undoRedoHelper.TryGetLastState(out state) && state.Text == Text) _undoRedoHelper.UpdateLastState(); @@ -301,7 +304,8 @@ namespace Avalonia.Controls { UpdateCommandStates(); } - if (SelectionStart == SelectionEnd) + + if (value == SelectionEnd) { CaretIndex = SelectionStart; } @@ -319,13 +323,15 @@ namespace Avalonia.Controls { value = CoerceCaretIndex(value); var changed = SetAndRaise(SelectionEndProperty, ref _selectionEnd, value); + if (changed) { UpdateCommandStates(); } - if (SelectionStart == SelectionEnd) + + if (value == SelectionStart) { - CaretIndex = SelectionEnd; + CaretIndex = value; } } } @@ -345,6 +351,7 @@ namespace Avalonia.Controls if (!_ignoreTextChanges) { var caretIndex = CaretIndex; + SelectionStart = CoerceCaretIndex(SelectionStart, value); SelectionEnd = CoerceCaretIndex(SelectionEnd, value); CaretIndex = CoerceCaretIndex(caretIndex, value); @@ -533,13 +540,27 @@ namespace Avalonia.Controls protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { _presenter = e.NameScope.Get("PART_TextPresenter"); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + _imClient.SetPresenter(_presenter, this); + if (IsFocused) { _presenter?.ShowCaret(); } } + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + _imClient.SetPresenter(null, null); + } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -631,9 +652,8 @@ namespace Avalonia.Controls _selectedTextChangesMadeSinceLastUndoSnapshot++; SnapshotUndoRedo(ignoreChangeCount: false); - string text = Text ?? string.Empty; - int caretIndex = CaretIndex; - int newLength = input.Length + text.Length - Math.Abs(SelectionStart - SelectionEnd); + var text = Text ?? string.Empty; + var newLength = input.Length + text.Length - Math.Abs(SelectionStart - SelectionEnd); if (MaxLength > 0 && newLength > MaxLength) { @@ -649,11 +669,11 @@ namespace Avalonia.Controls try { DeleteSelection(false); - caretIndex = CaretIndex; + var caretIndex = CaretIndex; text = Text ?? string.Empty; SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex)); - CaretIndex += input.Length; ClearSelection(); + if (IsUndoEnabled) { _undoRedoHelper.DiscardRedo(); @@ -663,6 +683,8 @@ namespace Avalonia.Controls { RaisePropertyChanged(TextProperty, oldText, _text); } + + CaretIndex = caretIndex + input.Length; } finally { @@ -684,6 +706,7 @@ namespace Avalonia.Controls public async void Cut() { var text = GetSelection(); + if (string.IsNullOrEmpty(text)) { return; @@ -703,6 +726,7 @@ namespace Avalonia.Controls public async void Copy() { var text = GetSelection(); + if (string.IsNullOrEmpty(text)) { return; @@ -739,11 +763,16 @@ namespace Avalonia.Controls protected override void OnKeyDown(KeyEventArgs e) { - string text = Text ?? string.Empty; - int caretIndex = CaretIndex; - bool movement = false; - bool selection = false; - bool handled = false; + if (_presenter == null) + { + return; + } + + var text = Text ?? string.Empty; + var caretIndex = CaretIndex; + var movement = false; + var selection = false; + var handled = false; var modifiers = e.KeyModifiers; var keymap = AvaloniaLocator.Current.GetService(); @@ -884,46 +913,85 @@ namespace Avalonia.Controls break; case Key.Up: - movement = MoveVertical(-1); + { selection = DetectSelection(); - break; + + _presenter.MoveCaretVertical(LogicalDirection.Backward); + + if (caretIndex != _presenter.CaretIndex) + { + movement = true; + } + if (selection) + { + SelectionEnd = _presenter.CaretIndex; + } + + break; + } case Key.Down: - movement = MoveVertical(1); + { selection = DetectSelection(); + + _presenter?.MoveCaretVertical(); + + if (caretIndex != _presenter.CaretIndex) + { + movement = true; + } + + if (selection) + { + SelectionEnd = _presenter.CaretIndex; + } + break; - + } case Key.Back: + { SnapshotUndoRedo(); + if (hasWholeWordModifiers && SelectionStart == SelectionEnd) { SetSelectionForControlBackspace(); } - if (!DeleteSelection() && CaretIndex > 0) + if (!DeleteSelection() && caretIndex > 0) { - var removedCharacters = 1; - // handle deleting /r/n - // you don't ever want to leave a dangling /r around. So, if deleting /n, check to see if - // a /r should also be deleted. - if (CaretIndex > 1 && - text[CaretIndex - 1] == '\n' && - text[CaretIndex - 2] == '\r') + var removedCharacters = 0; + + // \r\n needs special treatment here + if (caretIndex - 1 > 0 && text[caretIndex - 1] == '\n' && text[caretIndex - 2] == '\r') { removedCharacters = 2; } + else + { + Codepoint.ReadAt(text.AsMemory(), caretIndex - 1, out removedCharacters); + } + + if (removedCharacters == 0) + { + return; + } + + var length = Math.Max(0, caretIndex - removedCharacters); - SetTextInternal(text.Substring(0, caretIndex - removedCharacters) + + SetTextInternal(text.Substring(0, length) + text.Substring(caretIndex)); - CaretIndex -= removedCharacters; + + CaretIndex = caretIndex - removedCharacters; + ClearSelection(); } handled = true; break; - + } case Key.Delete: SnapshotUndoRedo(); + if (hasWholeWordModifiers && SelectionStart == SelectionEnd) { SetSelectionForControlDelete(); @@ -931,21 +999,18 @@ namespace Avalonia.Controls if (!DeleteSelection() && caretIndex < text.Length) { - var removedCharacters = 1; - // handle deleting /r/n - // you don't ever want to leave a dangling /r around. So, if deleting /n, check to see if - // a /r should also be deleted. - if (CaretIndex < text.Length - 1 && - text[caretIndex + 1] == '\n' && - text[caretIndex] == '\r') - { - removedCharacters = 2; - } + _presenter.MoveCaretHorizontal(); + + var removedCharacters = Math.Max(0, _presenter.CaretIndex - caretIndex); SetTextInternal(text.Substring(0, caretIndex) + text.Substring(caretIndex + removedCharacters)); + + CaretIndex = caretIndex; } + SnapshotUndoRedo(); + handled = true; break; @@ -983,11 +1048,7 @@ namespace Avalonia.Controls } } - if (movement && selection) - { - SelectionEnd = CaretIndex; - } - else if (movement) + if (movement && !selection) { ClearSelection(); } @@ -1000,19 +1061,28 @@ namespace Avalonia.Controls protected override void OnPointerPressed(PointerPressedEventArgs e) { + if (_presenter == null) + { + return; + } + var text = Text; - var clickInfo = e.GetCurrentPoint(this); - if (text != null && clickInfo.Properties.IsLeftButtonPressed && !(clickInfo.Pointer?.Captured is Border)) + + if (text != null && clickInfo.Properties.IsLeftButtonPressed && + !(clickInfo.Pointer?.Captured is Border)) { var point = e.GetPosition(_presenter); - var index = _presenter.GetCaretIndex(point); - var clickToSelect = index != CaretIndex && e.KeyModifiers.HasFlag(KeyModifiers.Shift); - if (!clickToSelect) - { - CaretIndex = index; - } + var oldIndex = CaretIndex; + + _presenter.MoveCaretToPoint(point); + + var index = _presenter.CaretIndex; + + var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift); + + SetAndRaise(CaretIndexProperty, ref _caretIndex, index); #pragma warning disable CS0618 // Type or member is obsolete switch (e.ClickCount) @@ -1021,13 +1091,14 @@ namespace Avalonia.Controls case 1: if (clickToSelect) { - SelectionStart = Math.Min(index, CaretIndex); - SelectionEnd = Math.Max(index, CaretIndex); + SelectionStart = Math.Min(oldIndex, index); + SelectionEnd = Math.Max(oldIndex, index); } else { SelectionStart = SelectionEnd = index; } + break; case 2: if (!StringUtils.IsStartOfWord(text, index)) @@ -1049,8 +1120,13 @@ namespace Avalonia.Controls protected override void OnPointerMoved(PointerEventArgs e) { + if (_presenter == null) + { + return; + } + // selection should not change during pointer move if the user right clicks - if (_presenter != null && e.Pointer.Captured == _presenter && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + if (e.Pointer.Captured == _presenter && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { var point = e.GetPosition(_presenter); @@ -1058,32 +1134,45 @@ namespace Avalonia.Controls MathUtilities.Clamp(point.X, 0, Math.Max(_presenter.Bounds.Width - 1, 0)), MathUtilities.Clamp(point.Y, 0, Math.Max(_presenter.Bounds.Height - 1, 0))); - CaretIndex = SelectionEnd = _presenter.GetCaretIndex(point); + _presenter.MoveCaretToPoint(point); + + SelectionEnd = _presenter.CaretIndex; } } protected override void OnPointerReleased(PointerReleasedEventArgs e) { - if (_presenter != null && e.Pointer.Captured == _presenter) + if (_presenter == null) { - if (e.InitialPressMouseButton == MouseButton.Right) + return; + } + + if (e.Pointer.Captured != _presenter) + { + return; + } + + if (e.InitialPressMouseButton == MouseButton.Right) + { + var point = e.GetPosition(_presenter); + + _presenter.MoveCaretToPoint(point); + + var caretIndex = _presenter.CaretIndex; + + // see if mouse clicked inside current selection + // if it did not, we change the selection to where the user clicked + var firstSelection = Math.Min(SelectionStart, SelectionEnd); + var lastSelection = Math.Max(SelectionStart, SelectionEnd); + var didClickInSelection = SelectionStart != SelectionEnd && + caretIndex >= firstSelection && caretIndex <= lastSelection; + if (!didClickInSelection) { - var point = e.GetPosition(_presenter); - var caretIndex = _presenter.GetCaretIndex(point); - - // see if mouse clicked inside current selection - // if it did not, we change the selection to where the user clicked - var firstSelection = Math.Min(SelectionStart, SelectionEnd); - var lastSelection = Math.Max(SelectionStart, SelectionEnd); - var didClickInSelection = SelectionStart != SelectionEnd && - caretIndex >= firstSelection && caretIndex <= lastSelection; - if (!didClickInSelection) - { - CaretIndex = SelectionEnd = SelectionStart = caretIndex; - } + CaretIndex = SelectionEnd = SelectionStart = caretIndex; } - e.Pointer.Capture(null); } + + e.Pointer.Capture(null); } protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) @@ -1127,106 +1216,46 @@ namespace Avalonia.Controls Text = string.Empty; } - private int DeleteCharacter(int index) - { - var start = index + 1; - var text = Text; - var c = text[index]; - var result = 1; - - if (c == '\n' && index > 0 && text[index - 1] == '\r') - { - --index; - ++result; - } - else if (c == '\r' && index < text.Length - 1 && text[index + 1] == '\n') - { - ++start; - ++result; - } - - Text = text.Substring(0, index) + text.Substring(start); - - return result; - } - private void MoveHorizontal(int direction, bool wholeWord, bool isSelecting) { var text = Text ?? string.Empty; - var caretIndex = CaretIndex; + var selectionStart = SelectionStart; if (!wholeWord) { - if (SelectionStart != SelectionEnd && !isSelecting) + if (_presenter == null) { - var start = Math.Min(SelectionStart, SelectionEnd); - var end = Math.Max(SelectionStart, SelectionEnd); - CaretIndex = direction < 0 ? start : end; return; } + + _presenter.MoveCaretHorizontal(direction > 0 ? LogicalDirection.Forward : LogicalDirection.Backward); - var index = caretIndex + direction; - - if (index < 0 || index > text.Length) + if (isSelecting) { - return; - } - else if (index == text.Length) - { - CaretIndex = index; - return; - } - - var c = text[index]; - - if (direction > 0) - { - CaretIndex += (c == '\r' && index < text.Length - 1 && text[index + 1] == '\n') ? 2 : 1; + SelectionEnd = _presenter.CaretIndex; } else { - CaretIndex -= (c == '\n' && index > 0 && text[index - 1] == '\r') ? 2 : 1; + SelectionStart = SelectionEnd = _presenter.CaretIndex; } } else { if (direction > 0) { - CaretIndex += StringUtils.NextWord(text, caretIndex) - caretIndex; + var offset = StringUtils.NextWord(text, selectionStart) - selectionStart; + + CaretIndex += offset; } else { - CaretIndex += StringUtils.PreviousWord(text, caretIndex) - caretIndex; + var offset = StringUtils.PreviousWord(text, selectionStart) - selectionStart; + + CaretIndex += offset; } } } - private bool MoveVertical(int count) - { - if (_presenter is null) - { - return false; - } - - var formattedText = _presenter.FormattedText; - var lines = formattedText.GetLines().ToList(); - var caretIndex = CaretIndex; - var lineIndex = GetLine(caretIndex, lines) + count; - - if (lineIndex >= 0 && lineIndex < lines.Count) - { - var line = lines[lineIndex]; - var rect = formattedText.HitTestTextPosition(caretIndex); - var y = count < 0 ? rect.Y : rect.Bottom; - var point = new Point(rect.X, y + (count * (line.Height / 2))); - var hit = formattedText.HitTestPoint(point); - CaretIndex = hit.TextPosition + (hit.IsTrailing ? 1 : 0); - return true; - } - - return false; - } - private void MoveHome(bool document) { if (_presenter is null) @@ -1243,17 +1272,17 @@ namespace Avalonia.Controls } else { - var lines = _presenter.FormattedText.GetLines(); + var lines = _presenter.TextLayout.TextLines; var pos = 0; foreach (var line in lines) { - if (pos + line.Length > caretIndex || pos + line.Length == text.Length) + if (pos + line.TextRange.Length > caretIndex || pos + line.TextRange.Length == text.Length) { break; } - pos += line.Length; + pos += line.TextRange.Length; } caretIndex = pos; @@ -1278,12 +1307,12 @@ namespace Avalonia.Controls } else { - var lines = _presenter.FormattedText.GetLines(); + var lines = _presenter.TextLayout.TextLines; var pos = 0; foreach (var line in lines) { - pos += line.Length; + pos += line.TextRange.Length; if (pos > caretIndex) { @@ -1360,25 +1389,6 @@ namespace Avalonia.Controls return text.Substring(start, end - start); } - private int GetLine(int caretIndex, IList lines) - { - int pos = 0; - int i; - - for (i = 0; i < lines.Count - 1; ++i) - { - var line = lines[i]; - pos += line.Length; - - if (pos > caretIndex) - { - break; - } - } - - return i; - } - private void SetTextInternal(string value, bool raiseTextChanged = true) { if (raiseTextChanged) diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index 334db2cafd..279efa2934 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using Avalonia.Controls.Presenters; using Avalonia.Input; using Avalonia.Input.TextInput; @@ -10,7 +11,7 @@ namespace Avalonia.Controls { private InputElement _parent; private TextPresenter _presenter; - private IDisposable _subscription; + public Rect CursorRectangle { get @@ -20,11 +21,15 @@ namespace Avalonia.Controls return default; } var transform = _presenter.TransformToVisual(_parent); + if (transform == null) { return default; } - return _presenter.GetCursorRectangle().TransformToAABB(transform.Value); + + var rect = _presenter.GetCursorRectangle().TransformToAABB(transform.Value); + + return rect; } } @@ -40,20 +45,25 @@ namespace Avalonia.Controls public string TextBeforeCursor => null; public string TextAfterCursor => null; - private void OnCaretIndexChanged(int index) => CursorRectangleChanged?.Invoke(this, EventArgs.Empty); + private void OnCaretBoundsChanged(object sender, EventArgs e) => CursorRectangleChanged?.Invoke(this, EventArgs.Empty); public void SetPresenter(TextPresenter presenter, InputElement parent) { _parent = parent; - _subscription?.Dispose(); - _subscription = null; + + if (_presenter != null) + { + _presenter.CaretBoundsChanged -= OnCaretBoundsChanged; + } + _presenter = presenter; + if (_presenter != null) { - _subscription = _presenter.GetObservable(TextPresenter.CaretIndexProperty) - .Subscribe(OnCaretIndexChanged); + _presenter.CaretBoundsChanged += OnCaretBoundsChanged; } + TextViewVisualChanged?.Invoke(this, EventArgs.Empty); CursorRectangleChanged?.Invoke(this, EventArgs.Empty); } diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 5d9a0c8eed..9eb35e0548 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -34,7 +34,7 @@ namespace Avalonia.Controls IStyleHost, ILogicalRoot, ITextInputMethodRoot, - IWeakSubscriber + IWeakEventSubscriber { /// /// Defines the property. @@ -74,6 +74,12 @@ namespace Avalonia.Controls public static readonly StyledProperty TransparencyBackgroundFallbackProperty = AvaloniaProperty.Register(nameof(TransparencyBackgroundFallback), Brushes.White); + private static readonly WeakEvent + ResourcesChangedWeakEvent = WeakEvent.Register( + (s, h) => s.ResourcesChanged += h, + (s, h) => s.ResourcesChanged -= h + ); + private readonly IInputManager _inputManager; private readonly IAccessKeyHandler _accessKeyHandler; private readonly IKeyboardNavigationHandler _keyboardNavigationHandler; @@ -128,6 +134,8 @@ namespace Avalonia.Controls "Could not create window implementation: maybe no windowing subsystem was initialized?"); } + impl = ValidatingToplevelImpl.Wrap(impl); + PlatformImpl = impl; _actualTransparencyLevel = PlatformImpl.TransparencyLevel; @@ -178,10 +186,7 @@ namespace Avalonia.Controls if (((IStyleHost)this).StylingParent is IResourceHost applicationResources) { - WeakSubscriptionManager.Subscribe( - applicationResources, - nameof(IResourceHost.ResourcesChanged), - this); + ResourcesChangedWeakEvent.Subscribe(applicationResources, this); } impl.LostFocus += PlatformImpl_LostFocus; @@ -286,7 +291,7 @@ namespace Avalonia.Controls /// IMouseDevice IInputRoot.MouseDevice => PlatformImpl?.MouseDevice; - void IWeakSubscriber.OnEvent(object sender, ResourcesChangedEventArgs e) + void IWeakEventSubscriber.OnEvent(object sender, WeakEvent ev, ResourcesChangedEventArgs e) { ((ILogical)this).NotifyResourcesChanged(e); } @@ -364,14 +369,15 @@ namespace Avalonia.Controls Renderer?.Dispose(); Renderer = null; + (this as IInputRoot).MouseDevice?.TopLevelClosed(this); + PlatformImpl = null; + var logicalArgs = new LogicalTreeAttachmentEventArgs(this, this, null); ((ILogical)this).NotifyDetachedFromLogicalTree(logicalArgs); var visualArgs = new VisualTreeAttachmentEventArgs(this, this); OnDetachedFromVisualTreeCore(visualArgs); - - (this as IInputRoot).MouseDevice?.TopLevelClosed(this); - PlatformImpl = null; + OnClosed(EventArgs.Empty); LayoutManager?.Dispose(); diff --git a/src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs b/src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs index 1a190391b7..74705a0262 100644 --- a/src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs +++ b/src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs @@ -83,7 +83,7 @@ namespace Avalonia.Controls.Utils "Collection listener not registered for this collection/listener combination."); } - private class Entry : IWeakSubscriber, IDisposable + private class Entry : IWeakEventSubscriber, IDisposable { private INotifyCollectionChanged _collection; @@ -91,23 +91,18 @@ namespace Avalonia.Controls.Utils { _collection = collection; Listeners = new List>(); - WeakSubscriptionManager.Subscribe( - _collection, - nameof(INotifyCollectionChanged.CollectionChanged), - this); + WeakEvents.CollectionChanged.Subscribe(_collection, this); } public List> Listeners { get; } public void Dispose() { - WeakSubscriptionManager.Unsubscribe( - _collection, - nameof(INotifyCollectionChanged.CollectionChanged), - this); + WeakEvents.CollectionChanged.Unsubscribe(_collection, this); } - void IWeakSubscriber.OnEvent(object? sender, NotifyCollectionChangedEventArgs e) + void IWeakEventSubscriber. + OnEvent(object? notifyCollectionChanged, WeakEvent ev, NotifyCollectionChangedEventArgs e) { static void Notify( INotifyCollectionChanged incc, diff --git a/src/Avalonia.Controls/Utils/StringUtils.cs b/src/Avalonia.Controls/Utils/StringUtils.cs index 8cf2e836bb..53937003c8 100644 --- a/src/Avalonia.Controls/Utils/StringUtils.cs +++ b/src/Avalonia.Controls/Utils/StringUtils.cs @@ -1,4 +1,5 @@ using System.Globalization; +using Avalonia.Media.TextFormatting.Unicode; namespace Avalonia.Controls.Utils { @@ -23,26 +24,38 @@ namespace Avalonia.Controls.Utils return false; } + var codepoint = new Codepoint(text[index]); + // A 'word' starts with an AlphaNumeric or some punctuation symbols immediately // preceeded by lwsp. - if (index > 0 && !char.IsWhiteSpace(text[index - 1])) + if (index > 0) { - return false; + var previousCodepoint = new Codepoint(text[index - 1]); + + if (!previousCodepoint.IsWhiteSpace) + { + return false; + } + + if (previousCodepoint.IsBreakChar) + { + return true; + } } - switch (CharUnicodeInfo.GetUnicodeCategory(text[index])) + switch (codepoint.GeneralCategory) { - case UnicodeCategory.LowercaseLetter: - case UnicodeCategory.TitlecaseLetter: - case UnicodeCategory.UppercaseLetter: - case UnicodeCategory.DecimalDigitNumber: - case UnicodeCategory.LetterNumber: - case UnicodeCategory.OtherNumber: - case UnicodeCategory.DashPunctuation: - case UnicodeCategory.InitialQuotePunctuation: - case UnicodeCategory.OpenPunctuation: - case UnicodeCategory.CurrencySymbol: - case UnicodeCategory.MathSymbol: + case GeneralCategory.LowercaseLetter: + case GeneralCategory.TitlecaseLetter: + case GeneralCategory.UppercaseLetter: + case GeneralCategory.DecimalNumber: + case GeneralCategory.LetterNumber: + case GeneralCategory.OtherNumber: + case GeneralCategory.DashPunctuation: + case GeneralCategory.InitialPunctuation: + case GeneralCategory.OpenPunctuation: + case GeneralCategory.CurrencySymbol: + case GeneralCategory.MathSymbol: return true; // TODO: How do you do this in .NET? @@ -56,6 +69,11 @@ namespace Avalonia.Controls.Utils public static int PreviousWord(string text, int cursor) { + if (string.IsNullOrEmpty(text)) + { + return 0; + } + int begin; int i; int cr; @@ -107,7 +125,12 @@ namespace Avalonia.Controls.Utils cr = LineEnd(text, cursor); - if (cr < text.Length && text[cr] == '\r' && text[cr + 1] == '\n') + if (cursor >= text.Length) + { + return cursor; + } + + if (cr < text.Length && text[cr] == '\r' && cr + 1 < text.Length && text[cr + 1] == '\n') { lf = cr + 1; } diff --git a/src/Avalonia.Controls/ValidatingToplevel.cs b/src/Avalonia.Controls/ValidatingToplevel.cs new file mode 100644 index 0000000000..4fb9b7c99f --- /dev/null +++ b/src/Avalonia.Controls/ValidatingToplevel.cs @@ -0,0 +1,344 @@ +using System; +using System.Collections.Generic; +using Avalonia.Controls.Platform; +using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; +using Avalonia.Platform; +using Avalonia.Rendering; + +namespace Avalonia.Controls; + +internal class ValidatingToplevelImpl : ITopLevelImpl, ITopLevelImplWithNativeControlHost, + ITopLevelImplWithNativeMenuExporter, ITopLevelImplWithTextInputMethod +{ + private readonly ITopLevelImpl _impl; + private bool _disposed; + + public ValidatingToplevelImpl(ITopLevelImpl impl) + { + _impl = impl ?? throw new InvalidOperationException( + "Could not create TopLevel implementation: maybe no windowing subsystem was initialized?"); + } + + public void Dispose() + { + _disposed = true; + _impl.Dispose(); + } + + protected void CheckDisposed() + { + if (_disposed) + throw new ObjectDisposedException(_impl.GetType().FullName); + } + + protected ITopLevelImpl Inner + { + get + { + CheckDisposed(); + return _impl; + } + } + + public static ITopLevelImpl Wrap(ITopLevelImpl impl) + { +#if DEBUG + if (impl is ValidatingToplevelImpl) + return impl; + return new ValidatingToplevelImpl(impl); +#else + return impl; +#endif + } + + public Size ClientSize => Inner.ClientSize; + public Size? FrameSize => Inner.FrameSize; + public double RenderScaling => Inner.RenderScaling; + public IEnumerable Surfaces => Inner.Surfaces; + + public Action Input + { + get => Inner.Input; + set => Inner.Input = value; + } + + public Action Paint + { + get => Inner.Paint; + set => Inner.Paint = value; + } + + public Action Resized + { + get => Inner.Resized; + set => Inner.Resized = value; + } + + public Action ScalingChanged + { + get => Inner.ScalingChanged; + set => Inner.ScalingChanged = value; + } + + public Action TransparencyLevelChanged + { + get => Inner.TransparencyLevelChanged; + set => Inner.TransparencyLevelChanged = value; + } + + public IRenderer CreateRenderer(IRenderRoot root) => Inner.CreateRenderer(root); + + public void Invalidate(Rect rect) => Inner.Invalidate(rect); + + public void SetInputRoot(IInputRoot inputRoot) => Inner.SetInputRoot(inputRoot); + + public Point PointToClient(PixelPoint point) => Inner.PointToClient(point); + + public PixelPoint PointToScreen(Point point) => Inner.PointToScreen(point); + + public void SetCursor(ICursorImpl cursor) => Inner.SetCursor(cursor); + + public Action Closed + { + get => Inner.Closed; + set => Inner.Closed = value; + } + + public Action LostFocus + { + get => Inner.LostFocus; + set => Inner.LostFocus = value; + } + + // Exception: for some reason we are notifying platform mouse device from TopLevel.cs + public IMouseDevice MouseDevice => _impl.MouseDevice; + public IPopupImpl CreatePopup() => Inner.CreatePopup(); + + public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) => + Inner.SetTransparencyLevelHint(transparencyLevel); + + + public WindowTransparencyLevel TransparencyLevel => Inner.TransparencyLevel; + public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => Inner.AcrylicCompensationLevels; + public INativeControlHostImpl NativeControlHost => (Inner as ITopLevelImplWithNativeControlHost)?.NativeControlHost; + + public ITopLevelNativeMenuExporter NativeMenuExporter => + (Inner as ITopLevelImplWithNativeMenuExporter)?.NativeMenuExporter; + + public ITextInputMethodImpl TextInputMethod => (Inner as ITopLevelImplWithTextInputMethod)?.TextInputMethod; +} + +internal class ValidatingWindowBaseImpl : ValidatingToplevelImpl, IWindowBaseImpl +{ + private readonly IWindowBaseImpl _impl; + + public ValidatingWindowBaseImpl(IWindowBaseImpl impl) : base(impl) + { + _impl = impl; + } + + protected new IWindowBaseImpl Inner + { + get + { + CheckDisposed(); + return _impl; + } + } + + public static IWindowBaseImpl Wrap(IWindowBaseImpl impl) + { +#if DEBUG + if (impl is ValidatingToplevelImpl) + return impl; + return new ValidatingWindowBaseImpl(impl); +#else + return impl; +#endif + } + + public void Show(bool activate, bool isDialog) => Inner.Show(activate, isDialog); + + public void Hide() => Inner.Hide(); + + public double DesktopScaling => Inner.DesktopScaling; + public PixelPoint Position => Inner.Position; + + public Action PositionChanged + { + get => Inner.PositionChanged; + set => Inner.PositionChanged = value; + } + + public void Activate() => Inner.Activate(); + + public Action Deactivated + { + get => Inner.Deactivated; + set => Inner.Deactivated = value; + } + + public Action Activated + { + get => Inner.Activated; + set => Inner.Activated = value; + } + + public IPlatformHandle Handle => Inner.Handle; + public Size MaxAutoSizeHint => Inner.MaxAutoSizeHint; + public void SetTopmost(bool value) => Inner.SetTopmost(value); + public IScreenImpl Screen => Inner.Screen; +} + +internal class ValidatingWindowImpl : ValidatingWindowBaseImpl, IWindowImpl +{ + private readonly IWindowImpl _impl; + + public ValidatingWindowImpl(IWindowImpl impl) : base(impl) + { + _impl = impl; + } + + protected new IWindowImpl Inner + { + get + { + CheckDisposed(); + return _impl; + } + } + + public static IWindowImpl Unwrap(IWindowImpl impl) + { + if (impl is ValidatingWindowImpl v) + return v.Inner; + return impl; + } + + public static IWindowImpl Wrap(IWindowImpl impl) + { +#if DEBUG + if (impl is ValidatingToplevelImpl) + return impl; + return new ValidatingWindowImpl(impl); +#else + return impl; +#endif + } + + public WindowState WindowState + { + get => Inner.WindowState; + set => Inner.WindowState = value; + } + + public Action WindowStateChanged + { + get => Inner.WindowStateChanged; + set => Inner.WindowStateChanged = value; + } + + public void SetTitle(string title) => Inner.SetTitle(title); + + public void SetParent(IWindowImpl parent) + { + //Workaround. SetParent will cast IWindowImpl to WindowImpl but ValidatingWindowImpl isn't actual WindowImpl so it will fail with InvalidCastException. + if (parent is ValidatingWindowImpl validatingToplevelImpl) + { + Inner.SetParent(validatingToplevelImpl.Inner); + } + else + { + Inner.SetParent(parent); + } + } + + public void SetEnabled(bool enable) => Inner.SetEnabled(enable); + + public Action GotInputWhenDisabled + { + get => Inner.GotInputWhenDisabled; + set => Inner.GotInputWhenDisabled = value; + } + + public void SetSystemDecorations(SystemDecorations enabled) => Inner.SetSystemDecorations(enabled); + + public void SetIcon(IWindowIconImpl icon) => Inner.SetIcon(icon); + + public void ShowTaskbarIcon(bool value) => Inner.ShowTaskbarIcon(value); + + public void CanResize(bool value) => Inner.CanResize(value); + + public Func Closing + { + get => Inner.Closing; + set => Inner.Closing = value; + } + + public bool IsClientAreaExtendedToDecorations => Inner.IsClientAreaExtendedToDecorations; + + public Action ExtendClientAreaToDecorationsChanged + { + get => Inner.ExtendClientAreaToDecorationsChanged; + set => Inner.ExtendClientAreaToDecorationsChanged = value; + } + + public bool NeedsManagedDecorations => Inner.NeedsManagedDecorations; + public Thickness ExtendedMargins => Inner.ExtendedMargins; + public Thickness OffScreenMargin => Inner.OffScreenMargin; + public void BeginMoveDrag(PointerPressedEventArgs e) => Inner.BeginMoveDrag(e); + + public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) => Inner.BeginResizeDrag(edge, e); + + public void Resize(Size clientSize, PlatformResizeReason reason) => + Inner.Resize(clientSize, reason); + + public void Move(PixelPoint point) => Inner.Move(point); + + public void SetMinMaxSize(Size minSize, Size maxSize) => Inner.SetMinMaxSize(minSize, maxSize); + + public void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint) => + Inner.SetExtendClientAreaToDecorationsHint(extendIntoClientAreaHint); + + public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) => + Inner.SetExtendClientAreaChromeHints(hints); + + public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) => + Inner.SetExtendClientAreaTitleBarHeightHint(titleBarHeight); +} + +internal class ValidatingPopupImpl : ValidatingWindowBaseImpl, IPopupImpl +{ + private readonly IPopupImpl _impl; + + public ValidatingPopupImpl(IPopupImpl impl) : base(impl) + { + _impl = impl; + } + + protected new IPopupImpl Inner + { + get + { + CheckDisposed(); + return _impl; + } + } + + public static IPopupImpl Wrap(IPopupImpl impl) + { +#if DEBUG + if (impl is ValidatingToplevelImpl) + return impl; + return new ValidatingPopupImpl(impl); +#else + return impl; +#endif + } + + public IPopupPositioner PopupPositioner => Inner.PopupPositioner; + public void SetWindowManagerAddShadowHint(bool enabled) => Inner.SetWindowManagerAddShadowHint(enabled); +} diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 4c94b725ea..b9ca594efa 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -237,13 +237,14 @@ namespace Avalonia.Controls /// /// The window implementation. public Window(IWindowImpl impl) - : base(impl) + : base(ValidatingWindowImpl.Wrap(impl)) { - impl.Closing = HandleClosing; - impl.GotInputWhenDisabled = OnGotInputWhenDisabled; - impl.WindowStateChanged = HandleWindowStateChanged; + var wrapped = (IWindowImpl)base.PlatformImpl!; + wrapped.Closing = HandleClosing; + wrapped.GotInputWhenDisabled = OnGotInputWhenDisabled; + wrapped.WindowStateChanged = HandleWindowStateChanged; _maxPlatformClientSize = PlatformImpl?.MaxAutoSizeHint ?? default(Size); - impl.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged; + wrapped.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged; this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x, PlatformResizeReason.Application)); PlatformImpl?.ShowTaskbarIcon(ShowInTaskbar); diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 5861d0452d..2207d0550a 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -57,12 +57,13 @@ namespace Avalonia.Controls { } - public WindowBase(IWindowBaseImpl impl, IAvaloniaDependencyResolver dependencyResolver) : base(impl, dependencyResolver) + public WindowBase(IWindowBaseImpl impl, IAvaloniaDependencyResolver dependencyResolver) : base(ValidatingWindowBaseImpl.Wrap(impl), dependencyResolver) { Screens = new Screens(PlatformImpl?.Screen); - impl.Activated = HandleActivated; - impl.Deactivated = HandleDeactivated; - impl.PositionChanged = HandlePositionChanged; + var wrapped = PlatformImpl!; + wrapped.Activated = HandleActivated; + wrapped.Deactivated = HandleDeactivated; + wrapped.PositionChanged = HandlePositionChanged; } /// diff --git a/src/Avalonia.DesktopRuntime/ApiCompatBaseline.txt b/src/Avalonia.DesktopRuntime/ApiCompatBaseline.txt new file mode 100644 index 0000000000..0493db9ab3 --- /dev/null +++ b/src/Avalonia.DesktopRuntime/ApiCompatBaseline.txt @@ -0,0 +1,3 @@ +Compat issues with assembly Avalonia.DesktopRuntime: +TypesMustExist : Type 'Avalonia.Shared.PlatformSupport.AssetLoader' does not exist in the implementation but it does exist in the contract. +Total Issues: 1 diff --git a/src/Avalonia.DesktopRuntime/AppBuilder.cs b/src/Avalonia.DesktopRuntime/AppBuilder.cs index ff0d84a6e9..2946324c83 100644 --- a/src/Avalonia.DesktopRuntime/AppBuilder.cs +++ b/src/Avalonia.DesktopRuntime/AppBuilder.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Reflection; using Avalonia.Controls; using Avalonia.Platform; -using Avalonia.Shared.PlatformSupport; +using Avalonia.PlatformSupport; namespace Avalonia { diff --git a/src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj b/src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj index f2a0faeb22..25effae46e 100644 --- a/src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj +++ b/src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj @@ -8,6 +8,7 @@ + @@ -16,6 +17,5 @@ - diff --git a/src/Avalonia.DesktopRuntime/RuntimeInfo.cs b/src/Avalonia.DesktopRuntime/RuntimeInfo.cs deleted file mode 100644 index 82eaadb895..0000000000 --- a/src/Avalonia.DesktopRuntime/RuntimeInfo.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using Avalonia.Platform; - - -namespace Avalonia.Shared.PlatformSupport -{ - internal partial class StandardRuntimePlatform - { - private static readonly Lazy Info = new Lazy(() => - { - OperatingSystemType os; - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - os = OperatingSystemType.OSX; - else if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - os = OperatingSystemType.Linux; - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - os = OperatingSystemType.WinNT; - else - throw new Exception("Unknown OS platform " + RuntimeInformation.OSDescription); - - return new RuntimePlatformInfo - { -#if NETCOREAPP2_0 - IsCoreClr = true, -#elif NET461 - IsDotNetFramework = false, -#endif - IsDesktop = true, - IsMono = false, - IsMobile = false, - IsUnix = os != OperatingSystemType.WinNT, - OperatingSystem = os, - }; - }); - - - public RuntimePlatformInfo GetRuntimeInfo() => Info.Value; - } -} diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml index 45bfe5ff81..c32638f6ca 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml @@ -2,6 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:views="clr-namespace:Avalonia.Diagnostics.Views" xmlns:diag="clr-namespace:Avalonia.Diagnostics" + xmlns:default="using:Avalonia.Themes.Default" Title="Avalonia DevTools" x:Class="Avalonia.Diagnostics.Views.MainWindow"> @@ -9,8 +10,7 @@ - - + diff --git a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml index 5f18bac44a..1843abebfd 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml @@ -1,13 +1,9 @@ - diff --git a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml index 30c6d39856..6247815303 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml @@ -1,82 +1,38 @@ - diff --git a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj index 678f75b43f..ef200b5532 100644 --- a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj +++ b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj @@ -13,10 +13,7 @@ - - - - + diff --git a/src/Avalonia.Themes.Default/AutoCompleteBox.xaml b/src/Avalonia.Themes.Default/Controls/AutoCompleteBox.xaml similarity index 100% rename from src/Avalonia.Themes.Default/AutoCompleteBox.xaml rename to src/Avalonia.Themes.Default/Controls/AutoCompleteBox.xaml diff --git a/src/Avalonia.Themes.Default/Button.xaml b/src/Avalonia.Themes.Default/Controls/Button.xaml similarity index 100% rename from src/Avalonia.Themes.Default/Button.xaml rename to src/Avalonia.Themes.Default/Controls/Button.xaml diff --git a/src/Avalonia.Themes.Default/ButtonSpinner.xaml b/src/Avalonia.Themes.Default/Controls/ButtonSpinner.xaml similarity index 100% rename from src/Avalonia.Themes.Default/ButtonSpinner.xaml rename to src/Avalonia.Themes.Default/Controls/ButtonSpinner.xaml diff --git a/src/Avalonia.Themes.Default/Calendar.xaml b/src/Avalonia.Themes.Default/Controls/Calendar.xaml similarity index 100% rename from src/Avalonia.Themes.Default/Calendar.xaml rename to src/Avalonia.Themes.Default/Controls/Calendar.xaml diff --git a/src/Avalonia.Themes.Default/CalendarButton.xaml b/src/Avalonia.Themes.Default/Controls/CalendarButton.xaml similarity index 100% rename from src/Avalonia.Themes.Default/CalendarButton.xaml rename to src/Avalonia.Themes.Default/Controls/CalendarButton.xaml diff --git a/src/Avalonia.Themes.Default/CalendarDatePicker.xaml b/src/Avalonia.Themes.Default/Controls/CalendarDatePicker.xaml similarity index 100% rename from src/Avalonia.Themes.Default/CalendarDatePicker.xaml rename to src/Avalonia.Themes.Default/Controls/CalendarDatePicker.xaml diff --git a/src/Avalonia.Themes.Default/CalendarDayButton.xaml b/src/Avalonia.Themes.Default/Controls/CalendarDayButton.xaml similarity index 100% rename from src/Avalonia.Themes.Default/CalendarDayButton.xaml rename to src/Avalonia.Themes.Default/Controls/CalendarDayButton.xaml diff --git a/src/Avalonia.Themes.Default/CalendarItem.xaml b/src/Avalonia.Themes.Default/Controls/CalendarItem.xaml similarity index 100% rename from src/Avalonia.Themes.Default/CalendarItem.xaml rename to src/Avalonia.Themes.Default/Controls/CalendarItem.xaml diff --git a/src/Avalonia.Themes.Default/CaptionButtons.xaml b/src/Avalonia.Themes.Default/Controls/CaptionButtons.xaml similarity index 100% rename from src/Avalonia.Themes.Default/CaptionButtons.xaml rename to src/Avalonia.Themes.Default/Controls/CaptionButtons.xaml diff --git a/src/Avalonia.Themes.Default/Carousel.xaml b/src/Avalonia.Themes.Default/Controls/Carousel.xaml similarity index 100% rename from src/Avalonia.Themes.Default/Carousel.xaml rename to src/Avalonia.Themes.Default/Controls/Carousel.xaml diff --git a/src/Avalonia.Themes.Default/CheckBox.xaml b/src/Avalonia.Themes.Default/Controls/CheckBox.xaml similarity index 100% rename from src/Avalonia.Themes.Default/CheckBox.xaml rename to src/Avalonia.Themes.Default/Controls/CheckBox.xaml diff --git a/src/Avalonia.Themes.Default/ComboBox.xaml b/src/Avalonia.Themes.Default/Controls/ComboBox.xaml similarity index 100% rename from src/Avalonia.Themes.Default/ComboBox.xaml rename to src/Avalonia.Themes.Default/Controls/ComboBox.xaml diff --git a/src/Avalonia.Themes.Default/ComboBoxItem.xaml b/src/Avalonia.Themes.Default/Controls/ComboBoxItem.xaml similarity index 100% rename from src/Avalonia.Themes.Default/ComboBoxItem.xaml rename to src/Avalonia.Themes.Default/Controls/ComboBoxItem.xaml diff --git a/src/Avalonia.Themes.Default/ContentControl.xaml b/src/Avalonia.Themes.Default/Controls/ContentControl.xaml similarity index 100% rename from src/Avalonia.Themes.Default/ContentControl.xaml rename to src/Avalonia.Themes.Default/Controls/ContentControl.xaml diff --git a/src/Avalonia.Themes.Default/ContextMenu.xaml b/src/Avalonia.Themes.Default/Controls/ContextMenu.xaml similarity index 100% rename from src/Avalonia.Themes.Default/ContextMenu.xaml rename to src/Avalonia.Themes.Default/Controls/ContextMenu.xaml diff --git a/src/Avalonia.Themes.Default/DataValidationErrors.xaml b/src/Avalonia.Themes.Default/Controls/DataValidationErrors.xaml similarity index 100% rename from src/Avalonia.Themes.Default/DataValidationErrors.xaml rename to src/Avalonia.Themes.Default/Controls/DataValidationErrors.xaml diff --git a/src/Avalonia.Themes.Default/DatePicker.xaml b/src/Avalonia.Themes.Default/Controls/DatePicker.xaml similarity index 100% rename from src/Avalonia.Themes.Default/DatePicker.xaml rename to src/Avalonia.Themes.Default/Controls/DatePicker.xaml diff --git a/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml b/src/Avalonia.Themes.Default/Controls/EmbeddableControlRoot.xaml similarity index 100% rename from src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml rename to src/Avalonia.Themes.Default/Controls/EmbeddableControlRoot.xaml diff --git a/src/Avalonia.Themes.Default/Expander.xaml b/src/Avalonia.Themes.Default/Controls/Expander.xaml similarity index 100% rename from src/Avalonia.Themes.Default/Expander.xaml rename to src/Avalonia.Themes.Default/Controls/Expander.xaml diff --git a/src/Avalonia.Themes.Default/FlyoutPresenter.xaml b/src/Avalonia.Themes.Default/Controls/FlyoutPresenter.xaml similarity index 100% rename from src/Avalonia.Themes.Default/FlyoutPresenter.xaml rename to src/Avalonia.Themes.Default/Controls/FlyoutPresenter.xaml diff --git a/src/Avalonia.Themes.Default/FocusAdorner.xaml b/src/Avalonia.Themes.Default/Controls/FocusAdorner.xaml similarity index 100% rename from src/Avalonia.Themes.Default/FocusAdorner.xaml rename to src/Avalonia.Themes.Default/Controls/FocusAdorner.xaml diff --git a/src/Avalonia.Themes.Default/GridSplitter.xaml b/src/Avalonia.Themes.Default/Controls/GridSplitter.xaml similarity index 100% rename from src/Avalonia.Themes.Default/GridSplitter.xaml rename to src/Avalonia.Themes.Default/Controls/GridSplitter.xaml diff --git a/src/Avalonia.Themes.Default/ItemsControl.xaml b/src/Avalonia.Themes.Default/Controls/ItemsControl.xaml similarity index 100% rename from src/Avalonia.Themes.Default/ItemsControl.xaml rename to src/Avalonia.Themes.Default/Controls/ItemsControl.xaml diff --git a/src/Avalonia.Themes.Default/Label.xaml b/src/Avalonia.Themes.Default/Controls/Label.xaml similarity index 100% rename from src/Avalonia.Themes.Default/Label.xaml rename to src/Avalonia.Themes.Default/Controls/Label.xaml diff --git a/src/Avalonia.Themes.Default/ListBox.xaml b/src/Avalonia.Themes.Default/Controls/ListBox.xaml similarity index 91% rename from src/Avalonia.Themes.Default/ListBox.xaml rename to src/Avalonia.Themes.Default/Controls/ListBox.xaml index e3417aa086..b1fcb830b3 100644 --- a/src/Avalonia.Themes.Default/ListBox.xaml +++ b/src/Avalonia.Themes.Default/Controls/ListBox.xaml @@ -6,6 +6,7 @@ + + Viewport="{TemplateBinding Viewport, Mode=TwoWay}" + IsScrollChainingEnabled="{TemplateBinding IsScrollChainingEnabled}"> + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml.cs b/src/Avalonia.Themes.Default/DefaultTheme.xaml.cs index effdb88537..598b418977 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml.cs +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml.cs @@ -1,4 +1,3 @@ -using Avalonia.Markup.Xaml; using Avalonia.Styling; namespace Avalonia.Themes.Default diff --git a/src/Avalonia.Themes.Default/SimpleTheme.cs b/src/Avalonia.Themes.Default/SimpleTheme.cs new file mode 100644 index 0000000000..1d9f2d5f9d --- /dev/null +++ b/src/Avalonia.Themes.Default/SimpleTheme.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Markup.Xaml.Styling; +using Avalonia.Styling; +#nullable enable + +namespace Avalonia.Themes.Default +{ + public class SimpleTheme : AvaloniaObject, IStyle, IResourceProvider + { + public static readonly StyledProperty ModeProperty = + AvaloniaProperty.Register(nameof(Mode)); + + private readonly Uri _baseUri; + private bool _isLoading; + private IStyle? _loaded; + private Styles _sharedStyles = new(); + private Styles _simpleDark = new(); + private Styles _simpleLight = new(); + /// + /// Initializes a new instance of the class. + /// + /// The base URL for the XAML context. + public SimpleTheme(Uri baseUri) + { + _baseUri = baseUri; + InitStyles(_baseUri); + } + + /// + /// Initializes a new instance of the class. + /// + /// The XAML service provider. + public SimpleTheme(IServiceProvider serviceProvider) + { + var service = serviceProvider.GetService(typeof(IUriContext)); + if (service == null) + { + throw new Exception("There is no service object of type IUriContext!"); + } + _baseUri = ((IUriContext)service).BaseUri; + InitStyles(_baseUri); + } + + public event EventHandler OwnerChanged + { + add + { + if (Loaded is IResourceProvider rp) + { + rp.OwnerChanged += value; + } + } + remove + { + if (Loaded is IResourceProvider rp) + { + rp.OwnerChanged -= value; + } + } + } + + IReadOnlyList IStyle.Children => _loaded?.Children ?? Array.Empty(); + + bool IResourceNode.HasResources => (Loaded as IResourceProvider)?.HasResources ?? false; + + public IStyle Loaded + { + get + { + if (_loaded == null) + { + _isLoading = true; + + if (Mode == SimpleThemeMode.Light) + { + _loaded = new Styles { _sharedStyles, _simpleLight }; + } + else if (Mode == SimpleThemeMode.Dark) + { + _loaded = new Styles { _sharedStyles, _simpleDark }; + } + _isLoading = false; + } + + return _loaded!; + } + } + + /// + /// Gets or sets the mode of the fluent theme (light, dark). + /// + public SimpleThemeMode Mode + { + get => GetValue(ModeProperty); + set => SetValue(ModeProperty, value); + } + public IResourceHost? Owner => (Loaded as IResourceProvider)?.Owner; + + void IResourceProvider.AddOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.AddOwner(owner); + + void IResourceProvider.RemoveOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.RemoveOwner(owner); + + public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) => Loaded.TryAttach(target, host); + + public bool TryGetResource(object key, out object? value) + { + if (!_isLoading && Loaded is IResourceProvider p) + { + return p.TryGetResource(key, out value); + } + + value = null; + return false; + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == ModeProperty) + { + if (Mode == SimpleThemeMode.Dark) + { + (Loaded as Styles)![1] = _simpleDark[0]; + } + else + { + (Loaded as Styles)![1] = _simpleLight[0]; + } + } + } + + private void InitStyles(Uri baseUri) + { + _sharedStyles = new Styles + { + new StyleInclude(baseUri) + { + Source = new Uri("avares://Avalonia.Themes.Default/DefaultTheme.xaml") + }, + new StyleInclude(baseUri) + { + Source = new Uri("avares://Avalonia.Themes.Default/Accents/Base.xaml") + } + }; + _simpleLight = new Styles + { + new StyleInclude(baseUri) + { + Source = new Uri("avares://Avalonia.Themes.Default/Accents/BaseLight.xaml") + } + }; + + _simpleDark = new Styles + { + new StyleInclude(baseUri) + { + Source = new Uri("avares://Avalonia.Themes.Default/Accents/BaseDark.xaml") + } + }; + } + + } +} diff --git a/src/Avalonia.Themes.Default/SimpleThemeMode.cs b/src/Avalonia.Themes.Default/SimpleThemeMode.cs new file mode 100644 index 0000000000..be33466327 --- /dev/null +++ b/src/Avalonia.Themes.Default/SimpleThemeMode.cs @@ -0,0 +1,8 @@ +namespace Avalonia.Themes.Default +{ + public enum SimpleThemeMode + { + Light, + Dark + } +} diff --git a/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml b/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml index f2344ab380..836cc27db3 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml @@ -1,7 +1,8 @@ + xmlns:converters="clr-namespace:Avalonia.Controls.Converters;assembly=Avalonia.Controls" + x:CompileBindings="True"> diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml index ca538e4b0a..3a6af60983 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml @@ -11,7 +11,7 @@ - + diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml index 6c4e94caf1..ffd3972b66 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml @@ -7,7 +7,8 @@ + xmlns:sys="clr-namespace:System;assembly=netstandard" + x:CompileBindings="True"> + @@ -107,7 +109,6 @@ Padding="{TemplateBinding Padding}" Watermark="{TemplateBinding Watermark}" UseFloatingWatermark="{TemplateBinding UseFloatingWatermark}" - DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" Grid.Column="0"/> @@ -136,8 +137,12 @@ DisplayDateEnd="{TemplateBinding DisplayDateEnd}" /> + + diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml index ff72044c93..9223c0dd71 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml @@ -11,7 +11,7 @@ - + diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml index 5bf3ac11af..a9c8281cf0 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml @@ -6,7 +6,9 @@ --> + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + x:CompileBindings="True" + x:DataType="CalendarItem"> @@ -32,6 +34,7 @@ - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml index 72c25cea37..831537f578 100644 --- a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml @@ -1,7 +1,9 @@ + xmlns:sys="clr-namespace:System;assembly=netstandard" + x:DataType="MenuItem" + x:CompileBindings="True"> diff --git a/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml b/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml index 799fe6ffe4..243095c004 100644 --- a/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml @@ -1,6 +1,7 @@ diff --git a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml index 3320fc9a41..9aa73fc52e 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml @@ -7,7 +7,8 @@ + xmlns:sys="clr-namespace:System;assembly=netstandard" + x:CompileBindings="True"> 40 1 diff --git a/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml b/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml index debdfb2772..2d18be91cb 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml @@ -1,6 +1,7 @@ + xmlns:sys="clr-namespace:System;assembly=netstandard" + x:CompileBindings="True"> + + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + x:DataType="WindowNotificationManager" + x:CompileBindings="True">