diff --git a/.gitignore b/.gitignore index 1d74816204..5fa78979c3 100644 --- a/.gitignore +++ b/.gitignore @@ -159,6 +159,12 @@ $RECYCLE.BIN/ *.userprefs *.nugetreferenceswitcher + +################# +## Rider +################# +.idea + ################# ## Cake ################# diff --git a/.gitmodules b/.gitmodules index f446fe421e..98b6d076c1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,3 @@ -[submodule "src/Avalonia.ReactiveUI/src"] - path = src/Avalonia.ReactiveUI/src - url = https://github.com/AvaloniaUI/ReactiveUI.git - branch = avalonia-snapshot [submodule "src/Avalonia.HtmlRenderer/external"] path = src/Avalonia.HtmlRenderer/external url = https://github.com/AvaloniaUI/HTML-Renderer.git diff --git a/.ncrunch/Avalonia.Direct2D1.RenderTests.v3.ncrunchproject b/.ncrunch/Avalonia.Direct2D1.RenderTests.v3.ncrunchproject index 235da29767..a8c3abe8f2 100644 --- a/.ncrunch/Avalonia.Direct2D1.RenderTests.v3.ncrunchproject +++ b/.ncrunch/Avalonia.Direct2D1.RenderTests.v3.ncrunchproject @@ -1,6 +1,7 @@  1000 + True True \ No newline at end of file diff --git a/.ncrunch/Avalonia.Markup.UnitTests.netcoreapp1.1.v3.ncrunchproject b/.ncrunch/Avalonia.Markup.UnitTests.netcoreapp1.1.v3.ncrunchproject new file mode 100644 index 0000000000..15d9efad87 --- /dev/null +++ b/.ncrunch/Avalonia.Markup.UnitTests.netcoreapp1.1.v3.ncrunchproject @@ -0,0 +1,9 @@ + + + + + Avalonia.Markup.UnitTests.Data.Plugins.DataAnnotationsValidationPluginTests.Produces_Aggregate_BindingNotificationsx + + + + \ No newline at end of file diff --git a/.ncrunch/Avalonia.Skia.RenderTests.v3.ncrunchproject b/.ncrunch/Avalonia.Skia.RenderTests.v3.ncrunchproject index 235da29767..a8c3abe8f2 100644 --- a/.ncrunch/Avalonia.Skia.RenderTests.v3.ncrunchproject +++ b/.ncrunch/Avalonia.Skia.RenderTests.v3.ncrunchproject @@ -1,6 +1,7 @@  1000 + True True \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index a3c02eea58..52a400d19a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,13 +3,14 @@ os: - linux - osx dist: trusty +osx_image: xcode8.3 env: global: - DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 - DOTNET_CLI_TELEMETRY_OPTOUT=1 mono: - latest -dotnet: 1.0.1 +dotnet: 2.0.0 script: - ./build.sh --target "Travis" --platform "Mono" --configuration "Release" notifications: diff --git a/Avalonia.sln b/Avalonia.sln index 391d02382e..39d44cb172 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26228.4 +VisualStudioVersion = 15.0.26430.16 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 @@ -159,6 +159,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\Microsoft.Reactive.Testing.props = build\Microsoft.Reactive.Testing.props build\Moq.props = build\Moq.props build\NetCore.props = build\NetCore.props + build\ReactiveUI.props = build\ReactiveUI.props build\Rx.props = build\Rx.props build\Serilog.props = build\Serilog.props build\Serilog.Sinks.Trace.props = build\Serilog.Sinks.Trace.props diff --git a/appveyor.yml b/appveyor.yml index 7457a1d5bb..ca548b6b0a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -17,9 +17,9 @@ init: - ps: if (Test-Path env:nuget_address) {[System.IO.File]::AppendAllText("C:\Windows\System32\drivers\etc\hosts", "`n$($env:nuget_address)`tapi.nuget.org")} install: - if not exist gtk-sharp-2.12.26.msi appveyor DownloadFile http://download.xamarin.com/GTKforWindows/Windows/gtk-sharp-2.12.26.msi - - if not exist dotnet-1.0.1.exe appveyor DownloadFile https://go.microsoft.com/fwlink/?linkid=843448 -FileName "dotnet-1.0.1.exe" + - if not exist dotnet-2.0.0.exe appveyor DownloadFile https://download.microsoft.com/download/0/F/D/0FD852A4-7EA1-4E2A-983A-0484AC19B92C/dotnet-sdk-2.0.0-win-x64.exe -FileName "dotnet-2.0.0.exe" - ps: Start-Process -FilePath "msiexec" -ArgumentList "/i gtk-sharp-2.12.26.msi /quiet /qn /norestart" -Wait - - ps: Start-Process -FilePath "dotnet-1.0.1.exe" -ArgumentList "/quiet" -Wait + - ps: Start-Process -FilePath "dotnet-2.0.0.exe" -ArgumentList "/quiet" -Wait - cmd: set PATH=%programfiles(x86)%\GtkSharp\2.12\bin\;%PATH% before_build: - git submodule update --init diff --git a/build.cake b/build.cake index 4e18f23780..b7b349b12b 100644 --- a/build.cake +++ b/build.cake @@ -162,7 +162,7 @@ void RunCoreTest(string project, Parameters parameters, bool coreOnly = false) project = System.IO.Path.Combine(project, System.IO.Path.GetFileName(project)+".csproj"); Information("Running tests from " + project); DotNetCoreRestore(project); - var frameworks = new List(){"netcoreapp1.1"}; + var frameworks = new List(){"netcoreapp2.0"}; if(parameters.IsRunningOnWindows) frameworks.Add("net461"); foreach(var fw in frameworks) diff --git a/build/ReactiveUI.props b/build/ReactiveUI.props new file mode 100644 index 0000000000..1e2ad69046 --- /dev/null +++ b/build/ReactiveUI.props @@ -0,0 +1,5 @@ + + + + + diff --git a/build/Rx.props b/build/Rx.props index 5d74a6b214..323026f5e2 100644 --- a/build/Rx.props +++ b/build/Rx.props @@ -1,11 +1,10 @@  - - - - - - - + + + + + + diff --git a/build/XUnit.props b/build/XUnit.props index d59cee6536..9d4d401743 100644 --- a/build/XUnit.props +++ b/build/XUnit.props @@ -9,7 +9,7 @@ - + diff --git a/docs/guidelines/build.md b/docs/guidelines/build.md index 822496814b..0cf91d913a 100644 --- a/docs/guidelines/build.md +++ b/docs/guidelines/build.md @@ -80,10 +80,11 @@ mono ./samples/ControlCatalog.Desktop/bin/Debug/ControlCatalog.Desktop.exe ### Building Avalonia in MonoDevelop -Unless you have a very current version of monodevelop (6.1.x or newer), it is necessary to manually -restore the Nuget depdendencies as [mentioned above](#restore-nuget-packages). You must then -disable MonoDevelop's inbuilt NuGet package manager add-in by going to `Tools -> Add-in Manager` or -it will complain that a newer version of NuGet is needed. +Flatpak version will *NOT* work. Version from https://github.com/cra0zy/monodevelop-run-installer/ might work if you are very lucky. Make sure that you have the latest version of Mono (from alpha update channel) and .NET Core SDK. Make sure to follow `FrameworkPathOverride` workaround from https://github.com/dotnet/sdk/issues/335 + +### Building and running Avalonia in Rider + +For Linux/OSX you'll probably need to apply workaround from https://github.com/dotnet/sdk/issues/335 + +Just add `export FrameworkPathOverride=/usr/lib/mono/4.6.1-api` (or `export FrameworkPathOverride=/Library/Frameworks/Mono.framework/Versions/Current/lib/mono/4.6.1-api` for OSX) -Finally, select the `Debug | Mono` or `Release | Mono` build configuration and you should be good to -go! diff --git a/packages.cake b/packages.cake index 22a519802f..82c21ae5aa 100644 --- a/packages.cake +++ b/packages.cake @@ -43,7 +43,6 @@ public class Packages } } - //new NuSpecDependency() { Id = "System.Threading.ThreadPool", TargetFramework = "netcoreapp1.0", Version = "4.3.0" }, public Packages(ICakeContext context, Parameters parameters) { // NUGET NUSPECS @@ -111,6 +110,7 @@ public class Packages var SplatVersion = packageVersions["Splat"].FirstOrDefault().Item1; var SpracheVersion = packageVersions["Sprache"].FirstOrDefault().Item1; var SystemReactiveVersion = packageVersions["System.Reactive"].FirstOrDefault().Item1; + var ReactiveUIVersion = packageVersions["reactiveui"].FirstOrDefault().Item1; var SystemValueTupleVersion = packageVersions["System.ValueTuple"].FirstOrDefault().Item1; SkiaSharpVersion = packageVersions["SkiaSharp"].FirstOrDefault().Item1; SkiaSharpLinuxVersion = packageVersions["Avalonia.Skia.Linux.Natives"].FirstOrDefault().Item1; @@ -124,6 +124,7 @@ public class Packages context.Information("Package: Splat, version: {0}", SplatVersion); context.Information("Package: Sprache, version: {0}", SpracheVersion); context.Information("Package: System.Reactive, version: {0}", SystemReactiveVersion); + context.Information("Package: reactiveui, version: {0}", ReactiveUIVersion); context.Information("Package: System.ValueTuple, version: {0}", SystemValueTupleVersion); context.Information("Package: SkiaSharp, version: {0}", SkiaSharpVersion); context.Information("Package: Avalonia.Skia.Linux.Natives, version: {0}", SkiaSharpLinuxVersion); @@ -176,7 +177,6 @@ public class Packages new [] { "./src/", "Avalonia.Visuals", ".xml" }, new [] { "./src/", "Avalonia.Styling", ".dll" }, new [] { "./src/", "Avalonia.Styling", ".xml" }, - new [] { "./src/", "Avalonia.ReactiveUI", ".dll" }, new [] { "./src/", "Avalonia.Themes.Default", ".dll" }, new [] { "./src/", "Avalonia.Themes.Default", ".xml" }, new [] { "./src/Markup/", "Avalonia.Markup", ".dll" }, @@ -186,12 +186,12 @@ public class Packages }; var coreLibrariesFiles = coreLibraries.Select((lib) => { - return (FilePath)context.File(lib[0] + lib[1] + "/bin/" + parameters.DirSuffix + "/netstandard1.3/" + lib[1] + lib[2]); + return (FilePath)context.File(lib[0] + lib[1] + "/bin/" + parameters.DirSuffix + "/netstandard2.0/" + lib[1] + lib[2]); }).ToList(); var coreLibrariesNuSpecContent = coreLibrariesFiles.Select((file) => { return new NuSpecContent { - Source = file.FullPath, Target = "lib/netstandard1.3" + Source = file.FullPath, Target = "lib/netstandard2.0" }; }); @@ -203,14 +203,14 @@ public class Packages var netcoreappCoreLibrariesNuSpecContent = coreLibrariesFiles.Select((file) => { return new NuSpecContent { - Source = file.FullPath, Target = "lib/netcoreapp1.0" + Source = file.FullPath, Target = "lib/netcoreapp2.0" }; }); var net45RuntimePlatformExtensions = new [] {".xml", ".dll"}; var net45RuntimePlatform = net45RuntimePlatformExtensions.Select(libSuffix => { return new NuSpecContent { - Source = ((FilePath)context.File("./src/Avalonia.DotNetFrameworkRuntime/bin/" + parameters.DirSuffix + "/Avalonia.DotNetFrameworkRuntime" + libSuffix)).FullPath, + Source = ((FilePath)context.File("./src/Avalonia.DotNetFrameworkRuntime/bin/" + parameters.DirSuffix + "/net461/Avalonia.DotNetFrameworkRuntime" + libSuffix)).FullPath, Target = "lib/net45" }; }); @@ -218,8 +218,8 @@ public class Packages var netCoreRuntimePlatformExtensions = new [] {".xml", ".dll"}; var netCoreRuntimePlatform = netCoreRuntimePlatformExtensions.Select(libSuffix => { return new NuSpecContent { - Source = ((FilePath)context.File("./src/Avalonia.DotNetCoreRuntime/bin/" + parameters.DirSuffix + "/netcoreapp1.0/Avalonia.DotNetCoreRuntime" + libSuffix)).FullPath, - Target = "lib/netcoreapp1.0" + Source = ((FilePath)context.File("./src/Avalonia.DotNetCoreRuntime/bin/" + parameters.DirSuffix + "/netcoreapp2.0/Avalonia.DotNetCoreRuntime" + libSuffix)).FullPath, + Target = "lib/netcoreapp2.0" }; }); @@ -238,15 +238,15 @@ public class Packages new NuSpecDependency() { Id = "Sprache", Version = SpracheVersion }, new NuSpecDependency() { Id = "System.Reactive", Version = SystemReactiveVersion }, //.NET Core - new NuSpecDependency() { Id = "System.Threading.ThreadPool", TargetFramework = "netcoreapp1.0", Version = "4.3.0" }, - new NuSpecDependency() { Id = "Microsoft.Extensions.DependencyModel", TargetFramework = "netcoreapp1.0", Version = "1.1.0" }, - new NuSpecDependency() { Id = "NETStandard.Library", TargetFramework = "netcoreapp1.0", Version = "1.6.0" }, - new NuSpecDependency() { Id = "Splat", TargetFramework = "netcoreapp1.0", Version = SplatVersion }, - new NuSpecDependency() { Id = "Serilog", TargetFramework = "netcoreapp1.0", Version = SerilogVersion }, - new NuSpecDependency() { Id = "Sprache", TargetFramework = "netcoreapp1.0", Version = SpracheVersion }, - new NuSpecDependency() { Id = "System.Reactive", TargetFramework = "netcoreapp1.0", Version = SystemReactiveVersion }, + new NuSpecDependency() { Id = "System.Threading.ThreadPool", TargetFramework = "netcoreapp2.0", Version = "4.3.0" }, + new NuSpecDependency() { Id = "Microsoft.Extensions.DependencyModel", TargetFramework = "netcoreapp2.0", Version = "1.1.0" }, + new NuSpecDependency() { Id = "NETStandard.Library", TargetFramework = "netcoreapp2.0", Version = "1.6.0" }, + new NuSpecDependency() { Id = "Splat", TargetFramework = "netcoreapp2.0", Version = SplatVersion }, + new NuSpecDependency() { Id = "Serilog", TargetFramework = "netcoreapp2.0", Version = SerilogVersion }, + new NuSpecDependency() { Id = "Sprache", TargetFramework = "netcoreapp2.0", Version = SpracheVersion }, + new NuSpecDependency() { Id = "System.Reactive", TargetFramework = "netcoreapp2.0", Version = SystemReactiveVersion }, } - .Deps(new string[]{null, "netcoreapp1.0"}, + .Deps(new string[]{null, "netcoreapp2.0"}, "System.ValueTuple", "System.ComponentModel.TypeConverter", "System.ComponentModel.Primitives", "System.Runtime.Serialization.Primitives", "System.Xml.XmlDocument", "System.Xml.ReaderWriter") .ToArray(), @@ -269,11 +269,28 @@ public class Packages }, Files = new [] { - new NuSpecContent { Source = "Avalonia.HtmlRenderer.dll", Target = "lib/netstandard1.3" } + new NuSpecContent { Source = "Avalonia.HtmlRenderer.dll", Target = "lib/netstandard2.0" } }, - BasePath = context.Directory("./src/Avalonia.HtmlRenderer/bin/" + parameters.DirSuffix + "/netstandard1.3"), + BasePath = context.Directory("./src/Avalonia.HtmlRenderer/bin/" + parameters.DirSuffix + "/netstandard2.0"), OutputDirectory = parameters.NugetRoot - } + }, + /////////////////////////////////////////////////////////////////////////////// + // Avalonia.ReactiveUI + /////////////////////////////////////////////////////////////////////////////// + new NuGetPackSettings() + { + Id = "Avalonia.ReactiveUI", + Dependencies = new DependencyBuilder(this) + { + new NuSpecDependency() { Id = "Avalonia", Version = parameters.Version }, + }.Deps(new string[] {null}, "reactiveui"), + Files = new [] + { + new NuSpecContent { Source = "Avalonia.ReactiveUI.dll", Target = "lib/netstandard2.0" } + }, + BasePath = context.Directory("./src/Avalonia.ReactiveUI/bin/" + parameters.DirSuffix + "/netstandard2.0"), + OutputDirectory = parameters.NugetRoot + }, }; var nuspecNuGetSettingsMobile = new [] @@ -331,7 +348,7 @@ public class Packages 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 + "/netstandard1.3/Avalonia.Win32.dll", Target = "lib/netstandard1.3" } + new NuSpecContent { Source = "Avalonia.Win32.NetStandard/bin/" + parameters.DirSuffix + "/netstandard2.0/Avalonia.Win32.dll", Target = "lib/netstandard2.0" } }, BasePath = context.Directory("./src/Windows"), OutputDirectory = parameters.NugetRoot @@ -352,9 +369,9 @@ public class Packages }, Files = new [] { - new NuSpecContent { Source = "Avalonia.Direct2D1.dll", Target = "lib/netstandard1.3" } + new NuSpecContent { Source = "Avalonia.Direct2D1.dll", Target = "lib/netstandard2.0" } }, - BasePath = context.Directory("./src/Windows/Avalonia.Direct2D1/bin/" + parameters.DirSuffix + "/netstandard1.3"), + BasePath = context.Directory("./src/Windows/Avalonia.Direct2D1/bin/" + parameters.DirSuffix + "/netstandard2.0"), OutputDirectory = parameters.NugetRoot }, /////////////////////////////////////////////////////////////////////////////// @@ -386,9 +403,9 @@ public class Packages }, Files = new [] { - new NuSpecContent { Source = "Avalonia.Gtk3.dll", Target = "lib/netstandard1.3" } + new NuSpecContent { Source = "Avalonia.Gtk3.dll", Target = "lib/netstandard2.0" } }, - BasePath = context.Directory("./src/Gtk/Avalonia.Gtk3/bin/" + parameters.DirSuffix + "/netstandard1.3"), + BasePath = context.Directory("./src/Gtk/Avalonia.Gtk3/bin/" + parameters.DirSuffix + "/netstandard2.0"), OutputDirectory = parameters.NugetRoot }, /////////////////////////////////////////////////////////////////////////////// @@ -418,18 +435,18 @@ public class Packages { new NuSpecDependency() { Id = "Avalonia", Version = parameters.Version }, new NuSpecDependency() { Id = "SkiaSharp", Version = SkiaSharpVersion }, - new NuSpecDependency() { Id = "Avalonia", Version = parameters.Version, TargetFramework="netcoreapp1.1" }, - new NuSpecDependency() { Id = "SkiaSharp", Version = SkiaSharpVersion, TargetFramework="netcoreapp1.1" }, - new NuSpecDependency() { Id = "Avalonia.Skia.Linux.Natives", Version = SkiaSharpLinuxVersion, TargetFramework="netcoreapp1.1" }, + new NuSpecDependency() { Id = "Avalonia", Version = parameters.Version, TargetFramework="netcoreapp2.0" }, + new NuSpecDependency() { Id = "SkiaSharp", Version = SkiaSharpVersion, TargetFramework="netcoreapp2.0" }, + new NuSpecDependency() { Id = "Avalonia.Skia.Linux.Natives", Version = SkiaSharpLinuxVersion, TargetFramework="netcoreapp2.0" }, new NuSpecDependency() { Id = "Avalonia", Version = parameters.Version, TargetFramework="net461" }, new NuSpecDependency() { Id = "SkiaSharp", Version = SkiaSharpVersion, TargetFramework="net461" }, new NuSpecDependency() { Id = "Avalonia.Skia.Linux.Natives", Version = SkiaSharpLinuxVersion, TargetFramework="net461" } }, Files = new [] { - new NuSpecContent { Source = "Avalonia.Skia.dll", Target = "lib/netstandard1.3" } + new NuSpecContent { Source = "Avalonia.Skia.dll", Target = "lib/netstandard2.0" } }, - BasePath = context.Directory("./src/Skia/Avalonia.Skia/bin/" + parameters.DirSuffix + "/netstandard1.3"), + BasePath = context.Directory("./src/Skia/Avalonia.Skia/bin/" + parameters.DirSuffix + "/netstandard2.0"), OutputDirectory = parameters.NugetRoot }, /////////////////////////////////////////////////////////////////////////////// @@ -448,9 +465,9 @@ public class Packages new NuSpecDependency() { Id = "Avalonia.Skia", TargetFramework="net45", Version = parameters.Version }, new NuSpecDependency() { Id = "Avalonia.Gtk3", TargetFramework="net45", Version = parameters.Version }, //.NET Core - new NuSpecDependency() { Id = "Avalonia.Win32", TargetFramework="netcoreapp1.0", Version = parameters.Version }, - new NuSpecDependency() { Id = "Avalonia.Skia", TargetFramework="netcoreapp1.0", Version = parameters.Version }, - new NuSpecDependency() { Id = "Avalonia.Gtk3", TargetFramework="netcoreapp1.0", Version = parameters.Version } + new NuSpecDependency() { Id = "Avalonia.Win32", TargetFramework="netcoreapp2.0", Version = parameters.Version }, + new NuSpecDependency() { Id = "Avalonia.Skia", TargetFramework="netcoreapp2.0", Version = parameters.Version }, + new NuSpecDependency() { Id = "Avalonia.Gtk3", TargetFramework="netcoreapp2.0", Version = parameters.Version } }, Files = new NuSpecContent[] { @@ -488,7 +505,7 @@ public class Packages }, Files = new [] { - new NuSpecContent { Source = "Avalonia.LinuxFramebuffer/bin/" + parameters.DirSuffix + "/netstandard1.3/Avalonia.LinuxFramebuffer.dll", Target = "lib/netstandard1.3" } + new NuSpecContent { Source = "Avalonia.LinuxFramebuffer/bin/" + parameters.DirSuffix + "/netstandard2.0/Avalonia.LinuxFramebuffer.dll", Target = "lib/netstandard2.0" } }, BasePath = context.Directory("./src/Linux/"), OutputDirectory = parameters.NugetRoot diff --git a/samples/BindingTest/App.xaml.cs b/samples/BindingTest/App.xaml.cs index 7bcaf837a8..42e5716541 100644 --- a/samples/BindingTest/App.xaml.cs +++ b/samples/BindingTest/App.xaml.cs @@ -20,6 +20,7 @@ namespace BindingTest AppBuilder.Configure() .UsePlatformDetect() + .UseReactiveUI() .Start(); } diff --git a/samples/BindingTest/BindingTest.csproj b/samples/BindingTest/BindingTest.csproj index eab656eb33..b4dd17194b 100644 --- a/samples/BindingTest/BindingTest.csproj +++ b/samples/BindingTest/BindingTest.csproj @@ -45,7 +45,6 @@ - @@ -163,4 +162,5 @@ + \ No newline at end of file diff --git a/samples/BindingTest/ViewModels/MainWindowViewModel.cs b/samples/BindingTest/ViewModels/MainWindowViewModel.cs index d8ea280abf..1116810ccb 100644 --- a/samples/BindingTest/ViewModels/MainWindowViewModel.cs +++ b/samples/BindingTest/ViewModels/MainWindowViewModel.cs @@ -28,15 +28,13 @@ namespace BindingTest.ViewModels SelectedItems = new ObservableCollection(); - ShuffleItems = ReactiveCommand.Create(); - ShuffleItems.Subscribe(_ => + ShuffleItems = ReactiveCommand.Create(() => { var r = new Random(); Items.Move(r.Next(Items.Count), 1); }); - StringValueCommand = ReactiveCommand.Create(); - StringValueCommand.Subscribe(param => + StringValueCommand = ReactiveCommand.Create(param => { BooleanFlag = !BooleanFlag; StringValue = param.ToString(); @@ -58,7 +56,7 @@ namespace BindingTest.ViewModels public ObservableCollection Items { get; } public ObservableCollection SelectedItems { get; } - public ReactiveCommand ShuffleItems { get; } + public ReactiveCommand ShuffleItems { get; } public string BooleanString { @@ -91,7 +89,7 @@ namespace BindingTest.ViewModels } public IObservable CurrentTimeObservable { get; } - public ReactiveCommand StringValueCommand { get; } + public ReactiveCommand StringValueCommand { get; } public DataAnnotationsErrorViewModel DataAnnotationsValidation { get; } = new DataAnnotationsErrorViewModel(); public ExceptionErrorViewModel ExceptionDataValidation { get; } = new ExceptionErrorViewModel(); diff --git a/samples/BindingTest/ViewModels/NestedCommandViewModel.cs b/samples/BindingTest/ViewModels/NestedCommandViewModel.cs index 3eca481c68..886ecbed8e 100644 --- a/samples/BindingTest/ViewModels/NestedCommandViewModel.cs +++ b/samples/BindingTest/ViewModels/NestedCommandViewModel.cs @@ -12,7 +12,7 @@ namespace BindingTest.ViewModels { public NestedCommandViewModel() { - Command = ReactiveCommand.Create(); + Command = ReactiveCommand.Create(() => { }); } public ICommand Command { get; } diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index e0e848b91b..b308c59789 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp1.1 + netcoreapp2.0 diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 7c67a239e6..30df53980c 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -16,6 +16,7 @@ namespace ControlCatalog.NetCore else AppBuilder.Configure() .UsePlatformDetect() + .UseReactiveUI() .Start(); } diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index a75de098e8..99ddce0757 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -1,6 +1,6 @@  - netstandard1.3 + netstandard2.0 False false diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index 413794dfa2..65c37c898b 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -1,6 +1,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using System; namespace ControlCatalog { @@ -19,7 +20,7 @@ namespace ControlCatalog // so we must refer to this resource DLL statically. For // now I am doing that here. But we need a better solution!! var theme = new Avalonia.Themes.Default.DefaultTheme(); - theme.FindResource("Button"); + theme.TryGetResource("Button", out _); AvaloniaXamlLoader.Load(this); } } diff --git a/samples/ControlCatalog/Pages/ToolTipPage.xaml b/samples/ControlCatalog/Pages/ToolTipPage.xaml index 29df11510c..5cf7fee4d1 100644 --- a/samples/ControlCatalog/Pages/ToolTipPage.xaml +++ b/samples/ControlCatalog/Pages/ToolTipPage.xaml @@ -1,22 +1,41 @@ - - ToolTip - A control which pops up a hint when a control is hovered + + ToolTip + A control which pops up a hint when a control is hovered - - - - - ToolTip - A control which pops up a hint when a control is hovered - - - Hover Here - + HorizontalAlignment="Center"> + + Hover Here + + + + + + ToolTip + A control which pops up a hint when a control is hovered + + + ToolTip bottom placement + + - \ No newline at end of file diff --git a/samples/RenderTest/Program.cs b/samples/RenderTest/Program.cs index 7a23e09dd4..514771097f 100644 --- a/samples/RenderTest/Program.cs +++ b/samples/RenderTest/Program.cs @@ -18,6 +18,7 @@ namespace RenderTest // again. AppBuilder.Configure() .UsePlatformDetect() + .UseReactiveUI() .Start(); } diff --git a/samples/RenderTest/RenderTest.csproj b/samples/RenderTest/RenderTest.csproj index b7e64f4dae..974b8802ac 100644 --- a/samples/RenderTest/RenderTest.csproj +++ b/samples/RenderTest/RenderTest.csproj @@ -191,4 +191,5 @@ + \ No newline at end of file diff --git a/samples/RenderTest/ViewModels/MainWindowViewModel.cs b/samples/RenderTest/ViewModels/MainWindowViewModel.cs index bd68752f66..b2fa2e8b7a 100644 --- a/samples/RenderTest/ViewModels/MainWindowViewModel.cs +++ b/samples/RenderTest/ViewModels/MainWindowViewModel.cs @@ -10,10 +10,8 @@ namespace RenderTest.ViewModels public MainWindowViewModel() { - ToggleDrawDirtyRects = ReactiveCommand.Create(); - ToggleDrawDirtyRects.Subscribe(_ => DrawDirtyRects = !DrawDirtyRects); - ToggleDrawFps = ReactiveCommand.Create(); - ToggleDrawFps.Subscribe(_ => DrawFps = !DrawFps); + ToggleDrawDirtyRects = ReactiveCommand.Create(() => DrawDirtyRects = !DrawDirtyRects); + ToggleDrawFps = ReactiveCommand.Create(() => DrawFps = !DrawFps); } public bool DrawDirtyRects @@ -28,7 +26,7 @@ namespace RenderTest.ViewModels set { this.RaiseAndSetIfChanged(ref drawFps, value); } } - public ReactiveCommand ToggleDrawDirtyRects { get; } - public ReactiveCommand ToggleDrawFps { get; } + public ReactiveCommand ToggleDrawDirtyRects { get; } + public ReactiveCommand ToggleDrawFps { get; } } } diff --git a/samples/VirtualizationTest/Program.cs b/samples/VirtualizationTest/Program.cs index d5649f6a06..e9156d803f 100644 --- a/samples/VirtualizationTest/Program.cs +++ b/samples/VirtualizationTest/Program.cs @@ -17,6 +17,7 @@ namespace VirtualizationTest AppBuilder.Configure() .UsePlatformDetect() + .UseReactiveUI() .Start(); } diff --git a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs index 86869eb46a..a0e1570a36 100644 --- a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs @@ -23,20 +23,15 @@ namespace VirtualizationTest.ViewModels public MainWindowViewModel() { this.WhenAnyValue(x => x.ItemCount).Subscribe(ResizeItems); - RecreateCommand = ReactiveCommand.Create(); - RecreateCommand.Subscribe(_ => Recreate()); + RecreateCommand = ReactiveCommand.Create(() => Recreate()); - AddItemCommand = ReactiveCommand.Create(); - AddItemCommand.Subscribe(_ => AddItem()); + AddItemCommand = ReactiveCommand.Create(() => AddItem()); - RemoveItemCommand = ReactiveCommand.Create(); - RemoveItemCommand.Subscribe(_ => Remove()); + RemoveItemCommand = ReactiveCommand.Create(() => Remove()); - SelectFirstCommand = ReactiveCommand.Create(); - SelectFirstCommand.Subscribe(_ => SelectItem(0)); + SelectFirstCommand = ReactiveCommand.Create(() => SelectItem(0)); - SelectLastCommand = ReactiveCommand.Create(); - SelectLastCommand.Subscribe(_ => SelectItem(Items.Count - 1)); + SelectLastCommand = ReactiveCommand.Create(() => SelectItem(Items.Count - 1)); } public string NewItemString @@ -78,11 +73,11 @@ namespace VirtualizationTest.ViewModels public IEnumerable VirtualizationModes => Enum.GetValues(typeof(ItemVirtualizationMode)).Cast(); - public ReactiveCommand AddItemCommand { get; private set; } - public ReactiveCommand RecreateCommand { get; private set; } - public ReactiveCommand RemoveItemCommand { get; private set; } - public ReactiveCommand SelectFirstCommand { get; private set; } - public ReactiveCommand SelectLastCommand { get; private set; } + public ReactiveCommand AddItemCommand { get; private set; } + public ReactiveCommand RecreateCommand { get; private set; } + public ReactiveCommand RemoveItemCommand { get; private set; } + public ReactiveCommand SelectFirstCommand { get; private set; } + public ReactiveCommand SelectLastCommand { get; private set; } private void ResizeItems(int count) { diff --git a/samples/VirtualizationTest/VirtualizationTest.csproj b/samples/VirtualizationTest/VirtualizationTest.csproj index 327e659966..9d31a6cb86 100644 --- a/samples/VirtualizationTest/VirtualizationTest.csproj +++ b/samples/VirtualizationTest/VirtualizationTest.csproj @@ -158,4 +158,5 @@ + \ No newline at end of file diff --git a/src/Avalonia.Animation/Avalonia.Animation.csproj b/src/Avalonia.Animation/Avalonia.Animation.csproj index 8f832dabcf..bafac2b261 100644 --- a/src/Avalonia.Animation/Avalonia.Animation.csproj +++ b/src/Avalonia.Animation/Avalonia.Animation.csproj @@ -1,6 +1,6 @@  - netstandard1.3 + netstandard2.0 false diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index 6906aed42b..58c510f483 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -1,6 +1,6 @@  - netstandard1.3 + netstandard2.0 false Avalonia diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index 190b3ee2be..ec1643427b 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -47,6 +47,9 @@ namespace Avalonia { Dictionary inner; + // Ensure the type's static ctor has been run. + RuntimeHelpers.RunClassConstructor(ownerType.TypeHandle); + if (_attached.TryGetValue(ownerType, out inner)) { return inner.Values; diff --git a/src/Avalonia.Base/Collections/AvaloniaDictionary.cs b/src/Avalonia.Base/Collections/AvaloniaDictionary.cs index 1aa239180c..b90dccf74e 100644 --- a/src/Avalonia.Base/Collections/AvaloniaDictionary.cs +++ b/src/Avalonia.Base/Collections/AvaloniaDictionary.cs @@ -16,6 +16,7 @@ namespace Avalonia.Collections /// The type of the dictionary key. /// The type of the dictionary value. public class AvaloniaDictionary : IDictionary, + IDictionary, INotifyCollectionChanged, INotifyPropertyChanged { @@ -51,6 +52,16 @@ namespace Avalonia.Collections /// public ICollection Values => _inner.Values; + bool IDictionary.IsFixedSize => ((IDictionary)_inner).IsFixedSize; + + ICollection IDictionary.Keys => ((IDictionary)_inner).Keys; + + ICollection IDictionary.Values => ((IDictionary)_inner).Values; + + bool ICollection.IsSynchronized => ((IDictionary)_inner).IsSynchronized; + + object ICollection.SyncRoot => ((IDictionary)_inner).SyncRoot; + /// /// Gets or sets the named resource. /// @@ -89,6 +100,8 @@ namespace Avalonia.Collections } } + object IDictionary.this[object key] { get => ((IDictionary)_inner)[key]; set => ((IDictionary)_inner)[key] = value; } + /// public void Add(TKey key, TValue value) { @@ -118,10 +131,7 @@ namespace Avalonia.Collections } /// - public bool ContainsKey(TKey key) - { - return _inner.ContainsKey(key); - } + public bool ContainsKey(TKey key) => _inner.ContainsKey(key); /// public void CopyTo(KeyValuePair[] array, int arrayIndex) @@ -130,21 +140,16 @@ namespace Avalonia.Collections } /// - public IEnumerator> GetEnumerator() - { - return _inner.GetEnumerator(); - } + public IEnumerator> GetEnumerator() => _inner.GetEnumerator(); /// public bool Remove(TKey key) { - TValue value; - - if (_inner.TryGetValue(key, out value)) + if (_inner.TryGetValue(key, out TValue value)) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Count")); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"Item[{key}]")); - + if (CollectionChanged != null) { var e = new NotifyCollectionChangedEventArgs( @@ -163,16 +168,13 @@ namespace Avalonia.Collections } /// - public bool TryGetValue(TKey key, out TValue value) - { - return _inner.TryGetValue(key, out value); - } + public bool TryGetValue(TKey key, out TValue value) => _inner.TryGetValue(key, out value); /// - IEnumerator IEnumerable.GetEnumerator() - { - return _inner.GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator(); + + /// + void ICollection.CopyTo(Array array, int index) => ((ICollection)_inner).CopyTo(array, index); /// void ICollection>.Add(KeyValuePair item) @@ -192,6 +194,18 @@ namespace Avalonia.Collections return Remove(item.Key); } + /// + void IDictionary.Add(object key, object value) => Add((TKey)key, (TValue)value); + + /// + bool IDictionary.Contains(object key) => ((IDictionary) _inner).Contains(key); + + /// + IDictionaryEnumerator IDictionary.GetEnumerator() => ((IDictionary)_inner).GetEnumerator(); + + /// + void IDictionary.Remove(object key) => Remove((TKey)key); + private void NotifyAdd(TKey key, TValue value) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Count")); diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 18eb1f7606..abae080515 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -29,7 +29,7 @@ namespace Avalonia /// method. /// - Tracks the lifetime of the application. /// - public class Application : IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IApplicationLifecycle + public class Application : IApplicationLifecycle, IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IResourceNode { /// /// The application-global data templates. @@ -40,6 +40,7 @@ namespace Avalonia new Lazy(() => (IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))); private readonly Styler _styler = new Styler(); private Styles _styles; + private IResourceDictionary _resources; /// /// Initializes a new instance of the class. @@ -49,6 +50,9 @@ namespace Avalonia OnExit += OnExiting; } + /// + public event EventHandler ResourcesChanged; + /// /// Gets the current instance of the class. /// @@ -97,6 +101,34 @@ namespace Avalonia /// public IClipboard Clipboard => _clipboard.Value; + /// + /// Gets the application's global resource dictionary. + /// + public IResourceDictionary Resources + { + get => _resources ?? (Resources = new ResourceDictionary()); + set + { + Contract.Requires(value != null); + + var hadResources = false; + + if (_resources != null) + { + hadResources = _resources.Count > 0; + _resources.ResourcesChanged -= ResourcesChanged; + } + + _resources = value; + _resources.ResourcesChanged += ResourcesChanged; + + if (hadResources || _resources.Count > 0) + { + ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); + } + } + } + /// /// Gets the application's global styles. /// @@ -119,6 +151,12 @@ namespace Avalonia /// bool IStyleHost.IsStylesInitialized => _styles != null; + /// + bool IResourceProvider.HasResources => _resources?.Count > 0; + + /// + IResourceNode IResourceNode.ResourceParent => null; + /// /// Initializes the application by loading XAML etc. /// @@ -145,13 +183,20 @@ namespace Avalonia { OnExit?.Invoke(this, EventArgs.Empty); } - + + /// + bool IResourceProvider.TryGetResource(string key, out object value) + { + value = null; + return (_resources?.TryGetResource(key, out value) ?? false) || + Styles.TryGetResource(key, out value); + } + /// /// Sent when the application is exiting. /// public event EventHandler OnExit; - /// /// Called when the application is exiting. /// diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 037546b186..d999e3b74e 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -1,6 +1,6 @@  - netstandard1.3 + netstandard2.0 false diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 4913037ea4..f3705c9127 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -97,8 +97,9 @@ namespace Avalonia.Controls private bool _isAttachedToLogicalTree; private IAvaloniaList _logicalChildren; private INameScope _nameScope; - private bool _styled; + private IResourceDictionary _resources; private Styles _styles; + private bool _styled; private Subject _styleDetach = new Subject(); /// @@ -153,6 +154,11 @@ namespace Avalonia.Controls /// public event EventHandler Initialized; + /// + /// Occurs when a resource in this control or a parent control has changed. + /// + public event EventHandler ResourcesChanged; + /// /// Gets or sets the name of the control. /// @@ -262,7 +268,32 @@ namespace Avalonia.Controls /// each control may in addition define its own styles which are applied to the control /// itself and its children. /// - public Styles Styles => _styles ?? (_styles = new Styles()); + public Styles Styles + { + get { return _styles ?? (Styles = new Styles()); } + set + { + Contract.Requires(value != null); + + if (_styles != value) + { + if (_styles != null) + { + (_styles as ISetStyleParent)?.SetParent(null); + _styles.ResourcesChanged -= ThisResourcesChanged; + } + + _styles = value; + + if (value is ISetStyleParent setParent && setParent.ResourceParent == null) + { + setParent.SetParent(this); + } + + _styles.ResourcesChanged += ThisResourcesChanged; + } + } + } /// /// Gets the control's logical parent. @@ -278,6 +309,34 @@ namespace Avalonia.Controls set { SetValue(ContextMenuProperty, value); } } + /// + /// Gets or sets the control's resource dictionary. + /// + public IResourceDictionary Resources + { + get => _resources ?? (Resources = new ResourceDictionary()); + set + { + Contract.Requires(value != null); + + var hadResources = false; + + if (_resources != null) + { + hadResources = _resources.Count > 0; + _resources.ResourcesChanged -= ThisResourcesChanged; + } + + _resources = value; + _resources.ResourcesChanged += ThisResourcesChanged; + + if (hadResources || _resources.Count > 0) + { + ((ILogical)this).NotifyResourcesChanged(new ResourcesChangedEventArgs()); + } + } + } + /// /// Gets or sets a user-defined object attached to the control. /// @@ -296,9 +355,35 @@ namespace Avalonia.Controls internal set { SetValue(TemplatedParentProperty, value); } } + /// + /// Gets the control's logical children. + /// + protected IAvaloniaList LogicalChildren + { + get + { + if (_logicalChildren == null) + { + var list = new AvaloniaList(); + list.ResetBehavior = ResetBehavior.Remove; + list.Validate = ValidateLogicalChild; + list.CollectionChanged += LogicalChildrenCollectionChanged; + _logicalChildren = list; + } + + return _logicalChildren; + } + } + /// bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null; + /// + /// Gets the collection in a form that allows adding and removing + /// pseudoclasses. + /// + protected IPseudoClasses PseudoClasses => Classes; + /// /// Gets a value indicating whether the element is attached to a rooted logical tree. /// @@ -314,6 +399,12 @@ namespace Avalonia.Controls /// IAvaloniaReadOnlyList ILogical.LogicalChildren => LogicalChildren; + /// + bool IResourceProvider.HasResources => _resources?.Count > 0 || Styles.HasResources; + + /// + IResourceNode IResourceNode.ResourceParent => ((IStyleHost)this).StylingParent as IResourceNode; + /// IAvaloniaReadOnlyList IStyleable.Classes => Classes; @@ -390,31 +481,24 @@ namespace Avalonia.Controls this.OnDetachedFromLogicalTreeCore(e); } - /// - /// Gets the control's logical children. - /// - protected IAvaloniaList LogicalChildren + /// + void ILogical.NotifyResourcesChanged(ResourcesChangedEventArgs e) { - get - { - if (_logicalChildren == null) - { - var list = new AvaloniaList(); - list.ResetBehavior = ResetBehavior.Remove; - list.Validate = ValidateLogicalChild; - list.CollectionChanged += LogicalChildrenCollectionChanged; - _logicalChildren = list; - } + ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); - return _logicalChildren; + foreach (var child in LogicalChildren) + { + child.NotifyResourcesChanged(e); } } - /// - /// Gets the collection in a form that allows adding and removing - /// pseudoclasses. - /// - protected IPseudoClasses PseudoClasses => Classes; + /// + bool IResourceProvider.TryGetResource(string key, out object value) + { + value = null; + return (_resources?.TryGetResource(key, out value) ?? false) || + (_styles?.TryGetResource(key, out value) ?? false); + } /// /// Sets the control's logical parent. @@ -450,6 +534,7 @@ namespace Avalonia.Controls } _parent = (IControl)parent; + ((ILogical)this).NotifyResourcesChanged(new ResourcesChangedEventArgs()); if (_parent is IStyleRoot || _parent?.IsAttachedToLogicalTree == true || this is IStyleRoot) { @@ -841,5 +926,10 @@ namespace Avalonia.Controls } } } + + private void ThisResourcesChanged(object sender, ResourcesChangedEventArgs e) + { + ((ILogical)this).NotifyResourcesChanged(e); + } } } diff --git a/src/Avalonia.Controls/DropDown.cs b/src/Avalonia.Controls/DropDown.cs index 0b1371b10e..5349fb1ca7 100644 --- a/src/Avalonia.Controls/DropDown.cs +++ b/src/Avalonia.Controls/DropDown.cs @@ -120,7 +120,7 @@ namespace Avalonia.Controls /// protected override void OnPointerPressed(PointerPressedEventArgs e) { - if (!IsDropDownOpen && ((IVisual)e.Source).GetVisualRoot() != typeof(PopupRoot)) + if (!IsDropDownOpen && ((IVisual)e.Source).GetVisualRoot() is PopupRoot) { IsDropDownOpen = true; e.Handled = true; diff --git a/src/Avalonia.Controls/IControl.cs b/src/Avalonia.Controls/IControl.cs index 41436c6d87..36e09b2ea1 100644 --- a/src/Avalonia.Controls/IControl.cs +++ b/src/Avalonia.Controls/IControl.cs @@ -20,6 +20,7 @@ namespace Avalonia.Controls ILayoutable, IInputElement, INamed, + IResourceNode, IStyleable, IStyleHost { diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index 69b946346e..37637b1624 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -31,7 +31,7 @@ namespace Avalonia.Platform IDisposable ShowDialog(); /// - /// Enables of disables system window decorations (title bar, buttons, etc) + /// Enables or disables system window decorations (title bar, buttons, etc) /// void SetSystemDecorations(bool enabled); @@ -39,5 +39,10 @@ namespace Avalonia.Platform /// Sets the icon of this window. /// void SetIcon(IWindowIconImpl icon); + + /// + /// Enables or disables the taskbar icon + /// + void ShowTaskbarIcon(bool value); } } diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index a2b5f3f8b4..5f6b3ad4c8 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -115,7 +115,7 @@ namespace Avalonia.Controls.Presenters if (_highlightBrush == null) { - _highlightBrush = (IBrush)this.FindStyleResource("HighlightBrush"); + _highlightBrush = (IBrush)this.FindResource("HighlightBrush"); } foreach (var rect in rects) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index daea187a69..5cd3b22fc9 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -277,7 +277,7 @@ namespace Avalonia.Controls.Primitives { base.OnDetachedFromLogicalTree(e); _topLevel = null; - + if (_popupRoot != null) { ((ISetLogicalParent)_popupRoot).SetParent(null); @@ -327,34 +327,40 @@ namespace Avalonia.Controls.Primitives /// /// The popup's position in screen coordinates. protected virtual Point GetPosition() + { + return GetPosition(PlacementTarget ?? this.GetVisualParent(), PlacementMode, PopupRoot, + HorizontalOffset, VerticalOffset); + } + + internal static Point GetPosition(Control target, PlacementMode placement, PopupRoot popupRoot, double horizontalOffset, double verticalOffset) { var zero = default(Point); - var mode = PlacementMode; - var target = PlacementTarget ?? this.GetVisualParent(); + var mode = placement; if (target?.GetVisualRoot() == null) { mode = PlacementMode.Pointer; - } + } switch (mode) { case PlacementMode.Pointer: - if(PopupRoot != null) + if (popupRoot != null) { // Scales the Horizontal and Vertical offset to screen co-ordinates. - var screenOffset = new Point(HorizontalOffset * (PopupRoot as ILayoutRoot).LayoutScaling, VerticalOffset * (PopupRoot as ILayoutRoot).LayoutScaling); - return (((IInputRoot)PopupRoot)?.MouseDevice?.Position ?? default(Point)) + screenOffset; + var screenOffset = new Point(horizontalOffset * (popupRoot as ILayoutRoot).LayoutScaling, + verticalOffset * (popupRoot as ILayoutRoot).LayoutScaling); + return (((IInputRoot)popupRoot)?.MouseDevice?.Position ?? default(Point)) + screenOffset; } return default(Point); case PlacementMode.Bottom: - - return target?.PointToScreen(new Point(0 + HorizontalOffset, target.Bounds.Height + VerticalOffset)) ?? zero; + return target?.PointToScreen(new Point(0 + horizontalOffset, target.Bounds.Height + verticalOffset)) ?? + zero; case PlacementMode.Right: - return target?.PointToScreen(new Point(target.Bounds.Width + HorizontalOffset, 0 + VerticalOffset)) ?? zero; + return target?.PointToScreen(new Point(target.Bounds.Width + horizontalOffset, 0 + verticalOffset)) ?? zero; default: throw new InvalidOperationException("Invalid value for Popup.PlacementMode"); diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index e1b69637af..e45f30f818 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -3,11 +3,7 @@ using System; using System.Reactive.Linq; -using System.Reactive.Subjects; using Avalonia.Controls.Primitives; -using Avalonia.Input; -using Avalonia.Threading; -using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -29,29 +25,50 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterAttached("Tip"); /// - /// The popup window used to display the active tooltip. + /// Defines the ToolTip.IsOpen attached property. /// - private static PopupRoot s_popup; + public static readonly AttachedProperty IsOpenProperty = + AvaloniaProperty.RegisterAttached("IsOpen"); /// - /// The control that the currently visible tooltip is attached to. + /// Defines the ToolTip.Placement property. /// - private static Control s_current; + public static readonly AttachedProperty PlacementProperty = + AvaloniaProperty.RegisterAttached("Placement", defaultValue: PlacementMode.Pointer); /// - /// Observable fired when a tooltip should be displayed for a control. The output from this - /// observable is throttled and calls when the time - /// period expires. + /// Defines the ToolTip.HorizontalOffset property. /// - private static readonly Subject s_show = new Subject(); + public static readonly AttachedProperty HorizontalOffsetProperty = + AvaloniaProperty.RegisterAttached("HorizontalOffset"); + + /// + /// Defines the ToolTip.VerticalOffset property. + /// + public static readonly AttachedProperty VerticalOffsetProperty = + AvaloniaProperty.RegisterAttached("VerticalOffset", 20); + + /// + /// Defines the ToolTip.ShowDelay property. + /// + public static readonly AttachedProperty ShowDelayProperty = + AvaloniaProperty.RegisterAttached("ShowDelay", 400); + + /// + /// Stores the curernt instance in the control. + /// + private static readonly AttachedProperty ToolTipProperty = + AvaloniaProperty.RegisterAttached("ToolTip"); + + private PopupRoot _popup; /// /// Initializes static members of the class. /// static ToolTip() { - TipProperty.Changed.Subscribe(TipChanged); - s_show.Throttle(TimeSpan.FromSeconds(0.5), AvaloniaScheduler.Instance).Subscribe(ShowToolTip); + TipProperty.Changed.Subscribe(ToolTipService.Instance.TipChanged); + IsOpenProperty.Changed.Subscribe(IsOpenChanged); } /// @@ -77,101 +94,160 @@ namespace Avalonia.Controls } /// - /// called when the property changes on a control. + /// Gets the value of the ToolTip.IsOpen attached property. /// - /// The event args. - private static void TipChanged(AvaloniaPropertyChangedEventArgs e) + /// The control to get the property from. + /// + /// A value indicating whether the tool tip is visible. + /// + public static bool GetIsOpen(Control element) { - var control = (Control)e.Sender; + return element.GetValue(IsOpenProperty); + } - if (e.OldValue != null) - { - control.PointerEnter -= ControlPointerEnter; - control.PointerLeave -= ControlPointerLeave; - } + /// + /// Sets the value of the ToolTip.IsOpen attached property. + /// + /// The control to get the property from. + /// A value indicating whether the tool tip is visible. + public static void SetIsOpen(Control element, bool value) + { + element.SetValue(IsOpenProperty, value); + } - if (e.NewValue != null) - { - control.PointerEnter += ControlPointerEnter; - control.PointerLeave += ControlPointerLeave; - } + /// + /// Gets the value of the ToolTip.Placement attached property. + /// + /// The control to get the property from. + /// + /// A value indicating how the tool tip is positioned. + /// + public static PlacementMode GetPlacement(Control element) + { + return element.GetValue(PlacementProperty); } /// - /// Shows a tooltip for the specified control. + /// Sets the value of the ToolTip.Placement attached property. /// - /// The control. - private static void ShowToolTip(Control control) + /// The control to get the property from. + /// A value indicating how the tool tip is positioned. + public static void SetPlacement(Control element, PlacementMode value) { - if (control != null && control.IsVisible && control.GetVisualRoot() != null) - { - var cp = (control.GetVisualRoot() as IInputRoot)?.MouseDevice?.GetPosition(control); + element.SetValue(PlacementProperty, value); + } - if (cp.HasValue && control.IsVisible && new Rect(control.Bounds.Size).Contains(cp.Value)) - { - var position = control.PointToScreen(cp.Value) + new Vector(0, 22); - - if (s_popup == null) - { - s_popup = new PopupRoot(); - s_popup.Content = new ToolTip(); - } - else - { - ((ISetLogicalParent)s_popup).SetParent(null); - } - - ((ISetLogicalParent)s_popup).SetParent(control); - ((ToolTip)s_popup.Content).Content = GetTip(control); - s_popup.Position = position; - s_popup.Show(); - - s_current = control; - } - } + /// + /// Gets the value of the ToolTip.HorizontalOffset attached property. + /// + /// The control to get the property from. + /// + /// A value indicating how the tool tip is positioned. + /// + public static double GetHorizontalOffset(Control element) + { + return element.GetValue(HorizontalOffsetProperty); + } + + /// + /// Sets the value of the ToolTip.HorizontalOffset attached property. + /// + /// The control to get the property from. + /// A value indicating how the tool tip is positioned. + public static void SetHorizontalOffset(Control element, double value) + { + element.SetValue(HorizontalOffsetProperty, value); + } + + /// + /// Gets the value of the ToolTip.VerticalOffset attached property. + /// + /// The control to get the property from. + /// + /// A value indicating how the tool tip is positioned. + /// + public static double GetVerticalOffset(Control element) + { + return element.GetValue(VerticalOffsetProperty); + } + + /// + /// Sets the value of the ToolTip.VerticalOffset attached property. + /// + /// The control to get the property from. + /// A value indicating how the tool tip is positioned. + public static void SetVerticalOffset(Control element, double value) + { + element.SetValue(VerticalOffsetProperty, value); } /// - /// Called when the pointer enters a control with an attached tooltip. + /// Gets the value of the ToolTip.ShowDelay attached property. /// - /// The event sender. - /// The event args. - private static void ControlPointerEnter(object sender, PointerEventArgs e) + /// The control to get the property from. + /// + /// A value indicating the time, in milliseconds, before a tool tip opens. + /// + public static int GetShowDelay(Control element) { - s_current = (Control)sender; - s_show.OnNext(s_current); + return element.GetValue(ShowDelayProperty); } /// - /// Called when the pointer leaves a control with an attached tooltip. + /// Sets the value of the ToolTip.ShowDelay attached property. /// - /// The event sender. - /// The event args. - private static void ControlPointerLeave(object sender, PointerEventArgs e) + /// The control to get the property from. + /// A value indicating the time, in milliseconds, before a tool tip opens. + public static void SetShowDelay(Control element, int value) { - var control = (Control)sender; + element.SetValue(ShowDelayProperty, value); + } + + private static void IsOpenChanged(AvaloniaPropertyChangedEventArgs e) + { + var control = (Control)e.Sender; - if (control == s_current) + if ((bool)e.NewValue) { - if (s_popup != null) + var tip = GetTip(control); + if (tip == null) return; + + var toolTip = control.GetValue(ToolTipProperty); + if (toolTip == null || (tip != toolTip && tip != toolTip.Content)) { - DisposeTooltip(); - s_show.OnNext(null); + toolTip?.Close(); + + toolTip = tip as ToolTip ?? new ToolTip { Content = tip }; + control.SetValue(ToolTipProperty, toolTip); } + + toolTip.Open(control); + } + else + { + var toolTip = control.GetValue(ToolTipProperty); + toolTip?.Close(); } } - private static void DisposeTooltip() + private void Open(Control control) { - if (s_popup != null) - { - // Clear the ToolTip's Content in case it has control content: this will - // reset its visual parent allowing it to be used again. - ((ToolTip)s_popup.Content).Content = null; + Close(); + + _popup = new PopupRoot { Content = this }; + ((ISetLogicalParent)_popup).SetParent(control); + _popup.Position = Popup.GetPosition(control, GetPlacement(control), _popup, + GetHorizontalOffset(control), GetVerticalOffset(control)); + _popup.Show(); + } - // Dispose of the popup. - s_popup.Dispose(); - s_popup = null; + private void Close() + { + if (_popup != null) + { + _popup.Content = null; + _popup.Hide(); + _popup = null; } } } diff --git a/src/Avalonia.Controls/ToolTipService.cs b/src/Avalonia.Controls/ToolTipService.cs new file mode 100644 index 0000000000..bfd7ef0f33 --- /dev/null +++ b/src/Avalonia.Controls/ToolTipService.cs @@ -0,0 +1,98 @@ +using System; +using Avalonia.Input; +using Avalonia.Threading; + +namespace Avalonia.Controls +{ + /// + /// Handeles interaction with controls. + /// + internal sealed class ToolTipService + { + public static ToolTipService Instance { get; } = new ToolTipService(); + + private DispatcherTimer _timer; + + private ToolTipService() { } + + /// + /// called when the property changes on a control. + /// + /// The event args. + internal void TipChanged(AvaloniaPropertyChangedEventArgs e) + { + var control = (Control)e.Sender; + + if (e.OldValue != null) + { + control.PointerEnter -= ControlPointerEnter; + control.PointerLeave -= ControlPointerLeave; + } + + if (e.NewValue != null) + { + control.PointerEnter += ControlPointerEnter; + control.PointerLeave += ControlPointerLeave; + } + } + + /// + /// Called when the pointer enters a control with an attached tooltip. + /// + /// The event sender. + /// The event args. + private void ControlPointerEnter(object sender, PointerEventArgs e) + { + StopTimer(); + + var control = (Control)sender; + var showDelay = ToolTip.GetShowDelay(control); + if (showDelay == 0) + { + Open(control); + } + else + { + StartShowTimer(showDelay, control); + } + } + + /// + /// Called when the pointer leaves a control with an attached tooltip. + /// + /// The event sender. + /// The event args. + private void ControlPointerLeave(object sender, PointerEventArgs e) + { + var control = (Control)sender; + Close(control); + } + + private void StartShowTimer(int showDelay, Control control) + { + _timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(showDelay) }; + _timer.Tick += (o, e) => Open(control); + _timer.Start(); + } + + private void Open(Control control) + { + StopTimer(); + + ToolTip.SetIsOpen(control, true); + } + + private void Close(Control control) + { + StopTimer(); + + ToolTip.SetIsOpen(control, false); + } + + private void StopTimer() + { + _timer?.Stop(); + _timer = null; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index f8db0e2a5b..1af347ab4e 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -10,9 +10,11 @@ using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Layout; using Avalonia.Logging; +using Avalonia.LogicalTree; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Styling; +using Avalonia.Utilities; using Avalonia.VisualTree; using JetBrains.Annotations; @@ -26,7 +28,13 @@ namespace Avalonia.Controls /// It handles scheduling layout, styling and rendering as well as /// tracking the widget's . /// - public abstract class TopLevel : ContentControl, IInputRoot, ILayoutRoot, IRenderRoot, ICloseable, IStyleRoot + public abstract class TopLevel : ContentControl, + IInputRoot, + ILayoutRoot, + IRenderRoot, + ICloseable, + IStyleRoot, + IWeakSubscriber { /// /// Defines the property. @@ -100,7 +108,6 @@ namespace Avalonia.Controls impl.Resized = HandleResized; impl.ScalingChanged = HandleScalingChanged; - _keyboardNavigationHandler?.SetOwner(this); _accessKeyHandler?.SetOwner(this); styler?.ApplyStyles(this); @@ -116,6 +123,14 @@ namespace Avalonia.Controls { _applicationLifecycle.OnExit += OnApplicationExiting; } + + if (((IStyleHost)this).StylingParent is IResourceProvider applicationResources) + { + WeakSubscriptionManager.Subscribe( + applicationResources, + nameof(IResourceProvider.ResourcesChanged), + this); + } } /// @@ -165,6 +180,11 @@ namespace Avalonia.Controls /// IMouseDevice IInputRoot.MouseDevice => PlatformImpl?.MouseDevice; + void IWeakSubscriber.OnEvent(object sender, ResourcesChangedEventArgs e) + { + ((ILogical)this).NotifyResourcesChanged(e); + } + /// /// Gets or sets a value indicating whether access keys are shown in the window. /// diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 46c625cc4c..9c0245a714 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -61,11 +61,17 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(SizeToContent)); /// - /// Enables of disables system window decorations (title bar, buttons, etc) + /// Enables or disables system window decorations (title bar, buttons, etc) /// public static readonly StyledProperty HasSystemDecorationsProperty = AvaloniaProperty.Register(nameof(HasSystemDecorations), true); - + + /// + /// Enables or disables the taskbar icon + /// + public static readonly StyledProperty ShowInTaskbarProperty = + AvaloniaProperty.Register(nameof(ShowInTaskbar), true); + /// /// Defines the property. /// @@ -92,6 +98,8 @@ namespace Avalonia.Controls HasSystemDecorationsProperty.Changed.AddClassHandler( (s, e) => s.PlatformImpl?.SetSystemDecorations((bool) e.NewValue)); + ShowInTaskbarProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.ShowTaskbarIcon((bool)e.NewValue)); + IconProperty.Changed.AddClassHandler((s, e) => s.PlatformImpl?.SetIcon(((WindowIcon)e.NewValue).PlatformImpl)); } @@ -152,7 +160,7 @@ namespace Avalonia.Controls } /// - /// Enables of disables system window decorations (title bar, buttons, etc) + /// Enables or disables system window decorations (title bar, buttons, etc) /// /// public bool HasSystemDecorations @@ -160,6 +168,16 @@ namespace Avalonia.Controls get { return GetValue(HasSystemDecorationsProperty); } set { SetValue(HasSystemDecorationsProperty, value); } } + + /// + /// Enables or disables the taskbar icon + /// + /// + public bool ShowInTaskbar + { + get { return GetValue(ShowInTaskbarProperty); } + set { SetValue(ShowInTaskbarProperty, value); } + } /// /// Gets or sets the minimized/maximized state of the window. diff --git a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj index a68e8760f2..63cb8132a9 100644 --- a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj +++ b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj @@ -1,6 +1,6 @@  - netstandard1.3 + netstandard2.0 false diff --git a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj index be3f397283..5cb56e0be1 100644 --- a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj +++ b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj @@ -1,6 +1,6 @@  - netstandard1.3 + netstandard2.0 false diff --git a/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj b/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj index 36443ce6f3..5c72d433ce 100644 --- a/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj +++ b/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj @@ -1,6 +1,6 @@  - netcoreapp1.0 + netcoreapp2.0 false diff --git a/src/Avalonia.DotNetFrameworkRuntime/Avalonia.DotNetFrameworkRuntime.csproj b/src/Avalonia.DotNetFrameworkRuntime/Avalonia.DotNetFrameworkRuntime.csproj index e2c866fe3d..181f5e3a1e 100644 --- a/src/Avalonia.DotNetFrameworkRuntime/Avalonia.DotNetFrameworkRuntime.csproj +++ b/src/Avalonia.DotNetFrameworkRuntime/Avalonia.DotNetFrameworkRuntime.csproj @@ -1,75 +1,21 @@ - - - - - Debug - AnyCPU - {4A1ABB09-9047-4BD5-A4AD-A055E52C5EE0} - Library - Properties - Avalonia.DotNetFrameworkRuntime - Avalonia.DotNetFrameworkRuntime - v4.6.1 - 512 - - - - true - full - false - bin\Debug\ - TRACE;DEBUG;FULLDOTNET - prompt - 4 - bin\Debug\Avalonia.DotNetFrameworkRuntime.xml - - - pdbonly - true - bin\Release\ - TRACE;FULLDOTNET - prompt - 4 - bin\Release\Avalonia.DotNetFrameworkRuntime.xml - true - - - - - - - - - - - - - - Properties\SharedAssemblyInfo.cs - - - - - - - - {B09B78D8-9B26-48B0-9149-D64A2F120F3F} - Avalonia.Base - - - {D2221C82-4A25-4583-9B43-D791E3F6820C} - Avalonia.Controls - - - {eb582467-6abb-43a1-b052-e981ba910e3a} - Avalonia.Visuals - - - {f1baa01a-f176-4c6a-b39d-5b40bb1b148f} - Avalonia.Styling - - - - - + + + net461 + false + bin\$(Configuration)\Avalonia.DotNetFrameworkRuntime.xml + $(DefineConstants);FULLDOTNET + true + + + + Properties\SharedAssemblyInfo.cs + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia.DotNetFrameworkRuntime/Properties/AssemblyInfo.cs b/src/Avalonia.DotNetFrameworkRuntime/Properties/AssemblyInfo.cs deleted file mode 100644 index 3a91d50a24..0000000000 --- a/src/Avalonia.DotNetFrameworkRuntime/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Reflection; -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("Avalonia.DotNetFrameworkRuntime")] - -// 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("4a1abb09-9047-4bd5-a4ad-a055e52c5ee0")] diff --git a/src/Avalonia.HtmlRenderer/Avalonia.HtmlRenderer.csproj b/src/Avalonia.HtmlRenderer/Avalonia.HtmlRenderer.csproj index e7822324a9..fde486fa12 100644 --- a/src/Avalonia.HtmlRenderer/Avalonia.HtmlRenderer.csproj +++ b/src/Avalonia.HtmlRenderer/Avalonia.HtmlRenderer.csproj @@ -1,6 +1,6 @@  - netstandard1.3 + netstandard2.0 False False false diff --git a/src/Avalonia.HtmlRenderer/Compat/Api.cs b/src/Avalonia.HtmlRenderer/Compat/Api.cs index 798b697277..d4f4c6abbe 100644 --- a/src/Avalonia.HtmlRenderer/Compat/Api.cs +++ b/src/Avalonia.HtmlRenderer/Compat/Api.cs @@ -4,18 +4,6 @@ using System.Text; namespace System.Net { - internal class AsyncCompletedEventArgs - { - public object UserState { get; set; } - public Exception Error { get; set; } - public bool Cancelled { get; set; } - - public AsyncCompletedEventArgs(Exception error, bool cancelled, object userState) - { - - } - } - class WebException : Exception { public object Response { get; set; } diff --git a/src/Avalonia.Input/Avalonia.Input.csproj b/src/Avalonia.Input/Avalonia.Input.csproj index b5482ebce1..901f7b5675 100644 --- a/src/Avalonia.Input/Avalonia.Input.csproj +++ b/src/Avalonia.Input/Avalonia.Input.csproj @@ -1,6 +1,6 @@  - netstandard1.3 + netstandard2.0 false diff --git a/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj b/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj index 9d22de86b3..d683c77468 100644 --- a/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj +++ b/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj @@ -1,6 +1,6 @@  - netstandard1.3 + netstandard2.0 false diff --git a/src/Avalonia.Layout/Avalonia.Layout.csproj b/src/Avalonia.Layout/Avalonia.Layout.csproj index d0260391d8..810a985e36 100644 --- a/src/Avalonia.Layout/Avalonia.Layout.csproj +++ b/src/Avalonia.Layout/Avalonia.Layout.csproj @@ -1,6 +1,6 @@  - netstandard1.3 + netstandard2.0 false diff --git a/src/Avalonia.Logging.Serilog/Avalonia.Logging.Serilog.csproj b/src/Avalonia.Logging.Serilog/Avalonia.Logging.Serilog.csproj index 9ac40cba07..b9fa2f208b 100644 --- a/src/Avalonia.Logging.Serilog/Avalonia.Logging.Serilog.csproj +++ b/src/Avalonia.Logging.Serilog/Avalonia.Logging.Serilog.csproj @@ -1,6 +1,6 @@  - netstandard1.3 + netstandard2.0 false diff --git a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs new file mode 100644 index 0000000000..e081763e99 --- /dev/null +++ b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs @@ -0,0 +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.Controls; +using Avalonia.Threading; +using ReactiveUI; +using System; +using System.Reactive.Concurrency; +using System.Threading; + +namespace Avalonia +{ + public static class AppBuilderExtensions + { + public static TAppBuilder UseReactiveUI(this TAppBuilder builder) + where TAppBuilder : AppBuilderBase, new() + { + return builder.AfterSetup(_ => + { + RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; + }); + } + } +} diff --git a/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj b/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj index 22d815d786..c29dbcd381 100644 --- a/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj +++ b/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj @@ -1,83 +1,10 @@  - netstandard1.3 - False + netstandard2.0 false - - true - full - false - bin\Debug\ - TRACE;DEBUG;MONO PORTABLE;NETSTANDARD1_1 - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE;MONO PORTABLE;RELEASE;NETSTANDARD1_1 - prompt - 4 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -88,5 +15,6 @@ + \ No newline at end of file diff --git a/src/Avalonia.ReactiveUI/Properties/AssemblyInfo.cs b/src/Avalonia.ReactiveUI/Properties/AssemblyInfo.cs index c8a5c5cc41..c9ead6f6e6 100644 --- a/src/Avalonia.ReactiveUI/Properties/AssemblyInfo.cs +++ b/src/Avalonia.ReactiveUI/Properties/AssemblyInfo.cs @@ -3,7 +3,4 @@ using System.Reflection; -// 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("Avalonia.ReactiveUI")] diff --git a/src/Avalonia.ReactiveUI/Registrations.cs b/src/Avalonia.ReactiveUI/Registrations.cs deleted file mode 100644 index 066aa46dc1..0000000000 --- a/src/Avalonia.ReactiveUI/Registrations.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Reactive.Concurrency; -using System.Threading; - - -namespace ReactiveUI -{ - /// - /// Ignore me. This class is a secret handshake between RxUI and RxUI.Xaml - /// in order to register certain classes on startup that would be difficult - /// to register otherwise. - /// - public class PlatformRegistrations : IWantsToRegisterStuff - { - public void Register(Action, Type> registerFunction) - { - RxApp.MainThreadScheduler = new SynchronizationContextScheduler(SynchronizationContext.Current); - } - } -} diff --git a/src/Avalonia.ReactiveUI/Shims.cs b/src/Avalonia.ReactiveUI/Shims.cs deleted file mode 100644 index 2729a68d6f..0000000000 --- a/src/Avalonia.ReactiveUI/Shims.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace System.Runtime.Serialization -{ - class IgnoreDataMemberAttribute : Attribute - { - } - - class DataMemberAttribute : Attribute - { - } - class OnDeserializedAttribute : Attribute - { - } - - class DataContractAttribute : Attribute - { - } - - class StreamingContext { } -} - -namespace System.Diagnostics.Contracts -{ - static class Contract - { - public static void Requires(bool condition) - { - - } - } -} diff --git a/src/Avalonia.ReactiveUI/src b/src/Avalonia.ReactiveUI/src deleted file mode 160000 index 3f725c808b..0000000000 --- a/src/Avalonia.ReactiveUI/src +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3f725c808b1d4c8457f0d3204e0a071aa462cd75 diff --git a/src/Avalonia.Styling/Avalonia.Styling.csproj b/src/Avalonia.Styling/Avalonia.Styling.csproj index 6bf37b522b..6c0b957bf4 100644 --- a/src/Avalonia.Styling/Avalonia.Styling.csproj +++ b/src/Avalonia.Styling/Avalonia.Styling.csproj @@ -1,7 +1,8 @@  - netstandard1.3 + netstandard2.0 false + Avalonia true diff --git a/src/Avalonia.Styling/Controls/IResourceDictionary.cs b/src/Avalonia.Styling/Controls/IResourceDictionary.cs new file mode 100644 index 0000000000..d7c86b7d74 --- /dev/null +++ b/src/Avalonia.Styling/Controls/IResourceDictionary.cs @@ -0,0 +1,19 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; + +namespace Avalonia.Controls +{ + /// + /// An indexed dictionary of resources. + /// + public interface IResourceDictionary : IResourceProvider, IDictionary + { + /// + /// Gets a collection of child resource dictionaries. + /// + IList MergedDictionaries { get; } + } +} diff --git a/src/Avalonia.Styling/Controls/IResourceNode.cs b/src/Avalonia.Styling/Controls/IResourceNode.cs new file mode 100644 index 0000000000..b6a6cdc3e3 --- /dev/null +++ b/src/Avalonia.Styling/Controls/IResourceNode.cs @@ -0,0 +1,15 @@ +using System; + +namespace Avalonia.Controls +{ + /// + /// Represents resource provider in a tree. + /// + public interface IResourceNode : IResourceProvider + { + /// + /// Gets the parent resource node, if any. + /// + IResourceNode ResourceParent { get; } + } +} diff --git a/src/Avalonia.Styling/Controls/IResourceProvider.cs b/src/Avalonia.Styling/Controls/IResourceProvider.cs new file mode 100644 index 0000000000..eec783623c --- /dev/null +++ b/src/Avalonia.Styling/Controls/IResourceProvider.cs @@ -0,0 +1,33 @@ +using System; + +namespace Avalonia.Controls +{ + /// + /// Represents an object that can be queried for resources. + /// + public interface IResourceProvider + { + /// + /// Raised when resources in the provider are changed. + /// + event EventHandler ResourcesChanged; + + /// + /// Gets a value indicating whether the element has resources. + /// + bool HasResources { get; } + + /// + /// Tries to find a resource within the provider. + /// + /// The resource key. + /// + /// When this method returns, contains the value associated with the specified key, + /// if the key is found; otherwise, null. + /// + /// + /// True if the resource if found, otherwise false. + /// + bool TryGetResource(string key, out object value); + } +} diff --git a/src/Avalonia.Styling/Controls/ResourceDictionary.cs b/src/Avalonia.Styling/Controls/ResourceDictionary.cs new file mode 100644 index 0000000000..74a861b36b --- /dev/null +++ b/src/Avalonia.Styling/Controls/ResourceDictionary.cs @@ -0,0 +1,101 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using Avalonia.Collections; + +namespace Avalonia.Controls +{ + /// + /// An indexed dictionary of resources. + /// + public class ResourceDictionary : AvaloniaDictionary, IResourceDictionary + { + private AvaloniaList _mergedDictionaries; + + /// + /// Initializes a new instance of the class. + /// + public ResourceDictionary() + { + CollectionChanged += OnCollectionChanged; + } + + /// + public event EventHandler ResourcesChanged; + + /// + public IList MergedDictionaries + { + get + { + if (_mergedDictionaries == null) + { + _mergedDictionaries = new AvaloniaList(); + _mergedDictionaries.ResetBehavior = ResetBehavior.Remove; + _mergedDictionaries.ForEachItem( + x => + { + if (x.HasResources) + { + OnResourcesChanged(); + } + + x.ResourcesChanged += MergedDictionaryResourcesChanged; + }, + x => + { + if (x.HasResources) + { + OnResourcesChanged(); + } + + x.ResourcesChanged -= MergedDictionaryResourcesChanged; + }, + () => { }); + } + + return _mergedDictionaries; + } + } + + /// + bool IResourceProvider.HasResources + { + get => Count > 0 || (_mergedDictionaries?.Any(x => x.HasResources) ?? false); + } + + /// + public bool TryGetResource(string key, out object value) + { + if (TryGetValue(key, out value)) + { + return true; + } + + if (_mergedDictionaries != null) + { + for (var i = _mergedDictionaries.Count - 1; i >= 0; --i) + { + if (_mergedDictionaries[i].TryGetResource(key, out value)) + { + return true; + } + } + } + + return false; + } + + private void OnResourcesChanged() + { + ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); + } + + private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) => OnResourcesChanged(); + private void MergedDictionaryResourcesChanged(object sender, ResourcesChangedEventArgs e) => OnResourcesChanged(); + } +} diff --git a/src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs b/src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs new file mode 100644 index 0000000000..1f25fa132d --- /dev/null +++ b/src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs @@ -0,0 +1,65 @@ +using System; +using System.Reactive; +using System.Reactive.Linq; + +namespace Avalonia.Controls +{ + public static class ResourceProviderExtensions + { + /// + /// Finds the specified resource by searching up the logical tree and then global styles. + /// + /// The control. + /// The resource key. + /// The resource, or if not found. + public static object FindResource(this IResourceNode control, string key) + { + if (control.TryFindResource(key, out var value)) + { + return value; + } + + return AvaloniaProperty.UnsetValue; + } + + /// + /// Tries to the specified resource by searching up the logical tree and then global styles. + /// + /// The control. + /// The resource key. + /// On return, contains the resource if found, otherwise null. + /// True if the resource was found; otherwise false. + public static bool TryFindResource(this IResourceNode control, string key, out object value) + { + Contract.Requires(control != null); + Contract.Requires(key != null); + + var current = control; + + while (current != null) + { + if (current is IResourceNode host) + { + if (host.TryGetResource(key, out value)) + { + return true; + } + } + + current = current.ResourceParent; + } + + value = null; + return false; + } + + public static IObservable GetResourceObservable(this IResourceNode target, string key) + { + return Observable.FromEventPattern( + x => target.ResourcesChanged += x, + x => target.ResourcesChanged -= x) + .StartWith((EventPattern)null) + .Select(x => target.FindResource(key)); + } + } +} diff --git a/src/Avalonia.Styling/Controls/ResourcesChangedEventArgs.cs b/src/Avalonia.Styling/Controls/ResourcesChangedEventArgs.cs new file mode 100644 index 0000000000..68c7a58ab9 --- /dev/null +++ b/src/Avalonia.Styling/Controls/ResourcesChangedEventArgs.cs @@ -0,0 +1,11 @@ +// 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.Controls +{ + public class ResourcesChangedEventArgs : EventArgs + { + } +} diff --git a/src/Avalonia.Styling/LogicalTree/ILogical.cs b/src/Avalonia.Styling/LogicalTree/ILogical.cs index 006a9f5cc1..8ee3c9ea4f 100644 --- a/src/Avalonia.Styling/LogicalTree/ILogical.cs +++ b/src/Avalonia.Styling/LogicalTree/ILogical.cs @@ -3,6 +3,7 @@ using System; using Avalonia.Collections; +using Avalonia.Controls; namespace Avalonia.LogicalTree { @@ -55,5 +56,15 @@ namespace Avalonia.LogicalTree /// this method yourself. /// void NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e); + + /// + /// Notifies the control that a change has been made to resources that apply to it. + /// + /// The event args. + /// + /// This method will be called automatically by the framework, you should not need to call + /// this method yourself. + /// + void NotifyResourcesChanged(ResourcesChangedEventArgs e); } } diff --git a/src/Avalonia.Styling/Properties/AssemblyInfo.cs b/src/Avalonia.Styling/Properties/AssemblyInfo.cs index b53681aeed..0a639139f7 100644 --- a/src/Avalonia.Styling/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Styling/Properties/AssemblyInfo.cs @@ -6,5 +6,7 @@ using System.Runtime.CompilerServices; using Avalonia.Metadata; [assembly: AssemblyTitle("Avalonia.Styling")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.LogicalTree")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Styling")] [assembly: InternalsVisibleTo("Avalonia.Styling.UnitTests")] \ No newline at end of file diff --git a/src/Avalonia.Styling/Styling/ISetStyleParent.cs b/src/Avalonia.Styling/Styling/ISetStyleParent.cs new file mode 100644 index 0000000000..9f5855b401 --- /dev/null +++ b/src/Avalonia.Styling/Styling/ISetStyleParent.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Controls; + +namespace Avalonia.Styling +{ + /// + /// Defines an interface through which a 's parent can be set. + /// + /// + /// You should not usually need to use this interface - it is for internal use only. + /// + public interface ISetStyleParent : IStyle + { + /// + /// Sets the style parent. + /// + /// The parent. + void SetParent(IResourceNode parent); + + /// + /// Notifies the style that a change has been made to resources that apply to it. + /// + /// The event args. + /// + /// This method will be called automatically by the framework, you should not need to call + /// this method yourself. + /// + void NotifyResourcesChanged(ResourcesChangedEventArgs e); + } +} diff --git a/src/Avalonia.Styling/Styling/IStyle.cs b/src/Avalonia.Styling/Styling/IStyle.cs index 7b8510fe2d..5f12763825 100644 --- a/src/Avalonia.Styling/Styling/IStyle.cs +++ b/src/Avalonia.Styling/Styling/IStyle.cs @@ -1,12 +1,14 @@ // 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.Controls; + namespace Avalonia.Styling { /// /// Defines the interface for styles. /// - public interface IStyle + public interface IStyle : IResourceNode { /// /// Attaches the style to a control if the style's selector matches. @@ -16,14 +18,5 @@ namespace Avalonia.Styling /// The control that contains this style. May be null. /// void Attach(IStyleable control, IStyleHost container); - - /// - /// Tries to find a named resource within the style. - /// - /// The resource name. - /// - /// The resource if found, otherwise . - /// - object FindResource(string name); } } diff --git a/src/Avalonia.Styling/Styling/Style.cs b/src/Avalonia.Styling/Styling/Style.cs index 3dfd9118af..4182ffada3 100644 --- a/src/Avalonia.Styling/Styling/Style.cs +++ b/src/Avalonia.Styling/Styling/Style.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Reactive.Linq; +using Avalonia.Controls; using Avalonia.Metadata; namespace Avalonia.Styling @@ -11,12 +13,12 @@ namespace Avalonia.Styling /// /// Defines a style. /// - public class Style : IStyle + public class Style : IStyle, ISetStyleParent { private static Dictionary> _applied = new Dictionary>(); - - private StyleResources _resources; + private IResourceNode _parent; + private IResourceDictionary _resources; /// /// Initializes a new instance of the class. @@ -34,33 +36,33 @@ namespace Avalonia.Styling Selector = selector(null); } + /// + public event EventHandler ResourcesChanged; + /// /// Gets or sets a dictionary of style resources. /// - public StyleResources Resources + public IResourceDictionary Resources { - get + get => _resources ?? (Resources = new ResourceDictionary()); + set { - if (_resources == null) + Contract.Requires(value != null); + + var hadResources = false; + + if (_resources != null) { - _resources = new StyleResources(); + hadResources = _resources.Count > 0; + _resources.ResourcesChanged -= ResourceDictionaryChanged; } - return _resources; - } + _resources = value; + _resources.ResourcesChanged += ResourceDictionaryChanged; - set - { - - var resources = Resources; - if (!Equals(resources, value)) + if (hadResources || _resources.Count > 0) { - foreach (var i in value) - { - resources[i.Key] = i.Value; - //resources.Add(i.Key, i.Value); - //(resources as IDictionary).Add(i); - } + ((ISetStyleParent)this).NotifyResourcesChanged(new ResourcesChangedEventArgs()); } } } @@ -76,6 +78,12 @@ namespace Avalonia.Styling [Content] public IList Setters { get; set; } = new List(); + /// + IResourceNode IResourceNode.ResourceParent => _parent; + + /// + bool IResourceProvider.HasResources => _resources?.Count > 0; + /// /// Attaches the style to a control if the style's selector matches. /// @@ -112,25 +120,11 @@ namespace Avalonia.Styling } } - /// - /// Tries to find a named resource within the style. - /// - /// The resource name. - /// - /// The resource if found, otherwise . - /// - public object FindResource(string name) + /// + public bool TryGetResource(string key, out object result) { - object result = null; - - if (_resources?.TryGetValue(name, out result) == true) - { - return result; - } - else - { - return AvaloniaProperty.UnsetValue; - } + result = null; + return _resources?.TryGetResource(key, out result) ?? false; } /// @@ -149,6 +143,23 @@ namespace Avalonia.Styling } } + /// + void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e) + { + ResourcesChanged?.Invoke(this, e); + } + + /// + void ISetStyleParent.SetParent(IResourceNode parent) + { + if (_parent != null && parent != null) + { + throw new InvalidOperationException("The Style already has a parent."); + } + + _parent = parent; + } + private static List GetSubscriptions(IStyleable control) { List subscriptions; @@ -179,5 +190,10 @@ namespace Avalonia.Styling _applied.Remove(control); } + + private void ResourceDictionaryChanged(object sender, ResourcesChangedEventArgs e) + { + ResourcesChanged?.Invoke(this, e); + } } } diff --git a/src/Avalonia.Styling/Styling/StyleExtensions.cs b/src/Avalonia.Styling/Styling/StyleExtensions.cs deleted file mode 100644 index d53d00aed3..0000000000 --- a/src/Avalonia.Styling/Styling/StyleExtensions.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; - -namespace Avalonia.Styling -{ - public static class StyleExtensions - { - /// - /// Tries to find a named style resource. - /// - /// The control from which to find the resource. - /// The resource name. - /// - /// The resource if found, otherwise . - /// - public static object FindStyleResource(this IStyleHost control, string name) - { - Contract.Requires(control != null); - Contract.Requires(name != null); - Contract.Requires(!string.IsNullOrWhiteSpace(name)); - - while (control != null) - { - if (control.IsStylesInitialized) - { - var result = control.Styles.FindResource(name); - - if (result != AvaloniaProperty.UnsetValue) - { - return result; - } - } - - control = control.StylingParent; - } - - return AvaloniaProperty.UnsetValue; - } - } -} diff --git a/src/Avalonia.Styling/Styling/StyleResources.cs b/src/Avalonia.Styling/Styling/StyleResources.cs deleted file mode 100644 index e447c6adfd..0000000000 --- a/src/Avalonia.Styling/Styling/StyleResources.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; - -namespace Avalonia.Styling -{ - /// - /// Holds resources for a . - /// - public class StyleResources : IDictionary, IDictionary - { - private Dictionary _inner = new Dictionary(); - - public object this[string key] - { - get { return _inner[key]; } - set { _inner[key] = value; } - } - - public int Count => _inner.Count; - - ICollection IDictionary.Keys => _inner.Keys; - - ICollection IDictionary.Values => _inner.Values; - - bool ICollection>.IsReadOnly => false; - - object IDictionary.this[object key] - { - get { return ((IDictionary)_inner)[key]; } - set { ((IDictionary)_inner)[key] = value; } - } - - ICollection IDictionary.Keys => _inner.Keys; - - ICollection IDictionary.Values => _inner.Values; - - bool ICollection.IsSynchronized => false; - - object ICollection.SyncRoot => ((IDictionary)_inner).SyncRoot; - - bool IDictionary.IsFixedSize => false; - - bool IDictionary.IsReadOnly => false; - - public void Add(string key, object value) => _inner.Add(key, value); - - public void Clear() => _inner.Clear(); - - public bool ContainsKey(string key) => _inner.ContainsKey(key); - - public bool Remove(string key) => _inner.Remove(key); - - public IEnumerator> GetEnumerator() => _inner.GetEnumerator(); - - public bool TryGetValue(string key, out object value) => _inner.TryGetValue(key, out value); - - bool ICollection>.Contains(KeyValuePair item) - { - return ((IDictionary)_inner).Contains(item); - } - - void ICollection>.Add(KeyValuePair item) - { - ((IDictionary)_inner).Add(item); - } - - void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) - { - ((IDictionary)_inner).CopyTo(array, arrayIndex); - } - - bool ICollection>.Remove(KeyValuePair item) - { - return ((IDictionary)_inner).Remove(item); - } - - void ICollection.CopyTo(Array array, int index) => ((IDictionary)_inner).CopyTo(array, index); - - IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator(); - - IDictionaryEnumerator IDictionary.GetEnumerator() => ((IDictionary)_inner).GetEnumerator(); - - void IDictionary.Add(object key, object value) => ((IDictionary)_inner).Add(key, value); - - bool IDictionary.Contains(object key) => ((IDictionary)_inner).Contains(key); - - void IDictionary.Remove(object key) => ((IDictionary)_inner).Remove(key); - } -} diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Styling/Styling/Styles.cs index 770ef8344f..714e7f6def 100644 --- a/src/Avalonia.Styling/Styling/Styles.cs +++ b/src/Avalonia.Styling/Styling/Styles.cs @@ -1,16 +1,95 @@ // 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.Linq; using Avalonia.Collections; +using Avalonia.Controls; namespace Avalonia.Styling { /// /// A style that consists of a number of child styles. /// - public class Styles : AvaloniaList, IStyle + public class Styles : AvaloniaList, IStyle, ISetStyleParent { + private IResourceNode _parent; + private IResourceDictionary _resources; + + public Styles() + { + ResetBehavior = ResetBehavior.Remove; + this.ForEachItem( + x => + { + if (x.ResourceParent == null && x is ISetStyleParent setParent) + { + setParent.SetParent(this); + setParent.NotifyResourcesChanged(new ResourcesChangedEventArgs()); + } + + if (x.HasResources) + { + ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); + } + + x.ResourcesChanged += SubResourceChanged; + }, + x => + { + if (x.ResourceParent == this && x is ISetStyleParent setParent) + { + setParent.SetParent(null); + setParent.NotifyResourcesChanged(new ResourcesChangedEventArgs()); + } + + if (x.HasResources) + { + ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); + } + + x.ResourcesChanged -= SubResourceChanged; + }, + () => { }); + } + + /// + public event EventHandler ResourcesChanged; + + /// + public bool HasResources => _resources?.Count > 0 || this.Any(x => x.HasResources); + + /// + /// Gets or sets a dictionary of style resources. + /// + public IResourceDictionary Resources + { + get => _resources ?? (Resources = new ResourceDictionary()); + set + { + Contract.Requires(value != null); + + var hadResources = false; + + if (_resources != null) + { + hadResources = _resources.Count > 0; + _resources.ResourcesChanged -= ResourceDictionaryChanged; + } + + _resources = value; + _resources.ResourcesChanged += ResourceDictionaryChanged; + + if (hadResources || _resources.Count > 0) + { + ((ISetStyleParent)this).NotifyResourcesChanged(new ResourcesChangedEventArgs()); + } + } + } + + /// + IResourceNode IResourceNode.ResourceParent => _parent; + /// /// Attaches the style to a control if the style's selector matches. /// @@ -26,26 +105,68 @@ namespace Avalonia.Styling } } - /// - /// Tries to find a named resource within the style. - /// - /// The resource name. - /// - /// The resource if found, otherwise . - /// - public object FindResource(string name) + /// + public bool TryGetResource(string key, out object value) + { + if (_resources != null && _resources.TryGetValue(key, out value)) + { + return true; + } + + for (var i = Count - 1; i >= 0; --i) + { + if (this[i].TryGetResource(key, out value)) + { + return true; + } + } + + value = null; + return false; + } + + /// + void ISetStyleParent.SetParent(IResourceNode parent) + { + if (_parent != null && parent != null) + { + throw new InvalidOperationException("The Style already has a parent."); + } + + _parent = parent; + } + + /// + void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e) + { + ResourcesChanged?.Invoke(this, e); + } + + private void ResourceDictionaryChanged(object sender, ResourcesChangedEventArgs e) { - foreach (var style in this.Reverse()) + foreach (var child in this) { - var result = style.FindResource(name); + (child as ISetStyleParent)?.NotifyResourcesChanged(e); + } + + ResourcesChanged?.Invoke(this, e); + } - if (result != AvaloniaProperty.UnsetValue) + private void SubResourceChanged(object sender, ResourcesChangedEventArgs e) + { + var foundSource = false; + + foreach (var child in this) + { + if (foundSource) { - return result; + (child as ISetStyleParent)?.NotifyResourcesChanged(e); } + + foundSource |= child == sender; } - return AvaloniaProperty.UnsetValue; + ResourcesChanged?.Invoke(this, e); } } } diff --git a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj index 4e980680d9..b4b200834b 100644 --- a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj +++ b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj @@ -1,6 +1,6 @@  - netstandard1.3 + netstandard2.0 false diff --git a/src/Avalonia.Visuals/Avalonia.Visuals.csproj b/src/Avalonia.Visuals/Avalonia.Visuals.csproj index 127760d8ac..fe18b0e446 100644 --- a/src/Avalonia.Visuals/Avalonia.Visuals.csproj +++ b/src/Avalonia.Visuals/Avalonia.Visuals.csproj @@ -1,6 +1,6 @@  - netstandard1.3 + netstandard2.0 false Avalonia diff --git a/src/Gtk/Avalonia.Gtk/WindowImpl.cs b/src/Gtk/Avalonia.Gtk/WindowImpl.cs index d8555b4e05..2c1826c509 100644 --- a/src/Gtk/Avalonia.Gtk/WindowImpl.cs +++ b/src/Gtk/Avalonia.Gtk/WindowImpl.cs @@ -6,11 +6,12 @@ using Gdk; namespace Avalonia.Gtk { using Gtk = global::Gtk; + public class WindowImpl : TopLevelImpl, IWindowImpl { private Gtk.Window _window; - private Gtk.Window Window => _window ?? (_window = (Gtk.Window) Widget); - + private Gtk.Window Window => _window ?? (_window = (Gtk.Window)Widget); + public WindowImpl(Gtk.WindowType type) : base(new PlatformHandleAwareWindow(type)) { Init(); @@ -29,8 +30,10 @@ namespace Avalonia.Gtk _lastClientSize = ClientSize; _lastPosition = Position; } + private Size _lastClientSize; private Point _lastPosition; + void OnConfigureEvent(object o, Gtk.ConfigureEventArgs args) { var evnt = args.Event; @@ -44,7 +47,7 @@ namespace Avalonia.Gtk } var newPosition = new Point(evnt.X, evnt.Y); - + if (newPosition != _lastPosition) { PositionChanged(newPosition); @@ -107,10 +110,7 @@ namespace Avalonia.Gtk Window.GetPosition(out x, out y); return new Point(x, y); } - set - { - Window.Move((int)value.X, (int)value.Y); - } + set { Window.Move((int)value.X, (int)value.Y); } } public IDisposable ShowDialog() @@ -127,5 +127,7 @@ namespace Avalonia.Gtk { Window.Icon = ((IconImpl)icon).Pixbuf; } + + public void ShowTaskbarIcon(bool value) => Window.SkipTaskbarHint = !value; } -} +} \ No newline at end of file diff --git a/src/Gtk/Avalonia.Gtk3/Avalonia.Gtk3.csproj b/src/Gtk/Avalonia.Gtk3/Avalonia.Gtk3.csproj index 0f4b8c2e1b..b58a83b3d2 100644 --- a/src/Gtk/Avalonia.Gtk3/Avalonia.Gtk3.csproj +++ b/src/Gtk/Avalonia.Gtk3/Avalonia.Gtk3.csproj @@ -1,6 +1,6 @@  - netstandard1.3 + netstandard2.0 False false diff --git a/src/Gtk/Avalonia.Gtk3/Interop/Native.cs b/src/Gtk/Avalonia.Gtk3/Interop/Native.cs index fb1a9955e3..ffc1d380a3 100644 --- a/src/Gtk/Avalonia.Gtk3/Interop/Native.cs +++ b/src/Gtk/Avalonia.Gtk3/Interop/Native.cs @@ -38,6 +38,9 @@ namespace Avalonia.Gtk3.Interop [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate void gtk_widget_hide(GtkWidget gtkWidget); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] + public delegate void gtk_widget_show(GtkWidget gtkWidget); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate void gtk_window_set_icon(GtkWindow window, Pixbuf pixbuf); @@ -56,14 +59,17 @@ namespace Avalonia.Gtk3.Interop [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate IntPtr gtk_widget_get_screen(GtkWidget gtkWidget); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate IntPtr gtk_widget_set_double_buffered(GtkWidget gtkWidget, bool value); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate IntPtr gtk_widget_set_events(GtkWidget gtkWidget, uint flags); [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gdk)] public delegate int gdk_screen_get_height(IntPtr screen); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gdk)] public delegate int gdk_screen_get_width(IntPtr screen); @@ -72,10 +78,10 @@ namespace Avalonia.Gtk3.Interop [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gdk)] public delegate int gdk_window_get_origin(IntPtr gdkWindow, out int x, out int y); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gdk)] public delegate void gdk_window_resize(IntPtr gtkWindow, int width, int height); - [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate void gtk_widget_realize(GtkWidget gtkWidget); @@ -86,33 +92,51 @@ namespace Avalonia.Gtk3.Interop [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate void gtk_window_set_decorated(GtkWindow gtkWindow, bool decorated); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] + public delegate void gtk_window_set_skip_taskbar_hint(GtkWindow gtkWindow, bool setting); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] + public delegate bool gtk_window_get_skip_taskbar_hint(GtkWindow gtkWindow); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] + public delegate void gtk_window_set_skip_pager_hint(GtkWindow gtkWindow, bool setting); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] + public delegate bool gtk_window_get_skip_pager_hint(GtkWindow gtkWindow); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate void gtk_window_get_size(GtkWindow gtkWindow, out int width, out int height); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate void gtk_window_resize(GtkWindow gtkWindow, int width, int height); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate void gtk_widget_set_size_request(GtkWidget widget, int width, int height); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate void gtk_window_set_default_size(GtkWindow gtkWindow, int width, int height); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate void gtk_window_get_position(GtkWindow gtkWindow, out int x, out int y); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate void gtk_window_move(GtkWindow gtkWindow, int x, int y); [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate GtkFileChooser gtk_file_chooser_dialog_new(Utf8Buffer title, GtkWindow parent, GtkFileChooserAction action, IntPtr ignore); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public unsafe delegate GSList* gtk_file_chooser_get_filenames(GtkFileChooser chooser); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate void gtk_file_chooser_set_select_multiple(GtkFileChooser chooser, bool allow); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate void gtk_file_chooser_set_filename(GtkFileChooser chooser, Utf8Buffer file); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate void gtk_dialog_add_button(GtkDialog raw, Utf8Buffer button_text, GtkResponseType response_id); - - - [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Cairo)] public delegate CairoSurface cairo_image_surface_create(int format, int width, int height); @@ -206,7 +230,6 @@ namespace Avalonia.Gtk3.Interop [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate void gtk_clipboard_clear(IntPtr clipboard); - [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.GdkPixBuf)] public delegate IntPtr gdk_pixbuf_new_from_file(Utf8Buffer filename, out IntPtr error); @@ -229,21 +252,27 @@ namespace Avalonia.Gtk3.Interop public delegate bool gdk_pixbuf_save_to_bufferv(Pixbuf pixbuf, out IntPtr buffer, out IntPtr buffer_size, Utf8Buffer type, IntPtr option_keys, IntPtr option_values, out IntPtr error); - [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gobject)] public delegate void g_object_unref(IntPtr instance); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gobject)] public delegate void g_object_ref(GObject instance); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gobject)] public delegate ulong g_signal_connect_object(GObject instance, Utf8Buffer signal, IntPtr handler, IntPtr userData, int flags); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gobject)] public delegate ulong g_signal_handler_disconnect(GObject instance, ulong connectionId); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Glib)] public delegate ulong g_timeout_add(uint interval, timeout_callback callback, IntPtr data); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Glib)] public delegate ulong g_free(IntPtr data); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Glib)] public unsafe delegate void g_slist_free(GSList* data); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gio)] public delegate GInputStream g_memory_input_stream_new_from_data(IntPtr ptr, IntPtr len, IntPtr destroyCallback); @@ -271,6 +300,10 @@ namespace Avalonia.Gtk3.Interop public static D.gtk_window_set_decorated GtkWindowSetDecorated; + 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; + public static D.gtk_window_get_skip_pager_hint GtkWindowGetSkipPagerHint; public static D.gtk_window_set_title GtkWindowSetTitle; public static D.gtk_application_new GtkApplicationNew; public static D.gtk_main_iteration GtkMainIteration; @@ -280,6 +313,7 @@ namespace Avalonia.Gtk3.Interop public static D.gtk_init GtkInit; public static D.gtk_window_present GtkWindowPresent; public static D.gtk_widget_hide GtkWidgetHide; + public static D.gtk_widget_show GtkWidgetShow; public static D.gdk_get_native_handle GetNativeGdkWindowHandle; public static D.gtk_widget_get_window GtkWidgetGetWindow; public static D.gtk_widget_get_scale_factor GtkWidgetGetScaleFactor; @@ -314,8 +348,7 @@ namespace Avalonia.Gtk3.Interop public static D.gtk_clipboard_request_text GtkClipboardRequestText; public static D.gtk_clipboard_set_text GtkClipboardSetText; public static D.gtk_clipboard_clear GtkClipboardRequestClear; - - + public static D.gtk_im_multicontext_new GtkImMulticontextNew; public static D.gtk_im_context_filter_keypress GtkImContextFilterKeypress; public static D.gtk_im_context_set_client_window GtkImContextSetClientWindow; diff --git a/src/Gtk/Avalonia.Gtk3/Interop/Resolver.cs b/src/Gtk/Avalonia.Gtk3/Interop/Resolver.cs index cf065b86dc..764cbfd6b6 100644 --- a/src/Gtk/Avalonia.Gtk3/Interop/Resolver.cs +++ b/src/Gtk/Avalonia.Gtk3/Interop/Resolver.cs @@ -104,14 +104,16 @@ namespace Avalonia.Gtk3.Interop var path = Custom?.Lookup(dll); if (path == null && Custom?.BasePath != null) path = Path.Combine(Custom.BasePath, name); - - try - { - return loader.LoadLibrary(path); - } - catch (Exception e) + if (path != null) { - exceptions.Add(e); + try + { + return loader.LoadLibrary(path); + } + catch (Exception e) + { + exceptions.Add(e); + } } throw new AggregateException("Unable to load " + dll, exceptions); } diff --git a/src/Gtk/Avalonia.Gtk3/WindowImpl.cs b/src/Gtk/Avalonia.Gtk3/WindowImpl.cs index f083185a84..e6935c4f20 100644 --- a/src/Gtk/Avalonia.Gtk3/WindowImpl.cs +++ b/src/Gtk/Avalonia.Gtk3/WindowImpl.cs @@ -2,6 +2,7 @@ using Avalonia.Controls; using Avalonia.Gtk3.Interop; using Avalonia.Platform; +using System.Runtime.InteropServices; namespace Avalonia.Gtk3 { @@ -59,6 +60,9 @@ namespace Avalonia.Gtk3 //Why do we even have that? } + public void ShowTaskbarIcon(bool value) => Native.GtkWindowSetSkipTaskbarHint(GtkWidget, !value); + + class EmptyDisposable : IDisposable { public void Dispose() diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj b/src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj index 82da3ec090..c38cb0f114 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj +++ b/src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj @@ -1,6 +1,6 @@  - netstandard1.3 + netstandard2.0 true diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 08ea6b6877..de015fc947 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -1,6 +1,6 @@  - netstandard1.3 + netstandard2.0 False false false @@ -34,6 +34,9 @@ + + + @@ -94,6 +97,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/DelayedBinding.cs b/src/Markup/Avalonia.Markup.Xaml/Data/DelayedBinding.cs index 083cb24b11..5ec40fe702 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Data/DelayedBinding.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Data/DelayedBinding.cs @@ -3,9 +3,11 @@ using System; using System.Collections.Generic; +using System.Reflection; using System.Runtime.CompilerServices; using Avalonia.Controls; using Avalonia.Data; +using Avalonia.Logging; namespace Avalonia.Markup.Xaml.Data { @@ -50,7 +52,36 @@ namespace Avalonia.Markup.Xaml.Data target.Initialized += ApplyBindings; } - bindings.Add(new Entry(binding, property)); + bindings.Add(new BindingEntry(property, binding)); + } + } + + /// + /// Adds a delayed value to a control. + /// + /// The control. + /// The property on the control to bind to. + /// A function which returns the value. + public static void Add(IControl target, PropertyInfo property, Func value) + { + if (target.IsInitialized) + { + property.SetValue(target, value(target)); + } + else + { + List bindings; + + if (!_entries.TryGetValue(target, out bindings)) + { + bindings = new List(); + _entries.Add(target, bindings); + + // TODO: Make this a weak event listener. + target.Initialized += ApplyBindings; + } + + bindings.Add(new ClrPropertyValueEntry(property, value)); } } @@ -60,13 +91,13 @@ namespace Avalonia.Markup.Xaml.Data /// The control. public static void ApplyBindings(IControl control) { - List bindings; + List entries; - if (_entries.TryGetValue(control, out bindings)) + if (_entries.TryGetValue(control, out entries)) { - foreach (var binding in bindings) + foreach (var entry in entries) { - control.Bind(binding.Property, binding.Binding); + entry.Apply(control); } _entries.Remove(control); @@ -80,9 +111,14 @@ namespace Avalonia.Markup.Xaml.Data target.Initialized -= ApplyBindings; } - private class Entry + private abstract class Entry + { + public abstract void Apply(IControl control); + } + + private class BindingEntry : Entry { - public Entry(IBinding binding, AvaloniaProperty property) + public BindingEntry(AvaloniaProperty property, IBinding binding) { Binding = binding; Property = property; @@ -90,6 +126,41 @@ namespace Avalonia.Markup.Xaml.Data public IBinding Binding { get; } public AvaloniaProperty Property { get; } + + public override void Apply(IControl control) + { + control.Bind(Property, Binding); + } + } + + private class ClrPropertyValueEntry : Entry + { + public ClrPropertyValueEntry(PropertyInfo property, Func value) + { + Property = property; + Value = value; + } + + public PropertyInfo Property { get; } + public Func Value { get; } + + public override void Apply(IControl control) + { + try + { + Property.SetValue(control, Value(control)); + } + catch (Exception e) + { + Logger.Error( + LogArea.Property, + control, + "Error setting {Property} on {Target}: {Exception}", + Property.Name, + control, + e); + } + } } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/ResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Data/ResourceInclude.cs new file mode 100644 index 0000000000..035765fae0 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/Data/ResourceInclude.cs @@ -0,0 +1,63 @@ +using System; +using System.ComponentModel; +using Avalonia.Controls; +using Portable.Xaml.ComponentModel; +using Portable.Xaml.Markup; + +namespace Avalonia.Markup.Xaml.Data +{ + /// + /// Loads a resource dictionary from a specified URL. + /// + public class ResourceInclude : MarkupExtension, IResourceProvider + { + private Uri _baseUri; + private IResourceDictionary _loaded; + + public event EventHandler ResourcesChanged; + + /// + /// Gets the loaded resource dictionary. + /// + public IResourceDictionary Loaded + { + get + { + if (_loaded == null) + { + var loader = new AvaloniaXamlLoader(); + _loaded = (IResourceDictionary)loader.Load(Source, _baseUri); + + if (_loaded.HasResources) + { + ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); + } + } + + return _loaded; + } + } + + /// + /// Gets or sets the source URL. + /// + public Uri Source { get; set; } + + /// + bool IResourceProvider.HasResources => Loaded.HasResources; + + /// + bool IResourceProvider.TryGetResource(string key, out object value) + { + return Loaded.TryGetResource(key, out value); + } + + /// + public override object ProvideValue(IServiceProvider serviceProvider) + { + var tdc = (ITypeDescriptorContext)serviceProvider; + _baseUri = tdc?.GetBaseUri(); + return this; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/StyleResourceBinding.cs b/src/Markup/Avalonia.Markup.Xaml/Data/StyleResourceBinding.cs index c1a895f797..787aebbdc6 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Data/StyleResourceBinding.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Data/StyleResourceBinding.cs @@ -46,11 +46,14 @@ namespace Avalonia.Markup.Xaml.Data if (host != null) { - resource = host.FindStyleResource(Name); + resource = host.FindResource(Name); } else if (style != null) { - resource = style.FindResource(Name); + if (!style.TryGetResource(Name, out resource)) + { + resource = AvaloniaProperty.UnsetValue; + } } if (resource != AvaloniaProperty.UnsetValue) diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs new file mode 100644 index 0000000000..231778be09 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs @@ -0,0 +1,71 @@ +// 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.ComponentModel; +using System.Linq; +using System.Reactive.Linq; +using Avalonia.Controls; +using Avalonia.Data; +using Portable.Xaml; +using Portable.Xaml.ComponentModel; +using Portable.Xaml.Markup; + +namespace Avalonia.Markup.Xaml.MarkupExtensions +{ + public class DynamicResourceExtension : MarkupExtension, IBinding + { + private IResourceNode _anchor; + + public DynamicResourceExtension() + { + } + + public DynamicResourceExtension(string resourceKey) + { + ResourceKey = resourceKey; + } + + public string ResourceKey { get; set; } + + public override object ProvideValue(IServiceProvider serviceProvider) + { + var context = (ITypeDescriptorContext)serviceProvider; + var provideTarget = context.GetService(); + + if (!(provideTarget.TargetObject is IResourceNode)) + { + _anchor = GetAnchor(context); + } + + return this; + } + + InstancedBinding IBinding.Initiate( + IAvaloniaObject target, + AvaloniaProperty targetProperty, + object anchor, + bool enableDataValidation) + { + var control = target as IResourceNode ?? _anchor; + + if (control != null) + { + return new InstancedBinding(control.GetResourceObservable(ResourceKey)); + } + + return null; + } + + private T GetAnchor(ITypeDescriptorContext context) where T : class + { + var schemaContext = context.GetService().SchemaContext; + var ambientProvider = context.GetService(); + var xamlType = schemaContext.GetXamlType(typeof(T)); + + // We override XamlType.CanAssignTo in BindingXamlType so the results we get back + // from GetAllAmbientValues aren't necessarily of the correct type. + return ambientProvider.GetAllAmbientValues(xamlType).OfType().FirstOrDefault(); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs new file mode 100644 index 0000000000..9089a13656 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -0,0 +1,84 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Reflection; +using Avalonia.Controls; +using Avalonia.Markup.Xaml.Data; +using Portable.Xaml; +using Portable.Xaml.ComponentModel; +using Portable.Xaml.Markup; + +namespace Avalonia.Markup.Xaml.MarkupExtensions +{ + public class StaticResourceExtension : MarkupExtension + { + public StaticResourceExtension() + { + } + + public StaticResourceExtension(string resourceKey) + { + ResourceKey = resourceKey; + } + + public string ResourceKey { get; set; } + + public override object ProvideValue(IServiceProvider serviceProvider) + { + var context = (ITypeDescriptorContext)serviceProvider; + var schemaContext = context.GetService().SchemaContext; + var ambientProvider = context.GetService(); + var resourceProviderType = schemaContext.GetXamlType(typeof(IResourceNode)); + var ambientValues = ambientProvider.GetAllAmbientValues(resourceProviderType); + + // 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 + // from GetAllAmbientValues aren't necessarily of the correct type. + if (ambientValue is IResourceNode resourceProvider) + { + if (resourceProvider is IControl control && control.StylingParent != null) + { + // If we've got to a control that has a StylingParent then it's probably + // a top level control and its StylingParent is pointing to the global + // styles. If this is case just do a FindResource on it. + return control.FindResource(ResourceKey); + } + else if (resourceProvider.TryGetResource(ResourceKey, out var value)) + { + return value; + } + } + } + + // The resource still hasn't been found, so add a delayed one-time binding. + var provideTarget = context.GetService(); + + if (provideTarget.TargetObject is IControl target && + provideTarget.TargetProperty is PropertyInfo property) + { + DelayedBinding.Add(target, property, GetValue); + return AvaloniaProperty.UnsetValue; + } + + throw new KeyNotFoundException($"Static resource '{ResourceKey}' not found."); + } + + private object GetValue(IControl control) + { + return control.FindResource(ResourceKey); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github index dfc5affa5d..eebf9dbb92 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 dfc5affa5d8f4ddf5a7707e3202d5593519de640 +Subproject commit eebf9dbb9275ecc48c18ec24f6fbad8cb494857f diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index 0f4824d493..fb308e62b8 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -3,27 +3,31 @@ using Avalonia.Styling; using System; +using Avalonia.Controls; namespace Avalonia.Markup.Xaml.Styling { /// /// Includes a style from a URL. /// - public class StyleInclude : IStyle + public class StyleInclude : IStyle, ISetStyleParent { private Uri _baseUri; private IStyle _loaded; + private IResourceNode _parent; /// /// Initializes a new instance of the class. /// /// - public StyleInclude(Uri baseUri) { _baseUri = baseUri; } + /// + public event EventHandler ResourcesChanged; + /// /// Gets or sets the source URL. /// @@ -40,12 +44,19 @@ namespace Avalonia.Markup.Xaml.Styling { var loader = new AvaloniaXamlLoader(); _loaded = (IStyle)loader.Load(Source, _baseUri); + (_loaded as ISetStyleParent)?.SetParent(this); } return _loaded; } } + /// + bool IResourceProvider.HasResources => Loaded.HasResources; + + /// + IResourceNode IResourceNode.ResourceParent => _parent; + /// public void Attach(IStyleable control, IStyleHost container) { @@ -55,16 +66,24 @@ namespace Avalonia.Markup.Xaml.Styling } } - /// - /// Tries to find a named resource within the style. - /// - /// The resource name. - /// - /// The resource if found, otherwise . - /// - public object FindResource(string name) + /// + public bool TryGetResource(string key, out object value) => Loaded.TryGetResource(key, out value); + + /// + void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e) { - return Loaded.FindResource(name); + (Loaded as ISetStyleParent)?.NotifyResourcesChanged(e); + } + + /// + void ISetStyleParent.SetParent(IResourceNode parent) + { + if (_parent != null && parent != null) + { + throw new InvalidOperationException("The Style already has a parent."); + } + + _parent = parent; } } } \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj index 3d16196b70..57552f852c 100644 --- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj +++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj @@ -1,6 +1,6 @@  - netstandard1.3 + netstandard2.0 false diff --git a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs index e7402fb23c..092910a08f 100644 --- a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs +++ b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs @@ -12,7 +12,7 @@ namespace Avalonia.Shared.PlatformSupport internal partial class StandardRuntimePlatform : IRuntimePlatform { -#if NETCOREAPP1_0 +#if NETCOREAPP2_0 public void PostThreadPoolItem(Action cb) => ThreadPool.QueueUserWorkItem(_ => cb(), null); #else public Assembly[] GetLoadedAssemblies() => AppDomain.CurrentDomain.GetAssemblies(); diff --git a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj index bae10e4d97..f5ed89d154 100644 --- a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj +++ b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj @@ -1,6 +1,6 @@  - netstandard1.3 + netstandard2.0 false Avalonia.Skia Avalonia.Skia diff --git a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj index 4d656680b1..a84c373886 100644 --- a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj +++ b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj @@ -1,6 +1,6 @@  - netstandard1.3 + netstandard2.0 false diff --git a/src/Windows/Avalonia.Win32.NetStandard/Avalonia.Win32.NetStandard.csproj b/src/Windows/Avalonia.Win32.NetStandard/Avalonia.Win32.NetStandard.csproj index cedf3c4213..bae634b030 100644 --- a/src/Windows/Avalonia.Win32.NetStandard/Avalonia.Win32.NetStandard.csproj +++ b/src/Windows/Avalonia.Win32.NetStandard/Avalonia.Win32.NetStandard.csproj @@ -1,6 +1,6 @@  - netstandard1.3 + netstandard2.0 False false Avalonia.Win32 diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 5473ef9bea..4fed7a34f4 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -699,10 +699,40 @@ namespace Avalonia.Win32.Interop public static extern int GetSystemMetrics(SystemMetric smIndex); [DllImport("user32.dll", SetLastError = true)] - public static extern uint GetWindowLong(IntPtr hWnd, int nIndex); + public static extern uint GetWindowLongPtr(IntPtr hWnd, int nIndex); + + [DllImport("user32.dll", SetLastError = true, EntryPoint = "GetWindowLong")] + public static extern uint GetWindowLong32b(IntPtr hWnd, int nIndex); + + public static uint GetWindowLong(IntPtr hWnd, int nIndex) + { + if(IntPtr.Size == 4) + { + return GetWindowLong32b(hWnd, nIndex); + } + else + { + return GetWindowLongPtr(hWnd, nIndex); + } + } + + [DllImport("user32.dll", SetLastError = true, EntryPoint = "SetWindowLong")] + private static extern uint SetWindowLong32b(IntPtr hWnd, int nIndex, uint value); [DllImport("user32.dll", SetLastError = true)] - public static extern uint SetWindowLong(IntPtr hWnd, int nIndex, uint value); + private static extern uint SetWindowLongPtr(IntPtr hWnd, int nIndex, uint value); + + public static uint SetWindowLong(IntPtr hWnd, int nIndex, uint value) + { + if (IntPtr.Size == 4) + { + return SetWindowLong32b(hWnd, nIndex, value); + } + else + { + return SetWindowLongPtr(hWnd, nIndex, value); + } + } [DllImport("user32.dll", SetLastError = true)] public static extern bool GetWindowPlacement(IntPtr hWnd, ref WINDOWPLACEMENT lpwndpl); @@ -811,14 +841,14 @@ namespace Avalonia.Win32.Interop return SetClassLong64(hWnd, nIndex, dwNewLong); } -#if !NETSTANDARD && !NETSTANDARD1_3 - [ComImport, ClassInterface(ClassInterfaceType.None), TypeLibType(TypeLibTypeFlags.FCanCreate), Guid("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7")] - internal class FileOpenDialogRCW { } + + [DllImport("ole32.dll", PreserveSig = true)] + internal static extern int CoCreateInstance(ref Guid clsid, + IntPtr ignore1, int ignore2, ref Guid iid, [MarshalAs(UnmanagedType.IUnknown), Out] out object pUnkOuter); [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)] internal static extern int SHCreateItemFromParsingName([MarshalAs(UnmanagedType.LPWStr)] string pszPath, IntPtr pbc, ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out IShellItem ppv); -#endif [DllImport("user32.dll", SetLastError = true)] public static extern bool OpenClipboard(IntPtr hWndOwner); @@ -1153,7 +1183,7 @@ namespace Avalonia.Win32.Interop public int flagsEx; } } -#if !NETSTANDARD && !NETSTANDARD1_3 + [ComImport(), Guid("42F85136-DB7E-439C-85F1-E4075D135FC8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IFileDialog { @@ -1253,5 +1283,4 @@ namespace Avalonia.Win32.Interop uint Compare([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, [In] uint hint, out int piOrder); } -#endif } diff --git a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs index 4d8c375aaf..e08fecbbd9 100644 --- a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs +++ b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs @@ -100,17 +100,14 @@ namespace Avalonia.Win32 var pofn = &ofn; // We should save the current directory to restore it later. -#if !NETSTANDARD var currentDirectory = Environment.CurrentDirectory; -#endif + var res = dialog is OpenFileDialog ? UnmanagedMethods.GetOpenFileName(new IntPtr(pofn)) : UnmanagedMethods.GetSaveFileName(new IntPtr(pofn)); // Restore the old current directory, since GetOpenFileName and GetSaveFileName change it after they're called -#if !NETSTANDARD Environment.CurrentDirectory = currentDirectory; -#endif if (!res) return null; @@ -155,15 +152,16 @@ namespace Avalonia.Win32 public Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) { -#if NETSTANDARD - throw new NotImplementedException(); -#else return Task.Factory.StartNew(() => { string result = string.Empty; var hWnd = parent?.Handle?.Handle ?? IntPtr.Zero; - var frm = (IFileDialog)(new UnmanagedMethods.FileOpenDialogRCW()); + var clsid = Guid.Parse("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7"); + var iid = Guid.Parse("42F85136-DB7E-439C-85F1-E4075D135FC8"); + + UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out var unk); + var frm = (IFileDialog)unk; uint options; frm.GetOptions(out options); options |= (uint)(UnmanagedMethods.FOS.FOS_PICKFOLDERS | UnmanagedMethods.FOS.FOS_FORCEFILESYSTEM | UnmanagedMethods.FOS.FOS_NOVALIDATE | UnmanagedMethods.FOS.FOS_NOTESTFILECREATE | UnmanagedMethods.FOS.FOS_DONTADDTORECENT); @@ -214,7 +212,6 @@ namespace Avalonia.Win32 return result; }); -#endif } } } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 4a30d48878..72485379a7 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -750,5 +750,27 @@ namespace Avalonia.Win32 return (int)(ptr.ToInt64() & 0xffffffff); } + + public void ShowTaskbarIcon(bool value) + { + var style = (UnmanagedMethods.WindowStyles)UnmanagedMethods.GetWindowLong(_hwnd, -20); + + style &= ~(UnmanagedMethods.WindowStyles.WS_VISIBLE); + + style |= UnmanagedMethods.WindowStyles.WS_EX_TOOLWINDOW; + if (value) + style |= UnmanagedMethods.WindowStyles.WS_EX_APPWINDOW; + else + style &= ~(UnmanagedMethods.WindowStyles.WS_EX_APPWINDOW); + + WINDOWPLACEMENT windowPlacement = UnmanagedMethods.WINDOWPLACEMENT.Default; + if (UnmanagedMethods.GetWindowPlacement(_hwnd, ref windowPlacement)) + { + //Toggle to make the styles stick + UnmanagedMethods.ShowWindow(_hwnd, ShowWindowCommand.Hide); + UnmanagedMethods.SetWindowLong(_hwnd, -20, (uint)style); + UnmanagedMethods.ShowWindow(_hwnd, windowPlacement.ShowCmd); + } + } } } diff --git a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj index c656801d90..8692cdef42 100644 --- a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj +++ b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj @@ -1,6 +1,6 @@  - net461;netcoreapp1.1 + net461;netcoreapp2.0 Library diff --git a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj index 957cdd7036..8b2f5093cf 100644 --- a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj +++ b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj @@ -1,6 +1,6 @@  - net461;netcoreapp1.1 + net461;netcoreapp2.0 Library diff --git a/tests/Avalonia.Controls.UnitTests/ControlTests_Resources.cs b/tests/Avalonia.Controls.UnitTests/ControlTests_Resources.cs new file mode 100644 index 0000000000..6a6cb48001 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/ControlTests_Resources.cs @@ -0,0 +1,217 @@ +// 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.Controls.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.Styling; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class ControlTests_Resources + { + [Fact] + public void FindResource_Should_Find_Control_Resource() + { + var target = new Control + { + Resources = + { + { "foo", "foo-value" }, + } + }; + + Assert.Equal("foo-value", target.FindResource("foo")); + } + + [Fact] + public void FindResource_Should_Find_Control_Resource_In_Parent() + { + Control target; + + var root = new Decorator + { + Resources = + { + { "foo", "foo-value" }, + }, + Child = target = new Control(), + }; + + Assert.Equal("foo-value", target.FindResource("foo")); + } + + [Fact] + public void FindResource_Should_Find_Application_Resource() + { + Control target; + + var app = new Application + { + Resources = + { + { "foo", "foo-value" }, + }, + }; + + var root = new TestRoot + { + Child = target = new Control(), + StylingParent = app, + }; + + Assert.Equal("foo-value", target.FindResource("foo")); + } + + [Fact] + public void FindResource_Should_Find_Style_Resource() + { + var target = new Control + { + Styles = + { + new Style + { + Resources = + { + { "foo", "foo-value" }, + } + } + }, + Resources = + { + { "bar", "bar-value" }, + }, + }; + + Assert.Equal("foo-value", target.FindResource("foo")); + } + + [Fact] + public void FindResource_Should_Find_Styles_Resource() + { + var target = new Control + { + Styles = + { + new Styles + { + Resources = + { + { "foo", "foo-value" }, + } + } + }, + Resources = + { + { "bar", "bar-value" }, + }, + }; + + Assert.Equal("foo-value", target.FindResource("foo")); + } + + [Fact] + public void FindResource_Should_Find_Application_Style_Resource() + { + Control target; + + var app = new Application + { + Styles = + { + new Style + { + Resources = + { + { "foo", "foo-value" }, + }, + } + }, + Resources = + { + { "bar", "bar-value" }, + }, + }; + + var root = new TestRoot + { + Child = target = new Control(), + StylingParent = app, + }; + + Assert.Equal("foo-value", target.FindResource("foo")); + } + + [Fact] + public void Adding_Resource_Should_Call_Raise_ResourceChanged_On_Logical_Children() + { + Border child; + + var target = new ContentControl + { + Content = child = new Border(), + Template = ContentControlTemplate(), + }; + + var raisedOnTarget = false; + var raisedOnPresenter = false; + var raisedOnChild = false; + + target.Measure(Size.Infinity); + target.ResourcesChanged += (_, __) => raisedOnTarget = true; + target.Presenter.ResourcesChanged += (_, __) => raisedOnPresenter = true; + child.ResourcesChanged += (_, __) => raisedOnChild = true; + + target.Resources.Add("foo", "bar"); + + Assert.True(raisedOnTarget); + Assert.False(raisedOnPresenter); + Assert.True(raisedOnChild); + } + + [Fact] + public void Adding_Resource_To_Styles_Should_Raise_ResourceChanged() + { + var target = new Decorator(); + var raised = false; + + target.ResourcesChanged += (_, __) => raised = true; + target.Styles.Resources.Add("foo", "bar"); + + Assert.True(raised); + } + + [Fact] + public void Adding_Resource_To_Nested_Style_Should_Raise_ResourceChanged() + { + Style style; + var target = new Decorator + { + Styles = + { + (style = new Style()), + } + }; + + var raised = false; + + target.ResourcesChanged += (_, __) => raised = true; + style.Resources.Add("foo", "bar"); + + Assert.True(raised); + } + + private IControlTemplate ContentControlTemplate() + { + return new FuncControlTemplate(x => + new ContentPresenter + { + Name = "PART_ContentPresenter", + [!ContentPresenter.ContentProperty] = x[!ContentControl.ContentProperty], + }); + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index da30336be6..da0719893f 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -219,6 +219,23 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Adding_Resource_To_Application_Should_Raise_ResourcesChanged() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var impl = new Mock(); + impl.SetupAllProperties(); + var target = new TestTopLevel(impl.Object); + var raised = false; + + target.ResourcesChanged += (_, __) => raised = true; + Application.Current.Resources.Add("foo", "bar"); + + Assert.True(raised); + } + } + private FuncControlTemplate CreateTemplate() { return new FuncControlTemplate(x => diff --git a/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj b/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj index 99cf4e98d2..98a1312782 100644 --- a/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj +++ b/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj @@ -1,6 +1,6 @@  - net461;netcoreapp1.1 + net461;netcoreapp2.0 Library diff --git a/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj b/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj index 86c9cf0617..78d2128478 100644 --- a/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj +++ b/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj @@ -1,6 +1,6 @@  - net461;netcoreapp1.1 + net461;netcoreapp2.0 Library diff --git a/tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj b/tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj index 0950856dca..0020ff46d9 100644 --- a/tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj +++ b/tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj @@ -1,6 +1,6 @@  - net461;netcoreapp1.1 + net461;netcoreapp2.0 Library diff --git a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs index cdfb253bf4..3e7ff496a8 100644 --- a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs +++ b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs @@ -162,6 +162,10 @@ namespace Avalonia.Layout.UnitTests private void RegisterServices() { var globalStyles = new Mock(); + var globalStylesResources = globalStyles.As(); + var outObj = (object)10; + globalStylesResources.Setup(x => x.TryGetResource("FontSizeNormal", out outObj)).Returns(true); + var renderInterface = new Mock(); renderInterface.Setup(x => x.CreateFormattedText( diff --git a/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj b/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj index 3ccd3da044..5d3b04e24b 100644 --- a/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj +++ b/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj @@ -1,6 +1,6 @@  - net461;netcoreapp1.1 + net461;netcoreapp2.0 Library diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj index f6f8f6bcb0..03bb71a383 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj @@ -1,6 +1,6 @@  - net461;netcoreapp1.1 + net461;netcoreapp2.0 Library @@ -19,7 +19,6 @@ - diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs index bd86877404..230e61f300 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs @@ -10,8 +10,9 @@ using Avalonia.Data; using Avalonia.Markup.Data; using Avalonia.Markup.Xaml.Data; using Moq; -using ReactiveUI; using Xunit; +using System.ComponentModel; +using System.Runtime.CompilerServices; namespace Avalonia.Markup.Xaml.UnitTests.Data { @@ -350,14 +351,25 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data } } - public class Source : ReactiveObject + public class Source : INotifyPropertyChanged { private string _foo; public string Foo { get { return _foo; } - set { this.RaiseAndSetIfChanged(ref _foo, value); } + set + { + _foo = value; + RaisePropertyChanged(); + } + } + + public event PropertyChangedEventHandler PropertyChanged; + + private void RaisePropertyChanged([CallerMemberName] string prop = "") + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop)); } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Source.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Source.cs index 778a93d326..c6006f3afb 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Source.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Source.cs @@ -6,8 +6,9 @@ using Avalonia.Controls; using Avalonia.Data; using Avalonia.Markup.Data; using Avalonia.Markup.Xaml.Data; -using ReactiveUI; using Xunit; +using System.ComponentModel; +using System.Runtime.CompilerServices; namespace Avalonia.Markup.Xaml.UnitTests.Data { @@ -24,15 +25,26 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data Assert.Equal(target.Text, "foo"); } - - public class Source : ReactiveObject + + public class Source : INotifyPropertyChanged { private string _foo; public string Foo { get { return _foo; } - set { this.RaiseAndSetIfChanged(ref _foo, value); } + set + { + _foo = value; + RaisePropertyChanged(); + } + } + + public event PropertyChangedEventHandler PropertyChanged; + + private void RaisePropertyChanged([CallerMemberName] string prop = "") + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop)); } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/ResourceIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/ResourceIncludeTests.cs new file mode 100644 index 0000000000..6dc56e425c --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/ResourceIncludeTests.cs @@ -0,0 +1,55 @@ +using System; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Data +{ + public class ResourceIncludeTests + { + public class StaticResourceExtensionTests + { + [Fact] + public void ResourceInclude_Loads_ResourceDictionary() + { + var includeXaml = @" + + #ff506070 + +"; + using (StartWithResources(("test:include.xaml", includeXaml))) + { + var xaml = @" + + + + + + + + + + +"; + + var loader = new AvaloniaXamlLoader(); + var userControl = (UserControl)loader.Load(xaml); + var border = userControl.FindControl("border"); + + var brush = (SolidColorBrush)border.Background; + Assert.Equal(0xff506070, brush.Color.ToUint32()); + } + } + + private IDisposable StartWithResources(params (string, string)[] assets) + { + var assetLoader = new MockAssetLoader(assets); + var services = new TestServices(assetLoader: assetLoader); + return UnitTestApplication.Start(services); + } + } + } +} \ No newline at end of file diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs new file mode 100644 index 0000000000..d55e34cbe6 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs @@ -0,0 +1,660 @@ +// 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.Linq; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.Markup.Xaml.Data; +using Avalonia.Media; +using Avalonia.Styling; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions +{ + public class DynamicResourceExtensionTests + { + [Fact] + public void DynamicResource_Can_Be_Assigned_To_Property() + { + var xaml = @" + + + #ff506070 + + + +"; + + var loader = new AvaloniaXamlLoader(); + var userControl = (UserControl)loader.Load(xaml); + var border = userControl.FindControl("border"); + + DelayedBinding.ApplyBindings(border); + + var brush = (SolidColorBrush)border.Background; + Assert.Equal(0xff506070, brush.Color.ToUint32()); + } + + [Fact] + public void DynamicResource_Can_Be_Assigned_To_Attached_Property() + { + var xaml = @" + + + 5 + + + +"; + + var loader = new AvaloniaXamlLoader(); + var userControl = (UserControl)loader.Load(xaml); + var border = userControl.FindControl("border"); + + DelayedBinding.ApplyBindings(border); + + Assert.Equal(5, Grid.GetColumn(border)); + } + + [Fact] + public void DynamicResource_From_Style_Can_Be_Assigned_To_Property() + { + var xaml = @" + + + + + + +"; + + var loader = new AvaloniaXamlLoader(); + var userControl = (UserControl)loader.Load(xaml); + var border = userControl.FindControl("border"); + + DelayedBinding.ApplyBindings(border); + + var brush = (SolidColorBrush)border.Background; + Assert.Equal(0xff506070, brush.Color.ToUint32()); + } + + [Fact] + public void DynamicResource_From_MergedDictionary_Can_Be_Assigned_To_Property() + { + var xaml = @" + + + + + + #ff506070 + + + + + + +"; + + var loader = new AvaloniaXamlLoader(); + var userControl = (UserControl)loader.Load(xaml); + var border = userControl.FindControl("border"); + + DelayedBinding.ApplyBindings(border); + + var brush = (SolidColorBrush)border.Background; + Assert.Equal(0xff506070, brush.Color.ToUint32()); + } + + [Fact] + public void DynamicResource_From_MergedDictionary_In_Style_Can_Be_Assigned_To_Property() + { + var xaml = @" + + + + + + +"; + + var loader = new AvaloniaXamlLoader(); + var userControl = (UserControl)loader.Load(xaml); + var border = userControl.FindControl("border"); + + DelayedBinding.ApplyBindings(border); + + var brush = (SolidColorBrush)border.Background; + Assert.Equal(0xff506070, brush.Color.ToUint32()); + } + + [Fact] + public void DynamicResource_From_Application_Can_Be_Assigned_To_Property_In_Window() + { + using (StyledWindow()) + { + Application.Current.Resources.Add("brush", new SolidColorBrush(0xff506070)); + + var xaml = @" + + +"; + + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var border = window.FindControl("border"); + + var brush = (SolidColorBrush)border.Background; + Assert.Equal(0xff506070, brush.Color.ToUint32()); + } + } + + [Fact] + public void DynamicResource_From_Application_Can_Be_Assigned_To_Property_In_UserControl() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + Application.Current.Resources.Add("brush", new SolidColorBrush(0xff506070)); + + var xaml = @" + + +"; + + var loader = new AvaloniaXamlLoader(); + var userControl = (UserControl)loader.Load(xaml); + var border = userControl.FindControl("border"); + + // We don't actually know where the global styles are until we attach the control + // to a window, as Window has StylingParent set to Application. + var window = new Window { Content = userControl }; + window.Show(); + + var brush = (SolidColorBrush)border.Background; + Assert.Equal(0xff506070, brush.Color.ToUint32()); + } + } + + [Fact] + public void DynamicResource_Can_Be_Assigned_To_Setter() + { + using (StyledWindow()) + { + var xaml = @" + + + #ff506070 + + + + +