diff --git a/.ncrunch/BindingDemo.v3.ncrunchproject b/.ncrunch/BindingDemo.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/BindingDemo.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/RenderDemo.v3.ncrunchproject b/.ncrunch/RenderDemo.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/RenderDemo.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/VirtualizationDemo.v3.ncrunchproject b/.ncrunch/VirtualizationDemo.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/VirtualizationDemo.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/azure-pipelines.yml b/azure-pipelines.yml index accad63faa..b70e0bf77f 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -14,7 +14,7 @@ jobs: displayName: 'Install Nuke' inputs: script: | - dotnet tool install --global Nuke.GlobalTool --version 0.12.3 + dotnet tool install --global Nuke.GlobalTool --version 0.24.0 - task: CmdLine@2 displayName: 'Run Nuke' inputs: @@ -35,16 +35,16 @@ jobs: vmImage: 'macOS-10.14' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.0.x' + displayName: 'Use .NET Core SDK 3.1.101' inputs: packageType: sdk - version: 3.0.x + version: 3.1.101 - task: UseDotNet@2 - displayName: 'Use .NET Core Runtime 2.1.x' + displayName: 'Use .NET Core Runtime 3.1.1' inputs: packageType: runtime - version: 2.1.x + version: 3.1.1 - task: CmdLine@2 displayName: 'Install Mono 5.18' @@ -74,7 +74,7 @@ jobs: displayName: 'Install Nuke' inputs: script: | - dotnet tool install --global Nuke.GlobalTool --version 0.12.3 + dotnet tool install --global Nuke.GlobalTool --version 0.24.0 - task: CmdLine@2 displayName: 'Run Nuke' @@ -116,7 +116,7 @@ jobs: displayName: 'Install Nuke' inputs: script: | - dotnet tool install --global Nuke.GlobalTool --version 0.12.3 + dotnet tool install --global Nuke.GlobalTool --version 0.24.0 - task: CmdLine@2 displayName: 'Run Nuke' diff --git a/global.json b/global.json index 1e599211d4..a3fdca9bed 100644 --- a/global.json +++ b/global.json @@ -1,4 +1,7 @@ { + "sdk": { + "version": "3.1.101" + }, "msbuild-sdks": { "Microsoft.Build.Traversal": "1.0.43", "MSBuild.Sdk.Extras": "2.0.46", diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 4a960d47a1..ee57f54e59 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -25,6 +25,12 @@ struct IAvnGlSurfaceRenderingSession; struct IAvnAppMenu; struct IAvnAppMenuItem; +enum SystemDecorations { + SystemDecorationsNone = 0, + SystemDecorationsBorderOnly = 1, + SystemDecorationsFull = 2, +}; + struct AvnSize { double Width, Height; @@ -236,7 +242,7 @@ AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase { virtual HRESULT ShowDialog (IAvnWindow* parent) = 0; virtual HRESULT SetCanResize(bool value) = 0; - virtual HRESULT SetHasDecorations(bool value) = 0; + virtual HRESULT SetHasDecorations(SystemDecorations value) = 0; virtual HRESULT SetTitle (void* utf8Title) = 0; virtual HRESULT SetTitleBarColor (AvnColor color) = 0; virtual HRESULT SetWindowState(AvnWindowState state) = 0; diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index b6ce172ffa..2c03407732 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -115,7 +115,6 @@ public: [NSApp activateIgnoringOtherApps:YES]; [Window setTitle:_lastTitle]; - [Window setTitleVisibility:NSWindowTitleVisible]; return S_OK; } @@ -411,7 +410,7 @@ class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, pub { private: bool _canResize = true; - bool _hasDecorations = true; + SystemDecorations _hasDecorations = SystemDecorationsFull; CGRect _lastUndecoratedFrame; AvnWindowState _lastWindowState; @@ -476,23 +475,26 @@ private: bool IsZoomed () { - return _hasDecorations ? [Window isZoomed] : UndecoratedIsMaximized(); + return _hasDecorations != SystemDecorationsNone ? [Window isZoomed] : UndecoratedIsMaximized(); } void DoZoom() { - if (_hasDecorations) + switch (_hasDecorations) { - [Window performZoom:Window]; - } - else - { - if (!UndecoratedIsMaximized()) - { - _lastUndecoratedFrame = [Window frame]; - } - - [Window zoom:Window]; + case SystemDecorationsNone: + if (!UndecoratedIsMaximized()) + { + _lastUndecoratedFrame = [Window frame]; + } + + [Window zoom:Window]; + break; + + case SystemDecorationsBorderOnly: + case SystemDecorationsFull: + [Window performZoom:Window]; + break; } } @@ -506,13 +508,35 @@ private: } } - virtual HRESULT SetHasDecorations(bool value) override + virtual HRESULT SetHasDecorations(SystemDecorations value) override { @autoreleasepool { _hasDecorations = value; UpdateStyle(); - + + switch (_hasDecorations) + { + case SystemDecorationsNone: + [Window setHasShadow:NO]; + [Window setTitleVisibility:NSWindowTitleHidden]; + [Window setTitlebarAppearsTransparent:YES]; + break; + + case SystemDecorationsBorderOnly: + [Window setHasShadow:YES]; + [Window setTitleVisibility:NSWindowTitleHidden]; + [Window setTitlebarAppearsTransparent:YES]; + break; + + case SystemDecorationsFull: + [Window setHasShadow:YES]; + [Window setTitleVisibility:NSWindowTitleVisible]; + [Window setTitlebarAppearsTransparent:NO]; + [Window setTitle:_lastTitle]; + break; + } + return S_OK; } } @@ -523,7 +547,6 @@ private: { _lastTitle = [NSString stringWithUTF8String:(const char*)utf8title]; [Window setTitle:_lastTitle]; - [Window setTitleVisibility:NSWindowTitleVisible]; return S_OK; } @@ -645,10 +668,26 @@ protected: virtual NSWindowStyleMask GetStyle() override { unsigned long s = NSWindowStyleMaskBorderless; - if(_hasDecorations) - s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable; - if(_canResize) - s = s | NSWindowStyleMaskResizable; + + switch (_hasDecorations) + { + case SystemDecorationsNone: + break; + + case SystemDecorationsBorderOnly: + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView; + break; + + case SystemDecorationsFull: + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskBorderless; + if(_canResize) + { + s = s | NSWindowStyleMaskResizable; + } + + break; + } + return s; } }; diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 7b3b8465ce..358846a14e 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -13,6 +13,7 @@ using Nuke.Common.Tooling; using Nuke.Common.Tools.DotNet; using Nuke.Common.Tools.MSBuild; using Nuke.Common.Utilities; +using Nuke.Common.Utilities.Collections; using static Nuke.Common.EnvironmentInfo; using static Nuke.Common.IO.FileSystemTasks; using static Nuke.Common.IO.PathConstruction; @@ -26,11 +27,13 @@ using static Nuke.Common.Tools.VSWhere.VSWhereTasks; running and debugging a particular target (optionally without deps) would be way easier ReSharper/Rider - https://plugins.jetbrains.com/plugin/10803-nuke-support VSCode - https://marketplace.visualstudio.com/items?itemName=nuke.support - + */ partial class Build : NukeBuild { + [Solution("Avalonia.sln")] readonly Solution Solution; + static Lazy MsBuildExe = new Lazy(() => { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -54,7 +57,7 @@ partial class Build : NukeBuild protected override void OnBuildInitialized() { Parameters = new BuildParameters(this); - Information("Building version {0} of Avalonia ({1}) using version {2} of Nuke.", + Information("Building version {0} of Avalonia ({1}) using version {2} of Nuke.", Parameters.Version, Parameters.Configuration, typeof(NukeBuild).Assembly.GetName().Version.ToString()); @@ -93,29 +96,24 @@ partial class Build : NukeBuild string projectFile, Configure configurator = null) { - return MSBuild(projectFile, c => - { + return MSBuild(c => c + .SetProjectFile(projectFile) // This is required for VS2019 image on Azure Pipelines - if (Parameters.IsRunningOnWindows && Parameters.IsRunningOnAzure) - { - var javaSdk = Environment.GetEnvironmentVariable("JAVA_HOME_8_X64"); - if (javaSdk != null) - c = c.AddProperty("JavaSdkDirectory", javaSdk); - } - - c = c.AddProperty("PackageVersion", Parameters.Version) - .AddProperty("iOSRoslynPathHackRequired", "true") - .SetToolPath(MsBuildExe.Value) - .SetConfiguration(Parameters.Configuration) - .SetVerbosity(MSBuildVerbosity.Minimal); - c = configurator?.Invoke(c) ?? c; - return c; - }); + .When(Parameters.IsRunningOnWindows && + Parameters.IsRunningOnAzure, c => c + .AddProperty("JavaSdkDirectory", GetVariable("JAVA_HOME_8_X64"))) + .AddProperty("PackageVersion", Parameters.Version) + .AddProperty("iOSRoslynPathHackRequired", true) + .SetToolPath(MsBuildExe.Value) + .SetConfiguration(Parameters.Configuration) + .SetVerbosity(MSBuildVerbosity.Minimal) + .Apply(configurator)); } + Target Clean => _ => _.Executes(() => { - DeleteDirectories(Parameters.BuildDirs); - EnsureCleanDirectories(Parameters.BuildDirs); + Parameters.BuildDirs.ForEach(DeleteDirectory); + Parameters.BuildDirs.ForEach(EnsureCleanDirectory); EnsureCleanDirectory(Parameters.ArtifactsDir); EnsureCleanDirectory(Parameters.NugetIntermediateRoot); EnsureCleanDirectory(Parameters.NugetRoot); @@ -134,97 +132,84 @@ partial class Build : NukeBuild ); else - DotNetBuild(Parameters.MSBuildSolution, c => c + DotNetBuild(c => c + .SetProjectFile(Parameters.MSBuildSolution) .AddProperty("PackageVersion", Parameters.Version) .SetConfiguration(Parameters.Configuration) ); }); - - void RunCoreTest(string project) + + void RunCoreTest(string projectName) { - if(!project.EndsWith(".csproj")) - project = System.IO.Path.Combine(project, System.IO.Path.GetFileName(project)+".csproj"); - Information("Running tests from " + project); - XDocument xdoc; - using (var s = File.OpenRead(project)) - xdoc = XDocument.Load(s); - - List frameworks = null; - var targets = xdoc.Root.Descendants("TargetFrameworks").FirstOrDefault(); - if (targets != null) - frameworks = targets.Value.Split(';').Where(f => !string.IsNullOrWhiteSpace(f)).ToList(); - else - frameworks = new List {xdoc.Root.Descendants("TargetFramework").First().Value}; - - foreach(var fw in frameworks) + Information($"Running tests from {projectName}"); + var project = Solution.GetProject(projectName).NotNull("project != null"); + + foreach (var fw in project.GetTargetFrameworks()) { if (fw.StartsWith("net4") - && RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + && RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && Environment.GetEnvironmentVariable("FORCE_LINUX_TESTS") != "1") { - Information($"Skipping {fw} tests on Linux - https://github.com/mono/mono/issues/13969"); + Information($"Skipping {projectName} ({fw}) tests on Linux - https://github.com/mono/mono/issues/13969"); continue; } - Information("Running for " + fw); - DotNetTest(c => - { - c = c - .SetProjectFile(project) - .SetConfiguration(Parameters.Configuration) - .SetFramework(fw) - .EnableNoBuild() - .EnableNoRestore(); - // NOTE: I can see that we could maybe add another extension method "Switch" or "If" to make this more convenient - if (Parameters.PublishTestResults) - c = c.SetLogger("trx").SetResultsDirectory(Parameters.TestResultsRoot); - return c; - }); + Information($"Running for {projectName} ({fw}) ..."); + + DotNetTest(c => c + .SetProjectFile(project) + .SetConfiguration(Parameters.Configuration) + .SetFramework(fw) + .EnableNoBuild() + .EnableNoRestore() + .When(Parameters.PublishTestResults, c => c + .SetLogger("trx") + .SetResultsDirectory(Parameters.TestResultsRoot))); } } Target RunCoreLibsTests => _ => _ - .OnlyWhen(() => !Parameters.SkipTests) + .OnlyWhenStatic(() => !Parameters.SkipTests) .DependsOn(Compile) .Executes(() => { - RunCoreTest("./tests/Avalonia.Animation.UnitTests"); - RunCoreTest("./tests/Avalonia.Base.UnitTests"); - RunCoreTest("./tests/Avalonia.Controls.UnitTests"); - RunCoreTest("./tests/Avalonia.Controls.DataGrid.UnitTests"); - RunCoreTest("./tests/Avalonia.Input.UnitTests"); - RunCoreTest("./tests/Avalonia.Interactivity.UnitTests"); - RunCoreTest("./tests/Avalonia.Layout.UnitTests"); - RunCoreTest("./tests/Avalonia.Markup.UnitTests"); - RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests"); - RunCoreTest("./tests/Avalonia.Styling.UnitTests"); - RunCoreTest("./tests/Avalonia.Visuals.UnitTests"); - RunCoreTest("./tests/Avalonia.Skia.UnitTests"); - RunCoreTest("./tests/Avalonia.ReactiveUI.UnitTests"); + RunCoreTest("Avalonia.Animation.UnitTests"); + RunCoreTest("Avalonia.Base.UnitTests"); + RunCoreTest("Avalonia.Controls.UnitTests"); + RunCoreTest("Avalonia.Controls.DataGrid.UnitTests"); + RunCoreTest("Avalonia.Input.UnitTests"); + RunCoreTest("Avalonia.Interactivity.UnitTests"); + RunCoreTest("Avalonia.Layout.UnitTests"); + RunCoreTest("Avalonia.Markup.UnitTests"); + RunCoreTest("Avalonia.Markup.Xaml.UnitTests"); + RunCoreTest("Avalonia.Styling.UnitTests"); + RunCoreTest("Avalonia.Visuals.UnitTests"); + RunCoreTest("Avalonia.Skia.UnitTests"); + RunCoreTest("Avalonia.ReactiveUI.UnitTests"); }); Target RunRenderTests => _ => _ - .OnlyWhen(() => !Parameters.SkipTests) + .OnlyWhenStatic(() => !Parameters.SkipTests) .DependsOn(Compile) .Executes(() => { - RunCoreTest("./tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj"); + RunCoreTest("Avalonia.Skia.RenderTests"); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - RunCoreTest("./tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj"); + RunCoreTest("Avalonia.Direct2D1.RenderTests"); }); - + Target RunDesignerTests => _ => _ - .OnlyWhen(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows) + .OnlyWhenStatic(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows) .DependsOn(Compile) .Executes(() => { - RunCoreTest("./tests/Avalonia.DesignerSupport.Tests"); + RunCoreTest("Avalonia.DesignerSupport.Tests"); }); [PackageExecutable("JetBrains.dotMemoryUnit", "dotMemoryUnit.exe")] readonly Tool DotMemoryUnit; Target RunLeakTests => _ => _ - .OnlyWhen(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows) + .OnlyWhenStatic(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows) .DependsOn(Compile) .Executes(() => { @@ -235,7 +220,7 @@ partial class Build : NukeBuild }); Target ZipFiles => _ => _ - .After(CreateNugetPackages, Compile, RunCoreLibsTests, Package) + .After(CreateNugetPackages, Compile, RunCoreLibsTests, Package) .Executes(() => { var data = Parameters; @@ -259,9 +244,10 @@ partial class Build : NukeBuild MsBuildCommon(Parameters.MSBuildSolution, c => c .AddTargets("Pack")); else - DotNetPack(Parameters.MSBuildSolution, c => - c.SetConfiguration(Parameters.Configuration) - .AddProperty("PackageVersion", Parameters.Version)); + DotNetPack(c => c + .SetProject(Parameters.MSBuildSolution) + .SetConfiguration(Parameters.Configuration) + .AddProperty("PackageVersion", Parameters.Version)); }); Target CreateNugetPackages => _ => _ @@ -274,32 +260,40 @@ partial class Build : NukeBuild new NumergeNukeLogger())) throw new Exception("Package merge failed"); }); - + Target RunTests => _ => _ .DependsOn(RunCoreLibsTests) .DependsOn(RunRenderTests) .DependsOn(RunDesignerTests) .DependsOn(RunLeakTests); - + Target Package => _ => _ .DependsOn(RunTests) .DependsOn(CreateNugetPackages); - + Target CiAzureLinux => _ => _ .DependsOn(RunTests); - + Target CiAzureOSX => _ => _ .DependsOn(Package) .DependsOn(ZipFiles); - + Target CiAzureWindows => _ => _ .DependsOn(Package) .DependsOn(ZipFiles); - + public static int Main() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Execute(x => x.Package) : Execute(x => x.RunTests); } + +public static class ToolSettingsExtensions +{ + public static T Apply(this T settings, Configure configurator) + { + return configurator != null ? configurator(settings) : settings; + } +} diff --git a/nukebuild/BuildParameters.cs b/nukebuild/BuildParameters.cs index 65ba5e9756..149716b416 100644 --- a/nukebuild/BuildParameters.cs +++ b/nukebuild/BuildParameters.cs @@ -4,24 +4,21 @@ using System.Linq; using System.Runtime.InteropServices; using System.Xml.Linq; using Nuke.Common; -using Nuke.Common.BuildServers; -using Nuke.Common.Execution; +using Nuke.Common.CI.AzurePipelines; using Nuke.Common.IO; -using static Nuke.Common.IO.FileSystemTasks; using static Nuke.Common.IO.PathConstruction; -using static Nuke.Common.Tools.MSBuild.MSBuildTasks; public partial class Build { [Parameter("configuration")] public string Configuration { get; set; } - + [Parameter("skip-tests")] public bool SkipTests { get; set; } - + [Parameter("force-nuget-version")] public string ForceNugetVersion { get; set; } - + public class BuildParameters { public string Configuration { get; } @@ -79,15 +76,15 @@ public partial class Build IsRunningOnUnix = Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX; IsRunningOnWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - IsRunningOnAzure = Host == HostType.TeamServices || + IsRunningOnAzure = Host == HostType.AzurePipelines || Environment.GetEnvironmentVariable("LOGNAME") == "vsts"; if (IsRunningOnAzure) { - RepositoryName = TeamServices.Instance.RepositoryUri; - RepositoryBranch = TeamServices.Instance.SourceBranch; - IsPullRequest = TeamServices.Instance.PullRequestId.HasValue; - IsMainRepo = StringComparer.OrdinalIgnoreCase.Equals(MainRepo, TeamServices.Instance.RepositoryUri); + RepositoryName = AzurePipelines.Instance.RepositoryUri; + RepositoryBranch = AzurePipelines.Instance.SourceBranch; + IsPullRequest = AzurePipelines.Instance.PullRequestId.HasValue; + IsMainRepo = StringComparer.OrdinalIgnoreCase.Equals(MainRepo, AzurePipelines.Instance.RepositoryUri); } IsMainRepo = StringComparer.OrdinalIgnoreCase.Equals(MainRepo, diff --git a/nukebuild/Shims.cs b/nukebuild/Shims.cs index 461d617643..1ac14bf622 100644 --- a/nukebuild/Shims.cs +++ b/nukebuild/Shims.cs @@ -19,9 +19,9 @@ public partial class Build Logger.Info(info, args); } - private void Zip(PathConstruction.AbsolutePath target, params string[] paths) => Zip(target, paths.AsEnumerable()); + private void Zip(AbsolutePath target, params string[] paths) => Zip(target, paths.AsEnumerable()); - private void Zip(PathConstruction.AbsolutePath target, IEnumerable paths) + private void Zip(AbsolutePath target, IEnumerable paths) { var targetPath = target.ToString(); bool finished = false, atLeastOneFileAdded = false; @@ -38,7 +38,7 @@ public partial class Build fileStream.CopyTo(entryStream); atLeastOneFileAdded = true; } - + foreach (var path in paths) { if (Directory.Exists(path)) @@ -64,7 +64,7 @@ public partial class Build finished = true; } - finally + finally { try { diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index 2a736e4653..584c36d033 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.0 + netcoreapp3.1 false False @@ -10,7 +10,7 @@ - + @@ -20,11 +20,11 @@ - + - + diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index 2c5a09bee7..9f7593b702 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -1,6 +1,6 @@  - netstandard2.0;net461;netcoreapp2.0 + netstandard2.0;net461;netcoreapp3.1 Avalonia @@ -20,8 +20,8 @@ Platform=$(Platform)" /> - <_PackageFiles Include="$(DesignerHostAppPath)/Avalonia.Designer.HostApp/bin/$(Configuration)/netcoreapp2.0/Avalonia.Designer.HostApp.dll"> - tools/netcoreapp2.0/designer + <_PackageFiles Include="$(DesignerHostAppPath)/Avalonia.Designer.HostApp/bin/$(Configuration)/netcoreapp3.1/Avalonia.Designer.HostApp.dll"> + tools/netcoreapp3.1/designer false None diff --git a/packages/Avalonia/Avalonia.props b/packages/Avalonia/Avalonia.props index 6f21971d3d..99d53c1e03 100644 --- a/packages/Avalonia/Avalonia.props +++ b/packages/Avalonia/Avalonia.props @@ -1,6 +1,6 @@ - $(MSBuildThisFileDirectory)\..\tools\netcoreapp2.0\designer\Avalonia.Designer.HostApp.dll + $(MSBuildThisFileDirectory)\..\tools\netcoreapp3.1\designer\Avalonia.Designer.HostApp.dll $(MSBuildThisFileDirectory)\..\tools\net461\designer\Avalonia.Designer.HostApp.exe $(MSBuildThisFileDirectory)\..\tools\netstandard2.0\Avalonia.Build.Tasks.dll false diff --git a/readme.md b/readme.md index 42b1e52205..40471b3b28 100644 --- a/readme.md +++ b/readme.md @@ -8,25 +8,21 @@ ## About -**Avalonia** is a WPF/UWP-inspired cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows (.NET Framework, .NET Core), Linux (via Xorg), macOS and with experimental support for Android and iOS. +**Avalonia** is a cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows (.NET Framework, .NET Core), Linux (via Xorg), macOS. -**Avalonia** is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and [breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) as we continue along into this project's development. To see the status of some of our features, please see our [Roadmap here](https://github.com/AvaloniaUI/Avalonia/issues/2239). +**Avalonia** is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and breaking changes as we continue along into this project's development. -| Control catalog | Desktop platforms | Mobile platforms | -|---|---|---| -| | | | +To see the status of some of our features, please see our [Roadmap here](https://github.com/AvaloniaUI/Avalonia/issues/2239). -[Awesome Avalonia](https://github.com/AvaloniaCommunity/awesome-avalonia) is curated list of awesome Avalonia UI tools, libraries, projects and resources. +You can also see what [breaking changes](https://github.com/AvaloniaUI/Avalonia/issues/3538) we have planned and what our [past breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) have been. -## Getting Started - -Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started. After installing it, open "New Project" dialog in Visual Studio, choose "Avalonia" in "Visual C#" section, select "Avalonia .NET Core Application" and press OK (screenshot). Now you can write code and markup that will work on multiple platforms! +[Awesome Avalonia](https://github.com/AvaloniaCommunity/awesome-avalonia) is community-curated list of awesome Avalonia UI tools, libraries, projects and resources. Go and see what people are building with Avalonia! -For those without Visual Studio, a starter guide for .NET Core CLI can be found [here](http://avaloniaui.net/docs/quickstart/create-new-project#net-core). +## Getting Started -If you need to develop Avalonia app with JetBrains Rider, go and *vote* on [this issue](https://youtrack.jetbrains.com/issue/RIDER-39247) in their tracker. JetBrains won't do things without their users telling them that they want the feature, so only **YOU** can make it happen. +The Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started, or you can use the .NET Core CLI. For a starer guide see our [documentation](http://avaloniaui.net/docs/quickstart/create-new-project). -Avalonia is delivered via NuGet package manager. You can find the packages here: [stable(ish)](https://www.nuget.org/packages/Avalonia/) +Avalonia is delivered via NuGet package manager. You can find the packages here: https://www.nuget.org/packages/Avalonia/ Use these commands in the Package Manager console to install Avalonia manually: ``` @@ -34,18 +30,17 @@ Install-Package Avalonia Install-Package Avalonia.Desktop ``` -## Bleeding Edge Builds +## JetBrains Rider -or use nightly build feeds as described here: -https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed +If you need to develop Avalonia app with JetBrains Rider, go and *vote* on [this issue](https://youtrack.jetbrains.com/issue/RIDER-39247) in their tracker. JetBrains won't do things without their users telling them that they want the feature, so only **YOU** can make it happen. -## Documentation +## Bleeding Edge Builds -You can take a look at the [getting started page](http://avaloniaui.net/docs/quickstart/) for an overview of how to get started but probably the best thing to do for now is to already know a little bit about WPF/Silverlight/UWP/XAML and ask questions in our [Gitter room](https://gitter.im/AvaloniaUI/Avalonia). +We also have a [nightly build](https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed) which tracks the current state of master. Although these packages are less stable than the release on NuGet.org, you'll get all the latest features and bugfixes right away and many of our users actually prefer this feed! -There's also a high-level [architecture document](http://avaloniaui.net/architecture/project-structure) that is currently a little bit out of date, and I've also started writing blog posts on Avalonia at http://grokys.github.io/. +## Documentation -Contributions for our docs are always welcome! +Documentation can be found on our website at http://avaloniaui.net/docs/. We also have a [tutorial](http://avaloniaui.net/docs/tutorial/) over there for newcomers. ## Building and Using @@ -60,14 +55,12 @@ Please read the [contribution guidelines](http://avaloniaui.net/contributing/con This project exists thanks to all the people who contribute. [[Contribute](http://avaloniaui.net/contributing/contributing)]. - ### Backers Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/Avalonia#backer)] - ### Sponsors Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/Avalonia#sponsor)] diff --git a/samples/BindingDemo/BindingDemo.csproj b/samples/BindingDemo/BindingDemo.csproj index a3f16dc222..ce33f42143 100644 --- a/samples/BindingDemo/BindingDemo.csproj +++ b/samples/BindingDemo/BindingDemo.csproj @@ -1,7 +1,7 @@  Exe - netcoreapp2.0;net461 + netcoreapp3.1 diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index 7919c3ac5a..772af9eacd 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.0 + netcoreapp3.1 true diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index b95e01c5ae..f3f70719e3 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -59,10 +59,17 @@ - + + + No Decorations + Border Only + Full Decorations + + Light Dark - + + diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index acb9bc5bc6..5f71b2ecd9 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -56,6 +56,20 @@ namespace ControlCatalog } }; Styles.Add(light); + + var decorations = this.Find("Decorations"); + decorations.SelectionChanged += (sender, e) => + { + Window window = (Window)VisualRoot; + window.SystemDecorations = (SystemDecorations)decorations.SelectedIndex; + }; + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + var decorations = this.Find("Decorations"); + decorations.SelectedIndex = (int)((Window)VisualRoot).SystemDecorations; } } } diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index 248f94082d..d25de9c1f5 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -14,7 +14,7 @@ - + @@ -22,7 +22,9 @@ - + diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index 38cbde9d92..b40fdb4a17 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -1,13 +1,11 @@ +using System; +using System.Runtime.InteropServices; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Notifications; -using Avalonia.Controls.Primitives; +using Avalonia.Input; using Avalonia.Markup.Xaml; -using Avalonia.Threading; using ControlCatalog.ViewModels; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; namespace ControlCatalog { @@ -35,6 +33,12 @@ namespace ControlCatalog mainMenu.AttachedToVisualTree += MenuAttached; } + public static string MenuQuitHeader => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "Quit Avalonia" : "E_xit"; + + public static KeyGesture MenuQuitGesture => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? + new KeyGesture(Key.Q, KeyModifiers.Meta) : + new KeyGesture(Key.F4, KeyModifiers.Alt); + public void MenuAttached(object sender, VisualTreeAttachmentEventArgs e) { if (NativeMenu.GetIsNativeMenuExported(this) && sender is Menu mainMenu) diff --git a/samples/ControlCatalog/Pages/ImagePage.xaml b/samples/ControlCatalog/Pages/ImagePage.xaml index 9b8f8af765..f61931ed72 100644 --- a/samples/ControlCatalog/Pages/ImagePage.xaml +++ b/samples/ControlCatalog/Pages/ImagePage.xaml @@ -7,7 +7,7 @@ Displays an image - + Bitmap @@ -22,6 +22,23 @@ + Crop + + None + Center + TopLeft + TopRight + BottomLeft + BottomRight + + + + + + + + + Drawing None diff --git a/samples/ControlCatalog/Pages/ImagePage.xaml.cs b/samples/ControlCatalog/Pages/ImagePage.xaml.cs index bbe89d1dfd..d8f4d6d5a2 100644 --- a/samples/ControlCatalog/Pages/ImagePage.xaml.cs +++ b/samples/ControlCatalog/Pages/ImagePage.xaml.cs @@ -1,6 +1,10 @@ +using System; +using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; namespace ControlCatalog.Pages { @@ -8,12 +12,14 @@ namespace ControlCatalog.Pages { private readonly Image _bitmapImage; private readonly Image _drawingImage; + private readonly Image _croppedImage; public ImagePage() { InitializeComponent(); _bitmapImage = this.FindControl("bitmapImage"); _drawingImage = this.FindControl("drawingImage"); + _croppedImage = this.FindControl("croppedImage"); } private void InitializeComponent() @@ -38,5 +44,32 @@ namespace ControlCatalog.Pages _drawingImage.Stretch = (Stretch)comboxBox.SelectedIndex; } } + + public void BitmapCropChanged(object sender, SelectionChangedEventArgs e) + { + if (_croppedImage != null) + { + var comboxBox = (ComboBox)sender; + var croppedBitmap = _croppedImage.Source as CroppedBitmap; + croppedBitmap.SourceRect = GetCropRect(comboxBox.SelectedIndex); + } + } + + private PixelRect GetCropRect(int index) + { + var bitmapWidth = 640; + var bitmapHeight = 426; + var cropSize = new PixelSize(320, 240); + return index switch + { + 1 => new PixelRect(new PixelPoint((bitmapWidth - cropSize.Width) / 2, (bitmapHeight - cropSize.Width) / 2), cropSize), + 2 => new PixelRect(new PixelPoint(0, 0), cropSize), + 3 => new PixelRect(new PixelPoint(bitmapWidth - cropSize.Width, 0), cropSize), + 4 => new PixelRect(new PixelPoint(0, bitmapHeight - cropSize.Height), cropSize), + 5 => new PixelRect(new PixelPoint(bitmapWidth - cropSize.Width, bitmapHeight - cropSize.Height), cropSize), + _ => PixelRect.Empty + }; + + } } } diff --git a/samples/ControlCatalog/Pages/MenuPage.xaml b/samples/ControlCatalog/Pages/MenuPage.xaml index 868f0df6ad..de9ea34e80 100644 --- a/samples/ControlCatalog/Pages/MenuPage.xaml +++ b/samples/ControlCatalog/Pages/MenuPage.xaml @@ -16,13 +16,13 @@ Defined in XAML - + - + diff --git a/samples/PlatformSanityChecks/PlatformSanityChecks.csproj b/samples/PlatformSanityChecks/PlatformSanityChecks.csproj index 2adef5f64e..86d762a5bc 100644 --- a/samples/PlatformSanityChecks/PlatformSanityChecks.csproj +++ b/samples/PlatformSanityChecks/PlatformSanityChecks.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.0 + netcoreapp3.1 diff --git a/samples/Previewer/Previewer.csproj b/samples/Previewer/Previewer.csproj index 2cdde0c945..cd3daf61e1 100644 --- a/samples/Previewer/Previewer.csproj +++ b/samples/Previewer/Previewer.csproj @@ -1,7 +1,7 @@  Exe - netcoreapp2.0 + netcoreapp3.1 diff --git a/samples/RemoteDemo/RemoteDemo.csproj b/samples/RemoteDemo/RemoteDemo.csproj index 2d7699561a..530cad805f 100644 --- a/samples/RemoteDemo/RemoteDemo.csproj +++ b/samples/RemoteDemo/RemoteDemo.csproj @@ -1,7 +1,7 @@ Exe - netcoreapp2.0 + netcoreapp3.1 diff --git a/samples/RenderDemo/RenderDemo.csproj b/samples/RenderDemo/RenderDemo.csproj index a3f16dc222..ce33f42143 100644 --- a/samples/RenderDemo/RenderDemo.csproj +++ b/samples/RenderDemo/RenderDemo.csproj @@ -1,7 +1,7 @@  Exe - netcoreapp2.0;net461 + netcoreapp3.1 diff --git a/samples/VirtualizationDemo/VirtualizationDemo.csproj b/samples/VirtualizationDemo/VirtualizationDemo.csproj index a3f16dc222..ce33f42143 100644 --- a/samples/VirtualizationDemo/VirtualizationDemo.csproj +++ b/samples/VirtualizationDemo/VirtualizationDemo.csproj @@ -1,7 +1,7 @@  Exe - netcoreapp2.0;net461 + netcoreapp3.1 diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 88b99cd99a..51062f1568 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -80,8 +80,12 @@ namespace Avalonia _inheritanceParent?.RemoveInheritanceChild(this); _inheritanceParent = value; - foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType())) + var properties = AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType()); + var propertiesCount = properties.Count; + + for (var i = 0; i < propertiesCount; i++) { + var property = properties[i]; if (valuestore?.IsSet(property) == true) { // If local value set there can be no change. diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index 29ab10278b..7d3d5492b6 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -3,9 +3,7 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Runtime.CompilerServices; -using Avalonia.Data; namespace Avalonia { @@ -28,8 +26,6 @@ namespace Avalonia new Dictionary>(); private readonly Dictionary> _directCache = new Dictionary>(); - private readonly Dictionary> _initializedCache = - new Dictionary>(); private readonly Dictionary> _inheritedCache = new Dictionary>(); @@ -49,7 +45,7 @@ namespace Avalonia /// /// The type. /// A collection of definitions. - public IEnumerable GetRegistered(Type type) + public IReadOnlyList GetRegistered(Type type) { Contract.Requires(type != null); @@ -83,7 +79,7 @@ namespace Avalonia /// /// The type. /// A collection of definitions. - public IEnumerable GetRegisteredAttached(Type type) + public IReadOnlyList GetRegisteredAttached(Type type) { Contract.Requires(type != null); @@ -114,7 +110,7 @@ namespace Avalonia /// /// The type. /// A collection of definitions. - public IEnumerable GetRegisteredDirect(Type type) + public IReadOnlyList GetRegisteredDirect(Type type) { Contract.Requires(type != null); @@ -145,7 +141,7 @@ namespace Avalonia /// /// The type. /// A collection of definitions. - public IEnumerable GetRegisteredInherited(Type type) + public IReadOnlyList GetRegisteredInherited(Type type) { Contract.Requires(type != null); @@ -157,16 +153,27 @@ namespace Avalonia result = new List(); var visited = new HashSet(); - foreach (var property in GetRegistered(type)) + var registered = GetRegistered(type); + var registeredCount = registered.Count; + + for (var i = 0; i < registeredCount; i++) { + var property = registered[i]; + if (property.Inherits) { result.Add(property); visited.Add(property); } } - foreach (var property in GetRegisteredAttached(type)) + + var registeredAttached = GetRegisteredAttached(type); + var registeredAttachedCount = registeredAttached.Count; + + for (var i = 0; i < registeredAttachedCount; i++) { + var property = registeredAttached[i]; + if (property.Inherits) { if (!visited.Contains(property)) @@ -185,7 +192,7 @@ namespace Avalonia /// /// The object. /// A collection of definitions. - public IEnumerable GetRegistered(IAvaloniaObject o) + public IReadOnlyList GetRegistered(IAvaloniaObject o) { Contract.Requires(o != null); @@ -229,8 +236,13 @@ namespace Avalonia throw new InvalidOperationException("Attached properties not supported."); } - foreach (AvaloniaProperty x in GetRegistered(type)) + var registered = GetRegistered(type); + var registeredCount = registered.Count; + + for (var i = 0; i < registeredCount; i++) { + AvaloniaProperty x = registered[i]; + if (x.Name == name) { return x; @@ -276,8 +288,13 @@ namespace Avalonia return property; } - foreach (var p in GetRegisteredDirect(o.GetType())) + var registeredDirect = GetRegisteredDirect(o.GetType()); + var registeredDirectCount = registeredDirect.Count; + + for (var i = 0; i < registeredDirectCount; i++) { + var p = registeredDirect[i]; + if (p == property) { return (DirectPropertyBase)p; @@ -308,8 +325,23 @@ namespace Avalonia Contract.Requires(type != null); Contract.Requires(property != null); - return Instance.GetRegistered(type).Any(x => x == property) || - Instance.GetRegisteredAttached(type).Any(x => x == property); + static bool ContainsProperty(IReadOnlyList properties, AvaloniaProperty property) + { + var propertiesCount = properties.Count; + + for (var i = 0; i < propertiesCount; i++) + { + if (properties[i] == property) + { + return true; + } + } + + return false; + } + + return ContainsProperty(Instance.GetRegistered(type), property) || + ContainsProperty(Instance.GetRegisteredAttached(type), property); } /// @@ -374,7 +406,6 @@ namespace Avalonia } _registeredCache.Clear(); - _initializedCache.Clear(); _inheritedCache.Clear(); } @@ -411,32 +442,7 @@ namespace Avalonia } _attachedCache.Clear(); - _initializedCache.Clear(); _inheritedCache.Clear(); } - - private readonly struct PropertyInitializationData - { - public AvaloniaProperty Property { get; } - public object Value { get; } - public bool IsDirect { get; } - public IDirectPropertyAccessor DirectAccessor { get; } - - public PropertyInitializationData(AvaloniaProperty property, IDirectPropertyAccessor directAccessor) - { - Property = property; - Value = null; - IsDirect = true; - DirectAccessor = directAccessor; - } - - public PropertyInitializationData(AvaloniaProperty property, IStyledPropertyAccessor styledAccessor, Type type) - { - Property = property; - Value = styledAccessor.GetDefaultValue(type); - IsDirect = false; - DirectAccessor = null; - } - } } } diff --git a/src/Avalonia.Controls/Calendar/DatePicker.cs b/src/Avalonia.Controls/Calendar/DatePicker.cs index ea06bdb394..07e42c64e4 100644 --- a/src/Avalonia.Controls/Calendar/DatePicker.cs +++ b/src/Avalonia.Controls/Calendar/DatePicker.cs @@ -476,7 +476,7 @@ namespace Avalonia.Controls { _dropDownButton.Click += DropDownButton_Click; _buttonPointerPressedSubscription = - _dropDownButton.AddHandler(PointerPressedEvent, DropDownButton_PointerPressed, handledEventsToo: true); + _dropDownButton.AddDisposableHandler(PointerPressedEvent, DropDownButton_PointerPressed, handledEventsToo: true); } if (_textBox != null) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 9d471a0fc0..d1f54da23a 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -9,6 +9,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.VisualTree; @@ -265,7 +266,7 @@ namespace Avalonia.Controls var toplevel = this.GetVisualRoot() as TopLevel; if (toplevel != null) { - _subscriptionsOnOpen = toplevel.AddHandler(PointerWheelChangedEvent, (s, ev) => + _subscriptionsOnOpen = toplevel.AddDisposableHandler(PointerWheelChangedEvent, (s, ev) => { //eat wheel scroll event outside dropdown popup while it's open if (IsDropDownOpen && (ev.Source as IVisual).GetVisualRoot() == toplevel) diff --git a/src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs b/src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs new file mode 100644 index 0000000000..603a470a93 --- /dev/null +++ b/src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs @@ -0,0 +1,195 @@ +using System; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Text; +using Avalonia.Data.Converters; +using Avalonia.Input; + +namespace Avalonia.Controls.Converters +{ + /// + /// Converts a to a string, formatting it according to the current + /// platform's style guidelines. + /// + public class PlatformKeyGestureConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is null) + { + return null; + } + else if (value is KeyGesture gesture && targetType == typeof(string)) + { + return ToPlatformString(gesture); + } + else + { + throw new NotSupportedException(); + } + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + + /// + /// Converts a to a string, formatting it according to the current + /// platform's style guidelines. + /// + /// The gesture. + /// The gesture formatted according to the current platform. + public static string ToPlatformString(KeyGesture gesture) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return ToString(gesture, "Win"); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return ToString(gesture, "Super"); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return ToOSXString(gesture); + } + else + { + return gesture.ToString(); + } + } + + private static string ToString(KeyGesture gesture, string meta) + { + var s = new StringBuilder(); + + static void Plus(StringBuilder s) + { + if (s.Length > 0) + { + s.Append("+"); + } + } + + if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Control)) + { + s.Append("Ctrl"); + } + + if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Shift)) + { + Plus(s); + s.Append("Shift"); + } + + if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Alt)) + { + Plus(s); + s.Append("Alt"); + } + + if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Meta)) + { + Plus(s); + s.Append(meta); + } + + Plus(s); + s.Append(ToString(gesture.Key)); + + return s.ToString(); + } + + private static string ToOSXString(KeyGesture gesture) + { + var s = new StringBuilder(); + + if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Control)) + { + s.Append('⌃'); + } + + if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Alt)) + { + s.Append('⌥'); + } + + if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Shift)) + { + s.Append('⇧'); + } + + if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Meta)) + { + s.Append('⌘'); + } + + s.Append(ToOSXString(gesture.Key)); + + return s.ToString(); + } + + private static string ToString(Key key) + { + return key switch + { + Key.Add => "+", + Key.Back => "Backspace", + Key.D0 => "0", + Key.D1 => "1", + Key.D2 => "2", + Key.D3 => "3", + Key.D4 => "4", + Key.D5 => "5", + Key.D6 => "6", + Key.D7 => "7", + Key.D8 => "8", + Key.D9 => "9", + Key.Decimal => ".", + Key.Divide => "/", + Key.Down => "Down Arrow", + Key.Left => "Left Arrow", + Key.Multiply => "*", + Key.OemBackslash => "\\", + Key.OemCloseBrackets => "]", + Key.OemComma => ",", + Key.OemMinus => "-", + Key.OemOpenBrackets => "[", + Key.OemPeriod=> ".", + Key.OemPipe => "|", + Key.OemPlus => "+", + Key.OemQuestion => "/", + Key.OemQuotes => "\"", + Key.OemSemicolon => ";", + Key.OemTilde => "`", + Key.Right => "Right Arrow", + Key.Separator => "/", + Key.Subtract => "-", + Key.Up => "Up Arrow", + _ => key.ToString(), + }; + } + + private static string ToOSXString(Key key) + { + return key switch + { + Key.Back => "⌫", + Key.Down => "↓", + Key.End => "↘", + Key.Escape => "⎋", + Key.Home => "↖", + Key.Left => "←", + Key.Return => "↩", + Key.PageDown => "⇞", + Key.PageUp => "⇟", + Key.Right => "→", + Key.Space => "␣", + Key.Tab => "⇥", + Key.Up => "↑", + _ => ToString(key), + }; + } + } +} diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index e0baa5e679..3164e56222 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -13,6 +13,7 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; +using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -48,6 +49,12 @@ namespace Avalonia.Controls public static readonly StyledProperty IconProperty = AvaloniaProperty.Register(nameof(Icon)); + /// + /// Defines the property. + /// + public static readonly StyledProperty InputGestureProperty = + AvaloniaProperty.Register(nameof(InputGesture)); + /// /// Defines the property. /// @@ -93,6 +100,7 @@ namespace Avalonia.Controls private ICommand _command; private bool _commandCanExecute = true; private Popup _popup; + private IDisposable _gridHack; /// /// Initializes static members of the class. @@ -194,6 +202,19 @@ namespace Avalonia.Controls set { SetValue(IconProperty, value); } } + /// + /// Gets or sets the input gesture that will be displayed in the menu item. + /// + /// + /// Setting this property does not cause the input gesture to be handled by the menu item, + /// it simply displays the gesture text in the menu. + /// + public KeyGesture InputGesture + { + get { return GetValue(InputGestureProperty); } + set { SetValue(InputGestureProperty, value); } + } + /// /// Gets or sets a value indicating whether the is currently selected. /// @@ -304,6 +325,32 @@ namespace Avalonia.Controls { Command.CanExecuteChanged -= CanExecuteChanged; } + + _gridHack?.Dispose(); + _gridHack = null; + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + if (this.GetVisualParent() is IControl parent) + { + // HACK: This nasty but it's all WPF's fault. Grid uses an inherited attached + // property to store SharedSizeGroup state, except property inheritance is done + // down the logical tree. In this case, the control which is setting + // Grid.IsSharedSizeScope="True" is not in the logical tree. Instead of fixing + // the way Grid stores shared size state, the developers of WPF just created a + // binding of the internal state of the visual parent to the menu item. We don't + // have much choice but to do the same for now unless we want to refactor Grid, + // which I honestly am not brave enough to do right now. Here's the same hack in + // the WPF codebase: + // + // https://github.com/dotnet/wpf/blob/89537909bdf36bc918e88b37751add46a8980bb0/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Controls/MenuItem.cs#L2126-L2141 + _gridHack = Bind( + DefinitionBase.PrivateSharedSizeScopeProperty, + parent.GetBindingObservable(DefinitionBase.PrivateSharedSizeScopeProperty)); + } } /// diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index 91b895f38a..238070bbef 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -36,7 +36,7 @@ namespace Avalonia.Platform /// /// Enables or disables system window decorations (title bar, buttons, etc) /// - void SetSystemDecorations(bool enabled); + void SetSystemDecorations(SystemDecorations enabled); /// /// Sets the icon of this window. diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs index 7e5c434caf..bed5389cb3 100644 --- a/src/Avalonia.Controls/Primitives/AccessText.cs +++ b/src/Avalonia.Controls/Primitives/AccessText.cs @@ -160,17 +160,6 @@ namespace Avalonia.Controls.Primitives return base.CreateTextLayout(constraint, StripAccessKey(text)); } - /// - /// Measures the control. - /// - /// The available size for the control. - /// The desired size. - protected override Size MeasureOverride(Size availableSize) - { - var result = base.MeasureOverride(availableSize); - return result.WithHeight(result.Height + 1); - } - /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 9dd481cf40..1a12b53b9d 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -279,7 +279,7 @@ namespace Avalonia.Controls.Primitives } } - DeferCleanup(topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel)); + DeferCleanup(topLevel.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel)); DeferCleanup(InputManager.Instance?.Process.Subscribe(ListenForNonClientClick)); diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 1655e22331..0278360ba5 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -20,6 +20,12 @@ namespace Avalonia.Controls public static readonly StyledProperty BackgroundProperty = Border.BackgroundProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty PaddingProperty = + Decorator.PaddingProperty.AddOwner(); + // TODO: Define these attached properties elsewhere (e.g. on a Text class) and AddOwner // them into TextBlock. @@ -29,7 +35,7 @@ namespace Avalonia.Controls public static readonly AttachedProperty FontFamilyProperty = AvaloniaProperty.RegisterAttached( nameof(FontFamily), - defaultValue: FontFamily.Default, + defaultValue: FontFamily.Default, inherits: true); /// @@ -110,20 +116,31 @@ namespace Avalonia.Controls static TextBlock() { ClipToBoundsProperty.OverrideDefaultValue(true); + AffectsRender( - BackgroundProperty, - ForegroundProperty, - FontWeightProperty, - FontSizeProperty, - FontStyleProperty); + BackgroundProperty, ForegroundProperty, FontSizeProperty, + FontWeightProperty, FontStyleProperty, TextWrappingProperty, + TextTrimmingProperty, TextAlignmentProperty, FontFamilyProperty, + TextDecorationsProperty, TextProperty, PaddingProperty); + + AffectsMeasure( + FontSizeProperty, FontWeightProperty, FontStyleProperty, + FontFamilyProperty, TextTrimmingProperty, TextProperty, + PaddingProperty); Observable.Merge( TextProperty.Changed, + ForegroundProperty.Changed, TextAlignmentProperty.Changed, + TextWrappingProperty.Changed, + TextTrimmingProperty.Changed, FontSizeProperty.Changed, FontStyleProperty.Changed, - FontWeightProperty.Changed - ).AddClassHandler((x, _) => x.OnTextPropertiesChanged()); + FontWeightProperty.Changed, + FontFamilyProperty.Changed, + TextDecorationsProperty.Changed, + PaddingProperty.Changed + ).AddClassHandler((x, _) => x.InvalidateTextLayout()); } /// @@ -145,6 +162,15 @@ namespace Avalonia.Controls } } + /// + /// Gets or sets the padding to place around the . + /// + public Thickness Padding + { + get { return GetValue(PaddingProperty); } + set { SetValue(PaddingProperty, value); } + } + /// /// Gets or sets a brush used to paint the control's background. /// @@ -363,7 +389,9 @@ namespace Avalonia.Controls context.FillRectangle(background, new Rect(Bounds.Size)); } - TextLayout?.Draw(context.PlatformImpl, new Point()); + var padding = Padding; + + TextLayout?.Draw(context.PlatformImpl, new Point(padding.Left, padding.Top)); } /// @@ -412,6 +440,10 @@ namespace Avalonia.Controls return new Size(); } + var padding = Padding; + + availableSize = availableSize.Deflate(padding); + if (_constraint != availableSize) { InvalidateTextLayout(); @@ -419,19 +451,17 @@ namespace Avalonia.Controls _constraint = availableSize; - return TextLayout?.Bounds.Size ?? Size.Empty; + var measuredSize = TextLayout?.Bounds.Size ?? Size.Empty; + + return measuredSize.Inflate(padding); } protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { base.OnAttachedToLogicalTree(e); - InvalidateTextLayout(); - InvalidateMeasure(); - } - private void OnTextPropertiesChanged() - { InvalidateTextLayout(); + InvalidateMeasure(); } } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index a438d7380b..4acfb75cb9 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -275,6 +275,22 @@ namespace Avalonia.Controls } } + public string SelectedText + { + get { return GetSelection(); } + set + { + if (value == null) + { + return; + } + + _undoRedoHelper.Snapshot(); + HandleTextInput(value); + _undoRedoHelper.Snapshot(); + } + } + /// /// Gets or sets the horizontal alignment of the content within the control. /// @@ -683,12 +699,12 @@ namespace Avalonia.Controls protected override void OnPointerPressed(PointerPressedEventArgs e) { - var point = e.GetPosition(_presenter); - var index = CaretIndex = _presenter.GetCaretIndex(point); var text = Text; - if (text != null && e.MouseButton == MouseButton.Left) + if (text != null && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { + var point = e.GetPosition(_presenter); + var index = CaretIndex = _presenter.GetCaretIndex(point); switch (e.ClickCount) { case 1: @@ -714,7 +730,8 @@ namespace Avalonia.Controls protected override void OnPointerMoved(PointerEventArgs e) { - if (_presenter != null && e.Pointer.Captured == _presenter) + // selection should not change during pointer move if the user right clicks + if (_presenter != null && e.Pointer.Captured == _presenter && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { var point = e.GetPosition(_presenter); @@ -727,6 +744,22 @@ namespace Avalonia.Controls { if (_presenter != null && e.Pointer.Captured == _presenter) { + if (e.InitialPressMouseButton == MouseButton.Right) + { + 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; + } + } e.Pointer.Capture(null); } } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 6554237b3a..f4a3f05f03 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -2,19 +2,19 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.ComponentModel; +using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; using Avalonia.Controls.Platform; +using Avalonia.Data; using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Styling; -using System.Collections.Generic; -using System.Linq; using JetBrains.Annotations; -using System.ComponentModel; -using Avalonia.Interactivity; namespace Avalonia.Controls { @@ -45,6 +45,27 @@ namespace Avalonia.Controls WidthAndHeight = 3, } + /// + /// Determines system decorations (title bar, border, etc) for a + /// + public enum SystemDecorations + { + /// + /// No decorations + /// + None = 0, + + /// + /// Window border without titlebar + /// + BorderOnly = 1, + + /// + /// Fully decorated (default) + /// + Full = 2 + } + /// /// A top-level window. /// @@ -59,8 +80,18 @@ namespace Avalonia.Controls /// /// Enables or disables system window decorations (title bar, buttons, etc) /// - public static readonly StyledProperty HasSystemDecorationsProperty = - AvaloniaProperty.Register(nameof(HasSystemDecorations), true); + [Obsolete("Use SystemDecorationsProperty instead")] + public static readonly DirectProperty HasSystemDecorationsProperty = + AvaloniaProperty.RegisterDirect( + nameof(HasSystemDecorations), + o => o.HasSystemDecorations, + (o, v) => o.HasSystemDecorations = v); + + /// + /// Defines the property. + /// + public static readonly StyledProperty SystemDecorationsProperty = + AvaloniaProperty.Register(nameof(SystemDecorations), SystemDecorations.Full); /// /// Enables or disables the taskbar icon @@ -124,9 +155,6 @@ namespace Avalonia.Controls { BackgroundProperty.OverrideDefaultValue(typeof(Window), Brushes.White); TitleProperty.Changed.AddClassHandler((s, e) => s.PlatformImpl?.SetTitle((string)e.NewValue)); - HasSystemDecorationsProperty.Changed.AddClassHandler( - (s, e) => s.PlatformImpl?.SetSystemDecorations((bool)e.NewValue)); - ShowInTaskbarProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.ShowTaskbarIcon((bool)e.NewValue)); IconProperty.Changed.AddClassHandler((s, e) => s.PlatformImpl?.SetIcon(((WindowIcon)e.NewValue)?.PlatformImpl)); @@ -135,12 +163,11 @@ namespace Avalonia.Controls WindowStateProperty.Changed.AddClassHandler( (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.WindowState = (WindowState)e.NewValue; }); - + MinWidthProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size((double)e.NewValue, w.MinHeight), new Size(w.MaxWidth, w.MaxHeight))); MinHeightProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, (double)e.NewValue), new Size(w.MaxWidth, w.MaxHeight))); MaxWidthProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size((double)e.NewValue, w.MaxHeight))); MaxHeightProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size(w.MaxWidth, (double)e.NewValue))); - } /// @@ -191,11 +218,30 @@ namespace Avalonia.Controls /// /// Enables or disables system window decorations (title bar, buttons, etc) /// - /// + [Obsolete("Use SystemDecorations instead")] public bool HasSystemDecorations { - get { return GetValue(HasSystemDecorationsProperty); } - set { SetValue(HasSystemDecorationsProperty, value); } + get => SystemDecorations == SystemDecorations.Full; + set + { + var oldValue = HasSystemDecorations; + + if (oldValue != value) + { + SystemDecorations = value ? SystemDecorations.Full : SystemDecorations.None; + RaisePropertyChanged(HasSystemDecorationsProperty, oldValue, value); + } + } + } + + /// + /// Sets the system decorations (title bar, border, etc) + /// + /// + public SystemDecorations SystemDecorations + { + get { return GetValue(SystemDecorationsProperty); } + set { SetValue(SystemDecorationsProperty, value); } } /// @@ -584,5 +630,27 @@ namespace Avalonia.Controls /// event needs to be raised. /// protected virtual void OnClosing(CancelEventArgs e) => Closing?.Invoke(this, e); + + protected override void OnPropertyChanged( + AvaloniaProperty property, + Optional oldValue, + BindingValue newValue, + BindingPriority priority) + { + if (property == SystemDecorationsProperty) + { + var typedNewValue = newValue.GetValueOrDefault(); + + PlatformImpl?.SetSystemDecorations(typedNewValue); + + var o = oldValue.GetValueOrDefault() == SystemDecorations.Full; + var n = typedNewValue == SystemDecorations.Full; + + if (o != n) + { + RaisePropertyChanged(HasSystemDecorationsProperty, o, n); + } + } + } } } diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 86e34ca6d4..7480b3519c 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -96,7 +96,7 @@ namespace Avalonia.DesignerSupport.Remote { } - public void SetSystemDecorations(bool enabled) + public void SetSystemDecorations(SystemDecorations enabled) { } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 4bba5ef41b..7bf1d236bd 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -110,7 +110,7 @@ namespace Avalonia.DesignerSupport.Remote { } - public void SetSystemDecorations(bool enabled) + public void SetSystemDecorations(SystemDecorations enabled) { } diff --git a/src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj b/src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj index c0e46416c1..3dcf7c9681 100644 --- a/src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj +++ b/src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj @@ -1,7 +1,7 @@ - net461;netcoreapp2.0 + net461;netcoreapp3.1 diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs index 0464047273..b3a8f4745e 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs @@ -22,7 +22,7 @@ namespace Avalonia.Diagnostics } } - return root.AddHandler( + return root.AddDisposableHandler( InputElement.KeyDownEvent, PreviewKeyDown, RoutingStrategies.Tunnel); diff --git a/src/Avalonia.Input/KeyGesture.cs b/src/Avalonia.Input/KeyGesture.cs index 490c31bef9..32236fe382 100644 --- a/src/Avalonia.Input/KeyGesture.cs +++ b/src/Avalonia.Input/KeyGesture.cs @@ -1,9 +1,11 @@ -// Copyright (c) The Avalonia Project. All rights reserved. +// Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; +using System.Text; namespace Avalonia.Input { @@ -108,19 +110,43 @@ namespace Avalonia.Input public override string ToString() { - var parts = new List(); + var s = new StringBuilder(); - foreach (var flag in Enum.GetValues(typeof(KeyModifiers)).Cast()) + static void Plus(StringBuilder s) { - if (KeyModifiers.HasFlag(flag) && flag != KeyModifiers.None) + if (s.Length > 0) { - parts.Add(flag.ToString()); + s.Append("+"); } } - parts.Add(Key.ToString()); + if (KeyModifiers.HasFlagCustom(KeyModifiers.Control)) + { + s.Append("Ctrl"); + } + + if (KeyModifiers.HasFlagCustom(KeyModifiers.Shift)) + { + Plus(s); + s.Append("Shift"); + } + + if (KeyModifiers.HasFlagCustom(KeyModifiers.Alt)) + { + Plus(s); + s.Append("Alt"); + } + + if (KeyModifiers.HasFlagCustom(KeyModifiers.Meta)) + { + Plus(s); + s.Append("Cmd"); + } + + Plus(s); + s.Append(Key); - return string.Join(" + ", parts); + return s.ToString(); } public bool Matches(KeyEventArgs keyEvent) => ResolveNumPadOperationKey(keyEvent.Key) == Key && keyEvent.KeyModifiers == KeyModifiers; @@ -141,7 +167,9 @@ namespace Avalonia.Input return KeyModifiers.Control; } - if (modifier.Equals("cmd".AsSpan(), StringComparison.OrdinalIgnoreCase)) + if (modifier.Equals("cmd".AsSpan(), StringComparison.OrdinalIgnoreCase) || + modifier.Equals("win".AsSpan(), StringComparison.OrdinalIgnoreCase) || + modifier.Equals("⌘".AsSpan(), StringComparison.OrdinalIgnoreCase)) { return KeyModifiers.Meta; } diff --git a/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj b/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj index 66f1e8cc26..730ca2bd6e 100644 --- a/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj +++ b/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj @@ -1,6 +1,8 @@  netstandard2.0 + Enable + CS8600;CS8602;CS8603 @@ -9,6 +11,4 @@ - - - + \ No newline at end of file diff --git a/src/Avalonia.Interactivity/EventRoute.cs b/src/Avalonia.Interactivity/EventRoute.cs new file mode 100644 index 0000000000..85ba33d7ba --- /dev/null +++ b/src/Avalonia.Interactivity/EventRoute.cs @@ -0,0 +1,200 @@ +using System; +using Avalonia.Collections.Pooled; + +namespace Avalonia.Interactivity +{ + /// + /// Holds the route for a routed event and supports raising an event on that route. + /// + public class EventRoute : IDisposable + { + private readonly RoutedEvent _event; + private PooledList? _route; + + /// + /// Initializes a new instance of the class. + /// + /// The routed event to be raised. + public EventRoute(RoutedEvent e) + { + e = e ?? throw new ArgumentNullException(nameof(e)); + + _event = e; + _route = null; + } + + /// + /// Gets a value indicating whether the route has any handlers. + /// + public bool HasHandlers => _route?.Count > 0; + + /// + /// Adds a handler to the route. + /// + /// The target on which the event should be raised. + /// The handler for the event. + /// The routing strategies to listen to. + /// + /// If true the handler will be raised even when the routed event is marked as handled. + /// + /// + /// An optional adapter which if supplied, will be called with + /// and the parameters for the event. This adapter can be used to avoid calling + /// `DynamicInvoke` on the handler. + /// + public void Add( + IInteractive target, + Delegate handler, + RoutingStrategies routes, + bool handledEventsToo = false, + Action? adapter = null) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + handler = handler ?? throw new ArgumentNullException(nameof(handler)); + + _route ??= new PooledList(16); + _route.Add(new RouteItem(target, handler, adapter, routes, handledEventsToo)); + } + + /// + /// Adds a class handler to the route. + /// + /// The target on which the event should be raised. + public void AddClassHandler(IInteractive target) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + + _route ??= new PooledList(16); + _route.Add(new RouteItem(target, null, null, 0, false)); + } + + /// + /// Raises an event along the route. + /// + /// The event source. + /// The event args. + public void RaiseEvent(IInteractive source, RoutedEventArgs e) + { + source = source ?? throw new ArgumentNullException(nameof(source)); + e = e ?? throw new ArgumentNullException(nameof(e)); + + e.Source = source; + + if (_event.RoutingStrategies == RoutingStrategies.Direct) + { + e.Route = RoutingStrategies.Direct; + RaiseEventImpl(e); + _event.InvokeRouteFinished(e); + } + else + { + if (_event.RoutingStrategies.HasFlagCustom(RoutingStrategies.Tunnel)) + { + e.Route = RoutingStrategies.Tunnel; + RaiseEventImpl(e); + _event.InvokeRouteFinished(e); + } + + if (_event.RoutingStrategies.HasFlagCustom(RoutingStrategies.Bubble)) + { + e.Route = RoutingStrategies.Bubble; + RaiseEventImpl(e); + _event.InvokeRouteFinished(e); + } + } + } + + /// + /// Disposes of the event route. + /// + public void Dispose() + { + _route?.Dispose(); + _route = null; + } + + private void RaiseEventImpl(RoutedEventArgs e) + { + if (_route is null) + { + return; + } + + if (e.Source is null) + { + throw new ArgumentException("Event source may not be null", nameof(e)); + } + + IInteractive? lastTarget = null; + var start = 0; + var end = _route.Count; + var step = 1; + + if (e.Route == RoutingStrategies.Tunnel) + { + start = end - 1; + step = end = -1; + } + + for (var i = start; i != end; i += step) + { + var entry = _route[i]; + + // If we've got to a new control then call any RoutedEvent.Raised listeners. + if (entry.Target != lastTarget) + { + if (!e.Handled) + { + _event.InvokeRaised(entry.Target, e); + } + + // If this is a direct event and we've already raised events then we're finished. + if (e.Route == RoutingStrategies.Direct && lastTarget is object) + { + return; + } + + lastTarget = entry.Target; + } + + // Raise the event handler. + if (entry.Handler is object && + entry.Routes.HasFlagCustom(e.Route) && + (!e.Handled || entry.HandledEventsToo)) + { + if (entry.Adapter is object) + { + entry.Adapter(entry.Handler, entry.Target, e); + } + else + { + entry.Handler.DynamicInvoke(entry.Target, e); + } + } + } + } + + private readonly struct RouteItem + { + public RouteItem( + IInteractive target, + Delegate? handler, + Action? adapter, + RoutingStrategies routes, + bool handledEventsToo) + { + Target = target; + Handler = handler; + Adapter = adapter; + Routes = routes; + HandledEventsToo = handledEventsToo; + } + + public IInteractive Target { get; } + public Delegate? Handler { get; } + public Action? Adapter { get; } + public RoutingStrategies Routes { get; } + public bool HandledEventsToo { get; } + } + } +} diff --git a/src/Avalonia.Interactivity/EventSubscription.cs b/src/Avalonia.Interactivity/EventSubscription.cs deleted file mode 100644 index e8fb1bfaf1..0000000000 --- a/src/Avalonia.Interactivity/EventSubscription.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; - -namespace Avalonia.Interactivity -{ - internal delegate void HandlerInvokeSignature(Delegate baseHandler, object sender, RoutedEventArgs args); - - internal class EventSubscription - { - public HandlerInvokeSignature InvokeAdapter { get; set; } - - public Delegate Handler { get; set; } - - public RoutingStrategies Routes { get; set; } - - public bool AlsoIfHandled { get; set; } - } -} diff --git a/src/Avalonia.Interactivity/IInteractive.cs b/src/Avalonia.Interactivity/IInteractive.cs index 47046b58e2..51aee78988 100644 --- a/src/Avalonia.Interactivity/IInteractive.cs +++ b/src/Avalonia.Interactivity/IInteractive.cs @@ -13,7 +13,7 @@ namespace Avalonia.Interactivity /// /// Gets the interactive parent of the object for bubbling and tunneling events. /// - IInteractive InteractiveParent { get; } + IInteractive? InteractiveParent { get; } /// /// Adds a handler for the specified routed event. @@ -23,7 +23,7 @@ namespace Avalonia.Interactivity /// The routing strategies to listen to. /// Whether handled events should also be listened for. /// A disposable that terminates the event subscription. - IDisposable AddHandler( + void AddHandler( RoutedEvent routedEvent, Delegate handler, RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, @@ -38,7 +38,7 @@ namespace Avalonia.Interactivity /// The routing strategies to listen to. /// Whether handled events should also be listened for. /// A disposable that terminates the event subscription. - IDisposable AddHandler( + void AddHandler( RoutedEvent routedEvent, EventHandler handler, RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, @@ -60,6 +60,13 @@ namespace Avalonia.Interactivity void RemoveHandler(RoutedEvent routedEvent, EventHandler handler) where TEventArgs : RoutedEventArgs; + /// + /// Adds the object's handlers for a routed event to an event route. + /// + /// The event. + /// The event route. + void AddToEventRoute(RoutedEvent routedEvent, EventRoute route); + /// /// Raises a routed event. /// diff --git a/src/Avalonia.Interactivity/Interactive.cs b/src/Avalonia.Interactivity/Interactive.cs index 27ece25183..321ecbc516 100644 --- a/src/Avalonia.Interactivity/Interactive.cs +++ b/src/Avalonia.Interactivity/Interactive.cs @@ -3,8 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; using Avalonia.Layout; using Avalonia.VisualTree; @@ -15,16 +13,12 @@ namespace Avalonia.Interactivity /// public class Interactive : Layoutable, IInteractive { - private Dictionary> _eventHandlers; - - private static readonly Dictionary s_invokeHandlerCache = new Dictionary(); + private Dictionary>? _eventHandlers; /// /// Gets the interactive parent of the object for bubbling and tunneling events. /// - IInteractive IInteractive.InteractiveParent => ((IVisual)this).VisualParent as IInteractive; - - private Dictionary> EventHandlers => _eventHandlers ?? (_eventHandlers = new Dictionary>()); + IInteractive? IInteractive.InteractiveParent => ((IVisual)this).VisualParent as IInteractive; /// /// Adds a handler for the specified routed event. @@ -33,24 +27,18 @@ namespace Avalonia.Interactivity /// The handler. /// The routing strategies to listen to. /// Whether handled events should also be listened for. - /// A disposable that terminates the event subscription. - public IDisposable AddHandler( + public void AddHandler( RoutedEvent routedEvent, Delegate handler, RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, bool handledEventsToo = false) { - Contract.Requires(routedEvent != null); - Contract.Requires(handler != null); + routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent)); + handler = handler ?? throw new ArgumentNullException(nameof(handler)); - var subscription = new EventSubscription - { - Handler = handler, - Routes = routes, - AlsoIfHandled = handledEventsToo, - }; + var subscription = new EventSubscription(handler, routes, handledEventsToo); - return AddEventSubscription(routedEvent, subscription); + AddEventSubscription(routedEvent, subscription); } /// @@ -61,44 +49,26 @@ namespace Avalonia.Interactivity /// The handler. /// The routing strategies to listen to. /// Whether handled events should also be listened for. - /// A disposable that terminates the event subscription. - public IDisposable AddHandler( + public void AddHandler( RoutedEvent routedEvent, EventHandler handler, RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, bool handledEventsToo = false) where TEventArgs : RoutedEventArgs { - Contract.Requires(routedEvent != null); - Contract.Requires(handler != null); + routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent)); + handler = handler ?? throw new ArgumentNullException(nameof(handler)); - // EventHandler delegate is not covariant, this forces us to create small wrapper - // that will cast our type erased instance and invoke it. - Type eventArgsType = routedEvent.EventArgsType; - - if (!s_invokeHandlerCache.TryGetValue(eventArgsType, out var invokeAdapter)) + static void InvokeAdapter(Delegate baseHandler, object sender, RoutedEventArgs args) { - void InvokeAdapter(Delegate baseHandler, object sender, RoutedEventArgs args) - { - var typedHandler = (EventHandler)baseHandler; - var typedArgs = (TEventArgs)args; + var typedHandler = (EventHandler)baseHandler; + var typedArgs = (TEventArgs)args; - typedHandler(sender, typedArgs); - } - - invokeAdapter = InvokeAdapter; - - s_invokeHandlerCache.Add(eventArgsType, invokeAdapter); + typedHandler(sender, typedArgs); } - var subscription = new EventSubscription - { - InvokeAdapter = invokeAdapter, - Handler = handler, - Routes = routes, - AlsoIfHandled = handledEventsToo, - }; + var subscription = new EventSubscription(handler, routes, handledEventsToo, (baseHandler, sender, args) => InvokeAdapter(baseHandler, sender, args)); - return AddEventSubscription(routedEvent, subscription); + AddEventSubscription(routedEvent, subscription); } /// @@ -108,14 +78,19 @@ namespace Avalonia.Interactivity /// The handler. public void RemoveHandler(RoutedEvent routedEvent, Delegate handler) { - Contract.Requires(routedEvent != null); - Contract.Requires(handler != null); + routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent)); + handler = handler ?? throw new ArgumentNullException(nameof(handler)); - List subscriptions = null; - - if (_eventHandlers?.TryGetValue(routedEvent, out subscriptions) == true) + if (_eventHandlers is object && + _eventHandlers.TryGetValue(routedEvent, out var subscriptions)) { - subscriptions.RemoveAll(x => x.Handler == handler); + for (var i = subscriptions.Count - 1; i >= 0; i--) + { + if (subscriptions[i].Handler == handler) + { + subscriptions.RemoveAt(i); + } + } } } @@ -137,191 +112,115 @@ namespace Avalonia.Interactivity /// The event args. public void RaiseEvent(RoutedEventArgs e) { - Contract.Requires(e != null); - - e.Source = e.Source ?? this; - - if (e.RoutedEvent.RoutingStrategies == RoutingStrategies.Direct) - { - e.Route = RoutingStrategies.Direct; - RaiseEventImpl(e); - e.RoutedEvent.InvokeRouteFinished(e); - } + e = e ?? throw new ArgumentNullException(nameof(e)); - if ((e.RoutedEvent.RoutingStrategies & RoutingStrategies.Tunnel) != 0) + if (e.RoutedEvent == null) { - TunnelEvent(e); - e.RoutedEvent.InvokeRouteFinished(e); + throw new ArgumentException("Cannot raise an event whose RoutedEvent is null."); } - if ((e.RoutedEvent.RoutingStrategies & RoutingStrategies.Bubble) != 0) - { - BubbleEvent(e); - e.RoutedEvent.InvokeRouteFinished(e); - } + using var route = BuildEventRoute(e.RoutedEvent); + route.RaiseEvent(this, e); } - /// - /// Bubbles an event. - /// - /// The event args. - private void BubbleEvent(RoutedEventArgs e) - { - Contract.Requires(e != null); - - e.Route = RoutingStrategies.Bubble; - - var traverser = HierarchyTraverser.Create(e); - - traverser.Traverse(this); - } - - /// - /// Tunnels an event. - /// - /// The event args. - private void TunnelEvent(RoutedEventArgs e) + void IInteractive.AddToEventRoute(RoutedEvent routedEvent, EventRoute route) { - Contract.Requires(e != null); - - e.Route = RoutingStrategies.Tunnel; - - var traverser = HierarchyTraverser.Create(e); + routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent)); + route = route ?? throw new ArgumentNullException(nameof(route)); - traverser.Traverse(this); + if (_eventHandlers != null && + _eventHandlers.TryGetValue(routedEvent, out var subscriptions)) + { + foreach (var sub in subscriptions) + { + route.Add(this, sub.Handler, sub.Routes, sub.HandledEventsToo, sub.InvokeAdapter); + } + } } /// - /// Carries out the actual invocation of an event on this object. + /// Builds an event route for a routed event. /// - /// The event args. - private void RaiseEventImpl(RoutedEventArgs e) + /// The routed event. + /// An describing the route. + /// + /// Usually, calling is sufficent to raise a routed + /// event, however there are situations in which the construction of the event args is expensive + /// and should be avoided if there are no handlers for an event. In these cases you can call + /// this method to build the event route and check the + /// property to see if there are any handlers registered on the route. If there are, call + /// to raise the event. + /// + protected EventRoute BuildEventRoute(RoutedEvent e) { - Contract.Requires(e != null); + e = e ?? throw new ArgumentNullException(nameof(e)); - e.RoutedEvent.InvokeRaised(this, e); + var result = new EventRoute(e); + var hasClassHandlers = e.HasRaisedSubscriptions; - List subscriptions = null; - - if (_eventHandlers?.TryGetValue(e.RoutedEvent, out subscriptions) == true) + if (e.RoutingStrategies.HasFlagCustom(RoutingStrategies.Bubble) || + e.RoutingStrategies.HasFlagCustom(RoutingStrategies.Tunnel)) { - foreach (var sub in subscriptions.ToList()) - { - bool correctRoute = (e.Route & sub.Routes) != 0; - bool notFinished = !e.Handled || sub.AlsoIfHandled; + IInteractive? element = this; - if (correctRoute && notFinished) + while (element != null) + { + if (hasClassHandlers) { - if (sub.InvokeAdapter != null) - { - sub.InvokeAdapter(sub.Handler, this, e); - } - else - { - sub.Handler.DynamicInvoke(this, e); - } + result.AddClassHandler(element); } + + element.AddToEventRoute(e, result); + element = element.InteractiveParent; } } - } - - private List GetEventSubscriptions(RoutedEvent routedEvent) - { - if (!EventHandlers.TryGetValue(routedEvent, out var subscriptions)) + else { - subscriptions = new List(); - EventHandlers.Add(routedEvent, subscriptions); - } - - return subscriptions; - } - - private IDisposable AddEventSubscription(RoutedEvent routedEvent, EventSubscription subscription) - { - List subscriptions = GetEventSubscriptions(routedEvent); - - subscriptions.Add(subscription); - - return new UnsubscribeDisposable(subscriptions, subscription); - } - - private sealed class UnsubscribeDisposable : IDisposable - { - private readonly List _subscriptions; - private readonly EventSubscription _subscription; + if (hasClassHandlers) + { + result.AddClassHandler(this); + } - public UnsubscribeDisposable(List subscriptions, EventSubscription subscription) - { - _subscriptions = subscriptions; - _subscription = subscription; + ((IInteractive)this).AddToEventRoute(e, result); } - public void Dispose() - { - _subscriptions.Remove(_subscription); - } + return result; } - private interface ITraverse + private void AddEventSubscription(RoutedEvent routedEvent, EventSubscription subscription) { - void Execute(IInteractive target, RoutedEventArgs e); - } + _eventHandlers ??= new Dictionary>(); - private struct NopTraverse : ITraverse - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Execute(IInteractive target, RoutedEventArgs e) + if (!_eventHandlers.TryGetValue(routedEvent, out var subscriptions)) { + subscriptions = new List(); + _eventHandlers.Add(routedEvent, subscriptions); } - } - private struct RaiseEventTraverse : ITraverse - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Execute(IInteractive target, RoutedEventArgs e) - { - ((Interactive)target).RaiseEventImpl(e); - } + subscriptions.Add(subscription); } - /// - /// Traverses interactive hierarchy allowing for raising events. - /// - /// Called before parent is traversed. - /// Called after parent has been traversed. - private struct HierarchyTraverser - where TPreTraverse : struct, ITraverse - where TPostTraverse : struct, ITraverse + private readonly struct EventSubscription { - private TPreTraverse _preTraverse; - private TPostTraverse _postTraverse; - private readonly RoutedEventArgs _args; - - private HierarchyTraverser(TPreTraverse preTraverse, TPostTraverse postTraverse, RoutedEventArgs args) - { - _preTraverse = preTraverse; - _postTraverse = postTraverse; - _args = args; - } - - public static HierarchyTraverser Create(RoutedEventArgs args) - { - return new HierarchyTraverser(new TPreTraverse(), new TPostTraverse(), args); + public EventSubscription( + Delegate handler, + RoutingStrategies routes, + bool handledEventsToo, + Action? invokeAdapter = null) + { + Handler = handler; + Routes = routes; + HandledEventsToo = handledEventsToo; + InvokeAdapter = invokeAdapter; } - public void Traverse(IInteractive target) - { - _preTraverse.Execute(target, _args); + public Action? InvokeAdapter { get; } - IInteractive parent = target.InteractiveParent; + public Delegate Handler { get; } - if (parent != null) - { - Traverse(parent); - } + public RoutingStrategies Routes { get; } - _postTraverse.Execute(target, _args); - } + public bool HandledEventsToo { get; } } } } diff --git a/src/Avalonia.Interactivity/InteractiveExtensions.cs b/src/Avalonia.Interactivity/InteractiveExtensions.cs index 07e4029240..e6c93e26b2 100644 --- a/src/Avalonia.Interactivity/InteractiveExtensions.cs +++ b/src/Avalonia.Interactivity/InteractiveExtensions.cs @@ -2,8 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections.Generic; -using System.Linq; +using System.Reactive.Disposables; using System.Reactive.Linq; namespace Avalonia.Interactivity @@ -13,6 +12,28 @@ namespace Avalonia.Interactivity /// public static class InteractiveExtensions { + /// + /// Adds a handler for the specified routed event and returns a disposable that can terminate the event subscription. + /// + /// The type of the event's args. + /// Target for adding given event handler. + /// The routed event. + /// The handler. + /// The routing strategies to listen to. + /// Whether handled events should also be listened for. + /// A disposable that terminates the event subscription. + public static IDisposable AddDisposableHandler(this IInteractive o, RoutedEvent routedEvent, + EventHandler handler, + RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, + bool handledEventsToo = false) where TEventArgs : RoutedEventArgs + { + o.AddHandler(routedEvent, handler, routes, handledEventsToo); + + return Disposable.Create( + (instance: o, handler, routedEvent), + state => state.instance.RemoveHandler(state.routedEvent, state.handler)); + } + /// /// Gets an observable for a . /// @@ -30,7 +51,10 @@ namespace Avalonia.Interactivity bool handledEventsToo = false) where TEventArgs : RoutedEventArgs { - return Observable.Create(x => o.AddHandler( + o = o ?? throw new ArgumentNullException(nameof(o)); + routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent)); + + return Observable.Create(x => o.AddDisposableHandler( routedEvent, (_, e) => x.OnNext(e), routes, diff --git a/src/Avalonia.Interactivity/RoutedEvent.cs b/src/Avalonia.Interactivity/RoutedEvent.cs index 55d9e61d87..e515efd3b4 100644 --- a/src/Avalonia.Interactivity/RoutedEvent.cs +++ b/src/Avalonia.Interactivity/RoutedEvent.cs @@ -25,10 +25,14 @@ namespace Avalonia.Interactivity Type eventArgsType, Type ownerType) { - Contract.Requires(name != null); - Contract.Requires(eventArgsType != null); - Contract.Requires(ownerType != null); - Contract.Requires(typeof(RoutedEventArgs).IsAssignableFrom(eventArgsType)); + name = name ?? throw new ArgumentNullException(nameof(name)); + eventArgsType = eventArgsType ?? throw new ArgumentNullException(nameof(name)); + ownerType = ownerType ?? throw new ArgumentNullException(nameof(name)); + + if (!typeof(RoutedEventArgs).IsAssignableFrom(eventArgsType)) + { + throw new InvalidCastException("eventArgsType must be derived from RoutedEventArgs."); + } EventArgsType = eventArgsType; Name = name; @@ -44,6 +48,8 @@ namespace Avalonia.Interactivity public RoutingStrategies RoutingStrategies { get; } + public bool HasRaisedSubscriptions => _raised.HasObservers; + public IObservable<(object, RoutedEventArgs)> Raised => _raised; public IObservable RouteFinished => _routeFinished; @@ -52,7 +58,7 @@ namespace Avalonia.Interactivity RoutingStrategies routingStrategy) where TEventArgs : RoutedEventArgs { - Contract.Requires(name != null); + name = name ?? throw new ArgumentNullException(nameof(name)); var routedEvent = new RoutedEvent(name, routingStrategy, typeof(TOwner)); RoutedEventRegistry.Instance.Register(typeof(TOwner), routedEvent); @@ -65,7 +71,7 @@ namespace Avalonia.Interactivity Type ownerType) where TEventArgs : RoutedEventArgs { - Contract.Requires(name != null); + name = name ?? throw new ArgumentNullException(nameof(name)); var routedEvent = new RoutedEvent(name, routingStrategy, ownerType); RoutedEventRegistry.Instance.Register(ownerType, routedEvent); @@ -108,8 +114,6 @@ namespace Avalonia.Interactivity public RoutedEvent(string name, RoutingStrategies routingStrategies, Type ownerType) : base(name, routingStrategies, typeof(TEventArgs), ownerType) { - Contract.Requires(name != null); - Contract.Requires(ownerType != null); } [Obsolete("Use overload taking Action.")] diff --git a/src/Avalonia.Interactivity/RoutedEventArgs.cs b/src/Avalonia.Interactivity/RoutedEventArgs.cs index 05bbf7b6a3..e00393322d 100644 --- a/src/Avalonia.Interactivity/RoutedEventArgs.cs +++ b/src/Avalonia.Interactivity/RoutedEventArgs.cs @@ -11,12 +11,12 @@ namespace Avalonia.Interactivity { } - public RoutedEventArgs(RoutedEvent routedEvent) + public RoutedEventArgs(RoutedEvent? routedEvent) { RoutedEvent = routedEvent; } - public RoutedEventArgs(RoutedEvent routedEvent, IInteractive source) + public RoutedEventArgs(RoutedEvent? routedEvent, IInteractive? source) { RoutedEvent = routedEvent; Source = source; @@ -24,10 +24,10 @@ namespace Avalonia.Interactivity public bool Handled { get; set; } - public RoutedEvent RoutedEvent { get; set; } + public RoutedEvent? RoutedEvent { get; set; } public RoutingStrategies Route { get; set; } - public IInteractive Source { get; set; } + public IInteractive? Source { get; set; } } } diff --git a/src/Avalonia.Interactivity/RoutedEventRegistry.cs b/src/Avalonia.Interactivity/RoutedEventRegistry.cs index 34c970a806..0111b115e6 100644 --- a/src/Avalonia.Interactivity/RoutedEventRegistry.cs +++ b/src/Avalonia.Interactivity/RoutedEventRegistry.cs @@ -32,8 +32,8 @@ namespace Avalonia.Interactivity /// public void Register(Type type, RoutedEvent @event) { - Contract.Requires(type != null); - Contract.Requires(@event != null); + type = type ?? throw new ArgumentNullException(nameof(type)); + @event = @event ?? throw new ArgumentNullException(nameof(@event)); if (!_registeredRoutedEvents.TryGetValue(type, out var list)) { @@ -66,7 +66,7 @@ namespace Avalonia.Interactivity /// All routed events registered with the provided type. public IReadOnlyList GetRegistered(Type type) { - Contract.Requires(type != null); + type = type ?? throw new ArgumentNullException(nameof(type)); if (_registeredRoutedEvents.TryGetValue(type, out var events)) { diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index c757576017..73ec81ce57 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -68,9 +68,9 @@ namespace Avalonia.Native _native.CanResize = value; } - public void SetSystemDecorations(bool enabled) + public void SetSystemDecorations(Controls.SystemDecorations enabled) { - _native.HasDecorations = enabled; + _native.HasDecorations = (Interop.SystemDecorations)enabled; } public void SetTitleBarColor (Avalonia.Media.Color color) diff --git a/src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs b/src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs index c884b6ac43..7906a29cb5 100644 --- a/src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs +++ b/src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs @@ -14,6 +14,7 @@ namespace Avalonia.Styling.Activators { private readonly IList _match; private readonly IAvaloniaReadOnlyList _classes; + private NotifyCollectionChangedEventHandler? _classesChangedHandler; public StyleClassActivator(IAvaloniaReadOnlyList classes, IList match) { @@ -21,6 +22,9 @@ namespace Avalonia.Styling.Activators _match = match; } + private NotifyCollectionChangedEventHandler ClassesChangedHandler => + _classesChangedHandler ??= ClassesChanged; + public static bool AreClassesMatching(IReadOnlyList classes, IList toMatch) { int remainingMatches = toMatch.Count; @@ -51,16 +55,15 @@ namespace Avalonia.Styling.Activators return remainingMatches == 0; } - protected override void Initialize() { PublishNext(IsMatching()); - _classes.CollectionChanged += ClassesChanged; + _classes.CollectionChanged += ClassesChangedHandler; } protected override void Deinitialize() { - _classes.CollectionChanged -= ClassesChanged; + _classes.CollectionChanged -= ClassesChangedHandler; } private void ClassesChanged(object sender, NotifyCollectionChangedEventArgs e) diff --git a/src/Avalonia.Themes.Default/MenuItem.xaml b/src/Avalonia.Themes.Default/MenuItem.xaml index 769ab893bf..314416cda0 100644 --- a/src/Avalonia.Themes.Default/MenuItem.xaml +++ b/src/Avalonia.Themes.Default/MenuItem.xaml @@ -1,6 +1,10 @@ - + + + + +