diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..9a0da4aa9b --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,17 @@ +This template is not intended to be prescriptive, but to help us review pull requests it would be useful if you included as much of the following information as possible: + +- What does the pull request do? +- What is the current behavior? +- What is the updated/expected behavior with this PR? +- How was the solution implemented (if it's not obvious)? + +Checklist: + +- [ ] Added unit tests (if possible)? +- [ ] Added XML documentation to any related classes? +- [ ] Consider submitting a PR to https://github.com/AvaloniaUI/Avaloniaui.net with user documentation + +If the pull request fixes issue(s) list them like this: + +Fixes #123 +Fixes #456 \ No newline at end of file diff --git a/Avalonia.sln b/Avalonia.sln index 7cf2cf3b8a..88914fe188 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.27130.2024 +VisualStudioVersion = 15.0.27130.2027 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Base", "src\Avalonia.Base\Avalonia.Base.csproj", "{B09B78D8-9B26-48B0-9149-D64A2F120F3F}" EndProject @@ -11,7 +11,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Layout", "src\Aval EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Windows", "Windows", "{B39A8919-9F95-48FE-AD7B-76E08B509888}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Win32", "src\Windows\Avalonia.Win32\Avalonia.Win32.csproj", "{811A76CF-1CF6-440F-963B-BBE31BD72A82}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Win32", "src\Windows\Avalonia.Win32\Avalonia.Win32.csproj", "{811A76CF-1CF6-440F-963B-BBE31BD72A82}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Direct2D1", "src\Windows\Avalonia.Direct2D1\Avalonia.Direct2D1.csproj", "{3E908F67-5543-4879-A1DC-08EACE79B3CD}" EndProject @@ -126,10 +126,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RenderTest", "samples\Rende EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Android", "samples\ControlCatalog.Android\ControlCatalog.Android.csproj", "{29132311-1848-4FD6-AE0C-4FF841151BD3}" EndProject -Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Avalonia.Win32.Shared", "src\Windows\Avalonia.Win32\Avalonia.Win32.Shared.shproj", "{9DEFC6B7-845B-4D8F-AFC0-D32BF0032B8C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Win32.NetStandard", "src\Windows\Avalonia.Win32.NetStandard\Avalonia.Win32.NetStandard.csproj", "{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DotNetCoreRuntime", "src\Avalonia.DotNetCoreRuntime\Avalonia.DotNetCoreRuntime.csproj", "{7863EA94-F0FB-4386-BF8C-E5BFA761560A}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia", "src\Skia\Avalonia.Skia\Avalonia.Skia.csproj", "{7D2D3083-71DD-4CC9-8907-39A0D86FB322}" @@ -196,14 +192,11 @@ 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 = 4 - src\Windows\Avalonia.Win32\Avalonia.Win32.Shared.projitems*{40759a76-d0f2-464e-8000-6ff0f5c4bd7c}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{4488ad85-1495-4809-9aa4-ddfe0a48527e}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{4a1abb09-9047-4bd5-a4ad-a055e52c5ee0}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{7863ea94-f0fb-4386-bf8c-e5bfa761560a}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{7b92af71-6287-4693-9dcb-bd5b6e927e23}*SharedItemsImports = 4 src\Shared\RenderHelpers\RenderHelpers.projitems*{7d2d3083-71dd-4cc9-8907-39a0d86fb322}*SharedItemsImports = 4 - src\Windows\Avalonia.Win32\Avalonia.Win32.Shared.projitems*{811a76cf-1cf6-440f-963b-bbe31bd72a82}*SharedItemsImports = 4 - src\Windows\Avalonia.Win32\Avalonia.Win32.Shared.projitems*{9defc6b7-845b-4d8f-afc0-d32bf0032b8c}*SharedItemsImports = 13 tests\Avalonia.RenderTests\Avalonia.RenderTests.projitems*{dabfd304-d6a4-4752-8123-c2ccf7ac7831}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{e4d9629c-f168-4224-3f51-a5e482ffbc42}*SharedItemsImports = 13 EndGlobalSection @@ -369,6 +362,7 @@ Global {811A76CF-1CF6-440F-963B-BBE31BD72A82}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU {811A76CF-1CF6-440F-963B-BBE31BD72A82}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU {811A76CF-1CF6-440F-963B-BBE31BD72A82}.Debug|NetCoreOnly.ActiveCfg = Debug|Any CPU + {811A76CF-1CF6-440F-963B-BBE31BD72A82}.Debug|NetCoreOnly.Build.0 = Debug|Any CPU {811A76CF-1CF6-440F-963B-BBE31BD72A82}.Debug|x86.ActiveCfg = Debug|Any CPU {811A76CF-1CF6-440F-963B-BBE31BD72A82}.Debug|x86.Build.0 = Debug|Any CPU {811A76CF-1CF6-440F-963B-BBE31BD72A82}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -378,6 +372,7 @@ Global {811A76CF-1CF6-440F-963B-BBE31BD72A82}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {811A76CF-1CF6-440F-963B-BBE31BD72A82}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {811A76CF-1CF6-440F-963B-BBE31BD72A82}.Release|NetCoreOnly.ActiveCfg = Release|Any CPU + {811A76CF-1CF6-440F-963B-BBE31BD72A82}.Release|NetCoreOnly.Build.0 = Release|Any CPU {811A76CF-1CF6-440F-963B-BBE31BD72A82}.Release|x86.ActiveCfg = Release|Any CPU {811A76CF-1CF6-440F-963B-BBE31BD72A82}.Release|x86.Build.0 = Release|Any CPU {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU @@ -1994,46 +1989,6 @@ Global {29132311-1848-4FD6-AE0C-4FF841151BD3}.Release|x86.ActiveCfg = Release|Any CPU {29132311-1848-4FD6-AE0C-4FF841151BD3}.Release|x86.Build.0 = Release|Any CPU {29132311-1848-4FD6-AE0C-4FF841151BD3}.Release|x86.Deploy.0 = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Ad-Hoc|NetCoreOnly.ActiveCfg = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Ad-Hoc|NetCoreOnly.Build.0 = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Ad-Hoc|x86.ActiveCfg = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Ad-Hoc|x86.Build.0 = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.AppStore|Any CPU.Build.0 = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.AppStore|iPhone.Build.0 = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.AppStore|NetCoreOnly.ActiveCfg = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.AppStore|NetCoreOnly.Build.0 = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.AppStore|x86.ActiveCfg = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.AppStore|x86.Build.0 = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Debug|iPhone.Build.0 = Debug|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Debug|NetCoreOnly.ActiveCfg = Debug|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Debug|NetCoreOnly.Build.0 = Debug|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Debug|x86.ActiveCfg = Debug|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Debug|x86.Build.0 = Debug|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Release|Any CPU.Build.0 = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Release|iPhone.ActiveCfg = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Release|iPhone.Build.0 = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Release|NetCoreOnly.ActiveCfg = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Release|NetCoreOnly.Build.0 = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Release|x86.ActiveCfg = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Release|x86.Build.0 = Release|Any CPU {7863EA94-F0FB-4386-BF8C-E5BFA761560A}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {7863EA94-F0FB-4386-BF8C-E5BFA761560A}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {7863EA94-F0FB-4386-BF8C-E5BFA761560A}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -2623,8 +2578,6 @@ Global {C7A69145-60B6-4882-97D6-A3921DD43978} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} {F1FDC5B0-4654-416F-AE69-E3E9BBD87801} = {9B9E3891-2366-4253-A952-D08BCEB71098} {29132311-1848-4FD6-AE0C-4FF841151BD3} = {9B9E3891-2366-4253-A952-D08BCEB71098} - {9DEFC6B7-845B-4D8F-AFC0-D32BF0032B8C} = {B39A8919-9F95-48FE-AD7B-76E08B509888} - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C} = {B39A8919-9F95-48FE-AD7B-76E08B509888} {7D2D3083-71DD-4CC9-8907-39A0D86FB322} = {3743B0F2-CC41-4F14-A8C8-267F579BF91E} {BB1F7BB5-6AD4-4776-94D9-C09D0A972658} = {B9894058-278A-46B5-B6ED-AD613FCC03B3} {39D7B147-1A5B-47C2-9D01-21FB7C47C4B3} = {9B9E3891-2366-4253-A952-D08BCEB71098} diff --git a/build/Rx.props b/build/Rx.props index 323026f5e2..7078e31195 100644 --- a/build/Rx.props +++ b/build/Rx.props @@ -5,6 +5,5 @@ - diff --git a/build/System.Drawing.Common.props b/build/System.Drawing.Common.props new file mode 100644 index 0000000000..a568152bbd --- /dev/null +++ b/build/System.Drawing.Common.props @@ -0,0 +1,5 @@ + + + + + diff --git a/packages.cake b/packages.cake index bc290fce22..17411aef4c 100644 --- a/packages.cake +++ b/packages.cake @@ -370,14 +370,13 @@ public class Packages new NuGetPackSettings() { Id = "Avalonia.Win32", - Dependencies = new [] + Dependencies = new DependencyBuilder(this) { new NuSpecDependency() { Id = "Avalonia", Version = parameters.Version } - }, + }.Deps(new string[]{null}, "System.Drawing.Common"), Files = new [] { - new NuSpecContent { Source = "Avalonia.Win32/bin/" + parameters.DirSuffix + "/Avalonia.Win32.dll", Target = "lib/net45" }, - new NuSpecContent { Source = "Avalonia.Win32.NetStandard/bin/" + parameters.DirSuffix + "/netstandard2.0/Avalonia.Win32.dll", Target = "lib/netstandard2.0" } + new NuSpecContent { Source = "Avalonia.Win32/bin/" + parameters.DirSuffix + "/netstandard2.0/Avalonia.Win32.dll", Target = "lib/netstandard2.0" } }, BasePath = context.Directory("./src/Windows"), OutputDirectory = parameters.NugetRoot diff --git a/parameters.cake b/parameters.cake index c727b3107f..e224cce151 100644 --- a/parameters.cake +++ b/parameters.cake @@ -97,7 +97,7 @@ public class Parameters else { // Use AssemblyVersion with Build as version - Version += "-build" + context.EnvironmentVariable("APPVEYOR_BUILD_NUMBER") + "-alpha"; + Version += "-build" + context.EnvironmentVariable("APPVEYOR_BUILD_NUMBER") + "-beta"; } } diff --git a/readme.md b/readme.md index 906c3a4b5c..2b26cbdd1a 100644 --- a/readme.md +++ b/readme.md @@ -2,15 +2,15 @@ # Avalonia -| Gitter Chat | Windows Build Status | Linux/Mac Build Status | -|---|---|---| -| [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) | [![Build status](https://ci.appveyor.com/api/projects/status/hubk3k0w9idyibfg/branch/master?svg=true)](https://ci.appveyor.com/project/AvaloniaUI/Avalonia/branch/master) | [![Build Status](https://travis-ci.org/AvaloniaUI/Avalonia.svg?branch=master)](https://travis-ci.org/AvaloniaUI/Avalonia) | +| Gitter Chat | Windows Build Status | Linux/Mac Build Status | Open Collective | +|---|---|---|---| +| [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) | [![Build status](https://ci.appveyor.com/api/projects/status/hubk3k0w9idyibfg/branch/master?svg=true)](https://ci.appveyor.com/project/AvaloniaUI/Avalonia/branch/master) | [![Build Status](https://travis-ci.org/AvaloniaUI/Avalonia.svg?branch=master)](https://travis-ci.org/AvaloniaUI/Avalonia) | [![Backers on Open Collective](https://opencollective.com/Avalonia/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/Avalonia/sponsors/badge.svg)](#sponsors) | ## About Avalonia is a WPF-inspired cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of OSs: Windows (.NET Framework, .NET Core), Linux (GTK), MacOS, Android and iOS. -Avalonia is now in alpha. This means that framework is now at a stage where you can have a play and hopefully create simple applications. There's still a lot missing, and you *will* find bugs, and the API *will* change, but this represents the first time where we've made it somewhat easy to have a play and experiment with the framework. +**Avalonia is currently in beta** which means that the framework is generally usable for writing applications, but there may be some bugs and breaking changes as we continue development. | Control catalog | Desktop platforms | Mobile platforms | |---|---|---| @@ -35,16 +35,46 @@ https://ci.appveyor.com/project/AvaloniaUI/Avalonia/branch/master/artifacts ## Documentation -As mentioned above, Avalonia is still in alpha and as such there's not much documentation yet. You can take a look at the [getting started page](http://avaloniaui.net/tutorial/gettingstarted) 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). +As mentioned above, Avalonia is still in beta and as such there's not much documentation yet. 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). -There's also a high-level [architecture document](http://avaloniaui.net/spec/architecture) that is currently a little bit out of date, and I've also started writing blog posts on Avalonia at http://grokys.github.io/. +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/. Contributions are always welcome! ## Building and Using -See the [build instructions here](http://avaloniaui.net/guidelines/build). +See the [build instructions here](http://avaloniaui.net/contributing/build). ## Contributing -Please read the [contribution guidelines](http://avaloniaui.net/guidelines/contributing) before submitting a pull request. +Please read the [contribution guidelines](http://avaloniaui.net/contributing/contributing) before submitting a pull request. + +### Contributors + +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/BindingTest/MainWindow.xaml b/samples/BindingTest/MainWindow.xaml index b0a4a5b7ed..4eb45e07c5 100644 --- a/samples/BindingTest/MainWindow.xaml +++ b/samples/BindingTest/MainWindow.xaml @@ -1,4 +1,5 @@ @@ -6,6 +7,9 @@ + + + @@ -40,6 +44,10 @@ + + diff --git a/samples/ControlCatalog.Desktop/Program.cs b/samples/ControlCatalog.Desktop/Program.cs index b151cabf43..a2048005a4 100644 --- a/samples/ControlCatalog.Desktop/Program.cs +++ b/samples/ControlCatalog.Desktop/Program.cs @@ -10,6 +10,7 @@ namespace ControlCatalog { internal class Program { + [STAThread] static void Main(string[] args) { // TODO: Make this work with GTK/Skia/Cairo depending on command-line args diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 346535d39d..b45a93455e 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -9,8 +9,10 @@ namespace ControlCatalog.NetCore { static class Program { + static void Main(string[] args) { + Thread.CurrentThread.TrySetApartmentState(ApartmentState.STA); if (args.Contains("--wait-for-attach")) { Console.WriteLine("Attach debugger and use 'Set next statement'"); diff --git a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj index 77f38d3bd7..c1c5cdcaf7 100644 --- a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj +++ b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj @@ -170,15 +170,14 @@ Avalonia.Themes.Default - {D2D3083-71DD-4CC9-8907-39A0D86FB322} + {7d2d3083-71dd-4cc9-8907-39a0d86fb322} Avalonia.Skia - false - false {d0a739b9-3c68-4ba6-a328-41606954b6bd} ControlCatalog + \ No newline at end of file diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index 0b4463ddb7..b8a8479a49 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -1,177 +1,17 @@  - netstandard2.0 - False - false + netstandard2.0 - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - Designer - - - Designer - - - Designer - - - Designer - - - Designer - - - Designer - - - Designer - - - Designer - - - Designer - - - Designer - - - - Designer - - - Designer - - - Designer - - - Designer - - - Designer - - - Designer - - - Designer - - - Designer - - - Designer - - - Designer - - - - - App.xaml - - - MainView.xaml - - - DecoratedWindow.xaml - - - MainWindow.xaml - - - DialogsPage.xaml - - - BorderPage.xaml - - - ButtonPage.xaml - - - CalendarPage.xaml - - - CanvasPage.xaml - - - CarouselPage.xaml - - - ContextMenuPage.xaml - - - CheckBoxPage.xaml - - - DropDownPage.xaml - - - ExpanderPage.xaml - - - ImagePage.xaml - - - LayoutTransformControlPage.xaml + + %(Filename) - - MenuPage.xaml - - - ProgressBarPage.xaml - - - RadioButtonPage.xaml - - - SliderPage.xaml - - - TreeViewPage.xaml - - - TextBoxPage.xaml - - - ToolTipPage.xaml - - - - - - - - - - - - + Designer + + @@ -188,20 +28,6 @@ - - - - - - Designer - - - - - MSBuild:Compile - - - - + \ No newline at end of file diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index e15dd7d69e..377871f658 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -5,18 +5,23 @@ + + + + + @@ -24,4 +29,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml new file mode 100644 index 0000000000..943fadf100 --- /dev/null +++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml @@ -0,0 +1,59 @@ + + + AutoCompleteBox + A control into which the user can input text + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs new file mode 100644 index 0000000000..6f3b8361cd --- /dev/null +++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs @@ -0,0 +1,143 @@ +using Avalonia.Controls; +using Avalonia.LogicalTree; +using Avalonia.Markup; +using Avalonia.Markup.Xaml; +using Avalonia.Markup.Xaml.Data; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace ControlCatalog.Pages +{ + public class AutoCompleteBoxPage : UserControl + { + public class StateData + { + public string Name { get; private set; } + public string Abbreviation { get; private set; } + public string Capital { get; private set; } + + public StateData(string name, string abbreviatoin, string capital) + { + Name = name; + Abbreviation = abbreviatoin; + Capital = capital; + } + + public override string ToString() + { + return Name; + } + } + + private StateData[] BuildAllStates() + { + return new StateData[] + { + new StateData("Alabama","AL","Montgomery"), + new StateData("Alaska","AK","Juneau"), + new StateData("Arizona","AZ","Phoenix"), + new StateData("Arkansas","AR","Little Rock"), + new StateData("California","CA","Sacramento"), + new StateData("Colorado","CO","Denver"), + new StateData("Connecticut","CT","Hartford"), + new StateData("Delaware","DE","Dover"), + new StateData("Florida","FL","Tallahassee"), + new StateData("Georgia","GA","Atlanta"), + new StateData("Hawaii","HI","Honolulu"), + new StateData("Idaho","ID","Boise"), + new StateData("Illinois","IL","Springfield"), + new StateData("Indiana","IN","Indianapolis"), + new StateData("Iowa","IA","Des Moines"), + new StateData("Kansas","KS","Topeka"), + new StateData("Kentucky","KY","Frankfort"), + new StateData("Louisiana","LA","Baton Rouge"), + new StateData("Maine","ME","Augusta"), + new StateData("Maryland","MD","Annapolis"), + new StateData("Massachusetts","MA","Boston"), + new StateData("Michigan","MI","Lansing"), + new StateData("Minnesota","MN","St. Paul"), + new StateData("Mississippi","MS","Jackson"), + new StateData("Missouri","MO","Jefferson City"), + new StateData("Montana","MT","Helena"), + new StateData("Nebraska","NE","Lincoln"), + new StateData("Nevada","NV","Carson City"), + new StateData("New Hampshire","NH","Concord"), + new StateData("New Jersey","NJ","Trenton"), + new StateData("New Mexico","NM","Santa Fe"), + new StateData("New York","NY","Albany"), + new StateData("North Carolina","NC","Raleigh"), + new StateData("North Dakota","ND","Bismarck"), + new StateData("Ohio","OH","Columbus"), + new StateData("Oklahoma","OK","Oklahoma City"), + new StateData("Oregon","OR","Salem"), + new StateData("Pennsylvania","PA","Harrisburg"), + new StateData("Rhode Island","RI","Providence"), + new StateData("South Carolina","SC","Columbia"), + new StateData("South Dakota","SD","Pierre"), + new StateData("Tennessee","TN","Nashville"), + new StateData("Texas","TX","Austin"), + new StateData("Utah","UT","Salt Lake City"), + new StateData("Vermont","VT","Montpelier"), + new StateData("Virginia","VA","Richmond"), + new StateData("Washington","WA","Olympia"), + new StateData("West Virginia","WV","Charleston"), + new StateData("Wisconsin","WI","Madison"), + new StateData("Wyoming","WY","Cheyenne"), + }; + } + public StateData[] States { get; private set; } + + public AutoCompleteBoxPage() + { + this.InitializeComponent(); + + States = BuildAllStates(); + + foreach (AutoCompleteBox box in GetAllAutoCompleteBox()) + { + box.Items = States; + } + + var converter = new FuncMultiValueConverter(parts => + { + return String.Format("{0} ({1})", parts.ToArray()); + }); + var binding = new MultiBinding { Converter = converter }; + binding.Bindings.Add(new Binding("Name")); + binding.Bindings.Add(new Binding("Abbreviation")); + + var multibindingBox = this.FindControl("MultiBindingBox"); + multibindingBox.ValueMemberBinding = binding; + + var asyncBox = this.FindControl("AsyncBox"); + asyncBox.AsyncPopulator = PopulateAsync; + } + private IEnumerable GetAllAutoCompleteBox() + { + return + this.GetLogicalDescendants() + .OfType(); + } + + private bool StringContains(string str, string query) + { + return str.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0; + } + private async Task> PopulateAsync(string searchText, CancellationToken cancellationToken) + { + await Task.Delay(TimeSpan.FromSeconds(1.5), cancellationToken); + + return + States.Where(data => StringContains(data.Name, searchText) || StringContains(data.Capital, searchText)) + .ToList(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml b/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml new file mode 100644 index 0000000000..1797fb48bc --- /dev/null +++ b/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml @@ -0,0 +1,24 @@ + + + + ButtonSpinner + The ButtonSpinner control allows you to add button spinners to any element and then respond to the Spin event to manipulate that element. + + + AllowSpin + ShowButtonSpinner + + + + + + + + + + \ No newline at end of file diff --git a/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml.cs b/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml.cs new file mode 100644 index 0000000000..1f753ab3ea --- /dev/null +++ b/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml.cs @@ -0,0 +1,54 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages +{ + public class ButtonSpinnerPage : UserControl + { + public ButtonSpinnerPage() + { + this.InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void OnSpin(object sender, SpinEventArgs e) + { + var spinner = (ButtonSpinner)sender; + var txtBox = (TextBlock)spinner.Content; + + int value = Array.IndexOf(_mountains, txtBox.Text); + if (e.Direction == SpinDirection.Increase) + value++; + else + value--; + + if (value < 0) + value = _mountains.Length - 1; + else if (value >= _mountains.Length) + value = 0; + + txtBox.Text = _mountains[value]; + } + + private readonly string[] _mountains = new[] + { + "Everest", + "K2 (Mount Godwin Austen)", + "Kangchenjunga", + "Lhotse", + "Makalu", + "Cho Oyu", + "Dhaulagiri", + "Manaslu", + "Nanga Parbat", + "Annapurna" + }; + } +} diff --git a/samples/ControlCatalog/Pages/DatePickerPage.xaml b/samples/ControlCatalog/Pages/DatePickerPage.xaml new file mode 100644 index 0000000000..92cfa7e178 --- /dev/null +++ b/samples/ControlCatalog/Pages/DatePickerPage.xaml @@ -0,0 +1,46 @@ + + + DatePicker + A control for selecting dates with a calendar drop-down + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/ControlCatalog/Pages/DatePickerPage.xaml.cs b/samples/ControlCatalog/Pages/DatePickerPage.xaml.cs new file mode 100644 index 0000000000..ef01887c9e --- /dev/null +++ b/samples/ControlCatalog/Pages/DatePickerPage.xaml.cs @@ -0,0 +1,36 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using System; + +namespace ControlCatalog.Pages +{ + public class DatePickerPage : UserControl + { + public DatePickerPage() + { + InitializeComponent(); + + var dp1 = this.FindControl("DatePicker1"); + var dp2 = this.FindControl("DatePicker2"); + var dp3 = this.FindControl("DatePicker3"); + var dp4 = this.FindControl("DatePicker4"); + var dp5 = this.FindControl("DatePicker5"); + + dp1.SelectedDate = DateTime.Today; + dp2.SelectedDate = DateTime.Today.AddDays(10); + dp3.SelectedDate = DateTime.Today.AddDays(20); + dp5.SelectedDate = DateTime.Today; + + dp4.TemplateApplied += (s, e) => + { + dp4.BlackoutDates.AddDatesInPast(); + }; + + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml b/samples/ControlCatalog/Pages/DragAndDropPage.xaml new file mode 100644 index 0000000000..af679d2f9a --- /dev/null +++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml @@ -0,0 +1,19 @@ + + + Drag+Drop + Example of Drag+Drop capabilities + + + + Drag Me + + + Drop some text or files here + + + + \ No newline at end of file diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs new file mode 100644 index 0000000000..718f21314e --- /dev/null +++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs @@ -0,0 +1,71 @@ +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Markup.Xaml; +using System; +using System.Collections.Generic; +using System.Text; + +namespace ControlCatalog.Pages +{ + public class DragAndDropPage : UserControl + { + private TextBlock _DropState; + private TextBlock _DragState; + private Border _DragMe; + private int DragCount = 0; + + public DragAndDropPage() + { + this.InitializeComponent(); + + _DragMe.PointerPressed += DoDrag; + + AddHandler(DragDrop.DropEvent, Drop); + AddHandler(DragDrop.DragOverEvent, DragOver); + } + + private async void DoDrag(object sender, Avalonia.Input.PointerPressedEventArgs e) + { + DataObject dragData = new DataObject(); + dragData.Set(DataFormats.Text, $"You have dragged text {++DragCount} times"); + + var result = await DragDrop.DoDragDrop(dragData, DragDropEffects.Copy); + switch(result) + { + case DragDropEffects.Copy: + _DragState.Text = "The text was copied"; break; + case DragDropEffects.Link: + _DragState.Text = "The text was linked"; break; + case DragDropEffects.None: + _DragState.Text = "The drag operation was canceled"; break; + } + } + + private void DragOver(object sender, DragEventArgs e) + { + // Only allow Copy or Link as Drop Operations. + e.DragEffects = e.DragEffects & (DragDropEffects.Copy | DragDropEffects.Link); + + // Only allow if the dragged data contains text or filenames. + if (!e.Data.Contains(DataFormats.Text) && !e.Data.Contains(DataFormats.FileNames)) + e.DragEffects = DragDropEffects.None; + } + + private void Drop(object sender, DragEventArgs e) + { + if (e.Data.Contains(DataFormats.Text)) + _DropState.Text = e.Data.GetText(); + else if (e.Data.Contains(DataFormats.FileNames)) + _DropState.Text = string.Join(Environment.NewLine, e.Data.GetFileNames()); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + + _DropState = this.Find("DropState"); + _DragState = this.Find("DragState"); + _DragMe = this.Find("DragMe"); + } + } +} diff --git a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml new file mode 100644 index 0000000000..a5c911f47d --- /dev/null +++ b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml @@ -0,0 +1,80 @@ + + + Numeric up-down control + Numeric up-down control provides a TextBox with button spinners that allow incrementing and decrementing numeric values by using the spinner buttons, keyboard up/down arrows, or mouse wheel. + + Features: + + + ShowButtonSpinner: + + + IsReadOnly: + + + AllowSpin: + + + ClipValueToMinMax: + + + + + FormatString: + + + + + + + + + + + + + ButtonSpinnerLocation: + + + CultureInfo: + + + Watermark: + + + Text: + + + + Minimum: + + + Maximum: + + + Increment: + + + Value: + + + + + + + Usage of NumericUpDown: + + + + + \ No newline at end of file diff --git a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs new file mode 100644 index 0000000000..92da64d87e --- /dev/null +++ b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Markup.Xaml; +using ReactiveUI; + +namespace ControlCatalog.Pages +{ + public class NumericUpDownPage : UserControl + { + public NumericUpDownPage() + { + this.InitializeComponent(); + var viewModel = new NumbersPageViewModel(); + DataContext = viewModel; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + } + + public class NumbersPageViewModel : ReactiveObject + { + private IList _formats; + private FormatObject _selectedFormat; + private IList _spinnerLocations; + + public NumbersPageViewModel() + { + SelectedFormat = Formats.FirstOrDefault(); + } + + public IList Formats + { + get + { + return _formats ?? (_formats = new List() + { + new FormatObject() {Name = "Currency", Value = "C2"}, + new FormatObject() {Name = "Fixed point", Value = "F2"}, + new FormatObject() {Name = "General", Value = "G"}, + new FormatObject() {Name = "Number", Value = "N"}, + new FormatObject() {Name = "Percent", Value = "P"}, + new FormatObject() {Name = "Degrees", Value = "{0:N2} °"}, + }); + } + } + + public IList SpinnerLocations + { + get + { + if (_spinnerLocations == null) + { + _spinnerLocations = new List(); + foreach (Location value in Enum.GetValues(typeof(Location))) + { + _spinnerLocations.Add(value); + } + } + return _spinnerLocations ; + } + } + + public IList Cultures { get; } = new List() + { + new CultureInfo("en-US"), + new CultureInfo("en-GB"), + new CultureInfo("fr-FR"), + new CultureInfo("ar-DZ"), + new CultureInfo("zh-CN"), + new CultureInfo("cs-CZ") + }; + + public FormatObject SelectedFormat + { + get { return _selectedFormat; } + set { this.RaiseAndSetIfChanged(ref _selectedFormat, value); } + } + } + + public class FormatObject + { + public string Value { get; set; } + public string Name { get; set; } + } +} diff --git a/samples/ControlCatalog/Properties/AssemblyInfo.cs b/samples/ControlCatalog/Properties/AssemblyInfo.cs deleted file mode 100644 index 30c069d7d8..0000000000 --- a/samples/ControlCatalog/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("ControlCatalog")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("ControlCatalog")] -[assembly: AssemblyCopyright("Copyright © 2015")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("61bec86c-f307-4295-b5b8-9428610d7d55")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj b/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj index 4271d05f91..e0f3e92c74 100644 --- a/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj +++ b/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj @@ -17,7 +17,9 @@ - + + PreserveNewest + diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/PopupImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/PopupImpl.cs index 0b683239fb..78f744cea0 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/PopupImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/PopupImpl.cs @@ -36,7 +36,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform _clientSize = value; UpdateParams(); } - + + public void SetMinMaxSize(Size minSize, Size maxSize) + { + } + public IScreenImpl Screen { get; } public Point Position diff --git a/src/Avalonia.Base/AttachedProperty.cs b/src/Avalonia.Base/AttachedProperty.cs index 9d4d40bfef..fdb04b6dfc 100644 --- a/src/Avalonia.Base/AttachedProperty.cs +++ b/src/Avalonia.Base/AttachedProperty.cs @@ -9,7 +9,7 @@ namespace Avalonia /// An attached avalonia property. /// /// The type of the property's value. - public class AttachedProperty : StyledPropertyBase + public class AttachedProperty : StyledProperty { /// /// Initializes a new instance of the class. @@ -35,11 +35,10 @@ namespace Avalonia /// /// The owner type. /// The property. - public StyledProperty AddOwner() where TOwner : IAvaloniaObject + public new AttachedProperty AddOwner() where TOwner : IAvaloniaObject { - var result = new StyledProperty(this, typeof(TOwner)); - AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), result); - return result; + AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), this); + return this; } } } diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index a46d567d28..4ab813333d 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -12,7 +12,6 @@ using Avalonia.Diagnostics; using Avalonia.Logging; using Avalonia.Threading; using Avalonia.Utilities; -using System.Reactive.Concurrency; namespace Avalonia { @@ -218,11 +217,6 @@ namespace Avalonia } else { - if (!AvaloniaPropertyRegistry.Instance.IsRegistered(this, property)) - { - ThrowNotRegistered(property); - } - return GetValueInternal(property); } } @@ -377,11 +371,6 @@ namespace Avalonia { PriorityValue v; - if (!AvaloniaPropertyRegistry.Instance.IsRegistered(this, property)) - { - ThrowNotRegistered(property); - } - if (!_values.TryGetValue(property, out v)) { v = CreatePriorityValue(property); @@ -804,11 +793,6 @@ namespace Avalonia var originalValue = value; - if (!AvaloniaPropertyRegistry.Instance.IsRegistered(this, property)) - { - ThrowNotRegistered(property); - } - if (!TypeUtilities.TryConvertImplicit(property.PropertyType, value, out value)) { throw new ArgumentException(string.Format( @@ -836,18 +820,32 @@ namespace Avalonia } /// - /// Given a returns a registered avalonia property that is - /// equal or throws if not found. + /// Given a direct property, returns a registered avalonia property that is equivalent or + /// throws if not found. /// /// The property. /// The registered property. - public AvaloniaProperty GetRegistered(AvaloniaProperty property) + private AvaloniaProperty GetRegistered(AvaloniaProperty property) { - var result = AvaloniaPropertyRegistry.Instance.FindRegistered(this, property); + var direct = property as IDirectPropertyAccessor; + + if (direct == null) + { + throw new AvaloniaInternalException( + "AvaloniaObject.GetRegistered should only be called for direct properties"); + } + + if (property.OwnerType.IsAssignableFrom(GetType())) + { + return property; + } + + var result = AvaloniaPropertyRegistry.Instance.GetRegistered(this) + .FirstOrDefault(x => x == property); if (result == null) { - ThrowNotRegistered(property); + throw new ArgumentException($"Property '{property.Name} not registered on '{this.GetType()}"); } return result; @@ -898,15 +896,5 @@ namespace Avalonia value, priority); } - - /// - /// Throws an exception indicating that the specified property is not registered on this - /// object. - /// - /// The property - private void ThrowNotRegistered(AvaloniaProperty p) - { - throw new ArgumentException($"Property '{p.Name} not registered on '{this.GetType()}"); - } } } diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index fb78e3b2a0..f7dabd3a43 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -311,7 +311,9 @@ namespace Avalonia defaultBindingMode: defaultBindingMode); var result = new AttachedProperty(name, typeof(TOwner), metadata, inherits); - AvaloniaPropertyRegistry.Instance.Register(typeof(THost), result); + var registry = AvaloniaPropertyRegistry.Instance; + registry.Register(typeof(TOwner), result); + registry.RegisterAttached(typeof(THost), result); return result; } @@ -344,7 +346,9 @@ namespace Avalonia defaultBindingMode: defaultBindingMode); var result = new AttachedProperty(name, ownerType, metadata, inherits); - AvaloniaPropertyRegistry.Instance.Register(typeof(THost), result); + var registry = AvaloniaPropertyRegistry.Instance; + registry.Register(ownerType, result); + registry.RegisterAttached(typeof(THost), result); return result; } diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index ec1643427b..c0a4ace6ed 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; using System.Runtime.CompilerServices; namespace Avalonia @@ -14,23 +13,14 @@ namespace Avalonia /// public class AvaloniaPropertyRegistry { - /// - /// The registered properties by type. - /// private readonly Dictionary> _registered = new Dictionary>(); - - /// - /// The registered properties by type cached values to increase performance. - /// - private readonly Dictionary> _registeredCache = - new Dictionary>(); - - /// - /// The registered attached properties by owner type. - /// private readonly Dictionary> _attached = new Dictionary>(); + private readonly Dictionary> _registeredCache = + new Dictionary>(); + private readonly Dictionary> _attachedCache = + new Dictionary>(); /// /// Gets the instance @@ -39,51 +29,68 @@ namespace Avalonia = new AvaloniaPropertyRegistry(); /// - /// Gets all attached s registered by an owner. + /// Gets all non-attached s registered on a type. /// - /// The owner type. + /// The type. /// A collection of definitions. - public IEnumerable GetAttached(Type ownerType) + public IEnumerable GetRegistered(Type type) { - Dictionary inner; + Contract.Requires(type != null); + + if (_registeredCache.TryGetValue(type, out var result)) + { + return result; + } - // Ensure the type's static ctor has been run. - RuntimeHelpers.RunClassConstructor(ownerType.TypeHandle); + var t = type; + result = new List(); - if (_attached.TryGetValue(ownerType, out inner)) + while (t != null) { - return inner.Values; + // Ensure the type's static ctor has been run. + RuntimeHelpers.RunClassConstructor(t.TypeHandle); + + if (_registered.TryGetValue(t, out var registered)) + { + result.AddRange(registered.Values); + } + + t = t.BaseType; } - return Enumerable.Empty(); + _registeredCache.Add(type, result); + return result; } /// - /// Gets all s registered on a type. + /// Gets all attached s registered on a type. /// /// The type. /// A collection of definitions. - public IEnumerable GetRegistered(Type type) + public IEnumerable GetRegisteredAttached(Type type) { Contract.Requires(type != null); - while (type != null) + if (_attachedCache.TryGetValue(type, out var result)) { - // Ensure the type's static ctor has been run. - RuntimeHelpers.RunClassConstructor(type.TypeHandle); + return result; + } - Dictionary inner; + var t = type; + result = new List(); - if (_registered.TryGetValue(type, out inner)) + while (t != null) + { + if (_attached.TryGetValue(t, out var attached)) { - foreach (var p in inner) - { - yield return p.Value; - } + result.AddRange(attached.Values); } - type = type.GetTypeInfo().BaseType; + t = t.BaseType; } + + _attachedCache.Add(type, result); + return result; } /// @@ -99,142 +106,92 @@ namespace Avalonia } /// - /// Finds a registered on a type. + /// Finds a registered non-attached property on a type by name. /// /// The type. - /// The property. - /// The registered property or null if not found. - /// - /// Calling AddOwner on a AvaloniaProperty creates a new AvaloniaProperty that is a - /// different object but is equal according to . - /// - public AvaloniaProperty FindRegistered(Type type, AvaloniaProperty property) + /// The property name. + /// + /// The registered property or null if no matching property found. + /// + /// + /// The property name contains a '.'. + /// + public AvaloniaProperty FindRegistered(Type type, string name) { - Type currentType = type; - Dictionary cache; - AvaloniaProperty result; + Contract.Requires(type != null); + Contract.Requires(name != null); - if (_registeredCache.TryGetValue(type, out cache)) + if (name.Contains('.')) { - if (cache.TryGetValue(property.Id, out result)) - { - return result; - } + throw new InvalidOperationException("Attached properties not supported."); } - while (currentType != null) - { - Dictionary inner; - - if (_registered.TryGetValue(currentType, out inner)) - { - if (inner.TryGetValue(property.Id, out result)) - { - if (cache == null) - { - _registeredCache[type] = cache = new Dictionary(); - } - - cache[property.Id] = result; - - return result; - } - } - - currentType = currentType.GetTypeInfo().BaseType; - } - - return null; + return GetRegistered(type).FirstOrDefault(x => x.Name == name); } /// - /// Finds registered on an object. + /// Finds a registered non-attached property on a type by name. /// /// The object. - /// The property. - /// The registered property or null if not found. - /// - /// Calling AddOwner on a AvaloniaProperty creates a new AvaloniaProperty that is a - /// different object but is equal according to . - /// - public AvaloniaProperty FindRegistered(object o, AvaloniaProperty property) + /// The property name. + /// + /// The registered property or null if no matching property found. + /// + /// + /// The property name contains a '.'. + /// + public AvaloniaProperty FindRegistered(AvaloniaObject o, string name) { - return FindRegistered(o.GetType(), property); + Contract.Requires(o != null); + Contract.Requires(name != null); + + return FindRegistered(o.GetType(), name); } /// - /// Finds a registered property on a type by name. + /// Finds a registered attached property on a type by name. /// /// The type. - /// - /// The property name. If an attached property it should be in the form - /// "OwnerType.PropertyName". - /// + /// The owner type. + /// The property name. /// /// The registered property or null if no matching property found. /// - public AvaloniaProperty FindRegistered(Type type, string name) + /// + /// The property name contains a '.'. + /// + public AvaloniaProperty FindRegisteredAttached(Type type, Type ownerType, string name) { Contract.Requires(type != null); + Contract.Requires(ownerType != null); Contract.Requires(name != null); - var parts = name.Split('.'); - var types = GetImplementedTypes(type).ToList(); - - if (parts.Length < 1 || parts.Length > 2) + if (name.Contains('.')) { - throw new ArgumentException("Invalid property name."); + throw new InvalidOperationException("Attached properties not supported."); } - string propertyName; - var results = GetRegistered(type); - - if (parts.Length == 1) - { - propertyName = parts[0]; - results = results.Where(x => !x.IsAttached || types.Contains(x.OwnerType.Name)); - } - else - { - if (!types.Contains(parts[0])) - { - results = results.Where(x => x.OwnerType.Name == parts[0]); - } - - propertyName = parts[1]; - } - - return results.FirstOrDefault(x => x.Name == propertyName); + return GetRegisteredAttached(type).FirstOrDefault(x => x.Name == name); } /// - /// Finds a registered property on an object by name. + /// Finds a registered non-attached property on a type by name. /// /// The object. - /// - /// The property name. If an attached property it should be in the form - /// "OwnerType.PropertyName". - /// + /// The owner type. + /// The property name. /// /// The registered property or null if no matching property found. /// - public AvaloniaProperty FindRegistered(AvaloniaObject o, string name) + /// + /// The property name contains a '.'. + /// + public AvaloniaProperty FindRegisteredAttached(AvaloniaObject o, Type ownerType, string name) { - return FindRegistered(o.GetType(), name); - } + Contract.Requires(o != null); + Contract.Requires(name != null); - /// - /// Returns a type and all its base types. - /// - /// The type. - /// The type and all its base types. - private IEnumerable GetImplementedTypes(Type type) - { - while (type != null) - { - yield return type.Name; - type = type.GetTypeInfo().BaseType; - } + return FindRegisteredAttached(o.GetType(), ownerType, name); } /// @@ -245,7 +202,11 @@ namespace Avalonia /// True if the property is registered, otherwise false. public bool IsRegistered(Type type, AvaloniaProperty property) { - return FindRegistered(type, property) != null; + Contract.Requires(type != null); + Contract.Requires(property != null); + + return Instance.GetRegistered(type).Any(x => x == property) || + Instance.GetRegisteredAttached(type).Any(x => x == property); } /// @@ -256,6 +217,9 @@ namespace Avalonia /// True if the property is registered, otherwise false. public bool IsRegistered(object o, AvaloniaProperty property) { + Contract.Requires(o != null); + Contract.Requires(property != null); + return IsRegistered(o.GetType(), property); } @@ -274,34 +238,53 @@ namespace Avalonia Contract.Requires(type != null); Contract.Requires(property != null); - Dictionary inner; - - if (!_registered.TryGetValue(type, out inner)) + if (!_registered.TryGetValue(type, out var inner)) { inner = new Dictionary(); + inner.Add(property.Id, property); _registered.Add(type, inner); } - - if (!inner.ContainsKey(property.Id)) + else if (!inner.ContainsKey(property.Id)) { inner.Add(property.Id, property); } + + _registeredCache.Clear(); + } - if (property.IsAttached) + /// + /// Registers an attached on a type. + /// + /// The type. + /// The property. + /// + /// You won't usually want to call this method directly, instead use the + /// + /// method. + /// + public void RegisterAttached(Type type, AvaloniaProperty property) + { + Contract.Requires(type != null); + Contract.Requires(property != null); + + if (!property.IsAttached) { - if (!_attached.TryGetValue(property.OwnerType, out inner)) - { - inner = new Dictionary(); - _attached.Add(property.OwnerType, inner); - } + throw new InvalidOperationException( + "Cannot register a non-attached property as attached."); + } - if (!inner.ContainsKey(property.Id)) - { - inner.Add(property.Id, property); - } + if (!_attached.TryGetValue(type, out var inner)) + { + inner = new Dictionary(); + inner.Add(property.Id, property); + _attached.Add(type, inner); + } + else + { + inner.Add(property.Id, property); } - _registeredCache.Clear(); + _attachedCache.Clear(); } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Collections/AvaloniaDictionary.cs b/src/Avalonia.Base/Collections/AvaloniaDictionary.cs index b90dccf74e..84ac85d3db 100644 --- a/src/Avalonia.Base/Collections/AvaloniaDictionary.cs +++ b/src/Avalonia.Base/Collections/AvaloniaDictionary.cs @@ -117,7 +117,7 @@ namespace Avalonia.Collections _inner = new Dictionary(); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Count")); - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"Item[]")); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Item[]")); if (CollectionChanged != null) @@ -222,4 +222,4 @@ namespace Avalonia.Collections } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs b/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs index 54cd132b95..b27b06a277 100644 --- a/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs +++ b/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs @@ -34,14 +34,18 @@ namespace Avalonia.Collections /// /// An action called when the collection is reset. /// + /// + /// Indicates if a weak subscription should be used to track changes to the collection. + /// /// A disposable used to terminate the subscription. public static IDisposable ForEachItem( this IAvaloniaReadOnlyList collection, Action added, Action removed, - Action reset) + Action reset, + bool weakSubscription = false) { - return collection.ForEachItem((_, i) => added(i), (_, i) => removed(i), reset); + return collection.ForEachItem((_, i) => added(i), (_, i) => removed(i), reset, weakSubscription); } /// @@ -63,12 +67,16 @@ namespace Avalonia.Collections /// An action called when the collection is reset. This will be followed by calls to /// for each item present in the collection after the reset. /// + /// + /// Indicates if a weak subscription should be used to track changes to the collection. + /// /// A disposable used to terminate the subscription. public static IDisposable ForEachItem( this IAvaloniaReadOnlyList collection, Action added, Action removed, - Action reset) + Action reset, + bool weakSubscription = false) { void Add(int index, IList items) { @@ -118,9 +126,17 @@ namespace Avalonia.Collections }; Add(0, (IList)collection); - collection.CollectionChanged += handler; - return Disposable.Create(() => collection.CollectionChanged -= handler); + if (weakSubscription) + { + return collection.WeakSubscribe(handler); + } + else + { + collection.CollectionChanged += handler; + + return Disposable.Create(() => collection.CollectionChanged -= handler); + } } public static IAvaloniaReadOnlyList CreateDerivedList( diff --git a/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs b/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs new file mode 100644 index 0000000000..d295cb91ce --- /dev/null +++ b/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs @@ -0,0 +1,127 @@ +// 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; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using Avalonia.Utilities; + +namespace Avalonia.Collections +{ + public static class NotifyCollectionChangedExtensions + { + /// + /// Gets a weak observable for the CollectionChanged event. + /// + /// The collection. + /// An observable. + public static IObservable GetWeakCollectionChangedObservable( + this INotifyCollectionChanged collection) + { + Contract.Requires(collection != null); + + return new WeakCollectionChangedObservable(new WeakReference(collection)); + } + + /// + /// Subcribes to the CollectionChanged event using a weak subscription. + /// + /// The collection. + /// + /// An action called when the collection event is raised. + /// + /// A disposable used to terminate the subscription. + public static IDisposable WeakSubscribe( + this INotifyCollectionChanged collection, + NotifyCollectionChangedEventHandler handler) + { + Contract.Requires(collection != null); + Contract.Requires(handler != null); + + return + collection.GetWeakCollectionChangedObservable() + .Subscribe(e => handler.Invoke(collection, e)); + } + + /// + /// Subcribes to the CollectionChanged event using a weak subscription. + /// + /// The collection. + /// + /// An action called when the collection event is raised. + /// + /// A disposable used to terminate the subscription. + public static IDisposable WeakSubscribe( + this INotifyCollectionChanged collection, + Action handler) + { + Contract.Requires(collection != null); + Contract.Requires(handler != null); + + return + collection.GetWeakCollectionChangedObservable() + .Subscribe(handler); + } + + private class WeakCollectionChangedObservable : ObservableBase, + IWeakSubscriber + { + private WeakReference _sourceReference; + private readonly Subject _changed = new Subject(); + + private int _count; + + public WeakCollectionChangedObservable(WeakReference source) + { + _sourceReference = source; + } + + public void OnEvent(object sender, NotifyCollectionChangedEventArgs e) + { + _changed.OnNext(e); + } + + protected override IDisposable SubscribeCore(IObserver observer) + { + if (_sourceReference.TryGetTarget(out INotifyCollectionChanged instance)) + { + if (_count++ == 0) + { + WeakSubscriptionManager.Subscribe( + instance, + nameof(instance.CollectionChanged), + this); + } + + return Observable.Using(() => Disposable.Create(DecrementCount), _ => _changed) + .Subscribe(observer); + } + else + { + _changed.OnCompleted(); + observer.OnCompleted(); + return Disposable.Empty; + } + } + + private void DecrementCount() + { + if (--_count == 0) + { + if (_sourceReference.TryGetTarget(out INotifyCollectionChanged instance)) + { + WeakSubscriptionManager.Unsubscribe( + instance, + nameof(instance.CollectionChanged), + this); + } + } + } + } + } +} diff --git a/src/Avalonia.Base/DirectProperty.cs b/src/Avalonia.Base/DirectProperty.cs index 8352528285..1ce73c20ba 100644 --- a/src/Avalonia.Base/DirectProperty.cs +++ b/src/Avalonia.Base/DirectProperty.cs @@ -75,6 +75,9 @@ namespace Avalonia /// public Action Setter { get; } + /// + Type IDirectPropertyAccessor.Owner => typeof(TOwner); + /// /// Registers the direct property on another type. /// diff --git a/src/Avalonia.Base/IDirectPropertyAccessor.cs b/src/Avalonia.Base/IDirectPropertyAccessor.cs index 62aeef73c7..4f46652693 100644 --- a/src/Avalonia.Base/IDirectPropertyAccessor.cs +++ b/src/Avalonia.Base/IDirectPropertyAccessor.cs @@ -1,6 +1,8 @@ // 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 { /// @@ -14,6 +16,11 @@ namespace Avalonia /// bool IsReadOnly { get; } + /// + /// Gets the class that registered the property. + /// + Type Owner { get; } + /// /// Gets the value of the property on the instance. /// diff --git a/src/Avalonia.Base/Threading/Dispatcher.cs b/src/Avalonia.Base/Threading/Dispatcher.cs index 7d29a4f969..cf7acb3e8a 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.cs @@ -81,12 +81,14 @@ namespace Avalonia.Threading /// public Task InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal) { + Contract.Requires(action != null); return _jobRunner?.InvokeAsync(action, priority); } /// public void Post(Action action, DispatcherPriority priority = DispatcherPriority.Normal) { + Contract.Requires(action != null); _jobRunner?.Post(action, priority); } diff --git a/src/Avalonia.Base/Utilities/StringTokenizer.cs b/src/Avalonia.Base/Utilities/StringTokenizer.cs new file mode 100644 index 0000000000..2559e52932 --- /dev/null +++ b/src/Avalonia.Base/Utilities/StringTokenizer.cs @@ -0,0 +1,205 @@ +using System; +using System.Globalization; +using static System.Char; + +namespace Avalonia.Utilities +{ + public struct StringTokenizer : IDisposable + { + private const char DefaultSeparatorChar = ','; + + private readonly string _s; + private readonly int _length; + private readonly char _separator; + private readonly string _exceptionMessage; + private readonly IFormatProvider _formatProvider; + private int _index; + private int _tokenIndex; + private int _tokenLength; + + public StringTokenizer(string s, IFormatProvider formatProvider, string exceptionMessage = null) + : this(s, GetSeparatorFromFormatProvider(formatProvider), exceptionMessage) + { + _formatProvider = formatProvider; + } + + public StringTokenizer(string s, char separator = DefaultSeparatorChar, string exceptionMessage = null) + { + _s = s ?? throw new ArgumentNullException(nameof(s)); + _length = s?.Length ?? 0; + _separator = separator; + _exceptionMessage = exceptionMessage; + _formatProvider = CultureInfo.InvariantCulture; + _index = 0; + _tokenIndex = -1; + _tokenLength = 0; + + while (_index < _length && IsWhiteSpace(_s, _index)) + { + _index++; + } + } + + public string CurrentToken => _tokenIndex < 0 ? null : _s.Substring(_tokenIndex, _tokenLength); + + public void Dispose() + { + if (_index != _length) + { + throw GetFormatException(); + } + } + + public bool TryReadInt32(out Int32 result, char? separator = null) + { + var success = TryReadString(out var stringResult, separator); + result = success ? int.Parse(stringResult, _formatProvider) : 0; + return success; + } + + public int ReadInt32(char? separator = null) + { + if (!TryReadInt32(out var result, separator)) + { + throw GetFormatException(); + } + + return result; + } + + public bool TryReadDouble(out double result, char? separator = null) + { + var success = TryReadString(out var stringResult, separator); + result = success ? double.Parse(stringResult, _formatProvider) : 0; + return success; + } + + public double ReadDouble(char? separator = null) + { + if (!TryReadDouble(out var result, separator)) + { + throw GetFormatException(); + } + + return result; + } + + public bool TryReadString(out string result, char? separator = null) + { + var success = TryReadToken(separator ?? _separator); + result = CurrentToken; + return success; + } + + public string ReadString(char? separator = null) + { + if (!TryReadString(out var result, separator)) + { + throw GetFormatException(); + } + + return result; + } + + private bool TryReadToken(char separator) + { + _tokenIndex = -1; + + if (_index >= _length) + { + return false; + } + + var c = _s[_index]; + + var index = _index; + var length = 0; + + while (_index < _length) + { + c = _s[_index]; + + if (IsWhiteSpace(c) || c == separator) + { + break; + } + + _index++; + length++; + } + + SkipToNextToken(separator); + + _tokenIndex = index; + _tokenLength = length; + + if (_tokenLength < 1) + { + throw GetFormatException(); + } + + return true; + } + + private void SkipToNextToken(char separator) + { + if (_index < _length) + { + var c = _s[_index]; + + if (c != separator && !IsWhiteSpace(c)) + { + throw GetFormatException(); + } + + var length = 0; + + while (_index < _length) + { + c = _s[_index]; + + if (c == separator) + { + length++; + _index++; + + if (length > 1) + { + throw GetFormatException(); + } + } + else + { + if (!IsWhiteSpace(c)) + { + break; + } + + _index++; + } + } + + if (length > 0 && _index >= _length) + { + throw GetFormatException(); + } + } + } + + private FormatException GetFormatException() => + _exceptionMessage != null ? new FormatException(_exceptionMessage) : new FormatException(); + + private static char GetSeparatorFromFormatProvider(IFormatProvider provider) + { + var c = DefaultSeparatorChar; + + var formatInfo = NumberFormatInfo.GetInstance(provider); + if (formatInfo.NumberDecimalSeparator.Length > 0 && c == formatInfo.NumberDecimalSeparator[0]) + { + c = ';'; + } + + return c; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 06c1a8b4cc..6fdca557eb 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -2,16 +2,17 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Reactive.Concurrency; using System.Threading; using Avalonia.Controls; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Input.Platform; +using Avalonia.Input.Raw; using Avalonia.Layout; -using Avalonia.Rendering; +using Avalonia.Platform; using Avalonia.Styling; using Avalonia.Threading; -using System.Reactive.Concurrency; namespace Avalonia { @@ -234,7 +235,9 @@ namespace Avalonia .Bind().ToConstant(_styler) .Bind().ToSingleton() .Bind().ToConstant(this) - .Bind().ToConstant(AvaloniaScheduler.Instance); + .Bind().ToConstant(AvaloniaScheduler.Instance) + .Bind().ToConstant(DragDropDevice.Instance) + .Bind().ToTransient(); } } } diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs new file mode 100644 index 0000000000..8e801d606b --- /dev/null +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -0,0 +1,2726 @@ +// (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. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Collections; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Controls.Utils; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Threading; +using Avalonia.VisualTree; + +namespace Avalonia.Controls +{ + /// + /// Provides data for the + /// + /// event. + /// + public class PopulatedEventArgs : EventArgs + { + /// + /// Gets the list of possible matches added to the drop-down portion of + /// the + /// control. + /// + /// The list of possible matches added to the + /// . + public IEnumerable Data { get; private set; } + + /// + /// Initializes a new instance of the + /// . + /// + /// The list of possible matches added to the + /// drop-down portion of the + /// control. + public PopulatedEventArgs(IEnumerable data) + { + Data = data; + } + } + + /// + /// Provides data for the + /// + /// event. + /// + /// Stable + public class PopulatingEventArgs : CancelEventArgs + { + /// + /// Gets the text that is used to determine which items to display in + /// the + /// control. + /// + /// The text that is used to determine which items to display in + /// the . + public string Parameter { get; private set; } + + /// + /// Initializes a new instance of the + /// . + /// + /// The value of the + /// + /// property, which is used to filter items for the + /// control. + public PopulatingEventArgs(string parameter) + { + Parameter = parameter; + } + } + + /// + /// Represents the filter used by the + /// control to + /// determine whether an item is a possible match for the specified text. + /// + /// true to indicate is a possible match + /// for ; otherwise false. + /// The string used as the basis for filtering. + /// The item that is compared with the + /// parameter. + /// The type used for filtering the + /// . This type can + /// be either a string or an object. + /// Stable + public delegate bool AutoCompleteFilterPredicate(string search, T item); + + /// + /// Specifies how text in the text box portion of the + /// control is used + /// to filter items specified by the + /// + /// property for display in the drop-down. + /// + /// Stable + public enum AutoCompleteFilterMode + { + /// + /// Specifies that no filter is used. All items are returned. + /// + None = 0, + + /// + /// Specifies a culture-sensitive, case-insensitive filter where the + /// returned items start with the specified text. The filter uses the + /// + /// method, specifying + /// as + /// the string comparison criteria. + /// + StartsWith = 1, + + /// + /// Specifies a culture-sensitive, case-sensitive filter where the + /// returned items start with the specified text. The filter uses the + /// + /// method, specifying + /// as the string + /// comparison criteria. + /// + StartsWithCaseSensitive = 2, + + /// + /// Specifies an ordinal, case-insensitive filter where the returned + /// items start with the specified text. The filter uses the + /// + /// method, specifying + /// as the + /// string comparison criteria. + /// + StartsWithOrdinal = 3, + + /// + /// Specifies an ordinal, case-sensitive filter where the returned items + /// start with the specified text. The filter uses the + /// + /// method, specifying as + /// the string comparison criteria. + /// + StartsWithOrdinalCaseSensitive = 4, + + /// + /// Specifies a culture-sensitive, case-insensitive filter where the + /// returned items contain the specified text. + /// + Contains = 5, + + /// + /// Specifies a culture-sensitive, case-sensitive filter where the + /// returned items contain the specified text. + /// + ContainsCaseSensitive = 6, + + /// + /// Specifies an ordinal, case-insensitive filter where the returned + /// items contain the specified text. + /// + ContainsOrdinal = 7, + + /// + /// Specifies an ordinal, case-sensitive filter where the returned items + /// contain the specified text. + /// + ContainsOrdinalCaseSensitive = 8, + + /// + /// Specifies a culture-sensitive, case-insensitive filter where the + /// returned items equal the specified text. The filter uses the + /// + /// method, specifying + /// as + /// the search comparison criteria. + /// + Equals = 9, + + /// + /// Specifies a culture-sensitive, case-sensitive filter where the + /// returned items equal the specified text. The filter uses the + /// + /// method, specifying + /// as the string + /// comparison criteria. + /// + EqualsCaseSensitive = 10, + + /// + /// Specifies an ordinal, case-insensitive filter where the returned + /// items equal the specified text. The filter uses the + /// + /// method, specifying + /// as the + /// string comparison criteria. + /// + EqualsOrdinal = 11, + + /// + /// Specifies an ordinal, case-sensitive filter where the returned items + /// equal the specified text. The filter uses the + /// + /// method, specifying as + /// the string comparison criteria. + /// + EqualsOrdinalCaseSensitive = 12, + + /// + /// Specifies that a custom filter is used. This mode is used when the + /// + /// or + /// + /// properties are set. + /// + Custom = 13, + } + + /// + /// Represents a control that provides a text box for user input and a + /// drop-down that contains possible matches based on the input in the text + /// box. + /// + public class AutoCompleteBox : TemplatedControl + { + /// + /// Specifies the name of the selection adapter TemplatePart. + /// + private const string ElementSelectionAdapter = "PART_SelectionAdapter"; + + /// + /// Specifies the name of the Selector TemplatePart. + /// + private const string ElementSelector = "PART_SelectingItemsControl"; + + /// + /// Specifies the name of the Popup TemplatePart. + /// + private const string ElementPopup = "PART_Popup"; + + /// + /// The name for the text box part. + /// + private const string ElementTextBox = "PART_TextBox"; + + private IEnumerable _itemsEnumerable; + + /// + /// Gets or sets a local cached copy of the items data. + /// + private List _items; + + /// + /// Gets or sets the observable collection that contains references to + /// all of the items in the generated view of data that is provided to + /// the selection-style control adapter. + /// + private AvaloniaList _view; + + /// + /// Gets or sets a value to ignore a number of pending change handlers. + /// The value is decremented after each use. This is used to reset the + /// value of properties without performing any of the actions in their + /// change handlers. + /// + /// The int is important as a value because the TextBox + /// TextChanged event does not immediately fire, and this will allow for + /// nested property changes to be ignored. + private int _ignoreTextPropertyChange; + + /// + /// Gets or sets a value indicating whether to ignore calling a pending + /// change handlers. + /// + private bool _ignorePropertyChange; + + /// + /// Gets or sets a value indicating whether to ignore the selection + /// changed event. + /// + private bool _ignoreTextSelectionChange; + + /// + /// Gets or sets a value indicating whether to skip the text update + /// processing when the selected item is updated. + /// + private bool _skipSelectedItemTextUpdate; + + /// + /// Gets or sets the last observed text box selection start location. + /// + private int _textSelectionStart; + + /// + /// Gets or sets a value indicating whether the user initiated the + /// current populate call. + /// + private bool _userCalledPopulate; + + /// + /// A value indicating whether the popup has been opened at least once. + /// + private bool _popupHasOpened; + + /// + /// Gets or sets the DispatcherTimer used for the MinimumPopulateDelay + /// condition for auto completion. + /// + private DispatcherTimer _delayTimer; + + /// + /// Gets or sets a value indicating whether a read-only dependency + /// property change handler should allow the value to be set. This is + /// used to ensure that read-only properties cannot be changed via + /// SetValue, etc. + /// + private bool _allowWrite; + + /// + /// The TextBox template part. + /// + private TextBox _textBox; + private IDisposable _textBoxSubscriptions; + + /// + /// The SelectionAdapter. + /// + private ISelectionAdapter _adapter; + + /// + /// A control that can provide updated string values from a binding. + /// + private BindingEvaluator _valueBindingEvaluator; + + /// + /// A weak subscription for the collection changed event. + /// + private IDisposable _collectionChangeSubscription; + + private IMemberSelector _valueMemberSelector; + private Func>> _asyncPopulator; + private CancellationTokenSource _populationCancellationTokenSource; + + private bool _itemTemplateIsFromValueMemeberBinding = true; + private bool _settingItemTemplateFromValueMemeberBinding; + + private object _selectedItem; + private bool _isDropDownOpen; + private bool _isFocused = false; + + private string _text = string.Empty; + private string _searchText = string.Empty; + + private AutoCompleteFilterPredicate _itemFilter; + private AutoCompleteFilterPredicate _textFilter = AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith); + + public static readonly RoutedEvent SelectionChangedEvent = + RoutedEvent.Register(nameof(SelectionChanged), RoutingStrategies.Bubble, typeof(AutoCompleteBox)); + + public static readonly StyledProperty WatermarkProperty = + TextBox.WatermarkProperty.AddOwner(); + + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly StyledProperty MinimumPrefixLengthProperty = + AvaloniaProperty.Register( + nameof(MinimumPrefixLength), 1, + validate: ValidateMinimumPrefixLength); + + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly StyledProperty MinimumPopulateDelayProperty = + AvaloniaProperty.Register( + nameof(MinimumPopulateDelay), + TimeSpan.Zero, + validate: ValidateMinimumPopulateDelay); + + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly StyledProperty MaxDropDownHeightProperty = + AvaloniaProperty.Register( + nameof(MaxDropDownHeight), + double.PositiveInfinity, + validate: ValidateMaxDropDownHeight); + + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly StyledProperty IsTextCompletionEnabledProperty = + AvaloniaProperty.Register(nameof(IsTextCompletionEnabled)); + + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly StyledProperty ItemTemplateProperty = + AvaloniaProperty.Register(nameof(ItemTemplate)); + + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly DirectProperty IsDropDownOpenProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsDropDownOpen), + o => o.IsDropDownOpen, + (o, v) => o.IsDropDownOpen = v); + + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier the + /// + /// dependency property. + public static readonly DirectProperty SelectedItemProperty = + AvaloniaProperty.RegisterDirect( + nameof(SelectedItem), + o => o.SelectedItem, + (o, v) => o.SelectedItem = v); + + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly DirectProperty TextProperty = + AvaloniaProperty.RegisterDirect( + nameof(Text), + o => o.Text, + (o, v) => o.Text = v); + + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly DirectProperty SearchTextProperty = + AvaloniaProperty.RegisterDirect( + nameof(SearchText), + o => o.SearchText, + unsetValue: string.Empty); + + /// + /// Gets the identifier for the + /// + /// dependency property. + /// + public static readonly StyledProperty FilterModeProperty = + AvaloniaProperty.Register( + nameof(FilterMode), + defaultValue: AutoCompleteFilterMode.StartsWith, + validate: ValidateFilterMode); + + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly DirectProperty> ItemFilterProperty = + AvaloniaProperty.RegisterDirect>( + nameof(ItemFilter), + o => o.ItemFilter, + (o, v) => o.ItemFilter = v); + + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly DirectProperty> TextFilterProperty = + AvaloniaProperty.RegisterDirect>( + nameof(TextFilter), + o => o.TextFilter, + (o, v) => o.TextFilter = v, + unsetValue: AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith)); + + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly DirectProperty ItemsProperty = + AvaloniaProperty.RegisterDirect( + nameof(Items), + o => o.Items, + (o, v) => o.Items = v); + + public static readonly DirectProperty ValueMemberSelectorProperty = + AvaloniaProperty.RegisterDirect( + nameof(ValueMemberSelector), + o => o.ValueMemberSelector, + (o, v) => o.ValueMemberSelector = v); + + public static readonly DirectProperty>>> AsyncPopulatorProperty = + AvaloniaProperty.RegisterDirect>>>( + nameof(AsyncPopulator), + o => o.AsyncPopulator, + (o, v) => o.AsyncPopulator = v); + + private static int ValidateMinimumPrefixLength(AutoCompleteBox control, int value) + { + Contract.Requires(value >= -1); + + return value; + } + + private static TimeSpan ValidateMinimumPopulateDelay(AutoCompleteBox control, TimeSpan value) + { + Contract.Requires(value.TotalMilliseconds >= 0.0); + + return value; + } + + private static double ValidateMaxDropDownHeight(AutoCompleteBox control, double value) + { + Contract.Requires(value >= 0.0); + + return value; + } + + private static bool IsValidFilterMode(AutoCompleteFilterMode mode) + { + switch (mode) + { + case AutoCompleteFilterMode.None: + case AutoCompleteFilterMode.StartsWith: + case AutoCompleteFilterMode.StartsWithCaseSensitive: + case AutoCompleteFilterMode.StartsWithOrdinal: + case AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive: + case AutoCompleteFilterMode.Contains: + case AutoCompleteFilterMode.ContainsCaseSensitive: + case AutoCompleteFilterMode.ContainsOrdinal: + case AutoCompleteFilterMode.ContainsOrdinalCaseSensitive: + case AutoCompleteFilterMode.Equals: + case AutoCompleteFilterMode.EqualsCaseSensitive: + case AutoCompleteFilterMode.EqualsOrdinal: + case AutoCompleteFilterMode.EqualsOrdinalCaseSensitive: + case AutoCompleteFilterMode.Custom: + return true; + default: + return false; + } + } + private static AutoCompleteFilterMode ValidateFilterMode(AutoCompleteBox control, AutoCompleteFilterMode value) + { + Contract.Requires(IsValidFilterMode(value)); + + return value; + } + + /// + /// Handle the change of the IsEnabled property. + /// + /// The event data. + private void OnControlIsEnabledChanged(AvaloniaPropertyChangedEventArgs e) + { + bool isEnabled = (bool)e.NewValue; + if (!isEnabled) + { + IsDropDownOpen = false; + } + } + + /// + /// MinimumPopulateDelayProperty property changed handler. Any current + /// dispatcher timer will be stopped. The timer will not be restarted + /// until the next TextUpdate call by the user. + /// + /// Event arguments. + private void OnMinimumPopulateDelayChanged(AvaloniaPropertyChangedEventArgs e) + { + var newValue = (TimeSpan)e.NewValue; + + // Stop any existing timer + if (_delayTimer != null) + { + _delayTimer.Stop(); + + if (newValue == TimeSpan.Zero) + { + _delayTimer = null; + } + } + + if (newValue > TimeSpan.Zero) + { + // Create or clear a dispatcher timer instance + if (_delayTimer == null) + { + _delayTimer = new DispatcherTimer(); + _delayTimer.Tick += PopulateDropDown; + } + + // Set the new tick interval + _delayTimer.Interval = newValue; + } + } + + /// + /// IsDropDownOpenProperty property changed handler. + /// + /// Event arguments. + private void OnIsDropDownOpenChanged(AvaloniaPropertyChangedEventArgs e) + { + // Ignore the change if requested + if (_ignorePropertyChange) + { + _ignorePropertyChange = false; + return; + } + + bool oldValue = (bool)e.OldValue; + bool newValue = (bool)e.NewValue; + + if (newValue) + { + TextUpdated(Text, true); + } + else + { + ClosingDropDown(oldValue); + } + + UpdatePseudoClasses(); + } + + private void OnSelectedItemPropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + if (_ignorePropertyChange) + { + _ignorePropertyChange = false; + return; + } + + // Update the text display + if (_skipSelectedItemTextUpdate) + { + _skipSelectedItemTextUpdate = false; + } + else + { + OnSelectedItemChanged(e.NewValue); + } + + // Fire the SelectionChanged event + List removed = new List(); + if (e.OldValue != null) + { + removed.Add(e.OldValue); + } + + List added = new List(); + if (e.NewValue != null) + { + added.Add(e.NewValue); + } + + OnSelectionChanged(new SelectionChangedEventArgs(SelectionChangedEvent, removed, added)); + } + + /// + /// TextProperty property changed handler. + /// + /// Event arguments. + private void OnTextPropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + TextUpdated((string)e.NewValue, false); + } + + private void OnSearchTextPropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + if (_ignorePropertyChange) + { + _ignorePropertyChange = false; + return; + } + + // Ensure the property is only written when expected + if (!_allowWrite) + { + // Reset the old value before it was incorrectly written + _ignorePropertyChange = true; + SetValue(e.Property, e.OldValue); + + throw new InvalidOperationException("Cannot set read-only property SearchText."); + } + } + + /// + /// FilterModeProperty property changed handler. + /// + /// Event arguments. + private void OnFilterModePropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + AutoCompleteFilterMode mode = (AutoCompleteFilterMode)e.NewValue; + + // Sets the filter predicate for the new value + TextFilter = AutoCompleteSearch.GetFilter(mode); + } + + /// + /// ItemFilterProperty property changed handler. + /// + /// Event arguments. + private void OnItemFilterPropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + AutoCompleteFilterPredicate value = e.NewValue as AutoCompleteFilterPredicate; + + // If null, revert to the "None" predicate + if (value == null) + { + FilterMode = AutoCompleteFilterMode.None; + } + else + { + FilterMode = AutoCompleteFilterMode.Custom; + TextFilter = null; + } + } + + /// + /// ItemsSourceProperty property changed handler. + /// + /// Event arguments. + private void OnItemsPropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + OnItemsChanged((IEnumerable)e.NewValue); + } + + private void OnItemTemplatePropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + if (!_settingItemTemplateFromValueMemeberBinding) + _itemTemplateIsFromValueMemeberBinding = false; + } + private void OnValueMemberBindingChanged(IBinding value) + { + if(_itemTemplateIsFromValueMemeberBinding) + { + var template = + new FuncDataTemplate( + typeof(object), + o => + { + var control = new ContentControl(); + control.Bind(ContentControl.ContentProperty, value); + return control; + }); + + _settingItemTemplateFromValueMemeberBinding = true; + ItemTemplate = template; + _settingItemTemplateFromValueMemeberBinding = false; + } + } + + static AutoCompleteBox() + { + FocusableProperty.OverrideDefaultValue(true); + + MinimumPopulateDelayProperty.Changed.AddClassHandler(x => x.OnMinimumPopulateDelayChanged); + IsDropDownOpenProperty.Changed.AddClassHandler(x => x.OnIsDropDownOpenChanged); + SelectedItemProperty.Changed.AddClassHandler(x => x.OnSelectedItemPropertyChanged); + TextProperty.Changed.AddClassHandler(x => x.OnTextPropertyChanged); + SearchTextProperty.Changed.AddClassHandler(x => x.OnSearchTextPropertyChanged); + FilterModeProperty.Changed.AddClassHandler(x => x.OnFilterModePropertyChanged); + ItemFilterProperty.Changed.AddClassHandler(x => x.OnItemFilterPropertyChanged); + ItemsProperty.Changed.AddClassHandler(x => x.OnItemsPropertyChanged); + IsEnabledProperty.Changed.AddClassHandler(x => x.OnControlIsEnabledChanged); + } + + /// + /// Initializes a new instance of the + /// class. + /// + public AutoCompleteBox() + { + ClearView(); + } + + /// + /// Gets or sets the minimum number of characters required to be entered + /// in the text box before the + /// displays + /// possible matches. + /// matches. + /// + /// + /// The minimum number of characters to be entered in the text box + /// before the + /// displays possible matches. The default is 1. + /// + /// + /// If you set MinimumPrefixLength to -1, the AutoCompleteBox will + /// not provide possible matches. There is no maximum value, but + /// setting MinimumPrefixLength to value that is too large will + /// prevent the AutoCompleteBox from providing possible matches as well. + /// + public int MinimumPrefixLength + { + get { return GetValue(MinimumPrefixLengthProperty); } + set { SetValue(MinimumPrefixLengthProperty, value); } + } + + /// + /// Gets or sets a value indicating whether the first possible match + /// found during the filtering process will be displayed automatically + /// in the text box. + /// + /// + /// True if the first possible match found will be displayed + /// automatically in the text box; otherwise, false. The default is + /// false. + /// + public bool IsTextCompletionEnabled + { + get { return GetValue(IsTextCompletionEnabledProperty); } + set { SetValue(IsTextCompletionEnabledProperty, value); } + } + + /// + /// Gets or sets the used + /// to display each item in the drop-down portion of the control. + /// + /// The used to + /// display each item in the drop-down. The default is null. + /// + /// You use the ItemTemplate property to specify the visualization + /// of the data objects in the drop-down portion of the AutoCompleteBox + /// control. If your AutoCompleteBox is bound to a collection and you + /// do not provide specific display instructions by using a + /// DataTemplate, the resulting UI of each item is a string + /// representation of each object in the underlying collection. + /// + public IDataTemplate ItemTemplate + { + get { return GetValue(ItemTemplateProperty); } + set { SetValue(ItemTemplateProperty, value); } + } + + /// + /// Gets or sets the minimum delay, after text is typed + /// in the text box before the + /// control + /// populates the list of possible matches in the drop-down. + /// + /// The minimum delay, after text is typed in + /// the text box, but before the + /// populates + /// the list of possible matches in the drop-down. The default is 0. + public TimeSpan MinimumPopulateDelay + { + get { return GetValue(MinimumPopulateDelayProperty); } + set { SetValue(MinimumPopulateDelayProperty, value); } + } + + /// + /// Gets or sets the maximum height of the drop-down portion of the + /// control. + /// + /// The maximum height of the drop-down portion of the + /// control. + /// The default is . + /// The specified value is less than 0. + public double MaxDropDownHeight + { + get { return GetValue(MaxDropDownHeightProperty); } + set { SetValue(MaxDropDownHeightProperty, value); } + } + + /// + /// Gets or sets a value indicating whether the drop-down portion of + /// the control is open. + /// + /// + /// True if the drop-down is open; otherwise, false. The default is + /// false. + /// + public bool IsDropDownOpen + { + get { return _isDropDownOpen; } + set { SetAndRaise(IsDropDownOpenProperty, ref _isDropDownOpen, value); } + } + + /// + /// Gets or sets the that + /// is used to get the values for display in the text portion of + /// the + /// control. + /// + /// The object used + /// when binding to a collection property. + [AssignBinding] + public IBinding ValueMemberBinding + { + get { return _valueBindingEvaluator?.ValueBinding; } + set + { + if (ValueMemberBinding != value) + { + _valueBindingEvaluator = new BindingEvaluator(value); + OnValueMemberBindingChanged(value); + } + } + } + + /// + /// Gets or sets the MemberSelector that is used to get values for + /// display in the text portion of the + /// control. + /// + /// The MemberSelector that is used to get values for display in + /// the text portion of the + /// control. + public IMemberSelector ValueMemberSelector + { + get { return _valueMemberSelector; } + set { SetAndRaise(ValueMemberSelectorProperty, ref _valueMemberSelector, value); } + } + + /// + /// Gets or sets the selected item in the drop-down. + /// + /// The selected item in the drop-down. + /// + /// If the IsTextCompletionEnabled property is true and text typed by + /// the user matches an item in the ItemsSource collection, which is + /// then displayed in the text box, the SelectedItem property will be + /// a null reference. + /// + public object SelectedItem + { + get { return _selectedItem; } + set { SetAndRaise(SelectedItemProperty, ref _selectedItem, value); } + } + + /// + /// Gets or sets the text in the text box portion of the + /// control. + /// + /// The text in the text box portion of the + /// control. + public string Text + { + get { return _text; } + set { SetAndRaise(TextProperty, ref _text, value); } + } + + /// + /// Gets the text that is used to filter items in the + /// + /// item collection. + /// + /// The text that is used to filter items in the + /// + /// item collection. + /// + /// The SearchText value is typically the same as the + /// Text property, but is set after the TextChanged event occurs + /// and before the Populating event. + /// + public string SearchText + { + get { return _searchText; } + private set + { + try + { + _allowWrite = true; + SetAndRaise(SearchTextProperty, ref _searchText, value); + } + finally + { + _allowWrite = false; + } + } + } + + /// + /// Gets or sets how the text in the text box is used to filter items + /// specified by the + /// + /// property for display in the drop-down. + /// + /// One of the + /// + /// values The default is + /// . + /// The specified value is + /// not a valid + /// . + /// + /// Use the FilterMode property to specify how possible matches are + /// filtered. For example, possible matches can be filtered in a + /// predefined or custom way. The search mode is automatically set to + /// Custom if you set the ItemFilter property. + /// + public AutoCompleteFilterMode FilterMode + { + get { return GetValue(FilterModeProperty); } + set { SetValue(FilterModeProperty, value); } + } + + public string Watermark + { + get { return GetValue(WatermarkProperty); } + set { SetValue(WatermarkProperty, value); } + } + + /// + /// Gets or sets the custom method that uses user-entered text to filter + /// the items specified by the + /// + /// property for display in the drop-down. + /// + /// The custom method that uses the user-entered text to filter + /// the items specified by the + /// + /// property. The default is null. + /// + /// The filter mode is automatically set to Custom if you set the + /// ItemFilter property. + /// + public AutoCompleteFilterPredicate ItemFilter + { + get { return _itemFilter; } + set { SetAndRaise(ItemFilterProperty, ref _itemFilter, value); } + } + + /// + /// Gets or sets the custom method that uses the user-entered text to + /// filter items specified by the + /// + /// property in a text-based way for display in the drop-down. + /// + /// The custom method that uses the user-entered text to filter + /// items specified by the + /// + /// property in a text-based way for display in the drop-down. + /// + /// The search mode is automatically set to Custom if you set the + /// TextFilter property. + /// + public AutoCompleteFilterPredicate TextFilter + { + get { return _textFilter; } + set { SetAndRaise(TextFilterProperty, ref _textFilter, value); } + } + + public Func>> AsyncPopulator + { + get { return _asyncPopulator; } + set { SetAndRaise(AsyncPopulatorProperty, ref _asyncPopulator, value); } + } + + /// + /// Gets or sets a collection that is used to generate the items for the + /// drop-down portion of the + /// control. + /// + /// The collection that is used to generate the items of the + /// drop-down portion of the + /// control. + public IEnumerable Items + { + get { return _itemsEnumerable; } + set { SetAndRaise(ItemsProperty, ref _itemsEnumerable, value); } + } + + /// + /// Gets or sets the drop down popup control. + /// + private Popup DropDownPopup { get; set; } + + /// + /// Gets or sets the Text template part. + /// + private TextBox TextBox + { + get { return _textBox; } + set + { + _textBoxSubscriptions?.Dispose(); + _textBox = value; + + // Attach handlers + if (_textBox != null) + { + _textBoxSubscriptions = + _textBox.GetObservable(TextBox.TextProperty) + .Subscribe(_ => OnTextBoxTextChanged()); + + if (Text != null) + { + UpdateTextValue(Text); + } + } + } + } + + private int TextBoxSelectionStart + { + get + { + if (TextBox != null) + { + return Math.Min(TextBox.SelectionStart, TextBox.SelectionEnd); + } + else + { + return 0; + } + } + } + private int TextBoxSelectionLength + { + get + { + if (TextBox != null) + { + return Math.Abs(TextBox.SelectionEnd - TextBox.SelectionStart); + } + else + { + return 0; + } + } + } + + /// + /// Gets or sets the selection adapter used to populate the drop-down + /// with a list of selectable items. + /// + /// The selection adapter used to populate the drop-down with a + /// list of selectable items. + /// + /// You can use this property when you create an automation peer to + /// use with AutoCompleteBox or deriving from AutoCompleteBox to + /// create a custom control. + /// + protected ISelectionAdapter SelectionAdapter + { + get { return _adapter; } + set + { + if (_adapter != null) + { + _adapter.SelectionChanged -= OnAdapterSelectionChanged; + _adapter.Commit -= OnAdapterSelectionComplete; + _adapter.Cancel -= OnAdapterSelectionCanceled; + _adapter.Cancel -= OnAdapterSelectionComplete; + _adapter.Items = null; + } + + _adapter = value; + + if (_adapter != null) + { + _adapter.SelectionChanged += OnAdapterSelectionChanged; + _adapter.Commit += OnAdapterSelectionComplete; + _adapter.Cancel += OnAdapterSelectionCanceled; + _adapter.Cancel += OnAdapterSelectionComplete; + _adapter.Items = _view; + } + } + } + + /// + /// Returns the + /// part, if + /// possible. + /// + /// + /// A object, + /// if possible. Otherwise, null. + /// + protected virtual ISelectionAdapter GetSelectionAdapterPart(INameScope nameScope) + { + ISelectionAdapter adapter = null; + SelectingItemsControl selector = nameScope.Find(ElementSelector); + if (selector != null) + { + // Check if it is already an IItemsSelector + adapter = selector as ISelectionAdapter; + if (adapter == null) + { + // Built in support for wrapping a Selector control + adapter = new SelectingItemsControlSelectionAdapter(selector); + } + } + if (adapter == null) + { + adapter = nameScope.Find(ElementSelectionAdapter); + } + return adapter; + } + + /// + /// Builds the visual tree for the + /// control + /// when a new template is applied. + /// + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + { + + if (DropDownPopup != null) + { + DropDownPopup.Closed -= DropDownPopup_Closed; + DropDownPopup = null; + } + + // Set the template parts. Individual part setters remove and add + // any event handlers. + Popup popup = e.NameScope.Find(ElementPopup); + if (popup != null) + { + DropDownPopup = popup; + DropDownPopup.Closed += DropDownPopup_Closed; + } + + SelectionAdapter = GetSelectionAdapterPart(e.NameScope); + TextBox = e.NameScope.Find(ElementTextBox); + + // If the drop down property indicates that the popup is open, + // flip its value to invoke the changed handler. + if (IsDropDownOpen && DropDownPopup != null && !DropDownPopup.IsOpen) + { + OpeningDropDown(false); + } + + base.OnTemplateApplied(e); + } + + /// + /// Provides handling for the + /// event. + /// + /// A + /// that contains the event data. + protected override void OnKeyDown(KeyEventArgs e) + { + Contract.Requires(e != null); + + base.OnKeyDown(e); + + if (e.Handled || !IsEnabled) + { + return; + } + + // The drop down is open, pass along the key event arguments to the + // selection adapter. If it isn't handled by the adapter's logic, + // then we handle some simple navigation scenarios for controlling + // the drop down. + if (IsDropDownOpen) + { + if (SelectionAdapter != null) + { + SelectionAdapter.HandleKeyDown(e); + if (e.Handled) + { + return; + } + } + + if (e.Key == Key.Escape) + { + OnAdapterSelectionCanceled(this, new RoutedEventArgs()); + e.Handled = true; + } + } + else + { + // The drop down is not open, the Down key will toggle it open. + if (e.Key == Key.Down) + { + IsDropDownOpen = true; + e.Handled = true; + } + } + + // Standard drop down navigation + switch (e.Key) + { + case Key.F4: + IsDropDownOpen = !IsDropDownOpen; + e.Handled = true; + break; + + case Key.Enter: + OnAdapterSelectionComplete(this, new RoutedEventArgs()); + e.Handled = true; + break; + + default: + break; + } + } + + /// + /// Provides handling for the + /// event. + /// + /// A + /// that contains the event data. + protected override void OnGotFocus(GotFocusEventArgs e) + { + base.OnGotFocus(e); + FocusChanged(HasFocus()); + } + + /// + /// Provides handling for the + /// event. + /// + /// A + /// that contains the event data. + protected override void OnLostFocus(RoutedEventArgs e) + { + base.OnLostFocus(e); + FocusChanged(HasFocus()); + } + + /// + /// Determines whether the text box or drop-down portion of the + /// control has + /// focus. + /// + /// true to indicate the + /// has focus; + /// otherwise, false. + protected bool HasFocus() + { + IVisual focused = FocusManager.Instance.Current; + + while (focused != null) + { + if (object.ReferenceEquals(focused, this)) + { + return true; + } + + // This helps deal with popups that may not be in the same + // visual tree + IVisual parent = focused.GetVisualParent(); + if (parent == null) + { + // Try the logical parent. + IControl element = focused as IControl; + if (element != null) + { + parent = element.Parent; + } + } + focused = parent; + } + return false; + } + + /// + /// Handles the FocusChanged event. + /// + /// A value indicating whether the control + /// currently has the focus. + private void FocusChanged(bool hasFocus) + { + // The OnGotFocus & OnLostFocus are asynchronously and cannot + // reliably tell you that have the focus. All they do is let you + // know that the focus changed sometime in the past. To determine + // if you currently have the focus you need to do consult the + // FocusManager (see HasFocus()). + + bool wasFocused = _isFocused; + _isFocused = hasFocus; + + if (hasFocus) + { + + if (!wasFocused && TextBox != null && TextBoxSelectionLength <= 0) + { + TextBox.Focus(); + TextBox.SelectionStart = 0; + TextBox.SelectionEnd = TextBox.Text?.Length ?? 0; + } + } + else + { + IsDropDownOpen = false; + _userCalledPopulate = false; + ClearTextBoxSelection(); + } + + _isFocused = hasFocus; + } + + /// + /// Occurs when the text in the text box portion of the + /// changes. + /// + public event EventHandler TextChanged; + + /// + /// Occurs when the + /// is + /// populating the drop-down with possible matches based on the + /// + /// property. + /// + /// + /// If the event is canceled, by setting the PopulatingEventArgs.Cancel + /// property to true, the AutoCompleteBox will not automatically + /// populate the selection adapter contained in the drop-down. + /// In this case, if you want possible matches to appear, you must + /// provide the logic for populating the selection adapter. + /// + public event EventHandler Populating; + + /// + /// Occurs when the + /// has + /// populated the drop-down with possible matches based on the + /// + /// property. + /// + public event EventHandler Populated; + + /// + /// Occurs when the value of the + /// + /// property is changing from false to true. + /// + public event EventHandler DropDownOpening; + + /// + /// Occurs when the value of the + /// + /// property has changed from false to true and the drop-down is open. + /// + public event EventHandler DropDownOpened; + + /// + /// Occurs when the + /// + /// property is changing from true to false. + /// + public event EventHandler DropDownClosing; + + /// + /// Occurs when the + /// + /// property was changed from true to false and the drop-down is open. + /// + public event EventHandler DropDownClosed; + + /// + /// Occurs when the selected item in the drop-down portion of the + /// has + /// changed. + /// + public event EventHandler SelectionChanged + { + add { AddHandler(SelectionChangedEvent, value); } + remove { RemoveHandler(SelectionChangedEvent, value); } + } + + /// + /// Raises the + /// + /// event. + /// + /// A + /// that + /// contains the event data. + protected virtual void OnPopulating(PopulatingEventArgs e) + { + Populating?.Invoke(this, e); + } + + /// + /// Raises the + /// + /// event. + /// + /// A + /// + /// that contains the event data. + protected virtual void OnPopulated(PopulatedEventArgs e) + { + Populated?.Invoke(this, e); + } + + /// + /// Raises the + /// + /// event. + /// + /// A + /// + /// that contains the event data. + protected virtual void OnSelectionChanged(SelectionChangedEventArgs e) + { + RaiseEvent(e); + } + + /// + /// Raises the + /// + /// event. + /// + /// A + /// + /// that contains the event data. + protected virtual void OnDropDownOpening(CancelEventArgs e) + { + DropDownOpening?.Invoke(this, e); + } + + /// + /// Raises the + /// + /// event. + /// + /// A + /// + /// that contains the event data. + protected virtual void OnDropDownOpened(EventArgs e) + { + DropDownOpened?.Invoke(this, e); + } + + /// + /// Raises the + /// + /// event. + /// + /// A + /// + /// that contains the event data. + protected virtual void OnDropDownClosing(CancelEventArgs e) + { + DropDownClosing?.Invoke(this, e); + } + + /// + /// Raises the + /// + /// event. + /// + /// A + /// + /// which contains the event data. + protected virtual void OnDropDownClosed(EventArgs e) + { + DropDownClosed?.Invoke(this, e); + } + + /// + /// Raises the + /// + /// event. + /// + /// A + /// that contains the event data. + protected virtual void OnTextChanged(RoutedEventArgs e) + { + TextChanged?.Invoke(this, e); + } + + /// + /// Begin closing the drop-down. + /// + /// The original value. + private void ClosingDropDown(bool oldValue) + { + var args = new CancelEventArgs(); + OnDropDownClosing(args); + + if (args.Cancel) + { + _ignorePropertyChange = true; + SetValue(IsDropDownOpenProperty, oldValue); + } + else + { + CloseDropDown(); + } + + UpdatePseudoClasses(); + } + + /// + /// Begin opening the drop down by firing cancelable events, opening the + /// drop-down or reverting, depending on the event argument values. + /// + /// The original value, if needed for a revert. + private void OpeningDropDown(bool oldValue) + { + var args = new CancelEventArgs(); + + // Opening + OnDropDownOpening(args); + + if (args.Cancel) + { + _ignorePropertyChange = true; + SetValue(IsDropDownOpenProperty, oldValue); + } + else + { + OpenDropDown(); + } + + UpdatePseudoClasses(); + } + + /// + /// Connects to the DropDownPopup Closed event. + /// + /// The source object. + /// The event data. + private void DropDownPopup_Closed(object sender, EventArgs e) + { + // Force the drop down dependency property to be false. + if (IsDropDownOpen) + { + IsDropDownOpen = false; + } + + // Fire the DropDownClosed event + if (_popupHasOpened) + { + OnDropDownClosed(EventArgs.Empty); + } + } + + /// + /// Handles the timer tick when using a populate delay. + /// + /// The source object. + /// The event arguments. + private void PopulateDropDown(object sender, EventArgs e) + { + if (_delayTimer != null) + { + _delayTimer.Stop(); + } + + // Update the prefix/search text. + SearchText = Text; + + if(TryPopulateAsync(SearchText)) + { + return; + } + + // The Populated event enables advanced, custom filtering. The + // client needs to directly update the ItemsSource collection or + // call the Populate method on the control to continue the + // display process if Cancel is set to true. + PopulatingEventArgs populating = new PopulatingEventArgs(SearchText); + OnPopulating(populating); + if (!populating.Cancel) + { + PopulateComplete(); + } + } + private bool TryPopulateAsync(string searchText) + { + _populationCancellationTokenSource?.Cancel(false); + _populationCancellationTokenSource?.Dispose(); + _populationCancellationTokenSource = null; + + if(_asyncPopulator == null) + { + return false; + } + + _populationCancellationTokenSource = new CancellationTokenSource(); + var task = PopulateAsync(searchText, _populationCancellationTokenSource.Token); + if (task.Status == TaskStatus.Created) + task.Start(); + + return true; + } + private async Task PopulateAsync(string searchText, CancellationToken cancellationToken) + { + + try + { + IEnumerable result = await _asyncPopulator.Invoke(searchText, cancellationToken); + var resultList = result.ToList(); + + if (cancellationToken.IsCancellationRequested) + { + return; + } + + await Dispatcher.UIThread.InvokeAsync(() => + { + if (!cancellationToken.IsCancellationRequested) + { + Items = resultList; + PopulateComplete(); + } + }); + } + catch (TaskCanceledException) + { } + finally + { + _populationCancellationTokenSource?.Dispose(); + _populationCancellationTokenSource = null; + } + + } + + /// + /// Private method that directly opens the popup, checks the expander + /// button, and then fires the Opened event. + /// + private void OpenDropDown() + { + if (DropDownPopup != null) + { + DropDownPopup.IsOpen = true; + } + _popupHasOpened = true; + OnDropDownOpened(EventArgs.Empty); + } + + /// + /// Private method that directly closes the popup, flips the Checked + /// value, and then fires the Closed event. + /// + private void CloseDropDown() + { + if (_popupHasOpened) + { + if (SelectionAdapter != null) + { + SelectionAdapter.SelectedItem = null; + } + if (DropDownPopup != null) + { + DropDownPopup.IsOpen = false; + } + OnDropDownClosed(EventArgs.Empty); + } + } + + /// + /// Formats an Item for text comparisons based on Converter + /// and ConverterCulture properties. + /// + /// The object to format. + /// A value indicating whether to clear + /// the data context after the lookup is performed. + /// Formatted Value. + private string FormatValue(object value, bool clearDataContext) + { + string result = FormatValue(value); + if(clearDataContext && _valueBindingEvaluator != null) + { + _valueBindingEvaluator.ClearDataContext(); + } + + return result; + } + + /// + /// Converts the specified object to a string by using the + /// and + /// values + /// of the binding object specified by the + /// + /// property. + /// + /// The object to format as a string. + /// The string representation of the specified object. + /// + /// Override this method to provide a custom string conversion. + /// + protected virtual string FormatValue(object value) + { + if (_valueBindingEvaluator != null) + { + return _valueBindingEvaluator.GetDynamicValue(value) ?? String.Empty; + } + + if (_valueMemberSelector != null) + { + value = _valueMemberSelector.Select(value); + } + + return value == null ? String.Empty : value.ToString(); + } + + /// + /// Handle the TextChanged event that is directly attached to the + /// TextBox part. This ensures that only user initiated actions will + /// result in an AutoCompleteBox suggestion and operation. + /// + private void OnTextBoxTextChanged() + { + //Uses Dispatcher.Post to allow the TextBox selection to update before processing + Dispatcher.UIThread.Post(() => + { + // Call the central updated text method as a user-initiated action + TextUpdated(_textBox.Text, true); + }); + } + + /// + /// Updates both the text box value and underlying text dependency + /// property value if and when they change. Automatically fires the + /// text changed events when there is a change. + /// + /// The new string value. + private void UpdateTextValue(string value) + { + UpdateTextValue(value, null); + } + + /// + /// Updates both the text box value and underlying text dependency + /// property value if and when they change. Automatically fires the + /// text changed events when there is a change. + /// + /// The new string value. + /// A nullable bool value indicating whether + /// the action was user initiated. In a user initiated mode, the + /// underlying text dependency property is updated. In a non-user + /// interaction, the text box value is updated. When user initiated is + /// null, all values are updated. + private void UpdateTextValue(string value, bool? userInitiated) + { + bool callTextChanged = false; + // Update the Text dependency property + if ((userInitiated == null || userInitiated == true) && Text != value) + { + _ignoreTextPropertyChange++; + Text = value; + callTextChanged = true; + } + + // Update the TextBox's Text dependency property + if ((userInitiated == null || userInitiated == false) && TextBox != null && TextBox.Text != value) + { + _ignoreTextPropertyChange++; + TextBox.Text = value ?? string.Empty; + + // Text dependency property value was set, fire event + if (!callTextChanged && (Text == value || Text == null)) + { + callTextChanged = true; + } + } + + if (callTextChanged) + { + OnTextChanged(new RoutedEventArgs()); + } + } + + /// + /// Handle the update of the text for the control from any source, + /// including the TextBox part and the Text dependency property. + /// + /// The new text. + /// A value indicating whether the update + /// is a user-initiated action. This should be a True value when the + /// TextUpdated method is called from a TextBox event handler. + private void TextUpdated(string newText, bool userInitiated) + { + // Only process this event if it is coming from someone outside + // setting the Text dependency property directly. + if (_ignoreTextPropertyChange > 0) + { + _ignoreTextPropertyChange--; + return; + } + + if (newText == null) + { + newText = string.Empty; + } + + // The TextBox.TextChanged event was not firing immediately and + // was causing an immediate update, even with wrapping. If there is + // a selection currently, no update should happen. + if (IsTextCompletionEnabled && TextBox != null && TextBoxSelectionLength > 0 && TextBoxSelectionStart != TextBox.Text.Length) + { + return; + } + + // Evaluate the conditions needed for completion. + // 1. Minimum prefix length + // 2. If a delay timer is in use, use it + bool populateReady = newText.Length >= MinimumPrefixLength && MinimumPrefixLength >= 0; + _userCalledPopulate = populateReady ? userInitiated : false; + + // Update the interface and values only as necessary + UpdateTextValue(newText, userInitiated); + + if (populateReady) + { + _ignoreTextSelectionChange = true; + + if (_delayTimer != null) + { + _delayTimer.Start(); + } + else + { + PopulateDropDown(this, EventArgs.Empty); + } + } + else + { + SearchText = string.Empty; + if (SelectedItem != null) + { + _skipSelectedItemTextUpdate = true; + } + SelectedItem = null; + if (IsDropDownOpen) + { + IsDropDownOpen = false; + } + } + } + + /// + /// A simple helper method to clear the view and ensure that a view + /// object is always present and not null. + /// + private void ClearView() + { + if (_view == null) + { + _view = new AvaloniaList(); + } + else + { + _view.Clear(); + } + } + + /// + /// Walks through the items enumeration. Performance is not going to be + /// perfect with the current implementation. + /// + private void RefreshView() + { + if (_items == null) + { + ClearView(); + return; + } + + // Cache the current text value + string text = Text ?? string.Empty; + + // Determine if any filtering mode is on + bool stringFiltering = TextFilter != null; + bool objectFiltering = FilterMode == AutoCompleteFilterMode.Custom && TextFilter == null; + + int view_index = 0; + int view_count = _view.Count; + List items = _items; + foreach (object item in items) + { + bool inResults = !(stringFiltering || objectFiltering); + if (!inResults) + { + inResults = stringFiltering ? TextFilter(text, FormatValue(item)) : ItemFilter(text, item); + } + + if (view_count > view_index && inResults && _view[view_index] == item) + { + // Item is still in the view + view_index++; + } + else if (inResults) + { + // Insert the item + if (view_count > view_index && _view[view_index] != item) + { + // Replace item + // Unfortunately replacing via index throws a fatal + // exception: View[view_index] = item; + // Cost: O(n) vs O(1) + _view.RemoveAt(view_index); + _view.Insert(view_index, item); + view_index++; + } + else + { + // Add the item + if (view_index == view_count) + { + // Constant time is preferred (Add). + _view.Add(item); + } + else + { + _view.Insert(view_index, item); + } + view_index++; + view_count++; + } + } + else if (view_count > view_index && _view[view_index] == item) + { + // Remove the item + _view.RemoveAt(view_index); + view_count--; + } + } + + // Clear the evaluator to discard a reference to the last item + if (_valueBindingEvaluator != null) + { + _valueBindingEvaluator.ClearDataContext(); + } + } + + /// + /// Handle any change to the ItemsSource dependency property, update + /// the underlying ObservableCollection view, and set the selection + /// adapter's ItemsSource to the view if appropriate. + /// + /// The new enumerable reference. + private void OnItemsChanged(IEnumerable newValue) + { + // Remove handler for oldValue.CollectionChanged (if present) + _collectionChangeSubscription?.Dispose(); + _collectionChangeSubscription = null; + + // Add handler for newValue.CollectionChanged (if possible) + if (newValue is INotifyCollectionChanged newValueINotifyCollectionChanged) + { + _collectionChangeSubscription = newValueINotifyCollectionChanged.WeakSubscribe(ItemsCollectionChanged); + } + + // Store a local cached copy of the data + _items = newValue == null ? null : new List(newValue.Cast().ToList()); + + // Clear and set the view on the selection adapter + ClearView(); + if (SelectionAdapter != null && SelectionAdapter.Items != _view) + { + SelectionAdapter.Items = _view; + } + if (IsDropDownOpen) + { + RefreshView(); + } + } + + /// + /// Method that handles the ObservableCollection.CollectionChanged event for the ItemsSource property. + /// + /// The object that raised the event. + /// The event data. + private void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + // Update the cache + if (e.Action == NotifyCollectionChangedAction.Remove && e.OldItems != null) + { + for (int index = 0; index < e.OldItems.Count; index++) + { + _items.RemoveAt(e.OldStartingIndex); + } + } + if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems != null && _items.Count >= e.NewStartingIndex) + { + for (int index = 0; index < e.NewItems.Count; index++) + { + _items.Insert(e.NewStartingIndex + index, e.NewItems[index]); + } + } + if (e.Action == NotifyCollectionChangedAction.Replace && e.NewItems != null && e.OldItems != null) + { + for (int index = 0; index < e.NewItems.Count; index++) + { + _items[e.NewStartingIndex] = e.NewItems[index]; + } + } + + // Update the view + if (e.Action == NotifyCollectionChangedAction.Remove || e.Action == NotifyCollectionChangedAction.Replace) + { + for (int index = 0; index < e.OldItems.Count; index++) + { + _view.Remove(e.OldItems[index]); + } + } + + if (e.Action == NotifyCollectionChangedAction.Reset) + { + // Significant changes to the underlying data. + ClearView(); + if (Items != null) + { + _items = new List(Items.Cast().ToList()); + } + } + + // Refresh the observable collection used in the selection adapter. + RefreshView(); + } + + /// + /// Notifies the + /// that the + /// + /// property has been set and the data can be filtered to provide + /// possible matches in the drop-down. + /// + /// + /// Call this method when you are providing custom population of + /// the drop-down portion of the AutoCompleteBox, to signal the control + /// that you are done with the population process. + /// Typically, you use PopulateComplete when the population process + /// is a long-running process and you want to cancel built-in filtering + /// of the ItemsSource items. In this case, you can handle the + /// Populated event and set PopulatingEventArgs.Cancel to true. + /// When the long-running process has completed you call + /// PopulateComplete to indicate the drop-down is populated. + /// + public void PopulateComplete() + { + // Apply the search filter + RefreshView(); + + // Fire the Populated event containing the read-only view data. + PopulatedEventArgs populated = new PopulatedEventArgs(new ReadOnlyCollection(_view)); + OnPopulated(populated); + + if (SelectionAdapter != null && SelectionAdapter.Items != _view) + { + SelectionAdapter.Items = _view; + } + + bool isDropDownOpen = _userCalledPopulate && (_view.Count > 0); + if (isDropDownOpen != IsDropDownOpen) + { + _ignorePropertyChange = true; + IsDropDownOpen = isDropDownOpen; + } + if (IsDropDownOpen) + { + OpeningDropDown(false); + } + else + { + ClosingDropDown(true); + } + + UpdateTextCompletion(_userCalledPopulate); + } + + /// + /// Performs text completion, if enabled, and a lookup on the underlying + /// item values for an exact match. Will update the SelectedItem value. + /// + /// A value indicating whether the operation + /// was user initiated. Text completion will not be performed when not + /// directly initiated by the user. + private void UpdateTextCompletion(bool userInitiated) + { + // By default this method will clear the selected value + object newSelectedItem = null; + string text = Text; + + // Text search is StartsWith explicit and only when enabled, in + // line with WPF's ComboBox lookup. When in use it will associate + // a Value with the Text if it is found in ItemsSource. This is + // only valid when there is data and the user initiated the action. + if (_view.Count > 0) + { + if (IsTextCompletionEnabled && TextBox != null && userInitiated) + { + int currentLength = TextBox.Text.Length; + int selectionStart = TextBoxSelectionStart; + if (selectionStart == text.Length && selectionStart > _textSelectionStart) + { + // When the FilterMode dependency property is set to + // either StartsWith or StartsWithCaseSensitive, the + // first item in the view is used. This will improve + // performance on the lookup. It assumes that the + // FilterMode the user has selected is an acceptable + // case sensitive matching function for their scenario. + object top = FilterMode == AutoCompleteFilterMode.StartsWith || FilterMode == AutoCompleteFilterMode.StartsWithCaseSensitive + ? _view[0] + : TryGetMatch(text, _view, AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith)); + + // If the search was successful, update SelectedItem + if (top != null) + { + newSelectedItem = top; + string topString = FormatValue(top, true); + + // Only replace partially when the two words being the same + int minLength = Math.Min(topString.Length, Text.Length); + if (AutoCompleteSearch.Equals(Text.Substring(0, minLength), topString.Substring(0, minLength))) + { + // Update the text + UpdateTextValue(topString); + + // Select the text past the user's caret + TextBox.SelectionStart = currentLength; + TextBox.SelectionEnd = topString.Length; + } + } + } + } + else + { + // Perform an exact string lookup for the text. This is a + // design change from the original Toolkit release when the + // IsTextCompletionEnabled property behaved just like the + // WPF ComboBox's IsTextSearchEnabled property. + // + // This change provides the behavior that most people expect + // to find: a lookup for the value is always performed. + newSelectedItem = TryGetMatch(text, _view, AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.EqualsCaseSensitive)); + } + } + + // Update the selected item property + + if (SelectedItem != newSelectedItem) + { + _skipSelectedItemTextUpdate = true; + } + SelectedItem = newSelectedItem; + + // Restore updates for TextSelection + if (_ignoreTextSelectionChange) + { + _ignoreTextSelectionChange = false; + if (TextBox != null) + { + _textSelectionStart = TextBoxSelectionStart; + } + } + } + + /// + /// Attempts to look through the view and locate the specific exact + /// text match. + /// + /// The search text. + /// The view reference. + /// The predicate to use for the partial or + /// exact match. + /// Returns the object or null. + private object TryGetMatch(string searchText, AvaloniaList view, AutoCompleteFilterPredicate predicate) + { + if (view != null && view.Count > 0) + { + foreach (object o in view) + { + if (predicate(searchText, FormatValue(o))) + { + return o; + } + } + } + + return null; + } + + private void UpdatePseudoClasses() + { + PseudoClasses.Set(":dropdownopen", IsDropDownOpen); + } + + private void ClearTextBoxSelection() + { + if (TextBox != null) + { + int length = TextBox.Text?.Length ?? 0; + TextBox.SelectionStart = length; + TextBox.SelectionEnd = length; + } + } + + /// + /// Called when the selected item is changed, updates the text value + /// that is displayed in the text box part. + /// + /// The new item. + private void OnSelectedItemChanged(object newItem) + { + string text; + + if (newItem == null) + { + text = SearchText; + } + else + { + text = FormatValue(newItem, true); + } + + // Update the Text property and the TextBox values + UpdateTextValue(text); + + // Move the caret to the end of the text box + ClearTextBoxSelection(); + } + + /// + /// Handles the SelectionChanged event of the selection adapter. + /// + /// The source object. + /// The selection changed event data. + private void OnAdapterSelectionChanged(object sender, SelectionChangedEventArgs e) + { + SelectedItem = _adapter.SelectedItem; + } + + //TODO Check UpdateTextCompletion + /// + /// Handles the Commit event on the selection adapter. + /// + /// The source object. + /// The event data. + private void OnAdapterSelectionComplete(object sender, RoutedEventArgs e) + { + IsDropDownOpen = false; + + // Completion will update the selected value + //UpdateTextCompletion(false); + + // Text should not be selected + ClearTextBoxSelection(); + + TextBox.Focus(); + } + + /// + /// Handles the Cancel event on the selection adapter. + /// + /// The source object. + /// The event data. + private void OnAdapterSelectionCanceled(object sender, RoutedEventArgs e) + { + UpdateTextValue(SearchText); + + // Completion will update the selected value + UpdateTextCompletion(false); + } + + /// + /// A predefined set of filter functions for the known, built-in + /// AutoCompleteFilterMode enumeration values. + /// + private static class AutoCompleteSearch + { + /// + /// Index function that retrieves the filter for the provided + /// AutoCompleteFilterMode. + /// + /// The built-in search mode. + /// Returns the string-based comparison function. + public static AutoCompleteFilterPredicate GetFilter(AutoCompleteFilterMode FilterMode) + { + switch (FilterMode) + { + case AutoCompleteFilterMode.Contains: + return Contains; + + case AutoCompleteFilterMode.ContainsCaseSensitive: + return ContainsCaseSensitive; + + case AutoCompleteFilterMode.ContainsOrdinal: + return ContainsOrdinal; + + case AutoCompleteFilterMode.ContainsOrdinalCaseSensitive: + return ContainsOrdinalCaseSensitive; + + case AutoCompleteFilterMode.Equals: + return Equals; + + case AutoCompleteFilterMode.EqualsCaseSensitive: + return EqualsCaseSensitive; + + case AutoCompleteFilterMode.EqualsOrdinal: + return EqualsOrdinal; + + case AutoCompleteFilterMode.EqualsOrdinalCaseSensitive: + return EqualsOrdinalCaseSensitive; + + case AutoCompleteFilterMode.StartsWith: + return StartsWith; + + case AutoCompleteFilterMode.StartsWithCaseSensitive: + return StartsWithCaseSensitive; + + case AutoCompleteFilterMode.StartsWithOrdinal: + return StartsWithOrdinal; + + case AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive: + return StartsWithOrdinalCaseSensitive; + + case AutoCompleteFilterMode.None: + case AutoCompleteFilterMode.Custom: + default: + return null; + } + } + + /// + /// An implementation of the Contains member of string that takes in a + /// string comparison. The traditional .NET string Contains member uses + /// StringComparison.Ordinal. + /// + /// The string. + /// The string value to search for. + /// The string comparison type. + /// Returns true when the substring is found. + private static bool Contains(string s, string value, StringComparison comparison) + { + return s.IndexOf(value, comparison) >= 0; + } + + /// + /// Check if the string value begins with the text. + /// + /// The AutoCompleteBox prefix text. + /// The item's string value. + /// Returns true if the condition is met. + public static bool StartsWith(string text, string value) + { + return value.StartsWith(text, StringComparison.CurrentCultureIgnoreCase); + } + + /// + /// Check if the string value begins with the text. + /// + /// The AutoCompleteBox prefix text. + /// The item's string value. + /// Returns true if the condition is met. + public static bool StartsWithCaseSensitive(string text, string value) + { + return value.StartsWith(text, StringComparison.CurrentCulture); + } + + /// + /// Check if the string value begins with the text. + /// + /// The AutoCompleteBox prefix text. + /// The item's string value. + /// Returns true if the condition is met. + public static bool StartsWithOrdinal(string text, string value) + { + return value.StartsWith(text, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Check if the string value begins with the text. + /// + /// The AutoCompleteBox prefix text. + /// The item's string value. + /// Returns true if the condition is met. + public static bool StartsWithOrdinalCaseSensitive(string text, string value) + { + return value.StartsWith(text, StringComparison.Ordinal); + } + + /// + /// Check if the prefix is contained in the string value. The current + /// culture's case insensitive string comparison operator is used. + /// + /// The AutoCompleteBox prefix text. + /// The item's string value. + /// Returns true if the condition is met. + public static bool Contains(string text, string value) + { + return Contains(value, text, StringComparison.CurrentCultureIgnoreCase); + } + + /// + /// Check if the prefix is contained in the string value. + /// + /// The AutoCompleteBox prefix text. + /// The item's string value. + /// Returns true if the condition is met. + public static bool ContainsCaseSensitive(string text, string value) + { + return Contains(value, text, StringComparison.CurrentCulture); + } + + /// + /// Check if the prefix is contained in the string value. + /// + /// The AutoCompleteBox prefix text. + /// The item's string value. + /// Returns true if the condition is met. + public static bool ContainsOrdinal(string text, string value) + { + return Contains(value, text, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Check if the prefix is contained in the string value. + /// + /// The AutoCompleteBox prefix text. + /// The item's string value. + /// Returns true if the condition is met. + public static bool ContainsOrdinalCaseSensitive(string text, string value) + { + return Contains(value, text, StringComparison.Ordinal); + } + + /// + /// Check if the string values are equal. + /// + /// The AutoCompleteBox prefix text. + /// The item's string value. + /// Returns true if the condition is met. + public static bool Equals(string text, string value) + { + return value.Equals(text, StringComparison.CurrentCultureIgnoreCase); + } + + /// + /// Check if the string values are equal. + /// + /// The AutoCompleteBox prefix text. + /// The item's string value. + /// Returns true if the condition is met. + public static bool EqualsCaseSensitive(string text, string value) + { + return value.Equals(text, StringComparison.CurrentCulture); + } + + /// + /// Check if the string values are equal. + /// + /// The AutoCompleteBox prefix text. + /// The item's string value. + /// Returns true if the condition is met. + public static bool EqualsOrdinal(string text, string value) + { + return value.Equals(text, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Check if the string values are equal. + /// + /// The AutoCompleteBox prefix text. + /// The item's string value. + /// Returns true if the condition is met. + public static bool EqualsOrdinalCaseSensitive(string text, string value) + { + return value.Equals(text, StringComparison.Ordinal); + } + } + + /// + /// A framework element that permits a binding to be evaluated in a new data + /// context leaf node. + /// + /// The type of dynamic binding to return. + public class BindingEvaluator : Control + { + /// + /// Gets or sets the string value binding used by the control. + /// + private IBinding _binding; + + #region public T Value + + /// + /// Identifies the Value dependency property. + /// + public static readonly StyledProperty ValueProperty = + AvaloniaProperty.Register, T>(nameof(Value)); + + /// + /// Gets or sets the data item value. + /// + public T Value + { + get { return GetValue(ValueProperty); } + set { SetValue(ValueProperty, value); } + } + + #endregion public string Value + + /// + /// Gets or sets the value binding. + /// + public IBinding ValueBinding + { + get { return _binding; } + set + { + _binding = value; + AvaloniaObjectExtensions.Bind(this, ValueProperty, value); + } + } + + /// + /// Initializes a new instance of the BindingEvaluator class. + /// + public BindingEvaluator() + { } + + /// + /// Initializes a new instance of the BindingEvaluator class, + /// setting the initial binding to the provided parameter. + /// + /// The initial string value binding. + public BindingEvaluator(IBinding binding) + : this() + { + ValueBinding = binding; + } + + /// + /// Clears the data context so that the control does not keep a + /// reference to the last-looked up item. + /// + public void ClearDataContext() + { + DataContext = null; + } + + /// + /// Updates the data context of the framework element and returns the + /// updated binding value. + /// + /// The object to use as the data context. + /// If set to true, this parameter will + /// clear the data context immediately after retrieving the value. + /// Returns the evaluated T value of the bound dependency + /// property. + public T GetDynamicValue(object o, bool clearDataContext) + { + DataContext = o; + T value = Value; + if (clearDataContext) + { + DataContext = null; + } + return value; + } + + /// + /// Updates the data context of the framework element and returns the + /// updated binding value. + /// + /// The object to use as the data context. + /// Returns the evaluated T value of the bound dependency + /// property. + public T GetDynamicValue(object o) + { + DataContext = o; + return Value; + } + } + } +} diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs index 002c5ea3f2..8acb3603c9 100644 --- a/src/Avalonia.Controls/Border.cs +++ b/src/Avalonia.Controls/Border.cs @@ -1,6 +1,8 @@ // 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 Avalonia; +using Avalonia.Controls.Utils; using Avalonia.Media; namespace Avalonia.Controls @@ -8,7 +10,7 @@ namespace Avalonia.Controls /// /// A control which decorates a child with a border and background. /// - public class Border : Decorator + public partial class Border : Decorator { /// /// Defines the property. @@ -25,21 +27,24 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty BorderThicknessProperty = - AvaloniaProperty.Register(nameof(BorderThickness)); + public static readonly StyledProperty BorderThicknessProperty = + AvaloniaProperty.Register(nameof(BorderThickness)); /// /// Defines the property. /// - public static readonly StyledProperty CornerRadiusProperty = - AvaloniaProperty.Register(nameof(CornerRadius)); + public static readonly StyledProperty CornerRadiusProperty = + AvaloniaProperty.Register(nameof(CornerRadius)); + + private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper(); /// /// Initializes static members of the class. /// static Border() { - AffectsRender(BackgroundProperty, BorderBrushProperty); + AffectsRender(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty); + AffectsMeasure(BorderThicknessProperty); } /// @@ -63,7 +68,7 @@ namespace Avalonia.Controls /// /// Gets or sets the thickness of the border. /// - public double BorderThickness + public Thickness BorderThickness { get { return GetValue(BorderThicknessProperty); } set { SetValue(BorderThicknessProperty, value); } @@ -72,7 +77,7 @@ namespace Avalonia.Controls /// /// Gets or sets the radius of the border rounded corners. /// - public float CornerRadius + public CornerRadius CornerRadius { get { return GetValue(CornerRadiusProperty); } set { SetValue(CornerRadiusProperty, value); } @@ -84,21 +89,7 @@ namespace Avalonia.Controls /// The drawing context. public override void Render(DrawingContext context) { - var background = Background; - var borderBrush = BorderBrush; - var borderThickness = BorderThickness; - var cornerRadius = CornerRadius; - var rect = new Rect(Bounds.Size).Deflate(BorderThickness); - - if (background != null) - { - context.FillRectangle(background, rect, cornerRadius); - } - - if (borderBrush != null && borderThickness > 0) - { - context.DrawRectangle(new Pen(borderBrush, borderThickness), rect, cornerRadius); - } + _borderRenderHelper.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush); } /// @@ -120,10 +111,12 @@ namespace Avalonia.Controls { if (Child != null) { - var padding = Padding + new Thickness(BorderThickness); + var padding = Padding + BorderThickness; Child.Arrange(new Rect(finalSize).Deflate(padding)); } + _borderRenderHelper.Update(finalSize, BorderThickness, CornerRadius); + return finalSize; } @@ -131,19 +124,17 @@ namespace Avalonia.Controls Size availableSize, IControl child, Thickness padding, - double borderThickness) + Thickness borderThickness) { - padding += new Thickness(borderThickness); + padding += borderThickness; if (child != null) { child.Measure(availableSize.Deflate(padding)); return child.DesiredSize.Inflate(padding); } - else - { - return new Size(padding.Left + padding.Right, padding.Bottom + padding.Top); - } + + return new Size(padding.Left + padding.Right, padding.Bottom + padding.Top); } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 4922761a84..fa69d72d67 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -245,7 +245,7 @@ namespace Avalonia.Controls { base.OnPointerReleased(e); - if (e.MouseButton == MouseButton.Left) + if (IsPressed && e.MouseButton == MouseButton.Left) { e.Device.Capture(null); IsPressed = false; diff --git a/src/Avalonia.Controls/ButtonSpinner.cs b/src/Avalonia.Controls/ButtonSpinner.cs new file mode 100644 index 0000000000..866237ecce --- /dev/null +++ b/src/Avalonia.Controls/ButtonSpinner.cs @@ -0,0 +1,263 @@ +using System; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; + +namespace Avalonia.Controls +{ + public enum Location + { + Left, + Right + } + + /// + /// Represents a spinner control that includes two Buttons. + /// + public class ButtonSpinner : Spinner + { + /// + /// Defines the property. + /// + public static readonly StyledProperty AllowSpinProperty = + AvaloniaProperty.Register(nameof(AllowSpin), true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ShowButtonSpinnerProperty = + AvaloniaProperty.Register(nameof(ShowButtonSpinner), true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ButtonSpinnerLocationProperty = + AvaloniaProperty.Register(nameof(ButtonSpinnerLocation), Location.Right); + + private Button _decreaseButton; + /// + /// Gets or sets the DecreaseButton template part. + /// + private Button DecreaseButton + { + get { return _decreaseButton; } + set + { + if (_decreaseButton != null) + { + _decreaseButton.Click -= OnButtonClick; + } + _decreaseButton = value; + if (_decreaseButton != null) + { + _decreaseButton.Click += OnButtonClick; + } + } + } + + private Button _increaseButton; + /// + /// Gets or sets the IncreaseButton template part. + /// + private Button IncreaseButton + { + get + { + return _increaseButton; + } + set + { + if (_increaseButton != null) + { + _increaseButton.Click -= OnButtonClick; + } + _increaseButton = value; + if (_increaseButton != null) + { + _increaseButton.Click += OnButtonClick; + } + } + } + + /// + /// Initializes static members of the class. + /// + static ButtonSpinner() + { + AllowSpinProperty.Changed.Subscribe(AllowSpinChanged); + PseudoClass(ButtonSpinnerLocationProperty, location => location == Location.Left, ":left"); + PseudoClass(ButtonSpinnerLocationProperty, location => location == Location.Right, ":right"); + } + + /// + /// Gets or sets a value indicating whether the should allow to spin. + /// + public bool AllowSpin + { + get { return GetValue(AllowSpinProperty); } + set { SetValue(AllowSpinProperty, value); } + } + + /// + /// Gets or sets a value indicating whether the spin buttons should be shown. + /// + public bool ShowButtonSpinner + { + get { return GetValue(ShowButtonSpinnerProperty); } + set { SetValue(ShowButtonSpinnerProperty, value); } + } + + /// + /// Gets or sets current location of the . + /// + public Location ButtonSpinnerLocation + { + get { return GetValue(ButtonSpinnerLocationProperty); } + set { SetValue(ButtonSpinnerLocationProperty, value); } + } + + /// + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + { + IncreaseButton = e.NameScope.Find /// The string. - /// The current culture. /// The . - public static IEnumerable ParseLengths(string s, CultureInfo culture) + public static IEnumerable ParseLengths(string s) { - return s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries).Select(x => Parse(x, culture)); + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture)) + { + while (tokenizer.TryReadString(out var item)) + { + yield return Parse(item); + } + } } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 4366de1cd6..6a26e29187 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -1,6 +1,7 @@ // 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; using System.Collections.Generic; using System.Collections.Specialized; @@ -54,6 +55,7 @@ namespace Avalonia.Controls private IEnumerable _items = new AvaloniaList(); private IItemContainerGenerator _itemContainerGenerator; + private IDisposable _itemsCollectionChangedSubscription; /// /// Initializes static members of the class. @@ -326,12 +328,8 @@ namespace Avalonia.Controls /// The event args. protected virtual void ItemsChanged(AvaloniaPropertyChangedEventArgs e) { - var incc = e.OldValue as INotifyCollectionChanged; - - if (incc != null) - { - incc.CollectionChanged -= ItemsCollectionChanged; - } + _itemsCollectionChangedSubscription?.Dispose(); + _itemsCollectionChangedSubscription = null; var oldValue = e.OldValue as IEnumerable; var newValue = e.NewValue as IEnumerable; @@ -428,7 +426,7 @@ namespace Avalonia.Controls if (incc != null) { - incc.CollectionChanged += ItemsCollectionChanged; + _itemsCollectionChangedSubscription = incc.WeakSubscribe(ItemsCollectionChanged); } } diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs index ed7a50d9b0..87e3853643 100644 --- a/src/Avalonia.Controls/LayoutTransformControl.cs +++ b/src/Avalonia.Controls/LayoutTransformControl.cs @@ -15,6 +15,9 @@ using System.Reactive.Linq; namespace Avalonia.Controls { + /// + /// Control that implements support for transformations as if applied by LayoutTransform. + /// public class LayoutTransformControl : ContentControl { public static readonly AvaloniaProperty LayoutTransformProperty = @@ -26,6 +29,9 @@ namespace Avalonia.Controls .AddClassHandler(x => x.OnLayoutTransformChanged); } + /// + /// Gets or sets a graphics transformation that should apply to this element when layout is performed. + /// public Transform LayoutTransform { get { return GetValue(LayoutTransformProperty); } diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index dadd3b910b..96f6fb59b0 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -93,6 +93,7 @@ namespace Avalonia.Controls static MenuItem() { SelectableMixin.Attach(IsSelectedProperty); + CommandProperty.Changed.Subscribe(CommandChanged); FocusableProperty.OverrideDefaultValue(true); IconProperty.Changed.AddClassHandler(x => x.IconChanged); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); @@ -424,6 +425,40 @@ namespace Avalonia.Controls } } + /// + /// Called when the property changes. + /// + /// The event args. + private static void CommandChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is MenuItem menuItem) + { + if (e.OldValue is ICommand oldCommand) + { + oldCommand.CanExecuteChanged -= menuItem.CanExecuteChanged; + } + + if (e.NewValue is ICommand newCommand) + { + newCommand.CanExecuteChanged += menuItem.CanExecuteChanged; + } + + menuItem.CanExecuteChanged(menuItem, EventArgs.Empty); + } + } + + /// + /// Called when the event fires. + /// + /// The event sender. + /// The event args. + private void CanExecuteChanged(object sender, EventArgs e) + { + // HACK: Just set the IsEnabled property for the moment. This needs to be changed to + // use IsEnabledCore etc. but it will do for now. + IsEnabled = Command == null || Command.CanExecute(CommandParameter); + } + /// /// Called when the property changes. /// diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs new file mode 100644 index 0000000000..59d2949b81 --- /dev/null +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -0,0 +1,998 @@ +using System; +using System.Globalization; +using System.IO; +using System.Linq; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Threading; +using Avalonia.Utilities; + +namespace Avalonia.Controls +{ + /// + /// Control that represents a TextBox with button spinners that allow incrementing and decrementing numeric values. + /// + public class NumericUpDown : TemplatedControl + { + /// + /// Defines the property. + /// + public static readonly StyledProperty AllowSpinProperty = + ButtonSpinner.AllowSpinProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ButtonSpinnerLocationProperty = + ButtonSpinner.ButtonSpinnerLocationProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ShowButtonSpinnerProperty = + ButtonSpinner.ShowButtonSpinnerProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly DirectProperty ClipValueToMinMaxProperty = + AvaloniaProperty.RegisterDirect(nameof(ClipValueToMinMax), + updown => updown.ClipValueToMinMax, (updown, b) => updown.ClipValueToMinMax = b); + + /// + /// Defines the property. + /// + public static readonly DirectProperty CultureInfoProperty = + AvaloniaProperty.RegisterDirect(nameof(CultureInfo), o => o.CultureInfo, + (o, v) => o.CultureInfo = v, CultureInfo.CurrentCulture); + + /// + /// Defines the property. + /// + public static readonly StyledProperty FormatStringProperty = + AvaloniaProperty.Register(nameof(FormatString), string.Empty); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IncrementProperty = + AvaloniaProperty.Register(nameof(Increment), 1.0d, validate: OnCoerceIncrement); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsReadOnlyProperty = + AvaloniaProperty.Register(nameof(IsReadOnly)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaximumProperty = + AvaloniaProperty.Register(nameof(Maximum), double.MaxValue, validate: OnCoerceMaximum); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinimumProperty = + AvaloniaProperty.Register(nameof(Minimum), double.MinValue, validate: OnCoerceMinimum); + + /// + /// Defines the property. + /// + public static readonly DirectProperty ParsingNumberStyleProperty = + AvaloniaProperty.RegisterDirect(nameof(ParsingNumberStyle), + updown => updown.ParsingNumberStyle, (updown, style) => updown.ParsingNumberStyle = style); + + /// + /// Defines the property. + /// + public static readonly DirectProperty TextProperty = + AvaloniaProperty.RegisterDirect(nameof(Text), o => o.Text, (o, v) => o.Text = v, + defaultBindingMode: BindingMode.TwoWay); + + /// + /// Defines the property. + /// + public static readonly DirectProperty ValueProperty = + AvaloniaProperty.RegisterDirect(nameof(Value), updown => updown.Value, + (updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay); + + /// + /// Defines the property. + /// + public static readonly StyledProperty WatermarkProperty = + AvaloniaProperty.Register(nameof(Watermark)); + + private IDisposable _textBoxTextChangedSubscription; + + private double _value; + private string _text; + private bool _internalValueSet; + private bool _clipValueToMinMax; + private bool _isSyncingTextAndValueProperties; + private bool _isTextChangedFromUI; + private CultureInfo _cultureInfo; + private NumberStyles _parsingNumberStyle = NumberStyles.Any; + + /// + /// Gets the Spinner template part. + /// + private Spinner Spinner { get; set; } + + /// + /// Gets the TextBox template part. + /// + private TextBox TextBox { get; set; } + + /// + /// Gets or sets the ability to perform increment/decrement operations via the keyboard, button spinners, or mouse wheel. + /// + public bool AllowSpin + { + get { return GetValue(AllowSpinProperty); } + set { SetValue(AllowSpinProperty, value); } + } + + /// + /// Gets or sets current location of the . + /// + public Location ButtonSpinnerLocation + { + get { return GetValue(ButtonSpinnerLocationProperty); } + set { SetValue(ButtonSpinnerLocationProperty, value); } + } + + /// + /// Gets or sets a value indicating whether the spin buttons should be shown. + /// + public bool ShowButtonSpinner + { + get { return GetValue(ShowButtonSpinnerProperty); } + set { SetValue(ShowButtonSpinnerProperty, value); } + } + + /// + /// Gets or sets if the value should be clipped when minimum/maximum is reached. + /// + public bool ClipValueToMinMax + { + get { return _clipValueToMinMax; } + set { SetAndRaise(ClipValueToMinMaxProperty, ref _clipValueToMinMax, value); } + } + + /// + /// Gets or sets the current CultureInfo. + /// + public CultureInfo CultureInfo + { + get { return _cultureInfo; } + set { SetAndRaise(CultureInfoProperty, ref _cultureInfo, value); } + } + + /// + /// Gets or sets the display format of the . + /// + public string FormatString + { + get { return GetValue(FormatStringProperty); } + set { SetValue(FormatStringProperty, value); } + } + + /// + /// Gets or sets the amount in which to increment the . + /// + public double Increment + { + get { return GetValue(IncrementProperty); } + set { SetValue(IncrementProperty, value); } + } + + /// + /// Gets or sets if the control is read only. + /// + public bool IsReadOnly + { + get { return GetValue(IsReadOnlyProperty); } + set { SetValue(IsReadOnlyProperty, value); } + } + + /// + /// Gets or sets the maximum allowed value. + /// + public double Maximum + { + get { return GetValue(MaximumProperty); } + set { SetValue(MaximumProperty, value); } + } + + /// + /// Gets or sets the minimum allowed value. + /// + public double Minimum + { + get { return GetValue(MinimumProperty); } + set { SetValue(MinimumProperty, value); } + } + + /// + /// Gets or sets the parsing style (AllowLeadingWhite, Float, AllowHexSpecifier, ...). By default, Any. + /// + public NumberStyles ParsingNumberStyle + { + get { return _parsingNumberStyle; } + set { SetAndRaise(ParsingNumberStyleProperty, ref _parsingNumberStyle, value); } + } + + /// + /// Gets or sets the formatted string representation of the value. + /// + public string Text + { + get { return _text; } + set { SetAndRaise(TextProperty, ref _text, value); } + } + + /// + /// Gets or sets the value. + /// + public double Value + { + get { return _value; } + set + { + value = OnCoerceValue(value); + SetAndRaise(ValueProperty, ref _value, value); + } + } + + /// + /// Gets or sets the object to use as a watermark if the is null. + /// + public string Watermark + { + get { return GetValue(WatermarkProperty); } + set { SetValue(WatermarkProperty, value); } + } + + /// + /// Initializes new instance of class. + /// + public NumericUpDown() + { + Initialized += (sender, e) => + { + if (!_internalValueSet && IsInitialized) + { + SyncTextAndValueProperties(false, null, true); + } + + SetValidSpinDirection(); + }; + } + + /// + /// Initializes static members of the class. + /// + static NumericUpDown() + { + CultureInfoProperty.Changed.Subscribe(OnCultureInfoChanged); + FormatStringProperty.Changed.Subscribe(FormatStringChanged); + IncrementProperty.Changed.Subscribe(IncrementChanged); + IsReadOnlyProperty.Changed.Subscribe(OnIsReadOnlyChanged); + MaximumProperty.Changed.Subscribe(OnMaximumChanged); + MinimumProperty.Changed.Subscribe(OnMinimumChanged); + TextProperty.Changed.Subscribe(OnTextChanged); + ValueProperty.Changed.Subscribe(OnValueChanged); + } + + /// + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + { + if (TextBox != null) + { + TextBox.PointerPressed -= TextBoxOnPointerPressed; + _textBoxTextChangedSubscription?.Dispose(); + } + TextBox = e.NameScope.Find("PART_TextBox"); + if (TextBox != null) + { + TextBox.Text = Text; + TextBox.PointerPressed += TextBoxOnPointerPressed; + _textBoxTextChangedSubscription = TextBox.GetObservable(TextBox.TextProperty).Subscribe(txt => TextBoxOnTextChanged()); + } + + if (Spinner != null) + { + Spinner.Spin -= OnSpinnerSpin; + } + + Spinner = e.NameScope.Find("PART_Spinner"); + + if (Spinner != null) + { + Spinner.Spin += OnSpinnerSpin; + } + + SetValidSpinDirection(); + } + + /// + protected override void OnKeyDown(KeyEventArgs e) + { + switch (e.Key) + { + case Key.Enter: + var commitSuccess = CommitInput(); + e.Handled = !commitSuccess; + break; + } + } + + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnCultureInfoChanged(CultureInfo oldValue, CultureInfo newValue) + { + if (IsInitialized) + { + SyncTextAndValueProperties(false, null); + } + } + + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnFormatStringChanged(string oldValue, string newValue) + { + if (IsInitialized) + { + SyncTextAndValueProperties(false, null); + } + } + + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnIncrementChanged(double oldValue, double newValue) + { + if (IsInitialized) + { + SetValidSpinDirection(); + } + } + + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnIsReadOnlyChanged(bool oldValue, bool newValue) + { + SetValidSpinDirection(); + } + + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnMaximumChanged(double oldValue, double newValue) + { + if (IsInitialized) + { + SetValidSpinDirection(); + } + if (ClipValueToMinMax) + { + Value = MathUtilities.Clamp(Value, Minimum, Maximum); + } + } + + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnMinimumChanged(double oldValue, double newValue) + { + if (IsInitialized) + { + SetValidSpinDirection(); + } + if (ClipValueToMinMax) + { + Value = MathUtilities.Clamp(Value, Minimum, Maximum); + } + } + + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnTextChanged(string oldValue, string newValue) + { + if (IsInitialized) + { + SyncTextAndValueProperties(true, Text); + } + } + + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnValueChanged(double oldValue, double newValue) + { + if (!_internalValueSet && IsInitialized) + { + SyncTextAndValueProperties(false, null, true); + } + + SetValidSpinDirection(); + + RaiseValueChangedEvent(oldValue, newValue); + } + + /// + /// Called when the property has to be coerced. + /// + /// The value. + protected virtual double OnCoerceIncrement(double baseValue) + { + return baseValue; + } + + /// + /// Called when the property has to be coerced. + /// + /// The value. + protected virtual double OnCoerceMaximum(double baseValue) + { + return Math.Max(baseValue, Minimum); + } + + /// + /// Called when the property has to be coerced. + /// + /// The value. + protected virtual double OnCoerceMinimum(double baseValue) + { + return Math.Min(baseValue, Maximum); + } + + /// + /// Called when the property has to be coerced. + /// + /// The value. + protected virtual double OnCoerceValue(double baseValue) + { + return baseValue; + } + + /// + /// Raises the OnSpin event when spinning is initiated by the end-user. + /// + /// The event args. + protected virtual void OnSpin(SpinEventArgs e) + { + if (e == null) + { + throw new ArgumentNullException("e"); + } + + var handler = Spinned; + handler?.Invoke(this, e); + + if (e.Direction == SpinDirection.Increase) + { + DoIncrement(); + } + else + { + DoDecrement(); + } + } + + /// + /// Raises the event. + /// + /// The old value. + /// The new value. + protected virtual void RaiseValueChangedEvent(double oldValue, double newValue) + { + var e = new NumericUpDownValueChangedEventArgs(ValueChangedEvent, oldValue, newValue); + RaiseEvent(e); + } + + /// + /// Converts the formatted text to a value. + /// + private double ConvertTextToValue(string text) + { + double result = 0; + + if (string.IsNullOrEmpty(text)) + { + return result; + } + + // Since the conversion from Value to text using a FormartString may not be parsable, + // we verify that the already existing text is not the exact same value. + var currentValueText = ConvertValueToText(); + if (Equals(currentValueText, text)) + { + return Value; + } + + result = ConvertTextToValueCore(currentValueText, text); + + if (ClipValueToMinMax) + { + return MathUtilities.Clamp(result, Minimum, Maximum); + } + + ValidateMinMax(result); + + return result; + } + + /// + /// Converts the value to formatted text. + /// + /// + private string ConvertValueToText() + { + //Manage FormatString of type "{}{0:N2} °" (in xaml) or "{0:N2} °" in code-behind. + if (FormatString.Contains("{0")) + { + return string.Format(CultureInfo, FormatString, Value); + } + + return Value.ToString(FormatString, CultureInfo); + } + + /// + /// Called by OnSpin when the spin direction is SpinDirection.Increase. + /// + private void OnIncrement() + { + var result = Value + Increment; + Value = MathUtilities.Clamp(result, Minimum, Maximum); + } + + /// + /// Called by OnSpin when the spin direction is SpinDirection.Descrease. + /// + private void OnDecrement() + { + var result = Value - Increment; + Value = MathUtilities.Clamp(result, Minimum, Maximum); + } + + /// + /// Sets the valid spin directions. + /// + private void SetValidSpinDirection() + { + var validDirections = ValidSpinDirections.None; + + // Zero increment always prevents spin. + if (Increment != 0 && !IsReadOnly) + { + if (Value < Maximum) + { + validDirections = validDirections | ValidSpinDirections.Increase; + } + + if (Value > Minimum) + { + validDirections = validDirections | ValidSpinDirections.Decrease; + } + } + + if (Spinner != null) + { + Spinner.ValidSpinDirection = validDirections; + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnCultureInfoChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is NumericUpDown upDown) + { + var oldValue = (CultureInfo)e.OldValue; + var newValue = (CultureInfo)e.NewValue; + upDown.OnCultureInfoChanged(oldValue, newValue); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void IncrementChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is NumericUpDown upDown) + { + var oldValue = (double)e.OldValue; + var newValue = (double)e.NewValue; + upDown.OnIncrementChanged(oldValue, newValue); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void FormatStringChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is NumericUpDown upDown) + { + var oldValue = (string)e.OldValue; + var newValue = (string)e.NewValue; + upDown.OnFormatStringChanged(oldValue, newValue); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnIsReadOnlyChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is NumericUpDown upDown) + { + var oldValue = (bool)e.OldValue; + var newValue = (bool)e.NewValue; + upDown.OnIsReadOnlyChanged(oldValue, newValue); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnMaximumChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is NumericUpDown upDown) + { + var oldValue = (double)e.OldValue; + var newValue = (double)e.NewValue; + upDown.OnMaximumChanged(oldValue, newValue); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnMinimumChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is NumericUpDown upDown) + { + var oldValue = (double)e.OldValue; + var newValue = (double)e.NewValue; + upDown.OnMinimumChanged(oldValue, newValue); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnTextChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is NumericUpDown upDown) + { + var oldValue = (string)e.OldValue; + var newValue = (string)e.NewValue; + upDown.OnTextChanged(oldValue, newValue); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnValueChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is NumericUpDown upDown) + { + var oldValue = (double)e.OldValue; + var newValue = (double)e.NewValue; + upDown.OnValueChanged(oldValue, newValue); + } + } + + private void SetValueInternal(double value) + { + _internalValueSet = true; + try + { + Value = value; + } + finally + { + _internalValueSet = false; + } + } + + private static double OnCoerceMaximum(NumericUpDown upDown, double value) + { + return upDown.OnCoerceMaximum(value); + } + + private static double OnCoerceMinimum(NumericUpDown upDown, double value) + { + return upDown.OnCoerceMinimum(value); + } + + private static double OnCoerceIncrement(NumericUpDown upDown, double value) + { + return upDown.OnCoerceIncrement(value); + } + + private void TextBoxOnTextChanged() + { + try + { + _isTextChangedFromUI = true; + if (TextBox != null) + { + Text = TextBox.Text; + } + } + finally + { + _isTextChangedFromUI = false; + } + } + + private void OnSpinnerSpin(object sender, SpinEventArgs e) + { + if (AllowSpin && !IsReadOnly) + { + var spin = !e.UsingMouseWheel; + spin |= ((TextBox != null) && TextBox.IsFocused); + + if (spin) + { + e.Handled = true; + OnSpin(e); + } + } + } + + private void DoDecrement() + { + if (Spinner == null || (Spinner.ValidSpinDirection & ValidSpinDirections.Decrease) == ValidSpinDirections.Decrease) + { + OnDecrement(); + } + } + + private void DoIncrement() + { + if (Spinner == null || (Spinner.ValidSpinDirection & ValidSpinDirections.Increase) == ValidSpinDirections.Increase) + { + OnIncrement(); + } + } + + public event EventHandler Spinned; + + private void TextBoxOnPointerPressed(object sender, PointerPressedEventArgs e) + { + if (e.Device.Captured != Spinner) + { + Dispatcher.UIThread.InvokeAsync(() => { e.Device.Capture(Spinner); }, DispatcherPriority.Input); + } + } + + /// + /// Defines the event. + /// + public static readonly RoutedEvent ValueChangedEvent = + RoutedEvent.Register(nameof(ValueChanged), RoutingStrategies.Bubble); + + /// + /// Raised when the changes. + /// + public event EventHandler ValueChanged + { + add { AddHandler(ValueChangedEvent, value); } + remove { RemoveHandler(ValueChangedEvent, value); } + } + + private bool CommitInput() + { + return SyncTextAndValueProperties(true, Text); + } + + /// + /// Synchronize and properties. + /// + /// If value should be updated from text. + /// The text. + private bool SyncTextAndValueProperties(bool updateValueFromText, string text) + { + return SyncTextAndValueProperties(updateValueFromText, text, false); + } + + /// + /// Synchronize and properties. + /// + /// If value should be updated from text. + /// The text. + /// Force text update. + private bool SyncTextAndValueProperties(bool updateValueFromText, string text, bool forceTextUpdate) + { + if (_isSyncingTextAndValueProperties) + return true; + + _isSyncingTextAndValueProperties = true; + var parsedTextIsValid = true; + try + { + if (updateValueFromText) + { + if (!string.IsNullOrEmpty(text)) + { + try + { + var newValue = ConvertTextToValue(text); + if (!Equals(newValue, Value)) + { + SetValueInternal(newValue); + } + } + catch + { + parsedTextIsValid = false; + } + } + } + + // Do not touch the ongoing text input from user. + if (!_isTextChangedFromUI) + { + var keepEmpty = !forceTextUpdate && string.IsNullOrEmpty(Text); + if (!keepEmpty) + { + var newText = ConvertValueToText(); + if (!Equals(Text, newText)) + { + Text = newText; + } + } + + // Sync Text and textBox + if (TextBox != null) + { + TextBox.Text = Text; + } + } + + if (_isTextChangedFromUI && !parsedTextIsValid) + { + // Text input was made from the user and the text + // repesents an invalid value. Disable the spinner in this case. + if (Spinner != null) + { + Spinner.ValidSpinDirection = ValidSpinDirections.None; + } + } + else + { + SetValidSpinDirection(); + } + } + finally + { + _isSyncingTextAndValueProperties = false; + } + return parsedTextIsValid; + } + + private double ConvertTextToValueCore(string currentValueText, string text) + { + double result; + + if (IsPercent(FormatString)) + { + result = decimal.ToDouble(ParsePercent(text, CultureInfo)); + } + else + { + // Problem while converting new text + if (!double.TryParse(text, ParsingNumberStyle, CultureInfo, out var outputValue)) + { + var shouldThrow = true; + + // Check if CurrentValueText is also failing => it also contains special characters. ex : 90° + if (!double.TryParse(currentValueText, ParsingNumberStyle, CultureInfo, out var _)) + { + // extract non-digit characters + var currentValueTextSpecialCharacters = currentValueText.Where(c => !char.IsDigit(c)); + var textSpecialCharacters = text.Where(c => !char.IsDigit(c)); + // same non-digit characters on currentValueText and new text => remove them on new Text to parse it again. + if (currentValueTextSpecialCharacters.Except(textSpecialCharacters).ToList().Count == 0) + { + foreach (var character in textSpecialCharacters) + { + text = text.Replace(character.ToString(), string.Empty); + } + // if without the special characters, parsing is good, do not throw + if (double.TryParse(text, ParsingNumberStyle, CultureInfo, out outputValue)) + { + shouldThrow = false; + } + } + } + + if (shouldThrow) + { + throw new InvalidDataException("Input string was not in a correct format."); + } + } + result = outputValue; + } + return result; + } + + private void ValidateMinMax(double value) + { + if (value < Minimum) + { + throw new ArgumentOutOfRangeException(nameof(Minimum), string.Format("Value must be greater than Minimum value of {0}", Minimum)); + } + else if (value > Maximum) + { + throw new ArgumentOutOfRangeException(nameof(Maximum), string.Format("Value must be less than Maximum value of {0}", Maximum)); + } + } + + /// + /// Parse percent format text + /// + /// Text to parse. + /// The culture info. + private static decimal ParsePercent(string text, IFormatProvider cultureInfo) + { + var info = NumberFormatInfo.GetInstance(cultureInfo); + text = text.Replace(info.PercentSymbol, null); + var result = decimal.Parse(text, NumberStyles.Any, info); + result = result / 100; + return result; + } + + + private bool IsPercent(string stringToTest) + { + var PIndex = stringToTest.IndexOf("P", StringComparison.Ordinal); + if (PIndex >= 0) + { + //stringToTest contains a "P" between 2 "'", it's considered as text, not percent + var isText = stringToTest.Substring(0, PIndex).Contains("'") + && stringToTest.Substring(PIndex, FormatString.Length - PIndex).Contains("'"); + + return !isText; + } + return false; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs new file mode 100644 index 0000000000..e994ffdd15 --- /dev/null +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs @@ -0,0 +1,16 @@ +using Avalonia.Interactivity; + +namespace Avalonia.Controls +{ + public class NumericUpDownValueChangedEventArgs : RoutedEventArgs + { + public NumericUpDownValueChangedEventArgs(RoutedEvent routedEvent, double oldValue, double newValue) : base(routedEvent) + { + OldValue = oldValue; + NewValue = newValue; + } + + public double OldValue { get; } + public double NewValue { get; } + } +} diff --git a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs index 0a01cf3df4..4f7ac82df7 100644 --- a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs @@ -55,7 +55,7 @@ namespace Avalonia.Platform /// Gets the platform window handle. /// IPlatformHandle Handle { get; } - + /// /// Gets the maximum size of a window on the system. /// @@ -65,7 +65,13 @@ namespace Avalonia.Platform /// Sets the client size of the toplevel. /// void Resize(Size clientSize); - + + /// + /// Minimum width of the window. + /// + /// + void SetMinMaxSize(Size minSize, Size maxSize); + /// /// Gets platform specific display information /// diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index 37637b1624..3f2c977718 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -44,5 +44,16 @@ namespace Avalonia.Platform /// Enables or disables the taskbar icon /// void ShowTaskbarIcon(bool value); + + /// + /// Enables or disables resizing of the window + /// + void CanResize(bool value); + + /// + /// Gets or sets a method called before the underlying implementation is destroyed. + /// Return true to prevent the underlying implementation from closing. + /// + Func Closing { get; set; } } } diff --git a/src/Avalonia.Controls/Platform/InProcessDragSource.cs b/src/Avalonia.Controls/Platform/InProcessDragSource.cs new file mode 100644 index 0000000000..e136efe2a9 --- /dev/null +++ b/src/Avalonia.Controls/Platform/InProcessDragSource.cs @@ -0,0 +1,210 @@ +using System; +using System.Linq; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Input.Platform; +using Avalonia.Input.Raw; +using Avalonia.Threading; +using Avalonia.VisualTree; + +namespace Avalonia.Platform +{ + class InProcessDragSource : IPlatformDragSource + { + private const InputModifiers MOUSE_INPUTMODIFIERS = InputModifiers.LeftMouseButton|InputModifiers.MiddleMouseButton|InputModifiers.RightMouseButton; + private readonly IDragDropDevice _dragDrop; + private readonly IInputManager _inputManager; + private readonly Subject _result = new Subject(); + + private DragDropEffects _allowedEffects; + private IDataObject _draggedData; + private IInputElement _lastRoot; + private Point _lastPosition; + private StandardCursorType _lastCursorType; + private object _originalCursor; + private InputModifiers? _initialInputModifiers; + + public InProcessDragSource() + { + _inputManager = AvaloniaLocator.Current.GetService(); + _dragDrop = AvaloniaLocator.Current.GetService(); + } + + public async Task DoDragDrop(IDataObject data, DragDropEffects allowedEffects) + { + Dispatcher.UIThread.VerifyAccess(); + if (_draggedData == null) + { + _draggedData = data; + _lastRoot = null; + _lastPosition = default(Point); + _allowedEffects = allowedEffects; + + using (_inputManager.PreProcess.OfType().Subscribe(ProcessMouseEvents)) + { + using (_inputManager.PreProcess.OfType().Subscribe(ProcessKeyEvents)) + { + var effect = await _result.FirstAsync(); + return effect; + } + } + } + return DragDropEffects.None; + } + + + private DragDropEffects RaiseEventAndUpdateCursor(RawDragEventType type, IInputElement root, Point pt, InputModifiers modifiers) + { + _lastPosition = pt; + + RawDragEvent rawEvent = new RawDragEvent(_dragDrop, type, root, pt, _draggedData, _allowedEffects); + var tl = root.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); + tl.PlatformImpl.Input(rawEvent); + + var effect = GetPreferredEffect(rawEvent.Effects & _allowedEffects, modifiers); + UpdateCursor(root, effect); + return effect; + } + + private DragDropEffects GetPreferredEffect(DragDropEffects effect, InputModifiers modifiers) + { + if (effect == DragDropEffects.Copy || effect == DragDropEffects.Move || effect == DragDropEffects.Link || effect == DragDropEffects.None) + return effect; // No need to check for the modifiers. + if (effect.HasFlag(DragDropEffects.Link) && modifiers.HasFlag(InputModifiers.Alt)) + return DragDropEffects.Link; + if (effect.HasFlag(DragDropEffects.Copy) && modifiers.HasFlag(InputModifiers.Control)) + return DragDropEffects.Copy; + return DragDropEffects.Move; + } + + private StandardCursorType GetCursorForDropEffect(DragDropEffects effects) + { + if (effects.HasFlag(DragDropEffects.Copy)) + return StandardCursorType.DragCopy; + if (effects.HasFlag(DragDropEffects.Move)) + return StandardCursorType.DragMove; + if (effects.HasFlag(DragDropEffects.Link)) + return StandardCursorType.DragLink; + return StandardCursorType.No; + } + + private void UpdateCursor(IInputElement root, DragDropEffects effect) + { + if (_lastRoot != root) + { + if (_lastRoot is InputElement ieLast) + { + if (_originalCursor == AvaloniaProperty.UnsetValue) + ieLast.ClearValue(InputElement.CursorProperty); + else + ieLast.Cursor = _originalCursor as Cursor; + } + + if (root is InputElement ieNew) + { + if (!ieNew.IsSet(InputElement.CursorProperty)) + _originalCursor = AvaloniaProperty.UnsetValue; + else + _originalCursor = root.Cursor; + } + else + _originalCursor = null; + + _lastCursorType = StandardCursorType.Arrow; + _lastRoot = root; + } + + if (root is InputElement ie) + { + var ct = GetCursorForDropEffect(effect); + if (ct != _lastCursorType) + { + _lastCursorType = ct; + ie.Cursor = new Cursor(ct); + } + } + } + + private void CancelDragging() + { + if (_lastRoot != null) + RaiseEventAndUpdateCursor(RawDragEventType.DragLeave, _lastRoot, _lastPosition, InputModifiers.None); + UpdateCursor(null, DragDropEffects.None); + _result.OnNext(DragDropEffects.None); + } + + private void ProcessKeyEvents(RawKeyEventArgs e) + { + if (e.Type == RawKeyEventType.KeyDown && e.Key == Key.Escape) + { + if (_lastRoot != null) + RaiseEventAndUpdateCursor(RawDragEventType.DragLeave, _lastRoot, _lastPosition, e.Modifiers); + UpdateCursor(null, DragDropEffects.None); + _result.OnNext(DragDropEffects.None); + e.Handled = true; + } + else if (e.Key == Key.LeftCtrl || e.Key == Key.RightCtrl || e.Key == Key.LeftAlt || e.Key == Key.RightAlt) + RaiseEventAndUpdateCursor(RawDragEventType.DragOver, _lastRoot, _lastPosition, e.Modifiers); + } + + private void ProcessMouseEvents(RawMouseEventArgs e) + { + if (!_initialInputModifiers.HasValue) + _initialInputModifiers = e.InputModifiers & MOUSE_INPUTMODIFIERS; + + + void CheckDraggingAccepted(InputModifiers changedMouseButton) + { + if (_initialInputModifiers.Value.HasFlag(changedMouseButton)) + { + var result = RaiseEventAndUpdateCursor(RawDragEventType.Drop, e.Root, e.Position, e.InputModifiers); + UpdateCursor(null, DragDropEffects.None); + _result.OnNext(result); + } + else + CancelDragging(); + e.Handled = true; + } + + switch (e.Type) + { + case RawMouseEventType.LeftButtonDown: + case RawMouseEventType.RightButtonDown: + case RawMouseEventType.MiddleButtonDown: + case RawMouseEventType.NonClientLeftButtonDown: + CancelDragging(); + e.Handled = true; + return; + case RawMouseEventType.LeaveWindow: + RaiseEventAndUpdateCursor(RawDragEventType.DragLeave, e.Root, e.Position, e.InputModifiers); break; + case RawMouseEventType.LeftButtonUp: + CheckDraggingAccepted(InputModifiers.LeftMouseButton); break; + case RawMouseEventType.MiddleButtonUp: + CheckDraggingAccepted(InputModifiers.MiddleMouseButton); break; + case RawMouseEventType.RightButtonUp: + CheckDraggingAccepted(InputModifiers.RightMouseButton); break; + case RawMouseEventType.Move: + var mods = e.InputModifiers & MOUSE_INPUTMODIFIERS; + if (_initialInputModifiers.Value != mods) + { + CancelDragging(); + e.Handled = true; + return; + } + + if (e.Root != _lastRoot) + { + if (_lastRoot != null) + RaiseEventAndUpdateCursor(RawDragEventType.DragLeave, _lastRoot, _lastRoot.PointToClient(e.Root.PointToScreen(e.Position)), e.InputModifiers); + RaiseEventAndUpdateCursor(RawDragEventType.DragEnter, e.Root, e.Position, e.InputModifiers); + } + else + RaiseEventAndUpdateCursor(RawDragEventType.DragOver, e.Root, e.Position, e.InputModifiers); + break; + } + } + } +} diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index d0a438cc2b..3cc750e20d 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -4,6 +4,7 @@ using System; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Controls.Utils; using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Media; @@ -31,7 +32,7 @@ namespace Avalonia.Controls.Presenters /// /// Defines the property. /// - public static readonly StyledProperty BorderThicknessProperty = + public static readonly StyledProperty BorderThicknessProperty = Border.BorderThicknessProperty.AddOwner(); /// @@ -57,7 +58,7 @@ namespace Avalonia.Controls.Presenters /// /// Defines the property. /// - public static readonly StyledProperty CornerRadiusProperty = + public static readonly StyledProperty CornerRadiusProperty = Border.CornerRadiusProperty.AddOwner(); /// @@ -76,17 +77,20 @@ namespace Avalonia.Controls.Presenters /// Defines the property. /// public static readonly StyledProperty PaddingProperty = - Border.PaddingProperty.AddOwner(); + Decorator.PaddingProperty.AddOwner(); private IControl _child; private bool _createdChild; private IDataTemplate _dataTemplate; + private readonly BorderRenderHelper _borderRenderer = new BorderRenderHelper(); /// /// Initializes static members of the class. /// static ContentPresenter() { + AffectsRender(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty); + AffectsMeasure(BorderThicknessProperty); ContentProperty.Changed.AddClassHandler(x => x.ContentChanged); ContentTemplateProperty.Changed.AddClassHandler(x => x.ContentChanged); TemplatedParentProperty.Changed.AddClassHandler(x => x.TemplatedParentChanged); @@ -120,7 +124,7 @@ namespace Avalonia.Controls.Presenters /// /// Gets or sets the thickness of the border. /// - public double BorderThickness + public Thickness BorderThickness { get { return GetValue(BorderThicknessProperty); } set { SetValue(BorderThicknessProperty, value); } @@ -157,7 +161,7 @@ namespace Avalonia.Controls.Presenters /// /// Gets or sets the radius of the border rounded corners. /// - public float CornerRadius + public CornerRadius CornerRadius { get { return GetValue(CornerRadiusProperty); } set { SetValue(CornerRadiusProperty, value); } @@ -221,7 +225,7 @@ namespace Avalonia.Controls.Presenters { var content = Content; var oldChild = Child; - var newChild = CreateChild(); + var newChild = CreateChild(); // Remove the old child if we're not recycling it. if (oldChild != null && newChild != oldChild) @@ -277,21 +281,7 @@ namespace Avalonia.Controls.Presenters /// public override void Render(DrawingContext context) { - var background = Background; - var borderBrush = BorderBrush; - var borderThickness = BorderThickness; - var cornerRadius = CornerRadius; - var rect = new Rect(Bounds.Size).Deflate(BorderThickness); - - if (background != null) - { - context.FillRectangle(background, rect, cornerRadius); - } - - if (borderBrush != null && borderThickness > 0) - { - context.DrawRectangle(new Pen(borderBrush, borderThickness), rect, cornerRadius); - } + _borderRenderer.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush); } /// @@ -344,7 +334,11 @@ namespace Avalonia.Controls.Presenters /// protected override Size ArrangeOverride(Size finalSize) { - return ArrangeOverrideImpl(finalSize, new Vector()); + finalSize = ArrangeOverrideImpl(finalSize, new Vector()); + + _borderRenderer.Update(finalSize, BorderThickness, CornerRadius); + + return finalSize; } /// @@ -372,74 +366,69 @@ namespace Avalonia.Controls.Presenters internal Size ArrangeOverrideImpl(Size finalSize, Vector offset) { - if (Child != null) - { - var padding = Padding; - var borderThickness = BorderThickness; - var horizontalContentAlignment = HorizontalContentAlignment; - var verticalContentAlignment = VerticalContentAlignment; - var useLayoutRounding = UseLayoutRounding; - var availableSizeMinusMargins = new Size( - Math.Max(0, finalSize.Width - padding.Left - padding.Right - borderThickness), - Math.Max(0, finalSize.Height - padding.Top - padding.Bottom - borderThickness)); - var size = availableSizeMinusMargins; - var scale = GetLayoutScale(); - var originX = offset.X + padding.Left + borderThickness; - var originY = offset.Y + padding.Top + borderThickness; - - if (horizontalContentAlignment != HorizontalAlignment.Stretch) - { - size = size.WithWidth(Math.Min(size.Width, DesiredSize.Width - padding.Left - padding.Right)); - } + if (Child == null) return finalSize; - if (verticalContentAlignment != VerticalAlignment.Stretch) - { - size = size.WithHeight(Math.Min(size.Height, DesiredSize.Height - padding.Top - padding.Bottom)); - } - - size = LayoutHelper.ApplyLayoutConstraints(Child, size); + var padding = Padding; + var borderThickness = BorderThickness; + var horizontalContentAlignment = HorizontalContentAlignment; + var verticalContentAlignment = VerticalContentAlignment; + var useLayoutRounding = UseLayoutRounding; + var availableSizeMinusMargins = new Size( + Math.Max(0, finalSize.Width - padding.Left - padding.Right - borderThickness.Left - borderThickness.Right), + Math.Max(0, finalSize.Height - padding.Top - padding.Bottom - borderThickness.Top - borderThickness.Bottom)); + var size = availableSizeMinusMargins; + var scale = GetLayoutScale(); + var originX = offset.X + padding.Left + borderThickness.Left; + var originY = offset.Y + padding.Top + borderThickness.Top; + + if (horizontalContentAlignment != HorizontalAlignment.Stretch) + { + size = size.WithWidth(Math.Min(size.Width, DesiredSize.Width - padding.Left - padding.Right)); + } - if (useLayoutRounding) - { - size = new Size( - Math.Ceiling(size.Width * scale) / scale, - Math.Ceiling(size.Height * scale) / scale); - availableSizeMinusMargins = new Size( - Math.Ceiling(availableSizeMinusMargins.Width * scale) / scale, - Math.Ceiling(availableSizeMinusMargins.Height * scale) / scale); - } + if (verticalContentAlignment != VerticalAlignment.Stretch) + { + size = size.WithHeight(Math.Min(size.Height, DesiredSize.Height - padding.Top - padding.Bottom)); + } - switch (horizontalContentAlignment) - { - case HorizontalAlignment.Center: - case HorizontalAlignment.Stretch: - originX += (availableSizeMinusMargins.Width - size.Width) / 2; - break; - case HorizontalAlignment.Right: - originX += availableSizeMinusMargins.Width - size.Width; - break; - } + if (useLayoutRounding) + { + size = new Size( + Math.Ceiling(size.Width * scale) / scale, + Math.Ceiling(size.Height * scale) / scale); + availableSizeMinusMargins = new Size( + Math.Ceiling(availableSizeMinusMargins.Width * scale) / scale, + Math.Ceiling(availableSizeMinusMargins.Height * scale) / scale); + } - switch (verticalContentAlignment) - { - case VerticalAlignment.Center: - case VerticalAlignment.Stretch: - originY += (availableSizeMinusMargins.Height - size.Height) / 2; - break; - case VerticalAlignment.Bottom: - originY += availableSizeMinusMargins.Height - size.Height; - break; - } + switch (horizontalContentAlignment) + { + case HorizontalAlignment.Center: + originX += (availableSizeMinusMargins.Width - size.Width) / 2; + break; + case HorizontalAlignment.Right: + originX += availableSizeMinusMargins.Width - size.Width; + break; + } - if (useLayoutRounding) - { - originX = Math.Floor(originX * scale) / scale; - originY = Math.Floor(originY * scale) / scale; - } + switch (verticalContentAlignment) + { + case VerticalAlignment.Center: + originY += (availableSizeMinusMargins.Height - size.Height) / 2; + break; + case VerticalAlignment.Bottom: + originY += availableSizeMinusMargins.Height - size.Height; + break; + } - Child.Arrange(new Rect(originX, originY, size.Width, size.Height)); + if (useLayoutRounding) + { + originX = Math.Floor(originX * scale) / scale; + originY = Math.Floor(originY * scale) / scale; } + Child.Arrange(new Rect(originX, originY, Math.Max(0, size.Width), Math.Max(0, size.Height))); + return finalSize; } diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index a68979cfa1..8d0c6f16cb 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -224,7 +224,7 @@ namespace Avalonia.Controls.Presenters CanVerticallyScroll ? Math.Max(Child.DesiredSize.Height, finalSize.Height) : finalSize.Height); ArrangeOverrideImpl(size, -Offset); Viewport = finalSize; - Extent = Child.Bounds.Size; + Extent = Child.Bounds.Size.Inflate(Child.Margin); return finalSize; } diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 5cd3b22fc9..656f3890cd 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -40,6 +40,12 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty PlacementModeProperty = AvaloniaProperty.Register(nameof(PlacementMode), defaultValue: PlacementMode.Bottom); + /// + /// Defines the property. + /// + public static readonly StyledProperty ObeyScreenEdgesProperty = + AvaloniaProperty.Register(nameof(ObeyScreenEdges)); + /// /// Defines the property. /// @@ -136,6 +142,16 @@ namespace Avalonia.Controls.Primitives set { SetValue(PlacementModeProperty, value); } } + /// + /// Gets or sets a value indicating whether the popup positions itself within the nearest screen boundary + /// when its opened at a position where it would otherwise overlap the screen edge. + /// + public bool ObeyScreenEdges + { + get => GetValue(ObeyScreenEdgesProperty); + set => SetValue(ObeyScreenEdgesProperty, value); + } + /// /// Gets or sets the Horizontal offset of the popup in relation to the /// @@ -215,7 +231,17 @@ namespace Avalonia.Controls.Primitives { var window = _topLevel as Window; if (window != null) + { window.Deactivated += WindowDeactivated; + } + else + { + var parentPopuproot = _topLevel as PopupRoot; + if (parentPopuproot != null && parentPopuproot.Parent != null) + { + ((Popup)(parentPopuproot.Parent)).Closed += ParentClosed; + } + } _topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel); _nonClientListener = InputManager.Instance.Process.Subscribe(ListenForNonClientClick); } @@ -224,6 +250,11 @@ namespace Avalonia.Controls.Primitives _popupRoot.Show(); + if (ObeyScreenEdges) + { + _popupRoot.SnapInsideScreenEdges(); + } + _ignoreIsOpenChanged = true; IsOpen = true; _ignoreIsOpenChanged = false; @@ -244,6 +275,14 @@ namespace Avalonia.Controls.Primitives var window = _topLevel as Window; if (window != null) window.Deactivated -= WindowDeactivated; + else + { + var parentPopuproot = _topLevel as PopupRoot; + if (parentPopuproot != null && parentPopuproot.Parent != null) + { + ((Popup)parentPopuproot.Parent).Closed -= ParentClosed; + } + } _nonClientListener?.Dispose(); _nonClientListener = null; } @@ -328,8 +367,10 @@ namespace Avalonia.Controls.Primitives /// The popup's position in screen coordinates. protected virtual Point GetPosition() { - return GetPosition(PlacementTarget ?? this.GetVisualParent(), PlacementMode, PopupRoot, + var result = GetPosition(PlacementTarget ?? this.GetVisualParent(), PlacementMode, PopupRoot, HorizontalOffset, VerticalOffset); + + return result; } internal static Point GetPosition(Control target, PlacementMode placement, PopupRoot popupRoot, double horizontalOffset, double verticalOffset) @@ -381,9 +422,7 @@ namespace Avalonia.Controls.Primitives { if (!StaysOpen) { - var root = ((IVisual)e.Source).GetVisualRoot(); - - if (root != this.PopupRoot) + if (!IsChildOrThis((IVisual)e.Source)) { Close(); e.Handled = true; @@ -391,6 +430,17 @@ namespace Avalonia.Controls.Primitives } } + private bool IsChildOrThis(IVisual child) + { + IVisual root = child.GetVisualRoot(); + while (root is PopupRoot) + { + if (root == PopupRoot) return true; + root = ((PopupRoot)root).Parent.GetVisualRoot(); + } + return false; + } + private void WindowDeactivated(object sender, EventArgs e) { if (!StaysOpen) @@ -398,5 +448,13 @@ namespace Avalonia.Controls.Primitives Close(); } } + + private void ParentClosed(object sender, EventArgs e) + { + if (!StaysOpen) + { + Close(); + } + } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index 507a085fed..457a7bd4b4 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -2,10 +2,12 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Linq; using Avalonia.Controls.Platform; using Avalonia.Controls.Presenters; using Avalonia.Interactivity; using Avalonia.Layout; +using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Styling; @@ -75,6 +77,30 @@ namespace Avalonia.Controls.Primitives /// public void Dispose() => PlatformImpl?.Dispose(); + /// + /// Moves the Popups position so that it doesnt overlap screen edges. + /// This method can be called immediately after Show has been called. + /// + public void SnapInsideScreenEdges() + { + var window = this.GetSelfAndLogicalAncestors().OfType().First(); + + var screen = window.Screens.ScreenFromPoint(Position); + + var screenX = Position.X + Bounds.Width - screen.Bounds.X; + var screenY = Position.Y + Bounds.Height - screen.Bounds.Y; + + if (screenX > screen.Bounds.Width) + { + Position = Position.WithX(Position.X - (screenX - screen.Bounds.Width)); + } + + if (screenY > screen.Bounds.Height) + { + Position = Position.WithY(Position.Y - (screenY - screen.Bounds.Height)); + } + } + /// protected override void OnTemplateApplied(TemplateAppliedEventArgs e) { diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index 6deef7c7b9..1a805a3822 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -32,7 +32,7 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property. /// - public static readonly StyledProperty BorderThicknessProperty = + public static readonly StyledProperty BorderThicknessProperty = Border.BorderThicknessProperty.AddOwner(); /// @@ -132,7 +132,7 @@ namespace Avalonia.Controls.Primitives /// /// Gets or sets the thickness of the control's border. /// - public double BorderThickness + public Thickness BorderThickness { get { return GetValue(BorderThicknessProperty); } set { SetValue(BorderThicknessProperty, value); } @@ -207,7 +207,7 @@ namespace Avalonia.Controls.Primitives /// The control. /// The property value. /// - public bool GetIsTemplateFocusTarget(Control control) + public static bool GetIsTemplateFocusTarget(Control control) { return control.GetValue(IsTemplateFocusTargetProperty); } @@ -223,7 +223,7 @@ namespace Avalonia.Controls.Primitives /// attached property is set to true on an element in the control template, then the focus /// adorner will be shown around that control instead. /// - public void SetIsTemplateFocusTarget(Control control, bool value) + public static void SetIsTemplateFocusTarget(Control control, bool value) { control.SetValue(IsTemplateFocusTargetProperty, value); } diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index 8cf6b149cb..34954dd0d5 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -26,12 +26,13 @@ namespace Avalonia.Controls static ProgressBar() { + PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical"); + PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal"); + ValueProperty.Changed.AddClassHandler(x => x.ValueChanged); IsIndeterminateProperty.Changed.AddClassHandler( (p, e) => { if (p._indicator != null) p.UpdateIsIndeterminate((bool)e.NewValue); }); - OrientationProperty.Changed.AddClassHandler( - (p, e) => { if (p._indicator != null) p.UpdateOrientation((Orientation)e.NewValue); }); } public bool IsIndeterminate @@ -59,7 +60,6 @@ namespace Avalonia.Controls _indicator = e.NameScope.Get("PART_Indicator"); UpdateIndicator(Bounds.Size); - UpdateOrientation(Orientation); UpdateIsIndeterminate(IsIndeterminate); } @@ -86,26 +86,6 @@ namespace Avalonia.Controls } } - private void UpdateOrientation(Orientation orientation) - { - if (orientation == Orientation.Horizontal) - { - MinHeight = 14; - MinWidth = 200; - - _indicator.HorizontalAlignment = HorizontalAlignment.Left; - _indicator.VerticalAlignment = VerticalAlignment.Stretch; - } - else - { - MinHeight = 200; - MinWidth = 14; - - _indicator.HorizontalAlignment = HorizontalAlignment.Stretch; - _indicator.VerticalAlignment = VerticalAlignment.Bottom; - } - } - private void UpdateIsIndeterminate(bool isIndeterminate) { if (isIndeterminate) diff --git a/src/Avalonia.Controls/Remote/RemoteWidget.cs b/src/Avalonia.Controls/Remote/RemoteWidget.cs index 83360a0010..ea8c3ebe52 100644 --- a/src/Avalonia.Controls/Remote/RemoteWidget.cs +++ b/src/Avalonia.Controls/Remote/RemoteWidget.cs @@ -14,7 +14,7 @@ namespace Avalonia.Controls.Remote { private readonly IAvaloniaRemoteTransportConnection _connection; private FrameMessage _lastFrame; - private WritableBitmap _bitmap; + private WriteableBitmap _bitmap; public RemoteWidget(IAvaloniaRemoteTransportConnection connection) { _connection = connection; @@ -62,7 +62,7 @@ namespace Avalonia.Controls.Remote var fmt = (PixelFormat) _lastFrame.Format; if (_bitmap == null || _bitmap.PixelWidth != _lastFrame.Width || _bitmap.PixelHeight != _lastFrame.Height) - _bitmap = new WritableBitmap(_lastFrame.Width, _lastFrame.Height, fmt); + _bitmap = new WriteableBitmap(_lastFrame.Width, _lastFrame.Height, fmt); using (var l = _bitmap.Lock()) { var lineLen = (fmt == PixelFormat.Rgb565 ? 2 : 4) * _lastFrame.Width; diff --git a/src/Avalonia.Controls/RepeatButton.cs b/src/Avalonia.Controls/RepeatButton.cs index a9ccb79fe1..07a1e82638 100644 --- a/src/Avalonia.Controls/RepeatButton.cs +++ b/src/Avalonia.Controls/RepeatButton.cs @@ -6,17 +6,32 @@ namespace Avalonia.Controls { public class RepeatButton : Button { + /// + /// Defines the property. + /// + public static readonly StyledProperty IntervalProperty = + AvaloniaProperty.Register(nameof(Interval), 100); + /// /// Defines the property. /// public static readonly StyledProperty DelayProperty = - AvaloniaProperty.Register(nameof(Delay), 100); + AvaloniaProperty.Register(nameof(Delay), 300); private DispatcherTimer _repeatTimer; /// /// Gets or sets the amount of time, in milliseconds, of repeating clicks. /// + public int Interval + { + get { return GetValue(IntervalProperty); } + set { SetValue(IntervalProperty, value); } + } + + /// + /// Gets or sets the amount of time, in milliseconds, to wait before repeating begins. + /// public int Delay { get { return GetValue(DelayProperty); } @@ -28,7 +43,7 @@ namespace Avalonia.Controls if (_repeatTimer == null) { _repeatTimer = new DispatcherTimer(); - _repeatTimer.Tick += (o, e) => OnClick(); + _repeatTimer.Tick += RepeatTimerOnTick; } if (_repeatTimer.IsEnabled) return; @@ -37,6 +52,16 @@ namespace Avalonia.Controls _repeatTimer.Start(); } + private void RepeatTimerOnTick(object sender, EventArgs e) + { + var interval = TimeSpan.FromMilliseconds(Interval); + if (_repeatTimer.Interval != interval) + { + _repeatTimer.Interval = interval; + } + OnClick(); + } + private void StopTimer() { _repeatTimer?.Stop(); diff --git a/src/Avalonia.Controls/RowDefinitions.cs b/src/Avalonia.Controls/RowDefinitions.cs index e677492580..2dfad7111a 100644 --- a/src/Avalonia.Controls/RowDefinitions.cs +++ b/src/Avalonia.Controls/RowDefinitions.cs @@ -27,7 +27,7 @@ namespace Avalonia.Controls public RowDefinitions(string s) : this() { - AddRange(GridLength.ParseLengths(s, CultureInfo.InvariantCulture).Select(x => new RowDefinition(x))); + AddRange(GridLength.ParseLengths(s).Select(x => new RowDefinition(x))); } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Screens.cs b/src/Avalonia.Controls/Screens.cs index b8ddce3aea..2bfddc048b 100644 --- a/src/Avalonia.Controls/Screens.cs +++ b/src/Avalonia.Controls/Screens.cs @@ -39,7 +39,7 @@ namespace Avalonia.Controls return currMaxScreen; } - public Screen SceenFromPoint(Point point) + public Screen ScreenFromPoint(Point point) { return All.FirstOrDefault(x=>x.Bounds.Contains(point)); } diff --git a/src/Avalonia.Controls/Spinner.cs b/src/Avalonia.Controls/Spinner.cs new file mode 100644 index 0000000000..e00ff3823c --- /dev/null +++ b/src/Avalonia.Controls/Spinner.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Interactivity; + +namespace Avalonia.Controls +{ + /// + /// Represents spin directions that are valid. + /// + [Flags] + public enum ValidSpinDirections + { + /// + /// Can not increase nor decrease. + /// + None = 0, + + /// + /// Can increase. + /// + Increase = 1, + + /// + /// Can decrease. + /// + Decrease = 2 + } + + /// + /// Represents spin directions that could be initiated by the end-user. + /// + public enum SpinDirection + { + /// + /// Represents a spin initiated by the end-user in order to Increase a value. + /// + Increase = 0, + + /// + /// Represents a spin initiated by the end-user in order to Decrease a value. + /// + Decrease = 1 + } + + /// + /// Provides data for the Spinner.Spin event. + /// + public class SpinEventArgs : RoutedEventArgs + { + /// + /// Gets the SpinDirection for the spin that has been initiated by the end-user. + /// + public SpinDirection Direction { get; } + + /// + /// Get or set whheter the spin event originated from a mouse wheel event. + /// + public bool UsingMouseWheel{ get; } + + /// + /// Initializes a new instance of the SpinEventArgs class. + /// + /// Spin direction. + public SpinEventArgs(SpinDirection direction) + { + Direction = direction; + } + + public SpinEventArgs(RoutedEvent routedEvent, SpinDirection direction) + : base(routedEvent) + { + Direction = direction; + } + + public SpinEventArgs(SpinDirection direction, bool usingMouseWheel) + { + Direction = direction; + UsingMouseWheel = usingMouseWheel; + } + + public SpinEventArgs(RoutedEvent routedEvent, SpinDirection direction, bool usingMouseWheel) + : base(routedEvent) + { + Direction = direction; + UsingMouseWheel = usingMouseWheel; + } + } + + /// + /// Base class for controls that represents controls that can spin. + /// + public abstract class Spinner : ContentControl + { + /// + /// Defines the property. + /// + public static readonly StyledProperty ValidSpinDirectionProperty = + AvaloniaProperty.Register(nameof(ValidSpinDirection), + ValidSpinDirections.Increase | ValidSpinDirections.Decrease); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent SpinEvent = + RoutedEvent.Register(nameof(Spin), RoutingStrategies.Bubble); + + /// + /// Initializes static members of the class. + /// + static Spinner() + { + ValidSpinDirectionProperty.Changed.Subscribe(OnValidSpinDirectionPropertyChanged); + } + + /// + /// Occurs when spinning is initiated by the end-user. + /// + public event EventHandler Spin + { + add { AddHandler(SpinEvent, value); } + remove { RemoveHandler(SpinEvent, value); } + } + + /// + /// Gets or sets allowed for this control. + /// + public ValidSpinDirections ValidSpinDirection + { + get { return GetValue(ValidSpinDirectionProperty); } + set { SetValue(ValidSpinDirectionProperty, value); } + } + + /// + /// Called when valid spin direction changed. + /// + /// The old value. + /// The new value. + protected virtual void OnValidSpinDirectionChanged(ValidSpinDirections oldValue, ValidSpinDirections newValue) + { + } + + /// + /// Raises the OnSpin event when spinning is initiated by the end-user. + /// + /// Spin event args. + protected virtual void OnSpin(SpinEventArgs e) + { + var valid = e.Direction == SpinDirection.Increase + ? ValidSpinDirections.Increase + : ValidSpinDirections.Decrease; + + //Only raise the event if spin is allowed. + if ((ValidSpinDirection & valid) == valid) + { + RaiseEvent(e); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnValidSpinDirectionPropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is Spinner spinner) + { + var oldValue = (ValidSpinDirections)e.OldValue; + var newValue = (ValidSpinDirections)e.NewValue; + spinner.OnValidSpinDirectionChanged(oldValue, newValue); + } + } + } +} diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index c7a77bdf0e..88a9fe077d 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -120,6 +120,7 @@ namespace Avalonia.Controls .Subscribe(_ => { InvalidateFormattedText(); + InvalidateMeasure(); }); } @@ -370,8 +371,6 @@ namespace Avalonia.Controls _constraint = _formattedText.Constraint; _formattedText = null; } - - InvalidateMeasure(); } /// @@ -402,6 +401,7 @@ namespace Avalonia.Controls { base.OnAttachedToLogicalTree(e); InvalidateFormattedText(); + InvalidateMeasure(); } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 3ec3d6ed5b..890926db54 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -85,6 +85,7 @@ namespace Avalonia.Controls private int _selectionEnd; private TextPresenter _presenter; private UndoRedoHelper _undoRedoHelper; + private bool _isUndoingRedoing; private bool _ignoreTextChanges; private static readonly string[] invalidCharacters = new String[1]{"\u007f"}; @@ -198,7 +199,11 @@ namespace Avalonia.Controls if (!_ignoreTextChanges) { CaretIndex = CoerceCaretIndex(CaretIndex, value?.Length ?? 0); - SetAndRaise(TextProperty, ref _text, value); + + if (SetAndRaise(TextProperty, ref _text, value) && !_isUndoingRedoing) + { + _undoRedoHelper.Clear(); + } } } } @@ -256,6 +261,8 @@ namespace Avalonia.Controls { _presenter?.ShowCaret(); } + + e.Handled = true; } protected override void OnLostFocus(RoutedEventArgs e) @@ -268,7 +275,11 @@ namespace Avalonia.Controls protected override void OnTextInput(TextInputEventArgs e) { - HandleTextInput(e.Text); + if (!e.Handled) + { + HandleTextInput(e.Text); + e.Handled = true; + } } private void HandleTextInput(string input) @@ -364,14 +375,30 @@ namespace Avalonia.Controls case Key.Z: if (modifiers == InputModifiers.Control) { - _undoRedoHelper.Undo(); + try + { + _isUndoingRedoing = true; + _undoRedoHelper.Undo(); + } + finally + { + _isUndoingRedoing = false; + } handled = true; } break; case Key.Y: if (modifiers == InputModifiers.Control) { - _undoRedoHelper.Redo(); + try + { + _isUndoingRedoing = true; + _undoRedoHelper.Redo(); + } + finally + { + _isUndoingRedoing = false; + } handled = true; } break; @@ -788,7 +815,7 @@ namespace Avalonia.Controls int pos = 0; int i; - for (i = 0; i < lines.Count; ++i) + for (i = 0; i < lines.Count - 1; ++i) { var line = lines[i]; pos += line.Length; diff --git a/src/Avalonia.Controls/UserControl.cs b/src/Avalonia.Controls/UserControl.cs index 7c9e591e31..e063a65e09 100644 --- a/src/Avalonia.Controls/UserControl.cs +++ b/src/Avalonia.Controls/UserControl.cs @@ -6,6 +6,9 @@ using Avalonia.Styling; namespace Avalonia.Controls { + /// + /// Provides the base class for defining a new control that encapsulates related existing controls and provides its own logic. + /// public class UserControl : ContentControl, IStyleable, INameScope { private readonly NameScope _nameScope = new NameScope(); @@ -24,6 +27,7 @@ namespace Avalonia.Controls remove { _nameScope.Unregistered -= value; } } + /// Type IStyleable.StyleKey => typeof(ContentControl); /// diff --git a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs new file mode 100644 index 0000000000..d9169e51f3 --- /dev/null +++ b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs @@ -0,0 +1,279 @@ +// 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 Avalonia.Media; + +namespace Avalonia.Controls.Utils +{ + internal class BorderRenderHelper + { + private bool _useComplexRendering; + private StreamGeometry _backgroundGeometryCache; + private StreamGeometry _borderGeometryCache; + + public void Update(Size finalSize, Thickness borderThickness, CornerRadius cornerRadius) + { + if (borderThickness.IsUniform && cornerRadius.IsUniform) + { + _backgroundGeometryCache = null; + _borderGeometryCache = null; + _useComplexRendering = false; + } + else + { + _useComplexRendering = true; + + var boundRect = new Rect(finalSize); + var innerRect = boundRect.Deflate(borderThickness); + var innerCoordinates = new BorderCoordinates(cornerRadius, borderThickness, false); + + StreamGeometry backgroundGeometry = null; + + if (innerRect.Width != 0 && innerRect.Height != 0) + { + backgroundGeometry = new StreamGeometry(); + + using (var ctx = backgroundGeometry.Open()) + { + CreateGeometry(ctx, innerRect, innerCoordinates); + } + + _backgroundGeometryCache = backgroundGeometry; + } + else + { + _backgroundGeometryCache = null; + } + + if (boundRect.Width != 0 && innerRect.Height != 0) + { + var outerCoordinates = new BorderCoordinates(cornerRadius, borderThickness, true); + var borderGeometry = new StreamGeometry(); + + using (var ctx = borderGeometry.Open()) + { + CreateGeometry(ctx, boundRect, outerCoordinates); + + if (backgroundGeometry != null) + { + CreateGeometry(ctx, innerRect, innerCoordinates); + } + } + + _borderGeometryCache = borderGeometry; + } + else + { + _borderGeometryCache = null; + } + } + } + + public void Render(DrawingContext context, Size size, Thickness borders, CornerRadius radii, IBrush background, IBrush borderBrush) + { + if (_useComplexRendering) + { + var backgroundGeometry = _backgroundGeometryCache; + if (backgroundGeometry != null) + { + context.DrawGeometry(background, null, backgroundGeometry); + } + + var borderGeometry = _borderGeometryCache; + if (borderGeometry != null) + { + context.DrawGeometry(borderBrush, null, borderGeometry); + } + } + else + { + var borderThickness = borders.Left; + var cornerRadius = (float)radii.TopLeft; + var rect = new Rect(size); + + if (background != null) + { + context.FillRectangle(background, rect.Deflate(borders), cornerRadius); + } + + if (borderBrush != null && borderThickness > 0) + { + context.DrawRectangle(new Pen(borderBrush, borderThickness), rect.Deflate(borderThickness), cornerRadius); + } + } + } + + private static void CreateGeometry(StreamGeometryContext context, Rect boundRect, BorderCoordinates borderCoordinates) + { + var topLeft = new Point(borderCoordinates.LeftTop, 0); + var topRight = new Point(boundRect.Width - borderCoordinates.RightTop, 0); + var rightTop = new Point(boundRect.Width, borderCoordinates.TopRight); + var rightBottom = new Point(boundRect.Width, boundRect.Height - borderCoordinates.BottomRight); + var bottomRight = new Point(boundRect.Width - borderCoordinates.RightBottom, boundRect.Height); + var bottomLeft = new Point(borderCoordinates.LeftBottom, boundRect.Height); + var leftBottom = new Point(0, boundRect.Height - borderCoordinates.BottomLeft); + var leftTop = new Point(0, borderCoordinates.TopLeft); + + + if (topLeft.X > topRight.X) + { + var scaledX = borderCoordinates.LeftTop / (borderCoordinates.LeftTop + borderCoordinates.RightTop) * boundRect.Width; + topLeft = new Point(scaledX, topLeft.Y); + topRight = new Point(scaledX, topRight.Y); + } + + if (rightTop.Y > rightBottom.Y) + { + var scaledY = borderCoordinates.TopRight / (borderCoordinates.TopRight + borderCoordinates.BottomRight) * boundRect.Height; + rightTop = new Point(rightTop.X, scaledY); + rightBottom = new Point(rightBottom.X, scaledY); + } + + if (bottomRight.X < bottomLeft.X) + { + var scaledX = borderCoordinates.LeftBottom / (borderCoordinates.LeftBottom + borderCoordinates.RightBottom) * boundRect.Width; + bottomRight = new Point(scaledX, bottomRight.Y); + bottomLeft = new Point(scaledX, bottomLeft.Y); + } + + if (leftBottom.Y < leftTop.Y) + { + var scaledY = borderCoordinates.TopLeft / (borderCoordinates.TopLeft + borderCoordinates.BottomLeft) * boundRect.Height; + leftBottom = new Point(leftBottom.X, scaledY); + leftTop = new Point(leftTop.X, scaledY); + } + + var offset = new Vector(boundRect.TopLeft.X, boundRect.TopLeft.Y); + topLeft += offset; + topRight += offset; + rightTop += offset; + rightBottom += offset; + bottomRight += offset; + bottomLeft += offset; + leftBottom += offset; + leftTop += offset; + + context.BeginFigure(topLeft, true); + + //Top + context.LineTo(topRight); + + //TopRight corner + var radiusX = boundRect.TopRight.X - topRight.X; + var radiusY = rightTop.Y - boundRect.TopRight.Y; + if (radiusX != 0 || radiusY != 0) + { + context.ArcTo(rightTop, new Size(radiusY, radiusY), 0, false, SweepDirection.Clockwise); + } + + //Right + context.LineTo(rightBottom); + + //BottomRight corner + radiusX = boundRect.BottomRight.X - bottomRight.X; + radiusY = boundRect.BottomRight.Y - rightBottom.Y; + if (radiusX != 0 || radiusY != 0) + { + context.ArcTo(bottomRight, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise); + } + + //Bottom + context.LineTo(bottomLeft); + + //BottomLeft corner + radiusX = bottomLeft.X - boundRect.BottomLeft.X; + radiusY = boundRect.BottomLeft.Y - leftBottom.Y; + if (radiusX != 0 || radiusY != 0) + { + context.ArcTo(leftBottom, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise); + } + + //Left + context.LineTo(leftTop); + + //TopLeft corner + radiusX = topLeft.X - boundRect.TopLeft.X; + radiusY = leftTop.Y - boundRect.TopLeft.Y; + + if (radiusX != 0 || radiusY != 0) + { + context.ArcTo(topLeft, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise); + } + + context.EndFigure(true); + } + + private struct BorderCoordinates + { + internal BorderCoordinates(CornerRadius cornerRadius, Thickness borderThickness, bool isOuter) + { + var left = 0.5 * borderThickness.Left; + var top = 0.5 * borderThickness.Top; + var right = 0.5 * borderThickness.Right; + var bottom = 0.5 * borderThickness.Bottom; + + if (isOuter) + { + if (cornerRadius.TopLeft == 0) + { + LeftTop = TopLeft = 0.0; + } + else + { + LeftTop = cornerRadius.TopLeft + left; + TopLeft = cornerRadius.TopLeft + top; + } + if (cornerRadius.TopRight == 0) + { + TopRight = RightTop = 0; + } + else + { + TopRight = cornerRadius.TopRight + top; + RightTop = cornerRadius.TopRight + right; + } + if (cornerRadius.BottomRight == 0) + { + RightBottom = BottomRight = 0; + } + else + { + RightBottom = cornerRadius.BottomRight + right; + BottomRight = cornerRadius.BottomRight + bottom; + } + if (cornerRadius.BottomLeft == 0) + { + BottomLeft = LeftBottom = 0; + } + else + { + BottomLeft = cornerRadius.BottomLeft + bottom; + LeftBottom = cornerRadius.BottomLeft + left; + } + } + else + { + LeftTop = Math.Max(0, cornerRadius.TopLeft - left); + TopLeft = Math.Max(0, cornerRadius.TopLeft - top); + TopRight = Math.Max(0, cornerRadius.TopRight - top); + RightTop = Math.Max(0, cornerRadius.TopRight - right); + RightBottom = Math.Max(0, cornerRadius.BottomRight - right); + BottomRight = Math.Max(0, cornerRadius.BottomRight - bottom); + BottomLeft = Math.Max(0, cornerRadius.BottomLeft - bottom); + LeftBottom = Math.Max(0, cornerRadius.BottomLeft - left); + } + } + + internal readonly double LeftTop; + internal readonly double TopLeft; + internal readonly double TopRight; + internal readonly double RightTop; + internal readonly double RightBottom; + internal readonly double BottomRight; + internal readonly double BottomLeft; + internal readonly double LeftBottom; + } + + } +} diff --git a/src/Avalonia.Controls/Utils/ISelectionAdapter.cs b/src/Avalonia.Controls/Utils/ISelectionAdapter.cs new file mode 100644 index 0000000000..3c1006a12e --- /dev/null +++ b/src/Avalonia.Controls/Utils/ISelectionAdapter.cs @@ -0,0 +1,64 @@ +// (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. + +using System; +using System.Collections; +using Avalonia.Interactivity; +using Avalonia.Input; + +namespace Avalonia.Controls.Utils +{ + /// + /// Defines an item collection, selection members, and key handling for the + /// selection adapter contained in the drop-down portion of an + /// control. + /// + public interface ISelectionAdapter + { + /// + /// Gets or sets the selected item. + /// + /// The currently selected item. + object SelectedItem { get; set; } + + /// + /// Occurs when the + /// + /// property value changes. + /// + event EventHandler SelectionChanged; + + /// + /// Gets or sets a collection that is used to generate content for the + /// selection adapter. + /// + /// The collection that is used to generate content for the + /// selection adapter. + IEnumerable Items { get; set; } + + /// + /// Occurs when a selected item is not cancelled and is committed as the + /// selected item. + /// + event EventHandler Commit; + + /// + /// Occurs when a selection has been canceled. + /// + event EventHandler Cancel; + + /// + /// Provides handling for the + /// event that occurs + /// when a key is pressed while the drop-down portion of the + /// has focus. + /// + /// A + /// that contains data about the + /// event. + void HandleKeyDown(KeyEventArgs e); + } + +} diff --git a/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs b/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs new file mode 100644 index 0000000000..43c8a5aa6c --- /dev/null +++ b/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs @@ -0,0 +1,342 @@ +// (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. + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; +using Avalonia.Input; +using Avalonia.LogicalTree; +using System.Collections; +using System.Diagnostics; + +namespace Avalonia.Controls.Utils +{ + /// + /// Represents the selection adapter contained in the drop-down portion of + /// an control. + /// + public class SelectingItemsControlSelectionAdapter : ISelectionAdapter + { + /// + /// The SelectingItemsControl instance. + /// + private SelectingItemsControl _selector; + + /// + /// Gets or sets a value indicating whether the selection change event + /// should not be fired. + /// + private bool IgnoringSelectionChanged { get; set; } + + /// + /// Gets or sets the underlying + /// + /// control. + /// + /// The underlying + /// + /// control. + public SelectingItemsControl SelectorControl + { + get { return _selector; } + + set + { + if (_selector != null) + { + _selector.SelectionChanged -= OnSelectionChanged; + _selector.PointerReleased -= OnSelectorPointerReleased; + } + + _selector = value; + + if (_selector != null) + { + _selector.SelectionChanged += OnSelectionChanged; + _selector.PointerReleased += OnSelectorPointerReleased; + } + } + } + + /// + /// Occurs when the + /// + /// property value changes. + /// + public event EventHandler SelectionChanged; + + /// + /// Occurs when an item is selected and is committed to the underlying + /// + /// control. + /// + public event EventHandler Commit; + + /// + /// Occurs when a selection is canceled before it is committed. + /// + public event EventHandler Cancel; + + /// + /// Initializes a new instance of the + /// + /// class. + /// + public SelectingItemsControlSelectionAdapter() + { + + } + + /// + /// Initializes a new instance of the + /// + /// class with the specified + /// + /// control. + /// + /// The + /// control + /// to wrap as a + /// . + public SelectingItemsControlSelectionAdapter(SelectingItemsControl selector) + { + SelectorControl = selector; + } + + /// + /// Gets or sets the selected item of the selection adapter. + /// + /// The selected item of the underlying selection adapter. + public object SelectedItem + { + get + { + return SelectorControl?.SelectedItem; + } + + set + { + IgnoringSelectionChanged = true; + if (SelectorControl != null) + { + SelectorControl.SelectedItem = value; + } + + // Attempt to reset the scroll viewer's position + if (value == null) + { + ResetScrollViewer(); + } + + IgnoringSelectionChanged = false; + } + } + + /// + /// Gets or sets a collection that is used to generate the content of + /// the selection adapter. + /// + /// The collection used to generate content for the selection + /// adapter. + public IEnumerable Items + { + get + { + return SelectorControl?.Items; + } + set + { + if (SelectorControl != null) + { + SelectorControl.Items = value; + } + } + } + + /// + /// If the control contains a ScrollViewer, this will reset the viewer + /// to be scrolled to the top. + /// + private void ResetScrollViewer() + { + if (SelectorControl != null) + { + ScrollViewer sv = SelectorControl.GetLogicalDescendants().OfType().FirstOrDefault(); + if (sv != null) + { + sv.Offset = new Vector(0, 0); + } + } + } + + /// + /// Handles the mouse left button up event on the selector control. + /// + /// The source object. + /// The event data. + private void OnSelectorPointerReleased(object sender, PointerReleasedEventArgs e) + { + if (e.MouseButton == MouseButton.Left) + { + OnCommit(); + } + } + + /// + /// Handles the SelectionChanged event on the SelectingItemsControl control. + /// + /// The source object. + /// The selection changed event data. + private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (IgnoringSelectionChanged) + { + return; + } + + SelectionChanged?.Invoke(sender, e); + } + + /// + /// Increments the + /// + /// property of the underlying + /// + /// control. + /// + protected void SelectedIndexIncrement() + { + if (SelectorControl != null) + { + SelectorControl.SelectedIndex = SelectorControl.SelectedIndex + 1 >= SelectorControl.ItemCount ? -1 : SelectorControl.SelectedIndex + 1; + } + } + + /// + /// Decrements the + /// + /// property of the underlying + /// + /// control. + /// + protected void SelectedIndexDecrement() + { + if (SelectorControl != null) + { + int index = SelectorControl.SelectedIndex; + if (index >= 0) + { + SelectorControl.SelectedIndex--; + } + else if (index == -1) + { + SelectorControl.SelectedIndex = SelectorControl.ItemCount - 1; + } + } + } + + /// + /// Provides handling for the + /// event that occurs + /// when a key is pressed while the drop-down portion of the + /// has focus. + /// + /// A + /// that contains data about the + /// event. + public void HandleKeyDown(KeyEventArgs e) + { + switch (e.Key) + { + case Key.Enter: + OnCommit(); + e.Handled = true; + break; + + case Key.Up: + SelectedIndexDecrement(); + e.Handled = true; + break; + + case Key.Down: + if ((e.Modifiers & InputModifiers.Alt) == InputModifiers.None) + { + SelectedIndexIncrement(); + e.Handled = true; + } + break; + + case Key.Escape: + OnCancel(); + e.Handled = true; + break; + + default: + break; + } + } + + /// + /// Raises the + /// + /// event. + /// + protected virtual void OnCommit() + { + OnCommit(this, new RoutedEventArgs()); + } + + /// + /// Fires the Commit event. + /// + /// The source object. + /// The event data. + private void OnCommit(object sender, RoutedEventArgs e) + { + Commit?.Invoke(sender, e); + + AfterAdapterAction(); + } + + /// + /// Raises the + /// + /// event. + /// + protected virtual void OnCancel() + { + OnCancel(this, new RoutedEventArgs()); + } + + /// + /// Fires the Cancel event. + /// + /// The source object. + /// The event data. + private void OnCancel(object sender, RoutedEventArgs e) + { + Cancel?.Invoke(sender, e); + + AfterAdapterAction(); + } + + /// + /// Change the selection after the actions are complete. + /// + private void AfterAdapterAction() + { + IgnoringSelectionChanged = true; + if (SelectorControl != null) + { + SelectorControl.SelectedItem = null; + SelectorControl.SelectedIndex = -1; + } + IgnoringSelectionChanged = false; + } + } +} diff --git a/src/Avalonia.Controls/Utils/UndoRedoHelper.cs b/src/Avalonia.Controls/Utils/UndoRedoHelper.cs index be4c1aa6c4..17cf681f15 100644 --- a/src/Avalonia.Controls/Utils/UndoRedoHelper.cs +++ b/src/Avalonia.Controls/Utils/UndoRedoHelper.cs @@ -59,7 +59,7 @@ namespace Avalonia.Controls.Utils public void UpdateLastState() { - _states.Last.Value = _host.UndoRedoState; + UpdateLastState(_host.UndoRedoState); } public void DiscardRedo() @@ -91,6 +91,12 @@ namespace Avalonia.Controls.Utils } } + public void Clear() + { + _states.Clear(); + _currentNode = null; + } + bool WeakTimer.IWeakTimerSubscriber.Tick() { Snapshot(); diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 893859b915..16ee3a46b3 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -13,6 +13,7 @@ using Avalonia.Styling; using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; +using System.ComponentModel; namespace Avalonia.Controls { @@ -85,9 +86,22 @@ namespace Avalonia.Controls public static readonly StyledProperty IconProperty = AvaloniaProperty.Register(nameof(Icon)); + /// + /// Defines the proeprty. + /// + public static readonly DirectProperty WindowStartupLocationProperty = + AvaloniaProperty.RegisterDirect( + nameof(WindowStartupLocation), + o => o.WindowStartupLocation, + (o, v) => o.WindowStartupLocation = v); + + public static readonly StyledProperty CanResizeProperty = + AvaloniaProperty.Register(nameof(CanResize), true); + private readonly NameScope _nameScope = new NameScope(); private object _dialogResult; private readonly Size _maxPlatformClientSize; + private WindowStartupLocation _windowStartupLoction; /// /// Initializes static members of the class. @@ -102,6 +116,8 @@ namespace Avalonia.Controls ShowInTaskbarProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.ShowTaskbarIcon((bool)e.NewValue)); IconProperty.Changed.AddClassHandler((s, e) => s.PlatformImpl?.SetIcon(((WindowIcon)e.NewValue).PlatformImpl)); + + CanResizeProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.CanResize((bool)e.NewValue)); } /// @@ -119,6 +135,7 @@ namespace Avalonia.Controls public Window(IWindowImpl impl) : base(impl) { + impl.Closing = HandleClosing; _maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size); Screens = new Screens(PlatformImpl?.Screen); } @@ -196,6 +213,15 @@ namespace Avalonia.Controls } } + /// + /// Enables or disables resizing of the window + /// + public bool CanResize + { + get { return GetValue(CanResizeProperty); } + set { SetValue(CanResizeProperty, value); } + } + /// /// Gets or sets the icon of the window. /// @@ -205,26 +231,38 @@ namespace Avalonia.Controls set { SetValue(IconProperty, value); } } + /// + /// Gets or sets the startup location of the window. + /// + public WindowStartupLocation WindowStartupLocation + { + get { return _windowStartupLoction; } + set { SetAndRaise(WindowStartupLocationProperty, ref _windowStartupLoction, value); } + } + /// Size ILayoutRoot.MaxClientSize => _maxPlatformClientSize; /// Type IStyleable.StyleKey => typeof(Window); + /// + /// Fired before a window is closed. + /// + public event EventHandler Closing; + /// /// Closes the window. /// public void Close() { - s_windows.Remove(this); - PlatformImpl?.Dispose(); - IsVisible = false; + Close(false); } protected override void HandleApplicationExiting() { base.HandleApplicationExiting(); - Close(); + Close(true); } /// @@ -239,7 +277,35 @@ namespace Avalonia.Controls public void Close(object dialogResult) { _dialogResult = dialogResult; - Close(); + Close(false); + } + + internal void Close(bool ignoreCancel) + { + var cancelClosing = false; + try + { + cancelClosing = HandleClosing(); + } + finally + { + if (ignoreCancel || !cancelClosing) + { + s_windows.Remove(this); + PlatformImpl?.Dispose(); + IsVisible = false; + } + } + } + + /// + /// Handles a closing notification from . + /// + protected virtual bool HandleClosing() + { + var args = new CancelEventArgs(); + Closing?.Invoke(this, args); + return args.Cancel; } /// @@ -274,6 +340,7 @@ namespace Avalonia.Controls s_windows.Add(this); EnsureInitialized(); + SetWindowStartupLocation(); IsVisible = true; LayoutManager.Instance.ExecuteInitialLayoutPass(this); @@ -314,6 +381,7 @@ namespace Avalonia.Controls s_windows.Add(this); EnsureInitialized(); + SetWindowStartupLocation(); IsVisible = true; LayoutManager.Instance.ExecuteInitialLayoutPass(this); @@ -337,7 +405,7 @@ namespace Avalonia.Controls modal?.Dispose(); SetIsEnabled(affectedWindows, true); activated?.Activate(); - result.SetResult((TResult)_dialogResult); + result.SetResult((TResult)(_dialogResult ?? default(TResult))); }); return result.Task; @@ -352,6 +420,25 @@ namespace Avalonia.Controls } } + void SetWindowStartupLocation() + { + if (WindowStartupLocation == WindowStartupLocation.CenterScreen) + { + var screen = Screens.ScreenFromPoint(Bounds.Position); + + if (screen != null) + Position = screen.WorkingArea.CenterRect(new Rect(ClientSize)).Position; + } + else if (WindowStartupLocation == WindowStartupLocation.CenterOwner) + { + if (Owner != null) + { + var positionAsSize = Owner.ClientSize / 2 - ClientSize / 2; + Position = Owner.Position + new Point(positionAsSize.Width, positionAsSize.Height); + } + } + } + /// void INameScope.Register(string name, object element) { diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index bcc09936ba..c427df1c26 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -29,14 +29,29 @@ namespace Avalonia.Controls public static readonly DirectProperty IsActiveProperty = AvaloniaProperty.RegisterDirect(nameof(IsActive), o => o.IsActive); + /// + /// Defines the property. + /// + public static readonly DirectProperty OwnerProperty = + AvaloniaProperty.RegisterDirect( + nameof(Owner), + o => o.Owner, + (o, v) => o.Owner = v); + private bool _hasExecutedInitialLayoutPass; private bool _isActive; private bool _ignoreVisibilityChange; + private WindowBase _owner; static WindowBase() { IsVisibleProperty.OverrideDefaultValue(false); IsVisibleProperty.Changed.AddClassHandler(x => x.IsVisibleChanged); + + 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))); } public WindowBase(IWindowBaseImpl impl) : this(impl, AvaloniaLocator.Current) @@ -100,6 +115,15 @@ namespace Avalonia.Controls private set; } + /// + /// Gets or sets the owner of the window. + /// + public WindowBase Owner + { + get { return _owner; } + set { SetAndRaise(OwnerProperty, ref _owner, value); } + } + /// /// Activates the window. /// diff --git a/src/Avalonia.Controls/WindowStartupLocation.cs b/src/Avalonia.Controls/WindowStartupLocation.cs new file mode 100644 index 0000000000..1818636076 --- /dev/null +++ b/src/Avalonia.Controls/WindowStartupLocation.cs @@ -0,0 +1,23 @@ +namespace Avalonia.Controls +{ + /// + /// Determines the startup location of the window. + /// + public enum WindowStartupLocation + { + /// + /// The startup location is defined by the Position property. + /// + Manual, + + /// + /// The startup location is the center of the screen. + /// + CenterScreen, + + /// + /// The startup location is the center of the owner window. If the owner window is not specified, the startup location will be . + /// + CenterOwner + } +} diff --git a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs index d9cac47bf3..4fbf0ebdec 100644 --- a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs +++ b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs @@ -56,7 +56,7 @@ namespace Avalonia.DesignerSupport } }; } - if (loaded is Application) + else if (loaded is Application) control = new TextBlock {Text = "Application can't be previewed in design view"}; else control = (Control) loaded; @@ -75,4 +75,4 @@ namespace Avalonia.DesignerSupport return window; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 3c7ef86d5d..9750b46aa2 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -39,6 +39,7 @@ namespace Avalonia.DesignerSupport.Remote public Action PositionChanged { get; set; } public Action Deactivated { get; set; } public Action Activated { get; set; } + public Func Closing { get; set; } public IPlatformHandle Handle { get; } public WindowState WindowState { get; set; } public Size MaxClientSize { get; } = new Size(4096, 4096); @@ -66,6 +67,10 @@ namespace Avalonia.DesignerSupport.Remote RenderIfNeeded(); } + public void SetMinMaxSize(Size minSize, Size maxSize) + { + } + public IScreenImpl Screen { get; } = new ScreenStub(); public void Activate() @@ -92,5 +97,9 @@ namespace Avalonia.DesignerSupport.Remote public void ShowTaskbarIcon(bool value) { } + + public void CanResize(bool value) + { + } } } \ No newline at end of file diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 2ed434a2dc..ee8569d748 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -26,6 +26,7 @@ namespace Avalonia.DesignerSupport.Remote public Action Paint { get; set; } public Action Resized { get; set; } public Action ScalingChanged { get; set; } + public Func Closing { get; set; } public Action Closed { get; set; } public IMouseDevice MouseDevice { get; } = new MouseDevice(); public Point Position { get; set; } @@ -77,6 +78,10 @@ namespace Avalonia.DesignerSupport.Remote public IScreenImpl Screen { get; } = new ScreenStub(); + public void SetMinMaxSize(Size minSize, Size maxSize) + { + } + public void SetTitle(string title) { } @@ -94,6 +99,10 @@ namespace Avalonia.DesignerSupport.Remote public void ShowTaskbarIcon(bool value) { } + + public void CanResize(bool value) + { + } } class ClipboardStub : IClipboard diff --git a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs index 2d3f978462..555a0b2354 100644 --- a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs +++ b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs @@ -86,7 +86,7 @@ namespace Avalonia.Diagnostics.ViewModels private void UpdateFocusedControl() { - _focusedControl = KeyboardDevice.Instance.FocusedElement?.GetType().Name; + FocusedControl = KeyboardDevice.Instance.FocusedElement?.GetType().Name; } } } diff --git a/src/Avalonia.DotNetCoreRuntime/AppBuilder.cs b/src/Avalonia.DotNetCoreRuntime/AppBuilder.cs index 2ce0ee32fb..5ae836eb24 100644 --- a/src/Avalonia.DotNetCoreRuntime/AppBuilder.cs +++ b/src/Avalonia.DotNetCoreRuntime/AppBuilder.cs @@ -10,6 +10,9 @@ using Avalonia.Shared.PlatformSupport; namespace Avalonia { + /// + /// Initializes platform-specific services for an . + /// public sealed class AppBuilder : AppBuilderBase { /// diff --git a/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj b/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj index 8630e7b228..a7586ac7ac 100644 --- a/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj +++ b/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj @@ -19,7 +19,7 @@ - + diff --git a/src/Avalonia.Input/Cursors.cs b/src/Avalonia.Input/Cursors.cs index e3860e58e5..02a026c998 100644 --- a/src/Avalonia.Input/Cursors.cs +++ b/src/Avalonia.Input/Cursors.cs @@ -38,7 +38,10 @@ namespace Avalonia.Input TopLeftCorner, TopRightCorner, BottomLeftCorner, - BottomRightCorner + BottomRightCorner, + DragMove, + DragCopy, + DragLink, // Not available in GTK directly, see http://www.pixelbeat.org/programming/x_cursors/ // We might enable them later, preferably, by loading pixmax direclty from theme with fallback image diff --git a/src/Avalonia.Input/DataFormats.cs b/src/Avalonia.Input/DataFormats.cs new file mode 100644 index 0000000000..559d2cb643 --- /dev/null +++ b/src/Avalonia.Input/DataFormats.cs @@ -0,0 +1,15 @@ +namespace Avalonia.Input +{ + public static class DataFormats + { + /// + /// Dataformat for plaintext + /// + public static string Text = nameof(Text); + + /// + /// Dataformat for one or more filenames + /// + public static string FileNames = nameof(FileNames); + } +} \ No newline at end of file diff --git a/src/Avalonia.Input/DataObject.cs b/src/Avalonia.Input/DataObject.cs new file mode 100644 index 0000000000..cb642f4d65 --- /dev/null +++ b/src/Avalonia.Input/DataObject.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Input +{ + public class DataObject : IDataObject + { + private readonly Dictionary _items = new Dictionary(); + + public bool Contains(string dataFormat) + { + return _items.ContainsKey(dataFormat); + } + + public object Get(string dataFormat) + { + if (_items.ContainsKey(dataFormat)) + return _items[dataFormat]; + return null; + } + + public IEnumerable GetDataFormats() + { + return _items.Keys; + } + + public IEnumerable GetFileNames() + { + return Get(DataFormats.FileNames) as IEnumerable; + } + + public string GetText() + { + return Get(DataFormats.Text) as string; + } + + public void Set(string dataFormat, object value) + { + _items[dataFormat] = value; + } + } +} diff --git a/src/Avalonia.Input/DragDrop.cs b/src/Avalonia.Input/DragDrop.cs new file mode 100644 index 0000000000..c312732afa --- /dev/null +++ b/src/Avalonia.Input/DragDrop.cs @@ -0,0 +1,54 @@ +using System.Threading.Tasks; +using Avalonia.Interactivity; +using Avalonia.Input.Platform; + +namespace Avalonia.Input +{ + public static class DragDrop + { + /// + /// Event which is raised, when a drag-and-drop operation enters the element. + /// + public static RoutedEvent DragEnterEvent = RoutedEvent.Register("DragEnter", RoutingStrategies.Bubble, typeof(DragDrop)); + /// + /// Event which is raised, when a drag-and-drop operation leaves the element. + /// + public static RoutedEvent DragLeaveEvent = RoutedEvent.Register("DragLeave", RoutingStrategies.Bubble, typeof(DragDrop)); + /// + /// Event which is raised, when a drag-and-drop operation is updated while over the element. + /// + public static RoutedEvent DragOverEvent = RoutedEvent.Register("DragOver", RoutingStrategies.Bubble, typeof(DragDrop)); + /// + /// Event which is raised, when a drag-and-drop operation should complete over the element. + /// + public static RoutedEvent DropEvent = RoutedEvent.Register("Drop", RoutingStrategies.Bubble, typeof(DragDrop)); + + public static AvaloniaProperty AllowDropProperty = AvaloniaProperty.RegisterAttached("AllowDrop", typeof(DragDrop), inherits: true); + + /// + /// Gets a value indicating whether the given element can be used as the target of a drag-and-drop operation. + /// + public static bool GetAllowDrop(Interactive interactive) + { + return interactive.GetValue(AllowDropProperty); + } + + /// + /// Sets a value indicating whether the given interactive can be used as the target of a drag-and-drop operation. + /// + public static void SetAllowDrop(Interactive interactive, bool value) + { + interactive.SetValue(AllowDropProperty, value); + } + + /// + /// Starts a dragging operation with the given and returns the applied drop effect from the target. + /// + /// + public static Task DoDragDrop(IDataObject data, DragDropEffects allowedEffects) + { + var src = AvaloniaLocator.Current.GetService(); + return src?.DoDragDrop(data, allowedEffects) ?? Task.FromResult(DragDropEffects.None); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Input/DragDropDevice.cs b/src/Avalonia.Input/DragDropDevice.cs new file mode 100644 index 0000000000..2615e3a212 --- /dev/null +++ b/src/Avalonia.Input/DragDropDevice.cs @@ -0,0 +1,111 @@ +using Avalonia.Interactivity; +using Avalonia.VisualTree; +using System.Linq; +using Avalonia.Input.Raw; + +namespace Avalonia.Input +{ + public class DragDropDevice : IDragDropDevice + { + public static readonly DragDropDevice Instance = new DragDropDevice(); + + private Interactive _lastTarget = null; + + private Interactive GetTarget(IInputElement root, Point local) + { + var target = root.InputHitTest(local)?.GetSelfAndVisualAncestors()?.OfType()?.FirstOrDefault(); + if (target != null && DragDrop.GetAllowDrop(target)) + return target; + return null; + } + + private DragDropEffects RaiseDragEvent(Interactive target, RoutedEvent routedEvent, DragDropEffects operation, IDataObject data) + { + if (target == null) + return DragDropEffects.None; + var args = new DragEventArgs(routedEvent, data) + { + RoutedEvent = routedEvent, + DragEffects = operation + }; + target.RaiseEvent(args); + return args.DragEffects; + } + + private DragDropEffects DragEnter(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects) + { + _lastTarget = GetTarget(inputRoot, point); + return RaiseDragEvent(_lastTarget, DragDrop.DragEnterEvent, effects, data); + } + + private DragDropEffects DragOver(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects) + { + var target = GetTarget(inputRoot, point); + + if (target == _lastTarget) + return RaiseDragEvent(target, DragDrop.DragOverEvent, effects, data); + + try + { + if (_lastTarget != null) + _lastTarget.RaiseEvent(new RoutedEventArgs(DragDrop.DragLeaveEvent)); + return RaiseDragEvent(target, DragDrop.DragEnterEvent, effects, data); + } + finally + { + _lastTarget = target; + } + } + + private void DragLeave(IInputElement inputRoot) + { + if (_lastTarget == null) + return; + try + { + _lastTarget.RaiseEvent(new RoutedEventArgs(DragDrop.DragLeaveEvent)); + } + finally + { + _lastTarget = null; + } + } + + private DragDropEffects Drop(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects) + { + try + { + return RaiseDragEvent(_lastTarget, DragDrop.DropEvent, effects, data); + } + finally + { + _lastTarget = null; + } + } + + public void ProcessRawEvent(RawInputEventArgs e) + { + if (!e.Handled && e is RawDragEvent margs) + ProcessRawEvent(margs); + } + + private void ProcessRawEvent(RawDragEvent e) + { + switch (e.Type) + { + case RawDragEventType.DragEnter: + e.Effects = DragEnter(e.InputRoot, e.Location, e.Data, e.Effects); + break; + case RawDragEventType.DragOver: + e.Effects = DragOver(e.InputRoot, e.Location, e.Data, e.Effects); + break; + case RawDragEventType.DragLeave: + DragLeave(e.InputRoot); + break; + case RawDragEventType.Drop: + e.Effects = Drop(e.InputRoot, e.Location, e.Data, e.Effects); + break; + } + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Input/DragDropEffects.cs b/src/Avalonia.Input/DragDropEffects.cs new file mode 100644 index 0000000000..bcda1091d5 --- /dev/null +++ b/src/Avalonia.Input/DragDropEffects.cs @@ -0,0 +1,13 @@ +using System; + +namespace Avalonia.Input +{ + [Flags] + public enum DragDropEffects + { + None = 0, + Copy = 1, + Move = 2, + Link = 4, + } +} \ No newline at end of file diff --git a/src/Avalonia.Input/DragEventArgs.cs b/src/Avalonia.Input/DragEventArgs.cs new file mode 100644 index 0000000000..12d5a8941e --- /dev/null +++ b/src/Avalonia.Input/DragEventArgs.cs @@ -0,0 +1,18 @@ +using Avalonia.Interactivity; + +namespace Avalonia.Input +{ + public class DragEventArgs : RoutedEventArgs + { + public DragDropEffects DragEffects { get; set; } + + public IDataObject Data { get; private set; } + + public DragEventArgs(RoutedEvent routedEvent, IDataObject data) + : base(routedEvent) + { + this.Data = data; + } + + } +} \ No newline at end of file diff --git a/src/Avalonia.Input/IDataObject.cs b/src/Avalonia.Input/IDataObject.cs new file mode 100644 index 0000000000..1b12323d99 --- /dev/null +++ b/src/Avalonia.Input/IDataObject.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; + +namespace Avalonia.Input +{ + /// + /// Interface to access information about the data of a drag-and-drop operation. + /// + public interface IDataObject + { + /// + /// Lists all formats which are present in the DataObject. + /// + /// + IEnumerable GetDataFormats(); + + /// + /// Checks wether a given DataFormat is present in this object + /// + /// + bool Contains(string dataFormat); + + /// + /// Returns the dragged text if the DataObject contains any text. + /// + /// + string GetText(); + + /// + /// Returns a list of filenames if the DataObject contains filenames. + /// + /// + IEnumerable GetFileNames(); + + /// + /// Tries to get the data of the given DataFormat. + /// + object Get(string dataFormat); + } +} \ No newline at end of file diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs index d815f8082b..2a1cdec1c0 100644 --- a/src/Avalonia.Input/KeyboardDevice.cs +++ b/src/Avalonia.Input/KeyboardDevice.cs @@ -46,13 +46,13 @@ namespace Avalonia.Input if (element != FocusedElement) { var interactive = FocusedElement as IInteractive; + FocusedElement = element; interactive?.RaiseEvent(new RoutedEventArgs { RoutedEvent = InputElement.LostFocusEvent, }); - FocusedElement = element; interactive = element as IInteractive; interactive?.RaiseEvent(new GotFocusEventArgs diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs index 6e077e887f..a9d5b83073 100644 --- a/src/Avalonia.Input/Navigation/TabNavigation.cs +++ b/src/Avalonia.Input/Navigation/TabNavigation.cs @@ -221,17 +221,16 @@ namespace Avalonia.Input.Navigation return parent; } - var siblings = parent.GetVisualChildren() + var allSiblings = parent.GetVisualChildren() .OfType() .Where(FocusExtensions.CanFocusDescendants); - var sibling = direction == NavigationDirection.Next ? - siblings.SkipWhile(x => x != container).Skip(1).FirstOrDefault() : - siblings.TakeWhile(x => x != container).LastOrDefault(); + var siblings = direction == NavigationDirection.Next ? + allSiblings.SkipWhile(x => x != container).Skip(1) : + allSiblings.TakeWhile(x => x != container).Reverse(); - if (sibling != null) + foreach (var sibling in siblings) { var customNext = GetCustomNext(sibling, direction); - if (customNext.handled) { return customNext.next; @@ -239,13 +238,17 @@ namespace Avalonia.Input.Navigation if (sibling.CanFocus()) { - next = sibling; + return sibling; } else { next = direction == NavigationDirection.Next ? GetFocusableDescendants(sibling, direction).FirstOrDefault() : GetFocusableDescendants(sibling, direction).LastOrDefault(); + if(next != null) + { + return next; + } } } diff --git a/src/Avalonia.Input/Platform/IPlatformDragSource.cs b/src/Avalonia.Input/Platform/IPlatformDragSource.cs new file mode 100644 index 0000000000..669251c50b --- /dev/null +++ b/src/Avalonia.Input/Platform/IPlatformDragSource.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Input; +using Avalonia.Interactivity; + +namespace Avalonia.Input.Platform +{ + public interface IPlatformDragSource + { + Task DoDragDrop(IDataObject data, DragDropEffects allowedEffects); + } +} diff --git a/src/Avalonia.Input/Raw/IDragDropDevice.cs b/src/Avalonia.Input/Raw/IDragDropDevice.cs new file mode 100644 index 0000000000..6aab1b868d --- /dev/null +++ b/src/Avalonia.Input/Raw/IDragDropDevice.cs @@ -0,0 +1,8 @@ +using Avalonia.Input; + +namespace Avalonia.Input.Raw +{ + public interface IDragDropDevice : IInputDevice + { + } +} \ No newline at end of file diff --git a/src/Avalonia.Input/Raw/RawDragEvent.cs b/src/Avalonia.Input/Raw/RawDragEvent.cs new file mode 100644 index 0000000000..49125b4c07 --- /dev/null +++ b/src/Avalonia.Input/Raw/RawDragEvent.cs @@ -0,0 +1,26 @@ +using System; +using Avalonia.Input; +using Avalonia.Input.Raw; + +namespace Avalonia.Input.Raw +{ + public class RawDragEvent : RawInputEventArgs + { + public IInputElement InputRoot { get; } + public Point Location { get; } + public IDataObject Data { get; } + public DragDropEffects Effects { get; set; } + public RawDragEventType Type { get; } + + public RawDragEvent(IDragDropDevice inputDevice, RawDragEventType type, + IInputElement inputRoot, Point location, IDataObject data, DragDropEffects effects) + :base(inputDevice, 0) + { + Type = type; + InputRoot = inputRoot; + Location = location; + Data = data; + Effects = effects; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Input/Raw/RawDragEventType.cs b/src/Avalonia.Input/Raw/RawDragEventType.cs new file mode 100644 index 0000000000..9635f77467 --- /dev/null +++ b/src/Avalonia.Input/Raw/RawDragEventType.cs @@ -0,0 +1,10 @@ +namespace Avalonia.Input.Raw +{ + public enum RawDragEventType + { + DragEnter, + DragOver, + DragLeave, + Drop + } +} \ No newline at end of file diff --git a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml index cb86598a42..4c85e172ff 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml @@ -20,7 +20,7 @@ Red #10ff0000 - 2 + 2 0.5 10 diff --git a/src/Avalonia.Themes.Default/AutoCompleteBox.xaml b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml new file mode 100644 index 0000000000..82dbf6064b --- /dev/null +++ b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml @@ -0,0 +1,43 @@ + + + + + \ No newline at end of file diff --git a/src/Avalonia.Themes.Default/ButtonSpinner.xaml b/src/Avalonia.Themes.Default/ButtonSpinner.xaml new file mode 100644 index 0000000000..a08f5b5e9b --- /dev/null +++ b/src/Avalonia.Themes.Default/ButtonSpinner.xaml @@ -0,0 +1,86 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia.Themes.Default/DatePicker.xaml b/src/Avalonia.Themes.Default/DatePicker.xaml new file mode 100644 index 0000000000..b706b5b4e5 --- /dev/null +++ b/src/Avalonia.Themes.Default/DatePicker.xaml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + /// The string. - /// The current culture. /// The . - public static Matrix Parse(string s, CultureInfo culture) + public static Matrix Parse(string s) { - var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) - .Select(x => x.Trim()) - .ToArray(); - - if (parts.Length == 6) + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Matrix")) { return new Matrix( - double.Parse(parts[0], culture), - double.Parse(parts[1], culture), - double.Parse(parts[2], culture), - double.Parse(parts[3], culture), - double.Parse(parts[4], culture), - double.Parse(parts[5], culture)); - } - else - { - throw new FormatException("Invalid Matrix."); + tokenizer.ReadDouble(), + tokenizer.ReadDouble(), + tokenizer.ReadDouble(), + tokenizer.ReadDouble(), + tokenizer.ReadDouble(), + tokenizer.ReadDouble() + ); } } } diff --git a/src/Avalonia.Visuals/Media/Brush.cs b/src/Avalonia.Visuals/Media/Brush.cs index 40ac24b605..d6b0e43cdc 100644 --- a/src/Avalonia.Visuals/Media/Brush.cs +++ b/src/Avalonia.Visuals/Media/Brush.cs @@ -34,26 +34,21 @@ namespace Avalonia.Media /// The . public static IBrush Parse(string s) { + Contract.Requires(s != null); + Contract.Requires(s.Length > 0); + if (s[0] == '#') { return new SolidColorBrush(Color.Parse(s)); } - else - { - var upper = s.ToUpperInvariant(); - var member = typeof(Brushes).GetTypeInfo().DeclaredProperties - .FirstOrDefault(x => x.Name.ToUpperInvariant() == upper); - if (member != null) - { - var brush = (ISolidColorBrush)member.GetValue(null); - return new SolidColorBrush(brush.Color, brush.Opacity); - } - else - { - throw new FormatException($"Invalid brush string: '{s}'."); - } + var brush = KnownColors.GetKnownBrush(s); + if (brush != null) + { + return brush; } + + throw new FormatException($"Invalid brush string: '{s}'."); } } } diff --git a/src/Avalonia.Visuals/Media/Brushes.cs b/src/Avalonia.Visuals/Media/Brushes.cs index 4c89c97b49..83ff043397 100644 --- a/src/Avalonia.Visuals/Media/Brushes.cs +++ b/src/Avalonia.Visuals/Media/Brushes.cs @@ -1,8 +1,6 @@ // 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 Avalonia.Media.Immutable; - namespace Avalonia.Media { /// @@ -10,857 +8,709 @@ namespace Avalonia.Media /// public static class Brushes { - /// - /// Initializes static members of the class. - /// - static Brushes() - { - AliceBlue = new ImmutableSolidColorBrush(Colors.AliceBlue); - AntiqueWhite = new ImmutableSolidColorBrush(Colors.AntiqueWhite); - Aqua = new ImmutableSolidColorBrush(Colors.Aqua); - Aquamarine = new ImmutableSolidColorBrush(Colors.Aquamarine); - Azure = new ImmutableSolidColorBrush(Colors.Azure); - Beige = new ImmutableSolidColorBrush(Colors.Beige); - Bisque = new ImmutableSolidColorBrush(Colors.Bisque); - Black = new ImmutableSolidColorBrush(Colors.Black); - BlanchedAlmond = new ImmutableSolidColorBrush(Colors.BlanchedAlmond); - Blue = new ImmutableSolidColorBrush(Colors.Blue); - BlueViolet = new ImmutableSolidColorBrush(Colors.BlueViolet); - Brown = new ImmutableSolidColorBrush(Colors.Brown); - BurlyWood = new ImmutableSolidColorBrush(Colors.BurlyWood); - CadetBlue = new ImmutableSolidColorBrush(Colors.CadetBlue); - Chartreuse = new ImmutableSolidColorBrush(Colors.Chartreuse); - Chocolate = new ImmutableSolidColorBrush(Colors.Chocolate); - Coral = new ImmutableSolidColorBrush(Colors.Coral); - CornflowerBlue = new ImmutableSolidColorBrush(Colors.CornflowerBlue); - Cornsilk = new ImmutableSolidColorBrush(Colors.Cornsilk); - Crimson = new ImmutableSolidColorBrush(Colors.Crimson); - Cyan = new ImmutableSolidColorBrush(Colors.Cyan); - DarkBlue = new ImmutableSolidColorBrush(Colors.DarkBlue); - DarkCyan = new ImmutableSolidColorBrush(Colors.DarkCyan); - DarkGoldenrod = new ImmutableSolidColorBrush(Colors.DarkGoldenrod); - DarkGray = new ImmutableSolidColorBrush(Colors.DarkGray); - DarkGreen = new ImmutableSolidColorBrush(Colors.DarkGreen); - DarkKhaki = new ImmutableSolidColorBrush(Colors.DarkKhaki); - DarkMagenta = new ImmutableSolidColorBrush(Colors.DarkMagenta); - DarkOliveGreen = new ImmutableSolidColorBrush(Colors.DarkOliveGreen); - DarkOrange = new ImmutableSolidColorBrush(Colors.DarkOrange); - DarkOrchid = new ImmutableSolidColorBrush(Colors.DarkOrchid); - DarkRed = new ImmutableSolidColorBrush(Colors.DarkRed); - DarkSalmon = new ImmutableSolidColorBrush(Colors.DarkSalmon); - DarkSeaGreen = new ImmutableSolidColorBrush(Colors.DarkSeaGreen); - DarkSlateBlue = new ImmutableSolidColorBrush(Colors.DarkSlateBlue); - DarkSlateGray = new ImmutableSolidColorBrush(Colors.DarkSlateGray); - DarkTurquoise = new ImmutableSolidColorBrush(Colors.DarkTurquoise); - DarkViolet = new ImmutableSolidColorBrush(Colors.DarkViolet); - DeepPink = new ImmutableSolidColorBrush(Colors.DeepPink); - DeepSkyBlue = new ImmutableSolidColorBrush(Colors.DeepSkyBlue); - DimGray = new ImmutableSolidColorBrush(Colors.DimGray); - DodgerBlue = new ImmutableSolidColorBrush(Colors.DodgerBlue); - Firebrick = new ImmutableSolidColorBrush(Colors.Firebrick); - FloralWhite = new ImmutableSolidColorBrush(Colors.FloralWhite); - ForestGreen = new ImmutableSolidColorBrush(Colors.ForestGreen); - Fuchsia = new ImmutableSolidColorBrush(Colors.Fuchsia); - Gainsboro = new ImmutableSolidColorBrush(Colors.Gainsboro); - GhostWhite = new ImmutableSolidColorBrush(Colors.GhostWhite); - Gold = new ImmutableSolidColorBrush(Colors.Gold); - Goldenrod = new ImmutableSolidColorBrush(Colors.Goldenrod); - Gray = new ImmutableSolidColorBrush(Colors.Gray); - Green = new ImmutableSolidColorBrush(Colors.Green); - GreenYellow = new ImmutableSolidColorBrush(Colors.GreenYellow); - Honeydew = new ImmutableSolidColorBrush(Colors.Honeydew); - HotPink = new ImmutableSolidColorBrush(Colors.HotPink); - IndianRed = new ImmutableSolidColorBrush(Colors.IndianRed); - Indigo = new ImmutableSolidColorBrush(Colors.Indigo); - Ivory = new ImmutableSolidColorBrush(Colors.Ivory); - Khaki = new ImmutableSolidColorBrush(Colors.Khaki); - Lavender = new ImmutableSolidColorBrush(Colors.Lavender); - LavenderBlush = new ImmutableSolidColorBrush(Colors.LavenderBlush); - LawnGreen = new ImmutableSolidColorBrush(Colors.LawnGreen); - LemonChiffon = new ImmutableSolidColorBrush(Colors.LemonChiffon); - LightBlue = new ImmutableSolidColorBrush(Colors.LightBlue); - LightCoral = new ImmutableSolidColorBrush(Colors.LightCoral); - LightCyan = new ImmutableSolidColorBrush(Colors.LightCyan); - LightGoldenrodYellow = new ImmutableSolidColorBrush(Colors.LightGoldenrodYellow); - LightGray = new ImmutableSolidColorBrush(Colors.LightGray); - LightGreen = new ImmutableSolidColorBrush(Colors.LightGreen); - LightPink = new ImmutableSolidColorBrush(Colors.LightPink); - LightSalmon = new ImmutableSolidColorBrush(Colors.LightSalmon); - LightSeaGreen = new ImmutableSolidColorBrush(Colors.LightSeaGreen); - LightSkyBlue = new ImmutableSolidColorBrush(Colors.LightSkyBlue); - LightSlateGray = new ImmutableSolidColorBrush(Colors.LightSlateGray); - LightSteelBlue = new ImmutableSolidColorBrush(Colors.LightSteelBlue); - LightYellow = new ImmutableSolidColorBrush(Colors.LightYellow); - Lime = new ImmutableSolidColorBrush(Colors.Lime); - LimeGreen = new ImmutableSolidColorBrush(Colors.LimeGreen); - Linen = new ImmutableSolidColorBrush(Colors.Linen); - Magenta = new ImmutableSolidColorBrush(Colors.Magenta); - Maroon = new ImmutableSolidColorBrush(Colors.Maroon); - MediumAquamarine = new ImmutableSolidColorBrush(Colors.MediumAquamarine); - MediumBlue = new ImmutableSolidColorBrush(Colors.MediumBlue); - MediumOrchid = new ImmutableSolidColorBrush(Colors.MediumOrchid); - MediumPurple = new ImmutableSolidColorBrush(Colors.MediumPurple); - MediumSeaGreen = new ImmutableSolidColorBrush(Colors.MediumSeaGreen); - MediumSlateBlue = new ImmutableSolidColorBrush(Colors.MediumSlateBlue); - MediumSpringGreen = new ImmutableSolidColorBrush(Colors.MediumSpringGreen); - MediumTurquoise = new ImmutableSolidColorBrush(Colors.MediumTurquoise); - MediumVioletRed = new ImmutableSolidColorBrush(Colors.MediumVioletRed); - MidnightBlue = new ImmutableSolidColorBrush(Colors.MidnightBlue); - MintCream = new ImmutableSolidColorBrush(Colors.MintCream); - MistyRose = new ImmutableSolidColorBrush(Colors.MistyRose); - Moccasin = new ImmutableSolidColorBrush(Colors.Moccasin); - NavajoWhite = new ImmutableSolidColorBrush(Colors.NavajoWhite); - Navy = new ImmutableSolidColorBrush(Colors.Navy); - OldLace = new ImmutableSolidColorBrush(Colors.OldLace); - Olive = new ImmutableSolidColorBrush(Colors.Olive); - OliveDrab = new ImmutableSolidColorBrush(Colors.OliveDrab); - Orange = new ImmutableSolidColorBrush(Colors.Orange); - OrangeRed = new ImmutableSolidColorBrush(Colors.OrangeRed); - Orchid = new ImmutableSolidColorBrush(Colors.Orchid); - PaleGoldenrod = new ImmutableSolidColorBrush(Colors.PaleGoldenrod); - PaleGreen = new ImmutableSolidColorBrush(Colors.PaleGreen); - PaleTurquoise = new ImmutableSolidColorBrush(Colors.PaleTurquoise); - PaleVioletRed = new ImmutableSolidColorBrush(Colors.PaleVioletRed); - PapayaWhip = new ImmutableSolidColorBrush(Colors.PapayaWhip); - PeachPuff = new ImmutableSolidColorBrush(Colors.PeachPuff); - Peru = new ImmutableSolidColorBrush(Colors.Peru); - Pink = new ImmutableSolidColorBrush(Colors.Pink); - Plum = new ImmutableSolidColorBrush(Colors.Plum); - PowderBlue = new ImmutableSolidColorBrush(Colors.PowderBlue); - Purple = new ImmutableSolidColorBrush(Colors.Purple); - Red = new ImmutableSolidColorBrush(Colors.Red); - RosyBrown = new ImmutableSolidColorBrush(Colors.RosyBrown); - RoyalBlue = new ImmutableSolidColorBrush(Colors.RoyalBlue); - SaddleBrown = new ImmutableSolidColorBrush(Colors.SaddleBrown); - Salmon = new ImmutableSolidColorBrush(Colors.Salmon); - SandyBrown = new ImmutableSolidColorBrush(Colors.SandyBrown); - SeaGreen = new ImmutableSolidColorBrush(Colors.SeaGreen); - SeaShell = new ImmutableSolidColorBrush(Colors.SeaShell); - Sienna = new ImmutableSolidColorBrush(Colors.Sienna); - Silver = new ImmutableSolidColorBrush(Colors.Silver); - SkyBlue = new ImmutableSolidColorBrush(Colors.SkyBlue); - SlateBlue = new ImmutableSolidColorBrush(Colors.SlateBlue); - SlateGray = new ImmutableSolidColorBrush(Colors.SlateGray); - Snow = new ImmutableSolidColorBrush(Colors.Snow); - SpringGreen = new ImmutableSolidColorBrush(Colors.SpringGreen); - SteelBlue = new ImmutableSolidColorBrush(Colors.SteelBlue); - Tan = new ImmutableSolidColorBrush(Colors.Tan); - Teal = new ImmutableSolidColorBrush(Colors.Teal); - Thistle = new ImmutableSolidColorBrush(Colors.Thistle); - Tomato = new ImmutableSolidColorBrush(Colors.Tomato); - Transparent = new ImmutableSolidColorBrush(Colors.Transparent); - Turquoise = new ImmutableSolidColorBrush(Colors.Turquoise); - Violet = new ImmutableSolidColorBrush(Colors.Violet); - Wheat = new ImmutableSolidColorBrush(Colors.Wheat); - White = new ImmutableSolidColorBrush(Colors.White); - WhiteSmoke = new ImmutableSolidColorBrush(Colors.WhiteSmoke); - Yellow = new ImmutableSolidColorBrush(Colors.Yellow); - YellowGreen = new ImmutableSolidColorBrush(Colors.YellowGreen); - } - /// /// Gets an colored brush. /// - public static ISolidColorBrush AliceBlue { get; private set; } + public static ISolidColorBrush AliceBlue => KnownColor.AliceBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush AntiqueWhite { get; private set; } + public static ISolidColorBrush AntiqueWhite => KnownColor.AntiqueWhite.ToBrush(); /// - /// Gets an colored brush. + /// Gets an colored brush. /// - public static ISolidColorBrush Aqua { get; private set; } + public static ISolidColorBrush Aqua => KnownColor.Aqua.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Aquamarine { get; private set; } + public static ISolidColorBrush Aquamarine => KnownColor.Aquamarine.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Azure { get; private set; } + public static ISolidColorBrush Azure => KnownColor.Azure.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Beige { get; private set; } + public static ISolidColorBrush Beige => KnownColor.Beige.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Bisque { get; private set; } + public static ISolidColorBrush Bisque => KnownColor.Bisque.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Black { get; private set; } + public static ISolidColorBrush Black => KnownColor.Black.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush BlanchedAlmond { get; private set; } + public static ISolidColorBrush BlanchedAlmond => KnownColor.BlanchedAlmond.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Blue { get; private set; } + public static ISolidColorBrush Blue => KnownColor.Blue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush BlueViolet { get; private set; } + public static ISolidColorBrush BlueViolet => KnownColor.BlueViolet.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Brown { get; private set; } + public static ISolidColorBrush Brown => KnownColor.Brown.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush BurlyWood { get; private set; } + public static ISolidColorBrush BurlyWood => KnownColor.BurlyWood.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush CadetBlue { get; private set; } + public static ISolidColorBrush CadetBlue => KnownColor.CadetBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Chartreuse { get; private set; } + public static ISolidColorBrush Chartreuse => KnownColor.Chartreuse.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Chocolate { get; private set; } + public static ISolidColorBrush Chocolate => KnownColor.Chocolate.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Coral { get; private set; } + public static ISolidColorBrush Coral => KnownColor.Coral.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush CornflowerBlue { get; private set; } + public static ISolidColorBrush CornflowerBlue => KnownColor.CornflowerBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Cornsilk { get; private set; } + public static ISolidColorBrush Cornsilk => KnownColor.Cornsilk.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Crimson { get; private set; } + public static ISolidColorBrush Crimson => KnownColor.Crimson.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Cyan { get; private set; } + public static ISolidColorBrush Cyan => KnownColor.Cyan.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkBlue { get; private set; } + public static ISolidColorBrush DarkBlue => KnownColor.DarkBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkCyan { get; private set; } + public static ISolidColorBrush DarkCyan => KnownColor.DarkCyan.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkGoldenrod { get; private set; } + public static ISolidColorBrush DarkGoldenrod => KnownColor.DarkGoldenrod.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkGray { get; private set; } + public static ISolidColorBrush DarkGray => KnownColor.DarkGray.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkGreen { get; private set; } + public static ISolidColorBrush DarkGreen => KnownColor.DarkGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkKhaki { get; private set; } + public static ISolidColorBrush DarkKhaki => KnownColor.DarkKhaki.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkMagenta { get; private set; } + public static ISolidColorBrush DarkMagenta => KnownColor.DarkMagenta.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkOliveGreen { get; private set; } + public static ISolidColorBrush DarkOliveGreen => KnownColor.DarkOliveGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkOrange { get; private set; } + public static ISolidColorBrush DarkOrange => KnownColor.DarkOrange.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkOrchid { get; private set; } + public static ISolidColorBrush DarkOrchid => KnownColor.DarkOrchid.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkRed { get; private set; } + public static ISolidColorBrush DarkRed => KnownColor.DarkRed.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkSalmon { get; private set; } + public static ISolidColorBrush DarkSalmon => KnownColor.DarkSalmon.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkSeaGreen { get; private set; } + public static ISolidColorBrush DarkSeaGreen => KnownColor.DarkSeaGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkSlateBlue { get; private set; } + public static ISolidColorBrush DarkSlateBlue => KnownColor.DarkSlateBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkSlateGray { get; private set; } + public static ISolidColorBrush DarkSlateGray => KnownColor.DarkSlateGray.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkTurquoise { get; private set; } + public static ISolidColorBrush DarkTurquoise => KnownColor.DarkTurquoise.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkViolet { get; private set; } + public static ISolidColorBrush DarkViolet => KnownColor.DarkViolet.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DeepPink { get; private set; } + public static ISolidColorBrush DeepPink => KnownColor.DeepPink.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DeepSkyBlue { get; private set; } + public static ISolidColorBrush DeepSkyBlue => KnownColor.DeepSkyBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DimGray { get; private set; } + public static ISolidColorBrush DimGray => KnownColor.DimGray.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DodgerBlue { get; private set; } + public static ISolidColorBrush DodgerBlue => KnownColor.DodgerBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Firebrick { get; private set; } + public static ISolidColorBrush Firebrick => KnownColor.Firebrick.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush FloralWhite { get; private set; } + public static ISolidColorBrush FloralWhite => KnownColor.FloralWhite.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush ForestGreen { get; private set; } + public static ISolidColorBrush ForestGreen => KnownColor.ForestGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Fuchsia { get; private set; } + public static ISolidColorBrush Fuchsia => KnownColor.Fuchsia.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Gainsboro { get; private set; } + public static ISolidColorBrush Gainsboro => KnownColor.Gainsboro.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush GhostWhite { get; private set; } + public static ISolidColorBrush GhostWhite => KnownColor.GhostWhite.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Gold { get; private set; } + public static ISolidColorBrush Gold => KnownColor.Gold.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Goldenrod { get; private set; } + public static ISolidColorBrush Goldenrod => KnownColor.Goldenrod.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Gray { get; private set; } + public static ISolidColorBrush Gray => KnownColor.Gray.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Green { get; private set; } + public static ISolidColorBrush Green => KnownColor.Green.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush GreenYellow { get; private set; } + public static ISolidColorBrush GreenYellow => KnownColor.GreenYellow.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Honeydew { get; private set; } + public static ISolidColorBrush Honeydew => KnownColor.Honeydew.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush HotPink { get; private set; } + public static ISolidColorBrush HotPink => KnownColor.HotPink.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush IndianRed { get; private set; } + public static ISolidColorBrush IndianRed => KnownColor.IndianRed.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Indigo { get; private set; } + public static ISolidColorBrush Indigo => KnownColor.Indigo.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Ivory { get; private set; } + public static ISolidColorBrush Ivory => KnownColor.Ivory.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Khaki { get; private set; } + public static ISolidColorBrush Khaki => KnownColor.Khaki.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Lavender { get; private set; } + public static ISolidColorBrush Lavender => KnownColor.Lavender.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LavenderBlush { get; private set; } + public static ISolidColorBrush LavenderBlush => KnownColor.LavenderBlush.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LawnGreen { get; private set; } + public static ISolidColorBrush LawnGreen => KnownColor.LawnGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LemonChiffon { get; private set; } + public static ISolidColorBrush LemonChiffon => KnownColor.LemonChiffon.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightBlue { get; private set; } + public static ISolidColorBrush LightBlue => KnownColor.LightBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightCoral { get; private set; } + public static ISolidColorBrush LightCoral => KnownColor.LightCoral.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightCyan { get; private set; } + public static ISolidColorBrush LightCyan => KnownColor.LightCyan.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightGoldenrodYellow { get; private set; } + public static ISolidColorBrush LightGoldenrodYellow => KnownColor.LightGoldenrodYellow.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightGray { get; private set; } + public static ISolidColorBrush LightGray => KnownColor.LightGray.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightGreen { get; private set; } + public static ISolidColorBrush LightGreen => KnownColor.LightGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightPink { get; private set; } + public static ISolidColorBrush LightPink => KnownColor.LightPink.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightSalmon { get; private set; } + public static ISolidColorBrush LightSalmon => KnownColor.LightSalmon.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightSeaGreen { get; private set; } + public static ISolidColorBrush LightSeaGreen => KnownColor.LightSeaGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightSkyBlue { get; private set; } + public static ISolidColorBrush LightSkyBlue => KnownColor.LightSkyBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightSlateGray { get; private set; } + public static ISolidColorBrush LightSlateGray => KnownColor.LightSlateGray.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightSteelBlue { get; private set; } + public static ISolidColorBrush LightSteelBlue => KnownColor.LightSteelBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightYellow { get; private set; } + public static ISolidColorBrush LightYellow => KnownColor.LightYellow.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Lime { get; private set; } + public static ISolidColorBrush Lime => KnownColor.Lime.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LimeGreen { get; private set; } + public static ISolidColorBrush LimeGreen => KnownColor.LimeGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Linen { get; private set; } + public static ISolidColorBrush Linen => KnownColor.Linen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Magenta { get; private set; } + public static ISolidColorBrush Magenta => KnownColor.Magenta.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Maroon { get; private set; } + public static ISolidColorBrush Maroon => KnownColor.Maroon.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumAquamarine { get; private set; } + public static ISolidColorBrush MediumAquamarine => KnownColor.MediumAquamarine.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumBlue { get; private set; } + public static ISolidColorBrush MediumBlue => KnownColor.MediumBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumOrchid { get; private set; } + public static ISolidColorBrush MediumOrchid => KnownColor.MediumOrchid.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumPurple { get; private set; } + public static ISolidColorBrush MediumPurple => KnownColor.MediumPurple.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumSeaGreen { get; private set; } + public static ISolidColorBrush MediumSeaGreen => KnownColor.MediumSeaGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumSlateBlue { get; private set; } + public static ISolidColorBrush MediumSlateBlue => KnownColor.MediumSlateBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumSpringGreen { get; private set; } + public static ISolidColorBrush MediumSpringGreen => KnownColor.MediumSpringGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumTurquoise { get; private set; } + public static ISolidColorBrush MediumTurquoise => KnownColor.MediumTurquoise.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumVioletRed { get; private set; } + public static ISolidColorBrush MediumVioletRed => KnownColor.MediumVioletRed.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MidnightBlue { get; private set; } + public static ISolidColorBrush MidnightBlue => KnownColor.MidnightBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MintCream { get; private set; } + public static ISolidColorBrush MintCream => KnownColor.MintCream.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MistyRose { get; private set; } + public static ISolidColorBrush MistyRose => KnownColor.MistyRose.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Moccasin { get; private set; } + public static ISolidColorBrush Moccasin => KnownColor.Moccasin.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush NavajoWhite { get; private set; } + public static ISolidColorBrush NavajoWhite => KnownColor.NavajoWhite.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Navy { get; private set; } + public static ISolidColorBrush Navy => KnownColor.Navy.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush OldLace { get; private set; } + public static ISolidColorBrush OldLace => KnownColor.OldLace.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Olive { get; private set; } + public static ISolidColorBrush Olive => KnownColor.Olive.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush OliveDrab { get; private set; } + public static ISolidColorBrush OliveDrab => KnownColor.OliveDrab.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Orange { get; private set; } + public static ISolidColorBrush Orange => KnownColor.Orange.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush OrangeRed { get; private set; } + public static ISolidColorBrush OrangeRed => KnownColor.OrangeRed.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Orchid { get; private set; } + public static ISolidColorBrush Orchid => KnownColor.Orchid.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush PaleGoldenrod { get; private set; } + public static ISolidColorBrush PaleGoldenrod => KnownColor.PaleGoldenrod.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush PaleGreen { get; private set; } + public static ISolidColorBrush PaleGreen => KnownColor.PaleGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush PaleTurquoise { get; private set; } + public static ISolidColorBrush PaleTurquoise => KnownColor.PaleTurquoise.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush PaleVioletRed { get; private set; } + public static ISolidColorBrush PaleVioletRed => KnownColor.PaleVioletRed.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush PapayaWhip { get; private set; } + public static ISolidColorBrush PapayaWhip => KnownColor.PapayaWhip.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush PeachPuff { get; private set; } + public static ISolidColorBrush PeachPuff => KnownColor.PeachPuff.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Peru { get; private set; } + public static ISolidColorBrush Peru => KnownColor.Peru.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Pink { get; private set; } + public static ISolidColorBrush Pink => KnownColor.Pink.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Plum { get; private set; } + public static ISolidColorBrush Plum => KnownColor.Plum.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush PowderBlue { get; private set; } + public static ISolidColorBrush PowderBlue => KnownColor.PowderBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Purple { get; private set; } + public static ISolidColorBrush Purple => KnownColor.Purple.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Red { get; private set; } + public static ISolidColorBrush Red => KnownColor.Red.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush RosyBrown { get; private set; } + public static ISolidColorBrush RosyBrown => KnownColor.RosyBrown.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush RoyalBlue { get; private set; } + public static ISolidColorBrush RoyalBlue => KnownColor.RoyalBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SaddleBrown { get; private set; } + public static ISolidColorBrush SaddleBrown => KnownColor.SaddleBrown.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Salmon { get; private set; } + public static ISolidColorBrush Salmon => KnownColor.Salmon.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SandyBrown { get; private set; } + public static ISolidColorBrush SandyBrown => KnownColor.SandyBrown.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SeaGreen { get; private set; } + public static ISolidColorBrush SeaGreen => KnownColor.SeaGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SeaShell { get; private set; } + public static ISolidColorBrush SeaShell => KnownColor.SeaShell.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Sienna { get; private set; } + public static ISolidColorBrush Sienna => KnownColor.Sienna.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Silver { get; private set; } + public static ISolidColorBrush Silver => KnownColor.Silver.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SkyBlue { get; private set; } + public static ISolidColorBrush SkyBlue => KnownColor.SkyBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SlateBlue { get; private set; } + public static ISolidColorBrush SlateBlue => KnownColor.SlateBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SlateGray { get; private set; } + public static ISolidColorBrush SlateGray => KnownColor.SlateGray.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Snow { get; private set; } + public static ISolidColorBrush Snow => KnownColor.Snow.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SpringGreen { get; private set; } + public static ISolidColorBrush SpringGreen => KnownColor.SpringGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SteelBlue { get; private set; } + public static ISolidColorBrush SteelBlue => KnownColor.SteelBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Tan { get; private set; } + public static ISolidColorBrush Tan => KnownColor.Tan.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Teal { get; private set; } + public static ISolidColorBrush Teal => KnownColor.Teal.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Thistle { get; private set; } + public static ISolidColorBrush Thistle => KnownColor.Thistle.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Tomato { get; private set; } + public static ISolidColorBrush Tomato => KnownColor.Tomato.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Transparent { get; private set; } + public static ISolidColorBrush Transparent => KnownColor.Transparent.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Turquoise { get; private set; } + public static ISolidColorBrush Turquoise => KnownColor.Turquoise.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Violet { get; private set; } + public static ISolidColorBrush Violet => KnownColor.Violet.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Wheat { get; private set; } + public static ISolidColorBrush Wheat => KnownColor.Wheat.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush White { get; private set; } + public static ISolidColorBrush White => KnownColor.White.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush WhiteSmoke { get; private set; } + public static ISolidColorBrush WhiteSmoke => KnownColor.WhiteSmoke.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Yellow { get; private set; } + public static ISolidColorBrush Yellow => KnownColor.Yellow.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush YellowGreen { get; private set; } + public static ISolidColorBrush YellowGreen => KnownColor.YellowGreen.ToBrush(); } } diff --git a/src/Avalonia.Visuals/Media/Color.cs b/src/Avalonia.Visuals/Media/Color.cs index cbf5a86fd6..82cc19347a 100644 --- a/src/Avalonia.Visuals/Media/Color.cs +++ b/src/Avalonia.Visuals/Media/Color.cs @@ -88,6 +88,9 @@ namespace Avalonia.Media /// The . public static Color Parse(string s) { + if (s == null) throw new ArgumentNullException(nameof(s)); + if (s.Length == 0) throw new FormatException(); + if (s[0] == '#') { var or = 0u; @@ -103,21 +106,15 @@ namespace Avalonia.Media return FromUInt32(uint.Parse(s.Substring(1), NumberStyles.HexNumber, CultureInfo.InvariantCulture) | or); } - else - { - var upper = s.ToUpperInvariant(); - var member = typeof(Colors).GetTypeInfo().DeclaredProperties - .FirstOrDefault(x => x.Name.ToUpperInvariant() == upper); - if (member != null) - { - return (Color)member.GetValue(null); - } - else - { - throw new FormatException($"Invalid color string: '{s}'."); - } + var knownColor = KnownColors.GetKnownColor(s); + + if (knownColor != KnownColor.None) + { + return knownColor.ToColor(); } + + throw new FormatException($"Invalid color string: '{s}'."); } /// @@ -128,8 +125,8 @@ namespace Avalonia.Media /// public override string ToString() { - uint rgb = ((uint)A << 24) | ((uint)R << 16) | ((uint)G << 8) | (uint)B; - return $"#{rgb:x8}"; + uint rgb = ToUint32(); + return KnownColors.GetKnownColorName(rgb) ?? $"#{rgb:x8}"; } /// diff --git a/src/Avalonia.Visuals/Media/Colors.cs b/src/Avalonia.Visuals/Media/Colors.cs index 7296d7dd39..1067cf66aa 100644 --- a/src/Avalonia.Visuals/Media/Colors.cs +++ b/src/Avalonia.Visuals/Media/Colors.cs @@ -1,6 +1,8 @@ // 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.Linq; + namespace Avalonia.Media { /// @@ -11,706 +13,706 @@ namespace Avalonia.Media /// /// Gets a color with an ARGB value of #fff0f8ff. /// - public static Color AliceBlue => Color.FromUInt32(0xfff0f8ff); + public static Color AliceBlue => KnownColor.AliceBlue.ToColor(); /// /// Gets a color with an ARGB value of #fffaebd7. /// - public static Color AntiqueWhite => Color.FromUInt32(0xfffaebd7); + public static Color AntiqueWhite => KnownColor.AntiqueWhite.ToColor(); /// /// Gets a color with an ARGB value of #ff00ffff. /// - public static Color Aqua => Color.FromUInt32(0xff00ffff); + public static Color Aqua => KnownColor.Aqua.ToColor(); /// /// Gets a color with an ARGB value of #ff7fffd4. /// - public static Color Aquamarine => Color.FromUInt32(0xff7fffd4); + public static Color Aquamarine => KnownColor.Aquamarine.ToColor(); /// /// Gets a color with an ARGB value of #fff0ffff. /// - public static Color Azure => Color.FromUInt32(0xfff0ffff); + public static Color Azure => KnownColor.Azure.ToColor(); /// /// Gets a color with an ARGB value of #fff5f5dc. /// - public static Color Beige => Color.FromUInt32(0xfff5f5dc); + public static Color Beige => KnownColor.Beige.ToColor(); /// /// Gets a color with an ARGB value of #ffffe4c4. /// - public static Color Bisque => Color.FromUInt32(0xffffe4c4); + public static Color Bisque => KnownColor.Bisque.ToColor(); /// /// Gets a color with an ARGB value of #ff000000. /// - public static Color Black => Color.FromUInt32(0xff000000); + public static Color Black => KnownColor.Black.ToColor(); /// /// Gets a color with an ARGB value of #ffffebcd. /// - public static Color BlanchedAlmond => Color.FromUInt32(0xffffebcd); + public static Color BlanchedAlmond => KnownColor.BlanchedAlmond.ToColor(); /// /// Gets a color with an ARGB value of #ff0000ff. /// - public static Color Blue => Color.FromUInt32(0xff0000ff); + public static Color Blue => KnownColor.Blue.ToColor(); /// /// Gets a color with an ARGB value of #ff8a2be2. /// - public static Color BlueViolet => Color.FromUInt32(0xff8a2be2); + public static Color BlueViolet => KnownColor.BlueViolet.ToColor(); /// /// Gets a color with an ARGB value of #ffa52a2a. /// - public static Color Brown => Color.FromUInt32(0xffa52a2a); + public static Color Brown => KnownColor.Brown.ToColor(); /// /// Gets a color with an ARGB value of #ffdeb887. /// - public static Color BurlyWood => Color.FromUInt32(0xffdeb887); + public static Color BurlyWood => KnownColor.BurlyWood.ToColor(); /// /// Gets a color with an ARGB value of #ff5f9ea0. /// - public static Color CadetBlue => Color.FromUInt32(0xff5f9ea0); + public static Color CadetBlue => KnownColor.CadetBlue.ToColor(); /// /// Gets a color with an ARGB value of #ff7fff00. /// - public static Color Chartreuse => Color.FromUInt32(0xff7fff00); + public static Color Chartreuse => KnownColor.Chartreuse.ToColor(); /// /// Gets a color with an ARGB value of #ffd2691e. /// - public static Color Chocolate => Color.FromUInt32(0xffd2691e); + public static Color Chocolate => KnownColor.Chocolate.ToColor(); /// /// Gets a color with an ARGB value of #ffff7f50. /// - public static Color Coral => Color.FromUInt32(0xffff7f50); + public static Color Coral => KnownColor.Coral.ToColor(); /// /// Gets a color with an ARGB value of #ff6495ed. /// - public static Color CornflowerBlue => Color.FromUInt32(0xff6495ed); + public static Color CornflowerBlue => KnownColor.CornflowerBlue.ToColor(); /// /// Gets a color with an ARGB value of #fffff8dc. /// - public static Color Cornsilk => Color.FromUInt32(0xfffff8dc); + public static Color Cornsilk => KnownColor.Cornsilk.ToColor(); /// /// Gets a color with an ARGB value of #ffdc143c. /// - public static Color Crimson => Color.FromUInt32(0xffdc143c); + public static Color Crimson => KnownColor.Crimson.ToColor(); /// /// Gets a color with an ARGB value of #ff00ffff. /// - public static Color Cyan => Color.FromUInt32(0xff00ffff); + public static Color Cyan => KnownColor.Cyan.ToColor(); /// /// Gets a color with an ARGB value of #ff00008b. /// - public static Color DarkBlue => Color.FromUInt32(0xff00008b); + public static Color DarkBlue => KnownColor.DarkBlue.ToColor(); /// /// Gets a color with an ARGB value of #ff008b8b. /// - public static Color DarkCyan => Color.FromUInt32(0xff008b8b); + public static Color DarkCyan => KnownColor.DarkCyan.ToColor(); /// /// Gets a color with an ARGB value of #ffb8860b. /// - public static Color DarkGoldenrod => Color.FromUInt32(0xffb8860b); + public static Color DarkGoldenrod => KnownColor.DarkGoldenrod.ToColor(); /// /// Gets a color with an ARGB value of #ffa9a9a9. /// - public static Color DarkGray => Color.FromUInt32(0xffa9a9a9); + public static Color DarkGray => KnownColor.DarkGray.ToColor(); /// /// Gets a color with an ARGB value of #ff006400. /// - public static Color DarkGreen => Color.FromUInt32(0xff006400); + public static Color DarkGreen => KnownColor.DarkGreen.ToColor(); /// /// Gets a color with an ARGB value of #ffbdb76b. /// - public static Color DarkKhaki => Color.FromUInt32(0xffbdb76b); + public static Color DarkKhaki => KnownColor.DarkKhaki.ToColor(); /// /// Gets a color with an ARGB value of #ff8b008b. /// - public static Color DarkMagenta => Color.FromUInt32(0xff8b008b); + public static Color DarkMagenta => KnownColor.DarkMagenta.ToColor(); /// /// Gets a color with an ARGB value of #ff556b2f. /// - public static Color DarkOliveGreen => Color.FromUInt32(0xff556b2f); + public static Color DarkOliveGreen => KnownColor.DarkOliveGreen.ToColor(); /// /// Gets a color with an ARGB value of #ffff8c00. /// - public static Color DarkOrange => Color.FromUInt32(0xffff8c00); + public static Color DarkOrange => KnownColor.DarkOrange.ToColor(); /// /// Gets a color with an ARGB value of #ff9932cc. /// - public static Color DarkOrchid => Color.FromUInt32(0xff9932cc); + public static Color DarkOrchid => KnownColor.DarkOrchid.ToColor(); /// /// Gets a color with an ARGB value of #ff8b0000. /// - public static Color DarkRed => Color.FromUInt32(0xff8b0000); + public static Color DarkRed => KnownColor.DarkRed.ToColor(); /// /// Gets a color with an ARGB value of #ffe9967a. /// - public static Color DarkSalmon => Color.FromUInt32(0xffe9967a); + public static Color DarkSalmon => KnownColor.DarkSalmon.ToColor(); /// /// Gets a color with an ARGB value of #ff8fbc8f. /// - public static Color DarkSeaGreen => Color.FromUInt32(0xff8fbc8f); + public static Color DarkSeaGreen => KnownColor.DarkSeaGreen.ToColor(); /// /// Gets a color with an ARGB value of #ff483d8b. /// - public static Color DarkSlateBlue => Color.FromUInt32(0xff483d8b); + public static Color DarkSlateBlue => KnownColor.DarkSlateBlue.ToColor(); /// /// Gets a color with an ARGB value of #ff2f4f4f. /// - public static Color DarkSlateGray => Color.FromUInt32(0xff2f4f4f); + public static Color DarkSlateGray => KnownColor.DarkSlateGray.ToColor(); /// /// Gets a color with an ARGB value of #ff00ced1. /// - public static Color DarkTurquoise => Color.FromUInt32(0xff00ced1); + public static Color DarkTurquoise => KnownColor.DarkTurquoise.ToColor(); /// /// Gets a color with an ARGB value of #ff9400d3. /// - public static Color DarkViolet => Color.FromUInt32(0xff9400d3); + public static Color DarkViolet => KnownColor.DarkViolet.ToColor(); /// /// Gets a color with an ARGB value of #ffff1493. /// - public static Color DeepPink => Color.FromUInt32(0xffff1493); + public static Color DeepPink => KnownColor.DeepPink.ToColor(); /// /// Gets a color with an ARGB value of #ff00bfff. /// - public static Color DeepSkyBlue => Color.FromUInt32(0xff00bfff); + public static Color DeepSkyBlue => KnownColor.DeepSkyBlue.ToColor(); /// /// Gets a color with an ARGB value of #ff696969. /// - public static Color DimGray => Color.FromUInt32(0xff696969); + public static Color DimGray => KnownColor.DimGray.ToColor(); /// /// Gets a color with an ARGB value of #ff1e90ff. /// - public static Color DodgerBlue => Color.FromUInt32(0xff1e90ff); + public static Color DodgerBlue => KnownColor.DodgerBlue.ToColor(); /// /// Gets a color with an ARGB value of #ffb22222. /// - public static Color Firebrick => Color.FromUInt32(0xffb22222); + public static Color Firebrick => KnownColor.Firebrick.ToColor(); /// /// Gets a color with an ARGB value of #fffffaf0. /// - public static Color FloralWhite => Color.FromUInt32(0xfffffaf0); + public static Color FloralWhite => KnownColor.FloralWhite.ToColor(); /// /// Gets a color with an ARGB value of #ff228b22. /// - public static Color ForestGreen => Color.FromUInt32(0xff228b22); + public static Color ForestGreen => KnownColor.ForestGreen.ToColor(); /// /// Gets a color with an ARGB value of #ffff00ff. /// - public static Color Fuchsia => Color.FromUInt32(0xffff00ff); + public static Color Fuchsia => KnownColor.Fuchsia.ToColor(); /// /// Gets a color with an ARGB value of #ffdcdcdc. /// - public static Color Gainsboro => Color.FromUInt32(0xffdcdcdc); + public static Color Gainsboro => KnownColor.Gainsboro.ToColor(); /// /// Gets a color with an ARGB value of #fff8f8ff. /// - public static Color GhostWhite => Color.FromUInt32(0xfff8f8ff); + public static Color GhostWhite => KnownColor.GhostWhite.ToColor(); /// /// Gets a color with an ARGB value of #ffffd700. /// - public static Color Gold => Color.FromUInt32(0xffffd700); + public static Color Gold => KnownColor.Gold.ToColor(); /// /// Gets a color with an ARGB value of #ffdaa520. /// - public static Color Goldenrod => Color.FromUInt32(0xffdaa520); + public static Color Goldenrod => KnownColor.Goldenrod.ToColor(); /// /// Gets a color with an ARGB value of #ff808080. /// - public static Color Gray => Color.FromUInt32(0xff808080); + public static Color Gray => KnownColor.Gray.ToColor(); /// /// Gets a color with an ARGB value of #ff008000. /// - public static Color Green => Color.FromUInt32(0xff008000); + public static Color Green => KnownColor.Green.ToColor(); /// /// Gets a color with an ARGB value of #ffadff2f. /// - public static Color GreenYellow => Color.FromUInt32(0xffadff2f); + public static Color GreenYellow => KnownColor.GreenYellow.ToColor(); /// /// Gets a color with an ARGB value of #fff0fff0. /// - public static Color Honeydew => Color.FromUInt32(0xfff0fff0); + public static Color Honeydew => KnownColor.Honeydew.ToColor(); /// /// Gets a color with an ARGB value of #ffff69b4. /// - public static Color HotPink => Color.FromUInt32(0xffff69b4); + public static Color HotPink => KnownColor.HotPink.ToColor(); /// /// Gets a color with an ARGB value of #ffcd5c5c. /// - public static Color IndianRed => Color.FromUInt32(0xffcd5c5c); + public static Color IndianRed => KnownColor.IndianRed.ToColor(); /// /// Gets a color with an ARGB value of #ff4b0082. /// - public static Color Indigo => Color.FromUInt32(0xff4b0082); + public static Color Indigo => KnownColor.Indigo.ToColor(); /// /// Gets a color with an ARGB value of #fffffff0. /// - public static Color Ivory => Color.FromUInt32(0xfffffff0); + public static Color Ivory => KnownColor.Ivory.ToColor(); /// /// Gets a color with an ARGB value of #fff0e68c. /// - public static Color Khaki => Color.FromUInt32(0xfff0e68c); + public static Color Khaki => KnownColor.Khaki.ToColor(); /// /// Gets a color with an ARGB value of #ffe6e6fa. /// - public static Color Lavender => Color.FromUInt32(0xffe6e6fa); + public static Color Lavender => KnownColor.Lavender.ToColor(); /// /// Gets a color with an ARGB value of #fffff0f5. /// - public static Color LavenderBlush => Color.FromUInt32(0xfffff0f5); + public static Color LavenderBlush => KnownColor.LavenderBlush.ToColor(); /// /// Gets a color with an ARGB value of #ff7cfc00. /// - public static Color LawnGreen => Color.FromUInt32(0xff7cfc00); + public static Color LawnGreen => KnownColor.LawnGreen.ToColor(); /// /// Gets a color with an ARGB value of #fffffacd. /// - public static Color LemonChiffon => Color.FromUInt32(0xfffffacd); + public static Color LemonChiffon => KnownColor.LemonChiffon.ToColor(); /// /// Gets a color with an ARGB value of #ffadd8e6. /// - public static Color LightBlue => Color.FromUInt32(0xffadd8e6); + public static Color LightBlue => KnownColor.LightBlue.ToColor(); /// /// Gets a color with an ARGB value of #fff08080. /// - public static Color LightCoral => Color.FromUInt32(0xfff08080); + public static Color LightCoral => KnownColor.LightCoral.ToColor(); /// /// Gets a color with an ARGB value of #ffe0ffff. /// - public static Color LightCyan => Color.FromUInt32(0xffe0ffff); + public static Color LightCyan => KnownColor.LightCyan.ToColor(); /// /// Gets a color with an ARGB value of #fffafad2. /// - public static Color LightGoldenrodYellow => Color.FromUInt32(0xfffafad2); + public static Color LightGoldenrodYellow => KnownColor.LightGoldenrodYellow.ToColor(); /// /// Gets a color with an ARGB value of #ffd3d3d3. /// - public static Color LightGray => Color.FromUInt32(0xffd3d3d3); + public static Color LightGray => KnownColor.LightGray.ToColor(); /// /// Gets a color with an ARGB value of #ff90ee90. /// - public static Color LightGreen => Color.FromUInt32(0xff90ee90); + public static Color LightGreen => KnownColor.LightGreen.ToColor(); /// /// Gets a color with an ARGB value of #ffffb6c1. /// - public static Color LightPink => Color.FromUInt32(0xffffb6c1); + public static Color LightPink => KnownColor.LightPink.ToColor(); /// /// Gets a color with an ARGB value of #ffffa07a. /// - public static Color LightSalmon => Color.FromUInt32(0xffffa07a); + public static Color LightSalmon => KnownColor.LightSalmon.ToColor(); /// /// Gets a color with an ARGB value of #ff20b2aa. /// - public static Color LightSeaGreen => Color.FromUInt32(0xff20b2aa); + public static Color LightSeaGreen => KnownColor.LightSeaGreen.ToColor(); /// /// Gets a color with an ARGB value of #ff87cefa. /// - public static Color LightSkyBlue => Color.FromUInt32(0xff87cefa); + public static Color LightSkyBlue => KnownColor.LightSkyBlue.ToColor(); /// /// Gets a color with an ARGB value of #ff778899. /// - public static Color LightSlateGray => Color.FromUInt32(0xff778899); + public static Color LightSlateGray => KnownColor.LightSlateGray.ToColor(); /// /// Gets a color with an ARGB value of #ffb0c4de. /// - public static Color LightSteelBlue => Color.FromUInt32(0xffb0c4de); + public static Color LightSteelBlue => KnownColor.LightSteelBlue.ToColor(); /// /// Gets a color with an ARGB value of #ffffffe0. /// - public static Color LightYellow => Color.FromUInt32(0xffffffe0); + public static Color LightYellow => KnownColor.LightYellow.ToColor(); /// /// Gets a color with an ARGB value of #ff00ff00. /// - public static Color Lime => Color.FromUInt32(0xff00ff00); + public static Color Lime => KnownColor.Lime.ToColor(); /// /// Gets a color with an ARGB value of #ff32cd32. /// - public static Color LimeGreen => Color.FromUInt32(0xff32cd32); + public static Color LimeGreen => KnownColor.LimeGreen.ToColor(); /// /// Gets a color with an ARGB value of #fffaf0e6. /// - public static Color Linen => Color.FromUInt32(0xfffaf0e6); + public static Color Linen => KnownColor.Linen.ToColor(); /// /// Gets a color with an ARGB value of #ffff00ff. /// - public static Color Magenta => Color.FromUInt32(0xffff00ff); + public static Color Magenta => KnownColor.Magenta.ToColor(); /// /// Gets a color with an ARGB value of #ff800000. /// - public static Color Maroon => Color.FromUInt32(0xff800000); + public static Color Maroon => KnownColor.Maroon.ToColor(); /// /// Gets a color with an ARGB value of #ff66cdaa. /// - public static Color MediumAquamarine => Color.FromUInt32(0xff66cdaa); + public static Color MediumAquamarine => KnownColor.MediumAquamarine.ToColor(); /// /// Gets a color with an ARGB value of #ff0000cd. /// - public static Color MediumBlue => Color.FromUInt32(0xff0000cd); + public static Color MediumBlue => KnownColor.MediumBlue.ToColor(); /// /// Gets a color with an ARGB value of #ffba55d3. /// - public static Color MediumOrchid => Color.FromUInt32(0xffba55d3); + public static Color MediumOrchid => KnownColor.MediumOrchid.ToColor(); /// /// Gets a color with an ARGB value of #ff9370db. /// - public static Color MediumPurple => Color.FromUInt32(0xff9370db); + public static Color MediumPurple => KnownColor.MediumPurple.ToColor(); /// /// Gets a color with an ARGB value of #ff3cb371. /// - public static Color MediumSeaGreen => Color.FromUInt32(0xff3cb371); + public static Color MediumSeaGreen => KnownColor.MediumSeaGreen.ToColor(); /// /// Gets a color with an ARGB value of #ff7b68ee. /// - public static Color MediumSlateBlue => Color.FromUInt32(0xff7b68ee); + public static Color MediumSlateBlue => KnownColor.MediumSlateBlue.ToColor(); /// /// Gets a color with an ARGB value of #ff00fa9a. /// - public static Color MediumSpringGreen => Color.FromUInt32(0xff00fa9a); + public static Color MediumSpringGreen => KnownColor.MediumSpringGreen.ToColor(); /// /// Gets a color with an ARGB value of #ff48d1cc. /// - public static Color MediumTurquoise => Color.FromUInt32(0xff48d1cc); + public static Color MediumTurquoise => KnownColor.MediumTurquoise.ToColor(); /// /// Gets a color with an ARGB value of #ffc71585. /// - public static Color MediumVioletRed => Color.FromUInt32(0xffc71585); + public static Color MediumVioletRed => KnownColor.MediumVioletRed.ToColor(); /// /// Gets a color with an ARGB value of #ff191970. /// - public static Color MidnightBlue => Color.FromUInt32(0xff191970); + public static Color MidnightBlue => KnownColor.MidnightBlue.ToColor(); /// /// Gets a color with an ARGB value of #fff5fffa. /// - public static Color MintCream => Color.FromUInt32(0xfff5fffa); + public static Color MintCream => KnownColor.MintCream.ToColor(); /// /// Gets a color with an ARGB value of #ffffe4e1. /// - public static Color MistyRose => Color.FromUInt32(0xffffe4e1); + public static Color MistyRose => KnownColor.MistyRose.ToColor(); /// /// Gets a color with an ARGB value of #ffffe4b5. /// - public static Color Moccasin => Color.FromUInt32(0xffffe4b5); + public static Color Moccasin => KnownColor.Moccasin.ToColor(); /// /// Gets a color with an ARGB value of #ffffdead. /// - public static Color NavajoWhite => Color.FromUInt32(0xffffdead); + public static Color NavajoWhite => KnownColor.NavajoWhite.ToColor(); /// /// Gets a color with an ARGB value of #ff000080. /// - public static Color Navy => Color.FromUInt32(0xff000080); + public static Color Navy => KnownColor.Navy.ToColor(); /// /// Gets a color with an ARGB value of #fffdf5e6. /// - public static Color OldLace => Color.FromUInt32(0xfffdf5e6); + public static Color OldLace => KnownColor.OldLace.ToColor(); /// /// Gets a color with an ARGB value of #ff808000. /// - public static Color Olive => Color.FromUInt32(0xff808000); + public static Color Olive => KnownColor.Olive.ToColor(); /// /// Gets a color with an ARGB value of #ff6b8e23. /// - public static Color OliveDrab => Color.FromUInt32(0xff6b8e23); + public static Color OliveDrab => KnownColor.OliveDrab.ToColor(); /// /// Gets a color with an ARGB value of #ffffa500. /// - public static Color Orange => Color.FromUInt32(0xffffa500); + public static Color Orange => KnownColor.Orange.ToColor(); /// /// Gets a color with an ARGB value of #ffff4500. /// - public static Color OrangeRed => Color.FromUInt32(0xffff4500); + public static Color OrangeRed => KnownColor.OrangeRed.ToColor(); /// /// Gets a color with an ARGB value of #ffda70d6. /// - public static Color Orchid => Color.FromUInt32(0xffda70d6); + public static Color Orchid => KnownColor.Orchid.ToColor(); /// /// Gets a color with an ARGB value of #ffeee8aa. /// - public static Color PaleGoldenrod => Color.FromUInt32(0xffeee8aa); + public static Color PaleGoldenrod => KnownColor.PaleGoldenrod.ToColor(); /// /// Gets a color with an ARGB value of #ff98fb98. /// - public static Color PaleGreen => Color.FromUInt32(0xff98fb98); + public static Color PaleGreen => KnownColor.PaleGreen.ToColor(); /// /// Gets a color with an ARGB value of #ffafeeee. /// - public static Color PaleTurquoise => Color.FromUInt32(0xffafeeee); + public static Color PaleTurquoise => KnownColor.PaleTurquoise.ToColor(); /// /// Gets a color with an ARGB value of #ffdb7093. /// - public static Color PaleVioletRed => Color.FromUInt32(0xffdb7093); + public static Color PaleVioletRed => KnownColor.PaleVioletRed.ToColor(); /// /// Gets a color with an ARGB value of #ffffefd5. /// - public static Color PapayaWhip => Color.FromUInt32(0xffffefd5); + public static Color PapayaWhip => KnownColor.PapayaWhip.ToColor(); /// /// Gets a color with an ARGB value of #ffffdab9. /// - public static Color PeachPuff => Color.FromUInt32(0xffffdab9); + public static Color PeachPuff => KnownColor.PeachPuff.ToColor(); /// /// Gets a color with an ARGB value of #ffcd853f. /// - public static Color Peru => Color.FromUInt32(0xffcd853f); + public static Color Peru => KnownColor.Peru.ToColor(); /// /// Gets a color with an ARGB value of #ffffc0cb. /// - public static Color Pink => Color.FromUInt32(0xffffc0cb); + public static Color Pink => KnownColor.Pink.ToColor(); /// /// Gets a color with an ARGB value of #ffdda0dd. /// - public static Color Plum => Color.FromUInt32(0xffdda0dd); + public static Color Plum => KnownColor.Plum.ToColor(); /// /// Gets a color with an ARGB value of #ffb0e0e6. /// - public static Color PowderBlue => Color.FromUInt32(0xffb0e0e6); + public static Color PowderBlue => KnownColor.PowderBlue.ToColor(); /// /// Gets a color with an ARGB value of #ff800080. /// - public static Color Purple => Color.FromUInt32(0xff800080); + public static Color Purple => KnownColor.Purple.ToColor(); /// /// Gets a color with an ARGB value of #ffff0000. /// - public static Color Red => Color.FromUInt32(0xffff0000); + public static Color Red => KnownColor.Red.ToColor(); /// /// Gets a color with an ARGB value of #ffbc8f8f. /// - public static Color RosyBrown => Color.FromUInt32(0xffbc8f8f); + public static Color RosyBrown => KnownColor.RosyBrown.ToColor(); /// /// Gets a color with an ARGB value of #ff4169e1. /// - public static Color RoyalBlue => Color.FromUInt32(0xff4169e1); + public static Color RoyalBlue => KnownColor.RoyalBlue.ToColor(); /// /// Gets a color with an ARGB value of #ff8b4513. /// - public static Color SaddleBrown => Color.FromUInt32(0xff8b4513); + public static Color SaddleBrown => KnownColor.SaddleBrown.ToColor(); /// /// Gets a color with an ARGB value of #fffa8072. /// - public static Color Salmon => Color.FromUInt32(0xfffa8072); + public static Color Salmon => KnownColor.Salmon.ToColor(); /// /// Gets a color with an ARGB value of #fff4a460. /// - public static Color SandyBrown => Color.FromUInt32(0xfff4a460); + public static Color SandyBrown => KnownColor.SandyBrown.ToColor(); /// /// Gets a color with an ARGB value of #ff2e8b57. /// - public static Color SeaGreen => Color.FromUInt32(0xff2e8b57); + public static Color SeaGreen => KnownColor.SeaGreen.ToColor(); /// /// Gets a color with an ARGB value of #fffff5ee. /// - public static Color SeaShell => Color.FromUInt32(0xfffff5ee); + public static Color SeaShell => KnownColor.SeaShell.ToColor(); /// /// Gets a color with an ARGB value of #ffa0522d. /// - public static Color Sienna => Color.FromUInt32(0xffa0522d); + public static Color Sienna => KnownColor.Sienna.ToColor(); /// /// Gets a color with an ARGB value of #ffc0c0c0. /// - public static Color Silver => Color.FromUInt32(0xffc0c0c0); + public static Color Silver => KnownColor.Silver.ToColor(); /// /// Gets a color with an ARGB value of #ff87ceeb. /// - public static Color SkyBlue => Color.FromUInt32(0xff87ceeb); + public static Color SkyBlue => KnownColor.SkyBlue.ToColor(); /// /// Gets a color with an ARGB value of #ff6a5acd. /// - public static Color SlateBlue => Color.FromUInt32(0xff6a5acd); + public static Color SlateBlue => KnownColor.SlateBlue.ToColor(); /// /// Gets a color with an ARGB value of #ff708090. /// - public static Color SlateGray => Color.FromUInt32(0xff708090); + public static Color SlateGray => KnownColor.SlateGray.ToColor(); /// /// Gets a color with an ARGB value of #fffffafa. /// - public static Color Snow => Color.FromUInt32(0xfffffafa); + public static Color Snow => KnownColor.Snow.ToColor(); /// /// Gets a color with an ARGB value of #ff00ff7f. /// - public static Color SpringGreen => Color.FromUInt32(0xff00ff7f); + public static Color SpringGreen => KnownColor.SpringGreen.ToColor(); /// /// Gets a color with an ARGB value of #ff4682b4. /// - public static Color SteelBlue => Color.FromUInt32(0xff4682b4); + public static Color SteelBlue => KnownColor.SteelBlue.ToColor(); /// /// Gets a color with an ARGB value of #ffd2b48c. /// - public static Color Tan => Color.FromUInt32(0xffd2b48c); + public static Color Tan => KnownColor.Tan.ToColor(); /// /// Gets a color with an ARGB value of #ff008080. /// - public static Color Teal => Color.FromUInt32(0xff008080); + public static Color Teal => KnownColor.Teal.ToColor(); /// /// Gets a color with an ARGB value of #ffd8bfd8. /// - public static Color Thistle => Color.FromUInt32(0xffd8bfd8); + public static Color Thistle => KnownColor.Thistle.ToColor(); /// /// Gets a color with an ARGB value of #ffff6347. /// - public static Color Tomato => Color.FromUInt32(0xffff6347); + public static Color Tomato => KnownColor.Tomato.ToColor(); /// /// Gets a color with an ARGB value of #00ffffff. /// - public static Color Transparent => Color.FromUInt32(0x00ffffff); + public static Color Transparent => KnownColor.Transparent.ToColor(); /// /// Gets a color with an ARGB value of #ff40e0d0. /// - public static Color Turquoise => Color.FromUInt32(0xff40e0d0); + public static Color Turquoise => KnownColor.Turquoise.ToColor(); /// /// Gets a color with an ARGB value of #ffee82ee. /// - public static Color Violet => Color.FromUInt32(0xffee82ee); + public static Color Violet => KnownColor.Violet.ToColor(); /// /// Gets a color with an ARGB value of #fff5deb3. /// - public static Color Wheat => Color.FromUInt32(0xfff5deb3); + public static Color Wheat => KnownColor.Wheat.ToColor(); /// /// Gets a color with an ARGB value of #ffffffff. /// - public static Color White => Color.FromUInt32(0xffffffff); + public static Color White => KnownColor.White.ToColor(); /// /// Gets a color with an ARGB value of #fff5f5f5. /// - public static Color WhiteSmoke => Color.FromUInt32(0xfff5f5f5); + public static Color WhiteSmoke => KnownColor.WhiteSmoke.ToColor(); /// /// Gets a color with an ARGB value of #ffffff00. /// - public static Color Yellow => Color.FromUInt32(0xffffff00); + public static Color Yellow => KnownColor.Yellow.ToColor(); /// /// Gets a color with an ARGB value of #ff9acd32. /// - public static Color YellowGreen => Color.FromUInt32(0xff9acd32); + public static Color YellowGreen => KnownColor.YellowGreen.ToColor(); } } \ No newline at end of file diff --git a/src/Avalonia.Visuals/Media/GradientBrush.cs b/src/Avalonia.Visuals/Media/GradientBrush.cs index 8c2c9a2c01..fb29f68373 100644 --- a/src/Avalonia.Visuals/Media/GradientBrush.cs +++ b/src/Avalonia.Visuals/Media/GradientBrush.cs @@ -22,7 +22,7 @@ namespace Avalonia.Media /// Defines the property. /// public static readonly StyledProperty> GradientStopsProperty = - AvaloniaProperty.Register>(nameof(Opacity)); + AvaloniaProperty.Register>(nameof(GradientStops)); /// /// Initializes a new instance of the class. diff --git a/src/Avalonia.Visuals/Media/Imaging/WritableBitmap.cs b/src/Avalonia.Visuals/Media/Imaging/WriteableBitmap.cs similarity index 51% rename from src/Avalonia.Visuals/Media/Imaging/WritableBitmap.cs rename to src/Avalonia.Visuals/Media/Imaging/WriteableBitmap.cs index df3e71dc85..af6fde6876 100644 --- a/src/Avalonia.Visuals/Media/Imaging/WritableBitmap.cs +++ b/src/Avalonia.Visuals/Media/Imaging/WriteableBitmap.cs @@ -9,15 +9,15 @@ using Avalonia.Utilities; namespace Avalonia.Media.Imaging { /// - /// Holds a writable bitmap image. + /// Holds a writeable bitmap image. /// - public class WritableBitmap : Bitmap + public class WriteableBitmap : Bitmap { - public WritableBitmap(int width, int height, PixelFormat? format = null) - : base(AvaloniaLocator.Current.GetService().CreateWritableBitmap(width, height, format)) + public WriteableBitmap(int width, int height, PixelFormat? format = null) + : base(AvaloniaLocator.Current.GetService().CreateWriteableBitmap(width, height, format)) { } - public ILockedFramebuffer Lock() => ((IWritableBitmapImpl) PlatformImpl.Item).Lock(); + public ILockedFramebuffer Lock() => ((IWriteableBitmapImpl) PlatformImpl.Item).Lock(); } } diff --git a/src/Avalonia.Visuals/Media/KnownColors.cs b/src/Avalonia.Visuals/Media/KnownColors.cs new file mode 100644 index 0000000000..0887d2c913 --- /dev/null +++ b/src/Avalonia.Visuals/Media/KnownColors.cs @@ -0,0 +1,224 @@ +using System; +using System.Reflection; +using System.Collections.Generic; + +namespace Avalonia.Media +{ + internal static class KnownColors + { + private static readonly IReadOnlyDictionary _knownColorNames; + private static readonly IReadOnlyDictionary _knownColors; + private static readonly Dictionary _knownBrushes; + + static KnownColors() + { + var knownColorNames = new Dictionary(StringComparer.OrdinalIgnoreCase); + var knownColors = new Dictionary(); + + foreach (var field in typeof(KnownColor).GetRuntimeFields()) + { + if (field.FieldType != typeof(KnownColor)) continue; + var knownColor = (KnownColor)field.GetValue(null); + if (knownColor == KnownColor.None) continue; + + knownColorNames.Add(field.Name, knownColor); + + // some known colors have the same value, so use the first + if (!knownColors.ContainsKey((uint)knownColor)) + { + knownColors.Add((uint)knownColor, field.Name); + } + } + + _knownColorNames = knownColorNames; + _knownColors = knownColors; + _knownBrushes = new Dictionary(); + } + + public static ISolidColorBrush GetKnownBrush(string s) + { + var color = GetKnownColor(s); + return color != KnownColor.None ? color.ToBrush() : null; + } + + public static KnownColor GetKnownColor(string s) + { + if (_knownColorNames.TryGetValue(s, out var color)) + { + return color; + } + + return KnownColor.None; + } + + public static string GetKnownColorName(uint rgb) + { + return _knownColors.TryGetValue(rgb, out var name) ? name : null; + } + + public static Color ToColor(this KnownColor color) + { + return Color.FromUInt32((uint)color); + } + + public static ISolidColorBrush ToBrush(this KnownColor color) + { + lock (_knownBrushes) + { + if (!_knownBrushes.TryGetValue(color, out var brush)) + { + brush = new Immutable.ImmutableSolidColorBrush(color.ToColor()); + _knownBrushes.Add(color, brush); + } + + return brush; + } + } + } + + internal enum KnownColor : uint + { + None, + AliceBlue = 0xfff0f8ff, + AntiqueWhite = 0xfffaebd7, + Aqua = 0xff00ffff, + Aquamarine = 0xff7fffd4, + Azure = 0xfff0ffff, + Beige = 0xfff5f5dc, + Bisque = 0xffffe4c4, + Black = 0xff000000, + BlanchedAlmond = 0xffffebcd, + Blue = 0xff0000ff, + BlueViolet = 0xff8a2be2, + Brown = 0xffa52a2a, + BurlyWood = 0xffdeb887, + CadetBlue = 0xff5f9ea0, + Chartreuse = 0xff7fff00, + Chocolate = 0xffd2691e, + Coral = 0xffff7f50, + CornflowerBlue = 0xff6495ed, + Cornsilk = 0xfffff8dc, + Crimson = 0xffdc143c, + Cyan = 0xff00ffff, + DarkBlue = 0xff00008b, + DarkCyan = 0xff008b8b, + DarkGoldenrod = 0xffb8860b, + DarkGray = 0xffa9a9a9, + DarkGreen = 0xff006400, + DarkKhaki = 0xffbdb76b, + DarkMagenta = 0xff8b008b, + DarkOliveGreen = 0xff556b2f, + DarkOrange = 0xffff8c00, + DarkOrchid = 0xff9932cc, + DarkRed = 0xff8b0000, + DarkSalmon = 0xffe9967a, + DarkSeaGreen = 0xff8fbc8f, + DarkSlateBlue = 0xff483d8b, + DarkSlateGray = 0xff2f4f4f, + DarkTurquoise = 0xff00ced1, + DarkViolet = 0xff9400d3, + DeepPink = 0xffff1493, + DeepSkyBlue = 0xff00bfff, + DimGray = 0xff696969, + DodgerBlue = 0xff1e90ff, + Firebrick = 0xffb22222, + FloralWhite = 0xfffffaf0, + ForestGreen = 0xff228b22, + Fuchsia = 0xffff00ff, + Gainsboro = 0xffdcdcdc, + GhostWhite = 0xfff8f8ff, + Gold = 0xffffd700, + Goldenrod = 0xffdaa520, + Gray = 0xff808080, + Green = 0xff008000, + GreenYellow = 0xffadff2f, + Honeydew = 0xfff0fff0, + HotPink = 0xffff69b4, + IndianRed = 0xffcd5c5c, + Indigo = 0xff4b0082, + Ivory = 0xfffffff0, + Khaki = 0xfff0e68c, + Lavender = 0xffe6e6fa, + LavenderBlush = 0xfffff0f5, + LawnGreen = 0xff7cfc00, + LemonChiffon = 0xfffffacd, + LightBlue = 0xffadd8e6, + LightCoral = 0xfff08080, + LightCyan = 0xffe0ffff, + LightGoldenrodYellow = 0xfffafad2, + LightGreen = 0xff90ee90, + LightGray = 0xffd3d3d3, + LightPink = 0xffffb6c1, + LightSalmon = 0xffffa07a, + LightSeaGreen = 0xff20b2aa, + LightSkyBlue = 0xff87cefa, + LightSlateGray = 0xff778899, + LightSteelBlue = 0xffb0c4de, + LightYellow = 0xffffffe0, + Lime = 0xff00ff00, + LimeGreen = 0xff32cd32, + Linen = 0xfffaf0e6, + Magenta = 0xffff00ff, + Maroon = 0xff800000, + MediumAquamarine = 0xff66cdaa, + MediumBlue = 0xff0000cd, + MediumOrchid = 0xffba55d3, + MediumPurple = 0xff9370db, + MediumSeaGreen = 0xff3cb371, + MediumSlateBlue = 0xff7b68ee, + MediumSpringGreen = 0xff00fa9a, + MediumTurquoise = 0xff48d1cc, + MediumVioletRed = 0xffc71585, + MidnightBlue = 0xff191970, + MintCream = 0xfff5fffa, + MistyRose = 0xffffe4e1, + Moccasin = 0xffffe4b5, + NavajoWhite = 0xffffdead, + Navy = 0xff000080, + OldLace = 0xfffdf5e6, + Olive = 0xff808000, + OliveDrab = 0xff6b8e23, + Orange = 0xffffa500, + OrangeRed = 0xffff4500, + Orchid = 0xffda70d6, + PaleGoldenrod = 0xffeee8aa, + PaleGreen = 0xff98fb98, + PaleTurquoise = 0xffafeeee, + PaleVioletRed = 0xffdb7093, + PapayaWhip = 0xffffefd5, + PeachPuff = 0xffffdab9, + Peru = 0xffcd853f, + Pink = 0xffffc0cb, + Plum = 0xffdda0dd, + PowderBlue = 0xffb0e0e6, + Purple = 0xff800080, + Red = 0xffff0000, + RosyBrown = 0xffbc8f8f, + RoyalBlue = 0xff4169e1, + SaddleBrown = 0xff8b4513, + Salmon = 0xfffa8072, + SandyBrown = 0xfff4a460, + SeaGreen = 0xff2e8b57, + SeaShell = 0xfffff5ee, + Sienna = 0xffa0522d, + Silver = 0xffc0c0c0, + SkyBlue = 0xff87ceeb, + SlateBlue = 0xff6a5acd, + SlateGray = 0xff708090, + Snow = 0xfffffafa, + SpringGreen = 0xff00ff7f, + SteelBlue = 0xff4682b4, + Tan = 0xffd2b48c, + Teal = 0xff008080, + Thistle = 0xffd8bfd8, + Tomato = 0xffff6347, + Transparent = 0x00ffffff, + Turquoise = 0xff40e0d0, + Violet = 0xffee82ee, + Wheat = 0xfff5deb3, + White = 0xffffffff, + WhiteSmoke = 0xfff5f5f5, + Yellow = 0xffffff00, + YellowGreen = 0xff9acd32 + } +} \ No newline at end of file diff --git a/src/Avalonia.Visuals/Media/SkewTransform.cs b/src/Avalonia.Visuals/Media/SkewTransform.cs new file mode 100644 index 0000000000..880b73750b --- /dev/null +++ b/src/Avalonia.Visuals/Media/SkewTransform.cs @@ -0,0 +1,69 @@ +// 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 Avalonia.VisualTree; + +namespace Avalonia.Media +{ + /// + /// Skews an . + /// + public class SkewTransform : Transform + { + /// + /// Defines the property. + /// + public static readonly StyledProperty AngleXProperty = + AvaloniaProperty.Register(nameof(AngleX)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty AngleYProperty = + AvaloniaProperty.Register(nameof(AngleY)); + + /// + /// Initializes a new instance of the class. + /// + public SkewTransform() + { + this.GetObservable(AngleXProperty).Subscribe(_ => RaiseChanged()); + this.GetObservable(AngleYProperty).Subscribe(_ => RaiseChanged()); + } + + /// + /// Initializes a new instance of the class. + /// + /// The skew angle of X-axis, in degrees. + /// The skew angle of Y-axis, in degrees. + public SkewTransform(double angleX, double angleY) : this() + { + AngleX = angleX; + AngleY = angleY; + } + + /// + /// Gets or sets the AngleX property. + /// + public double AngleX + { + get { return GetValue(AngleXProperty); } + set { SetValue(AngleXProperty, value); } + } + + /// + /// Gets or sets the AngleY property. + /// + public double AngleY + { + get { return GetValue(AngleYProperty); } + set { SetValue(AngleYProperty, value); } + } + + /// + /// Gets the tranform's . + /// + public override Matrix Value => Matrix.CreateSkew(Matrix.ToRadians(AngleX), Matrix.ToRadians(AngleY)); + } +} \ No newline at end of file diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index aab8521f6d..cc17efd2bb 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -61,13 +61,13 @@ namespace Avalonia.Platform double dpiY); /// - /// Creates a writable bitmap implementation. + /// Creates a writeable bitmap implementation. /// /// The width of the bitmap. /// The height of the bitmap. /// Pixel format (optional). - /// An . - IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? format = null); + /// An . + IWriteableBitmapImpl CreateWriteableBitmap(int width, int height, PixelFormat? format = null); /// /// Loads a bitmap implementation from a file.. diff --git a/src/Avalonia.Visuals/Platform/IWritableBitmapImpl.cs b/src/Avalonia.Visuals/Platform/IWriteableBitmapImpl.cs similarity index 75% rename from src/Avalonia.Visuals/Platform/IWritableBitmapImpl.cs rename to src/Avalonia.Visuals/Platform/IWriteableBitmapImpl.cs index b736c11dab..7ab5a7c100 100644 --- a/src/Avalonia.Visuals/Platform/IWritableBitmapImpl.cs +++ b/src/Avalonia.Visuals/Platform/IWriteableBitmapImpl.cs @@ -7,9 +7,9 @@ using System.Threading.Tasks; namespace Avalonia.Platform { /// - /// Defines the platform-specific interface for a . + /// Defines the platform-specific interface for a . /// - public interface IWritableBitmapImpl : IBitmapImpl + public interface IWriteableBitmapImpl : IBitmapImpl { ILockedFramebuffer Lock(); } diff --git a/src/Avalonia.Visuals/Point.cs b/src/Avalonia.Visuals/Point.cs index 5fbd082967..62be3bf276 100644 --- a/src/Avalonia.Visuals/Point.cs +++ b/src/Avalonia.Visuals/Point.cs @@ -1,6 +1,7 @@ // 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 Avalonia.Utilities; using System; using System.Globalization; using System.Linq; @@ -169,21 +170,15 @@ namespace Avalonia /// Parses a string. /// /// The string. - /// The current culture. /// The . - public static Point Parse(string s, CultureInfo culture) + public static Point Parse(string s) { - var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) - .Select(x => x.Trim()) - .ToList(); - - if (parts.Count == 2) - { - return new Point(double.Parse(parts[0], culture), double.Parse(parts[1], culture)); - } - else + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Point")) { - throw new FormatException("Invalid Point."); + return new Point( + tokenizer.ReadDouble(), + tokenizer.ReadDouble() + ); } } diff --git a/src/Avalonia.Visuals/Properties/AssemblyInfo.cs b/src/Avalonia.Visuals/Properties/AssemblyInfo.cs index 87347d64b1..900746d05a 100644 --- a/src/Avalonia.Visuals/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Visuals/Properties/AssemblyInfo.cs @@ -9,6 +9,7 @@ using Avalonia.Metadata; [assembly: InternalsVisibleTo("Avalonia.Visuals.UnitTests")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")] [assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests")] [assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests")] \ No newline at end of file diff --git a/src/Avalonia.Visuals/Rect.cs b/src/Avalonia.Visuals/Rect.cs index d562429fc7..73021ca29a 100644 --- a/src/Avalonia.Visuals/Rect.cs +++ b/src/Avalonia.Visuals/Rect.cs @@ -1,6 +1,7 @@ // 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 Avalonia.Utilities; using System; using System.Globalization; using System.Linq; @@ -486,25 +487,17 @@ namespace Avalonia /// Parses a string. /// /// The string. - /// The current culture. /// The parsed . - public static Rect Parse(string s, CultureInfo culture) + public static Rect Parse(string s) { - var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) - .Select(x => x.Trim()) - .ToList(); - - if (parts.Count == 4) + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Rect")) { return new Rect( - double.Parse(parts[0], culture), - double.Parse(parts[1], culture), - double.Parse(parts[2], culture), - double.Parse(parts[3], culture)); - } - else - { - throw new FormatException("Invalid Rect."); + tokenizer.ReadDouble(), + tokenizer.ReadDouble(), + tokenizer.ReadDouble(), + tokenizer.ReadDouble() + ); } } } diff --git a/src/Avalonia.Visuals/RelativePoint.cs b/src/Avalonia.Visuals/RelativePoint.cs index cc34feb5f3..e4d7ea05cb 100644 --- a/src/Avalonia.Visuals/RelativePoint.cs +++ b/src/Avalonia.Visuals/RelativePoint.cs @@ -1,6 +1,7 @@ // 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 Avalonia.Utilities; using System; using System.Globalization; using System.Linq; @@ -153,41 +154,35 @@ namespace Avalonia /// Parses a string. /// /// The string. - /// The current culture. /// The parsed . - public static RelativePoint Parse(string s, CultureInfo culture) + public static RelativePoint Parse(string s) { - var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) - .Select(x => x.Trim()) - .ToList(); - - if (parts.Count == 2) + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid RelativePoint")) { + var x = tokenizer.ReadString(); + var y = tokenizer.ReadString(); + var unit = RelativeUnit.Absolute; var scale = 1.0; - if (parts[0].EndsWith("%")) + if (x.EndsWith("%")) { - if (!parts[1].EndsWith("%")) + if (!y.EndsWith("%")) { throw new FormatException("If one coordinate is relative, both must be."); } - parts[0] = parts[0].TrimEnd('%'); - parts[1] = parts[1].TrimEnd('%'); + x = x.TrimEnd('%'); + y = y.TrimEnd('%'); unit = RelativeUnit.Relative; scale = 0.01; } return new RelativePoint( - double.Parse(parts[0], culture) * scale, - double.Parse(parts[1], culture) * scale, + double.Parse(x, CultureInfo.InvariantCulture) * scale, + double.Parse(y, CultureInfo.InvariantCulture) * scale, unit); } - else - { - throw new FormatException("Invalid Point."); - } } } } diff --git a/src/Avalonia.Visuals/RelativeRect.cs b/src/Avalonia.Visuals/RelativeRect.cs index a11f080e94..ad42e30057 100644 --- a/src/Avalonia.Visuals/RelativeRect.cs +++ b/src/Avalonia.Visuals/RelativeRect.cs @@ -1,6 +1,7 @@ // 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 Avalonia.Utilities; using System; using System.Globalization; using System.Linq; @@ -12,6 +13,8 @@ namespace Avalonia /// public struct RelativeRect : IEquatable { + private static readonly char[] PercentChar = { '%' }; + /// /// A rectangle that represents 100% of an area. /// @@ -159,52 +162,51 @@ namespace Avalonia Rect.Width * size.Width, Rect.Height * size.Height); } - + /// /// Parses a string. /// /// The string. - /// The current culture. /// The parsed . - public static RelativeRect Parse(string s, CultureInfo culture) + public static RelativeRect Parse(string s) { - var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) - .Select(x => x.Trim()) - .ToList(); - - if (parts.Count == 4) + using (var tokenizer = new StringTokenizer(s, exceptionMessage: "Invalid RelativeRect")) { + var x = tokenizer.ReadString(); + var y = tokenizer.ReadString(); + var width = tokenizer.ReadString(); + var height = tokenizer.ReadString(); + var unit = RelativeUnit.Absolute; var scale = 1.0; - if (parts[0].EndsWith("%")) + var xRelative = x.EndsWith("%", StringComparison.Ordinal); + var yRelative = y.EndsWith("%", StringComparison.Ordinal); + var widthRelative = width.EndsWith("%", StringComparison.Ordinal); + var heightRelative = height.EndsWith("%", StringComparison.Ordinal); + + if (xRelative && yRelative && widthRelative && heightRelative) { - if (!parts[1].EndsWith("%") - || !parts[2].EndsWith("%") - || !parts[3].EndsWith("%")) - { - throw new FormatException("If one coordinate is relative, all other must be too."); - } - - parts[0] = parts[0].TrimEnd('%'); - parts[1] = parts[1].TrimEnd('%'); - parts[2] = parts[2].TrimEnd('%'); - parts[3] = parts[3].TrimEnd('%'); + x = x.TrimEnd(PercentChar); + y = y.TrimEnd(PercentChar); + width = width.TrimEnd(PercentChar); + height = height.TrimEnd(PercentChar); + unit = RelativeUnit.Relative; scale = 0.01; } + else if (xRelative || yRelative || widthRelative || heightRelative) + { + throw new FormatException("If one coordinate is relative, all must be."); + } return new RelativeRect( - double.Parse(parts[0], culture) * scale, - double.Parse(parts[1], culture) * scale, - double.Parse(parts[2], culture) * scale, - double.Parse(parts[3], culture) * scale, + double.Parse(x, CultureInfo.InvariantCulture) * scale, + double.Parse(y, CultureInfo.InvariantCulture) * scale, + double.Parse(width, CultureInfo.InvariantCulture) * scale, + double.Parse(height, CultureInfo.InvariantCulture) * scale, unit); } - else - { - throw new FormatException("Invalid RelativeRect."); - } } } } diff --git a/src/Avalonia.Visuals/Rendering/DefaultRenderLoop.cs b/src/Avalonia.Visuals/Rendering/DefaultRenderLoop.cs index 5dff3715b3..9cf849f59b 100644 --- a/src/Avalonia.Visuals/Rendering/DefaultRenderLoop.cs +++ b/src/Avalonia.Visuals/Rendering/DefaultRenderLoop.cs @@ -41,12 +41,12 @@ namespace Avalonia.Rendering { add { + _tick += value; + if (_subscriberCount++ == 0) { Start(); } - - _tick += value; } remove diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs index 1668f592ec..75ef49f8e7 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs @@ -69,6 +69,11 @@ namespace Avalonia.Rendering.SceneGraph /// IReadOnlyList> DrawOperations { get; } + /// + /// Gets the opacity of the scene graph node. + /// + double Opacity { get; } + /// /// Sets up the drawing context for rendering the node's geometry. /// diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index b219a74119..799380cb85 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -167,11 +167,14 @@ namespace Avalonia.Rendering.SceneGraph using (context.PushPostTransform(m)) using (context.PushTransformContainer()) { - var clipBounds = bounds.TransformToAABB(contextImpl.Transform).Intersect(clip); + var clipBounds = clipToBounds ? + bounds.TransformToAABB(contextImpl.Transform).Intersect(clip) : + clip; forceRecurse = forceRecurse || - node.Transform != contextImpl.Transform || - node.ClipBounds != clipBounds; + node.ClipBounds != clipBounds || + node.Opacity != opacity || + node.Transform != contextImpl.Transform; node.Transform = contextImpl.Transform; node.ClipBounds = clipBounds; diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index 3ee689b6d2..306036ca2c 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -67,9 +67,7 @@ namespace Avalonia.Rendering.SceneGraph /// public bool HasAncestorGeometryClip { get; } - /// - /// Gets or sets the opacity of the scene graph node. - /// + /// public double Opacity { get { return _opacity; } diff --git a/src/Avalonia.Visuals/Size.cs b/src/Avalonia.Visuals/Size.cs index 6ad87c6120..b6889af6f0 100644 --- a/src/Avalonia.Visuals/Size.cs +++ b/src/Avalonia.Visuals/Size.cs @@ -1,6 +1,7 @@ // 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 Avalonia.Utilities; using System; using System.Globalization; using System.Linq; @@ -149,21 +150,14 @@ namespace Avalonia /// Parses a string. /// /// The string. - /// The current culture. /// The . - public static Size Parse(string s, CultureInfo culture) + public static Size Parse(string s) { - var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) - .Select(x => x.Trim()) - .ToList(); - - if (parts.Count == 2) - { - return new Size(double.Parse(parts[0], culture), double.Parse(parts[1], culture)); - } - else + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Size")) { - throw new FormatException("Invalid Size."); + return new Size( + tokenizer.ReadDouble(), + tokenizer.ReadDouble()); } } diff --git a/src/Avalonia.Visuals/Thickness.cs b/src/Avalonia.Visuals/Thickness.cs index dc9be7341d..43a5fed9e7 100644 --- a/src/Avalonia.Visuals/Thickness.cs +++ b/src/Avalonia.Visuals/Thickness.cs @@ -1,6 +1,7 @@ // 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 Avalonia.Utilities; using System; using System.Globalization; using System.Linq; @@ -90,7 +91,12 @@ namespace Avalonia /// /// Gets a value indicating whether all sides are set to 0. /// - public bool IsEmpty => Left == 0 && Top == 0 && Right == 0 && Bottom == 0; + public bool IsEmpty => Left.Equals(0) && IsUniform; + + /// + /// Gets a value indicating whether all sides are equal. + /// + public bool IsUniform => Left.Equals(Right) && Top.Equals(Bottom) && Right.Equals(Bottom); /// /// Compares two Thicknesses. @@ -159,32 +165,28 @@ namespace Avalonia /// Parses a string. /// /// The string. - /// The current culture. /// The . - public static Thickness Parse(string s, CultureInfo culture) + public static Thickness Parse(string s) { - var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) - .Select(x => x.Trim()) - .ToList(); - - switch (parts.Count) + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Thickness")) { - case 1: - var uniform = double.Parse(parts[0], culture); - return new Thickness(uniform); - case 2: - var horizontal = double.Parse(parts[0], culture); - var vertical = double.Parse(parts[1], culture); - return new Thickness(horizontal, vertical); - case 4: - var left = double.Parse(parts[0], culture); - var top = double.Parse(parts[1], culture); - var right = double.Parse(parts[2], culture); - var bottom = double.Parse(parts[3], culture); - return new Thickness(left, top, right, bottom); + if(tokenizer.TryReadDouble(out var a)) + { + if (tokenizer.TryReadDouble(out var b)) + { + if (tokenizer.TryReadDouble(out var c)) + { + return new Thickness(a, b, c, tokenizer.ReadDouble()); + } + + return new Thickness(a, b); + } + + return new Thickness(a); + } + + throw new FormatException("Invalid Thickness."); } - - throw new FormatException("Invalid Thickness."); } /// diff --git a/src/Gtk/Avalonia.Gtk3/CursorFactory.cs b/src/Gtk/Avalonia.Gtk3/CursorFactory.cs index ac547b8bc2..d6a3c1f260 100644 --- a/src/Gtk/Avalonia.Gtk3/CursorFactory.cs +++ b/src/Gtk/Avalonia.Gtk3/CursorFactory.cs @@ -32,7 +32,10 @@ namespace Avalonia.Gtk3 {StandardCursorType.TopLeftCorner, CursorType.TopLeftCorner}, {StandardCursorType.TopRightCorner, CursorType.TopRightCorner}, {StandardCursorType.BottomLeftCorner, CursorType.BottomLeftCorner}, - {StandardCursorType.BottomRightCorner, CursorType.BottomRightCorner} + {StandardCursorType.BottomRightCorner, CursorType.BottomRightCorner}, + {StandardCursorType.DragCopy, CursorType.CenterPtr}, + {StandardCursorType.DragMove, CursorType.Fleur}, + {StandardCursorType.DragLink, CursorType.Cross}, }; private static readonly Dictionary Cache = diff --git a/src/Gtk/Avalonia.Gtk3/FramebufferManager.cs b/src/Gtk/Avalonia.Gtk3/FramebufferManager.cs index a673047e8c..82dbf53579 100644 --- a/src/Gtk/Avalonia.Gtk3/FramebufferManager.cs +++ b/src/Gtk/Avalonia.Gtk3/FramebufferManager.cs @@ -26,8 +26,9 @@ namespace Avalonia.Gtk3 { // This method may be called from non-UI thread, don't touch anything that calls back to GTK/GDK var s = _window.ClientSize; - var width = (int) s.Width; - var height = (int) s.Height; + var width = Math.Max(1, (int) s.Width); + var height = Math.Max(1, (int) s.Height); + if (!Dispatcher.UIThread.CheckAccess() && Gtk3Platform.DisplayClassName.ToLower().Contains("x11")) { diff --git a/src/Gtk/Avalonia.Gtk3/Interop/GObject.cs b/src/Gtk/Avalonia.Gtk3/Interop/GObject.cs index 8d14515d28..24bcfd71e9 100644 --- a/src/Gtk/Avalonia.Gtk3/Interop/GObject.cs +++ b/src/Gtk/Avalonia.Gtk3/Interop/GObject.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Text; @@ -21,7 +22,12 @@ namespace Avalonia.Gtk3.Interop protected override bool ReleaseHandle() { if (handle != IntPtr.Zero) + { + Debug.Assert(Native.GTypeCheckInstanceIsFundamentallyA(handle, new IntPtr(Native.G_TYPE_OBJECT)), + "Handle is not a GObject"); Native.GObjectUnref(handle); + } + handle = IntPtr.Zero; return true; } diff --git a/src/Gtk/Avalonia.Gtk3/Interop/Native.cs b/src/Gtk/Avalonia.Gtk3/Interop/Native.cs index 3e945798d4..1adaf9f4e1 100644 --- a/src/Gtk/Avalonia.Gtk3/Interop/Native.cs +++ b/src/Gtk/Avalonia.Gtk3/Interop/Native.cs @@ -115,6 +115,8 @@ namespace Avalonia.Gtk3.Interop [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate void gtk_window_set_title(GtkWindow gtkWindow, Utf8Buffer title); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] + public delegate void gtk_window_set_resizable(GtkWindow gtkWindow, bool resizable); [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate void gtk_window_set_decorated(GtkWindow gtkWindow, bool decorated); @@ -219,7 +221,10 @@ namespace Avalonia.Gtk3.Interop public delegate void gtk_widget_queue_draw_area(GtkWidget widget, int x, int y, int width, int height); [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] - public delegate void gtk_widget_add_tick_callback(GtkWidget widget, TickCallback callback, IntPtr userData, IntPtr destroy); + public delegate uint gtk_widget_add_tick_callback(GtkWidget widget, TickCallback callback, IntPtr userData, IntPtr destroy); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] + public delegate uint gtk_widget_remove_tick_callback(GtkWidget widget, uint id); [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate GtkImContext gtk_im_multicontext_new(); @@ -256,8 +261,11 @@ namespace Avalonia.Gtk3.Interop [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate void gtk_window_unmaximize(GtkWindow window); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] + public delegate void gtk_window_close(GtkWindow window); - [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gdk)] + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate void gtk_window_set_geometry_hints(GtkWindow window, IntPtr geometry_widget, ref GdkGeometry geometry, GdkWindowHints geom_mask); [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gdk)] @@ -341,6 +349,9 @@ namespace Avalonia.Gtk3.Interop [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Glib)] public delegate ulong g_free(IntPtr data); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gobject)] + public delegate bool g_type_check_instance_is_fundamentally_a(IntPtr instance, IntPtr type); [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Glib)] public unsafe delegate void g_slist_free(GSList* data); @@ -386,6 +397,7 @@ namespace Avalonia.Gtk3.Interop public static D.gdk_screen_get_monitor_geometry GdkScreenGetMonitorGeometry; public static D.gdk_screen_get_monitor_workarea GdkScreenGetMonitorWorkarea; public static D.gtk_window_set_decorated GtkWindowSetDecorated; + public static D.gtk_window_set_resizable GtkWindowSetResizable; public static D.gtk_window_set_skip_taskbar_hint GtkWindowSetSkipTaskbarHint; public static D.gtk_window_get_skip_taskbar_hint GtkWindowGetSkipTaskbarHint; public static D.gtk_window_set_skip_pager_hint GtkWindowSetSkipPagerHint; @@ -412,6 +424,7 @@ namespace Avalonia.Gtk3.Interop public static D.gdk_window_set_override_redirect GdkWindowSetOverrideRedirect; public static D.gtk_widget_set_size_request GtkWindowSetSizeRequest; public static D.gtk_window_set_default_size GtkWindowSetDefaultSize; + public static D.gtk_window_set_geometry_hints GtkWindowSetGeometryHints; public static D.gtk_window_get_position GtkWindowGetPosition; public static D.gtk_window_move GtkWindowMove; public static D.gtk_file_chooser_dialog_new GtkFileChooserDialogNew; @@ -427,6 +440,7 @@ namespace Avalonia.Gtk3.Interop public static D.g_timeout_add GTimeoutAdd; public static D.g_timeout_add_full GTimeoutAddFull; public static D.g_free GFree; + public static D.g_type_check_instance_is_fundamentally_a GTypeCheckInstanceIsFundamentallyA; public static D.g_slist_free GSlistFree; public static D.g_memory_input_stream_new_from_data GMemoryInputStreamNewFromData; public static D.gtk_widget_set_double_buffered GtkWidgetSetDoubleBuffered; @@ -434,6 +448,7 @@ namespace Avalonia.Gtk3.Interop public static D.gdk_window_invalidate_rect GdkWindowInvalidateRect; public static D.gtk_widget_queue_draw_area GtkWidgetQueueDrawArea; public static D.gtk_widget_add_tick_callback GtkWidgetAddTickCallback; + public static D.gtk_widget_remove_tick_callback GtkWidgetRemoveTickCallback; public static D.gtk_widget_activate GtkWidgetActivate; public static D.gtk_clipboard_get_for_display GtkClipboardGetForDisplay; public static D.gtk_clipboard_request_text GtkClipboardRequestText; @@ -456,6 +471,7 @@ namespace Avalonia.Gtk3.Interop public static D.gtk_window_deiconify GtkWindowDeiconify; public static D.gtk_window_maximize GtkWindowMaximize; public static D.gtk_window_unmaximize GtkWindowUnmaximize; + public static D.gtk_window_close GtkWindowClose; public static D.gdk_window_begin_move_drag GdkWindowBeginMoveDrag; public static D.gdk_window_begin_resize_drag GdkWindowBeginResizeDrag; public static D.gdk_event_request_motions GdkEventRequestMotions; @@ -490,6 +506,8 @@ namespace Avalonia.Gtk3.Interop public static D.cairo_set_font_size CairoSetFontSize; public static D.cairo_move_to CairoMoveTo; public static D.cairo_destroy CairoDestroy; + + public const int G_TYPE_OBJECT = 80; } public enum GtkWindowType @@ -726,19 +744,19 @@ namespace Avalonia.Gtk3.Interop } [StructLayout(LayoutKind.Sequential)] - struct GdkGeometry + public struct GdkGeometry { - gint min_width; - gint min_height; - gint max_width; - gint max_height; - gint base_width; - gint base_height; - gint width_inc; - gint height_inc; - gdouble min_aspect; - gdouble max_aspect; - gint win_gravity; + public gint min_width; + public gint min_height; + public gint max_width; + public gint max_height; + public gint base_width; + public gint base_height; + public gint width_inc; + public gint height_inc; + public gdouble min_aspect; + public gdouble max_aspect; + public gint win_gravity; } enum GdkWindowHints diff --git a/src/Gtk/Avalonia.Gtk3/KeyTransform.cs b/src/Gtk/Avalonia.Gtk3/KeyTransform.cs index de3a4766a1..5a34db2e04 100644 --- a/src/Gtk/Avalonia.Gtk3/KeyTransform.cs +++ b/src/Gtk/Avalonia.Gtk3/KeyTransform.cs @@ -151,8 +151,8 @@ namespace Avalonia.Gtk.Common { GdkKey.R2, Key.F22 }, { GdkKey.F23, Key.F23 }, { GdkKey.R4, Key.F24 }, - //{ GdkKey.?, Key.NumLock } - //{ GdkKey.?, Key.Scroll } + { GdkKey.Num_Lock, Key.NumLock }, + { GdkKey.Scroll_Lock, Key.Scroll }, //{ GdkKey.?, Key.LeftShift } //{ GdkKey.?, Key.RightShift } //{ GdkKey.?, Key.LeftCtrl } @@ -177,12 +177,12 @@ namespace Avalonia.Gtk.Common //{ GdkKey.?, Key.SelectMedia } //{ GdkKey.?, Key.LaunchApplication1 } //{ GdkKey.?, Key.LaunchApplication2 } - //{ GdkKey.?, Key.OemSemicolon } - //{ GdkKey.?, Key.OemPlus } - //{ GdkKey.?, Key.OemComma } - //{ GdkKey.?, Key.OemMinus } - //{ GdkKey.?, Key.OemPeriod } - //{ GdkKey.?, Key.Oem2 } + { GdkKey.semicolon, Key.OemSemicolon }, + { GdkKey.plus, Key.OemPlus }, + { GdkKey.comma, Key.OemComma }, + { GdkKey.minus, Key.OemMinus }, + { GdkKey.period, Key.OemPeriod }, + { GdkKey.slash, Key.Oem2 } //{ GdkKey.?, Key.OemTilde } //{ GdkKey.?, Key.AbntC1 } //{ GdkKey.?, Key.AbntC2 } diff --git a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs index c41a136bce..0ebfea998a 100644 --- a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs +++ b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs @@ -33,11 +33,11 @@ namespace Avalonia.Gtk3 private readonly AutoResetEvent _canSetNextOperation = new AutoResetEvent(true); internal IntPtr? GdkWindowHandle; private bool _overrideRedirect; + private uint? _tickCallback; public WindowBaseImpl(GtkWindow gtkWidget) { GtkWidget = gtkWidget; - Disposables.Add(gtkWidget); _framebuffer = new FramebufferManager(this); _imContext = Native.GtkImMulticontextNew(); Disposables.Add(_imContext); @@ -54,6 +54,7 @@ namespace Avalonia.Gtk3 ConnectEvent("key-press-event", OnKeyEvent); ConnectEvent("key-release-event", OnKeyEvent); ConnectEvent("leave-notify-event", OnLeaveNotifyEvent); + ConnectEvent("delete-event", OnClosingEvent); Connect("destroy", OnDestroy); Native.GtkWidgetRealize(gtkWidget); GdkWindowHandle = this.Handle.Handle; @@ -62,7 +63,7 @@ namespace Avalonia.Gtk3 { Native.GtkWidgetSetDoubleBuffered(gtkWidget, false); _gcHandle = GCHandle.Alloc(this); - Native.GtkWidgetAddTickCallback(GtkWidget, PinnedStaticCallback, GCHandle.ToIntPtr(_gcHandle), IntPtr.Zero); + _tickCallback = Native.GtkWidgetAddTickCallback(GtkWidget, PinnedStaticCallback, GCHandle.ToIntPtr(_gcHandle), IntPtr.Zero); } } @@ -103,7 +104,7 @@ namespace Avalonia.Gtk3 private bool OnDestroy(IntPtr gtkwidget, IntPtr userdata) { - Dispose(); + DoDispose(true); return false; } @@ -125,6 +126,12 @@ namespace Avalonia.Gtk3 return rv; } + private unsafe bool OnClosingEvent(IntPtr w, IntPtr ev, IntPtr userdata) + { + bool? preventClosing = Closing?.Invoke(); + return preventClosing ?? false; + } + private unsafe bool OnButton(IntPtr w, IntPtr ev, IntPtr userdata) { var evnt = (GdkEventButton*)ev; @@ -297,14 +304,28 @@ namespace Avalonia.Gtk3 } - public void Dispose() + public void Dispose() => DoDispose(false); + + void DoDispose(bool fromDestroy) { + if (_tickCallback.HasValue) + { + if (!GtkWidget.IsClosed) + Native.GtkWidgetRemoveTickCallback(GtkWidget, _tickCallback.Value); + _tickCallback = null; + } + //We are calling it here, since signal handler will be detached if (!GtkWidget.IsClosed) Closed?.Invoke(); foreach(var d in Disposables.AsEnumerable().Reverse()) d.Dispose(); Disposables.Clear(); + + if (!fromDestroy && !GtkWidget.IsClosed) + Native.GtkWindowClose(GtkWidget); + GtkWidget.Dispose(); + if (_gcHandle.IsAllocated) { _gcHandle.Free(); @@ -320,6 +341,20 @@ namespace Avalonia.Gtk3 } } + public void SetMinMaxSize(Size minSize, Size maxSize) + { + if (GtkWidget.IsClosed) + return; + + GdkGeometry geometry = new GdkGeometry(); + geometry.min_width = minSize.Width > 0 ? (int)minSize.Width : -1; + geometry.min_height = minSize.Height > 0 ? (int)minSize.Height : -1; + geometry.max_width = !Double.IsInfinity(maxSize.Width) && maxSize.Width > 0 ? (int)maxSize.Width : 999999; + geometry.max_height = !Double.IsInfinity(maxSize.Height) && maxSize.Height > 0 ? (int)maxSize.Height : 999999; + + Native.GtkWindowSetGeometryHints(GtkWidget, IntPtr.Zero, ref geometry, GdkWindowHints.GDK_HINT_MIN_SIZE | GdkWindowHints.GDK_HINT_MAX_SIZE); + } + public IMouseDevice MouseDevice => Gtk3Platform.Mouse; public double Scaling => LastKnownScaleFactor = (int) (Native.GtkWidgetGetScaleFactor?.Invoke(GtkWidget) ?? 1); @@ -329,6 +364,7 @@ namespace Avalonia.Gtk3 string IPlatformHandle.HandleDescriptor => "HWND"; public Action Activated { get; set; } + public Func Closing { get; set; } public Action Closed { get; set; } public Action Deactivated { get; set; } public Action Input { get; set; } @@ -409,6 +445,7 @@ namespace Avalonia.Gtk3 { if (GtkWidget.IsClosed) return; + Native.GtkWindowResize(GtkWidget, (int)value.Width, (int)value.Height); if (OverrideRedirect) { diff --git a/src/Gtk/Avalonia.Gtk3/WindowImpl.cs b/src/Gtk/Avalonia.Gtk3/WindowImpl.cs index c586661a7a..2d309e19d4 100644 --- a/src/Gtk/Avalonia.Gtk3/WindowImpl.cs +++ b/src/Gtk/Avalonia.Gtk3/WindowImpl.cs @@ -61,6 +61,8 @@ namespace Avalonia.Gtk3 } public void ShowTaskbarIcon(bool value) => Native.GtkWindowSetSkipTaskbarHint(GtkWidget, !value); + + public void CanResize(bool value) => Native.GtkWindowSetResizable(GtkWidget, value); class EmptyDisposable : IDisposable diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 2e8771082c..e388a49a6e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -30,6 +30,7 @@ Properties\SharedAssemblyInfo.cs + diff --git a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs index 5047ce5e61..79523dd498 100644 --- a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs +++ b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs @@ -197,11 +197,12 @@ namespace Avalonia.Markup.Xaml return result; } - internal static object LoadFromReader(XamlReader reader, AvaloniaXamlContext context = null) + internal static object LoadFromReader(XamlReader reader, AvaloniaXamlContext context = null, IAmbientProvider parentAmbientProvider = null) { var writer = AvaloniaXamlObjectWriter.Create( reader.SchemaContext, - context); + context, + parentAmbientProvider); XamlServices.Transform(reader, writer); writer.ApplyAllDelayedProperties(); diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs index a34ccaa413..bc3caff3b9 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs @@ -53,10 +53,7 @@ namespace Avalonia.Markup.Xaml.Converters } } - // First look for non-attached property on the type and then look for an attached property. - var property = AvaloniaPropertyRegistry.Instance.FindRegistered(type, s) ?? - AvaloniaPropertyRegistry.Instance.GetAttached(type) - .FirstOrDefault(x => x.Name == propertyName); + AvaloniaProperty property = AvaloniaPropertyRegistry.Instance.FindRegistered(type, propertyName); if (property == null) { diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/CornerRadiusTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/CornerRadiusTypeConverter.cs new file mode 100644 index 0000000000..d8c1ecd4eb --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/CornerRadiusTypeConverter.cs @@ -0,0 +1,19 @@ +using System; +using System.ComponentModel; +using System.Globalization; + +namespace Avalonia.Markup.Xaml.Converters +{ + public class CornerRadiusTypeConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return sourceType == typeof(string); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + return CornerRadius.Parse((string)value); + } + } +} \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/GridLengthTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/GridLengthTypeConverter.cs index 1f72ca325c..05f3bed04d 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/GridLengthTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/GridLengthTypeConverter.cs @@ -18,7 +18,7 @@ namespace Avalonia.Markup.Xaml.Converters public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { - return GridLength.Parse((string)value, culture); + return GridLength.Parse((string)value); } } } \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/MatrixTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/MatrixTypeConverter.cs index c477ff5637..fec6e31771 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/MatrixTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/MatrixTypeConverter.cs @@ -17,7 +17,7 @@ namespace Avalonia.Markup.Xaml.Converters public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { - return Matrix.Parse((string)value, culture); + return Matrix.Parse((string)value); } } } \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/PointTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/PointTypeConverter.cs index 1381fe7a75..9ca7212c68 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/PointTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/PointTypeConverter.cs @@ -17,7 +17,7 @@ namespace Avalonia.Markup.Xaml.Converters public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { - return Point.Parse((string)value, culture); + return Point.Parse((string)value); } } } \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs index b6c6da3055..29c7dbfd39 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs @@ -23,7 +23,7 @@ namespace Avalonia.Markup.Xaml.Converters var result = new List(pointStrs.Length); foreach (var pointStr in pointStrs) { - result.Add(Point.Parse(pointStr, culture)); + result.Add(Point.Parse(pointStr)); } return result; diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/RectTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/RectTypeConverter.cs index c9c6462f89..0946f95938 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/RectTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/RectTypeConverter.cs @@ -17,7 +17,7 @@ namespace Avalonia.Markup.Xaml.Converters public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { - return Rect.Parse((string)value, culture); + return Rect.Parse((string)value); } } } \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/RelativePointTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/RelativePointTypeConverter.cs index f68b8d66e6..b3178f1496 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/RelativePointTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/RelativePointTypeConverter.cs @@ -17,7 +17,7 @@ namespace Avalonia.Markup.Xaml.Converters public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { - return RelativePoint.Parse((string)value, culture); + return RelativePoint.Parse((string)value); } } } \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/RelativeRectTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/RelativeRectTypeConverter.cs index 64e39e224a..38c2833815 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/RelativeRectTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/RelativeRectTypeConverter.cs @@ -17,7 +17,7 @@ namespace Avalonia.Markup.Xaml.Converters public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { - return RelativeRect.Parse((string)value, culture); + return RelativeRect.Parse((string)value); } } } \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/SizeTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/SizeTypeConverter.cs index 73fef9ab1f..535e7948d5 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/SizeTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/SizeTypeConverter.cs @@ -17,7 +17,7 @@ namespace Avalonia.Markup.Xaml.Converters public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { - return Size.Parse((string)value, culture); + return Size.Parse((string)value); } } } \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/ThicknessTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/ThicknessTypeConverter.cs index 6ca5ec2f66..3a7652a153 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/ThicknessTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/ThicknessTypeConverter.cs @@ -17,7 +17,7 @@ namespace Avalonia.Markup.Xaml.Converters public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { - return Thickness.Parse((string)value, culture); + return Thickness.Parse((string)value); } } } \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs index c6705cbb4b..6e8fe1e4c0 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -43,6 +43,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions Mode = Mode, Path = pathInfo.Path, Priority = Priority, + Source = Source, RelativeSource = pathInfo.RelativeSource ?? RelativeSource, DefaultAnchor = new WeakReference(GetDefaultAnchor((ITypeDescriptorContext)serviceProvider)) }; diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs index 9089a13656..8e71c5f81b 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -36,13 +36,6 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions // Look upwards though the ambient context for IResourceProviders which might be able // to give us the resource. - // - // TODO: If we're in a template then only the ambient values since the root of the - // template wil be included here. We need some way to get hold of the parent ambient - // context and search that. See the test: - // - // StaticResource_Can_Be_Assigned_To_Property_In_ControlTemplate_In_Styles_File - // foreach (var ambientValue in ambientValues) { // We override XamlType.CanAssignTo in BindingXamlType so the results we get back diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaDefaultTypeConverters.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaDefaultTypeConverters.cs index 1cf5b6a58e..1ae24c8a34 100644 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaDefaultTypeConverters.cs +++ b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaDefaultTypeConverters.cs @@ -43,6 +43,7 @@ namespace Avalonia.Markup.Xaml.PortableXaml { typeof(Selector), typeof(SelectorTypeConverter)}, { typeof(SolidColorBrush), typeof(BrushTypeConverter) }, { typeof(Thickness), typeof(ThicknessTypeConverter) }, + { typeof(CornerRadius), typeof(CornerRadiusTypeConverter) }, { typeof(TimeSpan), typeof(TimeSpanTypeConverter) }, //{ typeof(Uri), typeof(Converters.UriTypeConverter) }, { typeof(Cursor), typeof(CursorTypeConverter) }, diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlObjectWriter.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlObjectWriter.cs index e0e2553f46..240ca291a8 100644 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlObjectWriter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlObjectWriter.cs @@ -11,7 +11,8 @@ namespace Avalonia.Markup.Xaml.PortableXaml { public static AvaloniaXamlObjectWriter Create( XamlSchemaContext schemaContext, - AvaloniaXamlContext context) + AvaloniaXamlContext context, + IAmbientProvider parentAmbientProvider = null) { var nameScope = new AvaloniaNameScope { Instance = context?.RootInstance }; @@ -23,8 +24,9 @@ namespace Avalonia.Markup.Xaml.PortableXaml }; return new AvaloniaXamlObjectWriter(schemaContext, - writerSettings.WithContext(context), - nameScope); + writerSettings.WithContext(context), + nameScope, + parentAmbientProvider); } private readonly DelayedValuesHelper _delayedValuesHelper = new DelayedValuesHelper(); @@ -34,9 +36,9 @@ namespace Avalonia.Markup.Xaml.PortableXaml private AvaloniaXamlObjectWriter( XamlSchemaContext schemaContext, XamlObjectWriterSettings settings, - AvaloniaNameScope nameScope - ) - : base(schemaContext, settings) + AvaloniaNameScope nameScope, + IAmbientProvider parentAmbientProvider) + : base(schemaContext, settings, parentAmbientProvider) { _nameScope = nameScope; } diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs index bdb21abd77..fda5da902a 100644 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs +++ b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs @@ -200,8 +200,7 @@ namespace Avalonia.Markup.Xaml.PortableXaml var type = (getter ?? setter).DeclaringType; - var prop = AvaloniaPropertyRegistry.Instance.GetAttached(type) - .FirstOrDefault(v => v.Name == attachablePropertyName); + var prop = AvaloniaPropertyRegistry.Instance.FindRegistered(type, attachablePropertyName); if (prop != null) { diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlType.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlType.cs index 7de96ea220..59dbba7084 100644 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlType.cs +++ b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlType.cs @@ -19,16 +19,36 @@ namespace Avalonia.Markup.Xaml.PortableXaml public class AvaloniaXamlType : XamlType { + static readonly AvaloniaPropertyTypeConverter propertyTypeConverter = new AvaloniaPropertyTypeConverter(); + public AvaloniaXamlType(Type underlyingType, XamlSchemaContext schemaContext) : base(underlyingType, schemaContext) { } + protected override XamlMember LookupAttachableMember(string name) + { + var m = base.LookupAttachableMember(name); + + if (m == null) + { + // Might be an AddOwnered attached property. + var avProp = AvaloniaPropertyRegistry.Instance.FindRegistered(UnderlyingType, name); + + if (avProp?.IsAttached == true) + { + return new AvaloniaPropertyXamlMember(avProp, this); + } + } + + return m; + } + protected override XamlMember LookupMember(string name, bool skipReadOnlyCheck) { var m = base.LookupMember(name, skipReadOnlyCheck); - if (m == null) + if (m == null && !name.Contains(".")) { //so far Portable.xaml haven't found the member/property //but what if we have AvaloniaProperty diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github index c066401445..faa952f3a0 160000 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github +++ b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github @@ -1 +1 @@ -Subproject commit c0664014455392ac221a765e66f9837704339b6f +Subproject commit faa952f3a05b4bdf2986d686f4154b1ab084508a diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs index 1d4dafc413..63fb9f193c 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs @@ -10,8 +10,10 @@ namespace Avalonia.Markup.Xaml.Templates public class TemplateContent { - public TemplateContent(IEnumerable namespaces, XamlReader reader) + public TemplateContent(IEnumerable namespaces, XamlReader reader, + IAmbientProvider ambientProvider) { + ParentAmbientProvider = ambientProvider; List = new XamlNodeList(reader.SchemaContext); //we need to rpeserve all namespace and prefixes to writer @@ -26,9 +28,11 @@ namespace Avalonia.Markup.Xaml.Templates public XamlNodeList List { get; } + private IAmbientProvider ParentAmbientProvider { get; } + public IControl Load() { - return (IControl)AvaloniaXamlLoader.LoadFromReader(List.GetReader()); + return (IControl)AvaloniaXamlLoader.LoadFromReader(List.GetReader(), parentAmbientProvider: ParentAmbientProvider); } public static IControl Load(object templateContent) diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateLoader.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateLoader.cs index 1085131230..e29485ddb0 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateLoader.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateLoader.cs @@ -14,7 +14,8 @@ namespace Avalonia.Markup.Xaml.Templates { var tdc = (ITypeDescriptorContext)serviceProvider; var ns = tdc.GetService(); - return new TemplateContent(ns.GetNamespacePrefixes(), xamlReader); + var ambientProvider = tdc.GetService(); + return new TemplateContent(ns.GetNamespacePrefixes(), xamlReader, ambientProvider); } public override XamlReader Save(object value, IServiceProvider serviceProvider) diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs index 90eabc69fb..ac64459dd7 100644 --- a/src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs +++ b/src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Linq; using System.Reactive.Linq; using Avalonia.Data; @@ -15,9 +16,9 @@ namespace Avalonia.Markup.Data.Plugins /// public bool Match(object obj, string propertyName) { - if (obj is AvaloniaObject a) + if (obj is AvaloniaObject o) { - return AvaloniaPropertyRegistry.Instance.FindRegistered(a, propertyName) != null; + return LookupProperty(o, propertyName) != null; } return false; @@ -39,7 +40,7 @@ namespace Avalonia.Markup.Data.Plugins var instance = reference.Target; var o = (AvaloniaObject)instance; - var p = AvaloniaPropertyRegistry.Instance.FindRegistered(o, propertyName); + var p = LookupProperty(o, propertyName); if (p != null) { @@ -57,6 +58,54 @@ namespace Avalonia.Markup.Data.Plugins } } + private static AvaloniaProperty LookupProperty(AvaloniaObject o, string propertyName) + { + if (!propertyName.Contains(".")) + { + return AvaloniaPropertyRegistry.Instance.FindRegistered(o, propertyName); + } + else + { + var split = propertyName.Split('.'); + + if (split.Length == 2) + { + // HACK: We need a way to resolve types here using something like IXamlTypeResolver. + // We don't currently have that so we have to make our best guess. + var type = split[0]; + var name = split[1]; + var registry = AvaloniaPropertyRegistry.Instance; + var registered = registry.GetRegisteredAttached(o.GetType()) + .Concat(registry.GetRegistered(o.GetType())); + + foreach (var p in registered) + { + if (p.Name == name && IsOfType(p.OwnerType, type)) + { + return p; + } + } + } + } + + return null; + } + + private static bool IsOfType(Type type, string typeName) + { + while (type != null) + { + if (type.Name == typeName) + { + return true; + } + + type = type.BaseType; + } + + return false; + } + private class Accessor : PropertyAccessorBase { private readonly WeakReference _reference; diff --git a/src/OSX/Avalonia.MonoMac/Avalonia.MonoMac.csproj b/src/OSX/Avalonia.MonoMac/Avalonia.MonoMac.csproj index 3a279c05fb..c31c131ea9 100644 --- a/src/OSX/Avalonia.MonoMac/Avalonia.MonoMac.csproj +++ b/src/OSX/Avalonia.MonoMac/Avalonia.MonoMac.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 True @@ -8,9 +8,6 @@ - - - @@ -22,4 +19,4 @@ - + \ No newline at end of file diff --git a/src/OSX/Avalonia.MonoMac/ClipboardImpl.cs b/src/OSX/Avalonia.MonoMac/ClipboardImpl.cs new file mode 100644 index 0000000000..f7b98c0c1f --- /dev/null +++ b/src/OSX/Avalonia.MonoMac/ClipboardImpl.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Input.Platform; +using MonoMac.AppKit; + +namespace Avalonia.MonoMac +{ + class ClipboardImpl : IClipboard + { + public Task GetTextAsync() + { + return Task.FromResult(NSPasteboard.GeneralPasteboard.GetStringForType(NSPasteboard.NSStringType)); + } + + public Task SetTextAsync(string text) + { + NSPasteboard.GeneralPasteboard.ClearContents(); + if (text != null) + NSPasteboard.GeneralPasteboard.SetStringForType(text, NSPasteboard.NSStringType); + return Task.CompletedTask; + } + + public async Task ClearAsync() + { + NSPasteboard.GeneralPasteboard.ClearContents(); + } + } +} + diff --git a/src/OSX/Avalonia.MonoMac/Cursor.cs b/src/OSX/Avalonia.MonoMac/Cursor.cs index 10445e62e2..d9370e527b 100644 --- a/src/OSX/Avalonia.MonoMac/Cursor.cs +++ b/src/OSX/Avalonia.MonoMac/Cursor.cs @@ -51,6 +51,10 @@ namespace Avalonia.MonoMac [StandardCursorType.TopSide] = NSCursor.ResizeUpCursor, [StandardCursorType.UpArrow] = NSCursor.ResizeUpCursor, [StandardCursorType.Wait] = NSCursor.ArrowCursor, //TODO + [StandardCursorType.DragMove] = NSCursor.DragCopyCursor, // TODO + [StandardCursorType.DragCopy] = NSCursor.DragCopyCursor, + [StandardCursorType.DragLink] = NSCursor.DragLinkCursor, + }; } diff --git a/src/OSX/Avalonia.MonoMac/DragSource.cs b/src/OSX/Avalonia.MonoMac/DragSource.cs new file mode 100644 index 0000000000..41a206b580 --- /dev/null +++ b/src/OSX/Avalonia.MonoMac/DragSource.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Runtime.InteropServices; +using System.Runtime.Serialization.Formatters.Binary; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Input.Platform; +using Avalonia.Input.Raw; +using MonoMac; +using MonoMac.AppKit; +using MonoMac.CoreGraphics; +using MonoMac.Foundation; +using MonoMac.OpenGL; + +namespace Avalonia.MonoMac +{ + public class DragSource : NSDraggingSource, IPlatformDragSource + { + private const string NSPasteboardTypeString = "public.utf8-plain-text"; + private const string NSPasteboardTypeFileUrl = "public.file-url"; + + private readonly Subject _result = new Subject(); + private readonly IInputManager _inputManager; + private DragDropEffects _allowedEffects; + + public override bool IgnoreModifierKeysWhileDragging => false; + + public DragSource() + { + _inputManager = AvaloniaLocator.Current.GetService(); + } + + private string DataFormatToUTI(string s) + { + if (s == DataFormats.FileNames) + return NSPasteboardTypeFileUrl; + if (s == DataFormats.Text) + return NSPasteboardTypeString; + return s; + } + + private NSDraggingItem CreateDraggingItem(string format, object data) + { + var pasteboardItem = new NSPasteboardItem(); + NSData nsData; + if (data is string s) + { + if (format == DataFormats.FileNames) + s = new Uri(s).AbsoluteUri; // Ensure file uris... + nsData = NSData.FromString(s); + } + else if (data is Stream strm) + nsData = NSData.FromStream(strm); + else if (data is byte[] bytes) + nsData = NSData.FromArray(bytes); + else + { + BinaryFormatter bf = new BinaryFormatter(); + using (var ms = new MemoryStream()) + { + bf.Serialize(ms, data); + ms.Position = 0; + nsData = NSData.FromStream(ms); + } + } + pasteboardItem.SetDataForType(nsData, DataFormatToUTI(format)); + + NSPasteboardWriting writing = new NSPasteboardWriting(pasteboardItem.Handle); + + return new NSDraggingItem(writing); + } + + public IEnumerable CreateDraggingItems(string format, object data) + { + if (format == DataFormats.FileNames && data is IEnumerable files) + { + foreach (var file in files) + yield return CreateDraggingItem(format, file); + + yield break; + } + + yield return CreateDraggingItem(format, data); + } + + + public async Task DoDragDrop(IDataObject data, DragDropEffects allowedEffects) + { + // We need the TopLevelImpl + a mouse location so we just wait for the next event. + var mouseEv = await _inputManager.PreProcess.OfType().FirstAsync(); + var view = ((mouseEv.Root as TopLevel)?.PlatformImpl as TopLevelImpl)?.View; + if (view == null) + return DragDropEffects.None; + + // Prepare the source event: + var pt = view.TranslateLocalPoint(mouseEv.Position).ToMonoMacPoint(); + var ev = NSEvent.MouseEvent(NSEventType.LeftMouseDown, pt, 0, 0, 0, null, 0, 0, 0); + + _allowedEffects = allowedEffects; + var items = data.GetDataFormats().SelectMany(fmt => CreateDraggingItems(fmt, data.Get(fmt))).ToArray(); + view.BeginDraggingSession(items ,ev, this); + + return await _result; + } + + public override NSDragOperation DraggingSourceOperationMaskForLocal(bool flag) + { + return DraggingInfo.ConvertDragOperation(_allowedEffects); + } + + public override void DraggedImageEndedAtOperation(NSImage image, CGPoint screenPoint, NSDragOperation operation) + { + _result.OnNext(DraggingInfo.ConvertDragOperation(operation)); + _result.OnCompleted(); + } + } +} \ No newline at end of file diff --git a/src/OSX/Avalonia.MonoMac/DraggingInfo.cs b/src/OSX/Avalonia.MonoMac/DraggingInfo.cs new file mode 100644 index 0000000000..fc5f52713e --- /dev/null +++ b/src/OSX/Avalonia.MonoMac/DraggingInfo.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Input; +using MonoMac.AppKit; +using MonoMac.Foundation; + +namespace Avalonia.MonoMac +{ + class DraggingInfo : IDataObject + { + private readonly NSDraggingInfo _info; + + public DraggingInfo(NSDraggingInfo info) + { + _info = info; + } + + internal static NSDragOperation ConvertDragOperation(DragDropEffects d) + { + NSDragOperation result = NSDragOperation.None; + if (d.HasFlag(DragDropEffects.Copy)) + result |= NSDragOperation.Copy; + if (d.HasFlag(DragDropEffects.Link)) + result |= NSDragOperation.Link; + if (d.HasFlag(DragDropEffects.Move)) + result |= NSDragOperation.Move; + return result; + } + + internal static DragDropEffects ConvertDragOperation(NSDragOperation d) + { + DragDropEffects result = DragDropEffects.None; + if (d.HasFlag(NSDragOperation.Copy)) + result |= DragDropEffects.Copy; + if (d.HasFlag(NSDragOperation.Link)) + result |= DragDropEffects.Link; + if (d.HasFlag(NSDragOperation.Move)) + result |= DragDropEffects.Move; + return result; + } + + public Point Location => new Point(_info.DraggingLocation.X, _info.DraggingLocation.Y); + + public IEnumerable GetDataFormats() + { + return _info.DraggingPasteboard.Types.Select(NSTypeToWellknownType); + } + + private string NSTypeToWellknownType(string type) + { + if (type == NSPasteboard.NSStringType) + return DataFormats.Text; + if (type == NSPasteboard.NSFilenamesType) + return DataFormats.FileNames; + return type; + } + + public string GetText() + { + return _info.DraggingPasteboard.GetStringForType(NSPasteboard.NSStringType); + } + + public IEnumerable GetFileNames() + { + using(var fileNames = (NSArray)_info.DraggingPasteboard.GetPropertyListForType(NSPasteboard.NSFilenamesType)) + { + if (fileNames != null) + return NSArray.StringArrayFromHandle(fileNames.Handle); + } + + return Enumerable.Empty(); + } + + public bool Contains(string dataFormat) + { + return GetDataFormats().Any(f => f == dataFormat); + } + + public object Get(string dataFormat) + { + if (dataFormat == DataFormats.Text) + return GetText(); + if (dataFormat == DataFormats.FileNames) + return GetFileNames(); + + return _info.DraggingPasteboard.GetDataForType(dataFormat).ToArray(); + } + } +} \ No newline at end of file diff --git a/src/OSX/Avalonia.MonoMac/EmulatedFramebuffer.cs b/src/OSX/Avalonia.MonoMac/EmulatedFramebuffer.cs index 935ab53432..6cd2b16afa 100644 --- a/src/OSX/Avalonia.MonoMac/EmulatedFramebuffer.cs +++ b/src/OSX/Avalonia.MonoMac/EmulatedFramebuffer.cs @@ -12,6 +12,7 @@ namespace Avalonia.MonoMac private readonly TopLevelImpl.TopLevelView _view; private readonly CGSize _logicalSize; private readonly bool _isDeferred; + private readonly IUnmanagedBlob _blob; [DllImport("libc")] static extern void memset(IntPtr p, int c, IntPtr size); @@ -29,13 +30,13 @@ namespace Avalonia.MonoMac Dpi = new Vector(96 * pixelSize.Width / _logicalSize.Width, 96 * pixelSize.Height / _logicalSize.Height); Format = PixelFormat.Rgba8888; var size = Height * RowBytes; - Address = Marshal.AllocHGlobal(size); + _blob = AvaloniaLocator.Current.GetService().AllocBlob(size); memset(Address, 0, new IntPtr(size)); } public void Dispose() { - if (Address == IntPtr.Zero) + if (_blob.IsDisposed) return; var nfo = (int) CGBitmapFlags.ByteOrder32Big | (int) CGImageAlphaInfo.PremultipliedLast; CGImage image = null; @@ -71,14 +72,13 @@ namespace Avalonia.MonoMac else _view.SetBackBufferImage(new SavedImage(image, _logicalSize)); } - Marshal.FreeHGlobal(Address); - Address = IntPtr.Zero; + _blob.Dispose(); } } - public IntPtr Address { get; private set; } + public IntPtr Address => _blob.Address; public int Width { get; } public int Height { get; } public int RowBytes { get; } diff --git a/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs b/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs index a6b1f1d5b4..ba45ad8403 100644 --- a/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs +++ b/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs @@ -3,6 +3,7 @@ using System.Threading; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Input; +using Avalonia.Input.Platform; using Avalonia.Platform; using Avalonia.Rendering; using MonoMac.AppKit; @@ -32,8 +33,10 @@ namespace Avalonia.MonoMac .Bind().ToConstant(this) .Bind().ToConstant(this) .Bind().ToSingleton() + .Bind().ToSingleton() .Bind().ToConstant(s_renderLoop) - .Bind().ToConstant(PlatformThreadingInterface.Instance); + .Bind().ToConstant(PlatformThreadingInterface.Instance) + /*.Bind().ToTransient()*/; } public static void Initialize() diff --git a/src/OSX/Avalonia.MonoMac/PlatformThreadingInterface.cs b/src/OSX/Avalonia.MonoMac/PlatformThreadingInterface.cs index 88189ba12c..b02a3192de 100644 --- a/src/OSX/Avalonia.MonoMac/PlatformThreadingInterface.cs +++ b/src/OSX/Avalonia.MonoMac/PlatformThreadingInterface.cs @@ -66,11 +66,10 @@ namespace Avalonia.MonoMac var ev = app.NextEvent(NSEventMask.AnyEvent, NSDate.DistantFuture, NSRunLoop.NSDefaultRunLoopMode, true); if (ev != null) { - Console.WriteLine("NSEVENT"); app.SendEvent(ev); ev.Dispose(); } } } } -} \ No newline at end of file +} diff --git a/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs b/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs index 667ee12fa0..db7f29f05b 100644 --- a/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs +++ b/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs @@ -18,6 +18,7 @@ namespace Avalonia.MonoMac { public TopLevelView View { get; } private readonly IMouseDevice _mouse = AvaloniaLocator.Current.GetService(); + private readonly IDragDropDevice _dragDevice = AvaloniaLocator.Current.GetService(); protected TopLevelImpl() { View = new TopLevelView(this); @@ -39,6 +40,7 @@ namespace Avalonia.MonoMac private NSTrackingArea _area; private NSCursor _cursor; private bool _nonUiRedrawQueued; + private bool _isMouseOver; public CGSize PixelSize { get; set; } @@ -52,6 +54,10 @@ namespace Avalonia.MonoMac _tl = tl; _mouse = AvaloniaLocator.Current.GetService(); _keyboard = AvaloniaLocator.Current.GetService(); + + RegisterForDraggedTypes(new string[] { + "public.data" // register for any kind of data. + }); } protected override void Dispose(bool disposing) @@ -133,7 +139,11 @@ namespace Avalonia.MonoMac { ResetCursorRects(); if (_cursor != null) + { AddCursorRect(Frame, _cursor); + if (_isMouseOver) + _cursor.Set(); + } } static readonly NSCursor ArrowCursor = NSCursor.ArrowCursor; @@ -144,6 +154,48 @@ namespace Avalonia.MonoMac UpdateCursor(); } + private NSDragOperation SendRawDragEvent(NSDraggingInfo sender, RawDragEventType type) + { + Action input = _tl.Input; + IDragDropDevice dragDevice = _tl._dragDevice; + IInputRoot root = _tl?.InputRoot; + if (root == null || dragDevice == null || input == null) + return NSDragOperation.None; + + var dragOp = DraggingInfo.ConvertDragOperation(sender.DraggingSourceOperationMask); + DraggingInfo info = new DraggingInfo(sender); + var pt = TranslateLocalPoint(info.Location); + var args = new RawDragEvent(dragDevice, type, root, pt, info, dragOp); + input(args); + return DraggingInfo.ConvertDragOperation(args.Effects); + } + + public override NSDragOperation DraggingEntered(NSDraggingInfo sender) + { + return SendRawDragEvent(sender, RawDragEventType.DragEnter); + } + + public override NSDragOperation DraggingUpdated(NSDraggingInfo sender) + { + return SendRawDragEvent(sender, RawDragEventType.DragOver); + } + + public override void DraggingExited(NSDraggingInfo sender) + { + SendRawDragEvent(sender, RawDragEventType.DragLeave); + } + + public override bool PrepareForDragOperation(NSDraggingInfo sender) + { + return SendRawDragEvent(sender, RawDragEventType.DragOver) != NSDragOperation.None; + } + + public override bool PerformDragOperation(NSDraggingInfo sender) + { + return SendRawDragEvent(sender, RawDragEventType.Drop) != NSDragOperation.None; + } + + public override void SetFrameSize(CGSize newSize) { lock (SyncRoot) @@ -299,10 +351,17 @@ namespace Avalonia.MonoMac public override void MouseExited(NSEvent theEvent) { + _isMouseOver = false; MouseEvent(theEvent, RawMouseEventType.LeaveWindow); base.MouseExited(theEvent); } + public override void MouseEntered(NSEvent theEvent) + { + _isMouseOver = true; + base.MouseEntered(theEvent); + } + void KeyboardEvent(RawKeyEventType type, NSEvent ev) { var code = KeyTransform.TransformKeyCode(ev.KeyCode); diff --git a/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs b/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs index 9ce1756aae..8cbc6cbdd8 100644 --- a/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs +++ b/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs @@ -4,6 +4,7 @@ using Avalonia.Input.Raw; using Avalonia.Platform; using MonoMac.AppKit; using MonoMac.CoreGraphics; +using MonoMac.Foundation; using MonoMac.ObjCRuntime; namespace Avalonia.MonoMac @@ -69,6 +70,12 @@ namespace Avalonia.MonoMac _impl.PositionChanged?.Invoke(_impl.Position); } + public override bool WindowShouldClose(NSObject sender) + { + bool? preventClose = _impl.Closing?.Invoke(); + return preventClose != true; + } + public override void WillClose(global::MonoMac.Foundation.NSNotification notification) { _impl.Window.Dispose(); @@ -107,6 +114,7 @@ namespace Avalonia.MonoMac public Action PositionChanged { get; set; } public Action Deactivated { get; set; } public Action Activated { get; set; } + public Func Closing { get; set; } public override Size ClientSize => Window.ContentRectFor(Window.Frame).Size.ToAvaloniaSize(); @@ -153,6 +161,10 @@ namespace Avalonia.MonoMac Position = pos; } + public void SetMinMaxSize(Size minSize, Size maxSize) + { + } + public IScreenImpl Screen { get; diff --git a/src/OSX/Avalonia.MonoMac/WindowImpl.cs b/src/OSX/Avalonia.MonoMac/WindowImpl.cs index 6825fce82e..d01cbd6ae3 100644 --- a/src/OSX/Avalonia.MonoMac/WindowImpl.cs +++ b/src/OSX/Avalonia.MonoMac/WindowImpl.cs @@ -9,6 +9,7 @@ namespace Avalonia.MonoMac class WindowImpl : WindowBaseImpl, IWindowImpl { public bool IsDecorated = true; + public bool IsResizable = true; public CGRect? UndecoratedLastUnmaximizedFrame; public WindowImpl() @@ -76,10 +77,15 @@ namespace Avalonia.MonoMac protected override NSWindowStyle GetStyle() { + var windowStyle = NSWindowStyle.Borderless; + if (IsDecorated) - return NSWindowStyle.Closable | NSWindowStyle.Resizable | NSWindowStyle.Miniaturizable | - NSWindowStyle.Titled; - return NSWindowStyle.Borderless; + windowStyle |= NSWindowStyle.Closable | NSWindowStyle.Miniaturizable | NSWindowStyle.Titled; + + if (IsResizable) + windowStyle |= NSWindowStyle.Resizable; + + return windowStyle; } public void SetSystemDecorations(bool enabled) @@ -88,6 +94,12 @@ namespace Avalonia.MonoMac UpdateStyle(); } + public void CanResize(bool value) + { + IsResizable = value; + UpdateStyle(); + } + public void SetTitle(string title) => Window.Title = title; class ModalDisposable : IDisposable diff --git a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs index b777736f06..16977d5f97 100644 --- a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs +++ b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs @@ -28,11 +28,13 @@ namespace Avalonia.Shared.PlatformSupport class UnmanagedBlob : IUnmanagedBlob { private readonly StandardRuntimePlatform _plat; + private IntPtr _address; + private readonly object _lock = new object(); #if DEBUG private static readonly List Backtraces = new List(); private static Thread GCThread; private readonly string _backtrace; - + private static readonly object _btlock = new object(); class GCThreadDetector { @@ -55,28 +57,35 @@ namespace Avalonia.Shared.PlatformSupport public UnmanagedBlob(StandardRuntimePlatform plat, int size) { + if (size <= 0) + throw new ArgumentException("Positive number required", nameof(size)); _plat = plat; - Address = plat.Alloc(size); + _address = plat.Alloc(size); GC.AddMemoryPressure(size); Size = size; #if DEBUG _backtrace = Environment.StackTrace; - Backtraces.Add(_backtrace); + lock (_btlock) + Backtraces.Add(_backtrace); #endif } void DoDispose() { - if (!IsDisposed) + lock (_lock) { + if (!IsDisposed) + { #if DEBUG - Backtraces.Remove(_backtrace); + lock (_btlock) + Backtraces.Remove(_backtrace); #endif - _plat.Free(Address, Size); - GC.RemoveMemoryPressure(Size); - IsDisposed = true; - Address = IntPtr.Zero; - Size = 0; + _plat.Free(_address, Size); + GC.RemoveMemoryPressure(Size); + IsDisposed = true; + _address = IntPtr.Zero; + Size = 0; + } } } @@ -102,7 +111,7 @@ namespace Avalonia.Shared.PlatformSupport DoDispose(); } - public IntPtr Address { get; private set; } + public IntPtr Address => IsDisposed ? throw new ObjectDisposedException("UnmanagedBlob") : _address; public int Size { get; private set; } public bool IsDisposed { get; private set; } } @@ -155,4 +164,4 @@ namespace Avalonia.Shared.PlatformSupport void Free(IntPtr ptr, int len) => Marshal.FreeHGlobal(ptr); #endif } -} \ No newline at end of file +} diff --git a/src/Shared/SharedAssemblyInfo.cs b/src/Shared/SharedAssemblyInfo.cs index 98047b4cc8..44ebef4cd2 100644 --- a/src/Shared/SharedAssemblyInfo.cs +++ b/src/Shared/SharedAssemblyInfo.cs @@ -14,6 +14,6 @@ using System.Runtime.CompilerServices; [assembly: AssemblyTrademark("")] [assembly: NeutralResourcesLanguage("en")] -[assembly: AssemblyVersion("0.6.0")] -[assembly: AssemblyFileVersion("0.6.0")] -[assembly: AssemblyInformationalVersion("0.6.0")] +[assembly: AssemblyVersion("0.6.2")] +[assembly: AssemblyFileVersion("0.6.2")] +[assembly: AssemblyInformationalVersion("0.6.2")] diff --git a/src/Skia/Avalonia.Skia/BitmapImpl.cs b/src/Skia/Avalonia.Skia/BitmapImpl.cs index 00ab770e01..ccc5a37105 100644 --- a/src/Skia/Avalonia.Skia/BitmapImpl.cs +++ b/src/Skia/Avalonia.Skia/BitmapImpl.cs @@ -6,7 +6,7 @@ using SkiaSharp; namespace Avalonia.Skia { - class BitmapImpl : IRenderTargetBitmapImpl, IWritableBitmapImpl + class BitmapImpl : IRenderTargetBitmapImpl, IWriteableBitmapImpl { private Vector _dpi; diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index bd3769e4a5..50e65f45dc 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -88,7 +88,7 @@ namespace Avalonia.Skia return new FramebufferRenderTarget(fb); } - public IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? format = null) + public IWriteableBitmapImpl CreateWriteableBitmap(int width, int height, PixelFormat? format = null) { return new BitmapImpl(width, height, new Vector(96, 96), format); } diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index a47c871f5a..296edcb2d9 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -168,9 +168,9 @@ namespace Avalonia.Direct2D1 dpiY); } - public IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? format = null) + public IWriteableBitmapImpl CreateWriteableBitmap(int width, int height, PixelFormat? format = null) { - return new WritableWicBitmapImpl(s_imagingFactory, width, height, format); + return new WriteableWicBitmapImpl(s_imagingFactory, width, height, format); } public IStreamGeometryImpl CreateStreamGeometry() diff --git a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs index 8f11d1463b..120ab71ead 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs @@ -1,7 +1,6 @@ // 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 Avalonia.Platform; using SharpDX.Direct2D1; @@ -20,14 +19,12 @@ namespace Avalonia.Direct2D1.Media /// public Rect Bounds => Geometry.GetWidenedBounds(0).ToAvalonia(); - /// public Geometry Geometry { get; } /// public Rect GetRenderBounds(Avalonia.Media.Pen pen) { - var factory = AvaloniaLocator.Current.GetService(); - return Geometry.GetWidenedBounds((float)pen.Thickness).ToAvalonia(); + return Geometry.GetWidenedBounds((float)(pen?.Thickness ?? 0)).ToAvalonia(); } /// @@ -51,7 +48,7 @@ namespace Avalonia.Direct2D1.Media /// public bool StrokeContains(Avalonia.Media.Pen pen, Point point) { - return Geometry.StrokeContainsPoint(point.ToSharpDX(), (float)pen.Thickness); + return Geometry.StrokeContainsPoint(point.ToSharpDX(), (float)(pen?.Thickness ?? 0)); } public ITransformedGeometryImpl WithTransform(Matrix transform) diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WritableWicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs similarity index 86% rename from src/Windows/Avalonia.Direct2D1/Media/Imaging/WritableWicBitmapImpl.cs rename to src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs index 5dc07e06c4..fc931c32db 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WritableWicBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs @@ -9,9 +9,9 @@ using PixelFormat = Avalonia.Platform.PixelFormat; namespace Avalonia.Direct2D1.Media.Imaging { - class WritableWicBitmapImpl : WicBitmapImpl, IWritableBitmapImpl + class WriteableWicBitmapImpl : WicBitmapImpl, IWriteableBitmapImpl { - public WritableWicBitmapImpl(ImagingFactory factory, int width, int height, PixelFormat? pixelFormat) + public WriteableWicBitmapImpl(ImagingFactory factory, int width, int height, PixelFormat? pixelFormat) : base(factory, width, height, pixelFormat) { } diff --git a/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs b/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs index c76a5b5da5..124e33c5a3 100644 --- a/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs +++ b/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs @@ -103,7 +103,7 @@ namespace Avalonia.Direct2D1 /// Converts a pen to a Direct2D stroke style. /// /// The pen to convert. - /// The render target. + /// The render target. /// The Direct2D brush. public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.Pen pen, SharpDX.Direct2D1.RenderTarget renderTarget) { @@ -114,7 +114,7 @@ namespace Avalonia.Direct2D1 /// Converts a pen to a Direct2D stroke style. /// /// The pen to convert. - /// The render target. + /// The factory associated with this resource. /// The Direct2D brush. public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.Pen pen, Factory factory) { @@ -127,13 +127,16 @@ namespace Avalonia.Direct2D1 EndCap = pen.EndLineCap.ToDirect2D(), DashCap = pen.DashCap.ToDirect2D() }; - var dashes = new float[0]; + float[] dashes = null; if (pen.DashStyle?.Dashes != null && pen.DashStyle.Dashes.Count > 0) { properties.DashStyle = DashStyle.Custom; properties.DashOffset = (float)pen.DashStyle.Offset; - dashes = pen.DashStyle?.Dashes.Select(x => (float)x).ToArray(); + dashes = pen.DashStyle.Dashes.Select(x => (float)x).ToArray(); } + + dashes = dashes ?? Array.Empty(); + return new StrokeStyle(factory, properties, dashes); } diff --git a/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj b/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj index 368e5986b2..7b480ef328 100644 --- a/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj +++ b/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj @@ -34,6 +34,7 @@ + @@ -48,6 +49,9 @@ UnmanagedMethods.cs + + Component + diff --git a/src/Windows/Avalonia.Win32/Embedding/WinFormsAvaloniaControlHost.cs b/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs similarity index 99% rename from src/Windows/Avalonia.Win32/Embedding/WinFormsAvaloniaControlHost.cs rename to src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs index a484d6c0d2..fe626f4d38 100644 --- a/src/Windows/Avalonia.Win32/Embedding/WinFormsAvaloniaControlHost.cs +++ b/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs @@ -1,7 +1,6 @@ using System; using System.ComponentModel; using System.Windows.Forms; -using Avalonia.Controls; using Avalonia.Controls.Embedding; using Avalonia.Input; using Avalonia.VisualTree; diff --git a/src/Windows/Avalonia.Win32.NetStandard/Avalonia.Win32.NetStandard.csproj b/src/Windows/Avalonia.Win32.NetStandard/Avalonia.Win32.NetStandard.csproj deleted file mode 100644 index bae634b030..0000000000 --- a/src/Windows/Avalonia.Win32.NetStandard/Avalonia.Win32.NetStandard.csproj +++ /dev/null @@ -1,49 +0,0 @@ - - - netstandard2.0 - False - false - Avalonia.Win32 - Avalonia.Win32 - - - true - full - false - bin\Debug\ - TRACE;DEBUG;NETSTANDARD - prompt - 4 - true - - - pdbonly - true - bin\Release\ - TRACE;NETSTANDARD - prompt - 4 - true - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Windows/Avalonia.Win32.NetStandard/ComStreamWrapper.cs b/src/Windows/Avalonia.Win32.NetStandard/ComStreamWrapper.cs deleted file mode 100644 index 083021f58d..0000000000 --- a/src/Windows/Avalonia.Win32.NetStandard/ComStreamWrapper.cs +++ /dev/null @@ -1,197 +0,0 @@ -// -// System.Drawing.ComIStreamWrapper.cs -// -// Author: -// Kornél Pál -// -// Copyright (C) 2005-2008 Kornél Pál -// - -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System; -using System.IO; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Runtime.InteropServices.ComTypes; -using STATSTG = System.Runtime.InteropServices.ComTypes.STATSTG; - -namespace Avalonia.Win32 -{ - // Stream to IStream wrapper for COM interop - internal sealed class ComIStreamWrapper : IStream - { - private const int STG_E_INVALIDFUNCTION = unchecked((int)0x80030001); - - private readonly Stream baseStream; - private long position = -1; - - internal ComIStreamWrapper(Stream stream) - { - baseStream = stream; - } - - private void SetSizeToPosition() - { - if (position != -1) - { - if (position > baseStream.Length) - baseStream.SetLength(position); - baseStream.Position = position; - position = -1; - } - } - - public void Read(byte[] pv, int cb, IntPtr pcbRead) - { - int read = 0; - - if (cb != 0) - { - SetSizeToPosition(); - read = baseStream.Read(pv, 0, cb); - } - - if (pcbRead != IntPtr.Zero) - Marshal.WriteInt32(pcbRead, read); - } - - public void Write(byte[] pv, int cb, IntPtr pcbWritten) - { - if (cb != 0) - { - SetSizeToPosition(); - baseStream.Write(pv, 0, cb); - } - - if (pcbWritten != IntPtr.Zero) - Marshal.WriteInt32(pcbWritten, cb); - } - - public void Seek(long dlibMove, int dwOrigin, IntPtr plibNewPosition) - { - long length = baseStream.Length; - long newPosition; - - switch ((SeekOrigin)dwOrigin) - { - case SeekOrigin.Begin: - newPosition = dlibMove; - break; - case SeekOrigin.Current: - if (position == -1) - newPosition = baseStream.Position + dlibMove; - else - newPosition = position + dlibMove; - break; - case SeekOrigin.End: - newPosition = length + dlibMove; - break; - default: - throw new COMException(null, STG_E_INVALIDFUNCTION); - } - - if (newPosition > length) - position = newPosition; - else - { - baseStream.Position = newPosition; - position = -1; - } - - if (plibNewPosition != IntPtr.Zero) - Marshal.WriteInt64(plibNewPosition, newPosition); - } - - public void SetSize(long libNewSize) - { - baseStream.SetLength(libNewSize); - } - - public void CopyTo(IStream pstm, long cb, IntPtr pcbRead, IntPtr pcbWritten) - { - byte[] buffer; - long written = 0; - int read; - int count; - - if (cb != 0) - { - if (cb < 4096) - count = (int)cb; - else - count = 4096; - buffer = new byte[count]; - SetSizeToPosition(); - while (true) - { - if ((read = baseStream.Read(buffer, 0, count)) == 0) - break; - pstm.Write(buffer, read, IntPtr.Zero); - written += read; - if (written >= cb) - break; - if (cb - written < 4096) - count = (int)(cb - written); - } - } - - if (pcbRead != IntPtr.Zero) - Marshal.WriteInt64(pcbRead, written); - if (pcbWritten != IntPtr.Zero) - Marshal.WriteInt64(pcbWritten, written); - } - - public void Commit(int grfCommitFlags) - { - baseStream.Flush(); - SetSizeToPosition(); - } - - public void Revert() - { - throw new COMException(null, STG_E_INVALIDFUNCTION); - } - - public void LockRegion(long libOffset, long cb, int dwLockType) - { - throw new COMException(null, STG_E_INVALIDFUNCTION); - } - - public void UnlockRegion(long libOffset, long cb, int dwLockType) - { - throw new COMException(null, STG_E_INVALIDFUNCTION); - } - - public void Stat(out STATSTG pstatstg, int grfStatFlag) - { - pstatstg = new STATSTG(); - pstatstg.cbSize = baseStream.Length; - } - - public void Clone(out IStream ppstm) - { - ppstm = null; - throw new COMException(null, STG_E_INVALIDFUNCTION); - } - } -} \ No newline at end of file diff --git a/src/Windows/Avalonia.Win32.NetStandard/Gdip.cs b/src/Windows/Avalonia.Win32.NetStandard/Gdip.cs deleted file mode 100644 index b3d1c28689..0000000000 --- a/src/Windows/Avalonia.Win32.NetStandard/Gdip.cs +++ /dev/null @@ -1,128 +0,0 @@ -// -// Code copy-pasted from from Mono / System.Drawing.*.cs -// Original license below: -// -// Authors: -// Alexandre Pigolkine (pigolkine@gmx.de) -// Jordi Mas (jordi@ximian.com) -// Sanjay Gupta (gsanjay@novell.com) -// Ravindra (rkumar@novell.com) -// Peter Dennis Bartok (pbartok@novell.com) -// Sebastien Pouliot -// -// -// Copyright (C) 2004, 2007 Novell, Inc (http://www.novell.com) -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using System.Runtime.InteropServices.ComTypes; -using System.Text; -using System.Threading.Tasks; - -namespace Avalonia.Win32 -{ - static class Gdip - { - public enum Status - { - Ok = 0, - GenericError = 1, - InvalidParameter = 2, - OutOfMemory = 3, - ObjectBusy = 4, - InsufficientBuffer = 5, - NotImplemented = 6, - Win32Error = 7, - WrongState = 8, - Aborted = 9, - FileNotFound = 10, - ValueOverflow = 11, - AccessDenied = 12, - UnknownImageFormat = 13, - FontFamilyNotFound = 14, - FontStyleNotFound = 15, - NotTrueTypeFont = 16, - UnsupportedGdiplusVersion = 17, - GdiplusNotInitialized = 18, - PropertyNotFound = 19, - PropertyNotSupported = 20, - ProfileNotFound = 21 - } - - [StructLayout(LayoutKind.Sequential)] - internal struct GdiplusStartupInput - { - // internalted to silent compiler - internal uint GdiplusVersion; - internal IntPtr DebugEventCallback; - internal int SuppressBackgroundThread; - internal int SuppressExternalCodecs; - - internal static GdiplusStartupInput MakeGdiplusStartupInput() - { - GdiplusStartupInput result = new GdiplusStartupInput(); - result.GdiplusVersion = 1; - result.DebugEventCallback = IntPtr.Zero; - result.SuppressBackgroundThread = 0; - result.SuppressExternalCodecs = 0; - return result; - } - } - - [StructLayout(LayoutKind.Sequential)] - internal struct GdiplusStartupOutput - { - internal IntPtr NotificationHook; - internal IntPtr NotificationUnhook; - - internal static GdiplusStartupOutput MakeGdiplusStartupOutput() - { - GdiplusStartupOutput result = new GdiplusStartupOutput(); - result.NotificationHook = result.NotificationUnhook = IntPtr.Zero; - return result; - } - } - - - [DllImport("gdiplus.dll")] - public static extern Status GdiplusStartup(ref ulong token, ref GdiplusStartupInput input, ref GdiplusStartupOutput output); - - [DllImport("gdiplus.dll", ExactSpelling = true, CharSet = CharSet.Unicode)] - public static extern Status GdipLoadImageFromStream([MarshalAs(UnmanagedType.Interface, MarshalTypeRef = typeof(IStream))] IStream stream, out IntPtr image); - [DllImport("gdiplus.dll")] - public static extern Status GdipCreateHICONFromBitmap(IntPtr bmp, out IntPtr HandleIcon); - - [DllImport("gdiplus.dll")] - internal static extern Status GdipDisposeImage(IntPtr image); - - static Gdip() - { - ulong token = 0; - var input = GdiplusStartupInput.MakeGdiplusStartupInput(); - var output = GdiplusStartupOutput.MakeGdiplusStartupOutput(); - GdiplusStartup(ref token, ref input, ref output); - } - } -} diff --git a/src/Windows/Avalonia.Win32.NetStandard/IconImpl.cs b/src/Windows/Avalonia.Win32.NetStandard/IconImpl.cs deleted file mode 100644 index 49d039e655..0000000000 --- a/src/Windows/Avalonia.Win32.NetStandard/IconImpl.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Runtime.InteropServices.ComTypes; -using System.Text; -using System.Threading.Tasks; -using Avalonia.Platform; - -namespace Avalonia.Win32 -{ - public class IconImpl : IWindowIconImpl - { - private readonly MemoryStream _ms; - - public IconImpl(Stream data) - { - _ms = new MemoryStream(); - data.CopyTo(_ms); - _ms.Seek(0, SeekOrigin.Begin); - IntPtr bitmap; - var status = Gdip.GdipLoadImageFromStream(new ComIStreamWrapper(_ms), out bitmap); - if (status != Gdip.Status.Ok) - throw new Exception("Unable to load icon, gdip status: " + (int) status); - IntPtr icon; - status = Gdip.GdipCreateHICONFromBitmap(bitmap, out icon); - if (status != Gdip.Status.Ok) - throw new Exception("Unable to create HICON, gdip status: " + (int)status); - Gdip.GdipDisposeImage(bitmap); - HIcon = icon; - } - - public IntPtr HIcon { get;} - public void Save(Stream outputStream) - { - lock (_ms) - { - _ms.Seek(0, SeekOrigin.Begin); - _ms.CopyTo(outputStream); - } - } - - } -} diff --git a/src/Windows/Avalonia.Win32.NetStandard/NativeWin32Platform.cs b/src/Windows/Avalonia.Win32.NetStandard/NativeWin32Platform.cs deleted file mode 100644 index 2695a5b8b6..0000000000 --- a/src/Windows/Avalonia.Win32.NetStandard/NativeWin32Platform.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Avalonia.Platform; - -namespace Avalonia.Win32 -{ - partial class Win32Platform - { - //TODO: An actual implementation - public IWindowIconImpl LoadIcon(string fileName) - { - //No file IO for netstandard, still waiting for proper net core tooling - throw new NotSupportedException(); - } - - public IWindowIconImpl LoadIcon(Stream stream) => new IconImpl(stream); - - public IWindowIconImpl LoadIcon(IBitmapImpl bitmap) - { - var ms = new MemoryStream(); - bitmap.Save(ms); - ms.Seek(0, SeekOrigin.Begin); - return new IconImpl(ms); - } - } -} diff --git a/src/Windows/Avalonia.Win32.NetStandard/Win32Exception.cs b/src/Windows/Avalonia.Win32.NetStandard/Win32Exception.cs deleted file mode 100644 index 45926a881d..0000000000 --- a/src/Windows/Avalonia.Win32.NetStandard/Win32Exception.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Avalonia.Win32.NetStandard -{ - class AvaloniaWin32Exception : Exception - { - } -} diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.Shared.projitems b/src/Windows/Avalonia.Win32/Avalonia.Win32.Shared.projitems deleted file mode 100644 index cca6f5acf1..0000000000 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.Shared.projitems +++ /dev/null @@ -1,35 +0,0 @@ - - - - $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - true - 9defc6b7-845b-4d8f-afc0-d32bf0032b8c - - - Avalonia.Win32.Shared - - - - - - - - - - - - - - - - - - - - - - - Properties\SharedAssemblyInfo.cs - - - \ No newline at end of file diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj index 14f6211d68..5f26e4ad3e 100644 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj +++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj @@ -1,103 +1,17 @@ - - - + - Debug - AnyCPU - {811A76CF-1CF6-440F-963B-BBE31BD72A82} - Library - Properties - Avalonia.Win32 - Avalonia.Win32 - v4.6.1 - 512 - - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Debug\Avalonia.Win32.xml - true - CS1591 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - bin\Release\Avalonia.Win32.xml + netstandard2.0 true - CS1591 - true - - - - - - - - - - - - - - - - Component - - - - - - - {D211E587-D8BC-45B9-95A4-F297C8FA5200} - Avalonia.Animation - - - {B09B78D8-9B26-48B0-9149-D64A2F120F3F} - Avalonia.Base - - - {D2221C82-4A25-4583-9B43-D791E3F6820C} - Avalonia.Controls - - - {7062AE20-5DCC-4442-9645-8195BDECE63E} - Avalonia.Diagnostics - - - {62024B2D-53EB-4638-B26B-85EEAA54866E} - Avalonia.Input - - - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B} - Avalonia.Interactivity - - - {42472427-4774-4C81-8AFF-9F27B8E31721} - Avalonia.Layout - - - {EB582467-6ABB-43A1-B052-E981BA910E3A} - Avalonia.Visuals - - - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F} - Avalonia.Styling - + + + + + + + + - - - - \ No newline at end of file + + diff --git a/src/Windows/Avalonia.Win32/ClipboardFormats.cs b/src/Windows/Avalonia.Win32/ClipboardFormats.cs new file mode 100644 index 0000000000..5e0bbab975 --- /dev/null +++ b/src/Windows/Avalonia.Win32/ClipboardFormats.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using Avalonia.Input; +using Avalonia.Win32.Interop; + +namespace Avalonia.Win32 +{ + static class ClipboardFormats + { + private const int MAX_FORMAT_NAME_LENGTH = 260; + + class ClipboardFormat + { + public short Format { get; private set; } + public string Name { get; private set; } + public short[] Synthesized { get; private set; } + + public ClipboardFormat(string name, short format, params short[] synthesized) + { + Format = format; + Name = name; + Synthesized = synthesized; + } + } + + private static readonly List FormatList = new List() + { + new ClipboardFormat(DataFormats.Text, (short)UnmanagedMethods.ClipboardFormat.CF_UNICODETEXT, (short)UnmanagedMethods.ClipboardFormat.CF_TEXT), + new ClipboardFormat(DataFormats.FileNames, (short)UnmanagedMethods.ClipboardFormat.CF_HDROP), + }; + + + private static string QueryFormatName(short format) + { + StringBuilder sb = new StringBuilder(MAX_FORMAT_NAME_LENGTH); + if (UnmanagedMethods.GetClipboardFormatName(format, sb, sb.Capacity) > 0) + return sb.ToString(); + return null; + } + + public static string GetFormat(short format) + { + lock (FormatList) + { + var pd = FormatList.FirstOrDefault(f => f.Format == format || Array.IndexOf(f.Synthesized, format) >= 0); + if (pd == null) + { + string name = QueryFormatName(format); + if (string.IsNullOrEmpty(name)) + name = string.Format("Unknown_Format_{0}", format); + pd = new ClipboardFormat(name, format); + FormatList.Add(pd); + } + return pd.Name; + } + } + + public static short GetFormat(string format) + { + lock (FormatList) + { + var pd = FormatList.FirstOrDefault(f => StringComparer.OrdinalIgnoreCase.Equals(f.Name, format)); + if (pd == null) + { + int id = UnmanagedMethods.RegisterClipboardFormat(format); + if (id == 0) + throw new Win32Exception(); + pd = new ClipboardFormat(format, (short)id); + FormatList.Add(pd); + } + return pd.Format; + } + } + + + } +} \ No newline at end of file diff --git a/src/Windows/Avalonia.Win32/CursorFactory.cs b/src/Windows/Avalonia.Win32/CursorFactory.cs index 0d529d6b91..fa2fbe4810 100644 --- a/src/Windows/Avalonia.Win32/CursorFactory.cs +++ b/src/Windows/Avalonia.Win32/CursorFactory.cs @@ -9,6 +9,7 @@ using System.Text; using System.Threading.Tasks; using Avalonia.Input; using Avalonia.Platform; +using System.Runtime.InteropServices; namespace Avalonia.Win32 { @@ -20,6 +21,27 @@ namespace Avalonia.Win32 { } + static CursorFactory() + { + LoadModuleCursor(StandardCursorType.DragMove, "ole32.dll", 2); + LoadModuleCursor(StandardCursorType.DragCopy, "ole32.dll", 3); + LoadModuleCursor(StandardCursorType.DragLink, "ole32.dll", 4); + } + + private static void LoadModuleCursor(StandardCursorType cursorType, string module, int id) + { + IntPtr mh = UnmanagedMethods.GetModuleHandle(module); + if (mh != IntPtr.Zero) + { + IntPtr cursor = UnmanagedMethods.LoadCursor(mh, new IntPtr(id)); + if (cursor != IntPtr.Zero) + { + PlatformHandle phCursor = new PlatformHandle(cursor, PlatformConstants.CursorHandleType); + Cache.Add(cursorType, phCursor); + } + } + } + private static readonly Dictionary CursorTypeMapping = new Dictionary { @@ -47,6 +69,11 @@ namespace Avalonia.Win32 //Using SizeNorthEastSouthWest {StandardCursorType.TopRightCorner, 32643}, {StandardCursorType.BottomLeftCorner, 32643}, + + // Fallback, should have been loaded from ole32.dll + {StandardCursorType.DragMove, 32516}, + {StandardCursorType.DragCopy, 32516}, + {StandardCursorType.DragLink, 32516}, }; private static readonly Dictionary Cache = diff --git a/src/Windows/Avalonia.Win32/DataObject.cs b/src/Windows/Avalonia.Win32/DataObject.cs new file mode 100644 index 0000000000..13d5f662c2 --- /dev/null +++ b/src/Windows/Avalonia.Win32/DataObject.cs @@ -0,0 +1,360 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; +using System.Text; +using Avalonia.Input; +using Avalonia.Win32.Interop; +using IDataObject = Avalonia.Input.IDataObject; +using System.IO; +using System.Runtime.Serialization.Formatters.Binary; + +namespace Avalonia.Win32 +{ + class DataObject : IDataObject, IOleDataObject + { + // Compatibility with WinForms + WPF... + internal static readonly byte[] SerializedObjectGUID = new Guid("FD9EA796-3B13-4370-A679-56106BB288FB").ToByteArray(); + + class FormatEnumerator : IEnumFORMATETC + { + private FORMATETC[] _formats; + private int _current; + + private FormatEnumerator(FORMATETC[] formats, int current) + { + _formats = formats; + _current = current; + } + + public FormatEnumerator(IDataObject dataobj) + { + _formats = dataobj.GetDataFormats().Select(ConvertToFormatEtc).ToArray(); + _current = 0; + } + + private FORMATETC ConvertToFormatEtc(string aFormatName) + { + FORMATETC result = default(FORMATETC); + result.cfFormat = ClipboardFormats.GetFormat(aFormatName); + result.dwAspect = DVASPECT.DVASPECT_CONTENT; + result.ptd = IntPtr.Zero; + result.lindex = -1; + result.tymed = TYMED.TYMED_HGLOBAL; + return result; + } + + public void Clone(out IEnumFORMATETC newEnum) + { + newEnum = new FormatEnumerator(_formats, _current); + } + + public int Next(int celt, FORMATETC[] rgelt, int[] pceltFetched) + { + if (rgelt == null) + return unchecked((int)UnmanagedMethods.HRESULT.E_INVALIDARG); + + int i = 0; + while (i < celt && _current < _formats.Length) + { + rgelt[i] = _formats[_current]; + _current++; + i++; + } + if (pceltFetched != null) + pceltFetched[0] = i; + + if (i != celt) + return unchecked((int)UnmanagedMethods.HRESULT.S_FALSE); + return unchecked((int)UnmanagedMethods.HRESULT.S_OK); + } + + public int Reset() + { + _current = 0; + return unchecked((int)UnmanagedMethods.HRESULT.S_OK); + } + + public int Skip(int celt) + { + _current += Math.Min(celt, int.MaxValue - _current); + if (_current >= _formats.Length) + return unchecked((int)UnmanagedMethods.HRESULT.S_FALSE); + return unchecked((int)UnmanagedMethods.HRESULT.S_OK); + } + } + + private const int DV_E_TYMED = unchecked((int)0x80040069); + private const int DV_E_DVASPECT = unchecked((int)0x8004006B); + private const int DV_E_FORMATETC = unchecked((int)0x80040064); + private const int OLE_E_ADVISENOTSUPPORTED = unchecked((int)0x80040003); + private const int STG_E_MEDIUMFULL = unchecked((int)0x80030070); + private const int GMEM_ZEROINIT = 0x0040; + private const int GMEM_MOVEABLE = 0x0002; + + + IDataObject _wrapped; + + public DataObject(IDataObject wrapped) + { + _wrapped = wrapped; + } + + #region IDataObject + bool IDataObject.Contains(string dataFormat) + { + return _wrapped.Contains(dataFormat); + } + + IEnumerable IDataObject.GetDataFormats() + { + return _wrapped.GetDataFormats(); + } + + IEnumerable IDataObject.GetFileNames() + { + return _wrapped.GetFileNames(); + } + + string IDataObject.GetText() + { + return _wrapped.GetText(); + } + + object IDataObject.Get(string dataFormat) + { + return _wrapped.Get(dataFormat); + } + #endregion + + #region IOleDataObject + + int IOleDataObject.DAdvise(ref FORMATETC pFormatetc, ADVF advf, IAdviseSink adviseSink, out int connection) + { + if (_wrapped is IOleDataObject ole) + return ole.DAdvise(ref pFormatetc, advf, adviseSink, out connection); + connection = 0; + return OLE_E_ADVISENOTSUPPORTED; + } + + void IOleDataObject.DUnadvise(int connection) + { + if (_wrapped is IOleDataObject ole) + ole.DUnadvise(connection); + Marshal.ThrowExceptionForHR(OLE_E_ADVISENOTSUPPORTED); + } + + int IOleDataObject.EnumDAdvise(out IEnumSTATDATA enumAdvise) + { + if (_wrapped is IOleDataObject ole) + return ole.EnumDAdvise(out enumAdvise); + + enumAdvise = null; + return OLE_E_ADVISENOTSUPPORTED; + } + + IEnumFORMATETC IOleDataObject.EnumFormatEtc(DATADIR direction) + { + if (_wrapped is IOleDataObject ole) + return ole.EnumFormatEtc(direction); + if (direction == DATADIR.DATADIR_GET) + return new FormatEnumerator(_wrapped); + throw new NotSupportedException(); + } + + int IOleDataObject.GetCanonicalFormatEtc(ref FORMATETC formatIn, out FORMATETC formatOut) + { + if (_wrapped is IOleDataObject ole) + return ole.GetCanonicalFormatEtc(ref formatIn, out formatOut); + + formatOut = new FORMATETC(); + formatOut.ptd = IntPtr.Zero; + return unchecked((int)UnmanagedMethods.HRESULT.E_NOTIMPL); + } + + void IOleDataObject.GetData(ref FORMATETC format, out STGMEDIUM medium) + { + if (_wrapped is IOleDataObject ole) + { + ole.GetData(ref format, out medium); + return; + } + if(!format.tymed.HasFlag(TYMED.TYMED_HGLOBAL)) + Marshal.ThrowExceptionForHR(DV_E_TYMED); + + if (format.dwAspect != DVASPECT.DVASPECT_CONTENT) + Marshal.ThrowExceptionForHR(DV_E_DVASPECT); + + string fmt = ClipboardFormats.GetFormat(format.cfFormat); + if (string.IsNullOrEmpty(fmt) || !_wrapped.Contains(fmt)) + Marshal.ThrowExceptionForHR(DV_E_FORMATETC); + + medium = default(STGMEDIUM); + medium.tymed = TYMED.TYMED_HGLOBAL; + int result = WriteDataToHGlobal(fmt, ref medium.unionmember); + Marshal.ThrowExceptionForHR(result); + } + + void IOleDataObject.GetDataHere(ref FORMATETC format, ref STGMEDIUM medium) + { + if (_wrapped is IOleDataObject ole) + { + ole.GetDataHere(ref format, ref medium); + return; + } + + if (medium.tymed != TYMED.TYMED_HGLOBAL || !format.tymed.HasFlag(TYMED.TYMED_HGLOBAL)) + Marshal.ThrowExceptionForHR(DV_E_TYMED); + + if (format.dwAspect != DVASPECT.DVASPECT_CONTENT) + Marshal.ThrowExceptionForHR(DV_E_DVASPECT); + + string fmt = ClipboardFormats.GetFormat(format.cfFormat); + if (string.IsNullOrEmpty(fmt) || !_wrapped.Contains(fmt)) + Marshal.ThrowExceptionForHR(DV_E_FORMATETC); + + if (medium.unionmember == IntPtr.Zero) + Marshal.ThrowExceptionForHR(STG_E_MEDIUMFULL); + + int result = WriteDataToHGlobal(fmt, ref medium.unionmember); + Marshal.ThrowExceptionForHR(result); + } + + int IOleDataObject.QueryGetData(ref FORMATETC format) + { + if (_wrapped is IOleDataObject ole) + return ole.QueryGetData(ref format); + if (format.dwAspect != DVASPECT.DVASPECT_CONTENT) + return DV_E_DVASPECT; + if (!format.tymed.HasFlag(TYMED.TYMED_HGLOBAL)) + return DV_E_TYMED; + + string dataFormat = ClipboardFormats.GetFormat(format.cfFormat); + if (!string.IsNullOrEmpty(dataFormat) && _wrapped.Contains(dataFormat)) + return unchecked((int)UnmanagedMethods.HRESULT.S_OK); + return DV_E_FORMATETC; + } + + void IOleDataObject.SetData(ref FORMATETC formatIn, ref STGMEDIUM medium, bool release) + { + if (_wrapped is IOleDataObject ole) + { + ole.SetData(ref formatIn, ref medium, release); + return; + } + Marshal.ThrowExceptionForHR(unchecked((int)UnmanagedMethods.HRESULT.E_NOTIMPL)); + } + + private int WriteDataToHGlobal(string dataFormat, ref IntPtr hGlobal) + { + object data = _wrapped.Get(dataFormat); + if (dataFormat == DataFormats.Text || data is string) + return WriteStringToHGlobal(ref hGlobal, Convert.ToString(data)); + if (dataFormat == DataFormats.FileNames && data is IEnumerable files) + return WriteFileListToHGlobal(ref hGlobal, files); + if (data is Stream stream) + { + byte[] buffer = new byte[stream.Length - stream.Position]; + stream.Read(buffer, 0, buffer.Length); + return WriteBytesToHGlobal(ref hGlobal, buffer); + } + if (data is IEnumerable bytes) + { + var byteArr = bytes is byte[] ? (byte[])bytes : bytes.ToArray(); + return WriteBytesToHGlobal(ref hGlobal, byteArr); + } + return WriteBytesToHGlobal(ref hGlobal, SerializeObject(data)); + } + + private byte[] SerializeObject(object data) + { + using (var ms = new MemoryStream()) + { + ms.Write(SerializedObjectGUID, 0, SerializedObjectGUID.Length); + BinaryFormatter binaryFormatter = new BinaryFormatter(); + binaryFormatter.Serialize(ms, data); + return ms.ToArray(); + } + } + + private int WriteBytesToHGlobal(ref IntPtr hGlobal, byte[] data) + { + int required = data.Length; + if (hGlobal == IntPtr.Zero) + hGlobal = UnmanagedMethods.GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, required); + + long available = UnmanagedMethods.GlobalSize(hGlobal).ToInt64(); + if (required > available) + return STG_E_MEDIUMFULL; + + IntPtr ptr = UnmanagedMethods.GlobalLock(hGlobal); + try + { + Marshal.Copy(data, 0, ptr, data.Length); + return unchecked((int)UnmanagedMethods.HRESULT.S_OK); + } + finally + { + UnmanagedMethods.GlobalUnlock(hGlobal); + } + } + + private int WriteFileListToHGlobal(ref IntPtr hGlobal, IEnumerable files) + { + if (!files?.Any() ?? false) + return unchecked((int)UnmanagedMethods.HRESULT.S_OK); + + char[] filesStr = (string.Join("\0", files) + "\0\0").ToCharArray(); + _DROPFILES df = new _DROPFILES(); + df.pFiles = Marshal.SizeOf<_DROPFILES>(); + df.fWide = true; + + int required = (filesStr.Length * sizeof(char)) + Marshal.SizeOf<_DROPFILES>(); + if (hGlobal == IntPtr.Zero) + hGlobal = UnmanagedMethods.GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, required); + + long available = UnmanagedMethods.GlobalSize(hGlobal).ToInt64(); + if (required > available) + return STG_E_MEDIUMFULL; + + IntPtr ptr = UnmanagedMethods.GlobalLock(hGlobal); + try + { + Marshal.StructureToPtr(df, ptr, false); + + Marshal.Copy(filesStr, 0, ptr + Marshal.SizeOf<_DROPFILES>(), filesStr.Length); + return unchecked((int)UnmanagedMethods.HRESULT.S_OK); + } + finally + { + UnmanagedMethods.GlobalUnlock(hGlobal); + } + } + + private int WriteStringToHGlobal(ref IntPtr hGlobal, string data) + { + int required = (data.Length + 1) * sizeof(char); + if (hGlobal == IntPtr.Zero) + hGlobal = UnmanagedMethods.GlobalAlloc(GMEM_MOVEABLE|GMEM_ZEROINIT, required); + + long available = UnmanagedMethods.GlobalSize(hGlobal).ToInt64(); + if (required > available) + return STG_E_MEDIUMFULL; + + IntPtr ptr = UnmanagedMethods.GlobalLock(hGlobal); + try + { + char[] chars = (data + '\0').ToCharArray(); + Marshal.Copy(chars, 0, ptr, chars.Length); + return unchecked((int)UnmanagedMethods.HRESULT.S_OK); + } + finally + { + UnmanagedMethods.GlobalUnlock(hGlobal); + } + } + + #endregion + } +} diff --git a/src/Windows/Avalonia.Win32/DragSource.cs b/src/Windows/Avalonia.Win32/DragSource.cs new file mode 100644 index 0000000000..ea124e5f29 --- /dev/null +++ b/src/Windows/Avalonia.Win32/DragSource.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Input; +using Avalonia.Input.Platform; +using Avalonia.Threading; +using Avalonia.Win32.Interop; + +namespace Avalonia.Win32 +{ + class DragSource : IPlatformDragSource + { + public Task DoDragDrop(IDataObject data, DragDropEffects allowedEffects) + { + Dispatcher.UIThread.VerifyAccess(); + + OleDragSource src = new OleDragSource(); + DataObject dataObject = new DataObject(data); + int allowed = (int)OleDropTarget.ConvertDropEffect(allowedEffects); + + int[] finalEffect = new int[1]; + UnmanagedMethods.DoDragDrop(dataObject, src, allowed, finalEffect); + + return Task.FromResult(OleDropTarget.ConvertDropEffect((DropEffect)finalEffect[0]));} + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index a0518cf92e..2516abb56c 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; using System.Text; // ReSharper disable InconsistentNaming @@ -557,7 +558,18 @@ namespace Avalonia.Win32.Interop { DIB_RGB_COLORS = 0, /* color table in RGBs */ DIB_PAL_COLORS /* color table in palette indices */ - }; + } + + public enum WindowLongParam + { + GWL_WNDPROC = -4, + GWL_HINSTANCE = -6, + GWL_HWNDPARENT = -8, + GWL_ID = -12, + GWL_STYLE = -16, + GWL_EXSTYLE = -20, + GWL_USERDATA = -21 + } [StructLayout(LayoutKind.Sequential)] public struct RGBQUAD @@ -614,6 +626,16 @@ namespace Avalonia.Win32.Interop public uint[] cols; } + [StructLayout(LayoutKind.Sequential)] + public struct MINMAXINFO + { + public POINT ptReserved; + public POINT ptMaxSize; + public POINT ptMaxPosition; + public POINT ptMinTrackSize; + public POINT ptMaxTrackSize; + } + public const int SizeOf_BITMAPINFOHEADER = 40; [DllImport("user32.dll")] @@ -951,6 +973,32 @@ namespace Avalonia.Win32.Interop [DllImport("msvcrt.dll", EntryPoint="memcpy", SetLastError = false, CallingConvention=CallingConvention.Cdecl)] public static extern IntPtr CopyMemory(IntPtr dest, IntPtr src, UIntPtr count); + [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true)] + public static extern HRESULT RegisterDragDrop(IntPtr hwnd, IDropTarget target); + + [DllImport("ole32.dll", EntryPoint = "OleInitialize")] + public static extern HRESULT OleInitialize(IntPtr val); + + [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true)] + internal static extern void ReleaseStgMedium(ref STGMEDIUM medium); + + [DllImport("user32.dll", BestFitMapping = false, CharSet = CharSet.Auto, SetLastError = true)] + public static extern int GetClipboardFormatName(int format, StringBuilder lpString, int cchMax); + + [DllImport("user32.dll", BestFitMapping = false, CharSet = CharSet.Auto, SetLastError = true)] + public static extern int RegisterClipboardFormat(string format); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto, ExactSpelling = true, SetLastError = true)] + public static extern IntPtr GlobalSize(IntPtr hGlobal); + + [DllImport("shell32.dll", BestFitMapping = false, CharSet = CharSet.Auto)] + public static extern int DragQueryFile(IntPtr hDrop, int iFile, StringBuilder lpszFile, int cch); + + [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true, PreserveSig = false)] + public static extern void DoDragDrop(IOleDataObject dataObject, IDropSource dropSource, int allowedEffects, int[] finalEffect); + + + public enum MONITOR { MONITOR_DEFAULTTONULL = 0x00000000, @@ -990,10 +1038,28 @@ namespace Avalonia.Win32.Interop MDT_DEFAULT = MDT_EFFECTIVE_DPI } - public enum ClipboardFormat + public enum ClipboardFormat { + /// + /// Text format. Each line ends with a carriage return/linefeed (CR-LF) combination. A null character signals the end of the data. Use this format for ANSI text. + /// CF_TEXT = 1, - CF_UNICODETEXT = 13 + /// + /// A handle to a bitmap + /// + CF_BITMAP = 2, + /// + /// A memory object containing a BITMAPINFO structure followed by the bitmap bits. + /// + CF_DIB = 3, + /// + /// Unicode text format. Each line ends with a carriage return/linefeed (CR-LF) combination. A null character signals the end of the data. + /// + CF_UNICODETEXT = 13, + /// + /// A handle to type HDROP that identifies a list of files. + /// + CF_HDROP = 15, } public struct MSG @@ -1098,7 +1164,7 @@ namespace Avalonia.Win32.Interop } } - [StructLayout(LayoutKind.Sequential)] + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct WNDCLASSEX { public int cbSize; @@ -1136,7 +1202,9 @@ namespace Avalonia.Win32.Interop S_FALSE = 0x0001, S_OK = 0x0000, E_INVALIDARG = 0x80070057, - E_OUTOFMEMORY = 0x8007000E + E_OUTOFMEMORY = 0x8007000E, + E_NOTIMPL = 0x80004001, + E_UNEXPECTED = 0x8000FFFF, } public enum Icons @@ -1300,4 +1368,74 @@ namespace Avalonia.Win32.Interop uint Compare([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, [In] uint hint, out int piOrder); } + + [Flags] + internal enum DropEffect : int + { + None = 0, + Copy = 1, + Move = 2, + Link = 4, + Scroll = -2147483648, + } + + + + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("00000122-0000-0000-C000-000000000046")] + internal interface IDropTarget + { + [PreserveSig] + UnmanagedMethods.HRESULT DragEnter([MarshalAs(UnmanagedType.Interface)] [In] IOleDataObject pDataObj, [MarshalAs(UnmanagedType.U4)] [In] int grfKeyState, [MarshalAs(UnmanagedType.U8)] [In] long pt, [In] [Out] ref DropEffect pdwEffect); + [PreserveSig] + UnmanagedMethods.HRESULT DragOver([MarshalAs(UnmanagedType.U4)] [In] int grfKeyState, [MarshalAs(UnmanagedType.U8)] [In] long pt, [In] [Out] ref DropEffect pdwEffect); + [PreserveSig] + UnmanagedMethods.HRESULT DragLeave(); + [PreserveSig] + UnmanagedMethods.HRESULT Drop([MarshalAs(UnmanagedType.Interface)] [In] IOleDataObject pDataObj, [MarshalAs(UnmanagedType.U4)] [In] int grfKeyState, [MarshalAs(UnmanagedType.U8)] [In] long pt, [In] [Out] ref DropEffect pdwEffect); + } + + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("00000121-0000-0000-C000-000000000046")] + internal interface IDropSource + { + [PreserveSig] + int QueryContinueDrag(int fEscapePressed, [MarshalAs(UnmanagedType.U4)] [In] int grfKeyState); + [PreserveSig] + int GiveFeedback([MarshalAs(UnmanagedType.U4)] [In] int dwEffect); + } + + + [ComImport] + [Guid("0000010E-0000-0000-C000-000000000046")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IOleDataObject + { + void GetData([In] ref FORMATETC format, out STGMEDIUM medium); + void GetDataHere([In] ref FORMATETC format, ref STGMEDIUM medium); + [PreserveSig] + int QueryGetData([In] ref FORMATETC format); + [PreserveSig] + int GetCanonicalFormatEtc([In] ref FORMATETC formatIn, out FORMATETC formatOut); + void SetData([In] ref FORMATETC formatIn, [In] ref STGMEDIUM medium, [MarshalAs(UnmanagedType.Bool)] bool release); + IEnumFORMATETC EnumFormatEtc(DATADIR direction); + [PreserveSig] + int DAdvise([In] ref FORMATETC pFormatetc, ADVF advf, IAdviseSink adviseSink, out int connection); + void DUnadvise(int connection); + [PreserveSig] + int EnumDAdvise(out IEnumSTATDATA enumAdvise); + } + + + [StructLayoutAttribute(LayoutKind.Sequential)] + internal struct _DROPFILES + { + public Int32 pFiles; + public Int32 X; + public Int32 Y; + public bool fNC; + public bool fWide; + } } diff --git a/src/Windows/Avalonia.Win32/OleContext.cs b/src/Windows/Avalonia.Win32/OleContext.cs new file mode 100644 index 0000000000..085c0f8ea9 --- /dev/null +++ b/src/Windows/Avalonia.Win32/OleContext.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading; +using Avalonia.Platform; +using Avalonia.Threading; +using Avalonia.Win32.Interop; + +namespace Avalonia.Win32 +{ + class OleContext + { + private static OleContext fCurrent; + + internal static OleContext Current + { + get + { + if (!IsValidOleThread()) + return null; + + if (fCurrent == null) + fCurrent = new OleContext(); + return fCurrent; + } + } + + + private OleContext() + { + if (UnmanagedMethods.OleInitialize(IntPtr.Zero) != UnmanagedMethods.HRESULT.S_OK) + throw new SystemException("Failed to initialize OLE"); + } + + private static bool IsValidOleThread() + { + return Dispatcher.UIThread.CheckAccess() && + Thread.CurrentThread.GetApartmentState() == ApartmentState.STA; + } + + internal bool RegisterDragDrop(IPlatformHandle hwnd, IDropTarget target) + { + if (hwnd?.HandleDescriptor != "HWND" || target == null) + return false; + + return UnmanagedMethods.RegisterDragDrop(hwnd.Handle, target) == UnmanagedMethods.HRESULT.S_OK; + } + } +} diff --git a/src/Windows/Avalonia.Win32/OleDataObject.cs b/src/Windows/Avalonia.Win32/OleDataObject.cs new file mode 100644 index 0000000000..d7b663e7bf --- /dev/null +++ b/src/Windows/Avalonia.Win32/OleDataObject.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; +using System.Runtime.Serialization.Formatters.Binary; +using System.Text; +using Avalonia.Input; +using Avalonia.Win32.Interop; + +namespace Avalonia.Win32 +{ + class OleDataObject : Avalonia.Input.IDataObject + { + private IOleDataObject _wrapped; + + public OleDataObject(IOleDataObject wrapped) + { + _wrapped = wrapped; + } + + public bool Contains(string dataFormat) + { + return GetDataFormatsCore().Any(df => StringComparer.OrdinalIgnoreCase.Equals(df, dataFormat)); + } + + public IEnumerable GetDataFormats() + { + return GetDataFormatsCore().Distinct(); + } + + public string GetText() + { + return GetDataFromOleHGLOBAL(DataFormats.Text, DVASPECT.DVASPECT_CONTENT) as string; + } + + public IEnumerable GetFileNames() + { + return GetDataFromOleHGLOBAL(DataFormats.FileNames, DVASPECT.DVASPECT_CONTENT) as IEnumerable; + } + + public object Get(string dataFormat) + { + return GetDataFromOleHGLOBAL(dataFormat, DVASPECT.DVASPECT_CONTENT); + } + + private object GetDataFromOleHGLOBAL(string format, DVASPECT aspect) + { + FORMATETC formatEtc = new FORMATETC(); + formatEtc.cfFormat = ClipboardFormats.GetFormat(format); + formatEtc.dwAspect = aspect; + formatEtc.lindex = -1; + formatEtc.tymed = TYMED.TYMED_HGLOBAL; + if (_wrapped.QueryGetData(ref formatEtc) == 0) + { + _wrapped.GetData(ref formatEtc, out STGMEDIUM medium); + try + { + if (medium.unionmember != IntPtr.Zero && medium.tymed == TYMED.TYMED_HGLOBAL) + { + if (format == DataFormats.Text) + return ReadStringFromHGlobal(medium.unionmember); + if (format == DataFormats.FileNames) + return ReadFileNamesFromHGlobal(medium.unionmember); + + byte[] data = ReadBytesFromHGlobal(medium.unionmember); + + if (IsSerializedObject(data)) + { + using (var ms = new MemoryStream(data)) + { + ms.Position = DataObject.SerializedObjectGUID.Length; + BinaryFormatter binaryFormatter = new BinaryFormatter(); + return binaryFormatter.Deserialize(ms); + } + } + return data; + } + } + finally + { + UnmanagedMethods.ReleaseStgMedium(ref medium); + } + } + return null; + } + + private bool IsSerializedObject(byte[] data) + { + if (data.Length < DataObject.SerializedObjectGUID.Length) + return false; + for (int i = 0; i < DataObject.SerializedObjectGUID.Length; i++) + if (data[i] != DataObject.SerializedObjectGUID[i]) + return false; + return true; + } + + private static IEnumerable ReadFileNamesFromHGlobal(IntPtr hGlobal) + { + List files = new List(); + int fileCount = UnmanagedMethods.DragQueryFile(hGlobal, -1, null, 0); + if (fileCount > 0) + { + for (int i = 0; i < fileCount; i++) + { + int pathLen = UnmanagedMethods.DragQueryFile(hGlobal, i, null, 0); + StringBuilder sb = new StringBuilder(pathLen+1); + + if (UnmanagedMethods.DragQueryFile(hGlobal, i, sb, sb.Capacity) == pathLen) + { + files.Add(sb.ToString()); + } + } + } + return files; + } + + private static string ReadStringFromHGlobal(IntPtr hGlobal) + { + IntPtr ptr = UnmanagedMethods.GlobalLock(hGlobal); + try + { + return Marshal.PtrToStringAuto(ptr); + } + finally + { + UnmanagedMethods.GlobalUnlock(hGlobal); + } + } + + private static byte[] ReadBytesFromHGlobal(IntPtr hGlobal) + { + IntPtr source = UnmanagedMethods.GlobalLock(hGlobal); + try + { + int size = (int)UnmanagedMethods.GlobalSize(hGlobal).ToInt64(); + byte[] data = new byte[size]; + Marshal.Copy(source, data, 0, size); + return data; + } + finally + { + UnmanagedMethods.GlobalUnlock(hGlobal); + } + } + + private IEnumerable GetDataFormatsCore() + { + var enumFormat = _wrapped.EnumFormatEtc(DATADIR.DATADIR_GET); + if (enumFormat != null) + { + enumFormat.Reset(); + FORMATETC[] formats = new FORMATETC[1]; + int[] fetched = { 1 }; + while (fetched[0] > 0) + { + fetched[0] = 0; + if (enumFormat.Next(1, formats, fetched) == 0 && fetched[0] > 0) + { + if (formats[0].ptd != IntPtr.Zero) + Marshal.FreeCoTaskMem(formats[0].ptd); + + yield return ClipboardFormats.GetFormat(formats[0].cfFormat); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Windows/Avalonia.Win32/OleDragSource.cs b/src/Windows/Avalonia.Win32/OleDragSource.cs new file mode 100644 index 0000000000..522014abc0 --- /dev/null +++ b/src/Windows/Avalonia.Win32/OleDragSource.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Win32.Interop; + +namespace Avalonia.Win32 +{ + class OleDragSource : IDropSource + { + private const int DRAGDROP_S_USEDEFAULTCURSORS = 0x00040102; + private const int DRAGDROP_S_DROP = 0x00040100; + private const int DRAGDROP_S_CANCEL = 0x00040101; + + private const int KEYSTATE_LEFTMB = 1; + private const int KEYSTATE_MIDDLEMB = 16; + private const int KEYSTATE_RIGHTMB = 2; + private static readonly int[] MOUSE_BUTTONS = new int[] { KEYSTATE_LEFTMB, KEYSTATE_MIDDLEMB, KEYSTATE_RIGHTMB }; + + public int QueryContinueDrag(int fEscapePressed, int grfKeyState) + { + if (fEscapePressed != 0) + return DRAGDROP_S_CANCEL; + + int pressedMouseButtons = MOUSE_BUTTONS.Where(mb => (grfKeyState & mb) == mb).Count(); + + if (pressedMouseButtons >= 2) + return DRAGDROP_S_CANCEL; + if (pressedMouseButtons == 0) + return DRAGDROP_S_DROP; + + return unchecked((int)UnmanagedMethods.HRESULT.S_OK); + } + + public int GiveFeedback(int dwEffect) + { + return DRAGDROP_S_USEDEFAULTCURSORS; + } + } +} diff --git a/src/Windows/Avalonia.Win32/OleDropTarget.cs b/src/Windows/Avalonia.Win32/OleDropTarget.cs new file mode 100644 index 0000000000..973564a3d1 --- /dev/null +++ b/src/Windows/Avalonia.Win32/OleDropTarget.cs @@ -0,0 +1,159 @@ +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Platform; +using Avalonia.Win32.Interop; +using IDataObject = Avalonia.Input.IDataObject; + +namespace Avalonia.Win32 +{ + class OleDropTarget : IDropTarget + { + private readonly IInputElement _target; + private readonly ITopLevelImpl _tl; + private readonly IDragDropDevice _dragDevice; + + private IDataObject _currentDrag = null; + + public OleDropTarget(ITopLevelImpl tl, IInputElement target) + { + _dragDevice = AvaloniaLocator.Current.GetService(); + _tl = tl; + _target = target; + } + + public static DropEffect ConvertDropEffect(DragDropEffects operation) + { + DropEffect result = DropEffect.None; + if (operation.HasFlag(DragDropEffects.Copy)) + result |= DropEffect.Copy; + if (operation.HasFlag(DragDropEffects.Move)) + result |= DropEffect.Move; + if (operation.HasFlag(DragDropEffects.Link)) + result |= DropEffect.Link; + return result; + } + + public static DragDropEffects ConvertDropEffect(DropEffect effect) + { + DragDropEffects result = DragDropEffects.None; + if (effect.HasFlag(DropEffect.Copy)) + result |= DragDropEffects.Copy; + if (effect.HasFlag(DropEffect.Move)) + result |= DragDropEffects.Move; + if (effect.HasFlag(DropEffect.Link)) + result |= DragDropEffects.Link; + return result; + } + + UnmanagedMethods.HRESULT IDropTarget.DragEnter(IOleDataObject pDataObj, int grfKeyState, long pt, ref DropEffect pdwEffect) + { + var dispatch = _tl?.Input; + if (dispatch == null) + { + pdwEffect = DropEffect.None; + return UnmanagedMethods.HRESULT.S_OK; + } + _currentDrag = pDataObj as IDataObject; + if (_currentDrag == null) + _currentDrag = new OleDataObject(pDataObj); + var args = new RawDragEvent( + _dragDevice, + RawDragEventType.DragEnter, + _target, + GetDragLocation(pt), + _currentDrag, + ConvertDropEffect(pdwEffect) + ); + dispatch(args); + pdwEffect = ConvertDropEffect(args.Effects); + + return UnmanagedMethods.HRESULT.S_OK; + } + + UnmanagedMethods.HRESULT IDropTarget.DragOver(int grfKeyState, long pt, ref DropEffect pdwEffect) + { + var dispatch = _tl?.Input; + if (dispatch == null) + { + pdwEffect = DropEffect.None; + return UnmanagedMethods.HRESULT.S_OK; + } + + var args = new RawDragEvent( + _dragDevice, + RawDragEventType.DragOver, + _target, + GetDragLocation(pt), + _currentDrag, + ConvertDropEffect(pdwEffect) + ); + dispatch(args); + pdwEffect = ConvertDropEffect(args.Effects); + + return UnmanagedMethods.HRESULT.S_OK; + } + + UnmanagedMethods.HRESULT IDropTarget.DragLeave() + { + try + { + _tl?.Input(new RawDragEvent( + _dragDevice, + RawDragEventType.DragLeave, + _target, + default(Point), + null, + DragDropEffects.None + )); + return UnmanagedMethods.HRESULT.S_OK; + } + finally + { + _currentDrag = null; + } + } + + UnmanagedMethods.HRESULT IDropTarget.Drop(IOleDataObject pDataObj, int grfKeyState, long pt, ref DropEffect pdwEffect) + { + try + { + var dispatch = _tl?.Input; + if (dispatch == null) + { + pdwEffect = DropEffect.None; + return UnmanagedMethods.HRESULT.S_OK; + } + + _currentDrag = pDataObj as IDataObject; + if (_currentDrag == null) + _currentDrag= new OleDataObject(pDataObj); + + var args = new RawDragEvent( + _dragDevice, + RawDragEventType.Drop, + _target, + GetDragLocation(pt), + _currentDrag, + ConvertDropEffect(pdwEffect) + ); + dispatch(args); + pdwEffect = ConvertDropEffect(args.Effects); + + return UnmanagedMethods.HRESULT.S_OK; + } + finally + { + _currentDrag = null; + } + } + + private Point GetDragLocation(long dragPoint) + { + int x = (int)dragPoint; + int y = (int)(dragPoint >> 32); + + Point screenPt = new Point(x, y); + return _target.PointToClient(screenPt); + } + } +} \ No newline at end of file diff --git a/src/Windows/Avalonia.Win32/Properties/AssemblyInfo.cs b/src/Windows/Avalonia.Win32/Properties/AssemblyInfo.cs index 5b4d2cef23..38ec891f76 100644 --- a/src/Windows/Avalonia.Win32/Properties/AssemblyInfo.cs +++ b/src/Windows/Avalonia.Win32/Properties/AssemblyInfo.cs @@ -3,7 +3,5 @@ using Avalonia.Platform; using Avalonia.Win32; -using System.Reflection; -[assembly: AssemblyTitle("Avalonia.Win32")] [assembly: ExportWindowingSubsystem(OperatingSystemType.WinNT, 1, "Win32", typeof(Win32Platform), nameof(Win32Platform.Initialize))] diff --git a/src/Windows/Avalonia.Win32/ScreenImpl.cs b/src/Windows/Avalonia.Win32/ScreenImpl.cs index 4f4331e461..e1df24151d 100644 --- a/src/Windows/Avalonia.Win32/ScreenImpl.cs +++ b/src/Windows/Avalonia.Win32/ScreenImpl.cs @@ -2,16 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Linq; -using Avalonia.Controls; using Avalonia.Platform; -using Avalonia.Utilities; using static Avalonia.Win32.Interop.UnmanagedMethods; -#if NETSTANDARD -using Win32Exception = Avalonia.Win32.NetStandard.AvaloniaWin32Exception; -#endif - namespace Avalonia.Win32 { public class ScreenImpl : IScreenImpl @@ -41,8 +34,8 @@ namespace Avalonia.Win32 Rect avaloniaBounds = new Rect(bounds.left, bounds.top, bounds.right - bounds.left, bounds.bottom - bounds.top); Rect avaloniaWorkArea = - new Rect(workingArea.left, workingArea.top, workingArea.right - bounds.left, - workingArea.bottom - bounds.top); + new Rect(workingArea.left, workingArea.top, workingArea.right - workingArea.left, + workingArea.bottom - workingArea.top); screens[index] = new WinScreen(avaloniaBounds, avaloniaWorkArea, monitorInfo.dwFlags == 1, monitor); diff --git a/src/Windows/Avalonia.Win32/Settings.StyleCop b/src/Windows/Avalonia.Win32/Settings.StyleCop deleted file mode 100644 index 5bd6cda777..0000000000 --- a/src/Windows/Avalonia.Win32/Settings.StyleCop +++ /dev/null @@ -1,184 +0,0 @@ - - - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - - - \ No newline at end of file diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index a490e59cb7..a5088e794c 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -1,26 +1,23 @@ // 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 Avalonia.Input.Platform; using System; using System.Collections.Generic; -using System.Reactive.Disposables; +using System.ComponentModel; using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Reactive.Disposables; using System.Runtime.InteropServices; using System.Threading; +using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Input; +using Avalonia.Input.Platform; using Avalonia.Platform; -using Avalonia.Win32.Input; -using Avalonia.Win32.Interop; -using Avalonia.Controls; using Avalonia.Rendering; using Avalonia.Threading; -#if NETSTANDARD -using Win32Exception = Avalonia.Win32.NetStandard.AvaloniaWin32Exception; -#else -using System.ComponentModel; -#endif +using Avalonia.Win32.Input; +using Avalonia.Win32.Interop; namespace Avalonia { @@ -40,7 +37,7 @@ namespace Avalonia namespace Avalonia.Win32 { - partial class Win32Platform : IPlatformThreadingInterface, IPlatformSettings, IWindowingPlatform, IPlatformIconLoader + class Win32Platform : IPlatformThreadingInterface, IPlatformSettings, IWindowingPlatform, IPlatformIconLoader { private static readonly Win32Platform s_instance = new Win32Platform(); private static uint _uiThread; @@ -87,6 +84,9 @@ namespace Avalonia.Win32 UseDeferredRendering = deferredRendering; _uiThread = UnmanagedMethods.GetCurrentThreadId(); + + if (OleContext.Current != null) + AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); } public bool HasMessages() @@ -205,5 +205,40 @@ namespace Avalonia.Win32 { return new PopupImpl(); } + + public IWindowIconImpl LoadIcon(string fileName) + { + using (var stream = File.OpenRead(fileName)) + { + return CreateIconImpl(stream); + } + } + + public IWindowIconImpl LoadIcon(Stream stream) + { + return CreateIconImpl(stream); + } + + public IWindowIconImpl LoadIcon(IBitmapImpl bitmap) + { + using (var memoryStream = new MemoryStream()) + { + bitmap.Save(memoryStream); + return new IconImpl(new System.Drawing.Bitmap(memoryStream)); + } + } + + private static IconImpl CreateIconImpl(Stream stream) + { + try + { + return new IconImpl(new System.Drawing.Icon(stream)); + } + catch (ArgumentException) + { + return new IconImpl(new System.Drawing.Bitmap(stream)); + } + } + } } diff --git a/src/Windows/Avalonia.Win32/WinFormsWin32Platform.cs b/src/Windows/Avalonia.Win32/WinFormsWin32Platform.cs deleted file mode 100644 index ec666e1f37..0000000000 --- a/src/Windows/Avalonia.Win32/WinFormsWin32Platform.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Avalonia.Platform; - -namespace Avalonia.Win32 -{ - partial class Win32Platform - { - public IWindowIconImpl LoadIcon(string fileName) - { - using (var stream = File.OpenRead(fileName)) - { - return CreateImpl(stream); - } - } - - public IWindowIconImpl LoadIcon(Stream stream) - { - return CreateImpl(stream); - } - - public IWindowIconImpl LoadIcon(IBitmapImpl bitmap) - { - using (var memoryStream = new MemoryStream()) - { - bitmap.Save(memoryStream); - return new IconImpl(new System.Drawing.Bitmap(memoryStream)); - } - } - - private static IconImpl CreateImpl(Stream stream) - { - try - { - return new IconImpl(new System.Drawing.Icon(stream)); - } - catch (ArgumentException) - { - return new IconImpl(new System.Drawing.Bitmap(stream)); - } - } - } -} diff --git a/src/Windows/Avalonia.Win32/WindowFramebuffer.cs b/src/Windows/Avalonia.Win32/WindowFramebuffer.cs index df238c919e..83ab288c54 100644 --- a/src/Windows/Avalonia.Win32/WindowFramebuffer.cs +++ b/src/Windows/Avalonia.Win32/WindowFramebuffer.cs @@ -10,7 +10,7 @@ namespace Avalonia.Win32 public class WindowFramebuffer : ILockedFramebuffer { private readonly IntPtr _handle; - private IntPtr _pBitmap; + private IUnmanagedBlob _bitmapBlob; private UnmanagedMethods.BITMAPINFOHEADER _bmpInfo; public WindowFramebuffer(IntPtr handle, int width, int height) @@ -27,7 +27,7 @@ namespace Avalonia.Win32 _bmpInfo.Init(); _bmpInfo.biWidth = width; _bmpInfo.biHeight = -height; - _pBitmap = Marshal.AllocHGlobal(width * height * 4); + _bitmapBlob = AvaloniaLocator.Current.GetService().AllocBlob(width * height * 4); } ~WindowFramebuffer() @@ -35,7 +35,7 @@ namespace Avalonia.Win32 Deallocate(); } - public IntPtr Address => _pBitmap; + public IntPtr Address => _bitmapBlob.Address; public int RowBytes => Width * 4; public PixelFormat Format => PixelFormat.Bgra8888; @@ -70,21 +70,18 @@ namespace Avalonia.Win32 public void DrawToDevice(IntPtr hDC, int destX = 0, int destY = 0, int srcX = 0, int srcY = 0, int width = -1, int height = -1) { - if(_pBitmap == IntPtr.Zero) - throw new ObjectDisposedException("Framebuffer"); if (width == -1) width = Width; if (height == -1) height = Height; UnmanagedMethods.SetDIBitsToDevice(hDC, destX, destY, (uint) width, (uint) height, srcX, srcY, - 0, (uint)Height, _pBitmap, ref _bmpInfo, 0); + 0, (uint)Height, _bitmapBlob.Address, ref _bmpInfo, 0); } public bool DrawToWindow(IntPtr hWnd, int destX = 0, int destY = 0, int srcX = 0, int srcY = 0, int width = -1, int height = -1) { - - if (_pBitmap == IntPtr.Zero) + if (_bitmapBlob.IsDisposed) throw new ObjectDisposedException("Framebuffer"); if (hWnd == IntPtr.Zero) return false; @@ -102,13 +99,6 @@ namespace Avalonia.Win32 DrawToWindow(_handle); } - public void Deallocate() - { - if (_pBitmap != IntPtr.Zero) - { - Marshal.FreeHGlobal(_pBitmap); - _pBitmap = IntPtr.Zero; - } - } + public void Deallocate() => _bitmapBlob.Dispose(); } } \ No newline at end of file diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 348468e0e7..79678cbfdf 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -1,29 +1,24 @@ // 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 Avalonia.Input; using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; -using System.Linq; +using System.Reactive.Disposables; using System.Runtime.InteropServices; using Avalonia.Controls; -using System.Reactive.Disposables; +using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Platform; +using Avalonia.Rendering; using Avalonia.Win32.Input; using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods; -using Avalonia.Rendering; -using Avalonia.Threading; -#if NETSTANDARD -using Win32Exception = Avalonia.Win32.NetStandard.AvaloniaWin32Exception; -#endif namespace Avalonia.Win32 { - class WindowImpl : IWindowImpl + public class WindowImpl : IWindowImpl { private static readonly List s_instances = new List(); @@ -36,9 +31,14 @@ namespace Avalonia.Win32 private IInputRoot _owner; private bool _trackingMouse; private bool _decorated = true; + private bool _resizable = true; private double _scaling = 1; private WindowState _showWindowState; private FramebufferManager _framebuffer; + private OleDropTarget _dropTarget; + private Size _minSize; + private Size _maxSize; + #if USE_MANAGED_DRAG private readonly ManagedWindowResizeDragHelper _managedDrag; #endif @@ -61,6 +61,8 @@ namespace Avalonia.Win32 public Action Activated { get; set; } + public Func Closing { get; set; } + public Action Closed { get; set; } public Action Deactivated { get; set; } @@ -79,8 +81,8 @@ namespace Avalonia.Win32 { get { - var style = UnmanagedMethods.GetWindowLong(_hwnd, -16); - var exStyle = UnmanagedMethods.GetWindowLong(_hwnd, -20); + var style = UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_STYLE); + var exStyle = UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_EXSTYLE); var padding = new UnmanagedMethods.RECT(); if (UnmanagedMethods.AdjustWindowRectEx(ref padding, style, false, exStyle)) @@ -104,6 +106,12 @@ namespace Avalonia.Win32 } } + public void SetMinMaxSize(Size minSize, Size maxSize) + { + _minSize = minSize; + _maxSize = maxSize; + } + public IScreenImpl Screen { get; @@ -237,13 +245,19 @@ namespace Avalonia.Win32 return; } - var style = (UnmanagedMethods.WindowStyles)UnmanagedMethods.GetWindowLong(_hwnd, -16); + var style = (UnmanagedMethods.WindowStyles)UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_STYLE); + + var systemDecorationStyles = UnmanagedMethods.WindowStyles.WS_OVERLAPPED + | UnmanagedMethods.WindowStyles.WS_CAPTION + | UnmanagedMethods.WindowStyles.WS_SYSMENU + | UnmanagedMethods.WindowStyles.WS_MINIMIZEBOX + | UnmanagedMethods.WindowStyles.WS_MAXIMIZEBOX; - style |= UnmanagedMethods.WindowStyles.WS_OVERLAPPEDWINDOW; + style |= systemDecorationStyles; if (!value) { - style ^= UnmanagedMethods.WindowStyles.WS_OVERLAPPEDWINDOW; + style ^= systemDecorationStyles; } UnmanagedMethods.RECT windowRect; @@ -253,7 +267,7 @@ namespace Avalonia.Win32 Rect newRect; var oldThickness = BorderThickness; - UnmanagedMethods.SetWindowLong(_hwnd, -16, (uint)style); + UnmanagedMethods.SetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_STYLE, (uint)style); if (value) { @@ -313,6 +327,7 @@ namespace Avalonia.Win32 public void SetInputRoot(IInputRoot inputRoot) { _owner = inputRoot; + CreateDropTarget(); } public void SetTitle(string title) @@ -436,6 +451,14 @@ namespace Avalonia.Win32 return IntPtr.Zero; + case UnmanagedMethods.WindowsMessage.WM_CLOSE: + bool? preventClosing = Closing?.Invoke(); + if (preventClosing == true) + { + return IntPtr.Zero; + } + break; + case UnmanagedMethods.WindowsMessage.WM_DESTROY: //Window doesn't exist anymore _hwnd = IntPtr.Zero; @@ -604,7 +627,26 @@ namespace Avalonia.Win32 case UnmanagedMethods.WindowsMessage.WM_MOVE: PositionChanged?.Invoke(new Point((short)(ToInt32(lParam) & 0xffff), (short)(ToInt32(lParam) >> 16))); return IntPtr.Zero; - + + case UnmanagedMethods.WindowsMessage.WM_GETMINMAXINFO: + + MINMAXINFO mmi = Marshal.PtrToStructure(lParam); + + if (_minSize.Width > 0) + mmi.ptMinTrackSize.X = (int)((_minSize.Width * Scaling) + BorderThickness.Left + BorderThickness.Right); + + if (_minSize.Height > 0) + mmi.ptMinTrackSize.Y = (int)((_minSize.Height * Scaling) + BorderThickness.Top + BorderThickness.Bottom); + + if (!Double.IsInfinity(_maxSize.Width) && _maxSize.Width > 0) + mmi.ptMaxTrackSize.X = (int)((_maxSize.Width * Scaling) + BorderThickness.Left + BorderThickness.Right); + + if (!Double.IsInfinity(_maxSize.Height) && _maxSize.Height > 0) + mmi.ptMaxTrackSize.Y = (int)((_maxSize.Height * Scaling) + BorderThickness.Top + BorderThickness.Bottom); + + Marshal.StructureToPtr(mmi, lParam, true); + return IntPtr.Zero; + case UnmanagedMethods.WindowsMessage.WM_DISPLAYCHANGE: (Screen as ScreenImpl)?.InvalidateScreensCache(); return IntPtr.Zero; @@ -646,7 +688,7 @@ namespace Avalonia.Win32 // Ensure that the delegate doesn't get garbage collected by storing it as a field. _wndProcDelegate = new UnmanagedMethods.WndProc(WndProc); - _className = Guid.NewGuid().ToString(); + _className = "Avalonia-" + Guid.NewGuid(); UnmanagedMethods.WNDCLASSEX wndClassEx = new UnmanagedMethods.WNDCLASSEX { @@ -694,6 +736,13 @@ namespace Avalonia.Win32 } } + private void CreateDropTarget() + { + OleDropTarget odt = new OleDropTarget(this, _owner); + if (OleContext.Current?.RegisterDragDrop(Handle, odt) ?? false) + _dropTarget = odt; + } + private Point DipFromLParam(IntPtr lParam) { return new Point((short)(ToInt32(lParam) & 0xffff), (short)(ToInt32(lParam) >> 16)) / Scaling; @@ -784,11 +833,11 @@ namespace Avalonia.Win32 public void ShowTaskbarIcon(bool value) { - var style = (UnmanagedMethods.WindowStyles)UnmanagedMethods.GetWindowLong(_hwnd, -20); - - style &= ~(UnmanagedMethods.WindowStyles.WS_VISIBLE); + var style = (UnmanagedMethods.WindowStyles)UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_EXSTYLE); - style |= UnmanagedMethods.WindowStyles.WS_EX_TOOLWINDOW; + style &= ~(UnmanagedMethods.WindowStyles.WS_VISIBLE); + + style |= UnmanagedMethods.WindowStyles.WS_EX_TOOLWINDOW; if (value) style |= UnmanagedMethods.WindowStyles.WS_EX_APPWINDOW; else @@ -799,9 +848,27 @@ namespace Avalonia.Win32 { //Toggle to make the styles stick UnmanagedMethods.ShowWindow(_hwnd, ShowWindowCommand.Hide); - UnmanagedMethods.SetWindowLong(_hwnd, -20, (uint)style); + UnmanagedMethods.SetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_EXSTYLE, (uint)style); UnmanagedMethods.ShowWindow(_hwnd, windowPlacement.ShowCmd); } } + + public void CanResize(bool value) + { + if (value == _resizable) + { + return; + } + + var style = (UnmanagedMethods.WindowStyles)UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_STYLE); + + if (value) + style |= UnmanagedMethods.WindowStyles.WS_SIZEFRAME; + else + style &= ~(UnmanagedMethods.WindowStyles.WS_SIZEFRAME); + + UnmanagedMethods.SetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_STYLE, (uint)style); + _resizable = value; + } } } diff --git a/src/iOS/Avalonia.iOS/EmbeddableImpl.cs b/src/iOS/Avalonia.iOS/EmbeddableImpl.cs index 50a3cd9ec0..3d8bafeca9 100644 --- a/src/iOS/Avalonia.iOS/EmbeddableImpl.cs +++ b/src/iOS/Avalonia.iOS/EmbeddableImpl.cs @@ -14,6 +14,10 @@ namespace Avalonia.iOS } + public void SetMinMaxSize(Size minSize, Size maxSize) + { + } + public IDisposable ShowDialog() { return Disposable.Empty; diff --git a/src/iOS/Avalonia.iOS/EmulatedFramebuffer.cs b/src/iOS/Avalonia.iOS/EmulatedFramebuffer.cs index 58cf6edd78..517b372ba4 100644 --- a/src/iOS/Avalonia.iOS/EmulatedFramebuffer.cs +++ b/src/iOS/Avalonia.iOS/EmulatedFramebuffer.cs @@ -16,11 +16,15 @@ namespace Avalonia.iOS /// class EmulatedFramebuffer : ILockedFramebuffer { + private nfloat _viewWidth; + private nfloat _viewHeight; public EmulatedFramebuffer(UIView view) { var factor = (int) UIScreen.MainScreen.Scale; var frame = view.Frame; + _viewWidth = frame.Width; + _viewHeight = frame.Height; Width = (int) frame.Width * factor; Height = (int) frame.Height * factor; RowBytes = Width * 4; @@ -41,9 +45,9 @@ namespace Avalonia.iOS using (var context = UIGraphics.GetCurrentContext()) { // flip the image for CGContext.DrawImage - context.TranslateCTM(0, Height); + context.TranslateCTM(0, _viewHeight); context.ScaleCTM(1, -1); - context.DrawImage(new CGRect(0, 0, Width, Height), image); + context.DrawImage(new CGRect(0, 0, _viewWidth, _viewHeight), image); } Marshal.FreeHGlobal(Address); Address = IntPtr.Zero; @@ -57,3 +61,4 @@ namespace Avalonia.iOS public PixelFormat Format { get; } } } + diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_AddOwner.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_AddOwner.cs new file mode 100644 index 0000000000..4e033be3fb --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_AddOwner.cs @@ -0,0 +1,52 @@ +// 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 Xunit; + +namespace Avalonia.Base.UnitTests +{ + public class AvaloniaObjectTests_AddOwner + { + [Fact] + public void AddOwnered_Property_Retains_Default_Value() + { + var target = new Class2(); + + Assert.Equal("foodefault", target.GetValue(Class2.FooProperty)); + } + + [Fact] + public void AddOwnered_Property_Does_Not_Retain_Validation() + { + var target = new Class2(); + + target.SetValue(Class2.FooProperty, "throw"); + } + + private class Class1 : AvaloniaObject + { + public static readonly StyledProperty FooProperty = + AvaloniaProperty.Register( + "Foo", + "foodefault", + validate: ValidateFoo); + + private static string ValidateFoo(AvaloniaObject arg1, string arg2) + { + if (arg2 == "throw") + { + throw new IndexOutOfRangeException(); + } + + return arg2; + } + } + + private class Class2 : AvaloniaObject + { + public static readonly StyledProperty FooProperty = + Class1.FooProperty.AddOwner(); + } + } +} diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs new file mode 100644 index 0000000000..ab2a2d899d --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs @@ -0,0 +1,52 @@ +// 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 Xunit; + +namespace Avalonia.Base.UnitTests +{ + public class AvaloniaObjectTests_Attached + { + [Fact] + public void AddOwnered_Property_Retains_Default_Value() + { + var target = new Class2(); + + Assert.Equal("foodefault", target.GetValue(Class2.FooProperty)); + } + + [Fact] + public void AddOwnered_Property_Retains_Validation() + { + var target = new Class2(); + + Assert.Throws(() => target.SetValue(Class2.FooProperty, "throw")); + } + + private class Class1 : AvaloniaObject + { + public static readonly AttachedProperty FooProperty = + AvaloniaProperty.RegisterAttached( + "Foo", + "foodefault", + validate: ValidateFoo); + + private static string ValidateFoo(AvaloniaObject arg1, string arg2) + { + if (arg2 == "throw") + { + throw new IndexOutOfRangeException(); + } + + return arg2; + } + } + + private class Class2 : AvaloniaObject + { + public static readonly AttachedProperty FooProperty = + Class1.FooProperty.AddOwner(); + } + } +} diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index c75150ca6d..80cd52d529 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -92,14 +92,13 @@ namespace Avalonia.Base.UnitTests } [Fact] - public void Bind_Throws_Exception_For_Unregistered_Property() + public void Bind_Does_Not_Throw_Exception_For_Unregistered_Property() { Class1 target = new Class1(); - Assert.Throws(() => - { - target.Bind(Class2.BarProperty, Observable.Return("foo")); - }); + target.Bind(Class2.BarProperty, Observable.Never().StartWith("foo")); + + Assert.Equal("foo", target.GetValue(Class2.BarProperty)); } [Fact] diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs index 98f7289228..740023fd37 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs @@ -46,11 +46,11 @@ namespace Avalonia.Base.UnitTests } [Fact] - public void GetValue_Throws_Exception_For_Unregistered_Property() + public void GetValue_Doesnt_Throw_Exception_For_Unregistered_Property() { var target = new Class3(); - Assert.Throws(() => target.GetValue(Class1.FooProperty)); + Assert.Equal("foodefault", target.GetValue(Class1.FooProperty)); } private class Class1 : AvaloniaObject diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs index 1f9c47f8ae..a56cd717b9 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs @@ -30,6 +30,16 @@ namespace Avalonia.Base.UnitTests Assert.Equal("newvalue", target.GetValue(Class1.FooProperty)); } + [Fact] + public void SetValue_Sets_Attached_Value() + { + Class2 target = new Class2(); + + target.SetValue(AttachedOwner.AttachedProperty, "newvalue"); + + Assert.Equal("newvalue", target.GetValue(AttachedOwner.AttachedProperty)); + } + [Fact] public void SetValue_Raises_PropertyChanged() { @@ -84,14 +94,27 @@ namespace Avalonia.Base.UnitTests } [Fact] - public void SetValue_Throws_Exception_For_Unregistered_Property() + public void SetValue_Allows_Setting_Unregistered_Property() { Class1 target = new Class1(); - Assert.Throws(() => - { - target.SetValue(Class2.BarProperty, "invalid"); - }); + Assert.False(AvaloniaPropertyRegistry.Instance.IsRegistered(target, Class2.BarProperty)); + + target.SetValue(Class2.BarProperty, "bar"); + + Assert.Equal("bar", target.GetValue(Class2.BarProperty)); + } + + [Fact] + public void SetValue_Allows_Setting_Unregistered_Attached_Property() + { + Class1 target = new Class1(); + + Assert.False(AvaloniaPropertyRegistry.Instance.IsRegistered(target, AttachedOwner.AttachedProperty)); + + target.SetValue(AttachedOwner.AttachedProperty, "bar"); + + Assert.Equal("bar", target.GetValue(AttachedOwner.AttachedProperty)); } [Fact] @@ -189,6 +212,12 @@ namespace Avalonia.Base.UnitTests } } + private class AttachedOwner + { + public static readonly AttachedProperty AttachedProperty = + AvaloniaProperty.RegisterAttached("Attached"); + } + private class ImplictDouble { public ImplictDouble(double value) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs index da0b0252a3..8b2e500d37 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs @@ -4,12 +4,13 @@ using System.Linq; using System.Reactive.Linq; using Xunit; +using Xunit.Abstractions; namespace Avalonia.Base.UnitTests { public class AvaloniaPropertyRegistryTests { - public AvaloniaPropertyRegistryTests() + public AvaloniaPropertyRegistryTests(ITestOutputHelper s) { // Ensure properties are registered. AvaloniaProperty p; @@ -25,7 +26,7 @@ namespace Avalonia.Base.UnitTests .Select(x => x.Name) .ToArray(); - Assert.Equal(new[] { "Foo", "Baz", "Qux", "Attached" }, names); + Assert.Equal(new[] { "Foo", "Baz", "Qux" }, names); } [Fact] @@ -35,61 +36,41 @@ namespace Avalonia.Base.UnitTests .Select(x => x.Name) .ToArray(); - Assert.Equal(new[] { "Bar", "Flob", "Fred", "Foo", "Baz", "Qux", "Attached" }, names); + Assert.Equal(new[] { "Bar", "Flob", "Fred", "Foo", "Baz", "Qux" }, names); } [Fact] - public void GetAttached_Returns_Registered_Properties_For_Base_Types() + public void GetRegisteredAttached_Returns_Registered_Properties() { - string[] names = AvaloniaPropertyRegistry.Instance.GetAttached(typeof(AttachedOwner)).Select(x => x.Name).ToArray(); + string[] names = AvaloniaPropertyRegistry.Instance.GetRegisteredAttached(typeof(Class1)) + .Select(x => x.Name) + .ToArray(); Assert.Equal(new[] { "Attached" }, names); } [Fact] - public void FindRegistered_Finds_Untyped_Property() + public void GetRegisteredAttached_Returns_Registered_Properties_For_Base_Types() { - var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class1), "Foo"); + string[] names = AvaloniaPropertyRegistry.Instance.GetRegisteredAttached(typeof(Class2)) + .Select(x => x.Name) + .ToArray(); - Assert.Equal(Class1.FooProperty, result); + Assert.Equal(new[] { "Attached" }, names); } [Fact] - public void FindRegistered_Finds_Typed_Property() + public void FindRegistered_Finds_Property() { - var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class1), "Class1.Foo"); + var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class1), "Foo"); Assert.Equal(Class1.FooProperty, result); } [Fact] - public void FindRegistered_Finds_Typed_Inherited_Property() - { - var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class2), "Class1.Foo"); - - Assert.Equal(Class2.FooProperty, result); - } - - [Fact] - public void FindRegistered_Finds_Inherited_Property_With_Derived_Type_Name() - { - var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class2), "Class2.Foo"); - - Assert.Equal(Class2.FooProperty, result); - } - - [Fact] - public void FindRegistered_Finds_Attached_Property() - { - var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class2), "AttachedOwner.Attached"); - - Assert.Equal(AttachedOwner.AttachedProperty, result); - } - - [Fact] - public void FindRegistered_Doesnt_Finds_Unqualified_Attached_Property() + public void FindRegistered_Doesnt_Find_Nonregistered_Property() { - var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class2), "Attached"); + var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class1), "Bar"); Assert.Null(result); } @@ -99,55 +80,34 @@ namespace Avalonia.Base.UnitTests { var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(AttachedOwner), "Attached"); - Assert.True(AttachedOwner.AttachedProperty == result); + Assert.Same(AttachedOwner.AttachedProperty, result); } [Fact] - public void FindRegistered_Finds_AddOwnered_Untyped_Attached_Property() + public void FindRegistered_Finds_AddOwnered_Attached_Property() { var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class3), "Attached"); - Assert.True(AttachedOwner.AttachedProperty == result); + Assert.Same(AttachedOwner.AttachedProperty, result); } [Fact] - public void FindRegistered_Finds_AddOwnered_Typed_Attached_Property() + public void FindRegistered_Doesnt_Find_Non_AddOwnered_Attached_Property() { - var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class3), "Class3.Attached"); - - Assert.True(AttachedOwner.AttachedProperty == result); - } - - [Fact] - public void FindRegistered_Finds_AddOwnered_AttachedTyped_Attached_Property() - { - var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class3), "AttachedOwner.Attached"); - - Assert.True(AttachedOwner.AttachedProperty == result); - } - - [Fact] - public void FindRegistered_Finds_AddOwnered_BaseTyped_Attached_Property() - { - var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class3), "Class1.Attached"); - - Assert.True(AttachedOwner.AttachedProperty == result); - } - - [Fact] - public void FindRegistered_Doesnt_Find_Nonregistered_Property() - { - var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class1), "Bar"); + var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class2), "Attached"); Assert.Null(result); } [Fact] - public void FindRegistered_Doesnt_Find_Nonregistered_Attached_Property() + public void FindRegisteredAttached_Finds_Property() { - var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class4), "AttachedOwner.Attached"); + var result = AvaloniaPropertyRegistry.Instance.FindRegisteredAttached( + typeof(Class1), + typeof(AttachedOwner), + "Attached"); - Assert.Null(result); + Assert.Equal(AttachedOwner.AttachedProperty, result); } private class Class1 : AvaloniaObject @@ -176,18 +136,18 @@ namespace Avalonia.Base.UnitTests private class Class3 : Class1 { - public static readonly StyledProperty AttachedProperty = + public static readonly AttachedProperty AttachedProperty = AttachedOwner.AttachedProperty.AddOwner(); } - public class Class4 : AvaloniaObject - { - } - private class AttachedOwner : Class1 { public static readonly AttachedProperty AttachedProperty = AvaloniaProperty.RegisterAttached("Attached"); } + + private class AttachedOwner2 : AttachedOwner + { + } } } diff --git a/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs b/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs index 33af55fdf9..d24a646f74 100644 --- a/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs +++ b/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs @@ -33,7 +33,7 @@ namespace Avalonia.Benchmarks.Styling var border = (Border)textBox.GetVisualChildren().Single(); - if (border.BorderThickness != 2) + if (border.BorderThickness != new Thickness(2)) { throw new Exception("Styles not applied."); } diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs new file mode 100644 index 0000000000..f9da2ab6f3 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs @@ -0,0 +1,1042 @@ +// 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; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.Data; +using Avalonia.Markup.Xaml.Data; +using Avalonia.Platform; +using Avalonia.Threading; +using Avalonia.UnitTests; +using Moq; +using Xunit; +using System.Collections.ObjectModel; + +namespace Avalonia.Controls.UnitTests +{ + public class AutoCompleteBoxTests + { + [Fact] + public void Search_Filters() + { + Assert.True(GetFilter(AutoCompleteFilterMode.Contains)("am", "name")); + Assert.True(GetFilter(AutoCompleteFilterMode.Contains)("AME", "name")); + Assert.False(GetFilter(AutoCompleteFilterMode.Contains)("hello", "name")); + + Assert.True(GetFilter(AutoCompleteFilterMode.ContainsCaseSensitive)("na", "name")); + Assert.False(GetFilter(AutoCompleteFilterMode.ContainsCaseSensitive)("AME", "name")); + Assert.False(GetFilter(AutoCompleteFilterMode.ContainsCaseSensitive)("hello", "name")); + + Assert.Null(GetFilter(AutoCompleteFilterMode.Custom)); + Assert.Null(GetFilter(AutoCompleteFilterMode.None)); + + Assert.True(GetFilter(AutoCompleteFilterMode.Equals)("na", "na")); + Assert.True(GetFilter(AutoCompleteFilterMode.Equals)("na", "NA")); + Assert.False(GetFilter(AutoCompleteFilterMode.Equals)("hello", "name")); + + Assert.True(GetFilter(AutoCompleteFilterMode.EqualsCaseSensitive)("na", "na")); + Assert.False(GetFilter(AutoCompleteFilterMode.EqualsCaseSensitive)("na", "NA")); + Assert.False(GetFilter(AutoCompleteFilterMode.EqualsCaseSensitive)("hello", "name")); + + Assert.True(GetFilter(AutoCompleteFilterMode.StartsWith)("na", "name")); + Assert.True(GetFilter(AutoCompleteFilterMode.StartsWith)("NAM", "name")); + Assert.False(GetFilter(AutoCompleteFilterMode.StartsWith)("hello", "name")); + + Assert.True(GetFilter(AutoCompleteFilterMode.StartsWithCaseSensitive)("na", "name")); + Assert.False(GetFilter(AutoCompleteFilterMode.StartsWithCaseSensitive)("NAM", "name")); + Assert.False(GetFilter(AutoCompleteFilterMode.StartsWithCaseSensitive)("hello", "name")); + } + + [Fact] + public void Ordinal_Search_Filters() + { + Assert.True(GetFilter(AutoCompleteFilterMode.ContainsOrdinal)("am", "name")); + Assert.True(GetFilter(AutoCompleteFilterMode.ContainsOrdinal)("AME", "name")); + Assert.False(GetFilter(AutoCompleteFilterMode.ContainsOrdinal)("hello", "name")); + + Assert.True(GetFilter(AutoCompleteFilterMode.ContainsOrdinalCaseSensitive)("na", "name")); + Assert.False(GetFilter(AutoCompleteFilterMode.ContainsOrdinalCaseSensitive)("AME", "name")); + Assert.False(GetFilter(AutoCompleteFilterMode.ContainsOrdinalCaseSensitive)("hello", "name")); + + Assert.True(GetFilter(AutoCompleteFilterMode.EqualsOrdinal)("na", "na")); + Assert.True(GetFilter(AutoCompleteFilterMode.EqualsOrdinal)("na", "NA")); + Assert.False(GetFilter(AutoCompleteFilterMode.EqualsOrdinal)("hello", "name")); + + Assert.True(GetFilter(AutoCompleteFilterMode.EqualsOrdinalCaseSensitive)("na", "na")); + Assert.False(GetFilter(AutoCompleteFilterMode.EqualsOrdinalCaseSensitive)("na", "NA")); + Assert.False(GetFilter(AutoCompleteFilterMode.EqualsOrdinalCaseSensitive)("hello", "name")); + + Assert.True(GetFilter(AutoCompleteFilterMode.StartsWithOrdinal)("na", "name")); + Assert.True(GetFilter(AutoCompleteFilterMode.StartsWithOrdinal)("NAM", "name")); + Assert.False(GetFilter(AutoCompleteFilterMode.StartsWithOrdinal)("hello", "name")); + + Assert.True(GetFilter(AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive)("na", "name")); + Assert.False(GetFilter(AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive)("NAM", "name")); + Assert.False(GetFilter(AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive)("hello", "name")); + } + + [Fact] + public void Fires_DropDown_Events() + { + RunTest((control, textbox) => + { + bool openEvent = false; + bool closeEvent = false; + control.DropDownOpened += (s, e) => openEvent = true; + control.DropDownClosed += (s, e) => closeEvent = true; + control.Items = CreateSimpleStringArray(); + + textbox.Text = "a"; + Dispatcher.UIThread.RunJobs(); + Assert.True(control.SearchText == "a"); + Assert.True(control.IsDropDownOpen); + Assert.True(openEvent); + + textbox.Text = String.Empty; + Dispatcher.UIThread.RunJobs(); + Assert.True(control.SearchText == String.Empty); + Assert.False(control.IsDropDownOpen); + Assert.True(closeEvent); + }); + } + + [Fact] + public void Text_Completion_Via_Text_Property() + { + RunTest((control, textbox) => + { + control.IsTextCompletionEnabled = true; + + Assert.Equal(String.Empty, control.Text); + control.Text = "close"; + Assert.NotNull(control.SelectedItem); + }); + } + + [Fact] + public void Text_Completion_Selects_Text() + { + RunTest((control, textbox) => + { + control.IsTextCompletionEnabled = true; + + textbox.Text = "ac"; + textbox.SelectionEnd = textbox.SelectionStart = 2; + Dispatcher.UIThread.RunJobs(); + + Assert.True(control.IsDropDownOpen); + Assert.True(Math.Abs(textbox.SelectionEnd - textbox.SelectionStart) > 2); + }); + } + + [Fact] + public void TextChanged_Event_Fires() + { + RunTest((control, textbox) => + { + bool textChanged = false; + control.TextChanged += (s, e) => textChanged = true; + + textbox.Text = "a"; + Dispatcher.UIThread.RunJobs(); + Assert.True(textChanged); + + textChanged = false; + control.Text = "conversati"; + Dispatcher.UIThread.RunJobs(); + Assert.True(textChanged); + + textChanged = false; + control.Text = null; + Dispatcher.UIThread.RunJobs(); + Assert.True(textChanged); + }); + } + + [Fact] + public void MinimumPrefixLength_Works() + { + RunTest((control, textbox) => + { + textbox.Text = "a"; + Dispatcher.UIThread.RunJobs(); + Assert.True(control.IsDropDownOpen); + + + textbox.Text = String.Empty; + Dispatcher.UIThread.RunJobs(); + Assert.False(control.IsDropDownOpen); + + control.MinimumPrefixLength = 3; + + textbox.Text = "a"; + Dispatcher.UIThread.RunJobs(); + Assert.False(control.IsDropDownOpen); + + textbox.Text = "acc"; + Dispatcher.UIThread.RunJobs(); + Assert.True(control.IsDropDownOpen); + }); + } + + [Fact] + public void Can_Cancel_DropDown_Opening() + { + RunTest((control, textbox) => + { + control.DropDownOpening += (s, e) => e.Cancel = true; + + textbox.Text = "a"; + Dispatcher.UIThread.RunJobs(); + Assert.False(control.IsDropDownOpen); + }); + } + + [Fact] + public void Can_Cancel_DropDown_Closing() + { + RunTest((control, textbox) => + { + control.DropDownClosing += (s, e) => e.Cancel = true; + + textbox.Text = "a"; + Dispatcher.UIThread.RunJobs(); + Assert.True(control.IsDropDownOpen); + + control.IsDropDownOpen = false; + Assert.True(control.IsDropDownOpen); + }); + } + + [Fact] + public void Can_Cancel_Population() + { + RunTest((control, textbox) => + { + bool populating = false; + bool populated = false; + control.FilterMode = AutoCompleteFilterMode.None; + control.Populating += (s, e) => + { + e.Cancel = true; + populating = true; + }; + control.Populated += (s, e) => populated = true; + + textbox.Text = "accounti"; + Dispatcher.UIThread.RunJobs(); + + Assert.True(populating); + Assert.False(populated); + }); + } + + [Fact] + public void Custom_Population_Supported() + { + RunTest((control, textbox) => + { + string custom = "Custom!"; + string search = "accounti"; + bool populated = false; + bool populatedOk = false; + control.FilterMode = AutoCompleteFilterMode.None; + control.Populating += (s, e) => + { + control.Items = new string[] { custom }; + Assert.Equal(search, e.Parameter); + }; + control.Populated += (s, e) => + { + populated = true; + ReadOnlyCollection collection = e.Data as ReadOnlyCollection; + populatedOk = collection != null && collection.Count == 1; + }; + + textbox.Text = search; + Dispatcher.UIThread.RunJobs(); + + Assert.True(populated); + Assert.True(populatedOk); + }); + } + + [Fact] + public void Text_Completion() + { + RunTest((control, textbox) => + { + control.IsTextCompletionEnabled = true; + textbox.Text = "accounti"; + textbox.SelectionStart = textbox.SelectionEnd = textbox.Text.Length; + Dispatcher.UIThread.RunJobs(); + Assert.Equal("accounti", control.SearchText); + Assert.Equal("accounting", textbox.Text); + }); + } + + [Fact] + public void String_Search() + { + RunTest((control, textbox) => + { + textbox.Text = "a"; + Dispatcher.UIThread.RunJobs(); + Assert.Equal(textbox.Text, control.Text); + + textbox.Text = "acc"; + Dispatcher.UIThread.RunJobs(); + Assert.Equal(textbox.Text, control.Text); + + textbox.Text = "a"; + Dispatcher.UIThread.RunJobs(); + Assert.Equal(textbox.Text, control.Text); + + textbox.Text = ""; + Dispatcher.UIThread.RunJobs(); + Assert.Equal(textbox.Text, control.Text); + + textbox.Text = "cook"; + Dispatcher.UIThread.RunJobs(); + Assert.Equal(textbox.Text, control.Text); + + textbox.Text = "accept"; + Dispatcher.UIThread.RunJobs(); + Assert.Equal(textbox.Text, control.Text); + + textbox.Text = "cook"; + Dispatcher.UIThread.RunJobs(); + Assert.Equal(textbox.Text, control.Text); + }); + } + + [Fact] + public void Item_Search() + { + RunTest((control, textbox) => + { + control.FilterMode = AutoCompleteFilterMode.Custom; + control.ItemFilter = (search, item) => + { + string s = item as string; + return s == null ? false : true; + }; + + // Just set to null briefly to exercise that code path + AutoCompleteFilterPredicate filter = control.ItemFilter; + Assert.NotNull(filter); + control.ItemFilter = null; + Assert.Null(control.ItemFilter); + control.ItemFilter = filter; + Assert.NotNull(control.ItemFilter); + + textbox.Text = "a"; + Dispatcher.UIThread.RunJobs(); + Assert.Equal(textbox.Text, control.Text); + + textbox.Text = "acc"; + Dispatcher.UIThread.RunJobs(); + Assert.Equal(textbox.Text, control.Text); + + textbox.Text = "a"; + Dispatcher.UIThread.RunJobs(); + Assert.Equal(textbox.Text, control.Text); + + textbox.Text = ""; + Dispatcher.UIThread.RunJobs(); + Assert.Equal(textbox.Text, control.Text); + + textbox.Text = "cook"; + Dispatcher.UIThread.RunJobs(); + Assert.Equal(textbox.Text, control.Text); + + textbox.Text = "accept"; + Dispatcher.UIThread.RunJobs(); + Assert.Equal(textbox.Text, control.Text); + + textbox.Text = "cook"; + Dispatcher.UIThread.RunJobs(); + Assert.Equal(textbox.Text, control.Text); + }); + } + + /// + /// Retrieves a defined predicate filter through a new AutoCompleteBox + /// control instance. + /// + /// The FilterMode of interest. + /// Returns the predicate instance. + private static AutoCompleteFilterPredicate GetFilter(AutoCompleteFilterMode mode) + { + return new AutoCompleteBox { FilterMode = mode } + .TextFilter; + } + + /// + /// Creates a large list of strings for AutoCompleteBox testing. + /// + /// Returns a new List of string values. + private IList CreateSimpleStringArray() + { + return new List + { + "a", + "abide", + "able", + "about", + "above", + "absence", + "absurd", + "accept", + "acceptance", + "accepted", + "accepting", + "access", + "accessed", + "accessible", + "accident", + "accidentally", + "accordance", + "account", + "accounting", + "accounts", + "accusation", + "accustomed", + "ache", + "across", + "act", + "active", + "actual", + "actually", + "ada", + "added", + "adding", + "addition", + "additional", + "additions", + "address", + "addressed", + "addresses", + "addressing", + "adjourn", + "adoption", + "advance", + "advantage", + "adventures", + "advice", + "advisable", + "advise", + "affair", + "affectionately", + "afford", + "afore", + "afraid", + "after", + "afterwards", + "again", + "against", + "age", + "aged", + "agent", + "ago", + "agony", + "agree", + "agreed", + "agreement", + "ah", + "ahem", + "air", + "airs", + "ak", + "alarm", + "alarmed", + "alas", + "alice", + "alive", + "all", + "allow", + "almost", + "alone", + "along", + "aloud", + "already", + "also", + "alteration", + "altered", + "alternate", + "alternately", + "altogether", + "always", + "am", + "ambition", + "among", + "an", + "ancient", + "and", + "anger", + "angrily", + "angry", + "animal", + "animals", + "ann", + "annoy", + "annoyed", + "another", + "answer", + "answered", + "answers", + "antipathies", + "anxious", + "anxiously", + "any", + "anyone", + "anything", + "anywhere", + "appealed", + "appear", + "appearance", + "appeared", + "appearing", + "appears", + "applause", + "apple", + "apples", + "applicable", + "apply", + "approach", + "arch", + "archbishop", + "arches", + "archive", + "are", + "argue", + "argued", + "argument", + "arguments", + "arise", + "arithmetic", + "arm", + "arms", + "around", + "arranged", + "array", + "arrived", + "arrow", + "arrum", + "as", + "ascii", + "ashamed", + "ask", + "askance", + "asked", + "asking", + "asleep", + "assembled", + "assistance", + "associated", + "at", + "ate", + "atheling", + "atom", + "attached", + "attempt", + "attempted", + "attempts", + "attended", + "attending", + "attends", + "audibly", + "australia", + "author", + "authority", + "available", + "avoid", + "away", + "awfully", + "axes", + "axis", + "b", + "baby", + "back", + "backs", + "bad", + "bag", + "baked", + "balanced", + "bank", + "banks", + "banquet", + "bark", + "barking", + "barley", + "barrowful", + "based", + "bat", + "bathing", + "bats", + "bawled", + "be", + "beak", + "bear", + "beast", + "beasts", + "beat", + "beating", + "beau", + "beauti", + "beautiful", + "beautifully", + "beautify", + "became", + "because", + "become", + "becoming", + "bed", + "beds", + "bee", + "been", + "before", + "beg", + "began", + "begged", + "begin", + "beginning", + "begins", + "begun", + "behead", + "beheaded", + "beheading", + "behind", + "being", + "believe", + "believed", + "bells", + "belong", + "belongs", + "beloved", + "below", + "belt", + "bend", + "bent", + "besides", + "best", + "better", + "between", + "bill", + "binary", + "bird", + "birds", + "birthday", + "bit", + "bite", + "bitter", + "blacking", + "blades", + "blame", + "blasts", + "bleeds", + "blew", + "blow", + "blown", + "blows", + "body", + "boldly", + "bone", + "bones", + "book", + "books", + "boon", + "boots", + "bore", + "both", + "bother", + "bottle", + "bottom", + "bough", + "bound", + "bowed", + "bowing", + "box", + "boxed", + "boy", + "brain", + "branch", + "branches", + "brandy", + "brass", + "brave", + "breach", + "bread", + "break", + "breath", + "breathe", + "breeze", + "bright", + "brightened", + "bring", + "bringing", + "bristling", + "broke", + "broken", + "brother", + "brought", + "brown", + "brush", + "brushing", + "burn", + "burning", + "burnt", + "burst", + "bursting", + "busily", + "business", + "business@pglaf", + "busy", + "but", + "butter", + "buttercup", + "buttered", + "butterfly", + "buttons", + "by", + "bye", + "c", + "cackled", + "cake", + "cakes", + "calculate", + "calculated", + "call", + "called", + "calling", + "calmly", + "came", + "camomile", + "can", + "canary", + "candle", + "cannot", + "canterbury", + "canvas", + "capering", + "capital", + "card", + "cardboard", + "cards", + "care", + "carefully", + "cares", + "carried", + "carrier", + "carroll", + "carry", + "carrying", + "cart", + "cartwheels", + "case", + "cat", + "catch", + "catching", + "caterpillar", + "cats", + "cattle", + "caucus", + "caught", + "cauldron", + "cause", + "caused", + "cautiously", + "cease", + "ceiling", + "centre", + "certain", + "certainly", + "chain", + "chains", + "chair", + "chance", + "chanced", + "change", + "changed", + "changes", + "changing", + "chapter", + "character", + "charge", + "charges", + "charitable", + "charities", + "chatte", + "cheap", + "cheated", + "check", + "checked", + "checks", + "cheeks", + "cheered", + "cheerfully", + "cherry", + "cheshire", + "chief", + "child", + "childhood", + "children", + "chimney", + "chimneys", + "chin", + "choice", + "choke", + "choked", + "choking", + "choose", + "choosing", + "chop", + "chorus", + "chose", + "christmas", + "chrysalis", + "chuckled", + "circle", + "circumstances", + "city", + "civil", + "claim", + "clamour", + "clapping", + "clasped", + "classics", + "claws", + "clean", + "clear", + "cleared", + "clearer", + "clearly", + "clever", + "climb", + "clinging", + "clock", + "close", + "closed", + "closely", + "closer", + "clubs", + "coast", + "coaxing", + "codes", + "coils", + "cold", + "collar", + "collected", + "collection", + "come", + "comes", + "comfits", + "comfort", + "comfortable", + "comfortably", + "coming", + "commercial", + "committed", + "common", + "commotion", + "company", + "compilation", + "complained", + "complaining", + "completely", + "compliance", + "comply", + "complying", + "compressed", + "computer", + "computers", + "concept", + "concerning", + "concert", + "concluded", + "conclusion", + "condemn", + "conduct", + "confirmation", + "confirmed", + "confused", + "confusing", + "confusion", + "conger", + "conqueror", + "conquest", + "consented", + "consequential", + "consider", + "considerable", + "considered", + "considering", + "constant", + "consultation", + "contact", + "contain", + "containing", + "contempt", + "contemptuous", + "contemptuously", + "content", + "continued", + "contract", + "contradicted", + "contributions", + "conversation", + "conversations", + "convert", + "cook", + "cool", + "copied", + "copies", + "copy", + "copying", + "copyright", + "corner", + "corners", + "corporation", + "corrupt", + "cost", + "costs", + "could", + "couldn", + "counting", + "countries", + "country", + "couple", + "couples", + "courage", + "course", + "court", + "courtiers", + "coward", + "crab", + "crash", + "crashed", + "crawled", + "crawling", + "crazy", + "created", + "creating", + "creation", + "creature", + "creatures", + "credit", + "creep", + "crept", + "cried", + "cries", + "crimson", + "critical", + "crocodile", + "croquet", + "croqueted", + "croqueting", + "cross", + "crossed", + "crossly", + "crouched", + "crowd", + "crowded", + "crown", + "crumbs", + "crust", + "cry", + "crying", + "cucumber", + "cunning", + "cup", + "cupboards", + "cur", + "curiosity", + "curious", + "curiouser", + "curled", + "curls", + "curly", + "currants", + "current", + "curtain", + "curtsey", + "curtseying", + "curving", + "cushion", + "custard", + "custody", + "cut", + "cutting", + }; + } + private void RunTest(Action test) + { + using (UnitTestApplication.Start(Services)) + { + AutoCompleteBox control = CreateControl(); + control.Items = CreateSimpleStringArray(); + TextBox textBox = GetTextBox(control); + Dispatcher.UIThread.RunJobs(); + test.Invoke(control, textBox); + } + } + + private static TestServices Services => TestServices.StyledWindow; + + /*private static TestServices Services => TestServices.MockThreadingInterface.With( + standardCursorFactory: Mock.Of(), + windowingPlatform: new MockWindowingPlatform());*/ + + private AutoCompleteBox CreateControl() + { + var datePicker = + new AutoCompleteBox + { + Template = CreateTemplate() + }; + + datePicker.ApplyTemplate(); + return datePicker; + } + private TextBox GetTextBox(AutoCompleteBox control) + { + return control.GetTemplateChildren() + .OfType() + .First(); + } + private IControlTemplate CreateTemplate() + { + return new FuncControlTemplate(control => + { + var textBox = + new TextBox + { + Name = "PART_TextBox" + }; + var listbox = + new ListBox + { + Name = "PART_SelectingItemsControl" + }; + var popup = + new Popup + { + Name = "PART_Popup" + }; + + var panel = new Panel(); + panel.Children.Add(textBox); + panel.Children.Add(popup); + panel.Children.Add(listbox); + + return panel; + }); + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/BorderTests.cs b/tests/Avalonia.Controls.UnitTests/BorderTests.cs index c0d2a39ab2..9a6a041ec7 100644 --- a/tests/Avalonia.Controls.UnitTests/BorderTests.cs +++ b/tests/Avalonia.Controls.UnitTests/BorderTests.cs @@ -13,12 +13,34 @@ namespace Avalonia.Controls.UnitTests var target = new Border { Padding = new Thickness(6), - BorderThickness = 4, + BorderThickness = new Thickness(4) }; target.Measure(new Size(100, 100)); Assert.Equal(new Size(20, 20), target.DesiredSize); } + + [Fact] + public void Child_Should_Arrange_With_Zero_Height_Width_If_Padding_Greater_Than_Child_Size() + { + Border content; + + var target = new Border + { + Padding = new Thickness(6), + MaxHeight = 12, + MaxWidth = 12, + Child = content = new Border + { + Height = 0, + Width = 0 + } + }; + + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(new Rect(6, 6, 0, 0), content.Bounds); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs new file mode 100644 index 0000000000..fe4f1ea06e --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs @@ -0,0 +1,130 @@ +// 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; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.Data; +using Avalonia.Markup.Xaml.Data; +using Avalonia.Platform; +using Avalonia.UnitTests; +using Moq; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class DatePickerTests + { + private static bool CompareDates(DateTime first, DateTime second) + { + return first.Year == second.Year && + first.Month == second.Month && + first.Day == second.Day; + } + + [Fact] + public void SelectedDateChanged_Should_Fire_When_SelectedDate_Set() + { + using (UnitTestApplication.Start(Services)) + { + bool handled = false; + DatePicker datePicker = CreateControl(); + datePicker.SelectedDateChanged += (s,e) => + { + handled = true; + }; + DateTime value = new DateTime(2000, 10, 10); + datePicker.SelectedDate = value; + Threading.Dispatcher.UIThread.RunJobs(); + Assert.True(handled); + } + } + + [Fact] + public void Setting_Selected_Date_To_Blackout_Date_Should_Throw() + { + using (UnitTestApplication.Start(Services)) + { + DatePicker datePicker = CreateControl(); + datePicker.BlackoutDates.AddDatesInPast(); + + DateTime goodValue = DateTime.Today.AddDays(1); + datePicker.SelectedDate = goodValue; + Assert.True(CompareDates(datePicker.SelectedDate.Value, goodValue)); + + DateTime badValue = DateTime.Today.AddDays(-1); + Assert.ThrowsAny( + () => datePicker.SelectedDate = badValue); + } + } + + [Fact] + public void Adding_Blackout_Dates_Containing_Selected_Date_Should_Throw() + { + using (UnitTestApplication.Start(Services)) + { + DatePicker datePicker = CreateControl(); + datePicker.SelectedDate = DateTime.Today.AddDays(5); + + Assert.ThrowsAny( + () => datePicker.BlackoutDates.Add(new CalendarDateRange(DateTime.Today, DateTime.Today.AddDays(10)))); + } + } + + private static TestServices Services => TestServices.MockThreadingInterface.With( + standardCursorFactory: Mock.Of()); + + private DatePicker CreateControl() + { + var datePicker = + new DatePicker + { + Template = CreateTemplate() + }; + + datePicker.ApplyTemplate(); + return datePicker; + } + + private IControlTemplate CreateTemplate() + { + return new FuncControlTemplate(control => + { + var textBox = + new TextBox + { + Name = "PART_TextBox" + }; + var button = + new Button + { + Name = "PART_Button" + }; + var calendar = + new Calendar + { + Name = "PART_Calendar" + }; + var popup = + new Popup + { + Name = "PART_Popup" + }; + + var panel = new Panel(); + panel.Children.Add(textBox); + panel.Children.Add(button); + panel.Children.Add(popup); + panel.Children.Add(calendar); + + return panel; + }); + + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/GridLengthTests.cs b/tests/Avalonia.Controls.UnitTests/GridLengthTests.cs index 0a811333d4..ab4da0ca7e 100644 --- a/tests/Avalonia.Controls.UnitTests/GridLengthTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridLengthTests.cs @@ -13,7 +13,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Parse_Should_Parse_Auto() { - var result = GridLength.Parse("Auto", CultureInfo.InvariantCulture); + var result = GridLength.Parse("Auto"); Assert.Equal(GridLength.Auto, result); } @@ -21,7 +21,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Parse_Should_Parse_Auto_Lowercase() { - var result = GridLength.Parse("auto", CultureInfo.InvariantCulture); + var result = GridLength.Parse("auto"); Assert.Equal(GridLength.Auto, result); } @@ -29,7 +29,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Parse_Should_Parse_Star() { - var result = GridLength.Parse("*", CultureInfo.InvariantCulture); + var result = GridLength.Parse("*"); Assert.Equal(new GridLength(1, GridUnitType.Star), result); } @@ -37,7 +37,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Parse_Should_Parse_Star_Value() { - var result = GridLength.Parse("2*", CultureInfo.InvariantCulture); + var result = GridLength.Parse("2*"); Assert.Equal(new GridLength(2, GridUnitType.Star), result); } @@ -45,7 +45,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Parse_Should_Parse_Pixel_Value() { - var result = GridLength.Parse("2", CultureInfo.InvariantCulture); + var result = GridLength.Parse("2"); Assert.Equal(new GridLength(2, GridUnitType.Pixel), result); } @@ -53,13 +53,13 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Parse_Should_Throw_FormatException_For_Invalid_String() { - Assert.Throws(() => GridLength.Parse("2x", CultureInfo.InvariantCulture)); + Assert.Throws(() => GridLength.Parse("2x")); } [Fact] public void ParseLengths_Accepts_Comma_Separators() { - var result = GridLength.ParseLengths("*,Auto,2*,4", CultureInfo.InvariantCulture).ToList(); + var result = GridLength.ParseLengths("*,Auto,2*,4").ToList(); Assert.Equal( new[] @@ -75,7 +75,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void ParseLengths_Accepts_Space_Separators() { - var result = GridLength.ParseLengths("* Auto 2* 4", CultureInfo.InvariantCulture).ToList(); + var result = GridLength.ParseLengths("* Auto 2* 4").ToList(); Assert.Equal( new[] @@ -91,7 +91,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void ParseLengths_Accepts_Comma_Separators_With_Spaces() { - var result = GridLength.ParseLengths("*, Auto, 2* ,4", CultureInfo.InvariantCulture).ToList(); + var result = GridLength.ParseLengths("*, Auto, 2* ,4").ToList(); Assert.Equal( new[] diff --git a/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs b/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs index b6ef550da6..d5f9818f89 100644 --- a/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs @@ -30,6 +30,52 @@ namespace Avalonia.Controls.UnitTests new Size(50, 25)); } + [Fact] + public void Measure_On_Skew_X_axis_45_degrees_Is_Correct() + { + TransformMeasureSizeTest( + new Size(100, 100), + new SkewTransform() { AngleX = 45 }, + new Size(200, 100)); + + } + + [Fact] + public void Measure_On_Skew_Y_axis_45_degrees_Is_Correct() + { + TransformMeasureSizeTest( + new Size(100, 100), + new SkewTransform() { AngleY = 45 }, + new Size(100, 200)); + } + + [Fact] + public void Measure_On_Skew_X_axis_minus_45_degrees_Is_Correct() + { + TransformMeasureSizeTest( + new Size(100, 100), + new SkewTransform() { AngleX = -45 }, + new Size(200, 100)); + } + + [Fact] + public void Measure_On_Skew_Y_axis_minus_45_degrees_Is_Correct() + { + TransformMeasureSizeTest( + new Size(100, 100), + new SkewTransform() { AngleY = -45 }, + new Size(100, 200)); + } + + [Fact] + public void Measure_On_Skew_0_degrees_Is_Correct() + { + TransformMeasureSizeTest( + new Size(100, 100), + new SkewTransform() { AngleX = 0, AngleY = 0 }, + new Size(100, 100)); + } + [Fact] public void Measure_On_Rotate_90_degrees_Is_Correct() { @@ -125,7 +171,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Should_Generate_RenderTransform_90_degrees() + public void Should_Generate_RotateTransform_90_degrees() { LayoutTransformControl lt = CreateWithChildAndMeasureAndTransform( 100, @@ -147,7 +193,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Should_Generate_RenderTransform_minus_90_degrees() + public void Should_Generate_RotateTransform_minus_90_degrees() { LayoutTransformControl lt = CreateWithChildAndMeasureAndTransform( 100, @@ -189,6 +235,50 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(m.M32, res.M32, 3); } + [Fact] + public void Should_Generate_SkewTransform_45_degrees() + { + LayoutTransformControl lt = CreateWithChildAndMeasureAndTransform( + 100, + 100, + new SkewTransform() { AngleX = 45, AngleY = 45 }); + + Assert.NotNull(lt.TransformRoot.RenderTransform); + + Matrix m = lt.TransformRoot.RenderTransform.Value; + + Matrix res = Matrix.CreateSkew(Matrix.ToRadians(45), Matrix.ToRadians(45)); + + Assert.Equal(m.M11, res.M11, 3); + Assert.Equal(m.M12, res.M12, 3); + Assert.Equal(m.M21, res.M21, 3); + Assert.Equal(m.M22, res.M22, 3); + Assert.Equal(m.M31, res.M31, 3); + Assert.Equal(m.M32, res.M32, 3); + } + + [Fact] + public void Should_Generate_SkewTransform_minus_45_degrees() + { + LayoutTransformControl lt = CreateWithChildAndMeasureAndTransform( + 100, + 100, + new SkewTransform() { AngleX = -45, AngleY = -45 }); + + Assert.NotNull(lt.TransformRoot.RenderTransform); + + Matrix m = lt.TransformRoot.RenderTransform.Value; + + Matrix res = Matrix.CreateSkew(Matrix.ToRadians(-45), Matrix.ToRadians(-45)); + + Assert.Equal(m.M11, res.M11, 3); + Assert.Equal(m.M12, res.M12, 3); + Assert.Equal(m.M21, res.M21, 3); + Assert.Equal(m.M22, res.M22, 3); + Assert.Equal(m.M31, res.M31, 3); + Assert.Equal(m.M32, res.M32, 3); + } + private static void TransformMeasureSizeTest(Size size, Transform transform, Size expectedSize) { LayoutTransformControl lt = CreateWithChildAndMeasureAndTransform( diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Layout.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Layout.cs index 450b85696e..2c1074aa9a 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Layout.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Layout.cs @@ -80,6 +80,31 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(new Rect(expectedX, expectedY, expectedWidth, expectedHeight), content.Bounds); } + [Fact] + public void Should_Correctly_Align_Child_With_Fixed_Size() + { + Border content; + var target = new ContentPresenter + { + HorizontalContentAlignment = HorizontalAlignment.Stretch, + VerticalContentAlignment = VerticalAlignment.Stretch, + Content = content = new Border + { + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Bottom, + Width = 16, + Height = 16, + }, + }; + + target.UpdateChild(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + // Check correct result for Issue #1447. + Assert.Equal(new Rect(0, 84, 16, 16), content.Bounds); + } + [Fact] public void Content_Can_Be_Stretched() { @@ -187,8 +212,28 @@ namespace Avalonia.Controls.UnitTests.Presenters } [Fact] - public void Padding_Is_Applied_To_TopLeft_Aligned_Content() + public void Child_Arrange_With_Zero_Height_When_Padding_Height_Greater_Than_Child_Height() { + Border content; + var target = new ContentPresenter + { + Padding = new Thickness(32), + MaxHeight = 32, + MaxWidth = 32, + HorizontalContentAlignment = HorizontalAlignment.Center, + VerticalContentAlignment = VerticalAlignment.Center, + Content = content = new Border + { + Height = 0, + Width = 0, + }, + }; + + target.UpdateChild(); + + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(new Rect(48, 48, 0, 0), content.Bounds); } } } \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs index 3c8a692bfb..4501315c94 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs @@ -223,6 +223,26 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(100, child.Bounds.Width); } + [Fact] + public void Extent_Should_Include_Content_Margin() + { + var target = new ScrollContentPresenter + { + Content = new Border + { + Width = 100, + Height = 100, + Margin = new Thickness(5), + } + }; + + target.UpdateChild(); + target.Measure(new Size(50, 50)); + target.Arrange(new Rect(0, 0, 50, 50)); + + Assert.Equal(new Size(110, 110), target.Extent); + } + [Fact] public void Extent_Width_Should_Be_Arrange_Width_When_CanScrollHorizontally_False() { diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index df9ca6b1b1..a85c4df8af 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.UnitTests; @@ -70,7 +71,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void IsVisible_Should_Be_False_Atfer_Hide() + public void IsVisible_Should_Be_False_After_Hide() { using (UnitTestApplication.Start(TestServices.StyledWindow)) { @@ -84,7 +85,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void IsVisible_Should_Be_False_Atfer_Close() + public void IsVisible_Should_Be_False_After_Close() { using (UnitTestApplication.Start(TestServices.StyledWindow)) { @@ -98,7 +99,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void IsVisible_Should_Be_False_Atfer_Impl_Signals_Close() + public void IsVisible_Should_Be_False_After_Impl_Signals_Close() { var windowImpl = new Mock(); windowImpl.SetupProperty(x => x.Closed); @@ -231,11 +232,100 @@ namespace Avalonia.Controls.UnitTests } } - private void ClearOpenWindows() + [Fact] + public async Task ShowDialog_With_ValueType_Returns_Default_When_Closed() { - // HACK: We really need a decent way to have "statics" that can be scoped to - // AvaloniaLocator scopes. - ((IList)Window.OpenWindows).Clear(); + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var windowImpl = new Mock(); + windowImpl.SetupProperty(x => x.Closed); + windowImpl.Setup(x => x.Scaling).Returns(1); + + var target = new Window(windowImpl.Object); + var task = target.ShowDialog(); + + windowImpl.Object.Closed(); + + var result = await task; + Assert.False(result); + } + } + + [Fact] + public void Window_Should_Be_Centered_When_WindowStartupLocation_Is_CenterScreen() + { + var screen1 = new Mock(new Rect(new Size(1920, 1080)), new Rect(new Size(1920, 1040)), true); + var screen2 = new Mock(new Rect(new Size(1366, 768)), new Rect(new Size(1366, 728)), false); + + var screens = new Mock(); + screens.Setup(x => x.AllScreens).Returns(new Screen[] { screen1.Object, screen2.Object }); + + var windowImpl = new Mock(); + windowImpl.SetupProperty(x => x.Position); + windowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480)); + windowImpl.Setup(x => x.Scaling).Returns(1); + windowImpl.Setup(x => x.Screen).Returns(screens.Object); + + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = new Window(windowImpl.Object); + window.WindowStartupLocation = WindowStartupLocation.CenterScreen; + window.Position = new Point(60, 40); + + window.Show(); + + var expectedPosition = new Point( + screen1.Object.WorkingArea.Size.Width / 2 - window.ClientSize.Width / 2, + screen1.Object.WorkingArea.Size.Height / 2 - window.ClientSize.Height / 2); + + Assert.Equal(window.Position, expectedPosition); + } + } + + [Fact] + public void Window_Should_Be_Centered_Relative_To_Owner_When_WindowStartupLocation_Is_CenterOwner() + { + var parentWindowImpl = new Mock(); + parentWindowImpl.SetupProperty(x => x.Position); + parentWindowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480)); + parentWindowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1920, 1080)); + parentWindowImpl.Setup(x => x.Scaling).Returns(1); + + var windowImpl = new Mock(); + windowImpl.SetupProperty(x => x.Position); + windowImpl.Setup(x => x.ClientSize).Returns(new Size(320, 200)); + windowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1920, 1080)); + windowImpl.Setup(x => x.Scaling).Returns(1); + + var parentWindowServices = TestServices.StyledWindow.With( + windowingPlatform: new MockWindowingPlatform(() => parentWindowImpl.Object)); + + var windowServices = TestServices.StyledWindow.With( + windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object)); + + using (UnitTestApplication.Start(parentWindowServices)) + { + var parentWindow = new Window(); + parentWindow.Position = new Point(60, 40); + + parentWindow.Show(); + + using (UnitTestApplication.Start(windowServices)) + { + var window = new Window(); + window.WindowStartupLocation = WindowStartupLocation.CenterOwner; + window.Position = new Point(60, 40); + window.Owner = parentWindow; + + window.Show(); + + var expectedPosition = new Point( + parentWindow.Position.X + parentWindow.ClientSize.Width / 2 - window.ClientSize.Width / 2, + parentWindow.Position.Y + parentWindow.ClientSize.Height / 2 - window.ClientSize.Height / 2); + + Assert.Equal(window.Position, expectedPosition); + } + } } private IWindowImpl CreateImpl(Mock renderer) @@ -244,5 +334,12 @@ namespace Avalonia.Controls.UnitTests x.Scaling == 1 && x.CreateRenderer(It.IsAny()) == renderer.Object); } + + private void ClearOpenWindows() + { + // HACK: We really need a decent way to have "statics" that can be scoped to + // AvaloniaLocator scopes. + ((IList)Window.OpenWindows).Clear(); + } } } diff --git a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs index ad70dcd470..e779652322 100644 --- a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs +++ b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs @@ -80,6 +80,43 @@ namespace Avalonia.Input.UnitTests Assert.Equal(next, result); } + [Fact] + public void Next_Skips_Unfocusable_Siblings() + { + Button current; + Button next; + + var top = new StackPanel + { + Children = + { + new StackPanel + { + Children = + { + new Button { Name = "Button1" }, + new Button { Name = "Button2" }, + new StackPanel + { + Children = + { + (current = new Button { Name = "Button3" }), + } + }, + new TextBlock { Name = "TextBlock" }, + (next = new Button { Name = "Button4" }), + } + }, + new Button { Name = "Button5" }, + new Button { Name = "Button6" }, + } + }; + + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); + + Assert.Equal(next, result); + } + [Fact] public void Next_Continue_Doesnt_Enter_Panel_With_TabNavigation_None() { diff --git a/tests/Avalonia.Layout.UnitTests/ArrangeTests.cs b/tests/Avalonia.Layout.UnitTests/ArrangeTests.cs index df4bb17522..7562b7eff3 100644 --- a/tests/Avalonia.Layout.UnitTests/ArrangeTests.cs +++ b/tests/Avalonia.Layout.UnitTests/ArrangeTests.cs @@ -8,6 +8,22 @@ namespace Avalonia.Layout.UnitTests { public class ArrangeTests { + [Fact] + public void Bounds_Should_Not_Include_Margin() + { + var target = new Decorator + { + Width = 100, + Height = 100, + Margin = new Thickness(5), + }; + + Assert.False(target.IsMeasureValid); + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + Assert.Equal(new Rect(5, 5, 100, 100), target.Bounds); + } + [Fact] public void Margin_Should_Be_Subtracted_From_Arrange_FinalSize() { diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs index f5c0c6ec15..f42e0daf2a 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs @@ -58,7 +58,7 @@ namespace Avalonia.Markup.UnitTests.Data [Fact] public async Task Should_Convert_Get_String_To_Double() { - var data = new Class1 { StringValue = "5.6" }; + var data = new Class1 { StringValue = $"{5.6}" }; var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double)); var result = await target.Take(1); @@ -94,12 +94,12 @@ namespace Avalonia.Markup.UnitTests.Data [Fact] public void Should_Convert_Set_String_To_Double() { - var data = new Class1 { StringValue = (5.6).ToString() }; + var data = new Class1 { StringValue = $"{5.6}" }; var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double)); target.OnNext(6.7); - Assert.Equal((6.7).ToString(), data.StringValue); + Assert.Equal($"{6.7}", data.StringValue); GC.KeepAlive(data); } @@ -111,7 +111,7 @@ namespace Avalonia.Markup.UnitTests.Data var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string)); var result = await target.Take(1); - Assert.Equal((5.6).ToString(), result); + Assert.Equal($"{5.6}", result); GC.KeepAlive(data); } @@ -122,7 +122,7 @@ namespace Avalonia.Markup.UnitTests.Data var data = new Class1 { DoubleValue = 5.6 }; var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string)); - target.OnNext("6.7"); + target.OnNext($"{6.7}"); Assert.Equal(6.7, data.DoubleValue); @@ -318,15 +318,15 @@ namespace Avalonia.Markup.UnitTests.Data target.Subscribe(x => result.Add(x)); target.OnNext(1.2); - target.OnNext("3.4"); + target.OnNext($"{3.4}"); target.OnNext("bar"); Assert.Equal( new[] { - new BindingNotification("5.6"), - new BindingNotification("1.2"), - new BindingNotification("3.4"), + new BindingNotification($"{5.6}"), + new BindingNotification($"{1.2}"), + new BindingNotification($"{3.4}"), new BindingNotification( new InvalidCastException("'bar' is not a valid number."), BindingErrorType.Error) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs new file mode 100644 index 0000000000..4b0bee00f3 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.Styling; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions +{ + public class BindingExtensionTests + { + + [Fact] + public void BindingExtension_Binds_To_Source() + { + using (StyledWindow()) + { + var xaml = @" + + + foobar + + + +"; + + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + window.Show(); + + Assert.Equal("foobar", textBlock.Text); + } + } + + private IDisposable StyledWindow(params (string, string)[] assets) + { + var services = TestServices.StyledWindow.With( + assetLoader: new MockAssetLoader(assets), + theme: () => new Styles + { + WindowStyle(), + }); + + return UnitTestApplication.Start(services); + } + + private Style WindowStyle() + { + return new Style(x => x.OfType()) + { + Setters = + { + new Setter( + Window.TemplateProperty, + new FuncControlTemplate(x => + new ContentPresenter + { + Name = "PART_ContentPresenter", + [!ContentPresenter.ContentProperty] = x[!Window.ContentProperty], + })) + } + }; + } + } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs index 862ce2b3c0..8615efd967 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs @@ -323,7 +323,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions Assert.Equal(0xff506070, brush.Color.ToUint32()); } - [Fact(Skip = "Not yet supported by Portable.Xaml")] + [Fact] public void StaticResource_Can_Be_Assigned_To_Property_In_ControlTemplate_In_Styles_File() { var styleXaml = @" @@ -361,9 +361,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions var border = (Border)button.GetVisualChildren().Single(); var brush = (SolidColorBrush)border.Background; - - // To make this work we somehow need to be able to get hold of the parent ambient - // context from Portable.Xaml. See TODO in StaticResourceExtension. + Assert.Equal(0xff506070, brush.Color.ToUint32()); } } @@ -417,6 +415,79 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + [Fact] + public void StaticResource_Can_Be_Assigned_To_Binding_Converter_In_DataTemplate() + { + using (StyledWindow()) + { + var xaml = @" + + + + + + + + + +"; + + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + + window.DataContext = "foo"; + var presenter = window.FindControl("presenter"); + + window.Show(); + + var textBlock = (TextBlock)presenter.GetVisualChildren().Single(); + + Assert.NotNull(textBlock); + Assert.Equal("foobar", textBlock.Text); + } + } + + [Fact] + public void StaticResource_Is_Correctly_Chosen_From_Within_DataTemplate() + { + // this tests if IAmbientProviders in DataTemplate contexts are in correct order + // if they wouldn't be, Purple brush would be bound to + using (StyledWindow()) + { + var xaml = @" + + + + + + + + + + + + + + + +"; + + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + + window.Show(); + + var textBlock = window.GetVisualDescendants().OfType().Single(); + + Assert.NotNull(textBlock); + Assert.Equal("White-bar", textBlock.Text); + } + } + [Fact] public void Control_Property_Is_Not_Updated_When_Parent_Is_Changed() { diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/AttachedPropertyOwner.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/AttachedPropertyOwner.cs new file mode 100644 index 0000000000..aac5b01f96 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/AttachedPropertyOwner.cs @@ -0,0 +1,14 @@ +using System; +using Avalonia.Controls; + +namespace Avalonia.Markup.Xaml.UnitTests.Xaml +{ + public class AttachedPropertyOwner + { + public static readonly AttachedProperty DoubleProperty = + AvaloniaProperty.RegisterAttached("Double"); + + public static double GetDouble(Control control) => control.GetValue(DoubleProperty); + public static void SetDouble(Control control, double value) => control.SetValue(DoubleProperty, value); + } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index fb432c30d4..ac25b7ccbe 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs @@ -8,8 +8,10 @@ using Avalonia.Markup.Xaml.Data; using Avalonia.Markup.Xaml.Styling; using Avalonia.Markup.Xaml.Templates; using Avalonia.Media; +using Avalonia.Media.Immutable; using Avalonia.Styling; using Avalonia.UnitTests; +using Portable.Xaml; using System.Collections; using System.ComponentModel; using System.Linq; @@ -124,6 +126,24 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal("Foo", ToolTip.GetTip(target)); } + [Fact] + public void NonExistent_Property_Throws() + { + var xaml = + @""; + + Assert.Throws(() => AvaloniaXamlLoader.Parse(xaml)); + } + + [Fact] + public void Non_Attached_Property_With_Attached_Property_Syntax_Throws() + { + var xaml = + @""; + + Assert.Throws(() => AvaloniaXamlLoader.Parse(xaml)); + } + [Fact] public void ContentControl_ContentTemplate_Is_Functional() { @@ -359,8 +379,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml var control = AvaloniaXamlLoader.Parse(xaml); var bk = control.Background; - Assert.IsType(bk); - Assert.Equal(Colors.White, (bk as SolidColorBrush).Color); + Assert.IsType(bk); + Assert.Equal(Colors.White, (bk as ISolidColorBrush).Color); } [Fact] @@ -496,7 +516,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.NotNull(brush); - Assert.Equal(Colors.White, ((SolidColorBrush)brush).Color); + Assert.Equal(Colors.White, ((ISolidColorBrush)brush).Color); style.TryGetResource("Double", out var d); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs index a44d09dee7..568c6482f5 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs @@ -215,5 +215,71 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal("bar", textBlock.Text); } } + + [Fact] + public void Binding_To_Namespaced_Attached_Property_Works() + { + using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = (TextBlock)window.Content; + + window.DataContext = 5.6; + window.ApplyTemplate(); + + Assert.Equal(5.6, AttachedPropertyOwner.GetDouble(textBlock)); + } + } + + [Fact] + public void Binding_To_AddOwnered_Attached_Property_Works() + { + using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var testControl = (TestControl)window.Content; + + window.DataContext = 5.6; + window.ApplyTemplate(); + + Assert.Equal(5.6, testControl.Double); + } + } + + [Fact] + public void Binding_To_Attached_Property_Using_AddOwnered_Type_Works() + { + using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = (TextBlock)window.Content; + + window.DataContext = 5.6; + window.ApplyTemplate(); + + Assert.Equal(5.6, AttachedPropertyOwner.GetDouble(textBlock)); + } + } } } \ No newline at end of file diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TestControl.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TestControl.cs new file mode 100644 index 0000000000..d0591e5ee6 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TestControl.cs @@ -0,0 +1,17 @@ +using System; +using Avalonia.Controls; + +namespace Avalonia.Markup.Xaml.UnitTests.Xaml +{ + public class TestControl : Control + { + public static readonly StyledProperty DoubleProperty = + AttachedPropertyOwner.DoubleProperty.AddOwner(); + + public double Double + { + get => GetValue(DoubleProperty); + set => SetValue(DoubleProperty, value); + } + } +} diff --git a/tests/Avalonia.RenderTests/Controls/BorderTests.cs b/tests/Avalonia.RenderTests/Controls/BorderTests.cs index 3bd5a6e1cb..7d2e40c3b4 100644 --- a/tests/Avalonia.RenderTests/Controls/BorderTests.cs +++ b/tests/Avalonia.RenderTests/Controls/BorderTests.cs @@ -31,7 +31,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls Child = new Border { BorderBrush = Brushes.Black, - BorderThickness = 1, + BorderThickness = new Thickness(1), } }; @@ -50,7 +50,47 @@ namespace Avalonia.Direct2D1.RenderTests.Controls Child = new Border { BorderBrush = Brushes.Black, - BorderThickness = 2, + BorderThickness = new Thickness(2), + } + }; + + await RenderToFile(target); + CompareImages(); + } + + [Fact] + public async Task Border_Uniform_CornerRadius() + { + Decorator target = new Decorator + { + Padding = new Thickness(8), + Width = 200, + Height = 200, + Child = new Border + { + BorderBrush = Brushes.Black, + BorderThickness = new Thickness(2), + CornerRadius = new CornerRadius(16), + } + }; + + await RenderToFile(target); + CompareImages(); + } + + [Fact] + public async Task Border_NonUniform_CornerRadius() + { + Decorator target = new Decorator + { + Padding = new Thickness(8), + Width = 200, + Height = 200, + Child = new Border + { + BorderBrush = Brushes.Black, + BorderThickness = new Thickness(2), + CornerRadius = new CornerRadius(16, 4, 7, 10), } }; @@ -87,7 +127,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls Child = new Border { BorderBrush = Brushes.Black, - BorderThickness = 2, + BorderThickness = new Thickness(2), Child = new Border { Background = Brushes.Red, @@ -110,7 +150,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls Child = new Border { BorderBrush = Brushes.Black, - BorderThickness = 2, + BorderThickness = new Thickness(2), Padding = new Thickness(2), Child = new Border { @@ -134,7 +174,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls Child = new Border { BorderBrush = Brushes.Black, - BorderThickness = 2, + BorderThickness = new Thickness(2), Child = new Border { Background = Brushes.Red, @@ -159,7 +199,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls Child = new Border { BorderBrush = Brushes.Black, - BorderThickness = 2, + BorderThickness = new Thickness(2), Child = new TextBlock { Text = "Foo", @@ -186,7 +226,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls Child = new Border { BorderBrush = Brushes.Black, - BorderThickness = 2, + BorderThickness = new Thickness(2), Child = new TextBlock { Text = "Foo", @@ -213,7 +253,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls Child = new Border { BorderBrush = Brushes.Black, - BorderThickness = 2, + BorderThickness = new Thickness(2), Child = new TextBlock { Text = "Foo", @@ -240,7 +280,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls Child = new Border { BorderBrush = Brushes.Black, - BorderThickness = 2, + BorderThickness = new Thickness(2), Child = new TextBlock { Text = "Foo", @@ -267,7 +307,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls Child = new Border { BorderBrush = Brushes.Black, - BorderThickness = 2, + BorderThickness = new Thickness(2), Child = new TextBlock { Text = "Foo", @@ -294,7 +334,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls Child = new Border { BorderBrush = Brushes.Black, - BorderThickness = 2, + BorderThickness = new Thickness(2), Child = new TextBlock { Text = "Foo", @@ -321,7 +361,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls Child = new Border { BorderBrush = Brushes.Black, - BorderThickness = 2, + BorderThickness = new Thickness(2), Child = new TextBlock { Text = "Foo", @@ -348,7 +388,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls Child = new Border { BorderBrush = Brushes.Black, - BorderThickness = 2, + BorderThickness = new Thickness(2), Child = new TextBlock { Text = "Foo", diff --git a/tests/Avalonia.RenderTests/Media/BitmapTests.cs b/tests/Avalonia.RenderTests/Media/BitmapTests.cs index a7cd06a894..089579a0a0 100644 --- a/tests/Avalonia.RenderTests/Media/BitmapTests.cs +++ b/tests/Avalonia.RenderTests/Media/BitmapTests.cs @@ -106,9 +106,9 @@ namespace Avalonia.Direct2D1.RenderTests.Media [Theory] [InlineData(PixelFormat.Bgra8888), InlineData(PixelFormat.Rgba8888)] - public void WritableBitmapShouldBeUsable(PixelFormat fmt) + public void WriteableBitmapShouldBeUsable(PixelFormat fmt) { - var writableBitmap = new WritableBitmap(256, 256, fmt); + var writeableBitmap = new WriteableBitmap(256, 256, fmt); var data = new int[256 * 256]; for (int y = 0; y < 256; y++) @@ -116,7 +116,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media data[y * 256 + x] =(int)((uint)(x + (y << 8)) | 0xFF000000u); - using (var l = writableBitmap.Lock()) + using (var l = writeableBitmap.Lock()) { for(var r = 0; r<256; r++) { @@ -125,9 +125,9 @@ namespace Avalonia.Direct2D1.RenderTests.Media } - var name = nameof(WritableBitmapShouldBeUsable) + "_" + fmt; + var name = nameof(WriteableBitmapShouldBeUsable) + "_" + fmt; - writableBitmap.Save(System.IO.Path.Combine(OutputPath, name + ".out.png")); + writeableBitmap.Save(System.IO.Path.Combine(OutputPath, name + ".out.png")); CompareImagesNoRenderer(name); } diff --git a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs b/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs index 099b022862..cfa15ae304 100644 --- a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs @@ -42,7 +42,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media new Border { BorderBrush = Brushes.Blue, - BorderThickness = 2, + BorderThickness = new Thickness(2), HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, Child = new TextBlock diff --git a/tests/Avalonia.RenderTests/Shapes/PathTests.cs b/tests/Avalonia.RenderTests/Shapes/PathTests.cs index fab867f428..4703daca25 100644 --- a/tests/Avalonia.RenderTests/Shapes/PathTests.cs +++ b/tests/Avalonia.RenderTests/Shapes/PathTests.cs @@ -316,7 +316,7 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes Child = new Border { BorderBrush = Brushes.Red, - BorderThickness = 1, + BorderThickness = new Thickness(1), HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, Child = new Path diff --git a/tests/Avalonia.Styling.UnitTests/StyleTests.cs b/tests/Avalonia.Styling.UnitTests/StyleTests.cs index a7c559668b..5ef559b887 100644 --- a/tests/Avalonia.Styling.UnitTests/StyleTests.cs +++ b/tests/Avalonia.Styling.UnitTests/StyleTests.cs @@ -151,7 +151,7 @@ namespace Avalonia.Styling.UnitTests { Setters = new[] { - new Setter(Border.BorderThicknessProperty, 4), + new Setter(Border.BorderThicknessProperty, new Thickness(4)), } }; @@ -162,9 +162,9 @@ namespace Avalonia.Styling.UnitTests style.Attach(border, null); - Assert.Equal(4, border.BorderThickness); + Assert.Equal(new Thickness(4), border.BorderThickness); root.Child = null; - Assert.Equal(0, border.BorderThickness); + Assert.Equal(new Thickness(0), border.BorderThickness); } private class Class1 : Control diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index 8c6c949e07..de2b517956 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -39,7 +39,7 @@ namespace Avalonia.UnitTests return new MockStreamGeometryImpl(); } - public IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? format = default(PixelFormat?)) + public IWriteableBitmapImpl CreateWriteableBitmap(int width, int height, PixelFormat? format = default(PixelFormat?)) { throw new NotImplementedException(); } diff --git a/tests/Avalonia.Visuals.UnitTests/CornerRadiusTests.cs b/tests/Avalonia.Visuals.UnitTests/CornerRadiusTests.cs new file mode 100644 index 0000000000..56f9907409 --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/CornerRadiusTests.cs @@ -0,0 +1,43 @@ +// 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.Globalization; +using Xunit; + +namespace Avalonia.Visuals.UnitTests +{ + public class CornerRadiusTests + { + [Fact] + public void Parse_Parses_Single_Uniform_Radius() + { + var result = CornerRadius.Parse("3.4"); + + Assert.Equal(new CornerRadius(3.4), result); + } + + [Fact] + public void Parse_Parses_Top_Bottom() + { + var result = CornerRadius.Parse("1.1,2.2"); + + Assert.Equal(new CornerRadius(1.1, 2.2), result); + } + + [Fact] + public void Parse_Parses_TopLeft_TopRight_BottomRight_BottomLeft() + { + var result = CornerRadius.Parse("1.1,2.2,3.3,4.4"); + + Assert.Equal(new CornerRadius(1.1, 2.2, 3.3, 4.4), result); + } + + [Fact] + public void Parse_Accepts_Spaces() + { + var result = CornerRadius.Parse("1.1 2.2 3.3 4.4"); + + Assert.Equal(new CornerRadius(1.1, 2.2, 3.3, 4.4), result); + } + } +} \ No newline at end of file diff --git a/tests/Avalonia.Visuals.UnitTests/Media/BrushTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/BrushTests.cs index ae88a94073..a6015c52e5 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/BrushTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/BrushTests.cs @@ -12,7 +12,7 @@ namespace Avalonia.Visuals.UnitTests.Media [Fact] public void Parse_Parses_RGB_Hash_Brush() { - var result = (SolidColorBrush)Brush.Parse("#ff8844"); + var result = (ISolidColorBrush)Brush.Parse("#ff8844"); Assert.Equal(0xff, result.Color.R); Assert.Equal(0x88, result.Color.G); @@ -23,7 +23,7 @@ namespace Avalonia.Visuals.UnitTests.Media [Fact] public void Parse_Parses_ARGB_Hash_Brush() { - var result = (SolidColorBrush)Brush.Parse("#40ff8844"); + var result = (ISolidColorBrush)Brush.Parse("#40ff8844"); Assert.Equal(0xff, result.Color.R); Assert.Equal(0x88, result.Color.G); @@ -34,7 +34,7 @@ namespace Avalonia.Visuals.UnitTests.Media [Fact] public void Parse_Parses_Named_Brush_Lowercase() { - var result = (SolidColorBrush)Brush.Parse("red"); + var result = (ISolidColorBrush)Brush.Parse("red"); Assert.Equal(0xff, result.Color.R); Assert.Equal(0x00, result.Color.G); @@ -45,7 +45,7 @@ namespace Avalonia.Visuals.UnitTests.Media [Fact] public void Parse_Parses_Named_Brush_Uppercase() { - var result = (SolidColorBrush)Brush.Parse("RED"); + var result = (ISolidColorBrush)Brush.Parse("RED"); Assert.Equal(0xff, result.Color.R); Assert.Equal(0x00, result.Color.G); @@ -53,6 +53,16 @@ namespace Avalonia.Visuals.UnitTests.Media Assert.Equal(0xff, result.Color.A); } + [Fact] + public void Parse_ToString_Named_Brush_Roundtrip() + { + const string expectedName = "Red"; + var brush = (ISolidColorBrush)Brush.Parse(expectedName); + var name = brush.ToString(); + + Assert.Equal(expectedName, name); + } + [Fact] public void Parse_Hex_Value_Doesnt_Accept_Too_Few_Chars() { diff --git a/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs index 4c1e361952..ff1d17164e 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs @@ -8,7 +8,7 @@ namespace Avalonia.Visuals.UnitTests.Media [Fact] public void Parse_Parses() { - var matrix = Matrix.Parse("1,2,3,-4,5 6", CultureInfo.CurrentCulture); + var matrix = Matrix.Parse("1,2,3,-4,5 6"); var expected = new Matrix(1, 2, 3, -4, 5, 6); Assert.Equal(expected, matrix); } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/RectTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/RectTests.cs index 12070bfed3..cd0c7e1ace 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/RectTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/RectTests.cs @@ -8,7 +8,7 @@ namespace Avalonia.Visuals.UnitTests.Media [Fact] public void Parse_Parses() { - var rect = Rect.Parse("1,2 3,-4", CultureInfo.CurrentCulture); + var rect = Rect.Parse("1,2 3,-4"); var expected = new Rect(1, 2, 3, -4); Assert.Equal(expected, rect); } diff --git a/tests/Avalonia.Visuals.UnitTests/RelativePointTests.cs b/tests/Avalonia.Visuals.UnitTests/RelativePointTests.cs index f4a21fb6b4..b9eecc809f 100644 --- a/tests/Avalonia.Visuals.UnitTests/RelativePointTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/RelativePointTests.cs @@ -11,7 +11,7 @@ namespace Avalonia.Visuals.UnitTests [Fact] public void Parse_Should_Accept_Absolute_Value() { - var result = RelativePoint.Parse("4,5", CultureInfo.InvariantCulture); + var result = RelativePoint.Parse("4,5"); Assert.Equal(new RelativePoint(4, 5, RelativeUnit.Absolute), result); } @@ -19,7 +19,7 @@ namespace Avalonia.Visuals.UnitTests [Fact] public void Parse_Should_Accept_Relative_Value() { - var result = RelativePoint.Parse("25%, 50%", CultureInfo.InvariantCulture); + var result = RelativePoint.Parse("25%, 50%"); Assert.Equal(new RelativePoint(0.25, 0.5, RelativeUnit.Relative), result); } diff --git a/tests/Avalonia.Visuals.UnitTests/RelativeRectTests.cs b/tests/Avalonia.Visuals.UnitTests/RelativeRectTests.cs index 8ba4f3b739..68a0df2d9b 100644 --- a/tests/Avalonia.Visuals.UnitTests/RelativeRectTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/RelativeRectTests.cs @@ -1,6 +1,7 @@ // 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.Globalization; using Xunit; @@ -13,7 +14,7 @@ namespace Avalonia.Visuals.UnitTests [Fact] public void Parse_Should_Accept_Absolute_Value() { - var result = RelativeRect.Parse("4,5,50,60", CultureInfo.InvariantCulture); + var result = RelativeRect.Parse("4,5,50,60"); Assert.Equal(new RelativeRect(4, 5, 50, 60, RelativeUnit.Absolute), result, Compare); } @@ -21,9 +22,16 @@ namespace Avalonia.Visuals.UnitTests [Fact] public void Parse_Should_Accept_Relative_Value() { - var result = RelativeRect.Parse("10%, 20%, 40%, 70%", CultureInfo.InvariantCulture); + var result = RelativeRect.Parse("10%, 20%, 40%, 70%"); Assert.Equal(new RelativeRect(0.1, 0.2, 0.4, 0.7, RelativeUnit.Relative), result, Compare); } + + [Fact] + public void Parse_Should_Throw_Mixed_Values() + { + Assert.Throws(() => + RelativeRect.Parse("10%, 20%, 40, 70%")); + } } } diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs index dda1d73649..df4584518e 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs @@ -83,6 +83,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph Margin = new Thickness(10, 20, 30, 40), Child = canvas = new Canvas { + ClipToBounds = true, Background = Brushes.AliceBlue, } } @@ -129,6 +130,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph (border = new Border { Background = Brushes.AliceBlue, + ClipToBounds = true, Width = 100, Height = 100, [Canvas.LeftProperty] = 50, @@ -173,6 +175,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph (border = new Border { Background = Brushes.AliceBlue, + ClipToBounds = true, Width = 100, Height = 100, [Canvas.LeftProperty] = 50, @@ -254,6 +257,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph Margin = new Thickness(24, 26), Child = target = new Border { + ClipToBounds = true, Margin = new Thickness(26, 24), Width = 100, Height = 100, @@ -515,6 +519,50 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph } } + [Fact] + public void Should_Update_ClipBounds_For_Negative_Margin() + { + using (TestApplication()) + { + Decorator decorator; + Border border; + var tree = new TestRoot + { + Width = 100, + Height = 100, + Child = decorator = new Decorator + { + Margin = new Thickness(0, 10, 0, 0), + Child = border = new Border + { + Background = Brushes.Red, + ClipToBounds = true, + Margin = new Thickness(0, -5, 0, 0), + } + } + }; + + var layout = AvaloniaLocator.Current.GetService(); + layout.ExecuteInitialLayoutPass(tree); + + var scene = new Scene(tree); + var sceneBuilder = new SceneBuilder(); + sceneBuilder.UpdateAll(scene); + + var borderNode = scene.FindNode(border); + Assert.Equal(new Rect(0, 5, 100, 95), borderNode.ClipBounds); + + border.Margin = new Thickness(0, -8, 0, 0); + layout.ExecuteLayoutPass(); + + scene = scene.CloneScene(); + sceneBuilder.Update(scene, border); + + borderNode = scene.FindNode(border); + Assert.Equal(new Rect(0, 2, 100, 98), borderNode.ClipBounds); + } + } + [Fact] public void Should_Update_Descendent_Tranform_When_Margin_Changed() { @@ -656,6 +704,43 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph } } + [Fact] + public void Setting_Opacity_Should_Add_Descendent_Bounds_To_DirtyRects() + { + using (TestApplication()) + { + Decorator decorator; + Border border; + var tree = new TestRoot + { + Child = decorator = new Decorator + { + Child = border = new Border + { + Background = Brushes.Red, + Width = 100, + Height = 100, + } + } + }; + + tree.Measure(Size.Infinity); + tree.Arrange(new Rect(tree.DesiredSize)); + + var scene = new Scene(tree); + var sceneBuilder = new SceneBuilder(); + sceneBuilder.UpdateAll(scene); + + decorator.Opacity = 0.5; + scene = scene.CloneScene(); + sceneBuilder.Update(scene, decorator); + + Assert.NotEmpty(scene.Layers.Single().Dirty); + var dirty = scene.Layers.Single().Dirty.Single(); + Assert.Equal(new Rect(0, 0, 100, 100), dirty); + } + } + [Fact] public void Should_Set_GeometryClip() { diff --git a/tests/Avalonia.Visuals.UnitTests/ThicknessTests.cs b/tests/Avalonia.Visuals.UnitTests/ThicknessTests.cs index bd694d073a..ac4c6bc781 100644 --- a/tests/Avalonia.Visuals.UnitTests/ThicknessTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/ThicknessTests.cs @@ -4,14 +4,14 @@ using System.Globalization; using Xunit; -namespace Avalonia.Visuals.UnitTests.Media +namespace Avalonia.Visuals.UnitTests { public class ThicknessTests { [Fact] public void Parse_Parses_Single_Uniform_Size() { - var result = Thickness.Parse("1.2", CultureInfo.InvariantCulture); + var result = Thickness.Parse("1.2"); Assert.Equal(new Thickness(1.2), result); } @@ -19,7 +19,7 @@ namespace Avalonia.Visuals.UnitTests.Media [Fact] public void Parse_Parses_Horizontal_Vertical() { - var result = Thickness.Parse("1.2,3.4", CultureInfo.InvariantCulture); + var result = Thickness.Parse("1.2,3.4"); Assert.Equal(new Thickness(1.2, 3.4), result); } @@ -27,7 +27,7 @@ namespace Avalonia.Visuals.UnitTests.Media [Fact] public void Parse_Parses_Left_Top_Right_Bottom() { - var result = Thickness.Parse("1.2, 3.4, 5, 6", CultureInfo.InvariantCulture); + var result = Thickness.Parse("1.2, 3.4, 5, 6"); Assert.Equal(new Thickness(1.2, 3.4, 5, 6), result); } @@ -35,9 +35,9 @@ namespace Avalonia.Visuals.UnitTests.Media [Fact] public void Parse_Accepts_Spaces() { - var result = Thickness.Parse("1.2 3.4 5 6", CultureInfo.InvariantCulture); + var result = Thickness.Parse("1.2 3.4 5 6"); Assert.Equal(new Thickness(1.2, 3.4, 5, 6), result); } } -} +} \ No newline at end of file diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs index 54bb5d72d0..93b5a8a764 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs @@ -49,7 +49,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree throw new NotImplementedException(); } - public IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? fmt) + public IWriteableBitmapImpl CreateWriteableBitmap(int width, int height, PixelFormat? fmt) { throw new NotImplementedException(); } diff --git a/tests/TestFiles/Direct2D1/Controls/Border/Border_NonUniform_CornerRadius.expected.png b/tests/TestFiles/Direct2D1/Controls/Border/Border_NonUniform_CornerRadius.expected.png new file mode 100644 index 0000000000..9deb45aaeb Binary files /dev/null and b/tests/TestFiles/Direct2D1/Controls/Border/Border_NonUniform_CornerRadius.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Controls/Border/Border_Uniform_CornerRadius.expected.png b/tests/TestFiles/Direct2D1/Controls/Border/Border_Uniform_CornerRadius.expected.png new file mode 100644 index 0000000000..a4bfa75eb8 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Controls/Border/Border_Uniform_CornerRadius.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/Bitmap/WritableBitmapShouldBeUsable_Bgra8888.expected.png b/tests/TestFiles/Direct2D1/Media/Bitmap/WriteableBitmapShouldBeUsable_Bgra8888.expected.png similarity index 100% rename from tests/TestFiles/Direct2D1/Media/Bitmap/WritableBitmapShouldBeUsable_Bgra8888.expected.png rename to tests/TestFiles/Direct2D1/Media/Bitmap/WriteableBitmapShouldBeUsable_Bgra8888.expected.png diff --git a/tests/TestFiles/Direct2D1/Media/Bitmap/WritableBitmapShouldBeUsable_Rgba8888.expected.png b/tests/TestFiles/Direct2D1/Media/Bitmap/WriteableBitmapShouldBeUsable_Rgba8888.expected.png similarity index 100% rename from tests/TestFiles/Direct2D1/Media/Bitmap/WritableBitmapShouldBeUsable_Rgba8888.expected.png rename to tests/TestFiles/Direct2D1/Media/Bitmap/WriteableBitmapShouldBeUsable_Rgba8888.expected.png diff --git a/tests/TestFiles/Skia/Controls/Border/Border_NonUniform_CornerRadius.expected.png b/tests/TestFiles/Skia/Controls/Border/Border_NonUniform_CornerRadius.expected.png new file mode 100644 index 0000000000..9deb45aaeb Binary files /dev/null and b/tests/TestFiles/Skia/Controls/Border/Border_NonUniform_CornerRadius.expected.png differ diff --git a/tests/TestFiles/Skia/Controls/Border/Border_Uniform_CornerRadius.expected.png b/tests/TestFiles/Skia/Controls/Border/Border_Uniform_CornerRadius.expected.png new file mode 100644 index 0000000000..a4bfa75eb8 Binary files /dev/null and b/tests/TestFiles/Skia/Controls/Border/Border_Uniform_CornerRadius.expected.png differ diff --git a/tests/TestFiles/Skia/Media/Bitmap/WritableBitmapShouldBeUsable_Bgra8888.expected.png b/tests/TestFiles/Skia/Media/Bitmap/WriteableBitmapShouldBeUsable_Bgra8888.expected.png similarity index 100% rename from tests/TestFiles/Skia/Media/Bitmap/WritableBitmapShouldBeUsable_Bgra8888.expected.png rename to tests/TestFiles/Skia/Media/Bitmap/WriteableBitmapShouldBeUsable_Bgra8888.expected.png diff --git a/tests/TestFiles/Skia/Media/Bitmap/WritableBitmapShouldBeUsable_Rgba8888.expected.png b/tests/TestFiles/Skia/Media/Bitmap/WriteableBitmapShouldBeUsable_Rgba8888.expected.png similarity index 100% rename from tests/TestFiles/Skia/Media/Bitmap/WritableBitmapShouldBeUsable_Rgba8888.expected.png rename to tests/TestFiles/Skia/Media/Bitmap/WriteableBitmapShouldBeUsable_Rgba8888.expected.png